fix(delegate): handle content-block tool results
This commit is contained in:
parent
16beab421f
commit
f83918c31d
2 changed files with 62 additions and 2 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue