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/*, includingServerNotification,ServerRequest,EventMsg, and v2 payload schemas) - Codex TypeScript SDK thread events/items (
sdk/typescript/src/events.ts,items.ts) - Cursor ACP protocol (
agent acpJSON-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
| 1 | type 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. |
| 18 | export const RuntimeItemId = makeEntityId("RuntimeItemId"); |
| 19 | export type RuntimeItemId = typeof RuntimeItemId.Type; |
| 20 | export const RuntimeRequestId = makeEntityId("RuntimeRequestId"); |
| 21 | export type RuntimeRequestId = typeof RuntimeRequestId.Type; |
| 22 | export const RuntimeTaskId = makeEntityId("RuntimeTaskId"); |
| 23 | export type RuntimeTaskId = typeof RuntimeTaskId.Type; |
| 24 | export const RuntimeSessionId = makeEntityId("RuntimeSessionId"); |
| 25 | export type RuntimeSessionId = typeof RuntimeSessionId.Type; |
| 26 | |
| 27 | type 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
| 1 | type 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
| 1 | type 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
| 1 | type 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)
| 1 | type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error"; |
| 2 | type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error"; |
| 3 | type TurnState = "completed" | "failed" | "interrupted" | "cancelled"; |
| 4 | |
| 5 | type ContentStreamKind = |
| 6 | | "assistant_text" |
| 7 | | "reasoning_text" |
| 8 | | "reasoning_summary_text" |
| 9 | | "plan_text" |
| 10 | | "command_output" |
| 11 | | "file_change_output" |
| 12 | | "unknown"; |
| 13 | |
| 14 | type ItemLifecyclePayload = { |
| 15 | itemType: CanonicalItemType; |
| 16 | status?: "inProgress" | "completed" | "failed" | "declined"; |
| 17 | title?: string; |
| 18 | detail?: string; |
| 19 | data?: unknown; |
| 20 | }; |
| 21 | |
| 22 | type 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 | |
| 132 | type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType; |
| 133 | type MissingPayloadTypes = Exclude< |
| 134 | CanonicalRuntimeEventType, |
| 135 | CanonicalRuntimeEventTypeFromPayloadMap |
| 136 | >; |
| 137 | type ExtraPayloadTypes = Exclude< |
| 138 | CanonicalRuntimeEventTypeFromPayloadMap, |
| 139 | CanonicalRuntimeEventType |
| 140 | >; |
| 141 | // Both should be `never`. |
| 142 | |
| 143 | export type ProviderRuntimeEventV2 = { |
| 144 | [TType in CanonicalRuntimeEventTypeFromPayloadMap]: RuntimeEventBase< |
| 145 | TType, |
| 146 | RuntimeEventPayloadByType[TType] |
| 147 | >; |
| 148 | }[CanonicalRuntimeEventTypeFromPayloadMap]; |
1.6 ID Semantics
eventId,sessionId,threadId,turnId,itemId,requestIdare canonical T3 IDs (branded).- Provider IDs must go in
providerRefs.*(andraw), 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
| Current | Action |
|---|---|
session.started | Keep, add structured payload (resume/fork/config snapshot if known) |
session.exited | Keep, add reason, exitKind, recoverable |
thread.started | Keep, add source metadata |
turn.started | Keep, include optional model/provider state |
turn.completed | Keep, expand status + usage + stopReason + cost/modelUsage |
message.delta | Replace semantics with generic content.delta (message delta becomes one stream kind) |
runtime.error | Keep but include normalized error class + provider raw error info |
2.2 Delete (replaced)
| Current | Replacement |
|---|---|
message.completed | item.completed for assistant_message |
tool.started | item.started (command_execution, file_change, mcp_tool_call, dynamic_tool_call, etc.) |
tool.completed | item.completed |
approval.requested | request.opened |
approval.resolved | request.resolved |
checkpoint.captured | Remove 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 method | Canonical V2 | Notes |
|---|---|---|
thread/started | thread.started | thread.id -> threadId |
thread/status/changed | thread.state.changed | preserve full status object (notLoaded/idle/systemError/active{flags}) |
thread/archived / thread/unarchived / thread/closed | thread.state.changed | normalize state=archived/unarchived/closed |
thread/name/updated | thread.metadata.updated | threadName |
thread/tokenUsage/updated | thread.token-usage.updated | preserve totals + breakdown |
turn/started | turn.started | turn.id, initial turn item snapshot |
turn/completed | turn.completed | map TurnStatus: completed/interrupted/failed/inProgress |
turn/diff/updated | turn.diff.updated | keep unified diff payload |
turn/plan/updated | turn.plan.updated | preserve explanation, steps (pending/inProgress/completed) |
item/started | item.started | map item type to canonical item type |
item/completed | item.completed | map item type + terminal status |
item/agentMessage/delta | content.delta | streamKind=assistant_text |
item/plan/delta | content.delta | streamKind=plan_text |
item/commandExecution/outputDelta | content.delta | streamKind=command_output |
item/fileChange/outputDelta | content.delta | streamKind=file_change_output |
item/reasoning/summaryTextDelta | content.delta | streamKind=reasoning_summary_text, summaryIndex |
item/reasoning/summaryPartAdded | item.updated | item reasoning summary segment boundary |
item/reasoning/textDelta | content.delta | streamKind=reasoning_text, contentIndex |
item/mcpToolCall/progress | tool.progress | for MCP calls |
item/commandExecution/terminalInteraction | item.updated | interaction input on command execution item |
serverRequest/resolved | request.resolved | correlate by request id |
model/rerouted | model.rerouted | preserve fromModel/toModel/reason |
thread/compacted | thread.state.changed | compacted context marker |
thread/realtime/started | thread.realtime.started | realtime transport/session id |
thread/realtime/itemAdded | thread.realtime.item-added | raw realtime item |
thread/realtime/outputAudio/delta | thread.realtime.audio.delta | preserve sample metadata |
thread/realtime/error | thread.realtime.error | message |
thread/realtime/closed | thread.realtime.closed | optional reason |
error | runtime.error | preserve willRetry, codex error info |
deprecationNotice | deprecation.notice | summary/details |
configWarning | config.warning | summary/details/path/range |
account/updated | account.updated | auth mode updates |
account/rateLimits/updated | account.rate-limits.updated | rate limits snapshot |
mcpServer/oauthLogin/completed | mcp.oauth.completed | success/error/name |
windows/worldWritableWarning | runtime.warning | platform warning |
windowsSandbox/setupCompleted | session.state.changed + runtime.warning | success/failure diagnostic |
app/list/updated | runtime.warning or future app-list.updated | optional if UI wants app ecosystem surface |
account/login/completed | auth.status | login success/error |
3.2 Server Requests (ServerRequest.method)
| Codex request method | Canonical V2 |
|---|---|
item/commandExecution/requestApproval | request.opened (requestType=command_execution_approval) |
item/fileChange/requestApproval | request.opened (requestType=file_change_approval) |
applyPatchApproval | request.opened (requestType=apply_patch_approval) |
execCommandApproval | request.opened (requestType=exec_command_approval) |
item/tool/requestUserInput | user-input.requested + request.opened (requestType=tool_user_input) |
item/tool/call | request.opened (requestType=dynamic_tool_call) |
account/chatgptAuthTokens/refresh | request.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.completedagent_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.requestedplan_update,plan_delta->turn.plan.updated/content.delta(plan_text)turn_diff,turn_aborted,thread_rolled_back,context_compacted,model_reroute-> dedicatedturn.*/thread.*/model.rerouted- collab events (
collab_*) ->item.*(collab_agent_tool_call) ortask.*depending UI preference
4) Claude Agent SDK -> Canonical Mapping
4.1 SDKMessage Mapping (@anthropic-ai/claude-agent-sdk@0.2.62)
| Claude SDK message | Canonical V2 | Notes |
|---|---|---|
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.started | map tool name -> canonical item type (command_execution/file_change/mcp_tool_call/dynamic_tool_call) |
stream_event content_block_stop | item.completed | close tool item started above |
result subtype=success | turn.completed (status=completed) | include usage/modelUsage/cost/stop_reason |
result subtype=error_* | runtime.error + turn.completed(status=failed) | include error list + subtype |
system:init | session.configured | model/tools/permissionMode/skills/plugins/mcp status |
system:status | session.state.changed | compacting status + permission mode |
system:compact_boundary | thread.state.changed | context compaction boundary |
system:hook_started | hook.started | hook telemetry |
system:hook_progress | hook.progress | hook stdout/stderr/output |
system:hook_response | hook.completed | outcome + exit_code |
tool_progress | tool.progress | elapsed seconds, tool name, tool_use_id |
tool_use_summary | tool.summary | summary + related tool use ids |
auth_status | auth.status | authentication progress/errors |
system:task_started | task.started | background task/subagent activity |
system:task_progress | task.progress | incremental background task updates |
system:task_notification | task.completed | status completed/failed/stopped |
system:files_persisted | files.persisted | file 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 phase | Canonical 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 behavior | turn.completed(status=interrupted) or runtime.error as appropriate |
5) Cursor ACP -> Canonical Mapping
5.1 ACP session lifecycle (JSON-RPC methods)
| Cursor ACP method | Canonical V2 | Notes |
|---|---|---|
initialize (response) | session.configured | preserve protocolVersion, agentCapabilities, authMethods |
authenticate (response) | auth.status | success 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.completed | map stopReason (end_turn, cancelled, etc.) |
session/cancel (error -32601) | runtime.warning | currently unsupported in observed ACP version; adapter should fall back to process interrupt |
5.2 ACP notifications (session/update)
Cursor ACP update.sessionUpdate | Canonical V2 | Notes |
|---|---|---|
available_commands_update | session.configured | preserve availableCommands; optional secondary runtime.warning if list changes mid-turn |
agent_thought_chunk | content.delta (streamKind=reasoning_text) | map content.text |
agent_message_chunk | content.delta (streamKind=assistant_text) | map content.text |
tool_call | item.started | map toolCallId -> itemId; dedupe starts because ACP may emit multiple tool_call updates for the same id |
tool_call_update (in_progress) | item.updated | include status + partial metadata |
tool_call_update (completed) | item.completed | include rawOutput summary when present (terminal stdout/stderr, search totals, etc.) |
5.3 ACP server requests (permission)
| Cursor ACP request method | Canonical V2 | Notes |
|---|---|---|
session/request_permission | request.opened | requestType usually command_execution_approval for kind=execute; preserve options (allow/reject) |
| permission response sent by T3 | request.resolved | map T3 decision -> ACP optionId (allow-once, allow-always, reject-once) |
5.4 ACP runtime/error mapping notes
- Unknown session on
session/prompt(-32603) ->runtime.error(class=provider_error)and terminate active turn as failed. - Invalid params / method-not-found (
-32602,-32601) ->runtime.error(class=validation_error)orruntime.warningdepending recoverability. - Concurrent
session/promptcalls can cancel earlier prompt (stopReason=cancelled) -> emitturn.completed(state=cancelled)for the superseded turn. - Keep all ACP envelopes in
rawwithsourceset tocursor.acp.notificationorcursor.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 ThreadEvent | Canonical V2 |
|---|---|
thread.started | thread.started |
turn.started | turn.started |
turn.completed | turn.completed |
turn.failed | runtime.error + turn.completed(status=failed) |
item.started | item.started |
item.updated | item.updated |
item.completed | item.completed |
error | runtime.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)
| Concept | Codex source | Claude source | Cursor ACP source | Canonical |
|---|---|---|---|---|
| Turn start | turn/started | first active response turn + sendTurn start | session/prompt dispatch lifecycle | turn.started |
| Turn completion | turn/completed | result | session/prompt response (stopReason) | turn.completed |
| Assistant text streaming | item/agentMessage/delta | stream_event content_block_delta[text_delta] | session/update:agent_message_chunk | content.delta(assistant_text) |
| Reasoning/thought streaming | item/reasoning/textDelta | reasoning deltas in stream_event | session/update:agent_thought_chunk | content.delta(reasoning_text) |
| Tool call start | item/started with tool item | content_block_start tool_use/server_tool_use/mcp_tool_use | session/update:tool_call | item.started |
| Tool call end | item/completed with tool item | content_block_stop | session/update:tool_call_update(completed) | item.completed |
| Approval required | server request .../requestApproval | canUseTool callback | session/request_permission | request.opened |
| Approval resolved | serverRequest/resolved + response decision | callback resolution decision | JSON-RPC response to permission request id | request.resolved |
| Runtime error | error notification / failed turn | result error_* or assistant error | JSON-RPC error response (-3260x etc.) | runtime.error |
| Session capabilities/config | session_configured/init responses | system:init | initialize / available_commands_update | session.configured |
8) Implementation Guidance
- Emit one canonical event per source message minimum; emit additional derived events when needed (for example
runtime.error+turn.completed). - Always attach
rawpayload so provider-specific detail is never lost. - Replace current Codex adapter heuristic mapping (
tool.*,approval.*,message.*) with method/item-driven mapping from app-server schemas. - Extend Claude adapter to emit non-chat telemetry (
system:*,task_*,hook_*,tool_progress,auth_status,files_persisted) into canonical events. - 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)
- request lifecycle keyed by
- 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 inpayload+raw. - This supports richer integration ports without provider-specific logic in downstream consumers.