authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 17:57:39-08:00
committergravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-26 18:42:13-08:00
log10c1cae6abb3c87887b3d4a9cc6d2d4788145bd2
tree3b054dc8d3e1f1017e864a925546399a7b81cc7e
parent87854c288b8a6a21fc8f516ba6b0e893ef3e87e6

claude plan


1 files changed, 546 insertions(+), 0 deletions(-)

.plans/17-claude-code.md created+546
...@@ -0,0 +1,546 @@
1# Plan: Claude Code Integration (Orchestration Architecture)
2
3## Why this plan was rewritten
4
5The previous plan targeted a pre-orchestration architecture (`ProviderManager`, provider-native WS event methods, and direct provider UI wiring). The current app now routes everything through:
6
71. `orchestration.dispatchCommand` (client intent)
82. `OrchestrationEngine` (decide + persist + publish domain events)
93. `ProviderCommandReactor` (domain intent -> `ProviderService`)
104. `ProviderService` (adapter routing + canonical runtime stream)
115. `ProviderRuntimeIngestion` (provider runtime -> internal orchestration commands)
126. `orchestration.domainEvent` (single push channel consumed by web)
13
14Claude integration must plug into this path instead of reintroducing legacy provider-specific flows.
15
16---
17
18## Current constraints to design around
19
201. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads.
212. `ProviderService.startSession` currently defaults to `provider: "codex"` unless explicitly provided.
223. `thread.turn.start` has no provider field today, so first-turn provider selection cannot be explicitly requested.
234. `ProviderService` requires adapter `startSession()` to return a `ProviderSession` with `threadId`.
245. Checkpoint revert currently calls `providerService.rollbackConversation()`, so Claude adapter needs a rollback strategy compatible with current reactor behavior.
256. Web currently marks Claude as unavailable (`"Claude Code (soon)"`) and model picker is Codex-only.
26
27---
28
29## Architecture target
30
31Add Claude as a first-class provider adapter that emits canonical runtime events and works with existing orchestration reactors without adding new WS channels or bypass paths.
32
33Key decisions:
34
351. Keep orchestration provider-agnostic; adapt Claude inside adapter/layer boundaries.
362. Use the existing canonical runtime stream (`ProviderRuntimeEvent`) as the only ingestion contract.
373. Keep provider session routing in `ProviderService` and `ProviderSessionDirectory`.
384. Add explicit provider selection to turn-start intent so first turn can start Claude session intentionally.
39
40---
41
42## Phase 1: Contracts and command shape updates
43
44### 1.1 Provider-aware model contract
45
46Update `packages/contracts/src/model.ts` so model resolution can be provider-aware instead of Codex-only.
47
48Expected outcomes:
49
501. Introduce provider-scoped model lists (Codex + Claude).
512. Add helpers that resolve model by provider.
523. Preserve backwards compatibility for existing Codex defaults.
53
54### 1.2 Turn-start provider intent
55
56Update `packages/contracts/src/orchestration.ts`:
57
581. Add optional `provider: ProviderKind` to `ThreadTurnStartCommand`.
592. Carry provider through `ThreadTurnStartRequestedPayload`.
603. Keep existing command valid when provider is omitted.
61
62This removes the implicit “Codex unless session already exists” behavior as the only path.
63
64### 1.3 Provider session start input for Claude runtime knobs
65
66Update `packages/contracts/src/provider.ts`:
67
681. Extend `ProviderSessionStartInput` with optional Claude-specific fields (for example `claudeBinaryPath`, `permissionMode`, `maxThinkingTokens`).
692. Keep fields optional so current call sites remain valid.
703. Continue using generic `resumeThreadId` + `resumeCursor` as the cross-provider recovery mechanism.
71
72### 1.4 Contract tests
73
74Update/add tests in `packages/contracts/src/*.test.ts` for:
75
761. New command payload shape.
772. Provider-aware model resolution behavior.
783. Backward compatibility of existing command/schema decoding.
79
80---
81
82## Phase 2: Claude adapter implementation
83
84### 2.1 Add adapter service + layer
85
86Create:
87
881. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts`
892. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`
90
91Adapter must implement `ProviderAdapterShape<ProviderAdapterError>`.
92
93### 2.1.a SDK dependency and baseline config
94
95Add server dependency:
96
971. `@anthropic-ai/claude-agent-sdk`
98
99Baseline adapter options to support from day one:
100
1011. `cwd`
1022. `model`
1033. `pathToClaudeCodeExecutable` (from `claudeBinaryPath`)
1044. `permissionMode`
1055. `maxThinkingTokens`
1066. `resume`
1077. `resumeSessionAt`
1088. `includePartialMessages`
1099. `canUseTool`
11010. `hooks`
11111. `env` and `additionalDirectories` (if needed for sandbox/workspace parity)
112
113### 2.2 Claude runtime bridge
114
115Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle.
116
117Required capabilities:
118
1191. Long-lived session context per adapter session.
1202. Multi-turn input queue.
1213. Interrupt support.
1224. Approval request/response bridge.
1235. Resume support via `resumeThreadId` / `resumeCursor`.
124
125#### 2.2.a Agent SDK details to preserve
126
127The adapter should explicitly rely on these SDK capabilities:
128
1291. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers).
1302. Multi-turn input is supported via async-iterable prompt input.
1313. Tool approval decisions are provided via `canUseTool`.
1324. Resume support uses `resume` and optional `resumeSessionAt`.
1335. Hooks can be used for lifecycle signals (`Stop`, `PostToolUse`, etc.) when we need adapter-originated checkpoint/runtime events.
134
135#### 2.2.b Effect-native session lifecycle skeleton
136
137```ts
138import { query } from "@anthropic-ai/claude-agent-sdk";
139import { Effect } from "effect";
140
141const acquireSession = (input: ProviderSessionStartInput) =>
142 Effect.acquireRelease(
143 Effect.tryPromise({
144 try: async () => {
145 const abortController = new AbortController();
146 const result = query({
147 prompt: makePromptAsyncIterable(input.sessionId),
148 options: {
149 cwd: input.cwd,
150 model: input.model,
151 permissionMode: input.permissionMode,
152 maxThinkingTokens: input.maxThinkingTokens,
153 pathToClaudeCodeExecutable: input.claudeBinaryPath,
154 resume: input.resumeThreadId,
155 resumeSessionAt: readResumeCursor(input.resumeCursor),
156 signal: abortController.signal,
157 includePartialMessages: true,
158 canUseTool: makeCanUseTool(input.sessionId),
159 hooks: makeClaudeHooks(input.sessionId),
160 },
161 });
162 return { abortController, result };
163 },
164 catch: (cause) =>
165 new ProviderAdapterProcessError({
166 provider: "claudeCode",
167 sessionId: "pending",
168 detail: "Failed to start Claude runtime session.",
169 cause,
170 }),
171 }),
172 ({ abortController }) => Effect.sync(() => abortController.abort()),
173 );
174```
175
176#### 2.2.c AsyncIterable -> Effect Stream integration
177
178Preferred when available in the pinned Effect version:
179
180```ts
181const sdkMessageStream = Stream.fromAsyncIterable(
182 session.result,
183 (cause) =>
184 new ProviderAdapterProcessError({
185 provider: "claudeCode",
186 sessionId,
187 detail: "Claude runtime stream failed.",
188 cause,
189 }),
190);
191```
192
193Portable fallback (already aligned with current server patterns):
194
195```ts
196const queue = yield* Queue.unbounded<ClaudeSdkMessage>();
197yield* Effect.forkScoped(
198 Effect.tryPromise({
199 try: async () => {
200 for await (const message of session.result) {
201 Queue.offerAllUnsafe(queue, [message]);
202 }
203 },
204 catch: (cause) =>
205 new ProviderAdapterProcessError({
206 provider: "claudeCode",
207 sessionId,
208 detail: "Claude runtime stream pump failed.",
209 cause,
210 }),
211 }),
212);
213const sdkMessageStream = Stream.fromQueue(queue);
214```
215
216#### 2.2.d Multi-turn prompt queue pattern (Effect)
217
218Use an Effect queue as the single input boundary:
219
220```ts
221const promptQueue = yield* Queue.unbounded<ClaudePromptEnvelope>();
222
223const prompt: AsyncIterable<ClaudePromptEnvelope> = {
224 [Symbol.asyncIterator]() {
225 return {
226 next: async () => {
227 const item = await Effect.runPromise(Queue.take(promptQueue));
228 if (item.type === "terminate") {
229 return { done: true, value: undefined };
230 }
231 return { done: false, value: item };
232 },
233 };
234 },
235};
236```
237
238`sendTurn()` enqueues a user envelope, while `stopSession()` enqueues terminate and aborts.
239
240#### 2.2.e Approval bridge with `canUseTool`
241
242Map SDK approval checks to existing orchestration approval flows:
243
244```ts
245const canUseTool = async (toolName: string, toolInput: Record<string, unknown>) => {
246 if (approvalModeIsFullAccess(sessionId)) {
247 return { behavior: "allow", updatedInput: toolInput };
248 }
249
250 const requestId = ApprovalRequestId.makeUnsafe(crypto.randomUUID());
251 emitRuntimeEvent({
252 type: "approval.requested",
253 provider: "claudeCode",
254 sessionId,
255 requestId,
256 requestKind: classifyTool(toolName),
257 detail: summarizeToolRequest(toolName, toolInput),
258 // ... eventId/createdAt
259 });
260
261 const decision = await waitForApprovalDecision(requestId);
262 emitRuntimeEvent({
263 type: "approval.resolved",
264 provider: "claudeCode",
265 sessionId,
266 requestId,
267 decision,
268 // ... eventId/createdAt
269 });
270
271 return decision === "accept" || decision === "acceptForSession"
272 ? { behavior: "allow", updatedInput: toolInput }
273 : { behavior: "deny", message: "User declined tool execution." };
274};
275```
276
277#### 2.2.f Hooks and checkpoint signals
278
279If Claude hooks provide cleaner turn boundaries, convert them into canonical runtime events (`turn.completed` and optionally `checkpoint.captured`) so `CheckpointReactor` remains unchanged:
280
281```ts
282hooks: {
283 Stop: [
284 {
285 matcher: {},
286 hooks: [
287 async () => {
288 emitRuntimeEvent({
289 type: "checkpoint.captured",
290 provider: "claudeCode",
291 sessionId,
292 threadId,
293 turnId: activeTurnId,
294 turnCount: nextTurnCount(),
295 // ... eventId/createdAt
296 });
297 },
298 ],
299 },
300 ],
301}
302```
303
304#### 2.2.g Runtime control method wiring
305
306Adapter should expose SDK controls through existing service methods and runtime payload updates:
307
3081. `interruptTurn()` -> `result.interrupt()`
3092. Model override on send turn -> `result.setModel(model)` before enqueuing prompt
3103. Runtime mode changes -> `result.setPermissionMode(mode)` if we support live mode switching
3114. Thinking budget updates -> `result.setMaxThinkingTokens(tokens)` when configured
312
313### 2.3 Canonical event mapping
314
315Map Claude SDK events to `ProviderRuntimeEvent`:
316
3171. `session.started` / `session.exited`
3182. `turn.started` / `turn.completed`
3193. `message.delta` / `message.completed`
3204. `tool.started` / `tool.completed`
3215. `approval.requested` / `approval.resolved`
3226. `runtime.error`
323
324No provider-native event methods should leak beyond the adapter boundary.
325
326Reference mapping table:
327
328| Claude SDK message/callback | Canonical runtime event(s) | Notes |
329|---|---|---|
330| assistant partial text | `message.delta` | Emit deltas only (do not resend full content each chunk). |
331| assistant final message | `message.completed` | Must include stable `itemId` for dedupe/finalize behavior. |
332| result (success) | `turn.completed` (`status: "completed"`) | This is the main turn boundary for checkpointing. |
333| result (interrupted/cancelled) | `turn.completed` (`status: "interrupted"`/`"cancelled"`) | Preserve state semantics used by ingestion + UI. |
334| result (error) | `runtime.error` and `turn.completed` (`status: "failed"`) | Include concise error message. |
335| tool start | `tool.started` | Set `toolKind` using shared classifier (`command`, `file-change`, `other`). |
336| tool result | `tool.completed` | Include detail summary used by activity feed. |
337| canUseTool prompt | `approval.requested` | Source of request id shown in UI. |
338| approval decision returned | `approval.resolved` | Must correlate using same request id. |
339| session initialization | `session.started`, optional `thread.started` | Ensure `threadId` is emitted early and consistently. |
340| process exit/close | `session.exited` | Required for session status projection cleanup. |
341
342### 2.4 Session/thread/resume identifiers
343
344Define explicit adapter semantics:
345
3461. `sessionId`: adapter-owned stable session id.
3472. `threadId`: Claude conversation/session identifier returned as `ProviderThreadId`.
3483. `resumeCursor`: provider-specific cursor (for example message id) needed for precise recovery/rollback.
349
350### 2.5 Rollback/read strategy
351
352Implement `readThread()` and `rollbackThread()` in a way compatible with `CheckpointReactor`.
353
354Preferred:
355
3561. Track per-turn resume cursors and reconstruct session state on rollback.
3572. Keep adapter `sessionId` stable across internal rehydration/restart.
358
359Fallback (explicitly documented if chosen):
360
3611. Filesystem revert succeeds even when conversation rewind is partial.
3622. Reactor behavior is updated to surface a clear warning activity instead of silent drift.
363
364### 2.6 Adapter unit tests
365
366Add tests mirroring Codex adapter coverage:
367
3681. Provider validation (`provider === "claudeCode"`).
3692. Event mapping to canonical runtime events.
3703. Approval request lifecycle.
3714. Resume/recovery behavior.
3725. Rollback behavior.
373
374---
375
376## Phase 3: Register adapter in runtime composition
377
378### 3.1 Adapter registry wiring
379
380Update:
381
3821. `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts`
383
384Register both Codex and Claude adapters.
385
386### 3.2 Server layer composition
387
388Update:
389
3901. `apps/server/src/serverLayers.ts`
391
392Provide `ClaudeCodeAdapterLive` alongside Codex in `makeServerProviderLayer()`.
393
394### 3.3 Composition tests
395
396Update integration tests to ensure:
397
3981. Registry exposes Claude provider.
3992. Provider service can route Claude sessions.
400
401---
402
403## Phase 4: Orchestration command/reactor updates
404
405### 4.1 Decider propagation
406
407Update `apps/server/src/orchestration/decider.ts`:
408
4091. Carry optional `provider` from `thread.turn.start` command into `thread.turn-start-requested` event payload.
410
411### 4.2 ProviderCommandReactor provider selection
412
413Update `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`:
414
4151. Prefer provider from turn-start event payload when starting a new session.
4162. Fallback to existing thread session provider when payload omitted.
4173. Fallback to default provider only when neither is present.
418
419Switch behavior policy (explicit in implementation):
420
4211. If active session provider differs from requested provider, stop and recreate session before sending turn.
4222. Keep current provider when request omits provider.
423
424### 4.3 Reactor/invariant tests
425
426Update/add tests in:
427
4281. `apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts`
4292. `apps/server/src/orchestration/*.test.ts` as needed
430
431Validate first-turn provider choice, provider switching semantics, and backward compatibility when provider is omitted.
432
433---
434
435## Phase 5: Web provider UX enablement
436
437### 5.1 Enable Claude option
438
439Update `apps/web/src/session-logic.ts`:
440
4411. Mark Claude provider option as available.
442
443### 5.2 Provider selection in composer
444
445Update `apps/web/src/components/ChatView.tsx`:
446
4471. Add provider picker near model/runtime controls.
4482. Include selected provider in `thread.turn.start` command payload.
4493. Keep selected provider sticky per thread in client state (or derive from active session provider when present).
450
451### 5.3 Provider-aware model picker
452
453Update model picker usage so model options reflect selected provider.
454
455### 5.4 Web tests
456
457Update/add tests for:
458
4591. Provider picker interactions.
4602. Turn-start command includes provider.
4613. Model list switches by provider.
462
463---
464
465## Phase 6: Checkpoint and revert compatibility
466
467### 6.1 Validate checkpoint reactor expectations
468
469Exercise `apps/server/src/orchestration/Layers/CheckpointReactor.ts` flows with Claude:
470
4711. Turn baseline capture.
4722. Turn completion checkpoint capture.
4733. Revert + provider rollback coordination.
474
475### 6.2 Policy for partial rollback support
476
477If Claude cannot support full conversation rewind at parity with Codex:
478
4791. Document behavior clearly.
4802. Emit explicit thread activity for degraded rollback behavior.
4813. Ensure filesystem state remains correct and predictable.
482
483### 6.3 Checkpoint tests
484
485Add/update tests in checkpoint + provider service integration suites for Claude revert behavior.
486
487---
488
489## Phase 7: End-to-end hardening and observability
490
491### 7.1 Integration tests
492
493Add integration coverage for:
494
4951. `thread.turn.start` with `provider: "claudeCode"` from fresh thread.
4962. Session recovery after restart (provider session alias/recovery path).
4973. Approval request/respond flow.
4984. Interrupt behavior.
4995. Revert flow with rollback.
500
501### 7.2 WS behavior regression check
502
503Ensure no contract regressions:
504
5051. Client still consumes only `orchestration.domainEvent`.
5062. No new provider-specific WS channels introduced.
507
508### 7.3 Logging
509
510Confirm both native and canonical provider logs remain useful with multi-adapter setup:
511
5121. `provider-native.ndjson`
5132. `provider-canonical.ndjson`
514
515---
516
517## File checklist
518
519Likely files to touch:
520
5211. `packages/contracts/src/model.ts`
5222. `packages/contracts/src/orchestration.ts`
5233. `packages/contracts/src/provider.ts`
5244. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts` (new)
5255. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts` (new)
5266. `apps/server/src/provider/Layers/ProviderAdapterRegistry.ts`
5277. `apps/server/src/serverLayers.ts`
5288. `apps/server/src/orchestration/decider.ts`
5299. `apps/server/src/orchestration/Layers/ProviderCommandReactor.ts`
53010. `apps/web/src/session-logic.ts`
53111. `apps/web/src/components/ChatView.tsx`
53212. Related tests under `packages/contracts/src`, `apps/server/src/provider/Layers`, `apps/server/src/orchestration/Layers`, `apps/server/integration`, and `apps/web/src`.
533
534---
535
536## Delivery order
537
5381. Contracts for provider selection + models.
5392. Claude adapter + unit tests.
5403. Registry/layer wiring.
5414. Reactor updates for provider-aware session start.
5425. Web provider picker + provider-aware models.
5436. Checkpoint/revert compatibility.
5447. End-to-end integration tests and stabilization.
545
546This order keeps risk isolated and maintains a working orchestrated path at each stage.