From e21f7c18ea37e0a49e6c8d2a4b5ccfa242b13dbb Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 27 Jul 2026 05:22:10 +0200 Subject: [PATCH] feat(python/omp-rpc): added blocker field and blocked status to todo items - Add blocked todo status and optional blocker attribute to todo items. - Update RPC client and worker serialization logic to support todo blockers. - Add test coverage for parsing session state with blocked todos. --- python/omp-rpc/src/omp_rpc/client.py | 3 +++ python/omp-rpc/src/omp_rpc/protocol.py | 9 ++++++-- python/omp-rpc/tests/test_protocol.py | 31 ++++++++++++++++++++++++++ python/robomp/src/worker.py | 1 + 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/python/omp-rpc/src/omp_rpc/client.py b/python/omp-rpc/src/omp_rpc/client.py index a0c1cf729..beed7ad95 100644 --- a/python/omp-rpc/src/omp_rpc/client.py +++ b/python/omp-rpc/src/omp_rpc/client.py @@ -1673,6 +1673,7 @@ class RpcClient: "status": cast(JsonValue, seed.status), "notes": seed.notes, "details": seed.details, + "blocker": seed.blocker, } content = seed.get("content") @@ -1683,6 +1684,7 @@ class RpcClient: raw_status = seed.get("status") raw_notes = seed.get("notes") raw_details = seed.get("details") + raw_blocker = seed.get("blocker") if isinstance(raw_status, str): if raw_status not in _TODO_STATUS_VALUES: raise RpcError(f"Unsupported todo status: {raw_status}") @@ -1697,6 +1699,7 @@ class RpcClient: "status": cast(JsonValue, status), "notes": raw_notes if isinstance(raw_notes, str) else None, "details": raw_details if isinstance(raw_details, str) else None, + "blocker": raw_blocker if isinstance(raw_blocker, str) else None, } def is_phase_seed(seed: TodoSeed | TodoPhaseSeed) -> bool: diff --git a/python/omp-rpc/src/omp_rpc/protocol.py b/python/omp-rpc/src/omp_rpc/protocol.py index f14aea8f0..f903964a7 100644 --- a/python/omp-rpc/src/omp_rpc/protocol.py +++ b/python/omp-rpc/src/omp_rpc/protocol.py @@ -21,7 +21,9 @@ InterruptMode: TypeAlias = Literal["immediate", "wait"] StopReason: TypeAlias = Literal["stop", "length", "toolUse", "error", "aborted"] NotifyType: TypeAlias = Literal["info", "warning", "error"] WidgetPlacement: TypeAlias = Literal["aboveEditor", "belowEditor"] -TodoStatus: TypeAlias = Literal["pending", "in_progress", "completed", "abandoned"] +TodoStatus: TypeAlias = Literal[ + "pending", "in_progress", "completed", "abandoned", "blocked" +] ExtensionUiMethod: TypeAlias = Literal[ "select", "confirm", @@ -65,7 +67,7 @@ _WIDGET_PLACEMENT_VALUES: Final[frozenset[str]] = frozenset( {"aboveEditor", "belowEditor"} ) _TODO_STATUS_VALUES: Final[frozenset[str]] = frozenset( - {"pending", "in_progress", "completed", "abandoned"} + {"pending", "in_progress", "completed", "abandoned", "blocked"} ) _EXTENSION_UI_METHOD_VALUES: Final[frozenset[str]] = frozenset( { @@ -745,6 +747,8 @@ class TodoItem: status: TodoStatus notes: str | None = None details: str | None = None + # What a `blocked` task is waiting on; None for all other statuses. + blocker: str | None = None @dataclass(slots=True, frozen=True) @@ -1260,6 +1264,7 @@ def parse_todo_item(payload: JsonObject) -> TodoItem: ), notes=_optional_str(payload, "notes"), details=_optional_str(payload, "details"), + blocker=_optional_str(payload, "blocker"), ) diff --git a/python/omp-rpc/tests/test_protocol.py b/python/omp-rpc/tests/test_protocol.py index 58b2f91cd..c9f7b2697 100644 --- a/python/omp-rpc/tests/test_protocol.py +++ b/python/omp-rpc/tests/test_protocol.py @@ -176,6 +176,37 @@ class ProtocolParsingTests(unittest.TestCase): self.assertEqual(notification.todos[0].content, "Map tools") self.assertEqual(notification.todos[0].status, "pending") + def test_parse_session_state_accepts_blocked_todo(self) -> None: + # Regression: the TS agent added a `blocked` todo status (with a + # `blocker` note); resuming a session whose todos were blocked must + # not fail state parsing. + state = parse_session_state( + { + "sessionId": "session-123", + "steeringMode": "one-at-a-time", + "followUpMode": "one-at-a-time", + "interruptMode": "immediate", + "todoPhases": [ + { + "id": "phase-1", + "name": "Fix", + "tasks": [ + { + "id": "task-1", + "content": "Open PR", + "status": "blocked", + "blocker": "waiting on maintainer go-ahead", + } + ], + } + ], + } + ) + + task = state.todo_phases[0].tasks[0] + self.assertEqual(task.status, "blocked") + self.assertEqual(task.blocker, "waiting on maintainer go-ahead") + def test_assistant_text_excludes_thinking_by_default(self) -> None: message = { "role": "assistant", diff --git a/python/robomp/src/worker.py b/python/robomp/src/worker.py index ca14349eb..dddc67abd 100644 --- a/python/robomp/src/worker.py +++ b/python/robomp/src/worker.py @@ -645,6 +645,7 @@ def _run_rpc_blocking( "status": t.status, "notes": t.notes, "details": t.details, + "blocker": t.blocker, } for t in p.tasks ],