EVENTS.md

EVENTS.md

Goal

Define a breaking canonical provider-runtime event model that can represent the full useful surface of:

  • Claude Agent SDK types (@anthropic-ai/claude-agent-sdk@0.2.62)
  • Codex App Server protocol (schema/json/*, including ServerNotification, ServerRequest, EventMsg, and v2 payload schemas)
  • Codex TypeScript SDK thread events/items (sdk/typescript/src/events.ts, items.ts)
  • Cursor ACP protocol (agent acp JSON-RPC 2.0 over stdio)

This is a mapping/spec document only (no downstream compatibility constraints).


1) Proposed Canonical Runtime Event Model (V2)

1.1 Envelope

ts
1type RuntimeEventRaw = {
2 source:
3 | "codex.app-server.notification"
4 | "codex.app-server.request"
5 | "codex.eventmsg"
6 | "claude.sdk.message"
7 | "claude.sdk.permission"
8 | "codex.sdk.thread-event"
9 | "cursor.acp.notification"
10 | "cursor.acp.request"
11 | "cursor.acp.response";
12 method?: string;
13 messageType?: string;
14 payload: unknown;
15};
16
17// Add these to `packages/contracts/src/baseSchemas.ts` for V2.
18export const RuntimeItemId = makeEntityId("RuntimeItemId");
19export type RuntimeItemId = typeof RuntimeItemId.Type;
20export const RuntimeRequestId = makeEntityId("RuntimeRequestId");
21export type RuntimeRequestId = typeof RuntimeRequestId.Type;
22export const RuntimeTaskId = makeEntityId("RuntimeTaskId");
23export type RuntimeTaskId = typeof RuntimeTaskId.Type;
24export const RuntimeSessionId = makeEntityId("RuntimeSessionId");
25export type RuntimeSessionId = typeof RuntimeSessionId.Type;
26
27type RuntimeEventBase<TType extends CanonicalRuntimeEventType, TPayload> = {
28 eventId: EventId;
29 provider: ProviderKind;
30 sessionId: RuntimeSessionId;
31 createdAt: IsoDateTime;
32
33 // Canonical T3 IDs (not provider-native IDs).
34 threadId?: ThreadId;
35 turnId?: TurnId;
36 itemId?: RuntimeItemId;
37 requestId?: RuntimeRequestId;
38
39 // Provider-native IDs preserved for correlation/debugging.
40 providerRefs?: {
41 providerSessionId?: ProviderSessionId;
42 providerThreadId?: ProviderThreadId;
43 providerTurnId?: ProviderTurnId;
44 providerItemId?: ProviderItemId;
45 providerRequestId?: TrimmedNonEmptyString;
46 };
47
48 type: TType;
49 payload: TPayload;
50 raw?: RuntimeEventRaw;
51};

1.2 Canonical Event Families

ts
1type CanonicalRuntimeEventType =
2 // lifecycle
3 | "session.started"
4 | "session.configured"
5 | "session.state.changed"
6 | "session.exited"
7 | "thread.started"
8 | "thread.state.changed"
9 | "thread.metadata.updated"
10 | "thread.token-usage.updated"
11 | "thread.realtime.started"
12 | "thread.realtime.item-added"
13 | "thread.realtime.audio.delta"
14 | "thread.realtime.error"
15 | "thread.realtime.closed"
16 | "turn.started"
17 | "turn.completed"
18 | "turn.aborted"
19 | "turn.plan.updated"
20 | "turn.diff.updated"
21
22 // item + content stream
23 | "item.started"
24 | "item.updated"
25 | "item.completed"
26 | "content.delta"
27
28 // approvals / input / tool-protocol requests
29 | "request.opened"
30 | "request.resolved"
31 | "user-input.requested"
32 | "user-input.resolved"
33
34 // provider/system telemetry
35 | "task.started"
36 | "task.progress"
37 | "task.completed"
38 | "hook.started"
39 | "hook.progress"
40 | "hook.completed"
41 | "tool.progress"
42 | "tool.summary"
43 | "auth.status"
44 | "account.updated"
45 | "account.rate-limits.updated"
46 | "mcp.status.updated"
47 | "mcp.oauth.completed"
48 | "model.rerouted"
49 | "config.warning"
50 | "deprecation.notice"
51 | "files.persisted"
52 | "runtime.warning"
53 | "runtime.error";

1.3 Canonical Item Type

ts
1type CanonicalItemType =
2 | "user_message"
3 | "assistant_message"
4 | "reasoning"
5 | "plan"
6 | "command_execution"
7 | "file_change"
8 | "mcp_tool_call"
9 | "dynamic_tool_call"
10 | "collab_agent_tool_call"
11 | "web_search"
12 | "image_view"
13 | "review_entered"
14 | "review_exited"
15 | "context_compaction"
16 | "error"
17 | "unknown";

1.4 Canonical Request Type

ts
1type CanonicalRequestType =
2 | "command_execution_approval"
3 | "file_change_approval"
4 | "apply_patch_approval"
5 | "exec_command_approval"
6 | "tool_user_input"
7 | "dynamic_tool_call"
8 | "auth_tokens_refresh"
9 | "unknown";

1.5 Discriminated Union (Type-Safe)

ts
1type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error";
2type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error";
3type TurnState = "completed" | "failed" | "interrupted" | "cancelled";
4
5type ContentStreamKind =
6 | "assistant_text"
7 | "reasoning_text"
8 | "reasoning_summary_text"
9 | "plan_text"
10 | "command_output"
11 | "file_change_output"
12 | "unknown";
13
14type ItemLifecyclePayload = {
15 itemType: CanonicalItemType;
16 status?: "inProgress" | "completed" | "failed" | "declined";
17 title?: string;
18 detail?: string;
19 data?: unknown;
20};
21
22type RuntimeEventPayloadByType = {
23 "session.started": { message?: string; resume?: unknown };
24 "session.configured": { config: Record<string, unknown> };
25 "session.state.changed": { state: SessionState; reason?: string; detail?: string };
26 "session.exited": { reason?: string; recoverable?: boolean; exitKind?: "graceful" | "error" };
27
28 "thread.started": { providerThreadId?: ProviderThreadId };
29 "thread.state.changed": { state: ThreadState; detail?: unknown };
30 "thread.metadata.updated": { name?: string; metadata?: Record<string, unknown> };
31 "thread.token-usage.updated": { usage: unknown };
32 "thread.realtime.started": { realtimeSessionId?: string };
33 "thread.realtime.item-added": { item: unknown };
34 "thread.realtime.audio.delta": { audio: unknown };
35 "thread.realtime.error": { message: string };
36 "thread.realtime.closed": { reason?: string };
37
38 "turn.started": { model?: string; effort?: string };
39 "turn.completed": {
40 state: TurnState;
41 stopReason?: string | null;
42 usage?: unknown;
43 modelUsage?: Record<string, unknown>;
44 totalCostUsd?: number;
45 errorMessage?: string;
46 };
47 "turn.aborted": { reason: string };
48 "turn.plan.updated": {
49 explanation?: string | null;
50 plan: Array<{ step: string; status: "pending" | "inProgress" | "completed" }>;
51 };
52 "turn.diff.updated": { unifiedDiff: string };
53
54 "item.started": ItemLifecyclePayload;
55 "item.updated": ItemLifecyclePayload;
56 "item.completed": ItemLifecyclePayload;
57 "content.delta": {
58 streamKind: ContentStreamKind;
59 delta: string;
60 contentIndex?: number;
61 summaryIndex?: number;
62 };
63
64 "request.opened": {
65 requestType: CanonicalRequestType;
66 detail?: string;
67 args?: unknown;
68 };
69 "request.resolved": {
70 requestType: CanonicalRequestType;
71 decision?: string;
72 resolution?: unknown;
73 };
74 "user-input.requested": { questions: unknown };
75 "user-input.resolved": { answers: unknown };
76
77 "task.started": { taskId: RuntimeTaskId; description?: string; taskType?: string };
78 "task.progress": {
79 taskId: RuntimeTaskId;
80 description: string;
81 usage?: unknown;
82 lastToolName?: string;
83 };
84 "task.completed": {
85 taskId: RuntimeTaskId;
86 status: "completed" | "failed" | "stopped";
87 summary?: string;
88 usage?: unknown;
89 };
90 "hook.started": { hookId: string; hookName: string; hookEvent: string };
91 "hook.progress": { hookId: string; output?: string; stdout?: string; stderr?: string };
92 "hook.completed": {
93 hookId: string;
94 outcome: "success" | "error" | "cancelled";
95 output?: string;
96 stdout?: string;
97 stderr?: string;
98 exitCode?: number;
99 };
100 "tool.progress": {
101 toolUseId?: string;
102 toolName?: string;
103 summary?: string;
104 elapsedSeconds?: number;
105 };
106 "tool.summary": { summary: string; precedingToolUseIds?: string[] };
107 "auth.status": { isAuthenticating?: boolean; output?: string[]; error?: string };
108 "account.updated": { account: unknown };
109 "account.rate-limits.updated": { rateLimits: unknown };
110 "mcp.status.updated": { status: unknown };
111 "mcp.oauth.completed": { success: boolean; name?: string; error?: string };
112 "model.rerouted": { fromModel: string; toModel: string; reason: string };
113 "config.warning": { summary: string; details?: string; path?: string; range?: unknown };
114 "deprecation.notice": { summary: string; details?: string };
115 "files.persisted": {
116 files: Array<{ filename: string; fileId: string }>;
117 failed?: Array<{ filename: string; error: string }>;
118 };
119 "runtime.warning": { message: string; detail?: unknown };
120 "runtime.error": {
121 message: string;
122 class?:
123 | "provider_error"
124 | "transport_error"
125 | "permission_error"
126 | "validation_error"
127 | "unknown";
128 detail?: unknown;
129 };
130};
131
132type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType;
133type MissingPayloadTypes = Exclude<
134 CanonicalRuntimeEventType,
135 CanonicalRuntimeEventTypeFromPayloadMap
136>;
137type ExtraPayloadTypes = Exclude<
138 CanonicalRuntimeEventTypeFromPayloadMap,
139 CanonicalRuntimeEventType
140>;
141// Both should be `never`.
142
143export type ProviderRuntimeEventV2 = {
144 [TType in CanonicalRuntimeEventTypeFromPayloadMap]: RuntimeEventBase<
145 TType,
146 RuntimeEventPayloadByType[TType]
147 >;
148}[CanonicalRuntimeEventTypeFromPayloadMap];

1.6 ID Semantics

  • eventId, sessionId, threadId, turnId, itemId, requestId are canonical T3 IDs (branded).
  • Provider IDs must go in providerRefs.* (and raw), never in canonical fields.
  • If a canonical id is not known at emit time, omit it and emit only providerRefs + raw.

2) Changes vs Current ProviderRuntimeEvent (packages/contracts/src/providerRuntime.ts)

2.1 Update Existing

CurrentAction
session.startedKeep, add structured payload (resume/fork/config snapshot if known)
session.exitedKeep, add reason, exitKind, recoverable
thread.startedKeep, add source metadata
turn.startedKeep, include optional model/provider state
turn.completedKeep, expand status + usage + stopReason + cost/modelUsage
message.deltaReplace semantics with generic content.delta (message delta becomes one stream kind)
runtime.errorKeep but include normalized error class + provider raw error info

2.2 Delete (replaced)

CurrentReplacement
message.completeditem.completed for assistant_message
tool.starteditem.started (command_execution, file_change, mcp_tool_call, dynamic_tool_call, etc.)
tool.completeditem.completed
approval.requestedrequest.opened
approval.resolvedrequest.resolved
checkpoint.capturedRemove from provider runtime canonical set (keep checkpoint as orchestration-level projection concern)

2.3 Create New

session.configured, session.state.changed, thread.state.changed, thread.metadata.updated, thread.token-usage.updated, turn.aborted, turn.plan.updated, turn.diff.updated, item.started, item.updated, item.completed, content.delta, request.opened, request.resolved, user-input.requested, user-input.resolved, task.*, hook.*, tool.progress, tool.summary, auth.status, account.*, mcp.*, model.rerouted, config.warning, deprecation.notice, files.persisted, realtime thread events.


3) Codex App Server -> Canonical Mapping

3.1 Server Notifications (ServerNotification.method)

Codex methodCanonical V2Notes
thread/startedthread.startedthread.id -> threadId
thread/status/changedthread.state.changedpreserve full status object (notLoaded/idle/systemError/active{flags})
thread/archived / thread/unarchived / thread/closedthread.state.changednormalize state=archived/unarchived/closed
thread/name/updatedthread.metadata.updatedthreadName
thread/tokenUsage/updatedthread.token-usage.updatedpreserve totals + breakdown
turn/startedturn.startedturn.id, initial turn item snapshot
turn/completedturn.completedmap TurnStatus: completed/interrupted/failed/inProgress
turn/diff/updatedturn.diff.updatedkeep unified diff payload
turn/plan/updatedturn.plan.updatedpreserve explanation, steps (pending/inProgress/completed)
item/starteditem.startedmap item type to canonical item type
item/completeditem.completedmap item type + terminal status
item/agentMessage/deltacontent.deltastreamKind=assistant_text
item/plan/deltacontent.deltastreamKind=plan_text
item/commandExecution/outputDeltacontent.deltastreamKind=command_output
item/fileChange/outputDeltacontent.deltastreamKind=file_change_output
item/reasoning/summaryTextDeltacontent.deltastreamKind=reasoning_summary_text, summaryIndex
item/reasoning/summaryPartAddeditem.updateditem reasoning summary segment boundary
item/reasoning/textDeltacontent.deltastreamKind=reasoning_text, contentIndex
item/mcpToolCall/progresstool.progressfor MCP calls
item/commandExecution/terminalInteractionitem.updatedinteraction input on command execution item
serverRequest/resolvedrequest.resolvedcorrelate by request id
model/reroutedmodel.reroutedpreserve fromModel/toModel/reason
thread/compactedthread.state.changedcompacted context marker
thread/realtime/startedthread.realtime.startedrealtime transport/session id
thread/realtime/itemAddedthread.realtime.item-addedraw realtime item
thread/realtime/outputAudio/deltathread.realtime.audio.deltapreserve sample metadata
thread/realtime/errorthread.realtime.errormessage
thread/realtime/closedthread.realtime.closedoptional reason
errorruntime.errorpreserve willRetry, codex error info
deprecationNoticedeprecation.noticesummary/details
configWarningconfig.warningsummary/details/path/range
account/updatedaccount.updatedauth mode updates
account/rateLimits/updatedaccount.rate-limits.updatedrate limits snapshot
mcpServer/oauthLogin/completedmcp.oauth.completedsuccess/error/name
windows/worldWritableWarningruntime.warningplatform warning
windowsSandbox/setupCompletedsession.state.changed + runtime.warningsuccess/failure diagnostic
app/list/updatedruntime.warning or future app-list.updatedoptional if UI wants app ecosystem surface
account/login/completedauth.statuslogin success/error

3.2 Server Requests (ServerRequest.method)

Codex request methodCanonical V2
item/commandExecution/requestApprovalrequest.opened (requestType=command_execution_approval)
item/fileChange/requestApprovalrequest.opened (requestType=file_change_approval)
applyPatchApprovalrequest.opened (requestType=apply_patch_approval)
execCommandApprovalrequest.opened (requestType=exec_command_approval)
item/tool/requestUserInputuser-input.requested + request.opened (requestType=tool_user_input)
item/tool/callrequest.opened (requestType=dynamic_tool_call)
account/chatgptAuthTokens/refreshrequest.opened (requestType=auth_tokens_refresh)

3.3 EventMsg cross-reference (EventMsg.type)

EventMsg is broader than server notification coverage. Important equivalents:

  • task_started / task_complete -> turn.started / turn.completed
  • agent_message_delta, agent_message_content_delta -> content.delta (assistant_text)
  • agent_reasoning_delta, reasoning_content_delta, reasoning_raw_content_delta -> content.delta (reasoning*)
  • exec_command_begin/output_delta/end -> item.started/content.delta/item.completed (command_execution)
  • patch_apply_begin/end -> item.updated/item.completed (file_change)
  • exec_approval_request, apply_patch_approval_request, request_user_input -> request.opened / user-input.requested
  • plan_update, plan_delta -> turn.plan.updated / content.delta(plan_text)
  • turn_diff, turn_aborted, thread_rolled_back, context_compacted, model_reroute -> dedicated turn.* / thread.* / model.rerouted
  • collab events (collab_*) -> item.* (collab_agent_tool_call) or task.* depending UI preference

4) Claude Agent SDK -> Canonical Mapping

4.1 SDKMessage Mapping (@anthropic-ai/claude-agent-sdk@0.2.62)

Claude SDK messageCanonical V2Notes
assistant (SDKAssistantMessage)item.updated or item.completed (assistant_message)preserve message, uuid, optional error
stream_event (SDKPartialAssistantMessage) content_block_delta[text_delta]content.delta (assistant_text)primary streamed assistant text
stream_event content_block_start[tool_use/server_tool_use/mcp_tool_use]item.startedmap tool name -> canonical item type (command_execution/file_change/mcp_tool_call/dynamic_tool_call)
stream_event content_block_stopitem.completedclose tool item started above
result subtype=successturn.completed (status=completed)include usage/modelUsage/cost/stop_reason
result subtype=error_*runtime.error + turn.completed(status=failed)include error list + subtype
system:initsession.configuredmodel/tools/permissionMode/skills/plugins/mcp status
system:statussession.state.changedcompacting status + permission mode
system:compact_boundarythread.state.changedcontext compaction boundary
system:hook_startedhook.startedhook telemetry
system:hook_progresshook.progresshook stdout/stderr/output
system:hook_responsehook.completedoutcome + exit_code
tool_progresstool.progresselapsed seconds, tool name, tool_use_id
tool_use_summarytool.summarysummary + related tool use ids
auth_statusauth.statusauthentication progress/errors
system:task_startedtask.startedbackground task/subagent activity
system:task_progresstask.progressincremental background task updates
system:task_notificationtask.completedstatus completed/failed/stopped
system:files_persistedfiles.persistedfile ids persisted to backend
user / user (isReplay)optional item.started/item.completed (user_message)keep if we want full transcript parity
rate_limit / prompt_suggestion (in union)account.rate-limits.updated or item.updated(plan_suggestion)preserve raw payload even if sparse typing

4.2 Claude Permission Callback Mapping (canUseTool)

Claude callback phaseCanonical V2
permission request (toolName, input, suggestions, blockedPath, decisionReason, toolUseID)request.opened (requestType=command_execution_approval or file_change_approval or unknown)
user decision allow/deny (+updatedPermissions)request.resolved
interrupt/deny behaviorturn.completed(status=interrupted) or runtime.error as appropriate

5) Cursor ACP -> Canonical Mapping

5.1 ACP session lifecycle (JSON-RPC methods)

Cursor ACP methodCanonical V2Notes
initialize (response)session.configuredpreserve protocolVersion, agentCapabilities, authMethods
authenticate (response)auth.statussuccess when empty result; errors map to runtime.error(class=permission_error)
session/new (response)session.started + thread.started + session.state.changed(ready)persist sessionId in providerRefs.providerSessionId; preserve modes in payload/raw
session/load (response)session.started + thread.started + session.state.changed(ready)resume path; note ACP requires sessionId, cwd, mcpServers in request
session/prompt (response)turn.completedmap stopReason (end_turn, cancelled, etc.)
session/cancel (error -32601)runtime.warningcurrently unsupported in observed ACP version; adapter should fall back to process interrupt

5.2 ACP notifications (session/update)

Cursor ACP update.sessionUpdateCanonical V2Notes
available_commands_updatesession.configuredpreserve availableCommands; optional secondary runtime.warning if list changes mid-turn
agent_thought_chunkcontent.delta (streamKind=reasoning_text)map content.text
agent_message_chunkcontent.delta (streamKind=assistant_text)map content.text
tool_callitem.startedmap toolCallId -> itemId; dedupe starts because ACP may emit multiple tool_call updates for the same id
tool_call_update (in_progress)item.updatedinclude status + partial metadata
tool_call_update (completed)item.completedinclude rawOutput summary when present (terminal stdout/stderr, search totals, etc.)

5.3 ACP server requests (permission)

Cursor ACP request methodCanonical V2Notes
session/request_permissionrequest.openedrequestType usually command_execution_approval for kind=execute; preserve options (allow/reject)
permission response sent by T3request.resolvedmap T3 decision -> ACP optionId (allow-once, allow-always, reject-once)

5.4 ACP runtime/error mapping notes

  1. Unknown session on session/prompt (-32603) -> runtime.error(class=provider_error) and terminate active turn as failed.
  2. Invalid params / method-not-found (-32602, -32601) -> runtime.error(class=validation_error) or runtime.warning depending recoverability.
  3. Concurrent session/prompt calls can cancel earlier prompt (stopReason=cancelled) -> emit turn.completed(state=cancelled) for the superseded turn.
  4. Keep all ACP envelopes in raw with source set to cursor.acp.notification or cursor.acp.request.

6) Codex SDK Thread Events -> Canonical Mapping

(For parity with direct Codex SDK integrations and to cross-reference app-server behavior.)

Codex SDK ThreadEventCanonical V2
thread.startedthread.started
turn.startedturn.started
turn.completedturn.completed
turn.failedruntime.error + turn.completed(status=failed)
item.starteditem.started
item.updateditem.updated
item.completeditem.completed
errorruntime.error

Codex SDK ThreadItem.type (agent_message, reasoning, command_execution, file_change, mcp_tool_call, web_search, todo_list, error) maps directly to canonical item types (assistant_message, reasoning, command_execution, file_change, mcp_tool_call, web_search, plan, error).


7) Cross-Provider Equivalence (Claude <-> Codex <-> Cursor ACP)

ConceptCodex sourceClaude sourceCursor ACP sourceCanonical
Turn startturn/startedfirst active response turn + sendTurn startsession/prompt dispatch lifecycleturn.started
Turn completionturn/completedresultsession/prompt response (stopReason)turn.completed
Assistant text streamingitem/agentMessage/deltastream_event content_block_delta[text_delta]session/update:agent_message_chunkcontent.delta(assistant_text)
Reasoning/thought streamingitem/reasoning/textDeltareasoning deltas in stream_eventsession/update:agent_thought_chunkcontent.delta(reasoning_text)
Tool call startitem/started with tool itemcontent_block_start tool_use/server_tool_use/mcp_tool_usesession/update:tool_callitem.started
Tool call enditem/completed with tool itemcontent_block_stopsession/update:tool_call_update(completed)item.completed
Approval requiredserver request .../requestApprovalcanUseTool callbacksession/request_permissionrequest.opened
Approval resolvedserverRequest/resolved + response decisioncallback resolution decisionJSON-RPC response to permission request idrequest.resolved
Runtime errorerror notification / failed turnresult error_* or assistant errorJSON-RPC error response (-3260x etc.)runtime.error
Session capabilities/configsession_configured/init responsessystem:initinitialize / available_commands_updatesession.configured

8) Implementation Guidance

  1. Emit one canonical event per source message minimum; emit additional derived events when needed (for example runtime.error + turn.completed).
  2. Always attach raw payload so provider-specific detail is never lost.
  3. Replace current Codex adapter heuristic mapping (tool.*, approval.*, message.*) with method/item-driven mapping from app-server schemas.
  4. Extend Claude adapter to emit non-chat telemetry (system:*, task_*, hook_*, tool_progress, auth_status, files_persisted) into canonical events.
  5. Keep ID correlation deterministic:
    • request lifecycle keyed by requestId / approval id / tool use id
    • item lifecycle keyed by itemId (synthesize stable ids for Claude when missing)
  6. Treat unknown provider events as runtime.warning + raw passthrough instead of dropping.

9) Summary of Breaking Schema Direction

  • Move from a small, chat-centric runtime union to an item/lifecycle/protocol-complete canonical runtime model.
  • Normalize Claude and Codex onto the same event families (turn, item, content, request, task, hook, state), keeping provider specifics in payload + raw.
  • This supports richer integration ports without provider-specific logic in downstream consumers.