fix(delegate): handle content-block tool results

This commit is contained in:
Alexander Lehmann 2026-06-01 20:05:52 +02:00 committed by Teknium
parent 16beab421f
commit f83918c31d
2 changed files with 62 additions and 2 deletions

View file

@ -554,6 +554,37 @@ class TestDelegateObservability(unittest.TestCase):
self.assertIn("result_bytes", entry["tool_trace"][0])
self.assertEqual(entry["tool_trace"][0]["status"], "ok")
def test_tool_trace_handles_list_content_blocks(self):
"""Tool-result content blocks should not crash observability metadata."""
parent = _make_mock_parent(depth=0)
with patch("run_agent.AIAgent") as MockAgent:
mock_child = MagicMock()
mock_child.model = "claude-sonnet-4-6"
mock_child.session_prompt_tokens = 0
mock_child.session_completion_tokens = 0
mock_child.run_conversation.return_value = {
"final_response": "done",
"completed": True,
"interrupted": False,
"api_calls": 1,
"messages": [
{"role": "assistant", "tool_calls": [
{"id": "tc_1", "function": {"name": "image_generate", "arguments": '{"prompt": "x"}'}}
]},
{"role": "tool", "tool_call_id": "tc_1", "content": [
{"type": "text", "text": '{"success": true}'},
]},
],
}
MockAgent.return_value = mock_child
result = json.loads(delegate_task(goal="Test list content", parent_agent=parent))
trace = result["results"][0]["tool_trace"]
self.assertEqual(trace[0]["tool"], "image_generate")
self.assertEqual(trace[0]["status"], "ok")
self.assertGreater(trace[0]["result_bytes"], 0)
def test_tool_trace_detects_error(self):
"""Tool results containing 'error' should be marked as error status."""
parent = _make_mock_parent(depth=0)

View file

@ -276,7 +276,35 @@ def _extract_output_tail(
return tail
def _looks_like_error_output(content: str) -> bool:
def _stringify_tool_content(content: Any) -> str:
"""Return a stable text representation for tool-result content.
Most providers store tool results as strings, but some OpenAI-compatible
paths can return content-block lists. Delegate observability must never
crash while summarising a child run just because the transport used blocks.
"""
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict):
text = item.get("text")
if isinstance(text, str):
parts.append(text)
else:
parts.append(json.dumps(item, ensure_ascii=False, default=str))
else:
parts.append(str(item))
return "\n".join(parts)
if isinstance(content, dict):
return json.dumps(content, ensure_ascii=False, default=str)
return str(content)
def _looks_like_error_output(content: Any) -> bool:
"""Conservative stderr/error detector for tool-result previews.
The old heuristic flagged any preview containing the substring "error",
@ -286,6 +314,7 @@ def _looks_like_error_output(content: str) -> bool:
- structured JSON with ``status`` of error/failed
- first line starts with a classic error marker
"""
content = _stringify_tool_content(content)
if not content:
return False
@ -1672,7 +1701,7 @@ def _run_single_child(
if tc_id:
trace_by_id[tc_id] = entry_t
elif msg.get("role") == "tool":
content = msg.get("content", "")
content = _stringify_tool_content(msg.get("content", ""))
is_error = _looks_like_error_output(content)
result_meta = {
"result_bytes": len(content),