From 2e5e75699ef57f93967dffa64be9a70a7eec63a4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 5 Mar 2026 20:00:52 -0800 Subject: [PATCH] Remove Claude adapter from stack base Co-authored-by: codex --- .../provider/Layers/ClaudeCodeAdapter.test.ts | 930 --------- .../src/provider/Layers/ClaudeCodeAdapter.ts | 1857 ----------------- .../Layers/ProviderAdapterRegistry.test.ts | 27 +- .../Layers/ProviderAdapterRegistry.ts | 3 +- .../provider/Services/ClaudeCodeAdapter.ts | 32 - apps/server/src/serverLayers.ts | 5 - apps/web/src/routes/_chat.settings.tsx | 7 - apps/web/src/session-logic.test.ts | 12 +- apps/web/src/session-logic.ts | 1 - 9 files changed, 6 insertions(+), 2868 deletions(-) delete mode 100644 apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts delete mode 100644 apps/server/src/provider/Layers/ClaudeCodeAdapter.ts delete mode 100644 apps/server/src/provider/Services/ClaudeCodeAdapter.ts diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts deleted file mode 100644 index cd55c39837e4bd80e23a25b225f3cfd7f17780fa..0000000000000000000000000000000000000000 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +++ /dev/null @@ -1,930 +0,0 @@ -import type { - Options as ClaudeQueryOptions, - PermissionMode, - PermissionResult, - SDKMessage, - SDKUserMessage, -} from "@anthropic-ai/claude-agent-sdk"; -import { ApprovalRequestId, ThreadId } from "@t3tools/contracts"; -import { assert, describe, it } from "@effect/vitest"; -import { Effect, Fiber, Random, Stream } from "effect"; - -import { - ProviderAdapterValidationError, -} from "../Errors.ts"; -import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; -import { - makeClaudeCodeAdapterLive, - type ClaudeCodeAdapterLiveOptions, -} from "./ClaudeCodeAdapter.ts"; - -class FakeClaudeQuery implements AsyncIterable { - private readonly queue: Array = []; - private readonly resolvers: Array<(value: IteratorResult) => void> = []; - private done = false; - - public readonly interruptCalls: Array = []; - public readonly setModelCalls: Array = []; - public readonly setPermissionModeCalls: Array = []; - public readonly setMaxThinkingTokensCalls: Array = []; - public closeCalls = 0; - - emit(message: SDKMessage): void { - if (this.done) { - return; - } - const resolver = this.resolvers.shift(); - if (resolver) { - resolver({ done: false, value: message }); - return; - } - this.queue.push(message); - } - - finish(): void { - if (this.done) { - return; - } - this.done = true; - for (const resolver of this.resolvers.splice(0)) { - resolver({ done: true, value: undefined }); - } - } - - readonly interrupt = async (): Promise => { - this.interruptCalls.push(undefined); - }; - - readonly setModel = async (model?: string): Promise => { - this.setModelCalls.push(model); - }; - - readonly setPermissionMode = async (mode: PermissionMode): Promise => { - this.setPermissionModeCalls.push(mode); - }; - - readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise => { - this.setMaxThinkingTokensCalls.push(maxThinkingTokens); - }; - - readonly close = (): void => { - this.closeCalls += 1; - this.finish(); - }; - - [Symbol.asyncIterator](): AsyncIterator { - return { - next: () => { - if (this.queue.length > 0) { - const value = this.queue.shift(); - if (value) { - return Promise.resolve({ - done: false, - value, - }); - } - } - if (this.done) { - return Promise.resolve({ - done: true, - value: undefined, - }); - } - return new Promise((resolve) => { - this.resolvers.push(resolve); - }); - }, - }; - } -} - -interface Harness { - readonly layer: ReturnType; - readonly query: FakeClaudeQuery; - readonly getLastCreateQueryInput: () => - | { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - } - | undefined; -} - -function makeHarness(config?: { - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: ClaudeCodeAdapterLiveOptions["nativeEventLogger"]; -}): Harness { - const query = new FakeClaudeQuery(); - let createInput: - | { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - } - | undefined; - - const adapterOptions: ClaudeCodeAdapterLiveOptions = { - createQuery: (input) => { - createInput = input; - return query; - }, - ...(config?.nativeEventLogger - ? { - nativeEventLogger: config.nativeEventLogger, - } - : {}), - ...(config?.nativeEventLogPath - ? { - nativeEventLogPath: config.nativeEventLogPath, - } - : {}), - }; - - return { - layer: makeClaudeCodeAdapterLive(adapterOptions), - query, - getLastCreateQueryInput: () => createInput, - }; -} - -function makeDeterministicRandomService(seed = 0x1234_5678): { - nextIntUnsafe: () => number; - nextDoubleUnsafe: () => number; -} { - let state = seed >>> 0; - const nextIntUnsafe = (): number => { - state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0; - return state; - }; - - return { - nextIntUnsafe, - nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000, - }; -} - - -const THREAD_ID = ThreadId.makeUnsafe("thread-claude-1"); -const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-claude-resume"); - -describe("ClaudeCodeAdapterLive", () => { - it.effect("returns validation error for non-claudeCode provider on startSession", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - const result = yield* adapter - .startSession({ threadId: THREAD_ID, provider: "codex", runtimeMode: "full-access" }) - .pipe(Effect.result); - - assert.equal(result._tag, "Failure"); - if (result._tag !== "Failure") { - return; - } - assert.deepEqual( - result.failure, - new ProviderAdapterValidationError({ - provider: "claudeCode", - operation: "startSession", - issue: "Expected provider 'claudeCode' but received 'codex'.", - }), - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("derives bypass permission mode from full-access runtime policy", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.permissionMode, "bypassPermissions"); - assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("keeps explicit claude permission mode over runtime-derived defaults", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - providerOptions: { - claudeCode: { - permissionMode: "plan", - }, - }, - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.permissionMode, "plan"); - assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - model: "claude-sonnet-4-5", - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-1", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "Hi", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-2", - parent_tool_use_id: null, - event: { - type: "content_block_start", - index: 1, - content_block: { - type: "tool_use", - id: "tool-1", - name: "Bash", - input: { - command: "ls", - }, - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-1", - uuid: "stream-3", - parent_tool_use_id: null, - event: { - type: "content_block_stop", - index: 1, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-1", - uuid: "assistant-1", - parent_tool_use_id: null, - message: { - id: "assistant-message-1", - content: [{ type: "text", text: "Hi" }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-1", - uuid: "result-1", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "content.delta", - "item.started", - "item.completed", - "item.updated", - "item.completed", - "turn.completed", - ], - ); - - const turnStarted = runtimeEvents[3]; - assert.equal(turnStarted?.type, "turn.started"); - if (turnStarted?.type === "turn.started") { - assert.equal(String(turnStarted.turnId), String(turn.turnId)); - } - - const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Hi"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - - const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); - assert.equal(toolStarted?.type, "item.started"); - if (toolStarted?.type === "item.started") { - assert.equal(toolStarted.payload.itemType, "command_execution"); - } - - const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; - assert.equal(turnCompleted?.type, "turn.completed"); - if (turnCompleted?.type === "turn.completed") { - assert.equal(String(turnCompleted.turnId), String(turn.turnId)); - assert.equal(turnCompleted.payload.state, "completed"); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("emits completion only after turn result when assistant frames arrive before deltas", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-early-assistant", - uuid: "assistant-early", - parent_tool_use_id: null, - message: { - id: "assistant-message-early", - content: [{ type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-early-assistant", - uuid: "stream-early", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "Late text", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-early-assistant", - uuid: "result-early", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "item.updated", - "content.delta", - "item.completed", - "turn.completed", - ], - ); - - const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); - const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); - assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); - - const deltaEvent = runtimeEvents[deltaIndex]; - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Late text"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("falls back to assistant payload text when stream deltas are absent", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - harness.query.emit({ - type: "assistant", - session_id: "sdk-session-fallback-text", - uuid: "assistant-fallback", - parent_tool_use_id: null, - message: { - id: "assistant-message-fallback", - content: [{ type: "text", text: "Fallback hello" }], - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-fallback-text", - uuid: "result-fallback", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - "item.updated", - "content.delta", - "item.completed", - "turn.completed", - ], - ); - - const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); - assert.equal(deltaEvent?.type, "content.delta"); - if (deltaEvent?.type === "content.delta") { - assert.equal(deltaEvent.payload.delta, "Fallback hello"); - assert.equal(String(deltaEvent.turnId), String(turn.turnId)); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("does not fabricate provider thread ids before first SDK session_id", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe( - Stream.runCollect, - Effect.forkChild, - ); - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - assert.equal(session.threadId, undefined); - - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - assert.equal(turn.threadId, undefined); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-thread-real", - uuid: "stream-thread-real", - parent_tool_use_id: null, - event: { - type: "message_start", - message: { - id: "msg-thread-real", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-thread-real", - uuid: "result-thread-real", - } as unknown as SDKMessage); - - const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); - assert.deepEqual( - runtimeEvents.map((event) => event.type), - [ - "session.started", - "session.configured", - "session.state.changed", - "turn.started", - "thread.started", - ], - ); - - const sessionStarted = runtimeEvents[0]; - assert.equal(sessionStarted?.type, "session.started"); - if (sessionStarted?.type === "session.started") { - assert.equal("threadId" in sessionStarted, false); - } - - const threadStarted = runtimeEvents[4]; - assert.equal(threadStarted?.type, "thread.started"); - if (threadStarted?.type === "thread.started") { - assert.equal(threadStarted.threadId, "sdk-thread-real"); - } - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("bridges approval request/response lifecycle through canUseTool", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "approval-required", - }); - - yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); - - const createInput = harness.getLastCreateQueryInput(); - const canUseTool = createInput?.options.canUseTool; - assert.equal(typeof canUseTool, "function"); - if (!canUseTool) { - return; - } - - const permissionPromise = canUseTool( - "Bash", - { command: "pwd" }, - { - signal: new AbortController().signal, - suggestions: [ - { - type: "setMode", - mode: "default", - destination: "session", - }, - ], - toolUseID: "tool-use-1", - }, - ); - - const requested = yield* Stream.runHead(adapter.streamEvents); - assert.equal(requested._tag, "Some"); - if (requested._tag !== "Some") { - return; - } - assert.equal(requested.value.type, "request.opened"); - if (requested.value.type !== "request.opened") { - return; - } - const runtimeRequestId = requested.value.requestId; - assert.equal(typeof runtimeRequestId, "string"); - if (runtimeRequestId === undefined) { - return; - } - - yield* adapter.respondToRequest( - session.threadId, - ApprovalRequestId.makeUnsafe(runtimeRequestId), - "accept", - ); - - const resolved = yield* Stream.runHead(adapter.streamEvents); - assert.equal(resolved._tag, "Some"); - if (resolved._tag !== "Some") { - return; - } - assert.equal(resolved.value.type, "request.resolved"); - if (resolved.value.type !== "request.resolved") { - return; - } - assert.equal(resolved.value.requestId, requested.value.requestId); - assert.equal(resolved.value.payload.decision, "accept"); - - const permissionResult = yield* Effect.promise(() => permissionPromise); - assert.equal((permissionResult as PermissionResult).behavior, "allow"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("passes parsed resume cursor values to Claude query options", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const session = yield* adapter.startSession({ - threadId: RESUME_THREAD_ID, - provider: "claudeCode", - resumeCursor: { - threadId: "resume-thread-1", - resume: "550e8400-e29b-41d4-a716-446655440000", - resumeSessionAt: "assistant-99", - turnCount: 3, - }, - runtimeMode: "full-access", - }); - - assert.equal(session.threadId, "resume-thread-1"); - assert.deepEqual(session.resumeCursor, { - threadId: "resume-thread-1", - resume: "550e8400-e29b-41d4-a716-446655440000", - resumeSessionAt: "assistant-99", - turnCount: 3, - }); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); - assert.equal(createInput?.options.resumeSessionAt, "assistant-99"); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("does not synthesize resume session id from generated thread ids", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - assert.equal( - "resume" in (session.resumeCursor as Record), - false, - ); - - const createInput = harness.getLastCreateQueryInput(); - assert.equal(createInput?.options.resume, undefined); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - - const firstTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "first", - attachments: [], - }); - - const firstCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe( - Stream.runHead, - Effect.forkChild, - ); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-rollback", - uuid: "result-first", - } as unknown as SDKMessage); - - const firstCompleted = yield* Fiber.join(firstCompletedFiber); - assert.equal(firstCompleted._tag, "Some"); - if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { - assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); - } - - const secondTurn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "second", - attachments: [], - }); - - const secondCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe( - Stream.runHead, - Effect.forkChild, - ); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-rollback", - uuid: "result-second", - } as unknown as SDKMessage); - - const secondCompleted = yield* Fiber.join(secondCompletedFiber); - assert.equal(secondCompleted._tag, "Some"); - if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { - assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); - } - - const threadBeforeRollback = yield* adapter.readThread(session.threadId); - assert.equal(threadBeforeRollback.turns.length, 2); - - const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); - assert.equal(rolledBack.turns.length, 1); - assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); - - const threadAfterRollback = yield* adapter.readThread(session.threadId); - assert.equal(threadAfterRollback.turns.length, 1); - assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("updates model on sendTurn when model override is provided", () => { - const harness = makeHarness(); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - model: "claude-opus-4-6", - attachments: [], - }); - - assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); - - it.effect("writes provider-native observability records when enabled", () => { - const nativeEvents: Array<{ - event?: { - provider?: string; - method?: string; - threadId?: string; - turnId?: string; - }; - }> = []; - const harness = makeHarness({ - nativeEventLogger: { - filePath: "memory://claude-native-events", - write: (event) => { - nativeEvents.push(event as (typeof nativeEvents)[number]); - return Effect.void; - }, - close: () => Effect.void, - }, - }); - return Effect.gen(function* () { - const adapter = yield* ClaudeCodeAdapter; - - const session = yield* adapter.startSession({ - threadId: THREAD_ID, - provider: "claudeCode", - runtimeMode: "full-access", - }); - const turn = yield* adapter.sendTurn({ - threadId: session.threadId, - input: "hello", - attachments: [], - }); - - const turnCompletedFiber = yield* Stream.filter( - adapter.streamEvents, - (event) => event.type === "turn.completed", - ).pipe(Stream.runHead, Effect.forkChild); - - harness.query.emit({ - type: "stream_event", - session_id: "sdk-session-native-log", - uuid: "stream-native-log", - parent_tool_use_id: null, - event: { - type: "content_block_delta", - index: 0, - delta: { - type: "text_delta", - text: "hi", - }, - }, - } as unknown as SDKMessage); - - harness.query.emit({ - type: "result", - subtype: "success", - is_error: false, - errors: [], - session_id: "sdk-session-native-log", - uuid: "result-native-log", - } as unknown as SDKMessage); - - const turnCompleted = yield* Fiber.join(turnCompletedFiber); - assert.equal(turnCompleted._tag, "Some"); - - assert.equal(nativeEvents.length > 0, true); - assert.equal(nativeEvents.some((record) => record.event?.provider === "claudeCode"), true); - assert.equal(nativeEvents.some((record) => String(record.event?.threadId) === String(session.threadId)), true); - assert.equal( - nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), - true, - ); - assert.equal( - nativeEvents.some( - (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", - ), - true, - ); - }).pipe( - Effect.provideService(Random.Random, makeDeterministicRandomService()), - Effect.provide(harness.layer), - ); - }); -}); diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts deleted file mode 100644 index e6bccec4ed2fd55434129d4319b552608168b210..0000000000000000000000000000000000000000 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +++ /dev/null @@ -1,1857 +0,0 @@ -/** - * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter. - * - * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic - * provider adapter contract and emits canonical runtime events. - * - * @module ClaudeCodeAdapterLive - */ -import { - type CanUseTool, - query, - type Options as ClaudeQueryOptions, - type PermissionMode, - type PermissionResult, - type PermissionUpdate, - type SDKMessage, - type SDKResultMessage, - type SDKUserMessage, -} from "@anthropic-ai/claude-agent-sdk"; -import { - ApprovalRequestId, - type CanonicalItemType, - type CanonicalRequestType, - EventId, - type ProviderApprovalDecision, - ProviderItemId, - type ProviderRuntimeEvent, - type ProviderRuntimeTurnStatus, - type ProviderSendTurnInput, - type ProviderSession, - RuntimeItemId, - RuntimeRequestId, - RuntimeTaskId, - ThreadId, - TurnId, -} from "@t3tools/contracts"; -import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect"; - -import { - ProviderAdapterProcessError, - ProviderAdapterRequestError, - ProviderAdapterSessionClosedError, - ProviderAdapterSessionNotFoundError, - ProviderAdapterValidationError, - type ProviderAdapterError, -} from "../Errors.ts"; -import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; -import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; - -const PROVIDER = "claudeCode" as const; - -type PromptQueueItem = - | { - readonly type: "message"; - readonly message: SDKUserMessage; - } - | { - readonly type: "terminate"; - }; - -interface ClaudeResumeState { - readonly threadId?: ThreadId; - readonly resume?: string; - readonly resumeSessionAt?: string; - readonly turnCount?: number; -} - -interface ClaudeTurnState { - readonly turnId: TurnId; - readonly assistantItemId: string; - readonly startedAt: string; - readonly items: Array; - readonly messageCompleted: boolean; - readonly emittedTextDelta: boolean; - readonly fallbackAssistantText: string; -} - -interface PendingApproval { - readonly requestType: CanonicalRequestType; - readonly detail?: string; - readonly suggestions?: ReadonlyArray; - readonly decision: Deferred.Deferred; -} - -interface ToolInFlight { - readonly itemId: string; - readonly itemType: CanonicalItemType; - readonly toolName: string; - readonly title: string; - readonly detail?: string; -} - -interface ClaudeSessionContext { - session: ProviderSession; - readonly promptQueue: Queue.Queue; - readonly query: ClaudeQueryRuntime; - readonly startedAt: string; - resumeSessionId: string | undefined; - readonly pendingApprovals: Map; - readonly turns: Array<{ - id: TurnId; - items: Array; - }>; - readonly inFlightTools: Map; - turnState: ClaudeTurnState | undefined; - lastAssistantUuid: string | undefined; - lastThreadStartedId: string | undefined; - stopped: boolean; -} - -interface ClaudeQueryRuntime extends AsyncIterable { - readonly interrupt: () => Promise; - readonly setModel: (model?: string) => Promise; - readonly setPermissionMode: (mode: PermissionMode) => Promise; - readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise; - readonly close: () => void; -} - -export interface ClaudeCodeAdapterLiveOptions { - readonly createQuery?: (input: { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - }) => ClaudeQueryRuntime; - readonly nativeEventLogPath?: string; - readonly nativeEventLogger?: EventNdjsonLogger; -} - -function isUuid(value: string): boolean { - return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); -} - -function isSyntheticClaudeThreadId(value: string): boolean { - return value.startsWith("claude-thread-"); -} - -function toMessage(cause: unknown, fallback: string): string { - if (cause instanceof Error && cause.message.length > 0) { - return cause.message; - } - return fallback; -} - -function asRuntimeItemId(value: string): RuntimeItemId { - return RuntimeItemId.makeUnsafe(value); -} - -function asCanonicalTurnId(value: TurnId): TurnId { - return value; -} - -function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { - return RuntimeRequestId.makeUnsafe(value); -} - -function toPermissionMode(value: unknown): PermissionMode | undefined { - switch (value) { - case "default": - case "acceptEdits": - case "bypassPermissions": - case "plan": - case "dontAsk": - return value; - default: - return undefined; - } -} - -function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined { - if (!resumeCursor || typeof resumeCursor !== "object") { - return undefined; - } - const cursor = resumeCursor as { - threadId?: unknown; - resume?: unknown; - sessionId?: unknown; - resumeSessionAt?: unknown; - turnCount?: unknown; - }; - - const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; - const threadId = - threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) - ? ThreadId.makeUnsafe(threadIdCandidate) - : undefined; - const resumeCandidate = - typeof cursor.resume === "string" - ? cursor.resume - : typeof cursor.sessionId === "string" - ? cursor.sessionId - : undefined; - const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined; - const resumeSessionAt = - typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined; - const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; - - return { - ...(threadId ? { threadId } : {}), - ...(resume ? { resume } : {}), - ...(resumeSessionAt ? { resumeSessionAt } : {}), - ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 - ? { turnCount: turnCountValue } - : {}), - }; -} - -function classifyToolItemType(toolName: string): CanonicalItemType { - const normalized = toolName.toLowerCase(); - if ( - normalized.includes("bash") || - normalized.includes("command") || - normalized.includes("shell") || - normalized.includes("terminal") - ) { - return "command_execution"; - } - if ( - normalized.includes("edit") || - normalized.includes("write") || - normalized.includes("file") || - normalized.includes("patch") || - normalized.includes("replace") || - normalized.includes("create") || - normalized.includes("delete") - ) { - return "file_change"; - } - if (normalized.includes("mcp")) { - return "mcp_tool_call"; - } - return "dynamic_tool_call"; -} - -function classifyRequestType(toolName: string): CanonicalRequestType { - const normalized = toolName.toLowerCase(); - if (normalized === "read" || normalized.includes("read file") || normalized.includes("view")) { - return "file_read_approval"; - } - return classifyToolItemType(toolName) === "command_execution" - ? "command_execution_approval" - : "file_change_approval"; -} - -function summarizeToolRequest(toolName: string, input: Record): string { - const commandValue = input.command ?? input.cmd; - const command = typeof commandValue === "string" ? commandValue : undefined; - if (command && command.trim().length > 0) { - return `${toolName}: ${command.trim().slice(0, 400)}`; - } - - const serialized = JSON.stringify(input); - if (serialized.length <= 400) { - return `${toolName}: ${serialized}`; - } - return `${toolName}: ${serialized.slice(0, 397)}...`; -} - -function titleForTool(itemType: CanonicalItemType): string { - switch (itemType) { - case "command_execution": - return "Command run"; - case "file_change": - return "File change"; - case "mcp_tool_call": - return "MCP tool call"; - case "dynamic_tool_call": - return "Tool call"; - default: - return "Item"; - } -} - -function buildUserMessage(input: ProviderSendTurnInput): SDKUserMessage { - const fragments: string[] = []; - - if (input.input && input.input.trim().length > 0) { - fragments.push(input.input.trim()); - } - - for (const attachment of input.attachments ?? []) { - if (attachment.type === "image") { - fragments.push( - `Attached image: ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes).`, - ); - } - } - - const text = fragments.join("\n\n"); - - return { - type: "user", - session_id: "", - parent_tool_use_id: null, - message: { - role: "user", - content: [{ type: "text", text }], - }, - } as SDKUserMessage; -} - -function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { - if (result.subtype === "success") { - return "completed"; - } - - const errors = result.errors.join(" ").toLowerCase(); - if (errors.includes("interrupt")) { - return "interrupted"; - } - if (errors.includes("cancel")) { - return "cancelled"; - } - return "failed"; -} - -function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoning_text" { - return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; -} - -function providerThreadRef( - context: ClaudeSessionContext, -): { readonly providerThreadId: string } | {} { - return context.resumeSessionId ? { providerThreadId: context.resumeSessionId } : {}; -} - -function extractAssistantText(message: SDKMessage): string { - if (message.type !== "assistant") { - return ""; - } - - const content = (message.message as { content?: unknown } | undefined)?.content; - if (!Array.isArray(content)) { - return ""; - } - - const fragments: string[] = []; - for (const block of content) { - if (!block || typeof block !== "object") { - continue; - } - const candidate = block as { type?: unknown; text?: unknown }; - if ( - candidate.type === "text" && - typeof candidate.text === "string" && - candidate.text.length > 0 - ) { - fragments.push(candidate.text); - } - } - - return fragments.join(""); -} - -function toSessionError( - threadId: ThreadId, - cause: unknown, -): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { - const normalized = toMessage(cause, "").toLowerCase(); - if (normalized.includes("unknown session") || normalized.includes("not found")) { - return new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - cause, - }); - } - if (normalized.includes("closed")) { - return new ProviderAdapterSessionClosedError({ - provider: PROVIDER, - threadId, - cause, - }); - } - return undefined; -} - -function toRequestError( - threadId: ThreadId, - method: string, - cause: unknown, -): ProviderAdapterError { - const sessionError = toSessionError(threadId, cause); - if (sessionError) { - return sessionError; - } - return new ProviderAdapterRequestError({ - provider: PROVIDER, - method, - detail: toMessage(cause, `${method} failed`), - cause, - }); -} - -function sdkMessageType(value: unknown): string | undefined { - if (!value || typeof value !== "object") { - return undefined; - } - const record = value as { type?: unknown }; - return typeof record.type === "string" ? record.type : undefined; -} - -function sdkMessageSubtype(value: unknown): string | undefined { - if (!value || typeof value !== "object") { - return undefined; - } - const record = value as { subtype?: unknown }; - return typeof record.subtype === "string" ? record.subtype : undefined; -} - -function sdkNativeMethod(message: SDKMessage): string { - const subtype = sdkMessageSubtype(message); - if (subtype) { - return `claude/${message.type}/${subtype}`; - } - - if (message.type === "stream_event") { - const streamType = sdkMessageType(message.event); - if (streamType) { - const deltaType = - streamType === "content_block_delta" - ? sdkMessageType((message.event as { delta?: unknown }).delta) - : undefined; - if (deltaType) { - return `claude/${message.type}/${streamType}/${deltaType}`; - } - return `claude/${message.type}/${streamType}`; - } - } - - return `claude/${message.type}`; -} - -function sdkNativeItemId(message: SDKMessage): string | undefined { - if (message.type === "assistant") { - const maybeId = (message.message as { id?: unknown }).id; - if (typeof maybeId === "string") { - return maybeId; - } - return undefined; - } - - if (message.type === "stream_event") { - const event = message.event as { - type?: unknown; - content_block?: { id?: unknown }; - }; - if (event.type === "content_block_start" && typeof event.content_block?.id === "string") { - return event.content_block.id; - } - } - - return undefined; -} - -function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { - return Effect.gen(function* () { - const nativeEventLogger = - options?.nativeEventLogger ?? - (options?.nativeEventLogPath !== undefined - ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { - stream: "native", - }) - : undefined); - - const createQuery = - options?.createQuery ?? - ((input: { - readonly prompt: AsyncIterable; - readonly options: ClaudeQueryOptions; - }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime); - - const sessions = new Map(); - const runtimeEventQueue = yield* Queue.unbounded(); - - const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); - const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); - - const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => - Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); - - const logNativeSdkMessage = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - if (!nativeEventLogger) { - return; - } - - const observedAt = new Date().toISOString(); - const itemId = sdkNativeItemId(message); - - yield* nativeEventLogger - .write( - { - observedAt, - event: { - id: - "uuid" in message && typeof message.uuid === "string" - ? message.uuid - : crypto.randomUUID(), - kind: "notification", - provider: PROVIDER, - createdAt: observedAt, - method: sdkNativeMethod(message), - ...(typeof message.session_id === "string" - ? { providerThreadId: message.session_id } - : {}), - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), - payload: message, - }, - }, - null, - ); - }); - - const snapshotThread = ( - context: ClaudeSessionContext, - ): Effect.Effect<{ - threadId: ThreadId; - turns: ReadonlyArray<{ - id: TurnId; - items: ReadonlyArray; - }>; - }, ProviderAdapterValidationError> => - Effect.gen(function* () { - const threadId = context.session.threadId; - if (!threadId) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "readThread", - issue: "Session thread id is not initialized yet.", - }); - } - return { - threadId, - turns: context.turns.map((turn) => ({ - id: turn.id, - items: [...turn.items], - })), - }; - }); - - const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect => - Effect.gen(function* () { - const threadId = context.session.threadId; - if (!threadId) return; - - const resumeCursor = { - threadId, - ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}), - ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}), - turnCount: context.turns.length, - }; - - context.session = { - ...context.session, - resumeCursor, - updatedAt: yield* nowIso, - }; - }); - - const ensureThreadId = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - if (typeof message.session_id !== "string" || message.session_id.length === 0) { - return; - } - const nextThreadId = message.session_id; - context.resumeSessionId = message.session_id; - yield* updateResumeCursor(context); - - if (context.lastThreadStartedId !== nextThreadId) { - context.lastThreadStartedId = nextThreadId; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "thread.started", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - payload: { - providerThreadId: nextThreadId, - }, - providerRefs: {}, - raw: { - source: "claude.sdk.message", - method: "claude/thread/started", - payload: { - session_id: message.session_id, - }, - }, - }); - } - }); - - const emitRuntimeError = ( - context: ClaudeSessionContext, - message: string, - cause?: unknown, - ): Effect.Effect => - Effect.gen(function* () { - if (cause !== undefined) { - void cause; - } - const turnState = context.turnState; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "runtime.error", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), - payload: { - message, - class: "provider_error", - ...(cause !== undefined ? { detail: cause } : {}), - }, - providerRefs: { - ...providerThreadRef(context), - ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), - }, - }); - }); - - const emitRuntimeWarning = ( - context: ClaudeSessionContext, - message: string, - detail?: unknown, - ): Effect.Effect => - Effect.gen(function* () { - const turnState = context.turnState; - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "runtime.warning", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), - payload: { - message, - ...(detail !== undefined ? { detail } : {}), - }, - providerRefs: { - ...providerThreadRef(context), - ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), - }, - }); - }); - - const completeTurn = ( - context: ClaudeSessionContext, - status: ProviderRuntimeTurnStatus, - errorMessage?: string, - result?: SDKResultMessage, - ): Effect.Effect => - Effect.gen(function* () { - const turnState = context.turnState; - if (!turnState) { - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - payload: { - state: status, - ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), - ...(result?.usage ? { usage: result.usage } : {}), - ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), - ...(typeof result?.total_cost_usd === "number" - ? { totalCostUsd: result.total_cost_usd } - : {}), - ...(errorMessage ? { errorMessage } : {}), - }, - providerRefs: {}, - }); - return; - } - - if (!turnState.messageCompleted) { - if (!turnState.emittedTextDelta && turnState.fallbackAssistantText.length > 0) { - const deltaStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "content.delta", - eventId: deltaStamp.eventId, - provider: PROVIDER, - createdAt: deltaStamp.createdAt, - threadId: context.session.threadId, - turnId: turnState.turnId, - itemId: asRuntimeItemId(turnState.assistantItemId), - payload: { - streamKind: "assistant_text", - delta: turnState.fallbackAssistantText, - }, - providerRefs: { - ...providerThreadRef(context), - providerTurnId: String(turnState.turnId), - providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), - }, - }); - } - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - itemId: asRuntimeItemId(turnState.assistantItemId), - threadId: context.session.threadId, - turnId: turnState.turnId, - payload: { - itemType: "assistant_message", - status: "completed", - title: "Assistant message", - }, - providerRefs: { - ...providerThreadRef(context), - providerTurnId: turnState.turnId, - providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), - }, - }); - } - - context.turns.push({ - id: turnState.turnId, - items: [...turnState.items], - }); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - turnId: turnState.turnId, - payload: { - state: status, - ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), - ...(result?.usage ? { usage: result.usage } : {}), - ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), - ...(typeof result?.total_cost_usd === "number" - ? { totalCostUsd: result.total_cost_usd } - : {}), - ...(errorMessage ? { errorMessage } : {}), - }, - providerRefs: { - ...providerThreadRef(context), - providerTurnId: turnState.turnId, - }, - }); - - const updatedAt = yield* nowIso; - context.turnState = undefined; - context.session = { - ...context.session, - status: "ready", - activeTurnId: undefined, - updatedAt, - ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}), - }; - yield* updateResumeCursor(context); - }); - - const handleStreamEvent = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - if (message.type !== "stream_event") { - return; - } - - const { event } = message; - - if (event.type === "content_block_delta") { - if ( - event.delta.type === "text_delta" && - event.delta.text.length > 0 && - context.turnState - ) { - if (!context.turnState.emittedTextDelta) { - context.turnState = { - ...context.turnState, - emittedTextDelta: true, - }; - } - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "content.delta", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - turnId: context.turnState.turnId, - itemId: asRuntimeItemId(context.turnState.assistantItemId), - payload: { - streamKind: streamKindFromDeltaType(event.delta.type), - delta: event.delta.text, - }, - providerRefs: { - ...providerThreadRef(context), - providerTurnId: context.turnState.turnId, - providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), - }, - raw: { - source: "claude.sdk.message", - method: "claude/stream_event/content_block_delta", - payload: message, - }, - }); - } - return; - } - - if (event.type === "content_block_start") { - const { index, content_block: block } = event; - if ( - block.type !== "tool_use" && - block.type !== "server_tool_use" && - block.type !== "mcp_tool_use" - ) { - return; - } - - const toolName = block.name; - const itemType = classifyToolItemType(toolName); - const toolInput = - typeof block.input === "object" && block.input !== null - ? (block.input as Record) - : {}; - const itemId = block.id; - const detail = summarizeToolRequest(toolName, toolInput); - - const tool: ToolInFlight = { - itemId, - itemType, - toolName, - title: titleForTool(itemType), - detail, - }; - context.inFlightTools.set(index, tool); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.started", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - itemId: asRuntimeItemId(tool.itemId), - payload: { - itemType: tool.itemType, - status: "inProgress", - title: tool.title, - ...(tool.detail ? { detail: tool.detail } : {}), - data: { - toolName: tool.toolName, - input: toolInput, - }, - }, - providerRefs: { - ...providerThreadRef(context), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), - providerItemId: ProviderItemId.makeUnsafe(tool.itemId), - }, - raw: { - source: "claude.sdk.message", - method: "claude/stream_event/content_block_start", - payload: message, - }, - }); - return; - } - - if (event.type === "content_block_stop") { - const { index } = event; - const tool = context.inFlightTools.get(index); - if (!tool) { - return; - } - context.inFlightTools.delete(index); - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.completed", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - itemId: asRuntimeItemId(tool.itemId), - payload: { - itemType: tool.itemType, - status: "completed", - title: tool.title, - ...(tool.detail ? { detail: tool.detail } : {}), - }, - providerRefs: { - ...providerThreadRef(context), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), - providerItemId: ProviderItemId.makeUnsafe(tool.itemId), - }, - raw: { - source: "claude.sdk.message", - method: "claude/stream_event/content_block_stop", - payload: message, - }, - }); - } - }); - - const handleAssistantMessage = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - if (message.type !== "assistant") { - return; - } - - if (context.turnState) { - context.turnState.items.push(message.message); - const fallbackAssistantText = extractAssistantText(message); - if ( - fallbackAssistantText.length > 0 && - fallbackAssistantText !== context.turnState.fallbackAssistantText - ) { - context.turnState = { - ...context.turnState, - fallbackAssistantText, - }; - } - - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "item.updated", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - turnId: context.turnState.turnId, - itemId: asRuntimeItemId(context.turnState.assistantItemId), - payload: { - itemType: "assistant_message", - status: "inProgress", - title: "Assistant message", - data: message.message, - }, - providerRefs: { - ...providerThreadRef(context), - providerTurnId: context.turnState.turnId, - providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), - }, - raw: { - source: "claude.sdk.message", - method: "claude/assistant", - payload: message, - }, - }); - } - - context.lastAssistantUuid = message.uuid; - yield* updateResumeCursor(context); - }); - - const handleResultMessage = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - if (message.type !== "result") { - return; - } - - const status = turnStatusFromResult(message); - const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; - - if (status === "failed") { - yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); - } - - yield* completeTurn(context, status, errorMessage, message); - }); - - const handleSystemMessage = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - if (message.type !== "system") { - return; - } - - const stamp = yield* makeEventStamp(); - const base = { - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - providerRefs: { - ...providerThreadRef(context), - ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), - }, - raw: { - source: "claude.sdk.message" as const, - method: sdkNativeMethod(message), - messageType: `${message.type}:${message.subtype}`, - payload: message, - }, - }; - - switch (message.subtype) { - case "init": - yield* offerRuntimeEvent({ - ...base, - type: "session.configured", - payload: { - config: message as Record, - }, - }); - return; - case "status": - yield* offerRuntimeEvent({ - ...base, - type: "session.state.changed", - payload: { - state: message.status === "compacting" ? "waiting" : "running", - reason: `status:${message.status ?? "active"}`, - detail: message, - }, - }); - return; - case "compact_boundary": - yield* offerRuntimeEvent({ - ...base, - type: "thread.state.changed", - payload: { - state: "compacted", - detail: message, - }, - }); - return; - case "hook_started": - yield* offerRuntimeEvent({ - ...base, - type: "hook.started", - payload: { - hookId: message.hook_id, - hookName: message.hook_name, - hookEvent: message.hook_event, - }, - }); - return; - case "hook_progress": - yield* offerRuntimeEvent({ - ...base, - type: "hook.progress", - payload: { - hookId: message.hook_id, - output: message.output, - stdout: message.stdout, - stderr: message.stderr, - }, - }); - return; - case "hook_response": - yield* offerRuntimeEvent({ - ...base, - type: "hook.completed", - payload: { - hookId: message.hook_id, - outcome: message.outcome, - output: message.output, - stdout: message.stdout, - stderr: message.stderr, - ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}), - }, - }); - return; - case "task_started": - yield* offerRuntimeEvent({ - ...base, - type: "task.started", - payload: { - taskId: RuntimeTaskId.makeUnsafe(message.task_id), - description: message.description, - ...(message.task_type ? { taskType: message.task_type } : {}), - }, - }); - return; - case "task_progress": - yield* offerRuntimeEvent({ - ...base, - type: "task.progress", - payload: { - taskId: RuntimeTaskId.makeUnsafe(message.task_id), - description: message.description, - ...(message.usage ? { usage: message.usage } : {}), - ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), - }, - }); - return; - case "task_notification": - yield* offerRuntimeEvent({ - ...base, - type: "task.completed", - payload: { - taskId: RuntimeTaskId.makeUnsafe(message.task_id), - status: message.status, - ...(message.summary ? { summary: message.summary } : {}), - ...(message.usage ? { usage: message.usage } : {}), - }, - }); - return; - case "files_persisted": - yield* offerRuntimeEvent({ - ...base, - type: "files.persisted", - payload: { - files: Array.isArray(message.files) - ? message.files.map((file: { filename: string; file_id: string }) => ({ - filename: file.filename, - fileId: file.file_id, - })) - : [], - ...(Array.isArray(message.failed) - ? { - failed: message.failed.map((entry: { filename: string; error: string }) => ({ - filename: entry.filename, - error: entry.error, - })), - } - : {}), - }, - }); - return; - default: - yield* emitRuntimeWarning( - context, - `Unhandled Claude system message subtype '${message.subtype}'.`, - message, - ); - return; - } - }); - - const handleSdkTelemetryMessage = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - const stamp = yield* makeEventStamp(); - const base = { - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - providerRefs: { - ...providerThreadRef(context), - ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), - }, - raw: { - source: "claude.sdk.message" as const, - method: sdkNativeMethod(message), - messageType: message.type, - payload: message, - }, - }; - - if (message.type === "tool_progress") { - yield* offerRuntimeEvent({ - ...base, - type: "tool.progress", - payload: { - toolUseId: message.tool_use_id, - toolName: message.tool_name, - elapsedSeconds: message.elapsed_time_seconds, - ...(message.task_id ? { summary: `task:${message.task_id}` } : {}), - }, - }); - return; - } - - if (message.type === "tool_use_summary") { - yield* offerRuntimeEvent({ - ...base, - type: "tool.summary", - payload: { - summary: message.summary, - ...(message.preceding_tool_use_ids.length > 0 - ? { precedingToolUseIds: message.preceding_tool_use_ids } - : {}), - }, - }); - return; - } - - if (message.type === "auth_status") { - yield* offerRuntimeEvent({ - ...base, - type: "auth.status", - payload: { - isAuthenticating: message.isAuthenticating, - output: message.output, - ...(message.error ? { error: message.error } : {}), - }, - }); - return; - } - - if (message.type === "rate_limit_event") { - yield* offerRuntimeEvent({ - ...base, - type: "account.rate-limits.updated", - payload: { - rateLimits: message, - }, - }); - return; - } - }); - - const handleSdkMessage = ( - context: ClaudeSessionContext, - message: SDKMessage, - ): Effect.Effect => - Effect.gen(function* () { - yield* logNativeSdkMessage(context, message); - yield* ensureThreadId(context, message); - - switch (message.type) { - case "stream_event": - yield* handleStreamEvent(context, message); - return; - case "user": - return; - case "assistant": - yield* handleAssistantMessage(context, message); - return; - case "result": - yield* handleResultMessage(context, message); - return; - case "system": - yield* handleSystemMessage(context, message); - return; - case "tool_progress": - case "tool_use_summary": - case "auth_status": - case "rate_limit_event": - yield* handleSdkTelemetryMessage(context, message); - return; - default: - yield* emitRuntimeWarning( - context, - `Unhandled Claude SDK message type '${message.type}'.`, - message, - ); - return; - } - }); - - const runSdkStream = (context: ClaudeSessionContext): Effect.Effect => - Stream.fromAsyncIterable(context.query, (cause) => cause).pipe( - Stream.takeWhile(() => !context.stopped), - Stream.runForEach((message) => handleSdkMessage(context, message)), - Effect.catchCause((cause) => - Effect.gen(function* () { - if (Cause.hasInterruptsOnly(cause) || context.stopped) { - return; - } - const message = toMessage(Cause.squash(cause), "Claude runtime stream failed."); - yield* emitRuntimeError(context, message, cause); - yield* completeTurn(context, "failed", message); - }), - ), - ); - - const stopSessionInternal = ( - context: ClaudeSessionContext, - options?: { readonly emitExitEvent?: boolean }, - ): Effect.Effect => - Effect.gen(function* () { - if (context.stopped) return; - - context.stopped = true; - - for (const [requestId, pending] of context.pendingApprovals) { - yield* Deferred.succeed(pending.decision, "cancel"); - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "request.resolved", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - requestId: asRuntimeRequestId(requestId), - payload: { - requestType: pending.requestType, - decision: "cancel", - }, - providerRefs: { - ...providerThreadRef(context), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), - providerRequestId: requestId, - }, - }); - } - context.pendingApprovals.clear(); - - if (context.turnState) { - yield* completeTurn(context, "interrupted", "Session stopped."); - } - - yield* Queue.shutdown(context.promptQueue); - - context.query.close(); - - const updatedAt = yield* nowIso; - context.session = { - ...context.session, - status: "closed", - activeTurnId: undefined, - updatedAt, - }; - - if (options?.emitExitEvent !== false) { - const stamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.exited", - eventId: stamp.eventId, - provider: PROVIDER, - createdAt: stamp.createdAt, - threadId: context.session.threadId, - payload: { - reason: "Session stopped", - exitKind: "graceful", - }, - providerRefs: {}, - }); - } - - sessions.delete(context.session.threadId); - }); - - const requireSession = ( - threadId: ThreadId, - ): Effect.Effect => { - const context = sessions.get(threadId); - if (!context) { - return Effect.fail( - new ProviderAdapterSessionNotFoundError({ - provider: PROVIDER, - threadId, - }), - ); - } - if (context.stopped || context.session.status === "closed") { - return Effect.fail( - new ProviderAdapterSessionClosedError({ - provider: PROVIDER, - threadId, - }), - ); - } - return Effect.succeed(context); - }; - - const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => - Effect.gen(function* () { - if (input.provider !== undefined && input.provider !== PROVIDER) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "startSession", - issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, - }); - } - - const startedAt = yield* nowIso; - const resumeState = readClaudeResumeState(input.resumeCursor); - const threadId = input.threadId; - - const promptQueue = yield* Queue.unbounded(); - const prompt = Stream.fromQueue(promptQueue).pipe( - Stream.filter((item) => item.type === "message"), - Stream.map((item) => item.message), - Stream.toAsyncIterable, - ); - - const pendingApprovals = new Map(); - const inFlightTools = new Map(); - - const contextRef = yield* Ref.make(undefined); - - const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => - Effect.runPromise( - Effect.gen(function* () { - const context = yield* Ref.get(contextRef); - if (!context) { - return { - behavior: "deny", - message: "Claude session context is unavailable.", - } satisfies PermissionResult; - } - - const runtimeMode = input.runtimeMode ?? "full-access"; - if (runtimeMode === "full-access") { - return { - behavior: "allow", - updatedInput: toolInput, - } satisfies PermissionResult; - } - - const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4); - const requestType = classifyRequestType(toolName); - const detail = summarizeToolRequest(toolName, toolInput); - const decisionDeferred = yield* Deferred.make(); - const pendingApproval: PendingApproval = { - requestType, - detail, - decision: decisionDeferred, - ...(callbackOptions.suggestions - ? { suggestions: callbackOptions.suggestions } - : {}), - }; - - const requestedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "request.opened", - eventId: requestedStamp.eventId, - provider: PROVIDER, - createdAt: requestedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - requestId: asRuntimeRequestId(requestId), - payload: { - requestType, - detail, - args: { - toolName, - input: toolInput, - ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}), - }, - }, - providerRefs: { - ...(context.session.threadId - ? { providerThreadId: context.session.threadId } - : {}), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), - providerRequestId: requestId, - }, - raw: { - source: "claude.sdk.permission", - method: "canUseTool/request", - payload: { - toolName, - input: toolInput, - }, - }, - }); - - pendingApprovals.set(requestId, pendingApproval); - - const onAbort = () => { - if (!pendingApprovals.has(requestId)) { - return; - } - pendingApprovals.delete(requestId); - Effect.runFork(Deferred.succeed(decisionDeferred, "cancel")); - }; - - callbackOptions.signal.addEventListener("abort", onAbort, { - once: true, - }); - - const decision = yield* Deferred.await(decisionDeferred); - pendingApprovals.delete(requestId); - - const resolvedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "request.resolved", - eventId: resolvedStamp.eventId, - provider: PROVIDER, - createdAt: resolvedStamp.createdAt, - threadId: context.session.threadId, - ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), - requestId: asRuntimeRequestId(requestId), - payload: { - requestType, - decision, - }, - providerRefs: { - ...(context.session.threadId - ? { providerThreadId: context.session.threadId } - : {}), - ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), - providerRequestId: requestId, - }, - raw: { - source: "claude.sdk.permission", - method: "canUseTool/decision", - payload: { - decision, - }, - }, - }); - - if (decision === "accept" || decision === "acceptForSession") { - return { - behavior: "allow", - updatedInput: toolInput, - ...(decision === "acceptForSession" && pendingApproval.suggestions - ? { updatedPermissions: [...pendingApproval.suggestions] } - : {}), - } satisfies PermissionResult; - } - - return { - behavior: "deny", - message: - decision === "cancel" - ? "User cancelled tool execution." - : "User declined tool execution.", - } satisfies PermissionResult; - }), - ); - - const providerOptions = input.providerOptions?.claudeCode; - const permissionMode = - toPermissionMode(providerOptions?.permissionMode) ?? - (input.runtimeMode === "full-access" ? "bypassPermissions" : undefined); - - const queryOptions: ClaudeQueryOptions = { - ...(input.cwd ? { cwd: input.cwd } : {}), - ...(input.model ? { model: input.model } : {}), - ...(providerOptions?.binaryPath - ? { pathToClaudeCodeExecutable: providerOptions.binaryPath } - : {}), - ...(permissionMode ? { permissionMode } : {}), - ...(permissionMode === "bypassPermissions" - ? { allowDangerouslySkipPermissions: true } - : {}), - ...(providerOptions?.maxThinkingTokens !== undefined - ? { maxThinkingTokens: providerOptions.maxThinkingTokens } - : {}), - ...(resumeState?.resume ? { resume: resumeState.resume } : {}), - ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}), - includePartialMessages: true, - canUseTool, - env: process.env, - ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), - }; - - const queryRuntime = yield* Effect.try({ - try: () => - createQuery({ - prompt, - options: queryOptions, - }), - catch: (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - threadId, - detail: toMessage(cause, "Failed to start Claude runtime session."), - cause, - }), - }); - - const session: ProviderSession = { - threadId, - provider: PROVIDER, - status: "ready", - runtimeMode: input.runtimeMode, - ...(input.cwd ? { cwd: input.cwd } : {}), - ...(input.model ? { model: input.model } : {}), - ...(threadId ? { threadId } : {}), - resumeCursor: { - ...(threadId ? { threadId } : {}), - ...(resumeState?.resume ? { resume: resumeState.resume } : {}), - ...(resumeState?.resumeSessionAt - ? { resumeSessionAt: resumeState.resumeSessionAt } - : {}), - turnCount: resumeState?.turnCount ?? 0, - }, - createdAt: startedAt, - updatedAt: startedAt, - }; - - const context: ClaudeSessionContext = { - session, - promptQueue, - query: queryRuntime, - startedAt, - resumeSessionId: resumeState?.resume, - pendingApprovals, - turns: [], - inFlightTools, - turnState: undefined, - lastAssistantUuid: resumeState?.resumeSessionAt, - lastThreadStartedId: undefined, - stopped: false, - }; - yield* Ref.set(contextRef, context); - sessions.set(threadId, context); - - const sessionStartedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.started", - eventId: sessionStartedStamp.eventId, - provider: PROVIDER, - createdAt: sessionStartedStamp.createdAt, - threadId, - payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, - providerRefs: {}, - }); - - const configuredStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.configured", - eventId: configuredStamp.eventId, - provider: PROVIDER, - createdAt: configuredStamp.createdAt, - threadId, - payload: { - config: { - ...(input.model ? { model: input.model } : {}), - ...(input.cwd ? { cwd: input.cwd } : {}), - ...(permissionMode ? { permissionMode } : {}), - ...(providerOptions?.maxThinkingTokens !== undefined - ? { maxThinkingTokens: providerOptions.maxThinkingTokens } - : {}), - }, - }, - providerRefs: {}, - }); - - const readyStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "session.state.changed", - eventId: readyStamp.eventId, - provider: PROVIDER, - createdAt: readyStamp.createdAt, - threadId, - payload: { - state: "ready", - }, - providerRefs: {}, - }); - - Effect.runFork(runSdkStream(context)); - - return { - ...session, - }; - }); - - const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => - Effect.gen(function* () { - const context = yield* requireSession(input.threadId); - - if (context.turnState) { - return yield* new ProviderAdapterValidationError({ - provider: PROVIDER, - operation: "sendTurn", - issue: `Thread '${input.threadId}' already has an active turn '${context.turnState.turnId}'.`, - }); - } - - if (input.model) { - yield* Effect.tryPromise({ - try: () => context.query.setModel(input.model), - catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), - }); - } - - const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4); - const turnState: ClaudeTurnState = { - turnId, - assistantItemId: yield* Random.nextUUIDv4, - startedAt: yield* nowIso, - items: [], - messageCompleted: false, - emittedTextDelta: false, - fallbackAssistantText: "", - }; - - const updatedAt = yield* nowIso; - context.turnState = turnState; - context.session = { - ...context.session, - status: "running", - activeTurnId: turnId, - updatedAt, - }; - - const turnStartedStamp = yield* makeEventStamp(); - yield* offerRuntimeEvent({ - type: "turn.started", - eventId: turnStartedStamp.eventId, - provider: PROVIDER, - createdAt: turnStartedStamp.createdAt, - threadId: context.session.threadId, - turnId, - payload: input.model ? { model: input.model } : {}, - providerRefs: { - providerTurnId: String(turnId), - }, - }); - - const message = buildUserMessage(input); - - yield* Queue.offer(context.promptQueue, { - type: "message", - message, - }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); - - return { - threadId: context.session.threadId, - turnId, - ...(context.session.resumeCursor !== undefined - ? { resumeCursor: context.session.resumeCursor } - : {}), - }; - }); - - const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (threadId, _turnId) => - Effect.gen(function* () { - const context = yield* requireSession(threadId); - yield* Effect.tryPromise({ - try: () => context.query.interrupt(), - catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), - }); - }); - - const readThread: ClaudeCodeAdapterShape["readThread"] = (threadId) => - Effect.gen(function* () { - const context = yield* requireSession(threadId); - return yield* snapshotThread(context); - }); - - const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (threadId, numTurns) => - Effect.gen(function* () { - const context = yield* requireSession(threadId); - const nextLength = Math.max(0, context.turns.length - numTurns); - context.turns.splice(nextLength); - yield* updateResumeCursor(context); - return yield* snapshotThread(context); - }); - - const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( - threadId, - requestId, - decision, - ) => - Effect.gen(function* () { - const context = yield* requireSession(threadId); - const pending = context.pendingApprovals.get(requestId); - if (!pending) { - return yield* new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "item/requestApproval/decision", - detail: `Unknown pending approval request: ${requestId}`, - }); - } - - context.pendingApprovals.delete(requestId); - yield* Deferred.succeed(pending.decision, decision); - }); - - const respondToUserInput: ClaudeCodeAdapterShape["respondToUserInput"] = ( - threadId, - requestId, - _answers, - ) => - Effect.fail( - new ProviderAdapterRequestError({ - provider: PROVIDER, - method: "item/tool/requestUserInput", - detail: `Claude Code does not yet support structured user-input responses for thread '${threadId}' and request '${requestId}'.`, - }), - ); - - const stopSession: ClaudeCodeAdapterShape["stopSession"] = (threadId) => - Effect.gen(function* () { - const context = yield* requireSession(threadId); - yield* stopSessionInternal(context, { - emitExitEvent: true, - }); - }); - - const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => - Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); - - const hasSession: ClaudeCodeAdapterShape["hasSession"] = (threadId) => - Effect.sync(() => { - const context = sessions.get(threadId); - return context !== undefined && !context.stopped; - }); - - const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => - Effect.forEach( - sessions, - ([, context]) => - stopSessionInternal(context, { - emitExitEvent: true, - }), - { discard: true }, - ); - - yield* Effect.addFinalizer(() => - Effect.forEach( - sessions, - ([, context]) => - stopSessionInternal(context, { - emitExitEvent: false, - }), - { discard: true }, - ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), - ); - - return { - provider: PROVIDER, - capabilities: { - sessionModelSwitch: "in-session", - }, - startSession, - sendTurn, - interruptTurn, - readThread, - rollbackThread, - respondToRequest, - respondToUserInput, - stopSession, - listSessions, - hasSession, - stopAll, - streamEvents: Stream.fromQueue(runtimeEventQueue), - } satisfies ClaudeCodeAdapterShape; - }); -} - -export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter()); - -export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) { - return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options)); -} diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts index 795c106f0551b95b8848de129e400fd91381e869..a50112a62d82de852347ae56c51d733068f04576 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts @@ -4,7 +4,6 @@ import { assertFailure } from "@effect/vitest/utils"; import { Effect, Layer, Stream } from "effect"; -import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts"; import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"; import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts"; @@ -28,31 +27,11 @@ const fakeCodexAdapter: CodexAdapterShape = { streamEvents: Stream.empty, }; -const fakeClaudeAdapter: ClaudeCodeAdapterShape = { - provider: "claudeCode", - capabilities: { sessionModelSwitch: "in-session" }, - startSession: vi.fn(), - sendTurn: vi.fn(), - interruptTurn: vi.fn(), - respondToRequest: vi.fn(), - respondToUserInput: vi.fn(), - stopSession: vi.fn(), - listSessions: vi.fn(), - hasSession: vi.fn(), - readThread: vi.fn(), - rollbackThread: vi.fn(), - stopAll: vi.fn(), - streamEvents: Stream.empty, -}; - const layer = it.layer( Layer.mergeAll( Layer.provide( ProviderAdapterRegistryLive, - Layer.mergeAll( - Layer.succeed(CodexAdapter, fakeCodexAdapter), - Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter), - ), + Layer.succeed(CodexAdapter, fakeCodexAdapter), ), NodeServices.layer, ), @@ -63,12 +42,10 @@ layer("ProviderAdapterRegistryLive", (it) => { Effect.gen(function* () { const registry = yield* ProviderAdapterRegistry; const codex = yield* registry.getByProvider("codex"); - const claude = yield* registry.getByProvider("claudeCode"); assert.equal(codex, fakeCodexAdapter); - assert.equal(claude, fakeClaudeAdapter); const providers = yield* registry.listProviders(); - assert.deepEqual(providers, ["codex", "claudeCode"]); + assert.deepEqual(providers, ["codex"]); }), ); diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts index 37c430951878ce80bcadb504459b6223801aa301..b3911b14ba2bcca02c4ba3a385eb8f0c29f038bc 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts @@ -15,7 +15,6 @@ import { ProviderAdapterRegistry, type ProviderAdapterRegistryShape, } from "../Services/ProviderAdapterRegistry.ts"; -import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; import { CodexAdapter } from "../Services/CodexAdapter.ts"; export interface ProviderAdapterRegistryLiveOptions { @@ -27,7 +26,7 @@ const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOption const adapters = options?.adapters !== undefined ? options.adapters - : [yield* CodexAdapter, yield* ClaudeCodeAdapter]; + : [yield* CodexAdapter]; const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { diff --git a/apps/server/src/provider/Services/ClaudeCodeAdapter.ts b/apps/server/src/provider/Services/ClaudeCodeAdapter.ts deleted file mode 100644 index 80fb8771d801312f8223041c6c6ba891ee90afaf..0000000000000000000000000000000000000000 --- a/apps/server/src/provider/Services/ClaudeCodeAdapter.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * ClaudeCodeAdapter - Claude Code implementation of the generic provider adapter contract. - * - * This service owns Claude runtime/session semantics and emits canonical - * provider runtime events. It does not perform cross-provider routing, shared - * event fan-out, or checkpoint orchestration. - * - * Uses Effect `ServiceMap.Service` for dependency injection and returns the - * shared provider-adapter error channel with `provider: "claudeCode"` context. - * - * @module ClaudeCodeAdapter - */ -import { ServiceMap } from "effect"; - -import type { ProviderAdapterError } from "../Errors.ts"; -import type { ProviderAdapterShape } from "./ProviderAdapter.ts"; - -/** - * ClaudeCodeAdapterShape - Service API for the Claude Code provider adapter. - */ -export interface ClaudeCodeAdapterShape extends ProviderAdapterShape { - readonly provider: "claudeCode"; -} - -/** - * ClaudeCodeAdapter - Service tag for Claude Code provider adapter operations. - */ -export class ClaudeCodeAdapter extends ServiceMap.Service< - ClaudeCodeAdapter, - ClaudeCodeAdapterShape ->()("t3/provider/Services/ClaudeCodeAdapter") {} - diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts index 32dd79fea65b686c7e38103fefbbd6f98c09f134..cc4f139577b40e51374d7a5748d7947fbf3bee60 100644 --- a/apps/server/src/serverLayers.ts +++ b/apps/server/src/serverLayers.ts @@ -18,7 +18,6 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers/ProjectionSnapshotQuery"; import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion"; import { ProviderUnsupportedError } from "./provider/Errors"; -import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter"; import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter"; import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry"; import { makeProviderServiceLive } from "./provider/Layers/ProviderService"; @@ -57,12 +56,8 @@ export function makeServerProviderLayer(): Layer.Layer< const codexAdapterLayer = makeCodexAdapterLive( nativeEventLogger ? { nativeEventLogger } : undefined, ); - const claudeAdapterLayer = makeClaudeCodeAdapterLive( - nativeEventLogger ? { nativeEventLogger } : undefined, - ); const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( Layer.provide(codexAdapterLayer), - Layer.provide(claudeAdapterLayer), Layer.provideMerge(providerSessionDirectoryLayer), ); return makeProviderServiceLive( diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 4221bb9148129d34b3a02597d2703f09705dd707..4193cb108fa3b2b824d4d3c921f14844924baa10 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -47,13 +47,6 @@ const MODEL_PROVIDER_SETTINGS: Array<{ placeholder: "your-codex-model-slug", example: "gpt-6.7-codex-ultra-preview", }, - { - provider: "claudeCode", - title: "Claude Code", - description: "Save additional Claude model slugs for the picker and `/model` command.", - placeholder: "your-claude-model-slug", - example: "claude-sonnet-5-0", - }, ] as const; function getCustomModelsForProvider( diff --git a/apps/web/src/session-logic.test.ts b/apps/web/src/session-logic.test.ts index f161384db83f70296d903788afdf3fdca53256cd..144e209de4b228af2080e5f7cf4b6b95fa24a883 100644 --- a/apps/web/src/session-logic.test.ts +++ b/apps/web/src/session-logic.test.ts @@ -514,17 +514,11 @@ describe("isLatestTurnSettled", () => { }); describe("PROVIDER_OPTIONS", () => { - it("marks claudeCode as available", () => { + it("only advertises codex in the stack base", () => { const claude = PROVIDER_OPTIONS.find((option) => option.value === "claudeCode"); - expect(claude).toEqual({ - value: "claudeCode", - label: "Claude Code", - available: true, - }); - }); - - it("does not advertise cursor as an available provider", () => { const cursor = PROVIDER_OPTIONS.find((option) => option.value === "cursor"); + expect(PROVIDER_OPTIONS).toEqual([{ value: "codex", label: "Codex", available: true }]); + expect(claude).toBeUndefined(); expect(cursor).toBeUndefined(); }); }); diff --git a/apps/web/src/session-logic.ts b/apps/web/src/session-logic.ts index 36213bb6c5dcb52967b6104b6ca82689689db049..f41bf3d34bababa84f0865de7dac20e02c90d69b 100644 --- a/apps/web/src/session-logic.ts +++ b/apps/web/src/session-logic.ts @@ -16,7 +16,6 @@ export const PROVIDER_OPTIONS: Array<{ available: boolean; }> = [ { value: "codex", label: "Codex", available: true }, - { value: "claudeCode", label: "Claude Code", available: true }, ]; export interface WorkLogEntry { -- 2.54.0