authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 01:55:11-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
loge9ced44ac6729116a1ee8332077736ca648ee419
treedbecd418a43a8193c83df0905d13507e896cfdc2
parent802cd127d17d77ba9d598675574b9d01480a94b0

Wire Claude adapter into registry and server provider layer

Add provider-service routing coverage for explicit claudeCode sessions. Co-authored-by: codex <codex@users.noreply.github.com>

4 files changed, 68 insertions(+), 12 deletions(-)

apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+28-4
...@@ -4,6 +4,7 @@ import { assertFailure } from "@effect/vitest/utils";...@@ -4,6 +4,7 @@ import { assertFailure } from "@effect/vitest/utils";
44
5import { Effect, Layer, Stream } from "effect";5import { Effect, Layer, Stream } from "effect";
66
7import { ClaudeCodeAdapter, ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
7import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";8import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
8import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";9import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
9import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";10import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
...@@ -25,9 +26,30 @@ const fakeCodexAdapter: CodexAdapterShape = {...@@ -25,9 +26,30 @@ const fakeCodexAdapter: CodexAdapterShape = {
25 streamEvents: Stream.empty,26 streamEvents: Stream.empty,
26};27};
2728
29const fakeClaudeAdapter: ClaudeCodeAdapterShape = {
30 provider: "claudeCode",
31 startSession: vi.fn(),
32 sendTurn: vi.fn(),
33 interruptTurn: vi.fn(),
34 respondToRequest: vi.fn(),
35 stopSession: vi.fn(),
36 listSessions: vi.fn(),
37 hasSession: vi.fn(),
38 readThread: vi.fn(),
39 rollbackThread: vi.fn(),
40 stopAll: vi.fn(),
41 streamEvents: Stream.empty,
42};
43
28const layer = it.layer(44const layer = it.layer(
29 Layer.mergeAll(45 Layer.mergeAll(
30 Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)),46 Layer.provide(
47 ProviderAdapterRegistryLive,
48 Layer.mergeAll(
49 Layer.succeed(CodexAdapter, fakeCodexAdapter),
50 Layer.succeed(ClaudeCodeAdapter, fakeClaudeAdapter),
51 ),
52 ),
31 NodeServices.layer,53 NodeServices.layer,
32 ),54 ),
33);55);
...@@ -36,11 +58,13 @@ layer("ProviderAdapterRegistryLive", (it) => {...@@ -36,11 +58,13 @@ layer("ProviderAdapterRegistryLive", (it) => {
36 it.effect("resolves a registered provider adapter", () =>58 it.effect("resolves a registered provider adapter", () =>
37 Effect.gen(function* () {59 Effect.gen(function* () {
38 const registry = yield* ProviderAdapterRegistry;60 const registry = yield* ProviderAdapterRegistry;
39 const adapter = yield* registry.getByProvider("codex");61 const codex = yield* registry.getByProvider("codex");
40 assert.equal(adapter, fakeCodexAdapter);62 const claude = yield* registry.getByProvider("claudeCode");
63 assert.equal(codex, fakeCodexAdapter);
64 assert.equal(claude, fakeClaudeAdapter);
4165
42 const providers = yield* registry.listProviders();66 const providers = yield* registry.listProviders();
43 assert.deepEqual(providers, ["codex"]);67 assert.deepEqual(providers, ["codex", "claudeCode"]);
44 }),68 }),
45 );69 );
4670
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+5-1
...@@ -15,6 +15,7 @@ import {...@@ -15,6 +15,7 @@ import {
15 ProviderAdapterRegistry,15 ProviderAdapterRegistry,
16 type ProviderAdapterRegistryShape,16 type ProviderAdapterRegistryShape,
17} from "../Services/ProviderAdapterRegistry.ts";17} from "../Services/ProviderAdapterRegistry.ts";
18import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
18import { CodexAdapter } from "../Services/CodexAdapter.ts";19import { CodexAdapter } from "../Services/CodexAdapter.ts";
1920
20export interface ProviderAdapterRegistryLiveOptions {21export interface ProviderAdapterRegistryLiveOptions {
...@@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions {...@@ -23,7 +24,10 @@ export interface ProviderAdapterRegistryLiveOptions {
2324
24const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) =>25const makeProviderAdapterRegistry = (options?: ProviderAdapterRegistryLiveOptions) =>
25 Effect.gen(function* () {26 Effect.gen(function* () {
26 const adapters = options?.adapters !== undefined ? options.adapters : [yield* CodexAdapter];27 const adapters =
28 options?.adapters !== undefined
29 ? options.adapters
30 : [yield* CodexAdapter, yield* ClaudeCodeAdapter];
27 const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter]));31 const byProvider = new Map(adapters.map((adapter) => [adapter.provider, adapter]));
2832
29 const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => {33 const getByProvider: ProviderAdapterRegistryShape["getByProvider"] = (provider) => {
apps/server/src/provider/Layers/ProviderService.test.ts+32-6
...@@ -12,6 +12,7 @@ import type {...@@ -12,6 +12,7 @@ import type {
12import {12import {
13 ApprovalRequestId,13 ApprovalRequestId,
14 EventId,14 EventId,
15 type ProviderKind,
15 ProviderSessionStartInput,16 ProviderSessionStartInput,
16 ProviderSessionId,17 ProviderSessionId,
17 ProviderThreadId,18 ProviderThreadId,
...@@ -52,7 +53,7 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make...@@ -52,7 +53,7 @@ const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make
52const asEventId = (value: string): EventId => EventId.makeUnsafe(value);53const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
53const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);54const asThreadId = (value: string): ThreadId => ThreadId.makeUnsafe(value);
5455
55function makeFakeCodexAdapter() {56function makeFakeCodexAdapter(provider: ProviderKind = "codex") {
56 const sessions = new Map<ProviderSessionId, ProviderSession>();57 const sessions = new Map<ProviderSessionId, ProviderSession>();
57 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());58 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
58 let nextSession = 1;59 let nextSession = 1;
...@@ -64,7 +65,7 @@ function makeFakeCodexAdapter() {...@@ -64,7 +65,7 @@ function makeFakeCodexAdapter() {
64 const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`);65 const threadId = ProviderThreadId.makeUnsafe(`thread-${next}`);
65 const session: ProviderSession = {66 const session: ProviderSession = {
66 sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`),67 sessionId: ProviderSessionId.makeUnsafe(`sess-${next}`),
67 provider: "codex",68 provider,
68 status: "ready",69 status: "ready",
69 threadId,70 threadId,
70 resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` },71 resumeCursor: input.resumeCursor ?? { opaque: `cursor-${next}` },
...@@ -85,7 +86,7 @@ function makeFakeCodexAdapter() {...@@ -85,7 +86,7 @@ function makeFakeCodexAdapter() {
85 if (!sessions.has(input.sessionId)) {86 if (!sessions.has(input.sessionId)) {
86 return Effect.fail(87 return Effect.fail(
87 new ProviderAdapterSessionNotFoundError({88 new ProviderAdapterSessionNotFoundError({
88 provider: "codex",89 provider,
89 sessionId: input.sessionId,90 sessionId: input.sessionId,
90 }),91 }),
91 );92 );
...@@ -160,7 +161,7 @@ function makeFakeCodexAdapter() {...@@ -160,7 +161,7 @@ function makeFakeCodexAdapter() {
160 );161 );
161162
162 const adapter: ProviderAdapterShape<ProviderAdapterError> = {163 const adapter: ProviderAdapterShape<ProviderAdapterError> = {
163 provider: "codex",164 provider,
164 startSession,165 startSession,
165 sendTurn,166 sendTurn,
166 interruptTurn,167 interruptTurn,
...@@ -199,12 +200,15 @@ const sleep = (ms: number) =>...@@ -199,12 +200,15 @@ const sleep = (ms: number) =>
199200
200function makeProviderServiceLayer() {201function makeProviderServiceLayer() {
201 const codex = makeFakeCodexAdapter();202 const codex = makeFakeCodexAdapter();
203 const claude = makeFakeCodexAdapter("claudeCode");
202 const registry: typeof ProviderAdapterRegistry.Service = {204 const registry: typeof ProviderAdapterRegistry.Service = {
203 getByProvider: (provider) =>205 getByProvider: (provider) =>
204 provider === "codex"206 provider === "codex"
205 ? Effect.succeed(codex.adapter)207 ? Effect.succeed(codex.adapter)
206 : Effect.fail(new ProviderUnsupportedError({ provider })),208 : provider === "claudeCode"
207 listProviders: () => Effect.succeed(["codex"]),209 ? Effect.succeed(claude.adapter)
210 : Effect.fail(new ProviderUnsupportedError({ provider })),
211 listProviders: () => Effect.succeed(["codex", "claudeCode"]),
208 };212 };
209213
210 const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry);214 const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry);
...@@ -227,6 +231,7 @@ function makeProviderServiceLayer() {...@@ -227,6 +231,7 @@ function makeProviderServiceLayer() {
227231
228 return {232 return {
229 codex,233 codex,
234 claude,
230 layer,235 layer,
231 };236 };
232}237}
...@@ -448,6 +453,27 @@ routing.layer("ProviderServiceLive routing", (it) => {...@@ -448,6 +453,27 @@ routing.layer("ProviderServiceLive routing", (it) => {
448 }),453 }),
449 );454 );
450455
456 it.effect("routes explicit claudeCode provider session starts to the claude adapter", () =>
457 Effect.gen(function* () {
458 const provider = yield* ProviderService;
459
460 const session = yield* provider.startSession(asThreadId("thread-claude"), {
461 provider: "claudeCode",
462 cwd: "/tmp/project-claude",
463 });
464
465 assert.equal(session.provider, "claudeCode");
466 assert.equal(routing.claude.startSession.mock.calls.length, 1);
467 const startInput = routing.claude.startSession.mock.calls[0]?.[0];
468 assert.equal(typeof startInput === "object" && startInput !== null, true);
469 if (startInput && typeof startInput === "object") {
470 const startPayload = startInput as { provider?: string; cwd?: string };
471 assert.equal(startPayload.provider, "claudeCode");
472 assert.equal(startPayload.cwd, "/tmp/project-claude");
473 }
474 }),
475 );
476
451 it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () =>477 it.effect("recovers stale persisted sessions for rollback by resuming thread identity", () =>
452 Effect.gen(function* () {478 Effect.gen(function* () {
453 const provider = yield* ProviderService;479 const provider = yield* ProviderService;
apps/server/src/serverLayers.ts+3-1
...@@ -18,6 +18,7 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj...@@ -18,6 +18,7 @@ import { OrchestrationProjectionPipelineLive } from "./orchestration/Layers/Proj
18import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers/ProjectionSnapshotQuery";18import { OrchestrationProjectionSnapshotQueryLive } from "./orchestration/Layers/ProjectionSnapshotQuery";
19import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion";19import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion";
20import { ProviderUnsupportedError } from "./provider/Errors";20import { ProviderUnsupportedError } from "./provider/Errors";
21import { makeClaudeCodeAdapterLive } from "./provider/Layers/ClaudeCodeAdapter";
21import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";22import { makeCodexAdapterLive } from "./provider/Layers/CodexAdapter";
22import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";23import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
23import { makeProviderServiceLive } from "./provider/Layers/ProviderService";24import { makeProviderServiceLive } from "./provider/Layers/ProviderService";
...@@ -56,8 +57,9 @@ export function makeServerProviderLayer(): Layer.Layer<...@@ -56,8 +57,9 @@ export function makeServerProviderLayer(): Layer.Layer<
56 const codexAdapterLayer = makeCodexAdapterLive(57 const codexAdapterLayer = makeCodexAdapterLive(
57 nativeEventLogger ? { nativeEventLogger } : undefined,58 nativeEventLogger ? { nativeEventLogger } : undefined,
58 );59 );
60 const claudeAdapterLayer = makeClaudeCodeAdapterLive();
59 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(61 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(
60 Layer.provide(codexAdapterLayer),62 Layer.provide(Layer.mergeAll(codexAdapterLayer, claudeAdapterLayer)),
61 Layer.provideMerge(providerSessionDirectoryLayer),63 Layer.provideMerge(providerSessionDirectoryLayer),
62 );64 );
63 return makeProviderServiceLive(65 return makeProviderServiceLive(