History

Integrate Claude agent SDK into server provider adapter

- add `@anthropic-ai/claude-agent-sdk` dependency for `apps/server` - replace placeholder Claude adapter with live session/query/event handling - add comprehensive adapter tests for runtime events, approvals, resume, rollback, and model overrides

Julius Marminge committed 8 months ago

Commit e5f12e61 parent f00d4145 files · +1666 −206Browse files

Unified · Split

5 files
  1. apps/server/package.json +1 −0
  2. apps/server/src/main.test.ts +1 −1
  3. apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +465 −123
  4. apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +1164 −82
  5. bun.lock +35 −0
apps/server/package.json+1−0
@@ -18,6 +18,7 @@ · 17 unmodified lines
1818 "test": "vitest run"
1919 },
2020 "dependencies": {
21+ "@anthropic-ai/claude-agent-sdk": "^0.2.62",
2122 "@effect/platform-node": "catalog:",
2223 "@effect/sql-sqlite-bun": "catalog:",
2324 "@pierre/diffs": "^1.1.0-beta.16",
apps/server/src/main.test.ts+1−1
@@ -62,7 +62,7 @@ beforeEach(() => { · 61 unmodified lines
6262 findAvailablePort.mockImplementation((preferred: number) => Effect.succeed(preferred));
6363 });
6464
65−it.layer(testLayer)("server cli", (it) => {
65+it.layer(testLayer)("server CLI command", (it) => {
6666 it.effect("parses all CLI flags and wires scoped start/stop", () =>
6767 Effect.gen(function* () {
6868 yield* runCli([
apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts+465−123
@@ -1,27 +1,157 @@
1−import { EventId, ProviderSessionId, ProviderThreadId, ProviderTurnId } from "@t3tools/contracts";
1+import type {
2+ Options as ClaudeQueryOptions,
3+ PermissionMode,
4+ PermissionResult,
5+ SDKMessage,
6+ SDKUserMessage,
7+} from "@anthropic-ai/claude-agent-sdk";
8+import { ApprovalRequestId, type ProviderTurnId } from "@t3tools/contracts";
29 import { assert, describe, it } from "@effect/vitest";
3−import { Effect, Layer, Stream } from "effect";
10+import { Effect, Fiber, Random, Stream } from "effect";
411
512 import {
6− ProviderAdapterProcessError,
7− ProviderAdapterRequestError,
813 ProviderAdapterValidationError,
914 } from "../Errors.ts";
15+import { ClaudeCodeAdapter } from "../Services/ClaudeCodeAdapter.ts";
1016 import {
11− ClaudeCodeAdapter,
12− type ClaudeCodeAdapterShape,
13−} from "../Services/ClaudeCodeAdapter.ts";
14−import { makeClaudeCodeAdapterLive } from "./ClaudeCodeAdapter.ts";
17+ makeClaudeCodeAdapterLive,
18+ type ClaudeCodeAdapterLiveOptions,
19+} from "./ClaudeCodeAdapter.ts";
1520
16−const asSessionId = (value: string): ProviderSessionId => ProviderSessionId.makeUnsafe(value);
17−const asTurnId = (value: string): ProviderTurnId => ProviderTurnId.makeUnsafe(value);
18−const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
21+class FakeClaudeQuery implements AsyncIterable<SDKMessage> {
22+ private readonly queue: Array<SDKMessage> = [];
23+ private readonly resolvers: Array<(value: IteratorResult<SDKMessage>) => void> = [];
24+ private done = false;
1925
20−describe("ClaudeCodeAdapterLive", () => {
21− const layer = makeClaudeCodeAdapterLive();
26+ public readonly interruptCalls: Array<void> = [];
27+ public readonly setModelCalls: Array<string | undefined> = [];
28+ public readonly setPermissionModeCalls: Array<string> = [];
29+ public readonly setMaxThinkingTokensCalls: Array<number | null> = [];
30+ public closeCalls = 0;
31+
32+ emit(message: SDKMessage): void {
33+ if (this.done) {
34+ return;
35+ }
36+ const resolver = this.resolvers.shift();
37+ if (resolver) {
38+ resolver({ done: false, value: message });
39+ return;
40+ }
41+ this.queue.push(message);
42+ }
43+
44+ finish(): void {
45+ if (this.done) {
46+ return;
47+ }
48+ this.done = true;
49+ for (const resolver of this.resolvers.splice(0)) {
50+ resolver({ done: true, value: undefined });
51+ }
52+ }
53+
54+ readonly interrupt = async (): Promise<void> => {
55+ this.interruptCalls.push(undefined);
56+ };
57+
58+ readonly setModel = async (model?: string): Promise<void> => {
59+ this.setModelCalls.push(model);
60+ };
61+
62+ readonly setPermissionMode = async (mode: PermissionMode): Promise<void> => {
63+ this.setPermissionModeCalls.push(mode);
64+ };
65+
66+ readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise<void> => {
67+ this.setMaxThinkingTokensCalls.push(maxThinkingTokens);
68+ };
69+
70+ readonly close = (): void => {
71+ this.closeCalls += 1;
72+ this.finish();
73+ };
74+
75+ [Symbol.asyncIterator](): AsyncIterator<SDKMessage> {
76+ return {
77+ next: () => {
78+ if (this.queue.length > 0) {
79+ const value = this.queue.shift();
80+ if (value) {
81+ return Promise.resolve({
82+ done: false,
83+ value,
84+ });
85+ }
86+ }
87+ if (this.done) {
88+ return Promise.resolve({
89+ done: true,
90+ value: undefined,
91+ });
92+ }
93+ return new Promise((resolve) => {
94+ this.resolvers.push(resolve);
95+ });
96+ },
97+ };
98+ }
99+}
100+
101+interface Harness {
102+ readonly layer: ReturnType<typeof makeClaudeCodeAdapterLive>;
103+ readonly query: FakeClaudeQuery;
104+ readonly getLastCreateQueryInput: () =>
105+ | {
106+ readonly prompt: AsyncIterable<SDKUserMessage>;
107+ readonly options: ClaudeQueryOptions;
108+ }
109+ | undefined;
110+}
111+
112+function makeHarness(): Harness {
113+ const query = new FakeClaudeQuery();
114+ let createInput:
115+ | {
116+ readonly prompt: AsyncIterable<SDKUserMessage>;
117+ readonly options: ClaudeQueryOptions;
118+ }
119+ | undefined;
120+
121+ const options: ClaudeCodeAdapterLiveOptions = {
122+ createQuery: (input) => {
123+ createInput = input;
124+ return query;
125+ },
126+ };
127+
128+ return {
129+ layer: makeClaudeCodeAdapterLive(options),
130+ query,
131+ getLastCreateQueryInput: () => createInput,
132+ };
133+}
22134
23− it.effect("returns validation error for non-claudeCode provider on startSession", () =>
24− Effect.gen(function* () {
135+function makeDeterministicRandomService(seed = 0x1234_5678): {
136+ nextIntUnsafe: () => number;
137+ nextDoubleUnsafe: () => number;
138+} {
139+ let state = seed >>> 0;
140+ const nextIntUnsafe = (): number => {
141+ state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0;
142+ return state;
143+ };
144+
145+ return {
146+ nextIntUnsafe,
147+ nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000,
148+ };
149+}
150+
151+describe("ClaudeCodeAdapterLive", () => {
152+ it.effect("returns validation error for non-claudeCode provider on startSession", () => {
153+ const harness = makeHarness();
154+ return Effect.gen(function* () {
25155 const adapter = yield* ClaudeCodeAdapter;
26156 const result = yield* adapter.startSession({ provider: "codex" }).pipe(Effect.result);
27157
@@ -29,10 +159,6 @@ describe("ClaudeCodeAdapterLive", () => { · 1 unmodified line
29159 if (result._tag !== "Failure") {
30160 return;
31161 }
32− assert.equal(result.failure._tag, "ProviderAdapterValidationError");
33− if (result.failure._tag !== "ProviderAdapterValidationError") {
34− return;
35− }
36162 assert.deepEqual(
37163 result.failure,
38164 new ProviderAdapterValidationError({
@@ -41,126 +167,342 @@ describe("ClaudeCodeAdapterLive", () => { · 2 unmodified lines
41167 issue: "Expected provider 'claudeCode' but received 'codex'.",
42168 }),
43169 );
44− }).pipe(Effect.provide(layer)),
45− );
170+ }).pipe(
171+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
172+ Effect.provide(harness.layer),
173+ );
174+ });
46175
47− it.effect("returns typed process error when runtime is not configured", () =>
48− Effect.gen(function* () {
176+ it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => {
177+ const harness = makeHarness();
178+ return Effect.gen(function* () {
49179 const adapter = yield* ClaudeCodeAdapter;
50− const result = yield* adapter.startSession({ provider: "claudeCode" }).pipe(Effect.result);
51180
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− }
181+ const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe(
182+ Stream.runCollect,
183+ Effect.forkChild,
184+ );
185+
186+ const session = yield* adapter.startSession({
187+ provider: "claudeCode",
188+ model: "claude-sonnet-4-5",
189+ });
190+
191+ const turn = yield* adapter.sendTurn({
192+ sessionId: session.sessionId,
193+ input: "hello",
194+ attachments: [],
195+ });
196+
197+ harness.query.emit({
198+ type: "stream_event",
199+ session_id: "sdk-session-1",
200+ uuid: "stream-1",
201+ parent_tool_use_id: null,
202+ event: {
203+ type: "content_block_delta",
204+ index: 0,
205+ delta: {
206+ type: "text_delta",
207+ text: "Hi",
208+ },
209+ },
210+ } as unknown as SDKMessage);
211+
212+ harness.query.emit({
213+ type: "stream_event",
214+ session_id: "sdk-session-1",
215+ uuid: "stream-2",
216+ parent_tool_use_id: null,
217+ event: {
218+ type: "content_block_start",
219+ index: 1,
220+ content_block: {
221+ type: "tool_use",
222+ id: "tool-1",
223+ name: "Bash",
224+ input: {
225+ command: "ls",
226+ },
227+ },
228+ },
229+ } as unknown as SDKMessage);
230+
231+ harness.query.emit({
232+ type: "stream_event",
233+ session_id: "sdk-session-1",
234+ uuid: "stream-3",
235+ parent_tool_use_id: null,
236+ event: {
237+ type: "content_block_stop",
238+ index: 1,
239+ },
240+ } as unknown as SDKMessage);
241+
242+ harness.query.emit({
243+ type: "assistant",
244+ session_id: "sdk-session-1",
245+ uuid: "assistant-1",
246+ parent_tool_use_id: null,
247+ message: {
248+ id: "assistant-message-1",
249+ content: [{ type: "text", text: "Hi" }],
250+ },
251+ } as unknown as SDKMessage);
252+
253+ harness.query.emit({
254+ type: "result",
255+ subtype: "success",
256+ is_error: false,
257+ errors: [],
258+ session_id: "sdk-session-1",
259+ uuid: "result-1",
260+ } as unknown as SDKMessage);
261+
262+ const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
60263 assert.deepEqual(
61− result.failure,
62− new ProviderAdapterProcessError({
63− provider: "claudeCode",
64− sessionId: "pending",
65− detail: "Claude Code runtime is not configured.",
66− }),
264+ runtimeEvents.map((event) => event.type),
265+ [
266+ "session.started",
267+ "turn.started",
268+ "thread.started",
269+ "message.delta",
270+ "tool.started",
271+ "tool.completed",
272+ "message.completed",
273+ "turn.completed",
274+ ],
67275 );
68− }).pipe(Effect.provide(layer)),
69− );
70276
71− it.effect("returns typed request errors for turn operations when runtime is not configured", () =>
72− Effect.gen(function* () {
277+ const turnStarted = runtimeEvents[1];
278+ assert.equal(turnStarted?.type, "turn.started");
279+ if (turnStarted?.type === "turn.started") {
280+ assert.equal(turnStarted.turnId, turn.turnId);
281+ }
282+
283+ const deltaEvent = runtimeEvents[3];
284+ assert.equal(deltaEvent?.type, "message.delta");
285+ if (deltaEvent?.type === "message.delta") {
286+ assert.equal(deltaEvent.delta, "Hi");
287+ assert.equal(deltaEvent.turnId, turn.turnId);
288+ }
289+
290+ const toolStarted = runtimeEvents[4];
291+ assert.equal(toolStarted?.type, "tool.started");
292+ if (toolStarted?.type === "tool.started") {
293+ assert.equal(toolStarted.toolKind, "command");
294+ }
295+
296+ const turnCompleted = runtimeEvents[7];
297+ assert.equal(turnCompleted?.type, "turn.completed");
298+ if (turnCompleted?.type === "turn.completed") {
299+ assert.equal(turnCompleted.turnId, turn.turnId);
300+ assert.equal(turnCompleted.status, "completed");
301+ }
302+ }).pipe(
303+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
304+ Effect.provide(harness.layer),
305+ );
306+ });
307+
308+ it.effect("bridges approval request/response lifecycle through canUseTool", () => {
309+ const harness = makeHarness();
310+ return Effect.gen(function* () {
73311 const adapter = yield* ClaudeCodeAdapter;
74− const result = yield* adapter
75− .sendTurn({
76− sessionId: asSessionId("claude-sess-1"),
77− input: "hello",
78− attachments: [],
79− })
80− .pipe(Effect.result);
81312
82− assert.equal(result._tag, "Failure");
83− if (result._tag !== "Failure") {
313+ const session = yield* adapter.startSession({
314+ provider: "claudeCode",
315+ approvalPolicy: "on-request",
316+ });
317+
318+ const consumeSessionStarted = yield* Stream.runHead(adapter.streamEvents);
319+ assert.equal(consumeSessionStarted._tag, "Some");
320+
321+ const createInput = harness.getLastCreateQueryInput();
322+ const canUseTool = createInput?.options.canUseTool;
323+ assert.equal(typeof canUseTool, "function");
324+ if (!canUseTool) {
84325 return;
85326 }
86− assert.equal(result.failure._tag, "ProviderAdapterRequestError");
87− if (result.failure._tag !== "ProviderAdapterRequestError") {
327+
328+ const permissionPromise = canUseTool(
329+ "Bash",
330+ { command: "pwd" },
331+ {
332+ signal: new AbortController().signal,
333+ suggestions: [
334+ {
335+ type: "setMode",
336+ mode: "default",
337+ destination: "session",
338+ },
339+ ],
340+ toolUseID: "tool-use-1",
341+ },
342+ );
343+
344+ const requested = yield* Stream.runHead(adapter.streamEvents);
345+ assert.equal(requested._tag, "Some");
346+ if (requested._tag !== "Some") {
88347 return;
89348 }
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− }),
349+ assert.equal(requested.value.type, "approval.requested");
350+ if (requested.value.type !== "approval.requested") {
351+ return;
352+ }
353+
354+ yield* adapter.respondToRequest(
355+ session.sessionId,
356+ ApprovalRequestId.makeUnsafe(requested.value.requestId),
357+ "accept",
97358 );
98− }).pipe(Effect.provide(layer)),
99− );
100359
101− it.effect("emits no runtime events by default", () =>
102− Effect.gen(function* () {
360+ const resolved = yield* Stream.runHead(adapter.streamEvents);
361+ assert.equal(resolved._tag, "Some");
362+ if (resolved._tag !== "Some") {
363+ return;
364+ }
365+ assert.equal(resolved.value.type, "approval.resolved");
366+ if (resolved.value.type !== "approval.resolved") {
367+ return;
368+ }
369+ assert.equal(resolved.value.requestId, requested.value.requestId);
370+ assert.equal(resolved.value.decision, "accept");
371+
372+ const permissionResult = yield* Effect.promise(() => permissionPromise);
373+ assert.equal((permissionResult as PermissionResult).behavior, "allow");
374+ }).pipe(
375+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
376+ Effect.provide(harness.layer),
377+ );
378+ });
379+
380+ it.effect("passes parsed resume cursor values to Claude query options", () => {
381+ const harness = makeHarness();
382+ return Effect.gen(function* () {
103383 const adapter = yield* ClaudeCodeAdapter;
104− const firstEvent = yield* Stream.runHead(adapter.streamEvents);
105− assert.equal(firstEvent._tag, "None");
106− }).pipe(Effect.provide(layer)),
107− );
108−});
109384
110−describe("ClaudeCodeAdapter mocked layer", () => {
111− it.effect("supports replacing the adapter layer directly in tests", () =>
112− Effect.gen(function* () {
385+ const session = yield* adapter.startSession({
386+ provider: "claudeCode",
387+ resumeCursor: {
388+ threadId: "resume-thread-1",
389+ resume: "resume-session-1",
390+ resumeSessionAt: "assistant-99",
391+ turnCount: 3,
392+ },
393+ });
394+
395+ assert.equal(session.threadId, "resume-thread-1");
396+ assert.deepEqual(session.resumeCursor, {
397+ threadId: "resume-thread-1",
398+ resume: "resume-session-1",
399+ resumeSessionAt: "assistant-99",
400+ turnCount: 3,
401+ });
402+
403+ const createInput = harness.getLastCreateQueryInput();
404+ assert.equal(createInput?.options.resume, "resume-session-1");
405+ assert.equal(createInput?.options.resumeSessionAt, "assistant-99");
406+ }).pipe(
407+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
408+ Effect.provide(harness.layer),
409+ );
410+ });
411+
412+ it.effect("supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => {
413+ const harness = makeHarness();
414+ return Effect.gen(function* () {
415+ const adapter = yield* ClaudeCodeAdapter;
416+
417+ const session = yield* adapter.startSession({
418+ provider: "claudeCode",
419+ });
420+
421+ const firstTurn = yield* adapter.sendTurn({
422+ sessionId: session.sessionId,
423+ input: "first",
424+ attachments: [],
425+ });
426+
427+ const firstCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe(
428+ Stream.runHead,
429+ Effect.forkChild,
430+ );
431+
432+ harness.query.emit({
433+ type: "result",
434+ subtype: "success",
435+ is_error: false,
436+ errors: [],
437+ session_id: "sdk-session-rollback",
438+ uuid: "result-first",
439+ } as unknown as SDKMessage);
440+
441+ const firstCompleted = yield* Fiber.join(firstCompletedFiber);
442+ assert.equal(firstCompleted._tag, "Some");
443+ if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") {
444+ assert.equal(firstCompleted.value.turnId, firstTurn.turnId);
445+ }
446+
447+ const secondTurn = yield* adapter.sendTurn({
448+ sessionId: session.sessionId,
449+ input: "second",
450+ attachments: [],
451+ });
452+
453+ const secondCompletedFiber = yield* Stream.filter(adapter.streamEvents, (event) => event.type === "turn.completed").pipe(
454+ Stream.runHead,
455+ Effect.forkChild,
456+ );
457+
458+ harness.query.emit({
459+ type: "result",
460+ subtype: "success",
461+ is_error: false,
462+ errors: [],
463+ session_id: "sdk-session-rollback",
464+ uuid: "result-second",
465+ } as unknown as SDKMessage);
466+
467+ const secondCompleted = yield* Fiber.join(secondCompletedFiber);
468+ assert.equal(secondCompleted._tag, "Some");
469+ if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") {
470+ assert.equal(secondCompleted.value.turnId, secondTurn.turnId);
471+ }
472+
473+ const threadBeforeRollback = yield* adapter.readThread(session.sessionId);
474+ assert.equal(threadBeforeRollback.turns.length, 2);
475+
476+ const rolledBack = yield* adapter.rollbackThread(session.sessionId, 1);
477+ assert.equal(rolledBack.turns.length, 1);
478+ assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId as ProviderTurnId);
479+
480+ const threadAfterRollback = yield* adapter.readThread(session.sessionId);
481+ assert.equal(threadAfterRollback.turns.length, 1);
482+ assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId as ProviderTurnId);
483+ }).pipe(
484+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
485+ Effect.provide(harness.layer),
486+ );
487+ });
488+
489+ it.effect("updates model on sendTurn when model override is provided", () => {
490+ const harness = makeHarness();
491+ return Effect.gen(function* () {
113492 const adapter = yield* ClaudeCodeAdapter;
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");
493+
494+ const session = yield* adapter.startSession({ provider: "claudeCode" });
495+ yield* adapter.sendTurn({
496+ sessionId: session.sessionId,
497+ input: "hello",
498+ model: "claude-opus-4-6",
499+ attachments: [],
500+ });
501+
502+ assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]);
118503 }).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− ),
165− );
504+ Effect.provideService(Random.Random, makeDeterministicRandomService()),
505+ Effect.provide(harness.layer),
506+ );
507+ });
166508 });
apps/server/src/provider/Layers/ClaudeCodeAdapter.ts+1164−82
@@ -1,105 +1,1187 @@
11 /**
22 * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter.
33 *
4− * Until the Claude runtime bridge is implemented, this adapter is wired
5− * directly and returns typed "not configured" errors for runtime operations.
4+ * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic
5+ * provider adapter contract and emits canonical runtime events.
66 *
77 * @module ClaudeCodeAdapterLive
88 */
9−import type { ProviderRuntimeEvent } from "@t3tools/contracts";
10−import { Effect, Layer, Stream } from "effect";
9+import {
10+ type CanUseTool,
11+ query,
12+ type Options as ClaudeQueryOptions,
13+ type PermissionMode,
14+ type PermissionResult,
15+ type PermissionUpdate,
16+ type SDKMessage,
17+ type SDKResultMessage,
18+ type SDKUserMessage,
19+} from "@anthropic-ai/claude-agent-sdk";
20+import {
21+ ApprovalRequestId,
22+ EventId,
23+ type ProviderApprovalDecision,
24+ ProviderItemId,
25+ type ProviderRuntimeEvent,
26+ type ProviderRuntimeToolKind,
27+ type ProviderRuntimeTurnStatus,
28+ type ProviderSendTurnInput,
29+ ProviderSessionId,
30+ type ProviderSession,
31+ ProviderThreadId,
32+ ProviderTurnId,
33+} from "@t3tools/contracts";
34+import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Stream } from "effect";
1135
1236 import {
1337 ProviderAdapterProcessError,
1438 ProviderAdapterRequestError,
39+ ProviderAdapterSessionClosedError,
40+ ProviderAdapterSessionNotFoundError,
1541 ProviderAdapterValidationError,
42+ type ProviderAdapterError,
1643 } from "../Errors.ts";
1744 import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts";
1845
1946 const PROVIDER = "claudeCode" as const;
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({
28− provider: PROVIDER,
29− operation: "startSession",
30− issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
31− }),
47+
48+type PromptQueueItem =
49+ | {
50+ readonly type: "message";
51+ readonly message: SDKUserMessage;
52+ }
53+ | {
54+ readonly type: "terminate";
55+ };
56+
57+interface ClaudeResumeState {
58+ readonly threadId?: ProviderThreadId;
59+ readonly resume?: string;
60+ readonly resumeSessionAt?: string;
61+ readonly turnCount?: number;
62+}
63+
64+interface ClaudeTurnState {
65+ readonly turnId: ProviderTurnId;
66+ readonly assistantItemId: string;
67+ readonly startedAt: string;
68+ readonly items: Array<unknown>;
69+ readonly messageCompleted: boolean;
70+}
71+
72+interface PendingApproval {
73+ readonly requestKind: "command" | "file-change";
74+ readonly detail?: string;
75+ readonly suggestions?: ReadonlyArray<PermissionUpdate>;
76+ readonly decision: Deferred.Deferred<ProviderApprovalDecision>;
77+}
78+
79+interface ToolInFlight {
80+ readonly itemId: string;
81+ readonly toolKind: ProviderRuntimeToolKind;
82+ readonly title: string;
83+ readonly detail?: string;
84+}
85+
86+interface ClaudeSessionContext {
87+ session: ProviderSession;
88+ readonly promptQueue: Queue.Queue<PromptQueueItem>;
89+ readonly query: ClaudeQueryRuntime;
90+ readonly startedAt: string;
91+ readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;
92+ readonly turns: Array<{
93+ id: ReturnType<typeof ProviderTurnId.makeUnsafe>;
94+ items: Array<unknown>;
95+ }>;
96+ readonly inFlightTools: Map<number, ToolInFlight>;
97+ turnState: ClaudeTurnState | undefined;
98+ lastAssistantUuid: string | undefined;
99+ lastThreadStartedId: ProviderThreadId | undefined;
100+ stopped: boolean;
101+}
102+
103+interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {
104+ readonly interrupt: () => Promise<void>;
105+ readonly setModel: (model?: string) => Promise<void>;
106+ readonly setPermissionMode: (mode: PermissionMode) => Promise<void>;
107+ readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>;
108+ readonly close: () => void;
109+}
110+
111+export interface ClaudeCodeAdapterLiveOptions {
112+ readonly createQuery?: (input: {
113+ readonly prompt: AsyncIterable<SDKUserMessage>;
114+ readonly options: ClaudeQueryOptions;
115+ }) => ClaudeQueryRuntime;
116+}
117+
118+function toMessage(cause: unknown, fallback: string): string {
119+ if (cause instanceof Error && cause.message.length > 0) {
120+ return cause.message;
121+ }
122+ return fallback;
123+}
124+
125+function toPermissionMode(value: unknown): PermissionMode | undefined {
126+ switch (value) {
127+ case "default":
128+ case "acceptEdits":
129+ case "bypassPermissions":
130+ case "plan":
131+ case "dontAsk":
132+ return value;
133+ default:
134+ return undefined;
135+ }
136+}
137+
138+function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined {
139+ if (!resumeCursor || typeof resumeCursor !== "object") {
140+ return undefined;
141+ }
142+ const cursor = resumeCursor as {
143+ threadId?: unknown;
144+ resume?: unknown;
145+ sessionId?: unknown;
146+ resumeSessionAt?: unknown;
147+ turnCount?: unknown;
148+ };
149+
150+ const threadId = typeof cursor.threadId === "string" ? cursor.threadId : undefined;
151+ const resume =
152+ typeof cursor.resume === "string"
153+ ? cursor.resume
154+ : typeof cursor.sessionId === "string"
155+ ? cursor.sessionId
156+ : undefined;
157+ const resumeSessionAt =
158+ typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined;
159+ const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined;
160+
161+ return {
162+ ...(threadId ? { threadId: ProviderThreadId.makeUnsafe(threadId) } : {}),
163+ ...(resume ? { resume } : {}),
164+ ...(resumeSessionAt ? { resumeSessionAt } : {}),
165+ ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0
166+ ? { turnCount: turnCountValue }
167+ : {}),
168+ };
169+}
170+
171+function classifyToolKind(toolName: string): ProviderRuntimeToolKind {
172+ const normalized = toolName.toLowerCase();
173+ if (
174+ normalized.includes("bash") ||
175+ normalized.includes("command") ||
176+ normalized.includes("shell") ||
177+ normalized.includes("terminal")
178+ ) {
179+ return "command";
180+ }
181+ if (
182+ normalized.includes("edit") ||
183+ normalized.includes("write") ||
184+ normalized.includes("file") ||
185+ normalized.includes("patch") ||
186+ normalized.includes("replace") ||
187+ normalized.includes("create") ||
188+ normalized.includes("delete")
189+ ) {
190+ return "file-change";
191+ }
192+ return "other";
193+}
194+
195+function classifyRequestKind(toolName: string): "command" | "file-change" {
196+ return classifyToolKind(toolName) === "command" ? "command" : "file-change";
197+}
198+
199+function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {
200+ const commandValue = input.command ?? input.cmd;
201+ const command = typeof commandValue === "string" ? commandValue : undefined;
202+ if (command && command.trim().length > 0) {
203+ return `${toolName}: ${command.trim().slice(0, 400)}`;
204+ }
205+
206+ const serialized = JSON.stringify(input);
207+ if (serialized.length <= 400) {
208+ return `${toolName}: ${serialized}`;
209+ }
210+ return `${toolName}: ${serialized.slice(0, 397)}...`;
211+}
212+
213+function titleForTool(kind: ProviderRuntimeToolKind): string {
214+ switch (kind) {
215+ case "command":
216+ return "Command run";
217+ case "file-change":
218+ return "File change";
219+ case "other":
220+ return "Tool call";
221+ }
222+}
223+
224+function buildUserMessage(input: ProviderSendTurnInput): SDKUserMessage {
225+ const fragments: string[] = [];
226+
227+ if (input.input && input.input.trim().length > 0) {
228+ fragments.push(input.input.trim());
229+ }
230+
231+ for (const attachment of input.attachments ?? []) {
232+ if (attachment.type === "image") {
233+ fragments.push(
234+ `Attached image: ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes).`,
32235 );
33236 }
237+ }
34238
35− return Effect.fail(
36− new ProviderAdapterProcessError({
37− provider: PROVIDER,
38− sessionId: "pending",
39− detail: CLAUDE_RUNTIME_NOT_CONFIGURED,
40− }),
41− );
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({
239+ const text = fragments.join("\n\n");
240+
241+ return {
242+ type: "user",
243+ session_id: "",
244+ parent_tool_use_id: null,
245+ message: {
246+ role: "user",
247+ content: [{ type: "text", text }],
248+ },
249+ } as SDKUserMessage;
250+}
251+
252+function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {
253+ if (result.subtype === "success") {
254+ return "completed";
255+ }
256+
257+ const errors = result.errors.join(" ").toLowerCase();
258+ if (errors.includes("interrupt")) {
259+ return "interrupted";
260+ }
261+ if (errors.includes("cancel")) {
262+ return "cancelled";
263+ }
264+ return "failed";
265+}
266+
267+function toSessionError(
268+ sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
269+ cause: unknown,
270+): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined {
271+ const normalized = toMessage(cause, "").toLowerCase();
272+ if (normalized.includes("unknown session") || normalized.includes("not found")) {
273+ return new ProviderAdapterSessionNotFoundError({
274+ provider: PROVIDER,
275+ sessionId,
276+ cause,
277+ });
278+ }
279+ if (normalized.includes("closed")) {
280+ return new ProviderAdapterSessionClosedError({
281+ provider: PROVIDER,
282+ sessionId,
283+ cause,
284+ });
285+ }
286+ return undefined;
287+}
288+
289+function toRequestError(
290+ sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
291+ method: string,
292+ cause: unknown,
293+): ProviderAdapterError {
294+ const sessionError = toSessionError(sessionId, cause);
295+ if (sessionError) {
296+ return sessionError;
297+ }
298+ return new ProviderAdapterRequestError({
299+ provider: PROVIDER,
300+ method,
301+ detail: toMessage(cause, `${method} failed`),
302+ cause,
303+ });
304+}
305+
306+function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) {
307+ return Effect.gen(function* () {
308+ const createQuery =
309+ options?.createQuery ??
310+ ((input: {
311+ readonly prompt: AsyncIterable<SDKUserMessage>;
312+ readonly options: ClaudeQueryOptions;
313+ }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime);
314+
315+ const sessions = new Map<
316+ ReturnType<typeof ProviderSessionId.makeUnsafe>,
317+ ClaudeSessionContext
318+ >();
319+ const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
320+
321+ const now = (): Effect.Effect<string> =>
322+ DateTime.now.pipe(Effect.map(DateTime.formatIso));
323+ const randomId = (): Effect.Effect<string> => Random.nextUUIDv4;
324+ const makeEventId = (): Effect.Effect<ReturnType<typeof EventId.makeUnsafe>> =>
325+ randomId().pipe(Effect.map((id) => EventId.makeUnsafe(id)));
326+ const makeEventStamp = (): Effect.Effect<{
327+ readonly eventId: ReturnType<typeof EventId.makeUnsafe>;
328+ readonly createdAt: string;
329+ }> =>
330+ Effect.all({
331+ eventId: makeEventId(),
332+ createdAt: now(),
333+ });
334+
335+ const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
336+ Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid);
337+
338+ const snapshotThread = (
339+ context: ClaudeSessionContext,
340+ ): Effect.Effect<{
341+ threadId: ReturnType<typeof ProviderThreadId.makeUnsafe>;
342+ turns: ReadonlyArray<{
343+ id: ReturnType<typeof ProviderTurnId.makeUnsafe>;
344+ items: ReadonlyArray<unknown>;
345+ }>;
346+ }> =>
347+ Effect.gen(function* () {
348+ const threadId =
349+ context.session.threadId ??
350+ ProviderThreadId.makeUnsafe(`claude-thread-${yield* randomId()}`);
351+ return {
352+ threadId,
353+ turns: context.turns.map((turn) => ({
354+ id: turn.id,
355+ items: [...turn.items],
356+ })),
357+ };
358+ });
359+
360+ const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect<void> =>
361+ Effect.gen(function* () {
362+ const threadId = context.session.threadId;
363+ if (!threadId) {
364+ return;
365+ }
366+
367+ const resumeCursor = {
368+ threadId,
369+ resume: threadId,
370+ ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}),
371+ turnCount: context.turns.length,
372+ };
373+
374+ context.session = {
375+ ...context.session,
376+ resumeCursor,
377+ updatedAt: yield* now(),
378+ };
379+ });
380+
381+ const ensureThreadId = (
382+ context: ClaudeSessionContext,
383+ message: SDKMessage,
384+ ): Effect.Effect<void> =>
385+ Effect.gen(function* () {
386+ const nextThreadId = ProviderThreadId.makeUnsafe(message.session_id);
387+ const changed = context.session.threadId !== nextThreadId;
388+
389+ if (changed) {
390+ const updatedAt = yield* now();
391+ context.session = {
392+ ...context.session,
393+ threadId: nextThreadId,
394+ updatedAt,
395+ };
396+ yield* updateResumeCursor(context);
397+ }
398+
399+ if (context.lastThreadStartedId !== nextThreadId) {
400+ context.lastThreadStartedId = nextThreadId;
401+ const stamp = yield* makeEventStamp();
402+ yield* offerRuntimeEvent({
403+ type: "thread.started",
404+ eventId: stamp.eventId,
405+ provider: PROVIDER,
406+ sessionId: context.session.sessionId,
407+ createdAt: stamp.createdAt,
408+ threadId: nextThreadId,
409+ });
410+ }
411+ });
412+
413+ const emitRuntimeError = (
414+ context: ClaudeSessionContext,
415+ message: string,
416+ cause?: unknown,
417+ ): Effect.Effect<void> =>
418+ Effect.gen(function* () {
419+ if (cause !== undefined) {
420+ void cause;
421+ }
422+ const turnState = context.turnState;
423+ const stamp = yield* makeEventStamp();
424+ yield* offerRuntimeEvent({
425+ type: "runtime.error",
426+ eventId: stamp.eventId,
427+ provider: PROVIDER,
428+ sessionId: context.session.sessionId,
429+ createdAt: stamp.createdAt,
430+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
431+ ...(turnState ? { turnId: turnState.turnId } : {}),
432+ message,
433+ });
434+ });
435+
436+ const completeTurn = (
437+ context: ClaudeSessionContext,
438+ status: ProviderRuntimeTurnStatus,
439+ errorMessage?: string,
440+ ): Effect.Effect<void> =>
441+ Effect.gen(function* () {
442+ const turnState = context.turnState;
443+ if (!turnState) {
444+ const stamp = yield* makeEventStamp();
445+ yield* offerRuntimeEvent({
446+ type: "turn.completed",
447+ eventId: stamp.eventId,
448+ provider: PROVIDER,
449+ sessionId: context.session.sessionId,
450+ createdAt: stamp.createdAt,
451+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
452+ status,
453+ ...(errorMessage ? { errorMessage } : {}),
454+ });
455+ return;
456+ }
457+
458+ if (!turnState.messageCompleted) {
459+ const stamp = yield* makeEventStamp();
460+ yield* offerRuntimeEvent({
461+ type: "message.completed",
462+ eventId: stamp.eventId,
463+ provider: PROVIDER,
464+ sessionId: context.session.sessionId,
465+ createdAt: stamp.createdAt,
466+ itemId: ProviderItemId.makeUnsafe(turnState.assistantItemId),
467+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
468+ turnId: turnState.turnId,
469+ });
470+ }
471+
472+ context.turns.push({
473+ id: turnState.turnId,
474+ items: [...turnState.items],
475+ });
476+
477+ const stamp = yield* makeEventStamp();
478+ yield* offerRuntimeEvent({
479+ type: "turn.completed",
480+ eventId: stamp.eventId,
71481 provider: PROVIDER,
72− operation: "rollbackThread",
73− issue: "numTurns must be an integer >= 1.",
74− }),
482+ sessionId: context.session.sessionId,
483+ createdAt: stamp.createdAt,
484+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
485+ turnId: turnState.turnId,
486+ status,
487+ ...(errorMessage ? { errorMessage } : {}),
488+ });
489+
490+ const updatedAt = yield* now();
491+ context.turnState = undefined;
492+ context.session = {
493+ ...context.session,
494+ status: "ready",
495+ activeTurnId: undefined,
496+ updatedAt,
497+ ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}),
498+ };
499+ yield* updateResumeCursor(context);
500+ });
501+
502+ const handleStreamEvent = (
503+ context: ClaudeSessionContext,
504+ message: SDKMessage,
505+ ): Effect.Effect<void> =>
506+ Effect.gen(function* () {
507+ if (message.type !== "stream_event") {
508+ return;
509+ }
510+
511+ const { event } = message;
512+
513+ if (event.type === "content_block_delta") {
514+ if (
515+ event.delta.type === "text_delta" &&
516+ event.delta.text.length > 0 &&
517+ context.turnState
518+ ) {
519+ const stamp = yield* makeEventStamp();
520+ yield* offerRuntimeEvent({
521+ type: "message.delta",
522+ eventId: stamp.eventId,
523+ provider: PROVIDER,
524+ sessionId: context.session.sessionId,
525+ createdAt: stamp.createdAt,
526+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
527+ turnId: context.turnState.turnId,
528+ itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
529+ delta: event.delta.text,
530+ });
531+ }
532+ return;
533+ }
534+
535+ if (event.type === "content_block_start") {
536+ const { index, content_block: block } = event;
537+ if (
538+ block.type !== "tool_use" &&
539+ block.type !== "server_tool_use" &&
540+ block.type !== "mcp_tool_use"
541+ ) {
542+ return;
543+ }
544+
545+ const toolName = block.name;
546+ const toolKind = classifyToolKind(toolName);
547+ const toolInput =
548+ typeof block.input === "object" && block.input !== null
549+ ? (block.input as Record<string, unknown>)
550+ : {};
551+ const itemId = block.id;
552+ const detail = summarizeToolRequest(toolName, toolInput);
553+
554+ const tool: ToolInFlight = {
555+ itemId,
556+ toolKind,
557+ title: titleForTool(toolKind),
558+ detail,
559+ };
560+ context.inFlightTools.set(index, tool);
561+
562+ const stamp = yield* makeEventStamp();
563+ yield* offerRuntimeEvent({
564+ type: "tool.started",
565+ eventId: stamp.eventId,
566+ provider: PROVIDER,
567+ sessionId: context.session.sessionId,
568+ createdAt: stamp.createdAt,
569+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
570+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
571+ itemId: ProviderItemId.makeUnsafe(tool.itemId),
572+ toolKind: tool.toolKind,
573+ title: tool.title,
574+ ...(tool.detail ? { detail: tool.detail } : {}),
575+ });
576+ return;
577+ }
578+
579+ if (event.type === "content_block_stop") {
580+ const { index } = event;
581+ const tool = context.inFlightTools.get(index);
582+ if (!tool) {
583+ return;
584+ }
585+ context.inFlightTools.delete(index);
586+
587+ const stamp = yield* makeEventStamp();
588+ yield* offerRuntimeEvent({
589+ type: "tool.completed",
590+ eventId: stamp.eventId,
591+ provider: PROVIDER,
592+ sessionId: context.session.sessionId,
593+ createdAt: stamp.createdAt,
594+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
595+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
596+ itemId: ProviderItemId.makeUnsafe(tool.itemId),
597+ toolKind: tool.toolKind,
598+ title: tool.title,
599+ ...(tool.detail ? { detail: tool.detail } : {}),
600+ });
601+ }
602+ });
603+
604+ const handleAssistantMessage = (
605+ context: ClaudeSessionContext,
606+ message: SDKMessage,
607+ ): Effect.Effect<void> =>
608+ Effect.gen(function* () {
609+ if (message.type !== "assistant") {
610+ return;
611+ }
612+
613+ if (context.turnState) {
614+ context.turnState.items.push(message.message);
615+ if (!context.turnState.messageCompleted) {
616+ context.turnState = {
617+ ...context.turnState,
618+ messageCompleted: true,
619+ };
620+ const stamp = yield* makeEventStamp();
621+ yield* offerRuntimeEvent({
622+ type: "message.completed",
623+ eventId: stamp.eventId,
624+ provider: PROVIDER,
625+ sessionId: context.session.sessionId,
626+ createdAt: stamp.createdAt,
627+ itemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId),
628+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
629+ turnId: context.turnState.turnId,
630+ });
631+ }
632+ }
633+
634+ context.lastAssistantUuid = message.uuid;
635+ yield* updateResumeCursor(context);
636+ });
637+
638+ const handleResultMessage = (
639+ context: ClaudeSessionContext,
640+ message: SDKMessage,
641+ ): Effect.Effect<void> =>
642+ Effect.gen(function* () {
643+ if (message.type !== "result") {
644+ return;
645+ }
646+
647+ const status = turnStatusFromResult(message);
648+ const errorMessage = message.subtype === "success" ? undefined : message.errors[0];
649+
650+ if (status === "failed") {
651+ yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");
652+ }
653+
654+ yield* completeTurn(context, status, errorMessage);
655+ });
656+
657+ const handleSdkMessage = (
658+ context: ClaudeSessionContext,
659+ message: SDKMessage,
660+ ): Effect.Effect<void> =>
661+ Effect.gen(function* () {
662+ yield* ensureThreadId(context, message);
663+
664+ switch (message.type) {
665+ case "stream_event":
666+ yield* handleStreamEvent(context, message);
667+ return;
668+ case "assistant":
669+ yield* handleAssistantMessage(context, message);
670+ return;
671+ case "result":
672+ yield* handleResultMessage(context, message);
673+ return;
674+ default:
675+ return;
676+ }
677+ });
678+
679+ const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void> =>
680+ Stream.fromAsyncIterable(context.query, (cause) => cause).pipe(
681+ Stream.takeWhile(() => !context.stopped),
682+ Stream.runForEach((message) => handleSdkMessage(context, message)),
683+ Effect.catchCause((cause) =>
684+ Effect.gen(function* () {
685+ if (Cause.hasInterruptsOnly(cause) || context.stopped) {
686+ return;
687+ }
688+ const message = toMessage(Cause.squash(cause), "Claude runtime stream failed.");
689+ yield* emitRuntimeError(context, message, cause);
690+ yield* completeTurn(context, "failed", message);
691+ }),
692+ ),
75693 );
76− }
77694
78− return Effect.fail(
79− new ProviderAdapterRequestError({
80− provider: PROVIDER,
81− method: "thread/rollback",
82− detail: `${CLAUDE_RUNTIME_NOT_CONFIGURED} (session ${sessionId})`,
83− }),
695+ const stopSessionInternal = (
696+ context: ClaudeSessionContext,
697+ options?: { readonly emitExitEvent?: boolean },
698+ ): Effect.Effect<void> =>
699+ Effect.gen(function* () {
700+ if (context.stopped) {
701+ return;
702+ }
703+
704+ context.stopped = true;
705+
706+ for (const [requestId, pending] of context.pendingApprovals) {
707+ yield* Deferred.succeed(pending.decision, "cancel");
708+ const stamp = yield* makeEventStamp();
709+ yield* offerRuntimeEvent({
710+ type: "approval.resolved",
711+ eventId: stamp.eventId,
712+ provider: PROVIDER,
713+ sessionId: context.session.sessionId,
714+ createdAt: stamp.createdAt,
715+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
716+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
717+ requestId,
718+ requestKind: pending.requestKind,
719+ decision: "cancel",
720+ });
721+ }
722+ context.pendingApprovals.clear();
723+
724+ if (context.turnState) {
725+ yield* completeTurn(context, "interrupted", "Session stopped.");
726+ }
727+
728+ yield* Queue.offer(context.promptQueue, { type: "terminate" }).pipe(Effect.ignore);
729+
730+ yield* Effect.sync(() => context.query.close()).pipe(Effect.ignoreCause);
731+
732+ const updatedAt = yield* now();
733+ context.session = {
734+ ...context.session,
735+ status: "closed",
736+ activeTurnId: undefined,
737+ updatedAt,
738+ };
739+
740+ if (options?.emitExitEvent !== false) {
741+ const stamp = yield* makeEventStamp();
742+ yield* offerRuntimeEvent({
743+ type: "session.exited",
744+ eventId: stamp.eventId,
745+ provider: PROVIDER,
746+ sessionId: context.session.sessionId,
747+ createdAt: stamp.createdAt,
748+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
749+ message: "Session stopped",
750+ });
751+ }
752+
753+ sessions.delete(context.session.sessionId);
754+ });
755+
756+ const requireSession = (
757+ sessionId: ReturnType<typeof ProviderSessionId.makeUnsafe>,
758+ ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => {
759+ const context = sessions.get(sessionId);
760+ if (!context) {
761+ return Effect.fail(
762+ new ProviderAdapterSessionNotFoundError({
763+ provider: PROVIDER,
764+ sessionId,
765+ }),
766+ );
767+ }
768+ if (context.stopped || context.session.status === "closed") {
769+ return Effect.fail(
770+ new ProviderAdapterSessionClosedError({
771+ provider: PROVIDER,
772+ sessionId,
773+ }),
774+ );
775+ }
776+ return Effect.succeed(context);
777+ };
778+
779+ const startSession: ClaudeCodeAdapterShape["startSession"] = (input) =>
780+ Effect.gen(function* () {
781+ if (input.provider !== undefined && input.provider !== PROVIDER) {
782+ return yield* new ProviderAdapterValidationError({
783+ provider: PROVIDER,
784+ operation: "startSession",
785+ issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`,
786+ });
787+ }
788+
789+ const startedAt = yield* now();
790+ const sessionId = ProviderSessionId.makeUnsafe(`claude-session-${yield* randomId()}`);
791+ const resumeState = readClaudeResumeState(input.resumeCursor);
792+ const threadId =
793+ resumeState?.threadId ??
794+ ProviderThreadId.makeUnsafe(`claude-thread-${yield* randomId()}`);
795+
796+ const promptQueue = yield* Queue.unbounded<PromptQueueItem>();
797+ const prompt: AsyncIterable<SDKUserMessage> = {
798+ [Symbol.asyncIterator]() {
799+ return {
800+ next: async (): Promise<IteratorResult<SDKUserMessage>> => {
801+ const item = await Effect.runPromise(Queue.take(promptQueue));
802+ if (item.type === "terminate") {
803+ return {
804+ done: true,
805+ value: undefined,
806+ };
807+ }
808+
809+ return {
810+ done: false,
811+ value: item.message,
812+ };
813+ },
814+ };
815+ },
816+ };
817+
818+ const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>();
819+ const inFlightTools = new Map<number, ToolInFlight>();
820+
821+ let contextRef: ClaudeSessionContext | undefined;
822+
823+ const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) =>
824+ Effect.runPromise(
825+ Effect.gen(function* () {
826+ const context = contextRef;
827+ if (!context) {
828+ return {
829+ behavior: "deny",
830+ message: "Claude session context is unavailable.",
831+ } satisfies PermissionResult;
832+ }
833+
834+ const approvalPolicy = input.approvalPolicy ?? "never";
835+ if (approvalPolicy === "never") {
836+ return {
837+ behavior: "allow",
838+ updatedInput: toolInput,
839+ } satisfies PermissionResult;
840+ }
841+
842+ const requestId = ApprovalRequestId.makeUnsafe(yield* randomId());
843+ const requestKind = classifyRequestKind(toolName);
844+ const detail = summarizeToolRequest(toolName, toolInput);
845+ const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
846+ const pendingApproval: PendingApproval = {
847+ requestKind,
848+ detail,
849+ decision: decisionDeferred,
850+ ...(callbackOptions.suggestions
851+ ? { suggestions: callbackOptions.suggestions }
852+ : {}),
853+ };
854+
855+ const requestedStamp = yield* makeEventStamp();
856+ yield* offerRuntimeEvent({
857+ type: "approval.requested",
858+ eventId: requestedStamp.eventId,
859+ provider: PROVIDER,
860+ sessionId: context.session.sessionId,
861+ createdAt: requestedStamp.createdAt,
862+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
863+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
864+ requestId,
865+ requestKind,
866+ detail,
867+ });
868+
869+ pendingApprovals.set(requestId, pendingApproval);
870+
871+ const onAbort = () => {
872+ if (!pendingApprovals.has(requestId)) {
873+ return;
874+ }
875+ pendingApprovals.delete(requestId);
876+ Effect.runFork(Deferred.succeed(decisionDeferred, "cancel"));
877+ };
878+
879+ callbackOptions.signal.addEventListener("abort", onAbort, {
880+ once: true,
881+ });
882+
883+ const decision = yield* Deferred.await(decisionDeferred);
884+ pendingApprovals.delete(requestId);
885+
886+ const resolvedStamp = yield* makeEventStamp();
887+ yield* offerRuntimeEvent({
888+ type: "approval.resolved",
889+ eventId: resolvedStamp.eventId,
890+ provider: PROVIDER,
891+ sessionId: context.session.sessionId,
892+ createdAt: resolvedStamp.createdAt,
893+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
894+ ...(context.turnState ? { turnId: context.turnState.turnId } : {}),
895+ requestId,
896+ requestKind,
897+ decision,
898+ });
899+
900+ if (decision === "accept" || decision === "acceptForSession") {
901+ return {
902+ behavior: "allow",
903+ updatedInput: toolInput,
904+ ...(decision === "acceptForSession" && pendingApproval.suggestions
905+ ? { updatedPermissions: [...pendingApproval.suggestions] }
906+ : {}),
907+ } satisfies PermissionResult;
908+ }
909+
910+ return {
911+ behavior: "deny",
912+ message:
913+ decision === "cancel"
914+ ? "User cancelled tool execution."
915+ : "User declined tool execution.",
916+ } satisfies PermissionResult;
917+ }),
918+ );
919+
920+ const providerOptions = input.providerOptions?.claudeCode;
921+ const permissionMode = toPermissionMode(providerOptions?.permissionMode);
922+
923+ const queryOptions: ClaudeQueryOptions = {
924+ ...(input.cwd ? { cwd: input.cwd } : {}),
925+ ...(input.model ? { model: input.model } : {}),
926+ ...(providerOptions?.binaryPath
927+ ? { pathToClaudeCodeExecutable: providerOptions.binaryPath }
928+ : {}),
929+ ...(permissionMode ? { permissionMode } : {}),
930+ ...(permissionMode === "bypassPermissions"
931+ ? { allowDangerouslySkipPermissions: true }
932+ : {}),
933+ ...(providerOptions?.maxThinkingTokens !== undefined
934+ ? { maxThinkingTokens: providerOptions.maxThinkingTokens }
935+ : {}),
936+ ...(resumeState?.resume ? { resume: resumeState.resume } : {}),
937+ ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}),
938+ includePartialMessages: true,
939+ canUseTool,
940+ env: process.env,
941+ ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}),
942+ };
943+
944+ const queryRuntime = yield* Effect.try({
945+ try: () =>
946+ createQuery({
947+ prompt,
948+ options: queryOptions,
949+ }),
950+ catch: (cause) =>
951+ new ProviderAdapterProcessError({
952+ provider: PROVIDER,
953+ sessionId,
954+ detail: toMessage(cause, "Failed to start Claude runtime session."),
955+ cause,
956+ }),
957+ });
958+
959+ const session: ProviderSession = {
960+ sessionId,
961+ provider: PROVIDER,
962+ status: "ready",
963+ ...(input.cwd ? { cwd: input.cwd } : {}),
964+ ...(input.model ? { model: input.model } : {}),
965+ threadId,
966+ resumeCursor: {
967+ threadId,
968+ resume: resumeState?.resume ?? threadId,
969+ ...(resumeState?.resumeSessionAt
970+ ? { resumeSessionAt: resumeState.resumeSessionAt }
971+ : {}),
972+ turnCount: resumeState?.turnCount ?? 0,
973+ },
974+ createdAt: startedAt,
975+ updatedAt: startedAt,
976+ };
977+
978+ const context: ClaudeSessionContext = {
979+ session,
980+ promptQueue,
981+ query: queryRuntime,
982+ startedAt,
983+ pendingApprovals,
984+ turns: [],
985+ inFlightTools,
986+ turnState: undefined,
987+ lastAssistantUuid: resumeState?.resumeSessionAt,
988+ lastThreadStartedId: undefined,
989+ stopped: false,
990+ };
991+ contextRef = context;
992+ sessions.set(sessionId, context);
993+
994+ const sessionStartedStamp = yield* makeEventStamp();
995+ yield* offerRuntimeEvent({
996+ type: "session.started",
997+ eventId: sessionStartedStamp.eventId,
998+ provider: PROVIDER,
999+ sessionId,
1000+ createdAt: sessionStartedStamp.createdAt,
1001+ threadId,
1002+ });
1003+
1004+ Effect.runFork(runSdkStream(context));
1005+
1006+ return {
1007+ ...session,
1008+ };
1009+ });
1010+
1011+ const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) =>
1012+ Effect.gen(function* () {
1013+ const context = yield* requireSession(input.sessionId);
1014+
1015+ if (context.turnState) {
1016+ return yield* new ProviderAdapterValidationError({
1017+ provider: PROVIDER,
1018+ operation: "sendTurn",
1019+ issue: `Session '${input.sessionId}' already has an active turn '${context.turnState.turnId}'.`,
1020+ });
1021+ }
1022+
1023+ if (input.model) {
1024+ yield* Effect.tryPromise({
1025+ try: () => context.query.setModel(input.model),
1026+ catch: (cause) => toRequestError(input.sessionId, "turn/setModel", cause),
1027+ });
1028+ }
1029+
1030+ const turnId = ProviderTurnId.makeUnsafe(`claude-turn-${yield* randomId()}`);
1031+ const turnState: ClaudeTurnState = {
1032+ turnId,
1033+ assistantItemId: `claude-message-${yield* randomId()}`,
1034+ startedAt: yield* now(),
1035+ items: [],
1036+ messageCompleted: false,
1037+ };
1038+
1039+ const updatedAt = yield* now();
1040+ context.turnState = turnState;
1041+ context.session = {
1042+ ...context.session,
1043+ status: "running",
1044+ activeTurnId: turnId,
1045+ updatedAt,
1046+ };
1047+
1048+ const turnStartedStamp = yield* makeEventStamp();
1049+ yield* offerRuntimeEvent({
1050+ type: "turn.started",
1051+ eventId: turnStartedStamp.eventId,
1052+ provider: PROVIDER,
1053+ sessionId: context.session.sessionId,
1054+ createdAt: turnStartedStamp.createdAt,
1055+ ...(context.session.threadId ? { threadId: context.session.threadId } : {}),
1056+ turnId,
1057+ });
1058+
1059+ const message = buildUserMessage(input);
1060+
1061+ yield* Queue.offer(context.promptQueue, {
1062+ type: "message",
1063+ message,
1064+ }).pipe(Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)));
1065+
1066+ const threadId = context.session.threadId;
1067+ if (!threadId) {
1068+ return yield* new ProviderAdapterValidationError({
1069+ provider: PROVIDER,
1070+ operation: "sendTurn",
1071+ issue: "Session thread id is not initialized.",
1072+ });
1073+ }
1074+
1075+ return {
1076+ threadId,
1077+ turnId,
1078+ ...(context.session.resumeCursor !== undefined
1079+ ? { resumeCursor: context.session.resumeCursor }
1080+ : {}),
1081+ };
1082+ });
1083+
1084+ const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, _turnId) =>
1085+ Effect.gen(function* () {
1086+ const context = yield* requireSession(sessionId);
1087+ yield* Effect.tryPromise({
1088+ try: () => context.query.interrupt(),
1089+ catch: (cause) => toRequestError(sessionId, "turn/interrupt", cause),
1090+ });
1091+ });
1092+
1093+ const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) =>
1094+ Effect.gen(function* () {
1095+ const context = yield* requireSession(sessionId);
1096+ return yield* snapshotThread(context);
1097+ });
1098+
1099+ const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) =>
1100+ Effect.gen(function* () {
1101+ const context = yield* requireSession(sessionId);
1102+ const nextLength = Math.max(0, context.turns.length - numTurns);
1103+ context.turns.splice(nextLength);
1104+ yield* updateResumeCursor(context);
1105+ return yield* snapshotThread(context);
1106+ });
1107+
1108+ const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = (
1109+ sessionId,
1110+ requestId,
1111+ decision,
1112+ ) =>
1113+ Effect.gen(function* () {
1114+ const context = yield* requireSession(sessionId);
1115+ const pending = context.pendingApprovals.get(requestId);
1116+ if (!pending) {
1117+ return yield* new ProviderAdapterRequestError({
1118+ provider: PROVIDER,
1119+ method: "item/requestApproval/decision",
1120+ detail: `Unknown pending approval request: ${requestId}`,
1121+ });
1122+ }
1123+
1124+ context.pendingApprovals.delete(requestId);
1125+ yield* Deferred.succeed(pending.decision, decision);
1126+ });
1127+
1128+ const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) =>
1129+ Effect.gen(function* () {
1130+ const context = yield* requireSession(sessionId);
1131+ yield* stopSessionInternal(context, {
1132+ emitExitEvent: true,
1133+ });
1134+ });
1135+
1136+ const listSessions: ClaudeCodeAdapterShape["listSessions"] = () =>
1137+ Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session })));
1138+
1139+ const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) =>
1140+ Effect.sync(() => {
1141+ const context = sessions.get(sessionId);
1142+ return context !== undefined && !context.stopped;
1143+ });
1144+
1145+ const stopAll: ClaudeCodeAdapterShape["stopAll"] = () =>
1146+ Effect.forEach(
1147+ sessions,
1148+ ([, context]) =>
1149+ stopSessionInternal(context, {
1150+ emitExitEvent: true,
1151+ }),
1152+ { discard: true },
1153+ );
1154+
1155+ yield* Effect.addFinalizer(() =>
1156+ Effect.forEach(
1157+ sessions,
1158+ ([, context]) =>
1159+ stopSessionInternal(context, {
1160+ emitExitEvent: false,
1161+ }),
1162+ { discard: true },
1163+ ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))),
841164 );
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);
1165+
1166+ return {
1167+ provider: PROVIDER,
1168+ startSession,
1169+ sendTurn,
1170+ interruptTurn,
1171+ readThread,
1172+ rollbackThread,
1173+ respondToRequest,
1174+ stopSession,
1175+ listSessions,
1176+ hasSession,
1177+ stopAll,
1178+ streamEvents: Stream.fromQueue(runtimeEventQueue),
1179+ } satisfies ClaudeCodeAdapterShape;
1180+ });
1181+}
1182+
1183+export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter());
1184+
1185+export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) {
1186+ return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options));
1051187 }
bun.lock+35−0
@@ -35,6 +35,7 @@ · 34 unmodified lines
3535 "t3": "./dist/index.mjs",
3636 },
3737 "dependencies": {
38+ "@anthropic-ai/claude-agent-sdk": "^0.2.62",
3839 "@effect/platform-node": "catalog:",
3940 "@effect/sql-sqlite-bun": "catalog:",
4041 "@pierre/diffs": "^1.1.0-beta.16",
@@ -115,6 +116,8 @@ · 74 unmodified lines
115116 "effect": "https://pkg.pr.new/Effect-TS/effect-smol/effect@f591738",
116117 },
117118 "packages": {
119+ "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.62", "", { "optionalDependencies": { "@img/sharp-darwin-arm64": "^0.34.2", "@img/sharp-darwin-x64": "^0.34.2", "@img/sharp-linux-arm": "^0.34.2", "@img/sharp-linux-arm64": "^0.34.2", "@img/sharp-linux-x64": "^0.34.2", "@img/sharp-linuxmusl-arm64": "^0.34.2", "@img/sharp-linuxmusl-x64": "^0.34.2", "@img/sharp-win32-arm64": "^0.34.2", "@img/sharp-win32-x64": "^0.34.2" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-exRJ2djKP6erRpUrtnVf5iXzqeS9dAie9d1ghODZVVXdLqieSqCpaAhZhe0hL1yvP33OL0J5CgT9RAyPzhYY9A=="],
120+
118121 "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
119122
120123 "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
@@ -253,6 +256,38 @@ · 132 unmodified lines
253256
254257 "@hapi/topo": ["@hapi/topo@6.0.2", "", { "dependencies": { "@hapi/hoek": "^11.0.2" } }, "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg=="],
255258
259+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
260+
261+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
262+
263+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
264+
265+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
266+
267+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
268+
269+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
270+
271+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
272+
273+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
274+
275+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
276+
277+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
278+
279+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
280+
281+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
282+
283+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
284+
285+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
286+
287+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
288+
289+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
290+
256291 "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
257292
258293 "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],