History

Inline Claude adapter behavior into live layer

Julius Marminge committed 8 months ago

Commit e45cd8f1 parent fb1e7c12 files · +202 −498Browse files

Unified · Split

2 files
  1. apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +121 −233
  2. apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +81 −265
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+121−233
@@ -1,186 +1,79 @@
1−import type {
2− ProviderApprovalDecision,
3− ProviderRuntimeEvent,
4− ProviderSendTurnInput,
5− ProviderSession,
6− ProviderSessionStartInput,
7− ProviderTurnStartResult,
8−} from "@t3tools/contracts";
9−import {
10− ApprovalRequestId,
11− EventId,
12− ProviderSessionId,
13− ProviderThreadId,
14− ProviderTurnId,
15−} from "@t3tools/contracts";
16−import { afterAll, assert, it, vi } from "@effect/vitest";
17−import { assertFailure } from "@effect/vitest/utils";
18−
19−import { Effect, Fiber, Queue, Stream } from "effect";
1+import { EventId, ProviderSessionId, ProviderThreadId, ProviderTurnId } from "@t3tools/contracts";
2+import { assert, describe, it } from "@effect/vitest";
3+import { Effect, Layer, Stream } from "effect";
204
21−import { ProviderAdapterValidationError } from "../Errors.ts";
22−import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
23−import { type ClaudeCodeRuntime, makeClaudeCodeAdapterLive } from "./ClaudeCodeAdapter.ts";
5+import {
6+ ProviderAdapterProcessError,
7+ ProviderAdapterRequestError,
8+ ProviderAdapterValidationError,
9+} from "../Errors.ts";
10+import {
11+ ClaudeCodeAdapter,
12+ type ClaudeCodeAdapterShape,
13+} from "../Services/ClaudeCodeAdapter.ts";
14+import { makeClaudeCodeAdapterLive } from "./ClaudeCodeAdapter.ts";
2415
2516 const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value);
2617 const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);
2718 const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
28−const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.makeUnsafe(value);
29−
30−class FakeClaudeRuntime implements ClaudeCodeRuntime {
31− private readonly runtimeEventQueue = Effect.runSync(Queue.unbounded<ProviderRuntimeEvent>());
32− readonly streamEvents = Stream.fromQueue(this.runtimeEventQueue);
33−
34− public startSessionImpl = vi.fn(
35− (input: ProviderSessionStartInput): Effect.Effect<ProviderSession> => {
36− const now = new Date().toISOString();
37− return Effect.succeed({
38− sessionId: asSessionId("claude-sess-1"),
39− provider: "claudeCode",
40− status: "ready",
41− threadId: ProviderThreadId.makeUnsafe("claude-thread-1"),
42− resumeCursor: input.resumeCursor ?? { opaque: "claude-cursor-1" },
43− cwd: input.cwd,
44− model: input.model,
45− createdAt: now,
46− updatedAt: now,
47− });
48− },
49− );
50−
51− public sendTurnImpl = vi.fn(
52− (_input: ProviderSendTurnInput): Effect.Effect<ProviderTurnStartResult> =>
53− Effect.succeed({
54− threadId: ProviderThreadId.makeUnsafe("claude-thread-1"),
55− turnId: asTurnId("claude-turn-1"),
56− }),
57− );
58−
59− public interruptTurnImpl = vi.fn(
60− (_sessionId: ProviderSessionId, _turnId?: ProviderTurnId): Effect.Effect<void> => Effect.void,
61− );
62−
63− public readThreadImpl = vi.fn((_sessionId: ProviderSessionId): Effect.Effect<{
64− threadId: ProviderThreadId;
65− turns: never[];
66− }> =>
67− Effect.succeed({
68− threadId: ProviderThreadId.makeUnsafe("claude-thread-1"),
69− turns: [],
70− }));
71−
72− public rollbackThreadImpl = vi.fn(
73− (_sessionId: ProviderSessionId, _numTurns: number): Effect.Effect<{
74− threadId: ProviderThreadId;
75− turns: never[];
76− }> =>
77− Effect.succeed({
78− threadId: ProviderThreadId.makeUnsafe("claude-thread-1"),
79− turns: [],
80− }),
81− );
82−
83− public respondToRequestImpl = vi.fn(
84− (
85− _sessionId: ProviderSessionId,
86− _requestId: ApprovalRequestId,
87− _decision: ProviderApprovalDecision,
88− ): Effect.Effect<void> => Effect.void,
89− );
90−
91− public stopSessionImpl = vi.fn((_sessionId: ProviderSessionId): Effect.Effect<void> => Effect.void);
92− public listSessionsImpl = vi.fn((): Effect.Effect<ReadonlyArray<ProviderSession>> => Effect.succeed([]));
93− public hasSessionImpl = vi.fn((_sessionId: ProviderSessionId): Effect.Effect<boolean> => Effect.succeed(false));
94− public stopAllImpl = vi.fn((): Effect.Effect<void> => Effect.void);
9519
96− startSession(input: ProviderSessionStartInput): Effect.Effect<ProviderSession> {
97− return this.startSessionImpl(input);
98− }
20+describe("ClaudeCodeAdapterLive", () => {
21+ const layer = makeClaudeCodeAdapterLive();
9922
100− sendTurn(input: ProviderSendTurnInput): Effect.Effect<ProviderTurnStartResult> {
101− return this.sendTurnImpl(input);
102− }
103−
104− interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Effect.Effect<void> {
105− return this.interruptTurnImpl(sessionId, turnId);
106− }
107−
108− readThread(sessionId: ProviderSessionId) {
109− return this.readThreadImpl(sessionId);
110− }
111−
112− rollbackThread(sessionId: ProviderSessionId, numTurns: number) {
113− return this.rollbackThreadImpl(sessionId, numTurns);
114− }
115−
116− respondToRequest(
117− sessionId: ProviderSessionId,
118− requestId: ApprovalRequestId,
119− decision: ProviderApprovalDecision,
120− ): Effect.Effect<void> {
121− return this.respondToRequestImpl(sessionId, requestId, decision);
122− }
123−
124− stopSession(sessionId: ProviderSessionId): Effect.Effect<void> {
125− return this.stopSessionImpl(sessionId);
126− }
127−
128− listSessions(): Effect.Effect<ReadonlyArray<ProviderSession>> {
129− return this.listSessionsImpl();
130− }
131−
132− hasSession(sessionId: ProviderSessionId): Effect.Effect<boolean> {
133− return this.hasSessionImpl(sessionId);
134− }
135−
136− stopAll(): Effect.Effect<void> {
137− return this.stopAllImpl();
138− }
139−
140− emitRuntimeEvent(event: ProviderRuntimeEvent): void {
141− Queue.offerAllUnsafe(this.runtimeEventQueue, [event]);
142− }
143−}
144−
145−const validationRuntime = new FakeClaudeRuntime();
146−const validationLayer = it.layer(makeClaudeCodeAdapterLive({ runtime: validationRuntime }));
147−
148−validationLayer("ClaudeCodeAdapterLive validation", (it) => {
14923 it.effect("returns validation error for non-claudeCode provider on startSession", () =>
15024 Effect.gen(function* () {
15125 const adapter = yield* ClaudeCodeAdapter;
152− const result = yield* adapter
153− .startSession({
154− provider: "codex",
155− })
156− .pipe(Effect.result);
26+ const result = yield* adapter.startSession({ provider: "codex" }).pipe(Effect.result);
15727
158− assertFailure(
159− result,
28+ assert.equal(result._tag, "Failure");
29+ if (result._tag !== "Failure") {
30+ return;
31+ }
32+ assert.equal(result.failure._tag, "ProviderAdapterValidationError");
33+ if (result.failure._tag !== "ProviderAdapterValidationError") {
34+ return;
35+ }
36+ assert.deepEqual(
37+ result.failure,
16038 new ProviderAdapterValidationError({
16139 provider: "claudeCode",
16240 operation: "startSession",
16341 issue: "Expected provider 'claudeCode' but received 'codex'.",
16442 }),
16543 );
166− assert.equal(validationRuntime.startSessionImpl.mock.calls.length, 0);
167− }),
44+ }).pipe(Effect.provide(layer)),
16845 );
169−});
17046
171−const sessionErrorRuntime = new FakeClaudeRuntime();
172−sessionErrorRuntime.sendTurnImpl.mockImplementation(() => {
173− return Effect.fail(new Error("Unknown session: claude-sess-missing"));
174−});
175−const sessionErrorLayer = it.layer(makeClaudeCodeAdapterLive({ runtime: sessionErrorRuntime }));
47+ it.effect("returns typed process error when runtime is not configured", () =>
48+ Effect.gen(function* () {
49+ const adapter = yield* ClaudeCodeAdapter;
50+ const result = yield* adapter.startSession({ provider: "claudeCode" }).pipe(Effect.result);
17651
177−sessionErrorLayer("ClaudeCodeAdapterLive session errors", (it) => {
178− it.effect("maps unknown-session sendTurn errors to ProviderAdapterSessionNotFoundError", () =>
52+ assert.equal(result._tag, "Failure");
53+ if (result._tag !== "Failure") {
54+ return;
55+ }
56+ assert.equal(result.failure._tag, "ProviderAdapterProcessError");
57+ if (result.failure._tag !== "ProviderAdapterProcessError") {
58+ return;
59+ }
60+ assert.deepEqual(
61+ result.failure,
62+ new ProviderAdapterProcessError({
63+ provider: "claudeCode",
64+ sessionId: "pending",
65+ detail: "Claude Code runtime is not configured.",
66+ }),
67+ );
68+ }).pipe(Effect.provide(layer)),
69+ );
70+
71+ it.effect("returns typed request errors for turn operations when runtime is not configured", () =>
17972 Effect.gen(function* () {
18073 const adapter = yield* ClaudeCodeAdapter;
18174 const result = yield* adapter
18275 .sendTurn({
183− sessionId: asSessionId("claude-sess-missing"),
76+ sessionId: asSessionId("claude-sess-1"),
18477 input: "hello",
18578 attachments: [],
18679 })
@@ -190,89 +83,84 @@ sessionErrorLayer("ClaudeCodeAdapterLive session errors", (it) => { · 3 unmodified lines
19083 if (result._tag !== "Failure") {
19184 return;
19285 }
193−
194− assert.equal(result.failure._tag, "ProviderAdapterSessionNotFoundError");
195− if (result.failure._tag !== "ProviderAdapterSessionNotFoundError") {
86+ assert.equal(result.failure._tag, "ProviderAdapterRequestError");
87+ if (result.failure._tag !== "ProviderAdapterRequestError") {
19688 return;
19789 }
198− assert.equal(result.failure.provider, "claudeCode");
199− assert.equal(result.failure.sessionId, "claude-sess-missing");
200− assert.instanceOf(result.failure.cause, Error);
201− }),
90+ assert.deepEqual(
91+ result.failure,
92+ new ProviderAdapterRequestError({
93+ provider: "claudeCode",
94+ method: "turn/start",
95+ detail: "Claude Code runtime is not configured. (session claude-sess-1)",
96+ }),
97+ );
98+ }).pipe(Effect.provide(layer)),
20299 );
203−});
204100
205−const lifecycleRuntime = new FakeClaudeRuntime();
206−const lifecycleLayer = it.layer(makeClaudeCodeAdapterLive({ runtime: lifecycleRuntime }));
207−
208−lifecycleLayer("ClaudeCodeAdapterLive lifecycle", (it) => {
209− it.effect("forwards providerOptions and opaque resumeCursor to runtime startSession", () =>
101+ it.effect("emits no runtime events by default", () =>
210102 Effect.gen(function* () {
211103 const adapter = yield* ClaudeCodeAdapter;
212− const resumeCursor = {
213− threadId: "provider-thread-opaque",
214− sessionAt: "message:42",
215− };
216− const started = yield* adapter.startSession({
217− provider: "claudeCode",
218− cwd: "/tmp/claude-workspace",
219− model: "claude-sonnet-4",
220− resumeCursor,
221− providerOptions: {
222− claudeCode: {
223− binaryPath: "/usr/local/bin/claude",
224− permissionMode: "acceptEdits",
225− maxThinkingTokens: 4_096,
226− },
227− },
228− });
229−
230− assert.equal(started.provider, "claudeCode");
231− assert.deepEqual(started.resumeCursor, resumeCursor);
232− assert.deepEqual(lifecycleRuntime.startSessionImpl.mock.calls[0]?.[0], {
233− provider: "claudeCode",
234− cwd: "/tmp/claude-workspace",
235− model: "claude-sonnet-4",
236− resumeCursor,
237− providerOptions: {
238− claudeCode: {
239− binaryPath: "/usr/local/bin/claude",
240− permissionMode: "acceptEdits",
241− maxThinkingTokens: 4_096,
242− },
243− },
244− });
245− }),
104+ const firstEvent = yield* Stream.runHead(adapter.streamEvents);
105+ assert.equal(firstEvent._tag, "None");
106+ }).pipe(Effect.provide(layer)),
246107 );
108+});
247109
248− it.effect("passes through canonical runtime events without remapping", () =>
110+describe("ClaudeCodeAdapter mocked layer", () => {
111+ it.effect("supports replacing the adapter layer directly in tests", () =>
249112 Effect.gen(function* () {
250113 const adapter = yield* ClaudeCodeAdapter;
251− const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);
252− const runtimeEvent: ProviderRuntimeEvent = {
253− type: "approval.requested",
254− eventId: asEventId("evt-claude-approval"),
255− provider: "claudeCode",
256− sessionId: asSessionId("claude-sess-1"),
257− createdAt: new Date().toISOString(),
258− requestId: asRequestId("approval-claude-1"),
259− requestKind: "command",
260− };
261−
262− lifecycleRuntime.emitRuntimeEvent(runtimeEvent);
263− const firstEvent = yield* Fiber.join(firstEventFiber);
264−
265− assert.equal(firstEvent._tag, "Some");
266− if (firstEvent._tag !== "Some") {
267− return;
268− }
269− assert.deepEqual(firstEvent.value, runtimeEvent);
270− }),
114+ const started = yield* adapter.startSession({ provider: "claudeCode" });
115+ assert.equal(started.sessionId, "mock-sess-1");
116+ const event = yield* Stream.runHead(adapter.streamEvents);
117+ assert.equal(event._tag, "Some");
118+ }).pipe(
119+ Effect.provide(
120+ Layer.succeed(ClaudeCodeAdapter, {
121+ provider: "claudeCode",
122+ startSession: () =>
123+ Effect.succeed({
124+ sessionId: asSessionId("mock-sess-1"),
125+ provider: "claudeCode",
126+ status: "ready",
127+ threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
128+ createdAt: new Date().toISOString(),
129+ updatedAt: new Date().toISOString(),
130+ }),
131+ sendTurn: () =>
132+ Effect.succeed({
133+ threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
134+ turnId: asTurnId("mock-turn-1"),
135+ }),
136+ interruptTurn: () => Effect.void,
137+ readThread: () =>
138+ Effect.succeed({
139+ threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
140+ turns: [],
141+ }),
142+ rollbackThread: () =>
143+ Effect.succeed({
144+ threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
145+ turns: [],
146+ }),
147+ respondToRequest: () => Effect.void,
148+ stopSession: () => Effect.void,
149+ listSessions: () => Effect.succeed([]),
150+ hasSession: () => Effect.succeed(false),
151+ stopAll: () => Effect.void,
152+ streamEvents: Stream.make({
153+ type: "turn.completed",
154+ eventId: asEventId("evt-mock-1"),
155+ provider: "claudeCode",
156+ sessionId: asSessionId("mock-sess-1"),
157+ createdAt: new Date().toISOString(),
158+ threadId: ProviderThreadId.makeUnsafe("mock-thread-1"),
159+ turnId: asTurnId("mock-turn-1"),
160+ status: "completed",
161+ }),
162+ } satisfies ClaudeCodeAdapterShape),
163+ ),
164+ ),
271165 );
272166 });
273−
274−afterAll(() => {
275− assert.equal(validationRuntime.stopAllImpl.mock.calls.length, 1);
276− assert.equal(sessionErrorRuntime.stopAllImpl.mock.calls.length, 1);
277− assert.equal(lifecycleRuntime.stopAllImpl.mock.calls.length, 1);
278−});
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+81−265
@@ -1,289 +1,105 @@
11 /**
22 * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter.
33 *
4− * Wraps a Claude runtime bridge behind the `ClaudeCodeAdapter` service contract
5− * and maps runtime failures into the shared `ProviderAdapterError` algebra.
4+ * Until the Claude runtime bridge is implemented, this adapter is wired
5+ * directly and returns typed "not configured" errors for runtime operations.
66 *
77 * @module ClaudeCodeAdapterLive
88 */
9−import type {
10− ApprovalRequestId,
11− ProviderApprovalDecision,
12− ProviderRuntimeEvent,
13− ProviderSendTurnInput,
14− ProviderSession,
15− ProviderSessionId,
16− ProviderSessionStartInput,
17− ProviderTurnId,
18− ProviderTurnStartResult,
19−} from "@t3tools/contracts";
9+import type { ProviderRuntimeEvent } from "@t3tools/contracts";
2010 import { Effect, Layer, Stream } from "effect";
2111
2212 import {
2313 ProviderAdapterProcessError,
2414 ProviderAdapterRequestError,
25− ProviderAdapterSessionClosedError,
26− ProviderAdapterSessionNotFoundError,
2715 ProviderAdapterValidationError,
28− type ProviderAdapterError,
2916 } from "../Errors.ts";
30−import { type ProviderThreadSnapshot } from "../Services/ProviderAdapter.ts";
3117 import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
3218
3319 const PROVIDER = "claudeCode" as const;
34−
35−export interface ClaudeCodeRuntime {
36− readonly startSession: (
37− input: ProviderSessionStartInput,
38− ) => Effect.Effect<ProviderSession, unknown>;
39− readonly sendTurn: (
40− input: ProviderSendTurnInput,
41− ) => Effect.Effect<ProviderTurnStartResult, unknown>;
42− readonly interruptTurn: (
43− sessionId: ProviderSessionId,
44− turnId?: ProviderTurnId,
45− ) => Effect.Effect<void, unknown>;
46− readonly readThread: (
47− sessionId: ProviderSessionId,
48− ) => Effect.Effect<ProviderThreadSnapshot, unknown>;
49− readonly rollbackThread: (
50− sessionId: ProviderSessionId,
51− numTurns: number,
52− ) => Effect.Effect<ProviderThreadSnapshot, unknown>;
53− readonly respondToRequest: (
54− sessionId: ProviderSessionId,
55− requestId: ApprovalRequestId,
56− decision: ProviderApprovalDecision,
57− ) => Effect.Effect<void, unknown>;
58− readonly stopSession: (sessionId: ProviderSessionId) => Effect.Effect<void>;
59− readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
60− readonly hasSession: (sessionId: ProviderSessionId) => Effect.Effect<boolean>;
61− readonly stopAll: () => Effect.Effect<void>;
62− readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>;
63−}
64−
65−export interface ClaudeCodeAdapterLiveOptions {
66− readonly runtime?: ClaudeCodeRuntime;
67− readonly makeRuntime?: () => ClaudeCodeRuntime;
68−}
69−
70−function toMessage(cause: unknown, fallback: string): string {
71− if (cause instanceof Error && cause.message.length > 0) {
72− return cause.message;
73− }
74− return fallback;
75−}
76−
77−function toSessionError(
78− sessionId: ProviderSessionId,
79− cause: unknown,
80−): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
81− const normalized = toMessage(cause, "").toLowerCase();
82− if (normalized.includes("unknown session") || normalized.includes("unknown provider session")) {
83− return new ProviderAdapterSessionNotFoundError({
84− provider: PROVIDER,
85− sessionId,
86− cause,
87− });
88− }
89− if (normalized.includes("session is closed")) {
90− return new ProviderAdapterSessionClosedError({
91− provider: PROVIDER,
92− sessionId,
93− cause,
94− });
95− }
96− return undefined;
97−}
98−
99−function toRequestError(
100− sessionId: ProviderSessionId,
101− method: string,
102− cause: unknown,
103−): ProviderAdapterError {
104− const sessionError = toSessionError(sessionId, cause);
105− if (sessionError) {
106− return sessionError;
107− }
108− return new ProviderAdapterRequestError({
109− provider: PROVIDER,
110− method,
111− detail: toMessage(cause, `${method} failed`),
112− cause,
113− });
114−}
115−
116−function makeUnavailableRuntime(): ClaudeCodeRuntime {
117− const unavailableDetail = "Claude Code runtime is not configured.";
118−
119− return {
120− startSession: () =>
121− Effect.fail(
122− new ProviderAdapterProcessError({
20+const CLAUDE_RUNTIME_NOT_CONFIGURED = "Claude Code runtime is not configured.";
21+
22+const makeClaudeCodeAdapter = Effect.succeed({
23+ provider: PROVIDER,
24+ startSession: (input) => {
25+ if (input.provider !== undefined && input.provider !== PROVIDER) {
26+ return Effect.fail(
27+ new ProviderAdapterValidationError({
12328 provider: PROVIDER,
124− sessionId: "pending",
125− detail: unavailableDetail,
29+ operation: "startSession",
30+ issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
12631 }),
127− ),
128− sendTurn: (_input) =>
129− Effect.fail(
130− new ProviderAdapterRequestError({
131− provider: PROVIDER,
132− method: "turn/start",
133− detail: unavailableDetail,
134− }),
135− ),
136− interruptTurn: (_sessionId) =>
137− Effect.fail(
138− new ProviderAdapterRequestError({
139− provider: PROVIDER,
140− method: "turn/interrupt",
141− detail: unavailableDetail,
142− }),
143− ),
144− readThread: (_sessionId) =>
145− Effect.fail(
146− new ProviderAdapterRequestError({
147− provider: PROVIDER,
148− method: "thread/read",
149− detail: unavailableDetail,
150− }),
151− ),
152− rollbackThread: (_sessionId) =>
153− Effect.fail(
154− new ProviderAdapterRequestError({
155− provider: PROVIDER,
156− method: "thread/rollback",
157− detail: unavailableDetail,
158− }),
159− ),
160− respondToRequest: (_sessionId) =>
161− Effect.fail(
162− new ProviderAdapterRequestError({
163− provider: PROVIDER,
164− method: "item/requestApproval/decision",
165− detail: unavailableDetail,
166− }),
167− ),
168− stopSession: () => Effect.void,
169− listSessions: () => Effect.succeed([]),
170− hasSession: () => Effect.succeed(false),
171− stopAll: () => Effect.void,
172− streamEvents: Stream.empty,
173− };
174−}
32+ );
33+ }
17534
176−const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) =>
177− Effect.gen(function* () {
178− const runtime = yield* Effect.acquireRelease(
179− Effect.sync(() => {
180− if (options?.runtime) {
181− return options.runtime;
182− }
183− if (options?.makeRuntime) {
184− return options.makeRuntime();
185− }
186− return makeUnavailableRuntime();
35+ return Effect.fail(
36+ new ProviderAdapterProcessError({
37+ provider: PROVIDER,
38+ sessionId: "pending",
39+ detail: CLAUDE_RUNTIME_NOT_CONFIGURED,
18740 }),
188− (runtime) => runtime.stopAll(),
18941 );
190−
191− const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => {
192− if (input.provider !== undefined && input.provider !== PROVIDER) {
193− return Effect.fail(
194− new ProviderAdapterValidationError({
195− provider: PROVIDER,
196− operation: "startSession",
197− issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
198− }),
199− );
200− }
201−
202− return runtime.startSession(input).pipe(
203− Effect.mapError(
204− (cause) =>
205− new ProviderAdapterProcessError({
206− provider: PROVIDER,
207− sessionId: "pending",
208− detail: toMessage(cause, "Failed to start Claude Code adapter session."),
209− cause,
210− }),
211− ),
42+ },
43+ sendTurn: (input) =>
44+ Effect.fail(
45+ new ProviderAdapterRequestError({
46+ provider: PROVIDER,
47+ method: "turn/start",
48+ detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${input.sessionId})`,
49+ }),
50+ ),
51+ interruptTurn: (sessionId, _turnId) =>
52+ Effect.fail(
53+ new ProviderAdapterRequestError({
54+ provider: PROVIDER,
55+ method: "turn/interrupt",
56+ detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,
57+ }),
58+ ),
59+ readThread: (sessionId) =>
60+ Effect.fail(
61+ new ProviderAdapterRequestError({
62+ provider: PROVIDER,
63+ method: "thread/read",
64+ detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,
65+ }),
66+ ),
67+ rollbackThread: (sessionId, numTurns) => {
68+ if (!Number.isInteger(numTurns) || numTurns < 1) {
69+ return Effect.fail(
70+ new ProviderAdapterValidationError({
71+ provider: PROVIDER,
72+ operation: "rollbackThread",
73+ issue: "numTurns must be an integer >= 1.",
74+ }),
21275 );
213− };
214−
215− const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) =>
216− runtime
217− .sendTurn(input)
218− .pipe(Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)));
219−
220− const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, turnId) =>
221− runtime
222− .interruptTurn(sessionId, turnId)
223− .pipe(Effect.mapError((cause) => toRequestError(sessionId, "turn/interrupt", cause)));
224−
225− const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) =>
226− runtime
227− .readThread(sessionId)
228− .pipe(Effect.mapError((cause) => toRequestError(sessionId, "thread/read", cause)));
229−
230− const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) => {
231− if (!Number.isInteger(numTurns) || numTurns < 1) {
232− return Effect.fail(
233− new ProviderAdapterValidationError({
234− provider: PROVIDER,
235− operation: "rollbackThread",
236− issue: "numTurns must be an integer >= 1.",
237− }),
238− );
239− }
76+ }
24077
241− return runtime
242− .rollbackThread(sessionId, numTurns)
243− .pipe(Effect.mapError((cause) => toRequestError(sessionId, "thread/rollback", cause)));
244− };
245−
246− const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = (
247− sessionId,
248− requestId,
249− decision,
250− ) =>
251− runtime
252− .respondToRequest(sessionId, requestId, decision)
253− .pipe(
254− Effect.mapError((cause) =>
255− toRequestError(sessionId, "item/requestApproval/decision", cause),
256− ),
257− );
258−
259− const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) =>
260− runtime.stopSession(sessionId);
261−
262− const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => runtime.listSessions();
263−
264− const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) =>
265− runtime.hasSession(sessionId);
266−
267− const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => runtime.stopAll();
268−
269− return {
270− provider: PROVIDER,
271− startSession,
272− sendTurn,
273− interruptTurn,
274− readThread,
275− rollbackThread,
276− respondToRequest,
277− stopSession,
278− listSessions,
279− hasSession,
280− stopAll,
281− streamEvents: runtime.streamEvents,
282− } satisfies ClaudeCodeAdapterShape;
283− });
284−
285−export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter());
286−
287−export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) {
288− return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options));
78+ return Effect.fail(
79+ new ProviderAdapterRequestError({
80+ provider: PROVIDER,
81+ method: "thread/rollback",
82+ detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,
83+ }),
84+ );
85+ },
86+ respondToRequest: (sessionId, _requestId, _decision) =>
87+ Effect.fail(
88+ new ProviderAdapterRequestError({
89+ provider: PROVIDER,
90+ method: "item/requestApproval/decision",
91+ detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,
92+ }),
93+ ),
94+ stopSession: (_sessionId) => Effect.void,
95+ listSessions: () => Effect.succeed([]),
96+ hasSession: (_sessionId) => Effect.succeed(false),
97+ stopAll: () => Effect.void,
98+ streamEvents: Stream.empty as Stream.Stream<ProviderRuntimeEvent>,
99+} satisfies ClaudeCodeAdapterShape);
100+
101+export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);
102+
103+export function makeClaudeCodeAdapterLive() {
104+ return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);
289105 }