feat(session): add TodoItem + todo/write event vocabulary

Add the TodoItem type and a todo/write SessionEventMap variant carrying the
whole todo list as a snapshot (last-write-wins on replay). It is NOT a
SurfaceEventType: it produces no LLM message and never reaches
deriveMessages(), so it carries no surfaceOp and stays off the surface — it is
durable, replayable UI state that rides the existing session/event emit.

Tests cover the snapshot-clone-on-append contract, last-write-wins, the
not-on-surface guarantee, and a seeded replay round-trip. Docs: session.md
gains the TodoItem type-equiv block + the event member; core.md's variant count
goes to twelve; the type-equiv manifest gains TodoItem.
This commit is contained in:
Tianyi Cui
2026-06-29 01:39:25 +08:00
parent 185c09d6a5
commit 22a89847ac
5 changed files with 126 additions and 2 deletions

View File

@@ -149,6 +149,24 @@ export interface TurnEndReasonMap {
export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap]
/**
* One entry in an agent's todo list — the unit of the `todo_write` tool's
* whole-list state (the `todo/write` {@link SessionEventMap} event).
*
* Deliberately minimal: a human-readable `content` line and a three-state
* `status`. No id, priority, or `activeForm` — the list is replaced wholesale
* on every write (last-write-wins), so entries need no stable identity, and the
* status triple is exactly the ACP `PlanEntryStatus` (so the ACP bridge maps a
* todo list to a `plan` update 1:1, synthesizing the priority ACP additionally
* requires).
*/
export interface TodoItem {
/** What this task is — a short imperative line shown in the UI. */
content: string
/** Lifecycle state. `in_progress` marks the single task being worked now. */
status: 'pending' | 'in_progress' | 'completed'
}
/**
* The session event vocabulary — the append-only source of truth for an
* agent's whole interaction history. The LLM message history is *derived*
@@ -194,6 +212,23 @@ export interface SessionEventMap {
'tool/result': { turn: number; step: number; callId: CallId; content: ContentBlock[]; isError: boolean; error?: { name: string; code: string } }
/** Steering content injected between steps of a running turn. */
'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }
/**
* The agent's whole todo list, replaced wholesale on each write
* (last-write-wins on replay — the current list is the last `todo/write`).
* Written by the `todo_write` tool via
* `agent.session.append('todo/write', { todos })`.
*
* NOT a {@link SurfaceEventType}: it produces no LLM message and never reaches
* `deriveMessages()` — it is durable, replayable UI state. The full snapshot
* travels each time, so a resume re-derives the current list from the last
* event with no fold. UIs render off `session/event`: the stdio UI prints the
* list; the ACP bridge maps it to a `plan` sessionUpdate. This is a
* `SessionEventMap` member (it rides the existing `session/event` emit), not a
* first-class `interface Events` notification, so the cordis catalog gains no
* row for it.
* @mode emit
*/
'todo/write': { todos: TodoItem[] }
}
export type SessionEventType = keyof SessionEventMap