| @@ -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`. |