| ... | @@ -36,23 +36,32 @@ import { | ... | @@ -36,23 +36,32 @@ import { |
| 36 | const PROVIDER = "claudeCode" as const; | 36 | const PROVIDER = "claudeCode" as const; |
| 37 | | 37 | |
| 38 | export interface ClaudeCodeRuntime { | 38 | export interface ClaudeCodeRuntime { |
| 39 | readonly startSession: (input: ProviderSessionStartInput) => Promise<ProviderSession>; | 39 | readonly startSession: ( |
| 40 | readonly sendTurn: (input: ProviderSendTurnInput) => Promise<ProviderTurnStartResult>; | 40 | input: ProviderSessionStartInput, |
| 41 | readonly interruptTurn: (sessionId: ProviderSessionId, turnId?: ProviderTurnId) => Promise<void>; | 41 | ) => Effect.Effect<ProviderSession, unknown>; |
| 42 | readonly readThread: (sessionId: ProviderSessionId) => Promise<ProviderThreadSnapshot>; | 42 | readonly sendTurn: ( |
| | 43 | input: ProviderSendTurnInput, |
| | 44 | ) => Effect.Effect<ProviderTurnStartResult, unknown>; |
| | 45 | readonly interruptTurn: ( |
| | 46 | sessionId: ProviderSessionId, |
| | 47 | turnId?: ProviderTurnId, |
| | 48 | ) => Effect.Effect<void, unknown>; |
| | 49 | readonly readThread: ( |
| | 50 | sessionId: ProviderSessionId, |
| | 51 | ) => Effect.Effect<ProviderThreadSnapshot, unknown>; |
| 43 | readonly rollbackThread: ( | 52 | readonly rollbackThread: ( |
| 44 | sessionId: ProviderSessionId, | 53 | sessionId: ProviderSessionId, |
| 45 | numTurns: number, | 54 | numTurns: number, |
| 46 | ) => Promise<ProviderThreadSnapshot>; | 55 | ) => Effect.Effect<ProviderThreadSnapshot, unknown>; |
| 47 | readonly respondToRequest: ( | 56 | readonly respondToRequest: ( |
| 48 | sessionId: ProviderSessionId, | 57 | sessionId: ProviderSessionId, |
| 49 | requestId: ApprovalRequestId, | 58 | requestId: ApprovalRequestId, |
| 50 | decision: ProviderApprovalDecision, | 59 | decision: ProviderApprovalDecision, |
| 51 | ) => Promise<void>; | 60 | ) => Effect.Effect<void, unknown>; |
| 52 | readonly stopSession: (sessionId: ProviderSessionId) => void; | 61 | readonly stopSession: (sessionId: ProviderSessionId) => Effect.Effect<void>; |
| 53 | readonly listSessions: () => ReadonlyArray<ProviderSession>; | 62 | readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>; |
| 54 | readonly hasSession: (sessionId: ProviderSessionId) => boolean; | 63 | readonly hasSession: (sessionId: ProviderSessionId) => Effect.Effect<boolean>; |
| 55 | readonly stopAll: () => void; | 64 | readonly stopAll: () => Effect.Effect<void>; |
| 56 | readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>; | 65 | readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>; |
| 57 | } | 66 | } |
| 58 | | 67 | |
| ... | @@ -107,22 +116,20 @@ function toRequestError( | ... | @@ -107,22 +116,20 @@ function toRequestError( |
| 107 | }); | 116 | }); |
| 108 | } | 117 | } |
| 109 | | 118 | |
| 110 | function makeUnavailableRuntime(): ClaudeCodeRuntime { | 119 | const unavailableRuntimeError = () => new Error("Claude Code runtime is not configured."); |
| 111 | const unavailable = async (): Promise<never> => { | | |
| 112 | throw new Error("Claude Code runtime is not configured."); | | |
| 113 | }; | | |
| 114 | | 120 | |
| | 121 | function makeUnavailableRuntime(): ClaudeCodeRuntime { |
| 115 | return { | 122 | return { |
| 116 | startSession: unavailable, | 123 | startSession: () => Effect.fail(unavailableRuntimeError()), |
| 117 | sendTurn: unavailable, | 124 | sendTurn: () => Effect.fail(unavailableRuntimeError()), |
| 118 | interruptTurn: unavailable, | 125 | interruptTurn: () => Effect.fail(unavailableRuntimeError()), |
| 119 | readThread: unavailable, | 126 | readThread: () => Effect.fail(unavailableRuntimeError()), |
| 120 | rollbackThread: unavailable, | 127 | rollbackThread: () => Effect.fail(unavailableRuntimeError()), |
| 121 | respondToRequest: unavailable, | 128 | respondToRequest: () => Effect.fail(unavailableRuntimeError()), |
| 122 | stopSession: () => {}, | 129 | stopSession: () => Effect.void, |
| 123 | listSessions: () => [], | 130 | listSessions: () => Effect.succeed([]), |
| 124 | hasSession: () => false, | 131 | hasSession: () => Effect.succeed(false), |
| 125 | stopAll: () => {}, | 132 | stopAll: () => Effect.void, |
| 126 | streamEvents: Stream.empty, | 133 | streamEvents: Stream.empty, |
| 127 | }; | 134 | }; |
| 128 | } | 135 | } |
| ... | @@ -140,13 +147,9 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => | ... | @@ -140,13 +147,9 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => |
| 140 | return makeUnavailableRuntime(); | 147 | return makeUnavailableRuntime(); |
| 141 | }), | 148 | }), |
| 142 | (runtime) => | 149 | (runtime) => |
| 143 | Effect.sync(() => { | 150 | runtime.stopAll().pipe( |
| 144 | try { | 151 | Effect.orElseSucceed(() => undefined), |
| 145 | runtime.stopAll(); | 152 | ), |
| 146 | } catch { | | |
| 147 | // Finalizers should never fail and block shutdown. | | |
| 148 | } | | |
| 149 | }), | | |
| 150 | ); | 153 | ); |
| 151 | | 154 | |
| 152 | const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => { | 155 | const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => { |
| ... | @@ -160,35 +163,33 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => | ... | @@ -160,35 +163,33 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => |
| 160 | ); | 163 | ); |
| 161 | } | 164 | } |
| 162 | | 165 | |
| 163 | return Effect.tryPromise({ | 166 | return runtime.startSession(input).pipe( |
| 164 | try: () => runtime.startSession(input), | 167 | Effect.mapError( |
| 165 | catch: (cause) => | 168 | (cause) => |
| 166 | new ProviderAdapterProcessError({ | 169 | new ProviderAdapterProcessError({ |
| 167 | provider: PROVIDER, | 170 | provider: PROVIDER, |
| 168 | sessionId: "pending", | 171 | sessionId: "pending", |
| 169 | detail: toMessage(cause, "Failed to start Claude Code adapter session."), | 172 | detail: toMessage(cause, "Failed to start Claude Code adapter session."), |
| 170 | cause, | 173 | cause, |
| 171 | }), | 174 | }), |
| 172 | }); | 175 | ), |
| | 176 | ); |
| 173 | }; | 177 | }; |
| 174 | | 178 | |
| 175 | const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => | 179 | const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => |
| 176 | Effect.tryPromise({ | 180 | runtime.sendTurn(input).pipe( |
| 177 | try: () => runtime.sendTurn(input), | 181 | Effect.mapError((cause) => toRequestError(input.sessionId, "turn/start", cause)), |
| 178 | catch: (cause) => toRequestError(input.sessionId, "turn/start", cause), | 182 | ); |
| 179 | }); | | |
| 180 | | 183 | |
| 181 | const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, turnId) => | 184 | const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (sessionId, turnId) => |
| 182 | Effect.tryPromise({ | 185 | runtime.interruptTurn(sessionId, turnId).pipe( |
| 183 | try: () => runtime.interruptTurn(sessionId, turnId), | 186 | Effect.mapError((cause) => toRequestError(sessionId, "turn/interrupt", cause)), |
| 184 | catch: (cause) => toRequestError(sessionId, "turn/interrupt", cause), | 187 | ); |
| 185 | }); | | |
| 186 | | 188 | |
| 187 | const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) => | 189 | const readThread: ClaudeCodeAdapterShape["readThread"] = (sessionId) => |
| 188 | Effect.tryPromise({ | 190 | runtime.readThread(sessionId).pipe( |
| 189 | try: () => runtime.readThread(sessionId), | 191 | Effect.mapError((cause) => toRequestError(sessionId, "thread/read", cause)), |
| 190 | catch: (cause) => toRequestError(sessionId, "thread/read", cause), | 192 | ); |
| 191 | }); | | |
| 192 | | 193 | |
| 193 | const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) => { | 194 | const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (sessionId, numTurns) => { |
| 194 | if (!Number.isInteger(numTurns) || numTurns < 1) { | 195 | if (!Number.isInteger(numTurns) || numTurns < 1) { |
| ... | @@ -201,10 +202,9 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => | ... | @@ -201,10 +202,9 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => |
| 201 | ); | 202 | ); |
| 202 | } | 203 | } |
| 203 | | 204 | |
| 204 | return Effect.tryPromise({ | 205 | return runtime.rollbackThread(sessionId, numTurns).pipe( |
| 205 | try: () => runtime.rollbackThread(sessionId, numTurns), | 206 | Effect.mapError((cause) => toRequestError(sessionId, "thread/rollback", cause)), |
| 206 | catch: (cause) => toRequestError(sessionId, "thread/rollback", cause), | 207 | ); |
| 207 | }); | | |
| 208 | }; | 208 | }; |
| 209 | | 209 | |
| 210 | const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( | 210 | const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( |
| ... | @@ -212,26 +212,17 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => | ... | @@ -212,26 +212,17 @@ const makeClaudeCodeAdapter = (options?: ClaudeCodeAdapterLiveOptions) => |
| 212 | requestId, | 212 | requestId, |
| 213 | decision, | 213 | decision, |
| 214 | ) => | 214 | ) => |
| 215 | Effect.tryPromise({ | 215 | runtime.respondToRequest(sessionId, requestId, decision).pipe( |
| 216 | try: () => runtime.respondToRequest(sessionId, requestId, decision), | 216 | Effect.mapError((cause) => toRequestError(sessionId, "item/requestApproval/decision", cause)), |
| 217 | catch: (cause) => toRequestError(sessionId, "item/requestApproval/decision", cause), | 217 | ); |
| 218 | }); | | |
| 219 | | 218 | |
| 220 | const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) => | 219 | const stopSession: ClaudeCodeAdapterShape["stopSession"] = (sessionId) => runtime.stopSession(sessionId); |
| 221 | Effect.sync(() => { | | |
| 222 | runtime.stopSession(sessionId); | | |
| 223 | }); | | |
| 224 | | 220 | |
| 225 | const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => | 221 | const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => runtime.listSessions(); |
| 226 | Effect.sync(() => runtime.listSessions()); | | |
| 227 | | 222 | |
| 228 | const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) => | 223 | const hasSession: ClaudeCodeAdapterShape["hasSession"] = (sessionId) => runtime.hasSession(sessionId); |
| 229 | Effect.sync(() => runtime.hasSession(sessionId)); | | |
| 230 | | 224 | |
| 231 | const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => | 225 | const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => runtime.stopAll(); |
| 232 | Effect.sync(() => { | | |
| 233 | runtime.stopAll(); | | |
| 234 | }); | | |
| 235 | | 226 | |
| 236 | return { | 227 | return { |
| 237 | provider: PROVIDER, | 228 | provider: PROVIDER, |
| ... | @@ -254,4 +245,3 @@ export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeC | ... | @@ -254,4 +245,3 @@ export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeC |
| 254 | export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) { | 245 | export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) { |
| 255 | return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options)); | 246 | return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options)); |
| 256 | } | 247 | } |
| 257 | | | |