fix(runner): ignore empty injected payloads (#4337)
This commit is contained in:
+20
-5
@@ -257,12 +257,17 @@ class AgentRunner:
|
|||||||
return []
|
return []
|
||||||
injected_messages: list[dict[str, Any]] = []
|
injected_messages: list[dict[str, Any]] = []
|
||||||
for item in items:
|
for item in items:
|
||||||
if isinstance(item, dict) and item.get("role") == "user" and "content" in item:
|
if item is None:
|
||||||
injected_messages.append(item)
|
|
||||||
continue
|
continue
|
||||||
text = getattr(item, "content", str(item))
|
if isinstance(item, dict) and item.get("role") == "user" and "content" in item:
|
||||||
if text.strip():
|
if self._has_injection_content(item.get("content")):
|
||||||
injected_messages.append({"role": "user", "content": text})
|
injected_messages.append(item)
|
||||||
|
continue
|
||||||
|
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:
|
if len(injected_messages) > _MAX_INJECTIONS_PER_TURN:
|
||||||
dropped = len(injected_messages) - _MAX_INJECTIONS_PER_TURN
|
dropped = len(injected_messages) - _MAX_INJECTIONS_PER_TURN
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -272,6 +277,16 @@ class AgentRunner:
|
|||||||
injected_messages = injected_messages[:_MAX_INJECTIONS_PER_TURN]
|
injected_messages = injected_messages[:_MAX_INJECTIONS_PER_TURN]
|
||||||
return injected_messages
|
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:
|
async def run(self, spec: AgentRunSpec) -> AgentRunResult:
|
||||||
hook = spec.hook or AgentHook()
|
hook = spec.hook or AgentHook()
|
||||||
messages = list(spec.initial_messages)
|
messages = list(spec.initial_messages)
|
||||||
|
|||||||
@@ -152,6 +152,70 @@ async def test_drain_injections_skips_empty_content():
|
|||||||
assert result == [{"role": "user", "content": "valid"}]
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_drain_injections_handles_callback_exception():
|
async def test_drain_injections_handles_callback_exception():
|
||||||
"""If the callback raises, return empty list (error is logged)."""
|
"""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
|
assert result.had_injections is True
|
||||||
# Should cap: _MAX_INJECTION_CYCLES drained rounds + 1 final round that breaks
|
# Should cap: _MAX_INJECTION_CYCLES drained rounds + 1 final round that breaks
|
||||||
assert call_count["n"] == _MAX_INJECTION_CYCLES + 1
|
assert call_count["n"] == _MAX_INJECTION_CYCLES + 1
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user