Skip to content

feat(core): emit more turn items instead of legacy begin/end events#30283

Open
owenlin0 wants to merge 6 commits into
mainfrom
owen/emit_canonical_turn_item_lifecycle
Open

feat(core): emit more turn items instead of legacy begin/end events#30283
owenlin0 wants to merge 6 commits into
mainfrom
owen/emit_canonical_turn_item_lifecycle

Conversation

@owenlin0

@owenlin0 owenlin0 commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR makes canonical TurnItem lifecycle the live source of truth for these items:

  • command execution
  • dynamic tool calls
  • collab agent tool calls
  • sub-agent activity

These are new TurnItems added in #30282, which only previously had the legacy-style EventMsg::*Begin/End events. This PR switches core to emit these as ItemStarted / ItemCompleted + TurnItem instead, and app-server v2 handles those canonical item events directly.

Core still fans canonical item events back out into the old legacy EventMsg forms where we still need them for legacy rollout persistence, rollout trace, and raw core event consumers. App-server ignores that compatibility fanout so v2 clients do not get duplicate item notifications.

Rollout persistence behavior is unchanged in this PR.

Also, I moved all the legacy conversion code to a new module protocol/src/legacy_events.rs instead of being split across item and event schema modules.

Why

This is the second PR in the stack for making ThreadHistoryBuilder stateless, which relies on using TurnItem. First PR is at: #30282

Follow-up

The next PR #30188 actually writes TurnItem to rollout files when using thread.history_mode = "paginated".

@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch from 35c6ad7 to ee54284 Compare June 26, 2026 20:50
@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch 2 times, most recently from d581108 to e9d6a09 Compare June 26, 2026 21:18
@owenlin0 owenlin0 force-pushed the owen/add_projection_friendly_turn_items branch from 883f6f7 to 2d0b556 Compare June 26, 2026 21:22
@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch 4 times, most recently from acec956 to f691f27 Compare June 26, 2026 22:50
@owenlin0 owenlin0 marked this pull request as ready for review June 26, 2026 23:03
@owenlin0 owenlin0 requested a review from a team as a code owner June 26, 2026 23:03
owenlin0 added a commit that referenced this pull request Jun 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f691f279c3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/core/src/tools/events.rs
Comment thread codex-rs/core/src/tools/handlers/multi_agents.rs
@owenlin0 owenlin0 force-pushed the owen/add_projection_friendly_turn_items branch from 4304fa3 to d2d5882 Compare June 26, 2026 23:16
@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch from 9fd1ae2 to 9f2d003 Compare June 26, 2026 23:19
owenlin0 added a commit that referenced this pull request Jun 26, 2026
@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch from d3204c7 to 3041d21 Compare June 26, 2026 23:32
owenlin0 added a commit that referenced this pull request Jun 26, 2026
## Description

This PR adds canonical core `TurnItem` shapes for command execution,
dynamic tool calls, collab agent tool calls, and sub-agent activity, to
be stored in the rollout file soon.

It also teaches app-server protocol / `ThreadHistoryBuilder` how to
render those items, and adds the small legacy fanout helpers needed for
existing event-based consumers. No core producer or rollout persistence
behavior changes here, that will be done in a followup.

## Making ThreadHistoryBuilder stateless

This is the first PR in a stack to make `ThreadHistoryBuilder` stateless
enough that we can materialize app-server `ThreadItem`s from only a
given slice of `RolloutItem` history, without ever needing to replay the
whole thread from the beginning.

The persisted legacy `RolloutItem::EventMsg` records are mostly shaped
like live UI events, not like materialized `ThreadItem`s. They work if
we replay the full rollout in order, but they often do not contain
enough stable identity or complete item state to project an arbitrary
suffix on its own.

A few examples:

- `UserMessageEvent` and `AgentMessageEvent` have content, but
historically do not carry the persisted app-server item ID that should
become the SQLite primary key.
- `AgentReasoningEvent` and `AgentReasoningRawContentEvent` are
fragments. `ThreadHistoryBuilder` currently merges them into the last
reasoning item, which means a slice starting in the middle of reasoning
cannot know whether to append to an earlier item or create a new one.
- `WebSearchEndEvent`, `McpToolCallEndEvent`, collab end events, and
similar legacy events can often render a final-looking item, but they
usually rely on prior replay state to know which turn owns the item.
- Begin/end legacy events are partial views of one logical item. The
builder correlates them by `call_id` and mutates prior state to
synthesize the final `ThreadItem`.

That is the problem this direction fixes. A persisted canonical
lifecycle record looks much closer to the read model we actually want
later:

```rust
ItemCompletedEvent {
    turn_id,
    item: TurnItem { id, ...full snapshot... },
    completed_at_ms,
}
```

Once rollout has explicit `turn_id`, stable `item.id`, and a canonical
completed item snapshot, the future SQLite projector can reduce only the
new rollout suffix and upsert the affected `thread_items` rows. It no
longer needs to synthesize `item-N`, infer item ownership from the
active turn, or replay earlier events just to reconstruct the current
item snapshot.

## What changed

- Added core `TurnItem` variants and item structs for command execution,
dynamic tool calls, collab agent tool calls, and sub-agent activity.
- Added conversions from those canonical items back into the legacy
event shapes where current consumers still need them.
- Added app-server v2 `ThreadItem` conversion for the new core item
variants.
- Taught `ThreadHistoryBuilder` and rollout persistence metrics to
recognize the new item variants.

## Follow-up

The next PR #30283 switches the live
core producers for these item families onto canonical `ItemStarted` /
`ItemCompleted` events.
Base automatically changed from owen/add_projection_friendly_turn_items to main June 26, 2026 23:44
@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch from 3041d21 to 5d4da6f Compare June 26, 2026 23:47
@owenlin0 owenlin0 force-pushed the owen/emit_canonical_turn_item_lifecycle branch from 3a6d87b to 0c3b191 Compare June 27, 2026 00:16
@owenlin0 owenlin0 changed the title feat(core): emit canonical turn item lifecycle events feat(core): emit more turn items instead of legacy begin/end events Jun 27, 2026
@rpelevin

Copy link
Copy Markdown

I would make the acceptance boundary the canonical item lifecycle, not the event-name migration.

The important property is that every tool or runtime activity that becomes visible to the app-server has one durable item identity and one completion boundary. If core emits ItemStarted and ItemCompleted as the live source of truth, then command execution, dynamic tool calls, collab agent calls, and sub-agent activity should all converge through the same item lifecycle before the app-server renders or resumes dependent state.

The regression shape I would want is:

  1. emit a command execution start and completion and assert app-server v2 receives exactly one canonical item lifecycle pair;
  2. repeat for dynamic tools, collab agent tools, and sub-agent spawn, send input, wait, resume, and close flows;
  3. assert compatibility fanout still produces legacy events only for legacy rollout, trace, and raw event consumers;
  4. assert app-server v2 ignores compatibility fanout and never renders duplicate item notifications;
  5. assert completed item identity is stable across the begin, streamed updates, completion, and compatibility conversion path;
  6. assert an interrupted or failed tool path still lands in one terminal completed item rather than a dangling legacy end event.

That gives the stack a useful invariant: live clients consume canonical item lifecycle, while legacy event conversion is a projection for old consumers, not a parallel authority path. The migration is then safe because there is only one live item record that owns visibility, completion, and downstream resume semantics.

Boundary: architecture and regression-test feedback only; no claim about using this project, running this branch, validating implementation behavior, implementation correctness, performance measurements, merge readiness, security review, production readiness, partnership, customer interest, official alignment, OpenAI usage, Codex usage, conformance certification, or Neura usage.

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.

2 participants