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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user