Skip to content

feat(codex): republish todo_list revisions as they are ticked off - #473

Open
declan-scale wants to merge 3 commits into
nextfrom
declan-scale/codex-todo-list-updates
Open

feat(codex): republish todo_list revisions as they are ticked off#473
declan-scale wants to merge 3 commits into
nextfrom
declan-scale/codex-todo-list-updates

Conversation

@declan-scale

@declan-scale declan-scale commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What

Codex does not reopen its plan. One todo_list item is opened at the start of a turn and revised in place through item.updated, with item.completed only arriving at the very end. The tap dropped those updates, so a consumer saw the plan exactly twice:

  • as first written, nothing done
  • at end of turn, everything done

Real trace from the golden agent (item_1), request vs response of the same call:

arguments: [{"text": "Probe safe local utility tools", "completed": false}, ... x4]
result:    [{"text": "Probe safe local utility tools", "completed": true},  ... x4]

Nothing in between, though the agent ticked them off over ~7 minutes.

This forwards each revision as a ToolResponseContent under the item's existing tool_call_id. Consumers that key responses by call id (the GenAI Ops Hub chat does) now render the checklist filling in as the turn runs, and the last response received is still the final state.

Scope

_PROGRESSIVE_TOOL_ITEMS is {"todo_list"} only. The other tool item types carry no intermediate state worth acting on, and republishing command_execution output on every chunk would be pure noise. Updates for an item that was never opened are ignored — there is no request for them to answer.

tool_call_count still counts the call once, not once per revision.

Testing

  • pytest tests/lib/adk/test_codex_sync.py — 55 pass, 4 new (TestTodoListUpdates: republish per revision, call counted once, orphan update ignored, other tool items unaffected)
  • pytest tests/lib/core/harness/test_harness_codex_sync.py — 9 pass
  • ./scripts/lint — ruff + pyright clean

Paired change

The GenAI Ops Hub side of this is scaleapi/scaleapi#153721's follow-up, which maps every codex tool name onto a real chat block. It already takes the newest response per call id, so it picks these up with no further change.

🤖 Generated with Claude Code

Greptile Summary

This PR fixes a gap in the Codex event-stream tap where todo_list plan revisions were invisible to consumers: previously only the initial state and the final completed state were forwarded, with nothing in between even though Codex ticks items off over the course of a turn. Now each item.updated for a todo_list item is republished as a ToolResponseContent under the same tool_call_id, so consumers (e.g. the GenAI Ops Hub chat) can render the checklist filling in progressively.

  • _codex_sync.py: Adds _PROGRESSIVE_TOOL_ITEMS = frozenset({"todo_list"}) and a new branch in _handle_item that, for item.updated events on progressive items, emits a StreamTaskMessageFull(ToolResponseContent) under the existing tool_call_id — guarded by item_id in self._tool_open so orphan updates are silently dropped. tool_call_count is still incremented only once on item.started.
  • obs_ids.py: Updates the ddtrace import from the deprecated ddtrace.tracer to ddtrace.trace.tracer (ddtrace v3 API path); the existing except ImportError guard ensures graceful degradation on older installs.
  • performing_deep_research.py (example): Pins mcp<2 via uvx --with for all MCP server invocations to avoid the McpErrorMCPError rename introduced in mcp 2.0.0.

Confidence Score: 5/5

Safe to merge. The new progressive-update path is narrowly scoped to todo_list items, guarded by the _tool_open membership check, and covered by four targeted tests.

The change is purely additive — a new elif branch that only fires for todo_list item.updated events. The existing item.started / item.completed paths are untouched. The orphan-update guard and unchanged tool_call_count semantics both hold up under the new tests.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/agentex/lib/adk/_modules/_codex_sync.py Adds progressive republishing of todo_list item.updated events as ToolResponseContent under the same tool_call_id; logic is sound, guarded by the _tool_open membership check, and tool_call_count is correctly incremented only once on item.started.
tests/lib/adk/test_codex_sync.py Four new tests in TestTodoListUpdates covering: progressive republish, single call count, orphan update ignored, and other tool types unaffected.
src/agentex/lib/core/tracing/obs_ids.py Updates ddtrace import to ddtrace.trace.tracer for ddtrace v3 compatibility; gracefully falls back via the existing ImportError catch.
examples/tutorials/10_async/10_temporal/020_state_machine/project/workflows/deep_research/performing_deep_research.py Pins mcp<2 via uvx --with to avoid mcp 2.x breaking import. Well-commented with a drop condition.

Sequence Diagram

sequenceDiagram
    participant Codex as Codex CLI
    participant Tap as _CodexStreamProcessor
    participant Consumer as Consumer

    Codex->>Tap: "item.started {id: item_1, type: todo_list}"
    Tap->>Consumer: "StreamTaskMessageStart(ToolRequestContent, tool_call_id=item_1)"
    Tap->>Consumer: StreamTaskMessageDone

    Note over Codex,Consumer: Agent ticks items off over many minutes

    Codex->>Tap: "item.updated {id: item_1, items: [true, false]}"
    Tap->>Consumer: "StreamTaskMessageFull(ToolResponseContent, tool_call_id=item_1)"

    Codex->>Tap: "item.updated {id: item_1, items: [true, true]}"
    Tap->>Consumer: "StreamTaskMessageFull(ToolResponseContent, tool_call_id=item_1)"

    Codex->>Tap: "item.completed {id: item_1, items: [true, true]}"
    Tap->>Consumer: "StreamTaskMessageFull(ToolResponseContent, tool_call_id=item_1)"
Loading

Reviews (4): Last reviewed commit: "fix(tutorials): pin mcp<2 for uvx-spawne..." | Re-trigger Greptile

Codex does not reopen its plan: one `todo_list` item is opened at the
start of a turn and revised in place through `item.updated`, with
`item.completed` only arriving at the very end. The tap dropped those
updates, so a consumer saw the plan exactly twice — as first written
(nothing done) and then, at end of turn, fully finished.

Forward each revision as a `ToolResponseContent` under the item's
existing `tool_call_id`. Consumers that key responses by call id (the
GenAI Ops Hub chat does) now render the checklist filling in as the turn
runs, and the last response received is still the final state.

Scoped to `todo_list` via `_PROGRESSIVE_TOOL_ITEMS`: the other tool item
types carry no intermediate state worth acting on, and republishing
`command_execution` output on every chunk would be pure noise. Updates
for an item that was never opened are ignored — there is no request for
them to answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@declan-scale
declan-scale force-pushed the declan-scale/codex-todo-list-updates branch from 078bfad to da3b575 Compare July 28, 2026 14:08
@declan-scale
declan-scale changed the base branch from main to next July 28, 2026 14:08
declan-scale and others added 2 commits July 28, 2026 09:33
CI lint has been red on `next` since c67e177 added this file:

- ruff I001: typing import block not sorted per the repo's isort config
- pyright reportPrivateImportUsage: `tracer` is not re-exported from
  `ddtrace`, so it must be imported from `ddtrace.trace`

The `except ImportError` guard still covers ddtrace being absent, and
`ddtrace.trace.tracer` exists in the pinned ddtrace 4.10.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…chine

mcp 2.0.0 renamed `McpError` to `MCPError` and split types out into a
separate `mcp_types` package. `mcp-server-time` and `mcp-server-fetch`
still import the 1.x name, so both now die at import:

    ImportError: cannot import name 'McpError' from 'mcp.shared.exceptions'
    ... -> mcp.shared.exceptions.McpError: Connection closed

uvx builds each server its own isolated environment and resolves `mcp`
unpinned there, so the `mcp==1.23.0` this project installs does not apply.
With every server dead the deep research agent emits "Starting deep
research..." and then makes zero tool calls, so test_agent.py's
`uses_tool_requests` assertion fails (and the streaming test times out
waiting for the same content).

This is why the job passed at 03:48 today and failed at 14:08 with no
code change in between: mcp 2.0.0 landed in the gap.

Constraining uvx to mcp<2 restores all three servers. Verified by
completing an MCP initialize + tools/list handshake against the exact
argv this file now produces:

    mcp-server-time          tools=['get_current_time', 'convert_time']
    openai-websearch-mcp     tools=['openai_web_search']
    mcp-server-fetch         tools=['fetch']

The 30s test timeout is left alone; both tests passed in 21.1s total
when the servers worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant