fix(runner): ignore empty injected payloads (#4337)
This commit is contained in:
+18
-3
@@ -257,12 +257,17 @@ class AgentRunner:
|
||||
return []
|
||||
injected_messages: list[dict[str, Any]] = []
|
||||
for item in items:
|
||||
if item is None:
|
||||
continue
|
||||
if isinstance(item, dict) and item.get("role") == "user" and "content" in item:
|
||||
if self._has_injection_content(item.get("content")):
|
||||
injected_messages.append(item)
|
||||
continue
|
||||
text = getattr(item, "content", str(item))
|
||||
if text.strip():
|
||||
injected_messages.append({"role": "user", "content": text})
|
||||
if isinstance(item, dict):
|
||||
continue
|
||||
content = getattr(item, "content") if hasattr(item, "content") else str(item)
|
||||
if self._has_injection_content(content):
|
||||
injected_messages.append({"role": "user", "content": content})
|
||||
if len(injected_messages) > _MAX_INJECTIONS_PER_TURN:
|
||||
dropped = len(injected_messages) - _MAX_INJECTIONS_PER_TURN
|
||||
logger.warning(
|
||||
@@ -272,6 +277,16 @@ class AgentRunner:
|
||||
injected_messages = injected_messages[:_MAX_INJECTIONS_PER_TURN]
|
||||
return injected_messages
|
||||
|
||||
@staticmethod
|
||||
def _has_injection_content(content: Any) -> bool:
|
||||
if content is None:
|
||||
return False
|
||||
if isinstance(content, str):
|
||||
return bool(content.strip())
|
||||
if isinstance(content, list):
|
||||
return bool(content)
|
||||
return True
|
||||
|
||||
async def run(self, spec: AgentRunSpec) -> AgentRunResult:
|
||||
hook = spec.hook or AgentHook()
|
||||
messages = list(spec.initial_messages)
|
||||
|
||||
@@ -152,6 +152,70 @@ async def test_drain_injections_skips_empty_content():
|
||||
assert result == [{"role": "user", "content": "valid"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_filters_empty_dict_payloads():
|
||||
"""Pre-normalized dict injections should obey the same empty-content guard."""
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
multimodal = [{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}]
|
||||
msgs = [
|
||||
{"role": "user", "content": ""},
|
||||
{"role": "user", "content": " "},
|
||||
{"role": "user", "content": None},
|
||||
{"role": "assistant", "content": "should not be re-injected as user"},
|
||||
None,
|
||||
{"role": "user", "content": "valid"},
|
||||
{"role": "user", "content": multimodal},
|
||||
]
|
||||
|
||||
async def cb():
|
||||
return msgs
|
||||
|
||||
spec = AgentRunSpec(
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
)
|
||||
result = await runner._drain_injections(spec)
|
||||
assert result == [
|
||||
{"role": "user", "content": "valid"},
|
||||
{"role": "user", "content": multimodal},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_skips_objects_with_none_content():
|
||||
"""Objects exposing content=None should be skipped rather than stringified."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
from nanobot.agent.runner import AgentRunner, AgentRunSpec
|
||||
|
||||
provider = MagicMock()
|
||||
runner = AgentRunner(provider)
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
async def cb():
|
||||
return [
|
||||
SimpleNamespace(content=None),
|
||||
SimpleNamespace(content=""),
|
||||
SimpleNamespace(content="valid"),
|
||||
]
|
||||
|
||||
spec = AgentRunSpec(
|
||||
initial_messages=[], tools=tools, model="m",
|
||||
max_iterations=1, max_tool_result_chars=1000,
|
||||
injection_callback=cb,
|
||||
)
|
||||
result = await runner._drain_injections(spec)
|
||||
assert result == [{"role": "user", "content": "valid"}]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_injections_handles_callback_exception():
|
||||
"""If the callback raises, return empty list (error is logged)."""
|
||||
@@ -1155,4 +1219,3 @@ async def test_injection_cycle_cap_on_error_path():
|
||||
assert result.had_injections is True
|
||||
# Should cap: _MAX_INJECTION_CYCLES drained rounds + 1 final round that breaks
|
||||
assert call_count["n"] == _MAX_INJECTION_CYCLES + 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user