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
type RuntimeEventRaw = {
source:
| "codex.app-server.notification"
| "codex.app-server.request"
| "codex.eventmsg"
| "claude.sdk.message"
| "claude.sdk.permission"
| "codex.sdk.thread-event"
| "cursor.acp.notification"
| "cursor.acp.request"
| "cursor.acp.response";
method?: string;
messageType?: string;
payload: unknown;
};
// Add these to `packages/contracts/src/baseSchemas.ts` for V2.
export const RuntimeItemId = makeEntityId("RuntimeItemId");
export type RuntimeItemId = typeof RuntimeItemId.Type;
export const RuntimeRequestId = makeEntityId("RuntimeRequestId");
export type RuntimeRequestId = typeof RuntimeRequestId.Type;
export const RuntimeTaskId = makeEntityId("RuntimeTaskId");
export type RuntimeTaskId = typeof RuntimeTaskId.Type;
export const RuntimeSessionId = makeEntityId("RuntimeSessionId");
export type RuntimeSessionId = typeof RuntimeSessionId.Type;
type RuntimeEventBase<TType extends CanonicalRuntimeEventType, TPayload> = {
eventId: EventId;
provider: ProviderKind;
sessionId: RuntimeSessionId;
createdAt: IsoDateTime;
// Canonical T3 IDs (not provider-native IDs).
threadId?: ThreadId;
turnId?: TurnId;
itemId?: RuntimeItemId;
requestId?: RuntimeRequestId;
// Provider-native IDs preserved for correlation/debugging.
providerRefs?: {
providerSessionId?: ProviderSessionId;
providerThreadId?: ProviderThreadId;
providerTurnId?: ProviderTurnId;
providerItemId?: ProviderItemId;
providerRequestId?: TrimmedNonEmptyString;
};
type: TType;
payload: TPayload;
raw?: RuntimeEventRaw;
};
1.2 Canonical Event Families
type CanonicalRuntimeEventType =
// lifecycle
| "session.started"
| "session.configured"
| "session.state.changed"
| "session.exited"
| "thread.started"
| "thread.state.changed"
| "thread.metadata.updated"
| "thread.token-usage.updated"
| "thread.realtime.started"
| "thread.realtime.item-added"
| "thread.realtime.audio.delta"
| "thread.realtime.error"
| "thread.realtime.closed"
| "turn.started"
| "turn.completed"
| "turn.aborted"
| "turn.plan.updated"
| "turn.diff.updated"
// item + content stream
| "item.started"
| "item.updated"
| "item.completed"
| "content.delta"
// approvals / input / tool-protocol requests
| "request.opened"
| "request.resolved"
| "user-input.requested"
| "user-input.resolved"
// provider/system telemetry
| "task.started"
| "task.progress"
| "task.completed"
| "hook.started"
| "hook.progress"
| "hook.completed"
| "tool.progress"
| "tool.summary"
| "auth.status"
| "account.updated"
| "account.rate-limits.updated"
| "mcp.status.updated"
| "mcp.oauth.completed"
| "model.rerouted"
| "config.warning"
| "deprecation.notice"
| "files.persisted"
| "runtime.warning"
| "runtime.error";
1.3 Canonical Item Type
type CanonicalItemType =
| "user_message"
| "assistant_message"
| "reasoning"
| "plan"
| "command_execution"
| "file_change"
| "mcp_tool_call"
| "dynamic_tool_call"
| "collab_agent_tool_call"
| "web_search"
| "image_view"
| "review_entered"
| "review_exited"
| "context_compaction"
| "error"
| "unknown";
1.4 Canonical Request Type
type CanonicalRequestType =
| "command_execution_approval"
| "file_change_approval"
| "apply_patch_approval"
| "exec_command_approval"
| "tool_user_input"
| "dynamic_tool_call"
| "auth_tokens_refresh"
| "unknown";
1.5 Discriminated Union (Type-Safe)
type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error";
type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error";
type TurnState = "completed" | "failed" | "interrupted" | "cancelled";
type ContentStreamKind =
| "assistant_text"
| "reasoning_text"
| "reasoning_summary_text"
| "plan_text"
| "command_output"
| "file_change_output"
| "unknown";
type ItemLifecyclePayload = {
itemType: CanonicalItemType;
status?: "inProgress" | "completed" | "failed" | "declined";
title?: string;
detail?: string;
data?: unknown;
};
type RuntimeEventPayloadByType = {
"session.started": { message?: string; resume?: unknown };
"session.configured": { config: Record<string, unknown> };
"session.state.changed": { state: SessionState; reason?: string; detail?: string };
"session.exited": { reason?: string; recoverable?: boolean; exitKind?: "graceful" | "error" };
"thread.started": { providerThreadId?: ProviderThreadId };
"thread.state.changed": { state: ThreadState; detail?: unknown };
"thread.metadata.updated": { name?: string; metadata?: Record<string, unknown> };
"thread.token-usage.updated": { usage: unknown };
"thread.realtime.started": { realtimeSessionId?: string };
"thread.realtime.item-added": { item: unknown };
"thread.realtime.audio.delta": { audio: unknown };
"thread.realtime.error": { message: string };
"thread.realtime.closed": { reason?: string };
"turn.started": { model?: string; effort?: string };
"turn.completed": {
state: TurnState;
stopReason?: string | null;
usage?: unknown;
modelUsage?: Record<string, unknown>;
totalCostUsd?: number;
errorMessage?: string;
};
"turn.aborted": { reason: string };
"turn.plan.updated": {
explanation?: string | null;
plan: Array<{ step: string; status: "pending" | "inProgress" | "completed" }>;
};
"turn.diff.updated": { unifiedDiff: string };
"item.started": ItemLifecyclePayload;
"item.updated": ItemLifecyclePayload;
"item.completed": ItemLifecyclePayload;
"content.delta": {
streamKind: ContentStreamKind;
delta: string;
contentIndex?: number;
summaryIndex?: number;
};
"request.opened": {
requestType: CanonicalRequestType;
detail?: string;
args?: unknown;
};
"request.resolved": {
requestType: CanonicalRequestType;
decision?: string;
resolution?: unknown;
};
"user-input.requested": { questions: unknown };
"user-input.resolved": { answers: unknown };
"task.started": { taskId: RuntimeTaskId; description?: string; taskType?: string };
"task.progress": {
taskId: RuntimeTaskId;
description: string;
usage?: unknown;
lastToolName?: string;
};
"task.completed": {
taskId: RuntimeTaskId;
status: "completed" | "failed" | "stopped";
summary?: string;
usage?: unknown;
};
"hook.started": { hookId: string; hookName: string; hookEvent: string };
"hook.progress": { hookId: string; output?: string; stdout?: string; stderr?: string };
"hook.completed": {
hookId: string;
outcome: "success" | "error" | "cancelled";
output?: string;
stdout?: string;
stderr?: string;
exitCode?: number;
};
"tool.progress": {
toolUseId?: string;
toolName?: string;
summary?: string;
elapsedSeconds?: number;
};
"tool.summary": { summary: string; precedingToolUseIds?: string[] };
"auth.status": { isAuthenticating?: boolean; output?: string[]; error?: string };
"account.updated": { account: unknown };
"account.rate-limits.updated": { rateLimits: unknown };
"mcp.status.updated": { status: unknown };
"mcp.oauth.completed": { success: boolean; name?: string; error?: string };
"model.rerouted": { fromModel: string; toModel: string; reason: string };
"config.warning": { summary: string; details?: string; path?: string; range?: unknown };
"deprecation.notice": { summary: string; details?: string };
"files.persisted": {
files: Array<{ filename: string; fileId: string }>;
failed?: Array<{ filename: string; error: string }>;
};
"runtime.warning": { message: string; detail?: unknown };
"runtime.error": {
message: string;
class?:
| "provider_error"
| "transport_error"
| "permission_error"
| "validation_error"
| "unknown";
detail?: unknown;
};
};
type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType;
type MissingPayloadTypes = Exclude<
CanonicalRuntimeEventType,
CanonicalRuntimeEventTypeFromPayloadMap
>;
type ExtraPayloadTypes = Exclude<
CanonicalRuntimeEventTypeFromPayloadMap,
CanonicalRuntimeEventType
>;
// Both should be `never`.
export type ProviderRuntimeEventV2 = {
[TType in CanonicalRuntimeEventTypeFromPayloadMap]: RuntimeEventBase<
TType,
RuntimeEventPayloadByType[TType]
>;
}[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) |
Lines 1–500 of 564. Load more
# 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 type RuntimeEventRaw = { source: | "codex.app-server.notification" | "codex.app-server.request" | "codex.eventmsg" | "claude.sdk.message" | "claude.sdk.permission" | "codex.sdk.thread-event" | "cursor.acp.notification" | "cursor.acp.request" | "cursor.acp.response"; method?: string; messageType?: string; payload: unknown; }; // Add these to `packages/contracts/src/baseSchemas.ts` for V2. export const RuntimeItemId = makeEntityId("RuntimeItemId"); export type RuntimeItemId = typeof RuntimeItemId.Type; export const RuntimeRequestId = makeEntityId("RuntimeRequestId"); export type RuntimeRequestId = typeof RuntimeRequestId.Type; export const RuntimeTaskId = makeEntityId("RuntimeTaskId"); export type RuntimeTaskId = typeof RuntimeTaskId.Type; export const RuntimeSessionId = makeEntityId("RuntimeSessionId"); export type RuntimeSessionId = typeof RuntimeSessionId.Type; type RuntimeEventBase<TType extends CanonicalRuntimeEventType, TPayload> = { eventId: EventId; provider: ProviderKind; sessionId: RuntimeSessionId; createdAt: IsoDateTime; // Canonical T3 IDs (not provider-native IDs). threadId?: ThreadId; turnId?: TurnId; itemId?: RuntimeItemId; requestId?: RuntimeRequestId; // Provider-native IDs preserved for correlation/debugging. providerRefs?: { providerSessionId?: ProviderSessionId; providerThreadId?: ProviderThreadId; providerTurnId?: ProviderTurnId; providerItemId?: ProviderItemId; providerRequestId?: TrimmedNonEmptyString; }; type: TType; payload: TPayload; raw?: RuntimeEventRaw; }; ``` ### 1.2 Canonical Event Families ```ts type CanonicalRuntimeEventType = // lifecycle | "session.started" | "session.configured" | "session.state.changed" | "session.exited" | "thread.started" | "thread.state.changed" | "thread.metadata.updated" | "thread.token-usage.updated" | "thread.realtime.started" | "thread.realtime.item-added" | "thread.realtime.audio.delta" | "thread.realtime.error" | "thread.realtime.closed" | "turn.started" | "turn.completed" | "turn.aborted" | "turn.plan.updated" | "turn.diff.updated" // item + content stream | "item.started" | "item.updated" | "item.completed" | "content.delta" // approvals / input / tool-protocol requests | "request.opened" | "request.resolved" | "user-input.requested" | "user-input.resolved" // provider/system telemetry | "task.started" | "task.progress" | "task.completed" | "hook.started" | "hook.progress" | "hook.completed" | "tool.progress" | "tool.summary" | "auth.status" | "account.updated" | "account.rate-limits.updated" | "mcp.status.updated" | "mcp.oauth.completed" | "model.rerouted" | "config.warning" | "deprecation.notice" | "files.persisted" | "runtime.warning" | "runtime.error"; ``` ### 1.3 Canonical Item Type ```ts type CanonicalItemType = | "user_message" | "assistant_message" | "reasoning" | "plan" | "command_execution" | "file_change" | "mcp_tool_call" | "dynamic_tool_call" | "collab_agent_tool_call" | "web_search" | "image_view" | "review_entered" | "review_exited" | "context_compaction" | "error" | "unknown"; ``` ### 1.4 Canonical Request Type ```ts type CanonicalRequestType = | "command_execution_approval" | "file_change_approval" | "apply_patch_approval" | "exec_command_approval" | "tool_user_input" | "dynamic_tool_call" | "auth_tokens_refresh" | "unknown"; ``` ### 1.5 Discriminated Union (Type-Safe) ```ts type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error"; type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error"; type TurnState = "completed" | "failed" | "interrupted" | "cancelled"; type ContentStreamKind = | "assistant_text" | "reasoning_text" | "reasoning_summary_text" | "plan_text" | "command_output" | "file_change_output" | "unknown"; type ItemLifecyclePayload = { itemType: CanonicalItemType; status?: "inProgress" | "completed" | "failed" | "declined"; title?: string; detail?: string; data?: unknown; }; type RuntimeEventPayloadByType = { "session.started": { message?: string; resume?: unknown }; "session.configured": { config: Record<string, unknown> }; "session.state.changed": { state: SessionState; reason?: string; detail?: string }; "session.exited": { reason?: string; recoverable?: boolean; exitKind?: "graceful" | "error" }; "thread.started": { providerThreadId?: ProviderThreadId }; "thread.state.changed": { state: ThreadState; detail?: unknown }; "thread.metadata.updated": { name?: string; metadata?: Record<string, unknown> }; "thread.token-usage.updated": { usage: unknown }; "thread.realtime.started": { realtimeSessionId?: string }; "thread.realtime.item-added": { item: unknown }; "thread.realtime.audio.delta": { audio: unknown }; "thread.realtime.error": { message: string }; "thread.realtime.closed": { reason?: string }; "turn.started": { model?: string; effort?: string }; "turn.completed": { state: TurnState; stopReason?: string | null; usage?: unknown; modelUsage?: Record<string, unknown>; totalCostUsd?: number; errorMessage?: string; }; "turn.aborted": { reason: string }; "turn.plan.updated": { explanation?: string | null; plan: Array<{ step: string; status: "pending" | "inProgress" | "completed" }>; }; "turn.diff.updated": { unifiedDiff: string }; "item.started": ItemLifecyclePayload; "item.updated": ItemLifecyclePayload; "item.completed": ItemLifecyclePayload; "content.delta": { streamKind: ContentStreamKind; delta: string; contentIndex?: number; summaryIndex?: number; }; "request.opened": { requestType: CanonicalRequestType; detail?: string; args?: unknown; }; "request.resolved": { requestType: CanonicalRequestType; decision?: string; resolution?: unknown; }; "user-input.requested": { questions: unknown }; "user-input.resolved": { answers: unknown }; "task.started": { taskId: RuntimeTaskId; description?: string; taskType?: string }; "task.progress": { taskId: RuntimeTaskId; description: string; usage?: unknown; lastToolName?: string; }; "task.completed": { taskId: RuntimeTaskId; status: "completed" | "failed" | "stopped"; summary?: string; usage?: unknown; }; "hook.started": { hookId: string; hookName: string; hookEvent: string }; "hook.progress": { hookId: string; output?: string; stdout?: string; stderr?: string }; "hook.completed": { hookId: string; outcome: "success" | "error" | "cancelled"; output?: string; stdout?: string; stderr?: string; exitCode?: number; }; "tool.progress": { toolUseId?: string; toolName?: string; summary?: string; elapsedSeconds?: number; }; "tool.summary": { summary: string; precedingToolUseIds?: string[] }; "auth.status": { isAuthenticating?: boolean; output?: string[]; error?: string }; "account.updated": { account: unknown }; "account.rate-limits.updated": { rateLimits: unknown }; "mcp.status.updated": { status: unknown }; "mcp.oauth.completed": { success: boolean; name?: string; error?: string }; "model.rerouted": { fromModel: string; toModel: string; reason: string }; "config.warning": { summary: string; details?: string; path?: string; range?: unknown }; "deprecation.notice": { summary: string; details?: string }; "files.persisted": { files: Array<{ filename: string; fileId: string }>; failed?: Array<{ filename: string; error: string }>; }; "runtime.warning": { message: string; detail?: unknown }; "runtime.error": { message: string; class?: | "provider_error" | "transport_error" | "permission_error" | "validation_error" | "unknown"; detail?: unknown; }; }; type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType; type MissingPayloadTypes = Exclude< CanonicalRuntimeEventType, CanonicalRuntimeEventTypeFromPayloadMap >; type ExtraPayloadTypes = Exclude< CanonicalRuntimeEventTypeFromPayloadMap, CanonicalRuntimeEventType >; // Both should be `never`. export type ProviderRuntimeEventV2 = { [TType in CanonicalRuntimeEventTypeFromPayloadMap]: RuntimeEventBase< TType, RuntimeEventPayloadByType[TType] >; }[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 | 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.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 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`) |