authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-27 09:25:30-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log291411a45e29f49c5e47dc15cb6b199f08634b32
tree9a853dfa50f6e10e4dbb17408199922156847e87
parent87abc22f39afcae72ab4a16e3b65c2be427ec851

Inline Claude adapter behavior into live layer

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 202 insertions(+), 498 deletions(-)

apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+121-233
......@@ -1,186 +1,79 @@
1import type {
2 ProviderApprovalDecision,
3 ProviderRuntimeEvent,
4 ProviderSendTurnInput,
5 ProviderSession,
6 ProviderSessionStartInput,
7 ProviderTurnStartResult,
8} from "@t3tools/contracts";
9import {
10 ApprovalRequestId,
11 EventId,
12 ProviderSessionId,
13 ProviderThreadId,
14 ProviderTurnId,
15} from "@t3tools/contracts";
16import { afterAll, assert, it, vi } from "@effect/vitest";
17import { assertFailure } from "@effect/vitest/utils";
18
19import { Effect, Fiber, Queue, Stream } from "effect";
1import { EventId, ProviderSessionId, ProviderThreadId, ProviderTurnId } from "@t3tools/contracts";
2import { assert, describe, it } from "@effect/vitest";
3import { Effect, Layer, Stream } from "effect";
204
21import { ProviderAdapterValidationError } from "../Errors.ts";
22import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
23import { type ClaudeCodeRuntime, makeClaudeCodeAdapterLive } from "./ClaudeCodeAdapter.ts";
5import {
6 ProviderAdapterProcessError,
7 ProviderAdapterRequestError,
8 ProviderAdapterValidationError,
9} from "../Errors.ts";
10import {
11 ClaudeCodeAdapter,
12 type ClaudeCodeAdapterShape,
13} from "../Services/ClaudeCodeAdapter.ts";
14import { makeClaudeCodeAdapterLive } from "./ClaudeCodeAdapter.ts";
2415
2516const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value);
2617const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);
2718const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
28const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.makeUnsafe(value);
29
30class 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 }
20describe("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
145const validationRuntime = new FakeClaudeRuntime();
146const validationLayer = it.layer(makeClaudeCodeAdapterLive({ runtime: validationRuntime }));
147
148validationLayer("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
171const sessionErrorRuntime = new FakeClaudeRuntime();
172sessionErrorRuntime.sendTurnImpl.mockImplementation(() => {
173 return Effect.fail(new Error("Unknown session: claude-sess-missing"));
174});
175const 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
177sessionErrorLayer("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) => {
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
205const lifecycleRuntime = new FakeClaudeRuntime();
206const lifecycleLayer = it.layer(makeClaudeCodeAdapterLive({ runtime: lifecycleRuntime }));
207
208lifecycleLayer("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", () =>
110describe("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
274afterAll(() => {
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 */
9import type {
10 ApprovalRequestId,
11 ProviderApprovalDecision,
12 ProviderRuntimeEvent,
13 ProviderSendTurnInput,
14 ProviderSession,
15 ProviderSessionId,
16 ProviderSessionStartInput,
17 ProviderTurnId,
18 ProviderTurnStartResult,
19} from "@t3tools/contracts";
9import type { ProviderRuntimeEvent } from "@t3tools/contracts";
2010import { Effect, Layer, Stream } from "effect";
2111
2212import {
2313 ProviderAdapterProcessError,
2414 ProviderAdapterRequestError,
25 ProviderAdapterSessionClosedError,
26 ProviderAdapterSessionNotFoundError,
2715 ProviderAdapterValidationError,
28 type ProviderAdapterError,
2916} from "../Errors.ts";
30import { type ProviderThreadSnapshot } from "../Services/ProviderAdapter.ts";
3117import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
3218
3319const PROVIDER = "claudeCode" as const;
34
35export 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
65export interface ClaudeCodeAdapterLiveOptions {
66 readonly runtime?: ClaudeCodeRuntime;
67 readonly makeRuntime?: () => ClaudeCodeRuntime;
68}
69
70function 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
77function 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
99function 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
116function makeUnavailableRuntime(): ClaudeCodeRuntime {
117 const unavailableDetail = "Claude Code runtime is not configured.";
118
119 return {
120 startSession: () =>
121 Effect.fail(
122 new ProviderAdapterProcessError({
20const CLAUDE_RUNTIME_NOT_CONFIGURED = "Claude Code runtime is not configured.";
21
22const 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
176const 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
285export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter());
286
287export 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
101export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);
102
103export function makeClaudeCodeAdapterLive() {
104 return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter);
289105}