diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts index 29cfe9b5f3e2a610920b1ff9f680ef884facea15..edd2a5c18a1bb42b79d4b5ddc4f4ab78a42f7d0e 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts @@ -4,6 +4,7 @@ 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"; @@ -25,9 +26,30 @@ const fakeCodexAdapter: CodexAdapterShape = { streamEvents: Stream.empty, }; +const fakeClaudeAdapter: ClaudeCodeAdapterShape = { + provider: "claudeCode", + startSession: vi.fn(), + sendTurn: vi.fn(), + interruptTurn: vi.fn(), + respondToRequest: 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.succeed(CodexAdapter, fakeCodexAdapter)), + Layer.provide( + ProviderAdapterRegistryLive, + Layer.mergeAll( + Layer.succeed(CodexAdapter, fakeCodexAdapter), + Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter), + ), + ), NodeServices.layer, ), ); @@ -36,11 +58,13 @@ layer("ProviderAdapterRegistryLive", (it) => { it.effect("resolves a registered provider adapter", () => Effect.gen(function* () { const registry = yield* ProviderAdapterRegistry; - const adapter = yield* registry.getByProvider("codex"); - assert.equal(adapter, fakeCodexAdapter); + 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"]); + assert.deepEqual(providers, ["codex", "claudeCode"]); }), ); diff --git a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts index e59d85d0a0afcabbe52e99b705a932124741ef2e..37c430951878ce80bcadb504459b6223801aa301 100644 --- a/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderAdapterRegistry.ts @@ -15,6 +15,7 @@ import { ProviderAdapterRegistry, type ProviderAdapterRegistryShape, } from "../Services/ProviderAdapterRegistry.ts"; +import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts"; import { CodexAdapter } from "../Services/CodexAdapter.ts"; export interface ProviderAdapterRegistryLiveOptions { @@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions { const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) => Effect.gen(function* () { - const adapters = options?.adapters !== undefined ? options.adapters : [yield* CodexAdapter]; + const adapters = + options?.adapters !== undefined + ? options.adapters + : [yield* CodexAdapter, yield* ClaudeCodeAdapter]; const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter])); const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => { diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts index 2904ba48cf6c6ff452ab1d883fb839d9f1d64a55..efcb1cabeb8ad371fd32c31688a7ec803fd353cd 100644 --- a/apps/server/src/provider/Layers/ProviderService.test.ts +++ b/apps/server/src/provider/Layers/ProviderService.test.ts @@ -12,6 +12,7 @@ import type { import { ApprovalRequestId, EventId, + type ProviderKind, ProviderSessionStartInput, ProviderSessionId, ProviderThreadId, @@ -52,7 +53,7 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make const asEventId = (value: string): EventId => EventId.makeUnsafe(value); const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value); -function makeFakeCodexAdapter() { +function makeFakeCodexAdapter(provider: ProviderKind = "codex") { const sessions = new Map(); const runtimeEventPubSub = Effect.runSync(PubSub.unbounded()); let nextSession = 1; @@ -64,7 +65,7 @@ function makeFakeCodexAdapter() { const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`); const session: ProviderSession = { sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`), - provider: "codex", + provider, status: "ready", threadId, resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` }, @@ -85,7 +86,7 @@ function makeFakeCodexAdapter() { if (!sessions.has(input.sessionId)) { return Effect.fail( new ProviderAdapterSessionNotFoundError({ - provider: "codex", + provider, sessionId: input.sessionId, }), ); @@ -160,7 +161,7 @@ function makeFakeCodexAdapter() { ); const adapter: ProviderAdapterShape = { - provider: "codex", + provider, startSession, sendTurn, interruptTurn, @@ -199,12 +200,15 @@ const sleep = (ms: number) => function makeProviderServiceLayer() { const codex = makeFakeCodexAdapter(); + const claude = makeFakeCodexAdapter("claudeCode"); const registry: typeof ProviderAdapterRegistry.Service = { getByProvider: (provider) => provider === "codex" ? Effect.succeed(codex.adapter) - : Effect.fail(new ProviderUnsupportedError({ provider })), - listProviders: () => Effect.succeed(["codex"]), + : provider === "claudeCode" + ? Effect.succeed(claude.adapter) + : Effect.fail(new ProviderUnsupportedError({ provider })), + listProviders: () => Effect.succeed(["codex", "claudeCode"]), }; const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry); @@ -227,6 +231,7 @@ function makeProviderServiceLayer() { return { codex, + claude, layer, }; } @@ -448,6 +453,27 @@ routing.layer("ProviderServiceLive routing", (it) => { }), ); + it.effect("routes explicit claudeCode provider session starts to the claude adapter", () => + Effect.gen(function* () { + const provider = yield* ProviderService; + + const session = yield* provider.startSession(asThreadId("thread-claude"), { + provider: "claudeCode", + cwd: "/tmp/project-claude", + }); + + assert.equal(session.provider, "claudeCode"); + assert.equal(routing.claude.startSession.mock.calls.length, 1); + const startInput = routing.claude.startSession.mock.calls[0]?.[0]; + assert.equal(typeof startInput === "object" && startInput !== null, true); + if (startInput && typeof startInput === "object") { + const startPayload = startInput as { provider?: string; cwd?: string }; + assert.equal(startPayload.provider, "claudeCode"); + assert.equal(startPayload.cwd, "/tmp/project-claude"); + } + }), + ); + it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () => Effect.gen(function* () { const provider = yield* ProviderService; diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts index cc4f139577b40e51374d7a5748d7947fbf3bee60..2fff0dee330f1b4a4246a1dc40978a374ea631f0 100644 --- a/apps/server/src/serverLayers.ts +++ b/apps/server/src/serverLayers.ts @@ -18,6 +18,7 @@ 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"; @@ -56,8 +57,9 @@ export function makeServerProviderLayer(): Layer.Layer< const codexAdapterLayer = makeCodexAdapterLive( nativeEventLogger ? { nativeEventLogger } : undefined, ); + const claudeAdapterLayer = makeClaudeCodeAdapterLive(); const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe( - Layer.provide(codexAdapterLayer), + Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer)), Layer.provideMerge(providerSessionDirectoryLayer), ); return makeProviderServiceLive(