feat(core): publish tool.result on the bus for the activity panel

This commit is contained in:
hh
2026-08-28 17:01:00 +02:00
parent bd2d2368e1
commit fd8cbacd7b
2 changed files with 74 additions and 0 deletions
+34
View File
@@ -29,7 +29,9 @@ from claude_agent_sdk import (
AssistantMessage, AssistantMessage,
ResultMessage, ResultMessage,
StreamEvent, StreamEvent,
ToolResultBlock,
ToolUseBlock, ToolUseBlock,
UserMessage,
fork_session_via_store, fork_session_via_store,
project_key_for_directory, project_key_for_directory,
) )
@@ -963,6 +965,20 @@ class Conversations:
name=block.name, name=block.name,
input=block.input, input=block.input,
) )
elif isinstance(message, UserMessage):
blocks = message.content if isinstance(message.content, list) else ()
for block in blocks:
if isinstance(block, ToolResultBlock):
self._bus.publish(
"tool.result",
conversation_id=conversation_id,
turn_id=turn_id,
origin=origin,
parent_tool_use_id=parent,
tool_use_id=block.tool_use_id,
is_error=bool(block.is_error),
content=_result_preview(block.content),
)
elif isinstance(message, ResultMessage) and parent is None: elif isinstance(message, ResultMessage) and parent is None:
self._bus.publish( self._bus.publish(
"result", "result",
@@ -1055,6 +1071,24 @@ def _iso(value: datetime | None) -> str | None:
return _aware(value).isoformat(timespec="seconds") if value is not None else None return _aware(value).isoformat(timespec="seconds") if value is not None else None
def _result_preview(
content: str | list[dict[str, Any]] | None, limit: int = 400
) -> str:
"""Short text of a tool result for the panel: the transcript keeps the whole."""
if content is None:
return ""
text = (
content
if isinstance(content, str)
else "\n".join(
str(part.get("text", ""))
for part in content
if isinstance(part, dict) and part.get("type") == "text"
)
)
return text if len(text) <= limit else text[:limit] + ""
def _usage_dict(capture: TurnCapture) -> dict[str, Any] | None: def _usage_dict(capture: TurnCapture) -> dict[str, Any] | None:
usage = capture.usage usage = capture.usage
if usage is None: if usage is None:
+40
View File
@@ -12,6 +12,9 @@ from claude_agent_sdk import (
ResultMessage, ResultMessage,
StreamEvent, StreamEvent,
TextBlock, TextBlock,
ToolResultBlock,
ToolUseBlock,
UserMessage,
project_key_for_directory, project_key_for_directory,
) )
@@ -655,3 +658,40 @@ def test_agent_kinds_follow_prompts(tmp_path: Path) -> None:
kinds=("master", "deep"), kinds=("master", "deep"),
prompts=Prompts(master=()), prompts=Prompts(master=()),
) )
async def test_observer_publishes_tool_results_for_the_panel(world: World) -> None:
seen: list[dict[str, Any]] = []
async def collect() -> None:
async for event in world.bus.stream(conversation_id="c1"):
seen.append(event)
task = asyncio.create_task(collect())
await asyncio.sleep(0)
observe = world.conversations._observer("c1", "t1", "user")
observe(
AssistantMessage(
content=[ToolUseBlock(id="toolu_1", name="Bash", input={"command": "ls"})],
model="m",
parent_tool_use_id="toolu_0",
)
)
observe(
UserMessage(
content=[
ToolResultBlock(tool_use_id="toolu_1", content="a\nb", is_error=False)
],
parent_tool_use_id="toolu_0",
)
)
observe(UserMessage(content="plain text, no tool result"))
await asyncio.sleep(0.05)
task.cancel()
assert [e["type"] for e in seen] == ["tool", "tool.result"]
tool, result = seen
assert tool["tool_use_id"] == "toolu_1" and tool["parent_tool_use_id"] == "toolu_0"
assert result["tool_use_id"] == "toolu_1"
assert result["parent_tool_use_id"] == "toolu_0"
assert result["turn_id"] == "t1" and result["is_error"] is False
assert result["content"] == "a\nb"