| ... | ... | @@ -0,0 +1,520 @@ |
| 1 | # EVENTS.md |
| 2 | |
| 3 | ## Goal |
| 4 | |
| 5 | Define a **breaking** canonical provider-runtime event model that can represent the full useful surface of: |
| 6 | |
| 7 | - Claude Agent SDK types (`@anthropic-ai/claude-agent-sdk@0.2.62`) |
| 8 | - Codex App Server protocol (`schema/json/*`, including `ServerNotification`, `ServerRequest`, `EventMsg`, and v2 payload schemas) |
| 9 | - Codex TypeScript SDK thread events/items (`sdk/typescript/src/events.ts`, `items.ts`) |
| 10 | |
| 11 | This is a mapping/spec document only (no downstream compatibility constraints). |
| 12 | |
| 13 | --- |
| 14 | |
| 15 | ## 1) Proposed Canonical Runtime Event Model (V2) |
| 16 | |
| 17 | ### 1.1 Envelope |
| 18 | |
| 19 | ```ts |
| 20 | type RuntimeEventRaw = { |
| 21 | source: |
| 22 | | "codex.app-server.notification" |
| 23 | | "codex.app-server.request" |
| 24 | | "codex.eventmsg" |
| 25 | | "claude.sdk.message" |
| 26 | | "claude.sdk.permission" |
| 27 | | "codex.sdk.thread-event"; |
| 28 | method?: string; |
| 29 | messageType?: string; |
| 30 | payload: unknown; |
| 31 | }; |
| 32 | |
| 33 | // Add these to `packages/contracts/src/baseSchemas.ts` for V2. |
| 34 | export const RuntimeItemId = makeEntityId("RuntimeItemId"); |
| 35 | export type RuntimeItemId = typeof RuntimeItemId.Type; |
| 36 | export const RuntimeRequestId = makeEntityId("RuntimeRequestId"); |
| 37 | export type RuntimeRequestId = typeof RuntimeRequestId.Type; |
| 38 | export const RuntimeTaskId = makeEntityId("RuntimeTaskId"); |
| 39 | export type RuntimeTaskId = typeof RuntimeTaskId.Type; |
| 40 | export const RuntimeSessionId = makeEntityId("RuntimeSessionId"); |
| 41 | export type RuntimeSessionId = typeof RuntimeSessionId.Type; |
| 42 | |
| 43 | type RuntimeEventBase<TType extends CanonicalRuntimeEventType, TPayload> = { |
| 44 | eventId: EventId; |
| 45 | provider: ProviderKind; |
| 46 | sessionId: RuntimeSessionId; |
| 47 | createdAt: IsoDateTime; |
| 48 | |
| 49 | // Canonical T3 IDs (not provider-native IDs). |
| 50 | threadId?: ThreadId; |
| 51 | turnId?: TurnId; |
| 52 | itemId?: RuntimeItemId; |
| 53 | requestId?: RuntimeRequestId; |
| 54 | |
| 55 | // Provider-native IDs preserved for correlation/debugging. |
| 56 | providerRefs?: { |
| 57 | providerSessionId?: ProviderSessionId; |
| 58 | providerThreadId?: ProviderThreadId; |
| 59 | providerTurnId?: ProviderTurnId; |
| 60 | providerItemId?: ProviderItemId; |
| 61 | providerRequestId?: TrimmedNonEmptyString; |
| 62 | }; |
| 63 | |
| 64 | type: TType; |
| 65 | payload: TPayload; |
| 66 | raw?: RuntimeEventRaw; |
| 67 | }; |
| 68 | ``` |
| 69 | |
| 70 | ### 1.2 Canonical Event Families |
| 71 | |
| 72 | ```ts |
| 73 | type CanonicalRuntimeEventType = |
| 74 | // lifecycle |
| 75 | | "session.started" |
| 76 | | "session.configured" |
| 77 | | "session.state.changed" |
| 78 | | "session.exited" |
| 79 | | "thread.started" |
| 80 | | "thread.state.changed" |
| 81 | | "thread.metadata.updated" |
| 82 | | "thread.token-usage.updated" |
| 83 | | "thread.realtime.started" |
| 84 | | "thread.realtime.item-added" |
| 85 | | "thread.realtime.audio.delta" |
| 86 | | "thread.realtime.error" |
| 87 | | "thread.realtime.closed" |
| 88 | | "turn.started" |
| 89 | | "turn.completed" |
| 90 | | "turn.aborted" |
| 91 | | "turn.plan.updated" |
| 92 | | "turn.diff.updated" |
| 93 | |
| 94 | // item + content stream |
| 95 | | "item.started" |
| 96 | | "item.updated" |
| 97 | | "item.completed" |
| 98 | | "content.delta" |
| 99 | |
| 100 | // approvals / input / tool-protocol requests |
| 101 | | "request.opened" |
| 102 | | "request.resolved" |
| 103 | | "user-input.requested" |
| 104 | | "user-input.resolved" |
| 105 | |
| 106 | // provider/system telemetry |
| 107 | | "task.started" |
| 108 | | "task.progress" |
| 109 | | "task.completed" |
| 110 | | "hook.started" |
| 111 | | "hook.progress" |
| 112 | | "hook.completed" |
| 113 | | "tool.progress" |
| 114 | | "tool.summary" |
| 115 | | "auth.status" |
| 116 | | "account.updated" |
| 117 | | "account.rate-limits.updated" |
| 118 | | "mcp.status.updated" |
| 119 | | "mcp.oauth.completed" |
| 120 | | "model.rerouted" |
| 121 | | "config.warning" |
| 122 | | "deprecation.notice" |
| 123 | | "files.persisted" |
| 124 | | "runtime.warning" |
| 125 | | "runtime.error"; |
| 126 | ``` |
| 127 | |
| 128 | ### 1.3 Canonical Item Type |
| 129 | |
| 130 | ```ts |
| 131 | type CanonicalItemType = |
| 132 | | "user_message" |
| 133 | | "assistant_message" |
| 134 | | "reasoning" |
| 135 | | "plan" |
| 136 | | "command_execution" |
| 137 | | "file_change" |
| 138 | | "mcp_tool_call" |
| 139 | | "dynamic_tool_call" |
| 140 | | "collab_agent_tool_call" |
| 141 | | "web_search" |
| 142 | | "image_view" |
| 143 | | "review_entered" |
| 144 | | "review_exited" |
| 145 | | "context_compaction" |
| 146 | | "error" |
| 147 | | "unknown"; |
| 148 | ``` |
| 149 | |
| 150 | ### 1.4 Canonical Request Type |
| 151 | |
| 152 | ```ts |
| 153 | type CanonicalRequestType = |
| 154 | | "command_execution_approval" |
| 155 | | "file_change_approval" |
| 156 | | "apply_patch_approval" |
| 157 | | "exec_command_approval" |
| 158 | | "tool_user_input" |
| 159 | | "dynamic_tool_call" |
| 160 | | "auth_tokens_refresh" |
| 161 | | "unknown"; |
| 162 | ``` |
| 163 | |
| 164 | ### 1.5 Discriminated Union (Type-Safe) |
| 165 | |
| 166 | ```ts |
| 167 | type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error"; |
| 168 | type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error"; |
| 169 | type TurnState = "completed" | "failed" | "interrupted" | "cancelled"; |
| 170 | |
| 171 | type ContentStreamKind = |
| 172 | | "assistant_text" |
| 173 | | "reasoning_text" |
| 174 | | "reasoning_summary_text" |
| 175 | | "plan_text" |
| 176 | | "command_output" |
| 177 | | "file_change_output" |
| 178 | | "unknown"; |
| 179 | |
| 180 | type ItemLifecyclePayload = { |
| 181 | itemType: CanonicalItemType; |
| 182 | status?: "inProgress" | "completed" | "failed" | "declined"; |
| 183 | title?: string; |
| 184 | detail?: string; |
| 185 | data?: unknown; |
| 186 | }; |
| 187 | |
| 188 | type RuntimeEventPayloadByType = { |
| 189 | "session.started": { message?: string; resume?: unknown }; |
| 190 | "session.configured": { config: Record<string, unknown> }; |
| 191 | "session.state.changed": { state: SessionState; reason?: string; detail?: string }; |
| 192 | "session.exited": { reason?: string; recoverable?: boolean; exitKind?: "graceful" | "error" }; |
| 193 | |
| 194 | "thread.started": { providerThreadId?: ProviderThreadId }; |
| 195 | "thread.state.changed": { state: ThreadState; detail?: unknown }; |
| 196 | "thread.metadata.updated": { name?: string; metadata?: Record<string, unknown> }; |
| 197 | "thread.token-usage.updated": { usage: unknown }; |
| 198 | "thread.realtime.started": { realtimeSessionId?: string }; |
| 199 | "thread.realtime.item-added": { item: unknown }; |
| 200 | "thread.realtime.audio.delta": { audio: unknown }; |
| 201 | "thread.realtime.error": { message: string }; |
| 202 | "thread.realtime.closed": { reason?: string }; |
| 203 | |
| 204 | "turn.started": { model?: string; effort?: string }; |
| 205 | "turn.completed": { |
| 206 | state: TurnState; |
| 207 | stopReason?: string | null; |
| 208 | usage?: unknown; |
| 209 | modelUsage?: Record<string, unknown>; |
| 210 | totalCostUsd?: number; |
| 211 | errorMessage?: string; |
| 212 | }; |
| 213 | "turn.aborted": { reason: string }; |
| 214 | "turn.plan.updated": { |
| 215 | explanation?: string | null; |
| 216 | plan: Array<{ step: string; status: "pending" | "inProgress" | "completed" }>; |
| 217 | }; |
| 218 | "turn.diff.updated": { unifiedDiff: string }; |
| 219 | |
| 220 | "item.started": ItemLifecyclePayload; |
| 221 | "item.updated": ItemLifecyclePayload; |
| 222 | "item.completed": ItemLifecyclePayload; |
| 223 | "content.delta": { |
| 224 | streamKind: ContentStreamKind; |
| 225 | delta: string; |
| 226 | contentIndex?: number; |
| 227 | summaryIndex?: number; |
| 228 | }; |
| 229 | |
| 230 | "request.opened": { |
| 231 | requestType: CanonicalRequestType; |
| 232 | detail?: string; |
| 233 | args?: unknown; |
| 234 | }; |
| 235 | "request.resolved": { |
| 236 | requestType: CanonicalRequestType; |
| 237 | decision?: string; |
| 238 | resolution?: unknown; |
| 239 | }; |
| 240 | "user-input.requested": { questions: unknown }; |
| 241 | "user-input.resolved": { answers: unknown }; |
| 242 | |
| 243 | "task.started": { taskId: RuntimeTaskId; description?: string; taskType?: string }; |
| 244 | "task.progress": { |
| 245 | taskId: RuntimeTaskId; |
| 246 | description: string; |
| 247 | usage?: unknown; |
| 248 | lastToolName?: string; |
| 249 | }; |
| 250 | "task.completed": { |
| 251 | taskId: RuntimeTaskId; |
| 252 | status: "completed" | "failed" | "stopped"; |
| 253 | summary?: string; |
| 254 | usage?: unknown; |
| 255 | }; |
| 256 | "hook.started": { hookId: string; hookName: string; hookEvent: string }; |
| 257 | "hook.progress": { hookId: string; output?: string; stdout?: string; stderr?: string }; |
| 258 | "hook.completed": { |
| 259 | hookId: string; |
| 260 | outcome: "success" | "error" | "cancelled"; |
| 261 | output?: string; |
| 262 | stdout?: string; |
| 263 | stderr?: string; |
| 264 | exitCode?: number; |
| 265 | }; |
| 266 | "tool.progress": { |
| 267 | toolUseId?: string; |
| 268 | toolName?: string; |
| 269 | summary?: string; |
| 270 | elapsedSeconds?: number; |
| 271 | }; |
| 272 | "tool.summary": { summary: string; precedingToolUseIds?: string[] }; |
| 273 | "auth.status": { isAuthenticating?: boolean; output?: string[]; error?: string }; |
| 274 | "account.updated": { account: unknown }; |
| 275 | "account.rate-limits.updated": { rateLimits: unknown }; |
| 276 | "mcp.status.updated": { status: unknown }; |
| 277 | "mcp.oauth.completed": { success: boolean; name?: string; error?: string }; |
| 278 | "model.rerouted": { fromModel: string; toModel: string; reason: string }; |
| 279 | "config.warning": { summary: string; details?: string; path?: string; range?: unknown }; |
| 280 | "deprecation.notice": { summary: string; details?: string }; |
| 281 | "files.persisted": { |
| 282 | files: Array<{ filename: string; fileId: string }>; |
| 283 | failed?: Array<{ filename: string; error: string }>; |
| 284 | }; |
| 285 | "runtime.warning": { message: string; detail?: unknown }; |
| 286 | "runtime.error": { |
| 287 | message: string; |
| 288 | class?: |
| 289 | | "provider_error" |
| 290 | | "transport_error" |
| 291 | | "permission_error" |
| 292 | | "validation_error" |
| 293 | | "unknown"; |
| 294 | detail?: unknown; |
| 295 | }; |
| 296 | }; |
| 297 | |
| 298 | type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType; |
| 299 | type MissingPayloadTypes = Exclude< |
| 300 | CanonicalRuntimeEventType, |
| 301 | CanonicalRuntimeEventTypeFromPayloadMap |
| 302 | >; |
| 303 | type ExtraPayloadTypes = Exclude< |
| 304 | CanonicalRuntimeEventTypeFromPayloadMap, |
| 305 | CanonicalRuntimeEventType |
| 306 | >; |
| 307 | // Both should be `never`. |
| 308 | |
| 309 | export type ProviderRuntimeEventV2 = { |
| 310 | [TType in CanonicalRuntimeEventTypeFromPayloadMap]: RuntimeEventBase< |
| 311 | TType, |
| 312 | RuntimeEventPayloadByType[TType] |
| 313 | >; |
| 314 | }[CanonicalRuntimeEventTypeFromPayloadMap]; |
| 315 | ``` |
| 316 | |
| 317 | ### 1.6 ID Semantics |
| 318 | |
| 319 | - `eventId`, `sessionId`, `threadId`, `turnId`, `itemId`, `requestId` are **canonical T3 IDs** (branded). |
| 320 | - Provider IDs must go in `providerRefs.*` (and `raw`), never in canonical fields. |
| 321 | - If a canonical id is not known at emit time, omit it and emit only `providerRefs` + `raw`. |
| 322 | |
| 323 | --- |
| 324 | |
| 325 | ## 2) Changes vs Current `ProviderRuntimeEvent` (packages/contracts/src/providerRuntime.ts) |
| 326 | |
| 327 | ## 2.1 Update Existing |
| 328 | |
| 329 | | Current | Action | |
| 330 | | ----------------- | -------------------------------------------------------------------------------------- | |
| 331 | | `session.started` | Keep, add structured payload (`resume/fork/config snapshot if known`) | |
| 332 | | `session.exited` | Keep, add `reason`, `exitKind`, `recoverable` | |
| 333 | | `thread.started` | Keep, add source metadata | |
| 334 | | `turn.started` | Keep, include optional model/provider state | |
| 335 | | `turn.completed` | Keep, expand status + usage + stopReason + cost/modelUsage | |
| 336 | | `message.delta` | Replace semantics with generic `content.delta` (message delta becomes one stream kind) | |
| 337 | | `runtime.error` | Keep but include normalized error class + provider raw error info | |
| 338 | |
| 339 | ## 2.2 Delete (replaced) |
| 340 | |
| 341 | | Current | Replacement | |
| 342 | | --------------------- | ------------------------------------------------------------------------------------------------------ | |
| 343 | | `message.completed` | `item.completed` for `assistant_message` | |
| 344 | | `tool.started` | `item.started` (`command_execution`, `file_change`, `mcp_tool_call`, `dynamic_tool_call`, etc.) | |
| 345 | | `tool.completed` | `item.completed` | |
| 346 | | `approval.requested` | `request.opened` | |
| 347 | | `approval.resolved` | `request.resolved` | |
| 348 | | `checkpoint.captured` | Remove from provider runtime canonical set (keep checkpoint as orchestration-level projection concern) | |
| 349 | |
| 350 | ## 2.3 Create New |
| 351 | |
| 352 | `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. |
| 353 | |
| 354 | --- |
| 355 | |
| 356 | ## 3) Codex App Server -> Canonical Mapping |
| 357 | |
| 358 | ### 3.1 Server Notifications (`ServerNotification.method`) |
| 359 | |
| 360 | | Codex method | Canonical V2 | Notes | |
| 361 | | --------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------ | |
| 362 | | `thread/started` | `thread.started` | `thread.id -> threadId` | |
| 363 | | `thread/status/changed` | `thread.state.changed` | preserve full status object (`notLoaded/idle/systemError/active{flags}`) | |
| 364 | | `thread/archived` / `thread/unarchived` / `thread/closed` | `thread.state.changed` | normalize `state=archived/unarchived/closed` | |
| 365 | | `thread/name/updated` | `thread.metadata.updated` | `threadName` | |
| 366 | | `thread/tokenUsage/updated` | `thread.token-usage.updated` | preserve totals + breakdown | |
| 367 | | `turn/started` | `turn.started` | `turn.id`, initial turn item snapshot | |
| 368 | | `turn/completed` | `turn.completed` | map `TurnStatus`: `completed/interrupted/failed/inProgress` | |
| 369 | | `turn/diff/updated` | `turn.diff.updated` | keep unified diff payload | |
| 370 | | `turn/plan/updated` | `turn.plan.updated` | preserve `explanation`, steps (`pending/inProgress/completed`) | |
| 371 | | `item/started` | `item.started` | map item type to canonical item type | |
| 372 | | `item/completed` | `item.completed` | map item type + terminal status | |
| 373 | | `item/agentMessage/delta` | `content.delta` | `streamKind=assistant_text` | |
| 374 | | `item/plan/delta` | `content.delta` | `streamKind=plan_text` | |
| 375 | | `item/commandExecution/outputDelta` | `content.delta` | `streamKind=command_output` | |
| 376 | | `item/fileChange/outputDelta` | `content.delta` | `streamKind=file_change_output` | |
| 377 | | `item/reasoning/summaryTextDelta` | `content.delta` | `streamKind=reasoning_summary_text`, `summaryIndex` | |
| 378 | | `item/reasoning/summaryPartAdded` | `item.updated` | item reasoning summary segment boundary | |
| 379 | | `item/reasoning/textDelta` | `content.delta` | `streamKind=reasoning_text`, `contentIndex` | |
| 380 | | `item/mcpToolCall/progress` | `tool.progress` | for MCP calls | |
| 381 | | `item/commandExecution/terminalInteraction` | `item.updated` | interaction input on command execution item | |
| 382 | | `serverRequest/resolved` | `request.resolved` | correlate by request id | |
| 383 | | `model/rerouted` | `model.rerouted` | preserve `fromModel/toModel/reason` | |
| 384 | | `thread/compacted` | `thread.state.changed` | compacted context marker | |
| 385 | | `thread/realtime/started` | `thread.realtime.started` | realtime transport/session id | |
| 386 | | `thread/realtime/itemAdded` | `thread.realtime.item-added` | raw realtime item | |
| 387 | | `thread/realtime/outputAudio/delta` | `thread.realtime.audio.delta` | preserve sample metadata | |
| 388 | | `thread/realtime/error` | `thread.realtime.error` | message | |
| 389 | | `thread/realtime/closed` | `thread.realtime.closed` | optional reason | |
| 390 | | `error` | `runtime.error` | preserve `willRetry`, codex error info | |
| 391 | | `deprecationNotice` | `deprecation.notice` | summary/details | |
| 392 | | `configWarning` | `config.warning` | summary/details/path/range | |
| 393 | | `account/updated` | `account.updated` | auth mode updates | |
| 394 | | `account/rateLimits/updated` | `account.rate-limits.updated` | rate limits snapshot | |
| 395 | | `mcpServer/oauthLogin/completed` | `mcp.oauth.completed` | success/error/name | |
| 396 | | `windows/worldWritableWarning` | `runtime.warning` | platform warning | |
| 397 | | `windowsSandbox/setupCompleted` | `session.state.changed` + `runtime.warning` | success/failure diagnostic | |
| 398 | | `app/list/updated` | `runtime.warning` or future `app-list.updated` | optional if UI wants app ecosystem surface | |
| 399 | | `account/login/completed` | `auth.status` | login success/error | |
| 400 | |
| 401 | ### 3.2 Server Requests (`ServerRequest.method`) |
| 402 | |
| 403 | | Codex request method | Canonical V2 | |
| 404 | | --------------------------------------- | ------------------------------------------------------------------------- | |
| 405 | | `item/commandExecution/requestApproval` | `request.opened` (`requestType=command_execution_approval`) | |
| 406 | | `item/fileChange/requestApproval` | `request.opened` (`requestType=file_change_approval`) | |
| 407 | | `applyPatchApproval` | `request.opened` (`requestType=apply_patch_approval`) | |
| 408 | | `execCommandApproval` | `request.opened` (`requestType=exec_command_approval`) | |
| 409 | | `item/tool/requestUserInput` | `user-input.requested` + `request.opened` (`requestType=tool_user_input`) | |
| 410 | | `item/tool/call` | `request.opened` (`requestType=dynamic_tool_call`) | |
| 411 | | `account/chatgptAuthTokens/refresh` | `request.opened` (`requestType=auth_tokens_refresh`) | |
| 412 | |
| 413 | ### 3.3 EventMsg cross-reference (`EventMsg.type`) |
| 414 | |
| 415 | EventMsg is broader than server notification coverage. Important equivalents: |
| 416 | |
| 417 | - `task_started` / `task_complete` -> `turn.started` / `turn.completed` |
| 418 | - `agent_message_delta`, `agent_message_content_delta` -> `content.delta (assistant_text)` |
| 419 | - `agent_reasoning_delta`, `reasoning_content_delta`, `reasoning_raw_content_delta` -> `content.delta (reasoning*)` |
| 420 | - `exec_command_begin/output_delta/end` -> `item.started/content.delta/item.completed` (`command_execution`) |
| 421 | - `patch_apply_begin/end` -> `item.updated/item.completed` (`file_change`) |
| 422 | - `exec_approval_request`, `apply_patch_approval_request`, `request_user_input` -> `request.opened` / `user-input.requested` |
| 423 | - `plan_update`, `plan_delta` -> `turn.plan.updated` / `content.delta(plan_text)` |
| 424 | - `turn_diff`, `turn_aborted`, `thread_rolled_back`, `context_compacted`, `model_reroute` -> dedicated `turn.*` / `thread.*` / `model.rerouted` |
| 425 | - collab events (`collab_*`) -> `item.*` (`collab_agent_tool_call`) or `task.*` depending UI preference |
| 426 | |
| 427 | --- |
| 428 | |
| 429 | ## 4) Claude Agent SDK -> Canonical Mapping |
| 430 | |
| 431 | ### 4.1 SDKMessage Mapping (`@anthropic-ai/claude-agent-sdk@0.2.62`) |
| 432 | |
| 433 | | Claude SDK message | Canonical V2 | Notes | |
| 434 | | ------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | |
| 435 | | `assistant` (`SDKAssistantMessage`) | `item.updated` or `item.completed` (`assistant_message`) | preserve `message`, `uuid`, optional `error` | |
| 436 | | `stream_event` (`SDKPartialAssistantMessage`) `content_block_delta[text_delta]` | `content.delta` (`assistant_text`) | primary streamed assistant text | |
| 437 | | `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`) | |
| 438 | | `stream_event` `content_block_stop` | `item.completed` | close tool item started above | |
| 439 | | `result` `subtype=success` | `turn.completed` (`status=completed`) | include usage/modelUsage/cost/stop_reason | |
| 440 | | `result` `subtype=error_*` | `runtime.error` + `turn.completed(status=failed)` | include error list + subtype | |
| 441 | | `system:init` | `session.configured` | model/tools/permissionMode/skills/plugins/mcp status | |
| 442 | | `system:status` | `session.state.changed` | compacting status + permission mode | |
| 443 | | `system:compact_boundary` | `thread.state.changed` | context compaction boundary | |
| 444 | | `system:hook_started` | `hook.started` | hook telemetry | |
| 445 | | `system:hook_progress` | `hook.progress` | hook stdout/stderr/output | |
| 446 | | `system:hook_response` | `hook.completed` | outcome + exit_code | |
| 447 | | `tool_progress` | `tool.progress` | elapsed seconds, tool name, tool_use_id | |
| 448 | | `tool_use_summary` | `tool.summary` | summary + related tool use ids | |
| 449 | | `auth_status` | `auth.status` | authentication progress/errors | |
| 450 | | `system:task_started` | `task.started` | background task/subagent activity | |
| 451 | | `system:task_progress` | `task.progress` | incremental background task updates | |
| 452 | | `system:task_notification` | `task.completed` | status completed/failed/stopped | |
| 453 | | `system:files_persisted` | `files.persisted` | file ids persisted to backend | |
| 454 | | `user` / `user (isReplay)` | optional `item.started/item.completed (user_message)` | keep if we want full transcript parity | |
| 455 | | `rate_limit` / `prompt_suggestion` (in union) | `account.rate-limits.updated` or `item.updated(plan_suggestion)` | preserve raw payload even if sparse typing | |
| 456 | |
| 457 | ### 4.2 Claude Permission Callback Mapping (`canUseTool`) |
| 458 | |
| 459 | | Claude callback phase | Canonical V2 | |
| 460 | | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | |
| 461 | | permission request (`toolName`, input, suggestions, blockedPath, decisionReason, toolUseID) | `request.opened` (`requestType=command_execution_approval` or `file_change_approval` or `unknown`) | |
| 462 | | user decision allow/deny (+updatedPermissions) | `request.resolved` | |
| 463 | | interrupt/deny behavior | `turn.completed(status=interrupted)` or `runtime.error` as appropriate | |
| 464 | |
| 465 | --- |
| 466 | |
| 467 | ## 5) Codex SDK Thread Events -> Canonical Mapping |
| 468 | |
| 469 | (For parity with direct Codex SDK integrations and to cross-reference app-server behavior.) |
| 470 | |
| 471 | | Codex SDK ThreadEvent | Canonical V2 | |
| 472 | | --------------------- | ------------------------------------------------- | |
| 473 | | `thread.started` | `thread.started` | |
| 474 | | `turn.started` | `turn.started` | |
| 475 | | `turn.completed` | `turn.completed` | |
| 476 | | `turn.failed` | `runtime.error` + `turn.completed(status=failed)` | |
| 477 | | `item.started` | `item.started` | |
| 478 | | `item.updated` | `item.updated` | |
| 479 | | `item.completed` | `item.completed` | |
| 480 | | `error` | `runtime.error` | |
| 481 | |
| 482 | 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`). |
| 483 | |
| 484 | --- |
| 485 | |
| 486 | ## 6) Cross-Provider Equivalence (Claude <-> Codex) |
| 487 | |
| 488 | | Concept | Codex source | Claude source | Canonical | |
| 489 | | --------------------------- | -------------------------------------------- | ----------------------------------------------------------- | ------------------------------- | |
| 490 | | Turn start | `turn/started` | first active response turn + `sendTurn` start | `turn.started` | |
| 491 | | Turn completion | `turn/completed` | `result` | `turn.completed` | |
| 492 | | Assistant text streaming | `item/agentMessage/delta` | `stream_event content_block_delta[text_delta]` | `content.delta(assistant_text)` | |
| 493 | | Tool call start | `item/started` with tool item | `content_block_start tool_use/server_tool_use/mcp_tool_use` | `item.started` | |
| 494 | | Tool call end | `item/completed` with tool item | `content_block_stop` | `item.completed` | |
| 495 | | Approval required | server request `.../requestApproval` | `canUseTool` callback | `request.opened` | |
| 496 | | Approval resolved | `serverRequest/resolved` + response decision | callback resolution decision | `request.resolved` | |
| 497 | | Runtime error | `error` notification / failed turn | `result error_*` or assistant error | `runtime.error` | |
| 498 | | Model reroute | `model/rerouted` | (none native, but could appear as result/meta) | `model.rerouted` | |
| 499 | | Session capabilities/config | `session_configured`/init responses | `system:init` | `session.configured` | |
| 500 | |
| 501 | --- |
| 502 | |
| 503 | ## 7) Implementation Guidance |
| 504 | |
| 505 | 1. Emit one canonical event per source message minimum; emit additional derived events when needed (for example `runtime.error` + `turn.completed`). |
| 506 | 2. Always attach `raw` payload so provider-specific detail is never lost. |
| 507 | 3. Replace current Codex adapter heuristic mapping (`tool.*`, `approval.*`, `message.*`) with method/item-driven mapping from app-server schemas. |
| 508 | 4. Extend Claude adapter to emit non-chat telemetry (`system:*`, `task_*`, `hook_*`, `tool_progress`, `auth_status`, `files_persisted`) into canonical events. |
| 509 | 5. Keep ID correlation deterministic: |
| 510 | - request lifecycle keyed by `requestId` / approval id / tool use id |
| 511 | - item lifecycle keyed by `itemId` (synthesize stable ids for Claude when missing) |
| 512 | 6. Treat unknown provider events as `runtime.warning` + raw passthrough instead of dropping. |
| 513 | |
| 514 | --- |
| 515 | |
| 516 | ## 8) Summary of Breaking Schema Direction |
| 517 | |
| 518 | - Move from a small, chat-centric runtime union to an **item/lifecycle/protocol-complete canonical runtime model**. |
| 519 | - Normalize Claude and Codex onto the same event families (`turn`, `item`, `content`, `request`, `task`, `hook`, `state`), keeping provider specifics in `payload` + `raw`. |
| 520 | - This supports richer integration ports without provider-specific logic in downstream consumers. |