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:
@@ -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()
|
||||
|
||||
@@ -6,6 +6,7 @@ from omp_rpc import (
|
||||
AgentEndEvent,
|
||||
ExtensionUiRequest,
|
||||
SessionState,
|
||||
TodoReminderEvent,
|
||||
assistant_text,
|
||||
parse_notification,
|
||||
parse_session_state,
|
||||
@@ -50,6 +51,20 @@ class ProtocolParsingTests(unittest.TestCase):
|
||||
"autoCompactionEnabled": True,
|
||||
"messageCount": 4,
|
||||
"queuedMessageCount": 1,
|
||||
"todoPhases": [
|
||||
{
|
||||
"id": "phase-1",
|
||||
"name": "Todos",
|
||||
"tasks": [
|
||||
{
|
||||
"id": "task-1",
|
||||
"content": "Map tools",
|
||||
"status": "in_progress",
|
||||
"details": "Inspect read and edit first.",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"systemPrompt": "You are useful.",
|
||||
"dumpTools": [
|
||||
{
|
||||
@@ -65,6 +80,7 @@ class ProtocolParsingTests(unittest.TestCase):
|
||||
self.assertEqual(state.session_id, "session-123")
|
||||
self.assertEqual(state.follow_up_mode, "all")
|
||||
self.assertEqual(state.model.id if state.model else None, "claude-sonnet-4-5")
|
||||
self.assertEqual(state.todo_phases[0].tasks[0].status, "in_progress")
|
||||
self.assertEqual(state.dump_tools[0].name, "read")
|
||||
|
||||
def test_parse_agent_end_notification(self) -> None:
|
||||
@@ -117,6 +133,29 @@ class ProtocolParsingTests(unittest.TestCase):
|
||||
self.assertIsInstance(notification, ExtensionUiRequest)
|
||||
self.assertEqual(notification.method, "confirm")
|
||||
self.assertEqual(notification.message, "Continue?")
|
||||
self.assertTrue(notification.is_interactive())
|
||||
self.assertTrue(notification.requires_response())
|
||||
self.assertFalse(notification.is_passive())
|
||||
|
||||
def test_parse_todo_reminder_notification(self) -> None:
|
||||
notification = parse_notification(
|
||||
{
|
||||
"type": "todo_reminder",
|
||||
"attempt": 1,
|
||||
"maxAttempts": 3,
|
||||
"todos": [
|
||||
{
|
||||
"id": "task-1",
|
||||
"content": "Map tools",
|
||||
"status": "pending",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertIsInstance(notification, TodoReminderEvent)
|
||||
self.assertEqual(notification.todos[0].content, "Map tools")
|
||||
self.assertEqual(notification.todos[0].status, "pending")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user