diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts index 95a9f8fdab01fc96400880deecaa79f1d29b8711..061bc0ca5b9a1e1821be35e7248e73b3e22d0e09 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.test.ts @@ -32,9 +32,9 @@ class FakeClaudeRuntime implements ClaudeCodeRuntime { readonly streamEvents = Stream.fromQueue(this.runtimeEventQueue); public startSessionImpl = vi.fn( - async (input: ProviderSessionStartInput): Promise => { + (input: ProviderSessionStartInput): Effect.Effect => { const now = new Date().toISOString(); - return { + return Effect.succeed({ sessionId: asSessionId("claude-sess-1"), provider: "claudeCode", status: "ready", @@ -44,88 +44,97 @@ class FakeClaudeRuntime implements ClaudeCodeRuntime { model: input.model, createdAt: now, updatedAt: now, - }; + }); }, ); public sendTurnImpl = vi.fn( - async (_input: ProviderSendTurnInput): Promise => ({ - threadId: ProviderThreadId.makeUnsafe("claude-thread-1"), - turnId: asTurnId("claude-turn-1"), - }), + (_input: ProviderSendTurnInput): Effect.Effect => + Effect.succeed({ + threadId: ProviderThreadId.makeUnsafe("claude-thread-1"), + turnId: asTurnId("claude-turn-1"), + }), ); public interruptTurnImpl = vi.fn( - async (_sessionId: ProviderSessionId, _turnId?: ProviderTurnId): Promise => undefined, + (_sessionId: ProviderSessionId, _turnId?: ProviderTurnId): Effect.Effect => Effect.void, ); - public readThreadImpl = vi.fn(async (_sessionId: ProviderSessionId) => ({ - threadId: ProviderThreadId.makeUnsafe("claude-thread-1"), - turns: [], - })); - - public rollbackThreadImpl = vi.fn( - async (_sessionId: ProviderSessionId, _numTurns: number) => ({ + public readThreadImpl = vi.fn((_sessionId: ProviderSessionId): Effect.Effect<{ + threadId: ProviderThreadId; + turns: never[]; + }> => + Effect.succeed({ threadId: ProviderThreadId.makeUnsafe("claude-thread-1"), turns: [], - }), + })); + + public rollbackThreadImpl = vi.fn( + (_sessionId: ProviderSessionId, _numTurns: number): Effect.Effect<{ + threadId: ProviderThreadId; + turns: never[]; + }> => + Effect.succeed({ + threadId: ProviderThreadId.makeUnsafe("claude-thread-1"), + turns: [], + }), ); public respondToRequestImpl = vi.fn( - async ( + ( _sessionId: ProviderSessionId, _requestId: ApprovalRequestId, _decision: ProviderApprovalDecision, - ): Promise => undefined, + ): Effect.Effect => Effect.void, ); - public stopSessionImpl = vi.fn((_sessionId: ProviderSessionId) => undefined); - public listSessionsImpl = vi.fn((): ReadonlyArray => []); - public hasSessionImpl = vi.fn((_sessionId: ProviderSessionId): boolean => false); - public stopAllImpl = vi.fn(() => undefined); + public stopSessionImpl = vi.fn((_sessionId: ProviderSessionId): Effect.Effect => Effect.void); + public listSessionsImpl = vi.fn((): Effect.Effect> => Effect.succeed([])); + public hasSessionImpl = vi.fn((_sessionId: ProviderSessionId): Effect.Effect => Effect.succeed(false)); + public stopAllImpl = vi.fn((): Effect.Effect => Effect.void); - async startSession(input: ProviderSessionStartInput): Promise { + startSession(input: ProviderSessionStartInput): Effect.Effect { return this.startSessionImpl(input); } - async sendTurn(input: ProviderSendTurnInput): Promise { + sendTurn(input: ProviderSendTurnInput): Effect.Effect { return this.sendTurnImpl(input); } - async interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Promise { + interruptTurn(sessionId: ProviderSessionId, turnId?: ProviderTurnId): Effect.Effect { return this.interruptTurnImpl(sessionId, turnId); } - async readThread(sessionId: ProviderSessionId) { + readThread(sessionId: ProviderSessionId) { return this.readThreadImpl(sessionId); } - async rollbackThread(sessionId: ProviderSessionId, numTurns: number) { + rollbackThread(sessionId: ProviderSessionId, numTurns: number) { return this.rollbackThreadImpl(sessionId, numTurns); } - async respondToRequest( + respondToRequest( sessionId: ProviderSessionId, requestId: ApprovalRequestId, decision: ProviderApprovalDecision, - ): Promise { + ): Effect.Effect { return this.respondToRequestImpl(sessionId, requestId, decision); } - stopSession(sessionId: ProviderSessionId): void { - this.stopSessionImpl(sessionId); + stopSession(sessionId: ProviderSessionId): Effect.Effect { + return this.stopSessionImpl(sessionId); } - listSessions(): ReadonlyArray { + listSessions(): Effect.Effect> { return this.listSessionsImpl(); } - hasSession(sessionId: ProviderSessionId): boolean { + hasSession(sessionId: ProviderSessionId): Effect.Effect { return this.hasSessionImpl(sessionId); } - stopAll(): void { - this.stopAllImpl(); + stopAll(): Effect.Effect { + return this.stopAllImpl(); } emitRuntimeEvent(event: ProviderRuntimeEvent): void { @@ -160,8 +169,8 @@ validationLayer("ClaudeCodeAdapterLive validation", (it) => { }); const sessionErrorRuntime = new FakeClaudeRuntime(); -sessionErrorRuntime.sendTurnImpl.mockImplementation(async () => { - throw new Error("Unknown session: claude-sess-missing"); +sessionErrorRuntime.sendTurnImpl.mockImplementation(() => { + return Effect.fail(new Error("Unknown session: claude-sess-missing")); }); const sessionErrorLayer = it.layer(makeClaudeCodeAdapterLive({ runtime: sessionErrorRuntime })); diff --git a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts index 2472695f6ef28170b2b1bf0af2369b63547d12d0..b6ebe5eb9a654a51436ba0ee119ee6ea6341f4dd 100644 --- a/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeCodeAdapter.ts @@ -36,23 +36,32 @@ import { const PROVIDER = "claudeCode" as const; export interface ClaudeCodeRuntime { - readonly startSession: (input: ProviderSessionStartInput) => Promise; - readonly sendTurn: (input: ProviderSendTurnInput) => Promise; - readonly interruptTurn: (sessionId: ProviderSessionId, turnId?: ProviderTurnId) => Promise; - readonly readThread: (sessionId: ProviderSessionId) => Promise; + readonly startSession: ( + input: ProviderSessionStartInput, + ) => Effect.Effect; + readonly sendTurn: ( + input: ProviderSendTurnInput, + ) => Effect.Effect; + readonly interruptTurn: ( + sessionId: ProviderSessionId, + turnId?: ProviderTurnId, + ) => Effect.Effect; + readonly readThread: ( + sessionId: ProviderSessionId, + ) => Effect.Effect; readonly rollbackThread: ( sessionId: ProviderSessionId, numTurns: number, - ) => Promise; + ) => Effect.Effect; readonly respondToRequest: ( sessionId: ProviderSessionId, requestId: ApprovalRequestId, decision: ProviderApprovalDecision, - ) => Promise; - readonly stopSession: (sessionId: ProviderSessionId) => void; - readonly listSessions: () => ReadonlyArray; - readonly hasSession: (sessionId: ProviderSessionId) => boolean; - readonly stopAll: () => void; + ) => Effect.Effect; + readonly stopSession: (sessionId: ProviderSessionId) => Effect.Effect; + readonly listSessions: () => Effect.Effect>; + readonly hasSession: (sessionId: ProviderSessionId) => Effect.Effect; + readonly stopAll: () => Effect.Effect; readonly streamEvents: Stream.Stream; } @@ -107,22 +116,20 @@ function toRequestError( }); } +const unavailableRuntimeError = () => new Error("Claude Code runtime is not configured."); + function makeUnavailableRuntime(): ClaudeCodeRuntime { - const unavailable = async (): Promise => { - throw new Error("Claude Code runtime is not configured."); - }; - return { - startSession: unavailable, - sendTurn: unavailable, - interruptTurn: unavailable, - readThread: unavailable, - rollbackThread: unavailable, - respondToRequest: unavailable, - stopSession: () => {}, - listSessions: () => [], - hasSession: () => false, - stopAll: () => {}, + startSession: () => Effect.fail(unavailableRuntimeError()), + sendTurn: () => Effect.fail(unavailableRuntimeError()), + interruptTurn: () => Effect.fail(unavailableRuntimeError()), + readThread: () => Effect.fail(unavailableRuntimeError()), + rollbackThread: () => Effect.fail(unavailableRuntimeError()), + respondToRequest: () => Effect.fail(unavailableRuntimeError()), + stopSession: () => Effect.void, + listSessions: () => Effect.succeed([]), + hasSession: () => Effect.succeed(false), + stopAll: () => Effect.void, streamEvents: Stream.empty, }; } @@ -140,13 +147,9 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => return makeUnavailableRuntime(); }), (runtime) => - Effect.sync(() => { - try { - runtime.stopAll(); - } catch { - // Finalizers should never fail and block shutdown. - } - }), + runtime.stopAll().pipe( + Effect.orElseSucceed(() => undefined), + ), ); const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => { @@ -160,35 +163,33 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => ); } - return Effect.tryPromise({ - try: () => runtime.startSession(input), - catch: (cause) => - new ProviderAdapterProcessError({ - provider: PROVIDER, - sessionId: "pending", - detail: toMessage(cause, "Failed to start Claude Code adapter session."), - cause, - }), - }); + return runtime.startSession(input).pipe( + Effect.mapError( + (cause) => + new ProviderAdapterProcessError({ + provider: PROVIDER, + sessionId: "pending", + detail: toMessage(cause, "Failed to start Claude Code adapter session."), + cause, + }), + ), + ); }; const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => - Effect.tryPromise({ - try: () => runtime.sendTurn(input), - catch: (cause) => toRequestError(input.sessionId, "turn/start", cause), - }); + runtime.sendTurn(input).pipe( + Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)), + ); const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, turnId) => - Effect.tryPromise({ - try: () => runtime.interruptTurn(sessionId, turnId), - catch: (cause) => toRequestError(sessionId, "turn/interrupt", cause), - }); + runtime.interruptTurn(sessionId, turnId).pipe( + Effect.mapError((cause) => toRequestError(sessionId, "turn/interrupt", cause)), + ); const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) => - Effect.tryPromise({ - try: () => runtime.readThread(sessionId), - catch: (cause) => toRequestError(sessionId, "thread/read", cause), - }); + runtime.readThread(sessionId).pipe( + Effect.mapError((cause) => toRequestError(sessionId, "thread/read", cause)), + ); const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) => { if (!Number.isInteger(numTurns) || numTurns < 1) { @@ -201,10 +202,9 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => ); } - return Effect.tryPromise({ - try: () => runtime.rollbackThread(sessionId, numTurns), - catch: (cause) => toRequestError(sessionId, "thread/rollback", cause), - }); + return runtime.rollbackThread(sessionId, numTurns).pipe( + Effect.mapError((cause) => toRequestError(sessionId, "thread/rollback", cause)), + ); }; const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( @@ -212,26 +212,17 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => requestId, decision, ) => - Effect.tryPromise({ - try: () => runtime.respondToRequest(sessionId, requestId, decision), - catch: (cause) => toRequestError(sessionId, "item/requestApproval/decision", cause), - }); + runtime.respondToRequest(sessionId, requestId, decision).pipe( + Effect.mapError((cause) => toRequestError(sessionId, "item/requestApproval/decision", cause)), + ); - const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) => - Effect.sync(() => { - runtime.stopSession(sessionId); - }); + const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) => runtime.stopSession(sessionId); - const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => - Effect.sync(() => runtime.listSessions()); + const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => runtime.listSessions(); - const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) => - Effect.sync(() => runtime.hasSession(sessionId)); + const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) => runtime.hasSession(sessionId); - const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => - Effect.sync(() => { - runtime.stopAll(); - }); + const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => runtime.stopAll(); return { provider: PROVIDER, @@ -254,4 +245,3 @@ export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeC export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) { return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options)); } -