History

split: restore Claude SDK manifest and plan doc

Julius Marminge committed 8 months ago

Commit 480a8cf1 parent fa60ec42 files · +442 −0Browse files

Unified · Split

2 files
  1. .plans/17-claude-code.md +441 −0
  2. apps/server/package.json +1 −0
.plans/17-claude-code.md+441−0
@@ -0,0 +1,441 @@
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 (post-Stage 1)
19
201. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads.
212. Start input now uses typed `providerOptions` and generic `resumeCursor`; top-level provider-specific fields were removed.
223. `resumeCursor` is intentionally opaque outside adapters and must never be synthesized from `providerThreadId`.
234. `ProviderService` still 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 (completed)
65
66Update `packages/contracts/src/provider.ts`:
67
681. Move provider-specific start fields into typed `providerOptions`:
69 - `providerOptions.codex`
70 - `providerOptions.claudeCode`
712. Keep `resumeCursor` as the single cross-provider resume input in `ProviderSessionStartInput`.
723. Deprecate/remove `resumeThreadId` from the generic start contract.
734. Treat `resumeCursor` as adapter-owned opaque state.
74
75### 1.4 Contract tests (completed)
76
77Update/add tests in `packages/contracts/src/*.test.ts` for:
78
791. New command payload shape.
802. Provider-aware model resolution behavior.
813. Breaking-change expectations for removed top-level provider fields.
82
83---
84
85## Phase 2: Claude adapter implementation
86
87### 2.1 Add adapter service + layer
88
89Create:
90
911. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts`
922. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`
93
94Adapter must implement `ProviderAdapterShape<ProviderAdapterError>`.
95
96### 2.1.a SDK dependency and baseline config
97
98Add server dependency:
99
1001. `@anthropic-ai/claude-agent-sdk`
101
102Baseline adapter options to support from day one:
103
1041. `cwd`
1052. `model`
1063. `pathToClaudeCodeExecutable` (from `providerOptions.claudeCode.binaryPath`)
1074. `permissionMode` (from `providerOptions.claudeCode.permissionMode`)
1085. `maxThinkingTokens` (from `providerOptions.claudeCode.maxThinkingTokens`)
1096. `resume`
1107. `resumeSessionAt`
1118. `includePartialMessages`
1129. `canUseTool`
11310. `hooks`
11411. `env` and `additionalDirectories` (if needed for sandbox/workspace parity)
115
116### 2.2 Claude runtime bridge
117
118Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle.
119
120Required capabilities:
121
1221. Long-lived session context per adapter session.
1232. Multi-turn input queue.
1243. Interrupt support.
1254. Approval request/response bridge.
1265. Resume support via opaque `resumeCursor` (parsed inside Claude adapter only).
127
128#### 2.2.a Agent SDK details to preserve
129
130The adapter should explicitly rely on these SDK capabilities:
131
1321. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers).
1332. Multi-turn input is supported via async-iterable prompt input.
1343. Tool approval decisions are provided via `canUseTool`.
1354. Resume support uses `resume` and optional `resumeSessionAt`, both derived by parsing adapter-owned `resumeCursor`.
1365. Hooks can be used for lifecycle signals (`Stop`, `PostToolUse`, etc.) when we need adapter-originated checkpoint/runtime events.
137
138#### 2.2.b Effect-native session lifecycle skeleton
139
140```ts
141import { query } from "@anthropic-ai/claude-agent-sdk";
142import { Effect } from "effect";
143
144const acquireSession = (input: ProviderSessionStartInput) =>
145 Effect.acquireRelease(
146 Effect.tryPromise({
147 try: async () => {
148 const claudeOptions = input.providerOptions?.claudeCode;
149 const resumeState = readClaudeResumeState(input.resumeCursor);
150 const abortController = new AbortController();
151 const result = query({
152 prompt: makePromptAsyncIterable(),
153 options: {
154 cwd: input.cwd,
155 model: input.model,
156 permissionMode: claudeOptions?.permissionMode,
157 maxThinkingTokens: claudeOptions?.maxThinkingTokens,
158 pathToClaudeCodeExecutable: claudeOptions?.binaryPath,
159 resume: resumeState?.threadId,
160 resumeSessionAt: resumeState?.sessionAt,
161 signal: abortController.signal,
162 includePartialMessages: true,
163 canUseTool: makeCanUseTool(),
164 hooks: makeClaudeHooks(),
165 },
166 });
167 return { abortController, result };
168 },
169 catch: (cause) =>
170 new ProviderAdapterProcessError({
171 provider: "claudeCode",
172 sessionId: "pending",
173 detail: "Failed to start Claude runtime session.",
174 cause,
175 }),
176 }),
177 ({ abortController }) => Effect.sync(() => abortController.abort()),
178 );
179```
180
181#### 2.2.c AsyncIterable -> Effect Stream integration
182
183Preferred when available in the pinned Effect version:
184
185```ts
186const sdkMessageStream = Stream.fromAsyncIterable(
187 session.result,
188 (cause) =>
189 new ProviderAdapterProcessError({
190 provider: "claudeCode",
191 sessionId,
192 detail: "Claude runtime stream failed.",
193 cause,
194 }),
195);
196```
197
198Portable fallback (already aligned with current server patterns):
199
200```ts
201const sdkMessageStream = Stream.async<ClaudeSdkMessage, ProviderAdapterProcessError>((emit) => {
202 let cancelled = false;
203 void (async () => {
204 try {
205 for await (const message of session.result) {
206 if (cancelled) break;
207 emit.single(message);
208 }
209 emit.end();
210 } catch (cause) {
211 emit.fail(
212 new ProviderAdapterProcessError({
213 provider: "claudeCode",
214 sessionId,
215 detail: "Claude runtime stream failed.",
216 cause,
217 }),
218 );
219 }
220 })();
221 return Effect.sync(() => {
222 cancelled = true;
223 });
224});
225```
226
227### 2.3 Canonical event mapping
228
229Claude adapter must translate Agent SDK output into canonical `ProviderRuntimeEvent`.
230
231Initial mapping target:
232
2331. assistant text deltas -> `content.delta`
2342. final assistant text -> `item.completed` and/or `turn.completed`
2353. approval requests -> `request.opened`
2364. approval results -> `request.resolved`
2375. system lifecycle -> `session.*`, `thread.*`, `turn.*`
2386. errors -> `runtime.error`
2397. plan/proposed-plan content when derivable
240
241Implementation note:
242
2431. Keep raw Claude message on `raw` for debugging.
2442. Prefer canonical item/request kinds over provider-native enums.
2453. If Claude emits extra event kinds we do not model yet, map them to `tool.summary`, `runtime.warning`, or `unknown`-compatible payloads instead of dropping silently.
246
247### 2.4 Resume cursor strategy
248
249Define Claude-owned opaque resume state, e.g.:
250
251```ts
252interface ClaudeResumeCursor {
253 readonly version: 1;
254 readonly threadId?: string;
255 readonly sessionAt?: string;
256}
257```
258
259Rules:
260
2611. Serialize only adapter-owned state into `resumeCursor`.
2622. Parse/validate only inside Claude adapter.
2633. Store updated cursor when Claude runtime yields enough data to resume safely.
2644. Never overload orchestration thread id as Claude thread id.
265
266### 2.5 Interrupt and stop semantics
267
268Map orchestration stop/interrupt expectations onto SDK controls:
269
2701. `interruptTurn()` -> active query interrupt.
2712. `stopSession()` -> close session resources and prevent future sends.
2723. `rollbackThread()` -> see Phase 4.
273
274---
275
276## Phase 3: Provider service and composition
277
278### 3.1 Register Claude adapter
279
280Update provider registry layer to include Claude:
281
2821. add `claudeCode` -> `ClaudeCodeAdapter`
2832. ensure `ProviderService.listProviderStatuses()` reports Claude availability
284
285### 3.2 Persist provider binding
286
287Current `ProviderSessionDirectory` already stores provider/thread binding and opaque `resumeCursor`.
288
289Required validation:
290
2911. Claude bindings survive restart.
2922. resume cursor remains opaque and round-trips untouched.
2933. stopAll + restart can recover Claude sessions when possible.
294
295### 3.3 Provider start routing
296
297Update `ProviderCommandReactor` / orchestration flow:
298
2991. If a thread turn start requests `provider: "claudeCode"`, start Claude if no active session exists.
3002. If a thread already has Claude session binding, reuse it.
3013. If provider switches between Codex and Claude, explicitly stop/rebind before next send.
302
303---
304
305## Phase 4: Checkpoint and revert strategy
306
307Claude does not necessarily expose the same conversation rewind primitive as Codex app-server. Current architecture expects `providerService.rollbackConversation()`.
308
309Pick one explicit strategy:
310
311### Option A: provider-native rewind
312
313If SDK/runtime supports safe rewind:
314
3151. implement in Claude adapter
3162. keep `CheckpointReactor` unchanged
317
318### Option B: session restart + state truncation shim
319
320If no native rewind exists:
321
3221. Claude adapter returns successful rollback by:
323 - stopping current Claude session
324 - clearing/rewriting stored Claude resume cursor to last safe resumable point
325 - forcing next turn to recreate session from persisted orchestration state
3262. Document that rollback is “conversation reset to checkpoint boundary”, not provider-native turn deletion.
327
328Whichever option is chosen:
329
3301. behavior must be deterministic
3312. checkpoint revert tests must pass under orchestration expectations
3323. user-visible activity log should explain failures clearly when provider rollback is impossible
333
334---
335
336## Phase 5: Web integration
337
338### 5.1 Provider picker and model picker
339
340Update web state/UI:
341
3421. allow choosing Claude as thread provider before first turn
3432. show Claude model list from provider-aware model helpers
3443. preserve existing Codex default behavior when provider omitted
345
346Likely touch points:
347
3481. `apps/web/src/store.ts`
3492. `apps/web/src/components/ChatView.tsx`
3503. `apps/web/src/types.ts`
3514. `packages/shared/src/model.ts`
352
353### 5.2 Settings for Claude executable/options
354
355Add app settings if needed for:
356
3571. Claude binary path
3582. default permission mode
3593. default max thinking tokens
360
361Do not hardcode provider-specific config into generic session state if it belongs in app settings or typed `providerOptions`.
362
363### 5.3 Session rendering
364
365No new WS channel should be needed. Claude should appear through existing:
366
3671. thread messages
3682. activities/worklog
3693. approvals
3704. session state
3715. checkpoints/diffs
372
373---
374
375## Phase 6: Testing strategy
376
377### 6.1 Contract tests
378
379Cover:
380
3811. provider-aware model schemas
3822. provider field on turn-start command
3833. provider-specific start options schema
384
385### 6.2 Adapter layer tests
386
387Add `ClaudeCodeAdapter.test.ts` covering:
388
3891. session start
3902. event mapping
3913. approval bridge
3924. resume cursor parse/serialize
3935. interrupt behavior
3946. rollback behavior or explicit unsupported error path
395
396Use SDK-facing layer tests/mocks only at the boundary. Do not mock orchestration business logic in higher-level tests.
397
398### 6.3 Provider service integration tests
399
400Extend provider integration coverage so Claude is exercised through `ProviderService`:
401
4021. start Claude session
4032. send turn
4043. receive canonical runtime events
4054. restart/recover using persisted binding
406
407### 6.4 Orchestration integration tests
408
409Add/extend integration tests around:
410
4111. first-turn provider selection
4122. Claude approval requests routed through orchestration
4133. Claude runtime ingestion -> messages/activities/session updates
4144. checkpoint revert behavior under Claude
4155. stopAll/restart recovery
416
417These should validate real orchestration flows, not just adapter behavior.
418
419---
420
421## Phase 7: Rollout order
422
423Recommended implementation order:
424
4251. contracts/provider-aware models
4262. provider field on turn-start
4273. Claude adapter skeleton + start/send/stream
4284. canonical event mapping
4295. provider registry/service wiring
4306. orchestration recovery + checkpoint strategy
4317. web provider/model picker
4328. full integration tests
433
434---
435
436## Non-goals
437
4381. Reintroducing provider-specific WS methods/channels.
4392. Storing provider-native thread ids as orchestration ids.
4403. Bypassing orchestration engine for Claude-specific UI flows.
4414. Encoding Claude resume semantics outside adapter-owned `resumeCursor`.
apps/server/package.json+1−0
@@ -22,6 +22,7 @@ · 21 unmodified lines
22 "test": "vitest run"22 "test": "vitest run"
23 },23 },
24 "dependencies": {24 "dependencies": {
25 "@anthropic-ai/claude-agent-sdk": "^0.2.62",
25 "@effect/platform-node": "catalog:",26 "@effect/platform-node": "catalog:",
26 "@effect/sql-sqlite-bun": "catalog:",27 "@effect/sql-sqlite-bun": "catalog:",
27 "@pierre/diffs": "^1.1.0-beta.16",28 "@pierre/diffs": "^1.1.0-beta.16",