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+
5+The 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+
7+1. `orchestration.dispatchCommand` (client intent)
8+2. `OrchestrationEngine` (decide + persist + publish domain events)
9+3. `ProviderCommandReactor` (domain intent -> `ProviderService`)
10+4. `ProviderService` (adapter routing + canonical runtime stream)
11+5. `ProviderRuntimeIngestion` (provider runtime -> internal orchestration commands)
12+6. `orchestration.domainEvent` (single push channel consumed by web)
13+
14+Claude 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+
20+1. Provider runtime ingestion expects canonical `ProviderRuntimeEvent` shapes, not provider-native payloads.
21+2. Start input now uses typed `providerOptions` and generic `resumeCursor`; top-level provider-specific fields were removed.
22+3. `resumeCursor` is intentionally opaque outside adapters and must never be synthesized from `providerThreadId`.
23+4. `ProviderService` still requires adapter `startSession()` to return a `ProviderSession` with `threadId`.
24+5. Checkpoint revert currently calls `providerService.rollbackConversation()`, so Claude adapter needs a rollback strategy compatible with current reactor behavior.
25+6. Web currently marks Claude as unavailable (`"Claude Code (soon)"`) and model picker is Codex-only.
26+
27+---
28+
29+## Architecture target
30+
31+Add 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+
33+Key decisions:
34+
35+1. Keep orchestration provider-agnostic; adapt Claude inside adapter/layer boundaries.
36+2. Use the existing canonical runtime stream (`ProviderRuntimeEvent`) as the only ingestion contract.
37+3. Keep provider session routing in `ProviderService` and `ProviderSessionDirectory`.
38+4. 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+
46+Update `packages/contracts/src/model.ts` so model resolution can be provider-aware instead of Codex-only.
47+
48+Expected outcomes:
49+
50+1. Introduce provider-scoped model lists (Codex + Claude).
51+2. Add helpers that resolve model by provider.
52+3. Preserve backwards compatibility for existing Codex defaults.
53+
54+### 1.2 Turn-start provider intent
55+
56+Update `packages/contracts/src/orchestration.ts`:
57+
58+1. Add optional `provider: ProviderKind` to `ThreadTurnStartCommand`.
59+2. Carry provider through `ThreadTurnStartRequestedPayload`.
60+3. Keep existing command valid when provider is omitted.
61+
62+This 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+
66+Update `packages/contracts/src/provider.ts`:
67+
68+1. Move provider-specific start fields into typed `providerOptions`:
69+ - `providerOptions.codex`
70+ - `providerOptions.claudeCode`
71+2. Keep `resumeCursor` as the single cross-provider resume input in `ProviderSessionStartInput`.
72+3. Deprecate/remove `resumeThreadId` from the generic start contract.
73+4. Treat `resumeCursor` as adapter-owned opaque state.
74+
75+### 1.4 Contract tests (completed)
76+
77+Update/add tests in `packages/contracts/src/*.test.ts` for:
78+
79+1. New command payload shape.
80+2. Provider-aware model resolution behavior.
81+3. 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+
89+Create:
90+
91+1. `apps/server/src/provider/Services/ClaudeCodeAdapter.ts`
92+2. `apps/server/src/provider/Layers/ClaudeCodeAdapter.ts`
93+
94+Adapter must implement `ProviderAdapterShape<ProviderAdapterError>`.
95+
96+### 2.1.a SDK dependency and baseline config
97+
98+Add server dependency:
99+
100+1. `@anthropic-ai/claude-agent-sdk`
101+
102+Baseline adapter options to support from day one:
103+
104+1. `cwd`
105+2. `model`
106+3. `pathToClaudeCodeExecutable` (from `providerOptions.claudeCode.binaryPath`)
107+4. `permissionMode` (from `providerOptions.claudeCode.permissionMode`)
108+5. `maxThinkingTokens` (from `providerOptions.claudeCode.maxThinkingTokens`)
109+6. `resume`
110+7. `resumeSessionAt`
111+8. `includePartialMessages`
112+9. `canUseTool`
113+10. `hooks`
114+11. `env` and `additionalDirectories` (if needed for sandbox/workspace parity)
115+
116+### 2.2 Claude runtime bridge
117+
118+Implement a Claude runtime bridge (either directly in adapter layer or via dedicated manager file) that wraps Agent SDK query lifecycle.
119+
120+Required capabilities:
121+
122+1. Long-lived session context per adapter session.
123+2. Multi-turn input queue.
124+3. Interrupt support.
125+4. Approval request/response bridge.
126+5. Resume support via opaque `resumeCursor` (parsed inside Claude adapter only).
127+
128+#### 2.2.a Agent SDK details to preserve
129+
130+The adapter should explicitly rely on these SDK capabilities:
131+
132+1. `query()` returns an async iterable message stream and control methods (`interrupt`, `setModel`, `setPermissionMode`, `setMaxThinkingTokens`, account/status helpers).
133+2. Multi-turn input is supported via async-iterable prompt input.
134+3. Tool approval decisions are provided via `canUseTool`.
135+4. Resume support uses `resume` and optional `resumeSessionAt`, both derived by parsing adapter-owned `resumeCursor`.
136+5. 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
141+import { query } from "@anthropic-ai/claude-agent-sdk";
142+import { Effect } from "effect";
143+
144+const 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+
183+Preferred when available in the pinned Effect version:
184+
185+```ts
186+const 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+
198+Portable fallback (already aligned with current server patterns):
199+
200+```ts
201+const 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+
229+Claude adapter must translate Agent SDK output into canonical `ProviderRuntimeEvent`.
230+
231+Initial mapping target:
232+
233+1. assistant text deltas -> `content.delta`
234+2. final assistant text -> `item.completed` and/or `turn.completed`
235+3. approval requests -> `request.opened`
236+4. approval results -> `request.resolved`
237+5. system lifecycle -> `session.*`, `thread.*`, `turn.*`
238+6. errors -> `runtime.error`
239+7. plan/proposed-plan content when derivable
240+
241+Implementation note:
242+
243+1. Keep raw Claude message on `raw` for debugging.
244+2. Prefer canonical item/request kinds over provider-native enums.
245+3. 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+
249+Define Claude-owned opaque resume state, e.g.:
250+
251+```ts
252+interface ClaudeResumeCursor {
253+ readonly version: 1;
254+ readonly threadId?: string;
255+ readonly sessionAt?: string;
256+}
257+```
258+
259+Rules:
260+
261+1. Serialize only adapter-owned state into `resumeCursor`.
262+2. Parse/validate only inside Claude adapter.
263+3. Store updated cursor when Claude runtime yields enough data to resume safely.
264+4. Never overload orchestration thread id as Claude thread id.
265+
266+### 2.5 Interrupt and stop semantics
267+
268+Map orchestration stop/interrupt expectations onto SDK controls:
269+
270+1. `interruptTurn()` -> active query interrupt.
271+2. `stopSession()` -> close session resources and prevent future sends.
272+3. `rollbackThread()` -> see Phase 4.
273+
274+---
275+
276+## Phase 3: Provider service and composition
277+
278+### 3.1 Register Claude adapter
279+
280+Update provider registry layer to include Claude:
281+
282+1. add `claudeCode` -> `ClaudeCodeAdapter`
283+2. ensure `ProviderService.listProviderStatuses()` reports Claude availability
284+
285+### 3.2 Persist provider binding
286+
287+Current `ProviderSessionDirectory` already stores provider/thread binding and opaque `resumeCursor`.
288+
289+Required validation:
290+
291+1. Claude bindings survive restart.
292+2. resume cursor remains opaque and round-trips untouched.
293+3. stopAll + restart can recover Claude sessions when possible.
294+
295+### 3.3 Provider start routing
296+
297+Update `ProviderCommandReactor` / orchestration flow:
298+
299+1. If a thread turn start requests `provider: "claudeCode"`, start Claude if no active session exists.
300+2. If a thread already has Claude session binding, reuse it.
301+3. If provider switches between Codex and Claude, explicitly stop/rebind before next send.
302+
303+---
304+
305+## Phase 4: Checkpoint and revert strategy
306+
307+Claude does not necessarily expose the same conversation rewind primitive as Codex app-server. Current architecture expects `providerService.rollbackConversation()`.
308+
309+Pick one explicit strategy:
310+
311+### Option A: provider-native rewind
312+
313+If SDK/runtime supports safe rewind:
314+
315+1. implement in Claude adapter
316+2. keep `CheckpointReactor` unchanged
317+
318+### Option B: session restart + state truncation shim
319+
320+If no native rewind exists:
321+
322+1. 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
326+2. Document that rollback is “conversation reset to checkpoint boundary”, not provider-native turn deletion.
327+
328+Whichever option is chosen:
329+
330+1. behavior must be deterministic
331+2. checkpoint revert tests must pass under orchestration expectations
332+3. 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+
340+Update web state/UI:
341+
342+1. allow choosing Claude as thread provider before first turn
343+2. show Claude model list from provider-aware model helpers
344+3. preserve existing Codex default behavior when provider omitted
345+
346+Likely touch points:
347+
348+1. `apps/web/src/store.ts`
349+2. `apps/web/src/components/ChatView.tsx`
350+3. `apps/web/src/types.ts`
351+4. `packages/shared/src/model.ts`
352+
353+### 5.2 Settings for Claude executable/options
354+
355+Add app settings if needed for:
356+
357+1. Claude binary path
358+2. default permission mode
359+3. default max thinking tokens
360+
361+Do 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+
365+No new WS channel should be needed. Claude should appear through existing:
366+
367+1. thread messages
368+2. activities/worklog
369+3. approvals
370+4. session state
371+5. checkpoints/diffs
372+
373+---
374+
375+## Phase 6: Testing strategy
376+
377+### 6.1 Contract tests
378+
379+Cover:
380+
381+1. provider-aware model schemas
382+2. provider field on turn-start command
383+3. provider-specific start options schema
384+
385+### 6.2 Adapter layer tests
386+
387+Add `ClaudeCodeAdapter.test.ts` covering:
388+
389+1. session start
390+2. event mapping
391+3. approval bridge
392+4. resume cursor parse/serialize
393+5. interrupt behavior
394+6. rollback behavior or explicit unsupported error path
395+
396+Use 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+
400+Extend provider integration coverage so Claude is exercised through `ProviderService`:
401+
402+1. start Claude session
403+2. send turn
404+3. receive canonical runtime events
405+4. restart/recover using persisted binding
406+
407+### 6.4 Orchestration integration tests
408+
409+Add/extend integration tests around:
410+
411+1. first-turn provider selection
412+2. Claude approval requests routed through orchestration
413+3. Claude runtime ingestion -> messages/activities/session updates
414+4. checkpoint revert behavior under Claude
415+5. stopAll/restart recovery
416+
417+These should validate real orchestration flows, not just adapter behavior.
418+
419+---
420+
421+## Phase 7: Rollout order
422+
423+Recommended implementation order:
424+
425+1. contracts/provider-aware models
426+2. provider field on turn-start
427+3. Claude adapter skeleton + start/send/stream
428+4. canonical event mapping
429+5. provider registry/service wiring
430+6. orchestration recovery + checkpoint strategy
431+7. web provider/model picker
432+8. full integration tests
433+
434+---
435+
436+## Non-goals
437+
438+1. Reintroducing provider-specific WS methods/channels.
439+2. Storing provider-native thread ids as orchestration ids.
440+3. Bypassing orchestration engine for Claude-specific UI flows.
441+4. Encoding Claude resume semantics outside adapter-owned `resumeCursor`.
apps/server/package.json+1−0
@@ -22,6 +22,7 @@ · 21 unmodified lines
2222 "test": "vitest run"
2323 },
2424 "dependencies": {
25+ "@anthropic-ai/claude-agent-sdk": "^0.2.62",
2526 "@effect/platform-node": "catalog:",
2627 "@effect/sql-sqlite-bun": "catalog:",
2728 "@pierre/diffs": "^1.1.0-beta.16",