feat(omp-rpc): introduced typed event listeners and todo phase management to RPC client

- Added typed event listeners and granular event handling for all RPC notification types.
- Added set_todos RPC command and todoPhases session state field for todo phase management.
- Added RpcClient initialization parameters (thinking, tools, no_session, rpc_defaults) for startup configuration.
- Added install_headless_ui() method and todo management methods (get_todos, set_todos, clear_todos).
- Added TodoItem and TodoPhase dataclasses with parser functions for structured todo representation.
- Added RPC mode behavior: disables session title generation by default and resets workflow settings to built-in defaults.
This commit is contained in:
can1357
2026-04-08 04:52:46 +02:00
parent 67e12b39fe
commit d7261bcbeb
11 changed files with 740 additions and 17 deletions
+104
View File
@@ -41,6 +41,7 @@ FAKE_SERVER = textwrap.dedent(
}
print(json.dumps({"type": "ready"}), flush=True)
todo_phases = []
for raw_line in sys.stdin:
raw_line = raw_line.strip()
@@ -91,6 +92,23 @@ FAKE_SERVER = textwrap.dedent(
"autoCompactionEnabled": True,
"messageCount": 0,
"queuedMessageCount": 0,
"todoPhases": todo_phases,
},
}
),
flush=True,
)
elif command_type == "set_todos":
todo_phases = command.get("phases", [])
print(
json.dumps(
{
"id": request_id,
"type": "response",
"command": "set_todos",
"success": True,
"data": {
"todoPhases": todo_phases,
},
}
),
@@ -146,6 +164,7 @@ FAKE_SERVER = textwrap.dedent(
continue
print(json.dumps({"type": "agent_start"}), flush=True)
print(json.dumps({"type": "turn_start"}), flush=True)
partial = assistant_message("")
print(
json.dumps(
@@ -164,6 +183,7 @@ FAKE_SERVER = textwrap.dedent(
)
assistant = assistant_message("pong")
print(json.dumps({"type": "message_end", "message": assistant}), flush=True)
print(json.dumps({"type": "turn_end", "message": assistant, "toolResults": []}), flush=True)
print(json.dumps({"type": "agent_end", "messages": [assistant]}), flush=True)
else:
print(
@@ -190,6 +210,46 @@ class RpcClientTests(unittest.TestCase):
request_timeout=2.0,
)
def test_command_builder_supports_common_rpc_options(self) -> None:
client = RpcClient(
executable="omp",
model="openrouter/anthropic/claude-sonnet-4.6",
cwd="/tmp/workspace",
thinking="high",
append_system_prompt="extra instructions",
provider_session_id="provider-session-1",
tools=("read", "edit", "write"),
no_session=True,
no_skills=True,
no_rules=True,
extra_args=("--foo", "bar"),
)
self.assertEqual(
client.command,
(
"omp",
"--mode",
"rpc",
"--model",
"openrouter/anthropic/claude-sonnet-4.6",
"--thinking",
"high",
"--append-system-prompt",
"extra instructions",
"--provider-session-id",
"provider-session-1",
"--tools",
"read,edit,write",
"--no-session",
"--no-skills",
"--no-rules",
"--no-title",
"--foo",
"bar",
),
)
def test_get_state_and_bash(self) -> None:
with self.make_client() as client:
state = client.get_state()
@@ -215,6 +275,50 @@ class RpcClientTests(unittest.TestCase):
client.send_ui_value(request.id, "approved")
client.wait_for_idle(timeout=2.0)
def test_install_headless_ui_cancels_interactive_requests(self) -> None:
seen_methods: list[str] = []
with self.make_client() as client:
client.install_headless_ui(on_request=lambda request: seen_methods.append(request.method))
client.prompt_and_wait("needs ui", timeout=2.0)
self.assertEqual(seen_methods, ["input"])
def test_ready_and_typed_event_listeners(self) -> None:
ready_types: list[str] = []
event_types: list[str] = []
notification_types: list[str] = []
client = self.make_client()
client.on_ready(lambda event: ready_types.append(event.type))
client.on_notification(lambda notification: notification_types.append(notification.type))
client.on_turn_start(lambda event: event_types.append(event.type))
client.on_message_update(lambda event: event_types.append(event.type))
client.on_agent_end(lambda event: event_types.append(event.type))
try:
client.start()
client.prompt_and_wait("say hello", timeout=2.0)
finally:
client.stop()
self.assertEqual(ready_types, ["ready"])
self.assertEqual(event_types, ["turn_start", "message_update", "agent_end"])
self.assertIn("ready", notification_types)
self.assertIn("turn_start", notification_types)
self.assertIn("agent_end", notification_types)
def test_set_todos_supports_flat_items(self) -> None:
with self.make_client() as client:
phases = client.set_todos(["Map tools", "Exercise edits"])
self.assertEqual(len(phases), 1)
self.assertEqual(phases[0].name, "Todos")
self.assertEqual(phases[0].tasks[0].content, "Map tools")
self.assertEqual(phases[0].tasks[1].status, "pending")
state = client.get_state()
self.assertEqual(state.todo_phases[0].tasks[1].content, "Exercise edits")
if __name__ == "__main__":
unittest.main()