authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 11:28:54-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log6a10e97a97f768137f7978375e4cfa13211b063e
treea02efac86b25fd9c039d1958b37f0e3c96d3dc65
parent1542b00db21f7586354adcc2ffd4f1e2a20bd02b

Align Claude permissions and provider-specific reasoning effort

- Default Claude sessions to bypass permissions when approval policy is `never`, while preserving explicit `permissionMode` - Hide/send reasoning effort only for providers that support it in ChatView - Add coverage for Claude permission-mode derivation and precedence, and update runtime event model docs

6 files changed, 608 insertions(+), 10 deletions(-)

EVENTS.md created+520
...@@ -0,0 +1,520 @@
1# EVENTS.md
2
3## Goal
4
5Define 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
11This 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
20type 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.
34export const RuntimeItemId = makeEntityId("RuntimeItemId");
35export type RuntimeItemId = typeof RuntimeItemId.Type;
36export const RuntimeRequestId = makeEntityId("RuntimeRequestId");
37export type RuntimeRequestId = typeof RuntimeRequestId.Type;
38export const RuntimeTaskId = makeEntityId("RuntimeTaskId");
39export type RuntimeTaskId = typeof RuntimeTaskId.Type;
40export const RuntimeSessionId = makeEntityId("RuntimeSessionId");
41export type RuntimeSessionId = typeof RuntimeSessionId.Type;
42
43type 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
73type 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
131type 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
153type 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
167type SessionState = "starting" | "ready" | "running" | "waiting" | "stopped" | "error";
168type ThreadState = "active" | "idle" | "archived" | "closed" | "compacted" | "error";
169type TurnState = "completed" | "failed" | "interrupted" | "cancelled";
170
171type ContentStreamKind =
172 | "assistant_text"
173 | "reasoning_text"
174 | "reasoning_summary_text"
175 | "plan_text"
176 | "command_output"
177 | "file_change_output"
178 | "unknown";
179
180type ItemLifecyclePayload = {
181 itemType: CanonicalItemType;
182 status?: "inProgress" | "completed" | "failed" | "declined";
183 title?: string;
184 detail?: string;
185 data?: unknown;
186};
187
188type 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
298type CanonicalRuntimeEventTypeFromPayloadMap = keyof RuntimeEventPayloadByType;
299type MissingPayloadTypes = Exclude<
300 CanonicalRuntimeEventType,
301 CanonicalRuntimeEventTypeFromPayloadMap
302>;
303type ExtraPayloadTypes = Exclude<
304 CanonicalRuntimeEventTypeFromPayloadMap,
305 CanonicalRuntimeEventType
306>;
307// Both should be `never`.
308
309export 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
415EventMsg 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
482Codex 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
5051. Emit one canonical event per source message minimum; emit additional derived events when needed (for example `runtime.error` + `turn.completed`).
5062. Always attach `raw` payload so provider-specific detail is never lost.
5073. Replace current Codex adapter heuristic mapping (`tool.*`, `approval.*`, `message.*`) with method/item-driven mapping from app-server schemas.
5084. Extend Claude adapter to emit non-chat telemetry (`system:*`, `task_*`, `hook_*`, `tool_progress`, `auth_status`, `files_persisted`) into canonical events.
5095. 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)
5126. 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.
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+42
...@@ -182,6 +182,48 @@ describe("ClaudeCodeAdapterLive", () => {...@@ -182,6 +182,48 @@ describe("ClaudeCodeAdapterLive", () => {
182 );182 );
183 });183 });
184184
185 it.effect("derives bypass permission mode from full-access runtime policy", () => {
186 const harness = makeHarness();
187 return Effect.gen(function* () {
188 const adapter = yield* ClaudeCodeAdapter;
189 yield* adapter.startSession({
190 provider: "claudeCode",
191 approvalPolicy: "never",
192 sandboxMode: "danger-full-access",
193 });
194
195 const createInput = harness.getLastCreateQueryInput();
196 assert.equal(createInput?.options.permissionMode, "bypassPermissions");
197 assert.equal(createInput?.options.allowDangerouslySkipPermissions, true);
198 }).pipe(
199 Effect.provideService(Random.Random, makeDeterministicRandomService()),
200 Effect.provide(harness.layer),
201 );
202 });
203
204 it.effect("keeps explicit claude permission mode over runtime-derived defaults", () => {
205 const harness = makeHarness();
206 return Effect.gen(function* () {
207 const adapter = yield* ClaudeCodeAdapter;
208 yield* adapter.startSession({
209 provider: "claudeCode",
210 approvalPolicy: "never",
211 providerOptions: {
212 claudeCode: {
213 permissionMode: "plan",
214 },
215 },
216 });
217
218 const createInput = harness.getLastCreateQueryInput();
219 assert.equal(createInput?.options.permissionMode, "plan");
220 assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined);
221 }).pipe(
222 Effect.provideService(Random.Random, makeDeterministicRandomService()),
223 Effect.provide(harness.layer),
224 );
225 });
226
185 it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => {227 it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => {
186 const harness = makeHarness();228 const harness = makeHarness();
187 return Effect.gen(function* () {229 return Effect.gen(function* () {
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+3-1
...@@ -1036,7 +1036,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {...@@ -1036,7 +1036,9 @@ function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
1036 );1036 );
10371037
1038 const providerOptions = input.providerOptions?.claudeCode;1038 const providerOptions = input.providerOptions?.claudeCode;
1039 const permissionMode = toPermissionMode(providerOptions?.permissionMode);1039 const permissionMode =
1040 toPermissionMode(providerOptions?.permissionMode) ??
1041 (input.approvalPolicy === "never" ? "bypassPermissions" : undefined);
10401042
1041 const queryOptions: ClaudeQueryOptions = {1043 const queryOptions: ClaudeQueryOptions = {
1042 ...(input.cwd ? { cwd: input.cwd } : {}),1044 ...(input.cwd ? { cwd: input.cwd } : {}),
apps/web/src/components/ChatView.tsx+17-7
...@@ -8,12 +8,12 @@ import {...@@ -8,12 +8,12 @@ import {
8 type MessageId,8 type MessageId,
9 getDefaultModel,9 getDefaultModel,
10 getModelOptions,10 getModelOptions,
11 getReasoningOptions,
11 type ProjectId,12 type ProjectId,
12 type ProjectEntry,13 type ProjectEntry,
13 type ProjectScript,14 type ProjectScript,
14 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,15 PROVIDER_SEND_TURN_MAX_ATTACHMENTS,
15 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,16 PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,
16 REASONING_OPTIONS,
17 type ReasoningEffort,17 type ReasoningEffort,
18 type ResolvedKeybindingsConfig,18 type ResolvedKeybindingsConfig,
19 type ProviderApprovalDecision,19 type ProviderApprovalDecision,
...@@ -659,6 +659,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -659,6 +659,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
659 );659 );
660 const selectedModel = resolveModelSlugForProvider(selectedProvider, composerDraft.model ?? baseThreadModel);660 const selectedModel = resolveModelSlugForProvider(selectedProvider, composerDraft.model ?? baseThreadModel);
661 const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING;661 const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING;
662 const reasoningOptions = getReasoningOptions(selectedProvider);
663 const supportsReasoningEffort = reasoningOptions.length > 0;
662 const searchableModelOptions = useMemo(664 const searchableModelOptions = useMemo(
663 () =>665 () =>
664 PROVIDER_OPTIONS.filter((option) => option.available).flatMap((option) =>666 PROVIDER_OPTIONS.filter((option) => option.available).flatMap((option) =>
...@@ -2168,7 +2170,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2168,7 +2170,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
2168 },2170 },
2169 model: selectedModel || undefined,2171 model: selectedModel || undefined,
2170 provider: selectedProvider,2172 provider: selectedProvider,
2171 effort: selectedEffort || undefined,2173 ...(supportsReasoningEffort ? { effort: selectedEffort } : {}),
2172 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",2174 assistantDeliveryMode: settings.enableAssistantStreaming ? "streaming" : "buffered",
2173 approvalPolicy,2175 approvalPolicy,
2174 sandboxMode,2176 sandboxMode,
...@@ -2722,10 +2724,17 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2722,10 +2724,17 @@ export default function ChatView({ threadId }: ChatViewProps) {
2722 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />2724 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
27232725
2724 {/* Reasoning effort */}2726 {/* Reasoning effort */}
2725 <ReasoningEffortPicker effort={selectedEffort} onEffortChange={onEffortSelect} />2727 {supportsReasoningEffort ? (
27262728 <>
2727 {/* Divider */}2729 <ReasoningEffortPicker
2728 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />2730 effort={selectedEffort}
2731 options={reasoningOptions}
2732 onEffortChange={onEffortSelect}
2733 />
2734 {/* Divider */}
2735 <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />
2736 </>
2737 ) : null}
27292738
2730 {/* Runtime mode toggle */}2739 {/* Runtime mode toggle */}
2731 <Button2740 <Button
...@@ -3945,6 +3954,7 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {...@@ -3945,6 +3954,7 @@ const ProviderModelPicker = memo(function ProviderModelPicker(props: {
39453954
3946const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: {3955const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: {
3947 effort: ReasoningEffort;3956 effort: ReasoningEffort;
3957 options: ReadonlyArray<ReasoningEffort>;
3948 onEffortChange: (effort: ReasoningEffort) => void;3958 onEffortChange: (effort: ReasoningEffort) => void;
3949}) {3959}) {
3950 return (3960 return (
...@@ -3956,7 +3966,7 @@ const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: {...@@ -3956,7 +3966,7 @@ const ReasoningEffortPicker = memo(function ReasoningEffortPicker(props: {
3956 <SelectValue />3966 <SelectValue />
3957 </SelectTrigger>3967 </SelectTrigger>
3958 <SelectPopup alignItemWithTrigger={false}>3968 <SelectPopup alignItemWithTrigger={false}>
3959 {REASONING_OPTIONS.map((effort) => (3969 {props.options.map((effort) => (
3960 <SelectItem key={effort} value={effort}>3970 <SelectItem key={effort} value={effort}>
3961 {effort}3971 {effort}
3962 {effort === DEFAULT_REASONING ? " (default)" : ""}3972 {effort === DEFAULT_REASONING ? " (default)" : ""}
packages/contracts/src/model.test.ts+12
...@@ -7,7 +7,9 @@ import {...@@ -7,7 +7,9 @@ import {
7 MODEL_OPTIONS_BY_PROVIDER,7 MODEL_OPTIONS_BY_PROVIDER,
8 getDefaultModel,8 getDefaultModel,
9 getModelOptions,9 getModelOptions,
10 getReasoningOptions,
10 normalizeModelSlug,11 normalizeModelSlug,
12 REASONING_OPTIONS,
11 resolveModelSlug,13 resolveModelSlug,
12 resolveModelSlugForProvider,14 resolveModelSlugForProvider,
13} from "./model";15} from "./model";
...@@ -72,3 +74,13 @@ describe("resolveModelSlug", () => {...@@ -72,3 +74,13 @@ describe("resolveModelSlug", () => {
72 expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode);74 expect(getModelOptions("claudeCode")).toEqual(MODEL_OPTIONS_BY_PROVIDER.claudeCode);
73 });75 });
74});76});
77
78describe("getReasoningOptions", () => {
79 it("returns codex reasoning options for codex", () => {
80 expect(getReasoningOptions("codex")).toEqual(REASONING_OPTIONS);
81 });
82
83 it("returns no reasoning options for claudeCode", () => {
84 expect(getReasoningOptions("claudeCode")).toEqual([]);
85 });
86});
packages/contracts/src/model.ts+14-2
...@@ -103,6 +103,18 @@ export function resolveModelSlugForProvider(...@@ -103,6 +103,18 @@ export function resolveModelSlugForProvider(
103 return resolveModelSlug(model, provider);103 return resolveModelSlug(model, provider);
104}104}
105105
106export const REASONING_OPTIONS = ["xhigh", "high", "medium", "low"] as const;106export const REASONING_OPTIONS_BY_PROVIDER = {
107export type ReasoningEffort = (typeof REASONING_OPTIONS)[number];107 codex: ["xhigh", "high", "medium", "low"],
108 claudeCode: [],
109} as const satisfies Record<ProviderKind, readonly string[]>;
110
111// Backward compatibility for existing Codex-only call sites.
112export const REASONING_OPTIONS = REASONING_OPTIONS_BY_PROVIDER.codex;
113export type ReasoningEffort = (typeof REASONING_OPTIONS_BY_PROVIDER.codex)[number];
108export const DEFAULT_REASONING: ReasoningEffort = "high";114export const DEFAULT_REASONING: ReasoningEffort = "high";
115
116export function getReasoningOptions(
117 provider: ProviderKind = "codex",
118): ReadonlyArray<ReasoningEffort> {
119 return REASONING_OPTIONS_BY_PROVIDER[provider] as ReadonlyArray<ReasoningEffort>;
120}