fix(runner): ignore empty injected payloads (#4337)

This commit is contained in:
Stellar鱼
2026-06-15 15:13:03 +08:00
committed by GitHub
parent fbbb09e9a5
commit a54e56c69e
2 changed files with 84 additions and 6 deletions
+20 -5
View File
@@ -257,12 +257,17 @@ class AgentRunner:
return []
injected_messages: list[dict[str, Any]] = []
for item in items:
if isinstance(item, dict) and item.get("role") == "user" and "content" in item:
injected_messages.append(item)
if item is None:
continue
text = getattr(item, "content", str(item))
if text.strip():
injected_messages.append({"role": "user", "content": text})
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
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)
+64 -1
View File
@@ -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