feat(python): implemented host tool event normalization and error checks

- Added `_normalize_host_tool_event` to `RpcClient` to remap transport tool events to executed host tools.
- Updated worker tool-end handling to verify `is_error` is false before marking terminal actions complete.
- Added test coverage in `test_client.py` and `test_worker.py` for host tool event normalization and errored review submission behavior.
This commit is contained in:
can1357
2026-07-27 07:11:18 +02:00
parent 0388946e85
commit a9b7df8821
6 changed files with 199 additions and 12 deletions
+92 -4
View File
@@ -70,6 +70,8 @@ FAKE_SERVER = textwrap.dedent(
}
registered_host_tools = []
host_event_tool_call_id = "toolu_host_1"
host_event_tool_name = "echo_host"
def current_state():
return {
@@ -391,6 +393,8 @@ FAKE_SERVER = textwrap.dedent(
continue
if message == "needs host tool":
print(json.dumps({"type": "agent_start"}), flush=True)
host_event_tool_call_id = "toolu_host_1"
host_event_tool_name = "echo_host"
print(
json.dumps(
{
@@ -404,6 +408,34 @@ FAKE_SERVER = textwrap.dedent(
flush=True,
)
continue
if message == "needs xd host tool":
print(json.dumps({"type": "agent_start"}), flush=True)
host_event_tool_call_id = "toolu_write_1"
host_event_tool_name = "write"
print(
json.dumps(
{
"type": "tool_execution_start",
"toolCallId": "toolu_write_1",
"toolName": "write",
"args": {"path": "xd://echo_host", "content": '{"message": "hello"}'},
}
),
flush=True,
)
print(
json.dumps(
{
"type": "host_tool_call",
"id": "host-call-2",
"toolCallId": "toolu_write_1",
"toolName": "echo_host",
"arguments": {"message": "hello"},
}
),
flush=True,
)
continue
if message == "notifications":
print(json.dumps({"type": "extension_error", "extensionPath": "/tmp/ext.py", "event": "run", "error": "boom"}), flush=True)
print(json.dumps({"type": "unknown_future_event", "value": 1}), flush=True)
@@ -418,8 +450,8 @@ FAKE_SERVER = textwrap.dedent(
json.dumps(
{
"type": "tool_execution_update",
"toolCallId": "toolu_host_1",
"toolName": "echo_host",
"toolCallId": host_event_tool_call_id,
"toolName": host_event_tool_name,
"args": {"message": "hello"},
"partialResult": command["partialResult"],
}
@@ -431,8 +463,8 @@ FAKE_SERVER = textwrap.dedent(
json.dumps(
{
"type": "tool_execution_end",
"toolCallId": "toolu_host_1",
"toolName": "echo_host",
"toolCallId": host_event_tool_call_id,
"toolName": host_event_tool_name,
"result": command["result"],
"isError": command.get("isError", False),
}
@@ -908,6 +940,62 @@ class RpcClientTests(unittest.TestCase):
self.assertEqual(len(end_events), 1)
self.assertEqual(end_events[0].result["content"][0]["text"], "host:hello")
def test_xd_dispatched_custom_tool_events_carry_host_tool_name(self) -> None:
"""Events for an xd:// device dispatch are renamed to the executed host tool.
With `tools.xdev` on, omp invokes a custom tool through `write
xd://<name>` and the wire events carry the transport tool (`write`).
Consumers must observe the host-tool name on update/end events
regardless of transport — roboomp's terminal-action detection
triple-posted PR reviews when end events only said `write`
(oh-my-pi#6696). `tool_execution_start` precedes the `host_tool_call`
frame on the wire and keeps the transport name.
"""
def echo_host(args: dict[str, str], context) -> str:
context.send_update(f"working:{args['message']}")
return f"host:{args['message']}"
with self.make_client(
custom_tools=(
host_tool(
name="echo_host",
description="Echo from the Python host process",
parameters={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
"additionalProperties": False,
},
execute=echo_host,
),
)
) as client:
turn = client.prompt_and_wait("needs xd host tool", timeout=2.0)
start_names = [
event.tool_name
for event in turn.events
if getattr(event, "type", None) == "tool_execution_start"
]
update_events = [
event
for event in turn.events
if getattr(event, "type", None) == "tool_execution_update"
]
end_events = [
event
for event in turn.events
if getattr(event, "type", None) == "tool_execution_end"
]
self.assertEqual(start_names, ["write"])
self.assertEqual(
[event.tool_name for event in update_events], ["echo_host"]
)
self.assertEqual([event.tool_name for event in end_events], ["echo_host"])
self.assertEqual(end_events[0].tool_call_id, "toolu_write_1")
self.assertEqual(end_events[0].result["content"][0]["text"], "host:hello")
def test_extension_ui_round_trip(self) -> None:
with self.make_client() as client:
client.prompt("needs ui")