| ... | @@ -1,1857 +0,0 @@ |
| 1 | /** |
| 2 | * ClaudeCodeAdapterLive - Scoped live implementation for the Claude Code provider adapter. |
| 3 | * |
| 4 | * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic |
| 5 | * provider adapter contract and emits canonical runtime events. |
| 6 | * |
| 7 | * @module ClaudeCodeAdapterLive |
| 8 | */ |
| 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 | type CanonicalItemType, |
| 23 | type CanonicalRequestType, |
| 24 | EventId, |
| 25 | type ProviderApprovalDecision, |
| 26 | ProviderItemId, |
| 27 | type ProviderRuntimeEvent, |
| 28 | type ProviderRuntimeTurnStatus, |
| 29 | type ProviderSendTurnInput, |
| 30 | type ProviderSession, |
| 31 | RuntimeItemId, |
| 32 | RuntimeRequestId, |
| 33 | RuntimeTaskId, |
| 34 | ThreadId, |
| 35 | TurnId, |
| 36 | } from "@t3tools/contracts"; |
| 37 | import { Cause, DateTime, Deferred, Effect, Layer, Queue, Random, Ref, Stream } from "effect"; |
| 38 | |
| 39 | import { |
| 40 | ProviderAdapterProcessError, |
| 41 | ProviderAdapterRequestError, |
| 42 | ProviderAdapterSessionClosedError, |
| 43 | ProviderAdapterSessionNotFoundError, |
| 44 | ProviderAdapterValidationError, |
| 45 | type ProviderAdapterError, |
| 46 | } from "../Errors.ts"; |
| 47 | import { ClaudeCodeAdapter, type ClaudeCodeAdapterShape } from "../Services/ClaudeCodeAdapter.ts"; |
| 48 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| 49 | |
| 50 | const PROVIDER = "claudeCode" as const; |
| 51 | |
| 52 | type PromptQueueItem = |
| 53 | | { |
| 54 | readonly type: "message"; |
| 55 | readonly message: SDKUserMessage; |
| 56 | } |
| 57 | | { |
| 58 | readonly type: "terminate"; |
| 59 | }; |
| 60 | |
| 61 | interface ClaudeResumeState { |
| 62 | readonly threadId?: ThreadId; |
| 63 | readonly resume?: string; |
| 64 | readonly resumeSessionAt?: string; |
| 65 | readonly turnCount?: number; |
| 66 | } |
| 67 | |
| 68 | interface ClaudeTurnState { |
| 69 | readonly turnId: TurnId; |
| 70 | readonly assistantItemId: string; |
| 71 | readonly startedAt: string; |
| 72 | readonly items: Array<unknown>; |
| 73 | readonly messageCompleted: boolean; |
| 74 | readonly emittedTextDelta: boolean; |
| 75 | readonly fallbackAssistantText: string; |
| 76 | } |
| 77 | |
| 78 | interface PendingApproval { |
| 79 | readonly requestType: CanonicalRequestType; |
| 80 | readonly detail?: string; |
| 81 | readonly suggestions?: ReadonlyArray<PermissionUpdate>; |
| 82 | readonly decision: Deferred.Deferred<ProviderApprovalDecision>; |
| 83 | } |
| 84 | |
| 85 | interface ToolInFlight { |
| 86 | readonly itemId: string; |
| 87 | readonly itemType: CanonicalItemType; |
| 88 | readonly toolName: string; |
| 89 | readonly title: string; |
| 90 | readonly detail?: string; |
| 91 | } |
| 92 | |
| 93 | interface ClaudeSessionContext { |
| 94 | session: ProviderSession; |
| 95 | readonly promptQueue: Queue.Queue<PromptQueueItem>; |
| 96 | readonly query: ClaudeQueryRuntime; |
| 97 | readonly startedAt: string; |
| 98 | resumeSessionId: string | undefined; |
| 99 | readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>; |
| 100 | readonly turns: Array<{ |
| 101 | id: TurnId; |
| 102 | items: Array<unknown>; |
| 103 | }>; |
| 104 | readonly inFlightTools: Map<number, ToolInFlight>; |
| 105 | turnState: ClaudeTurnState | undefined; |
| 106 | lastAssistantUuid: string | undefined; |
| 107 | lastThreadStartedId: string | undefined; |
| 108 | stopped: boolean; |
| 109 | } |
| 110 | |
| 111 | interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> { |
| 112 | readonly interrupt: () => Promise<void>; |
| 113 | readonly setModel: (model?: string) => Promise<void>; |
| 114 | readonly setPermissionMode: (mode: PermissionMode) => Promise<void>; |
| 115 | readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>; |
| 116 | readonly close: () => void; |
| 117 | } |
| 118 | |
| 119 | export interface ClaudeCodeAdapterLiveOptions { |
| 120 | readonly createQuery?: (input: { |
| 121 | readonly prompt: AsyncIterable<SDKUserMessage>; |
| 122 | readonly options: ClaudeQueryOptions; |
| 123 | }) => ClaudeQueryRuntime; |
| 124 | readonly nativeEventLogPath?: string; |
| 125 | readonly nativeEventLogger?: EventNdjsonLogger; |
| 126 | } |
| 127 | |
| 128 | function isUuid(value: string): boolean { |
| 129 | return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value); |
| 130 | } |
| 131 | |
| 132 | function isSyntheticClaudeThreadId(value: string): boolean { |
| 133 | return value.startsWith("claude-thread-"); |
| 134 | } |
| 135 | |
| 136 | function toMessage(cause: unknown, fallback: string): string { |
| 137 | if (cause instanceof Error && cause.message.length > 0) { |
| 138 | return cause.message; |
| 139 | } |
| 140 | return fallback; |
| 141 | } |
| 142 | |
| 143 | function asRuntimeItemId(value: string): RuntimeItemId { |
| 144 | return RuntimeItemId.makeUnsafe(value); |
| 145 | } |
| 146 | |
| 147 | function asCanonicalTurnId(value: TurnId): TurnId { |
| 148 | return value; |
| 149 | } |
| 150 | |
| 151 | function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId { |
| 152 | return RuntimeRequestId.makeUnsafe(value); |
| 153 | } |
| 154 | |
| 155 | function toPermissionMode(value: unknown): PermissionMode | undefined { |
| 156 | switch (value) { |
| 157 | case "default": |
| 158 | case "acceptEdits": |
| 159 | case "bypassPermissions": |
| 160 | case "plan": |
| 161 | case "dontAsk": |
| 162 | return value; |
| 163 | default: |
| 164 | return undefined; |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined { |
| 169 | if (!resumeCursor || typeof resumeCursor !== "object") { |
| 170 | return undefined; |
| 171 | } |
| 172 | const cursor = resumeCursor as { |
| 173 | threadId?: unknown; |
| 174 | resume?: unknown; |
| 175 | sessionId?: unknown; |
| 176 | resumeSessionAt?: unknown; |
| 177 | turnCount?: unknown; |
| 178 | }; |
| 179 | |
| 180 | const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined; |
| 181 | const threadId = |
| 182 | threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate) |
| 183 | ? ThreadId.makeUnsafe(threadIdCandidate) |
| 184 | : undefined; |
| 185 | const resumeCandidate = |
| 186 | typeof cursor.resume === "string" |
| 187 | ? cursor.resume |
| 188 | : typeof cursor.sessionId === "string" |
| 189 | ? cursor.sessionId |
| 190 | : undefined; |
| 191 | const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined; |
| 192 | const resumeSessionAt = |
| 193 | typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined; |
| 194 | const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined; |
| 195 | |
| 196 | return { |
| 197 | ...(threadId ? { threadId } : {}), |
| 198 | ...(resume ? { resume } : {}), |
| 199 | ...(resumeSessionAt ? { resumeSessionAt } : {}), |
| 200 | ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0 |
| 201 | ? { turnCount: turnCountValue } |
| 202 | : {}), |
| 203 | }; |
| 204 | } |
| 205 | |
| 206 | function classifyToolItemType(toolName: string): CanonicalItemType { |
| 207 | const normalized = toolName.toLowerCase(); |
| 208 | if ( |
| 209 | normalized.includes("bash") || |
| 210 | normalized.includes("command") || |
| 211 | normalized.includes("shell") || |
| 212 | normalized.includes("terminal") |
| 213 | ) { |
| 214 | return "command_execution"; |
| 215 | } |
| 216 | if ( |
| 217 | normalized.includes("edit") || |
| 218 | normalized.includes("write") || |
| 219 | normalized.includes("file") || |
| 220 | normalized.includes("patch") || |
| 221 | normalized.includes("replace") || |
| 222 | normalized.includes("create") || |
| 223 | normalized.includes("delete") |
| 224 | ) { |
| 225 | return "file_change"; |
| 226 | } |
| 227 | if (normalized.includes("mcp")) { |
| 228 | return "mcp_tool_call"; |
| 229 | } |
| 230 | return "dynamic_tool_call"; |
| 231 | } |
| 232 | |
| 233 | function classifyRequestType(toolName: string): CanonicalRequestType { |
| 234 | const normalized = toolName.toLowerCase(); |
| 235 | if (normalized === "read" || normalized.includes("read file") || normalized.includes("view")) { |
| 236 | return "file_read_approval"; |
| 237 | } |
| 238 | return classifyToolItemType(toolName) === "command_execution" |
| 239 | ? "command_execution_approval" |
| 240 | : "file_change_approval"; |
| 241 | } |
| 242 | |
| 243 | function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string { |
| 244 | const commandValue = input.command ?? input.cmd; |
| 245 | const command = typeof commandValue === "string" ? commandValue : undefined; |
| 246 | if (command && command.trim().length > 0) { |
| 247 | return `${toolName}: ${command.trim().slice(0, 400)}`; |
| 248 | } |
| 249 | |
| 250 | const serialized = JSON.stringify(input); |
| 251 | if (serialized.length <= 400) { |
| 252 | return `${toolName}: ${serialized}`; |
| 253 | } |
| 254 | return `${toolName}: ${serialized.slice(0, 397)}...`; |
| 255 | } |
| 256 | |
| 257 | function titleForTool(itemType: CanonicalItemType): string { |
| 258 | switch (itemType) { |
| 259 | case "command_execution": |
| 260 | return "Command run"; |
| 261 | case "file_change": |
| 262 | return "File change"; |
| 263 | case "mcp_tool_call": |
| 264 | return "MCP tool call"; |
| 265 | case "dynamic_tool_call": |
| 266 | return "Tool call"; |
| 267 | default: |
| 268 | return "Item"; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | function buildUserMessage(input: ProviderSendTurnInput): SDKUserMessage { |
| 273 | const fragments: string[] = []; |
| 274 | |
| 275 | if (input.input && input.input.trim().length > 0) { |
| 276 | fragments.push(input.input.trim()); |
| 277 | } |
| 278 | |
| 279 | for (const attachment of input.attachments ?? []) { |
| 280 | if (attachment.type === "image") { |
| 281 | fragments.push( |
| 282 | `Attached image: ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes).`, |
| 283 | ); |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | const text = fragments.join("\n\n"); |
| 288 | |
| 289 | return { |
| 290 | type: "user", |
| 291 | session_id: "", |
| 292 | parent_tool_use_id: null, |
| 293 | message: { |
| 294 | role: "user", |
| 295 | content: [{ type: "text", text }], |
| 296 | }, |
| 297 | } as SDKUserMessage; |
| 298 | } |
| 299 | |
| 300 | function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus { |
| 301 | if (result.subtype === "success") { |
| 302 | return "completed"; |
| 303 | } |
| 304 | |
| 305 | const errors = result.errors.join(" ").toLowerCase(); |
| 306 | if (errors.includes("interrupt")) { |
| 307 | return "interrupted"; |
| 308 | } |
| 309 | if (errors.includes("cancel")) { |
| 310 | return "cancelled"; |
| 311 | } |
| 312 | return "failed"; |
| 313 | } |
| 314 | |
| 315 | function streamKindFromDeltaType(deltaType: string): "assistant_text" | "reasoning_text" { |
| 316 | return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text"; |
| 317 | } |
| 318 | |
| 319 | function providerThreadRef( |
| 320 | context: ClaudeSessionContext, |
| 321 | ): { readonly providerThreadId: string } | {} { |
| 322 | return context.resumeSessionId ? { providerThreadId: context.resumeSessionId } : {}; |
| 323 | } |
| 324 | |
| 325 | function extractAssistantText(message: SDKMessage): string { |
| 326 | if (message.type !== "assistant") { |
| 327 | return ""; |
| 328 | } |
| 329 | |
| 330 | const content = (message.message as { content?: unknown } | undefined)?.content; |
| 331 | if (!Array.isArray(content)) { |
| 332 | return ""; |
| 333 | } |
| 334 | |
| 335 | const fragments: string[] = []; |
| 336 | for (const block of content) { |
| 337 | if (!block || typeof block !== "object") { |
| 338 | continue; |
| 339 | } |
| 340 | const candidate = block as { type?: unknown; text?: unknown }; |
| 341 | if ( |
| 342 | candidate.type === "text" && |
| 343 | typeof candidate.text === "string" && |
| 344 | candidate.text.length > 0 |
| 345 | ) { |
| 346 | fragments.push(candidate.text); |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | return fragments.join(""); |
| 351 | } |
| 352 | |
| 353 | function toSessionError( |
| 354 | threadId: ThreadId, |
| 355 | cause: unknown, |
| 356 | ): ProviderAdapterSessionNotFoundError | ProviderAdapterSessionClosedError | undefined { |
| 357 | const normalized = toMessage(cause, "").toLowerCase(); |
| 358 | if (normalized.includes("unknown session") || normalized.includes("not found")) { |
| 359 | return new ProviderAdapterSessionNotFoundError({ |
| 360 | provider: PROVIDER, |
| 361 | threadId, |
| 362 | cause, |
| 363 | }); |
| 364 | } |
| 365 | if (normalized.includes("closed")) { |
| 366 | return new ProviderAdapterSessionClosedError({ |
| 367 | provider: PROVIDER, |
| 368 | threadId, |
| 369 | cause, |
| 370 | }); |
| 371 | } |
| 372 | return undefined; |
| 373 | } |
| 374 | |
| 375 | function toRequestError( |
| 376 | threadId: ThreadId, |
| 377 | method: string, |
| 378 | cause: unknown, |
| 379 | ): ProviderAdapterError { |
| 380 | const sessionError = toSessionError(threadId, cause); |
| 381 | if (sessionError) { |
| 382 | return sessionError; |
| 383 | } |
| 384 | return new ProviderAdapterRequestError({ |
| 385 | provider: PROVIDER, |
| 386 | method, |
| 387 | detail: toMessage(cause, `${method} failed`), |
| 388 | cause, |
| 389 | }); |
| 390 | } |
| 391 | |
| 392 | function sdkMessageType(value: unknown): string | undefined { |
| 393 | if (!value || typeof value !== "object") { |
| 394 | return undefined; |
| 395 | } |
| 396 | const record = value as { type?: unknown }; |
| 397 | return typeof record.type === "string" ? record.type : undefined; |
| 398 | } |
| 399 | |
| 400 | function sdkMessageSubtype(value: unknown): string | undefined { |
| 401 | if (!value || typeof value !== "object") { |
| 402 | return undefined; |
| 403 | } |
| 404 | const record = value as { subtype?: unknown }; |
| 405 | return typeof record.subtype === "string" ? record.subtype : undefined; |
| 406 | } |
| 407 | |
| 408 | function sdkNativeMethod(message: SDKMessage): string { |
| 409 | const subtype = sdkMessageSubtype(message); |
| 410 | if (subtype) { |
| 411 | return `claude/${message.type}/${subtype}`; |
| 412 | } |
| 413 | |
| 414 | if (message.type === "stream_event") { |
| 415 | const streamType = sdkMessageType(message.event); |
| 416 | if (streamType) { |
| 417 | const deltaType = |
| 418 | streamType === "content_block_delta" |
| 419 | ? sdkMessageType((message.event as { delta?: unknown }).delta) |
| 420 | : undefined; |
| 421 | if (deltaType) { |
| 422 | return `claude/${message.type}/${streamType}/${deltaType}`; |
| 423 | } |
| 424 | return `claude/${message.type}/${streamType}`; |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | return `claude/${message.type}`; |
| 429 | } |
| 430 | |
| 431 | function sdkNativeItemId(message: SDKMessage): string | undefined { |
| 432 | if (message.type === "assistant") { |
| 433 | const maybeId = (message.message as { id?: unknown }).id; |
| 434 | if (typeof maybeId === "string") { |
| 435 | return maybeId; |
| 436 | } |
| 437 | return undefined; |
| 438 | } |
| 439 | |
| 440 | if (message.type === "stream_event") { |
| 441 | const event = message.event as { |
| 442 | type?: unknown; |
| 443 | content_block?: { id?: unknown }; |
| 444 | }; |
| 445 | if (event.type === "content_block_start" && typeof event.content_block?.id === "string") { |
| 446 | return event.content_block.id; |
| 447 | } |
| 448 | } |
| 449 | |
| 450 | return undefined; |
| 451 | } |
| 452 | |
| 453 | function makeClaudeCodeAdapter(options?: ClaudeCodeAdapterLiveOptions) { |
| 454 | return Effect.gen(function* () { |
| 455 | const nativeEventLogger = |
| 456 | options?.nativeEventLogger ?? |
| 457 | (options?.nativeEventLogPath !== undefined |
| 458 | ? yield* makeEventNdjsonLogger(options.nativeEventLogPath, { |
| 459 | stream: "native", |
| 460 | }) |
| 461 | : undefined); |
| 462 | |
| 463 | const createQuery = |
| 464 | options?.createQuery ?? |
| 465 | ((input: { |
| 466 | readonly prompt: AsyncIterable<SDKUserMessage>; |
| 467 | readonly options: ClaudeQueryOptions; |
| 468 | }) => query({ prompt: input.prompt, options: input.options }) as ClaudeQueryRuntime); |
| 469 | |
| 470 | const sessions = new Map<ThreadId, ClaudeSessionContext>(); |
| 471 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 472 | |
| 473 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| 474 | const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.makeUnsafe(id)); |
| 475 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); |
| 476 | |
| 477 | const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| 478 | Queue.offer(runtimeEventQueue, event).pipe(Effect.asVoid); |
| 479 | |
| 480 | const logNativeSdkMessage = ( |
| 481 | context: ClaudeSessionContext, |
| 482 | message: SDKMessage, |
| 483 | ): Effect.Effect<void> => |
| 484 | Effect.gen(function* () { |
| 485 | if (!nativeEventLogger) { |
| 486 | return; |
| 487 | } |
| 488 | |
| 489 | const observedAt = new Date().toISOString(); |
| 490 | const itemId = sdkNativeItemId(message); |
| 491 | |
| 492 | yield* nativeEventLogger |
| 493 | .write( |
| 494 | { |
| 495 | observedAt, |
| 496 | event: { |
| 497 | id: |
| 498 | "uuid" in message && typeof message.uuid === "string" |
| 499 | ? message.uuid |
| 500 | : crypto.randomUUID(), |
| 501 | kind: "notification", |
| 502 | provider: PROVIDER, |
| 503 | createdAt: observedAt, |
| 504 | method: sdkNativeMethod(message), |
| 505 | ...(typeof message.session_id === "string" |
| 506 | ? { providerThreadId: message.session_id } |
| 507 | : {}), |
| 508 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 509 | ...(itemId ? { itemId: ProviderItemId.makeUnsafe(itemId) } : {}), |
| 510 | payload: message, |
| 511 | }, |
| 512 | }, |
| 513 | null, |
| 514 | ); |
| 515 | }); |
| 516 | |
| 517 | const snapshotThread = ( |
| 518 | context: ClaudeSessionContext, |
| 519 | ): Effect.Effect<{ |
| 520 | threadId: ThreadId; |
| 521 | turns: ReadonlyArray<{ |
| 522 | id: TurnId; |
| 523 | items: ReadonlyArray<unknown>; |
| 524 | }>; |
| 525 | }, ProviderAdapterValidationError> => |
| 526 | Effect.gen(function* () { |
| 527 | const threadId = context.session.threadId; |
| 528 | if (!threadId) { |
| 529 | return yield* new ProviderAdapterValidationError({ |
| 530 | provider: PROVIDER, |
| 531 | operation: "readThread", |
| 532 | issue: "Session thread id is not initialized yet.", |
| 533 | }); |
| 534 | } |
| 535 | return { |
| 536 | threadId, |
| 537 | turns: context.turns.map((turn) => ({ |
| 538 | id: turn.id, |
| 539 | items: [...turn.items], |
| 540 | })), |
| 541 | }; |
| 542 | }); |
| 543 | |
| 544 | const updateResumeCursor = (context: ClaudeSessionContext): Effect.Effect<void> => |
| 545 | Effect.gen(function* () { |
| 546 | const threadId = context.session.threadId; |
| 547 | if (!threadId) return; |
| 548 | |
| 549 | const resumeCursor = { |
| 550 | threadId, |
| 551 | ...(context.resumeSessionId ? { resume: context.resumeSessionId } : {}), |
| 552 | ...(context.lastAssistantUuid ? { resumeSessionAt: context.lastAssistantUuid } : {}), |
| 553 | turnCount: context.turns.length, |
| 554 | }; |
| 555 | |
| 556 | context.session = { |
| 557 | ...context.session, |
| 558 | resumeCursor, |
| 559 | updatedAt: yield* nowIso, |
| 560 | }; |
| 561 | }); |
| 562 | |
| 563 | const ensureThreadId = ( |
| 564 | context: ClaudeSessionContext, |
| 565 | message: SDKMessage, |
| 566 | ): Effect.Effect<void> => |
| 567 | Effect.gen(function* () { |
| 568 | if (typeof message.session_id !== "string" || message.session_id.length === 0) { |
| 569 | return; |
| 570 | } |
| 571 | const nextThreadId = message.session_id; |
| 572 | context.resumeSessionId = message.session_id; |
| 573 | yield* updateResumeCursor(context); |
| 574 | |
| 575 | if (context.lastThreadStartedId !== nextThreadId) { |
| 576 | context.lastThreadStartedId = nextThreadId; |
| 577 | const stamp = yield* makeEventStamp(); |
| 578 | yield* offerRuntimeEvent({ |
| 579 | type: "thread.started", |
| 580 | eventId: stamp.eventId, |
| 581 | provider: PROVIDER, |
| 582 | createdAt: stamp.createdAt, |
| 583 | threadId: context.session.threadId, |
| 584 | payload: { |
| 585 | providerThreadId: nextThreadId, |
| 586 | }, |
| 587 | providerRefs: {}, |
| 588 | raw: { |
| 589 | source: "claude.sdk.message", |
| 590 | method: "claude/thread/started", |
| 591 | payload: { |
| 592 | session_id: message.session_id, |
| 593 | }, |
| 594 | }, |
| 595 | }); |
| 596 | } |
| 597 | }); |
| 598 | |
| 599 | const emitRuntimeError = ( |
| 600 | context: ClaudeSessionContext, |
| 601 | message: string, |
| 602 | cause?: unknown, |
| 603 | ): Effect.Effect<void> => |
| 604 | Effect.gen(function* () { |
| 605 | if (cause !== undefined) { |
| 606 | void cause; |
| 607 | } |
| 608 | const turnState = context.turnState; |
| 609 | const stamp = yield* makeEventStamp(); |
| 610 | yield* offerRuntimeEvent({ |
| 611 | type: "runtime.error", |
| 612 | eventId: stamp.eventId, |
| 613 | provider: PROVIDER, |
| 614 | createdAt: stamp.createdAt, |
| 615 | threadId: context.session.threadId, |
| 616 | ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), |
| 617 | payload: { |
| 618 | message, |
| 619 | class: "provider_error", |
| 620 | ...(cause !== undefined ? { detail: cause } : {}), |
| 621 | }, |
| 622 | providerRefs: { |
| 623 | ...providerThreadRef(context), |
| 624 | ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), |
| 625 | }, |
| 626 | }); |
| 627 | }); |
| 628 | |
| 629 | const emitRuntimeWarning = ( |
| 630 | context: ClaudeSessionContext, |
| 631 | message: string, |
| 632 | detail?: unknown, |
| 633 | ): Effect.Effect<void> => |
| 634 | Effect.gen(function* () { |
| 635 | const turnState = context.turnState; |
| 636 | const stamp = yield* makeEventStamp(); |
| 637 | yield* offerRuntimeEvent({ |
| 638 | type: "runtime.warning", |
| 639 | eventId: stamp.eventId, |
| 640 | provider: PROVIDER, |
| 641 | createdAt: stamp.createdAt, |
| 642 | threadId: context.session.threadId, |
| 643 | ...(turnState ? { turnId: asCanonicalTurnId(turnState.turnId) } : {}), |
| 644 | payload: { |
| 645 | message, |
| 646 | ...(detail !== undefined ? { detail } : {}), |
| 647 | }, |
| 648 | providerRefs: { |
| 649 | ...providerThreadRef(context), |
| 650 | ...(turnState ? { providerTurnId: String(turnState.turnId) } : {}), |
| 651 | }, |
| 652 | }); |
| 653 | }); |
| 654 | |
| 655 | const completeTurn = ( |
| 656 | context: ClaudeSessionContext, |
| 657 | status: ProviderRuntimeTurnStatus, |
| 658 | errorMessage?: string, |
| 659 | result?: SDKResultMessage, |
| 660 | ): Effect.Effect<void> => |
| 661 | Effect.gen(function* () { |
| 662 | const turnState = context.turnState; |
| 663 | if (!turnState) { |
| 664 | const stamp = yield* makeEventStamp(); |
| 665 | yield* offerRuntimeEvent({ |
| 666 | type: "turn.completed", |
| 667 | eventId: stamp.eventId, |
| 668 | provider: PROVIDER, |
| 669 | createdAt: stamp.createdAt, |
| 670 | threadId: context.session.threadId, |
| 671 | payload: { |
| 672 | state: status, |
| 673 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), |
| 674 | ...(result?.usage ? { usage: result.usage } : {}), |
| 675 | ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), |
| 676 | ...(typeof result?.total_cost_usd === "number" |
| 677 | ? { totalCostUsd: result.total_cost_usd } |
| 678 | : {}), |
| 679 | ...(errorMessage ? { errorMessage } : {}), |
| 680 | }, |
| 681 | providerRefs: {}, |
| 682 | }); |
| 683 | return; |
| 684 | } |
| 685 | |
| 686 | if (!turnState.messageCompleted) { |
| 687 | if (!turnState.emittedTextDelta && turnState.fallbackAssistantText.length > 0) { |
| 688 | const deltaStamp = yield* makeEventStamp(); |
| 689 | yield* offerRuntimeEvent({ |
| 690 | type: "content.delta", |
| 691 | eventId: deltaStamp.eventId, |
| 692 | provider: PROVIDER, |
| 693 | createdAt: deltaStamp.createdAt, |
| 694 | threadId: context.session.threadId, |
| 695 | turnId: turnState.turnId, |
| 696 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 697 | payload: { |
| 698 | streamKind: "assistant_text", |
| 699 | delta: turnState.fallbackAssistantText, |
| 700 | }, |
| 701 | providerRefs: { |
| 702 | ...providerThreadRef(context), |
| 703 | providerTurnId: String(turnState.turnId), |
| 704 | providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), |
| 705 | }, |
| 706 | }); |
| 707 | } |
| 708 | |
| 709 | const stamp = yield* makeEventStamp(); |
| 710 | yield* offerRuntimeEvent({ |
| 711 | type: "item.completed", |
| 712 | eventId: stamp.eventId, |
| 713 | provider: PROVIDER, |
| 714 | createdAt: stamp.createdAt, |
| 715 | itemId: asRuntimeItemId(turnState.assistantItemId), |
| 716 | threadId: context.session.threadId, |
| 717 | turnId: turnState.turnId, |
| 718 | payload: { |
| 719 | itemType: "assistant_message", |
| 720 | status: "completed", |
| 721 | title: "Assistant message", |
| 722 | }, |
| 723 | providerRefs: { |
| 724 | ...providerThreadRef(context), |
| 725 | providerTurnId: turnState.turnId, |
| 726 | providerItemId: ProviderItemId.makeUnsafe(turnState.assistantItemId), |
| 727 | }, |
| 728 | }); |
| 729 | } |
| 730 | |
| 731 | context.turns.push({ |
| 732 | id: turnState.turnId, |
| 733 | items: [...turnState.items], |
| 734 | }); |
| 735 | |
| 736 | const stamp = yield* makeEventStamp(); |
| 737 | yield* offerRuntimeEvent({ |
| 738 | type: "turn.completed", |
| 739 | eventId: stamp.eventId, |
| 740 | provider: PROVIDER, |
| 741 | createdAt: stamp.createdAt, |
| 742 | threadId: context.session.threadId, |
| 743 | turnId: turnState.turnId, |
| 744 | payload: { |
| 745 | state: status, |
| 746 | ...(result?.stop_reason !== undefined ? { stopReason: result.stop_reason } : {}), |
| 747 | ...(result?.usage ? { usage: result.usage } : {}), |
| 748 | ...(result?.modelUsage ? { modelUsage: result.modelUsage } : {}), |
| 749 | ...(typeof result?.total_cost_usd === "number" |
| 750 | ? { totalCostUsd: result.total_cost_usd } |
| 751 | : {}), |
| 752 | ...(errorMessage ? { errorMessage } : {}), |
| 753 | }, |
| 754 | providerRefs: { |
| 755 | ...providerThreadRef(context), |
| 756 | providerTurnId: turnState.turnId, |
| 757 | }, |
| 758 | }); |
| 759 | |
| 760 | const updatedAt = yield* nowIso; |
| 761 | context.turnState = undefined; |
| 762 | context.session = { |
| 763 | ...context.session, |
| 764 | status: "ready", |
| 765 | activeTurnId: undefined, |
| 766 | updatedAt, |
| 767 | ...(status === "failed" && errorMessage ? { lastError: errorMessage } : {}), |
| 768 | }; |
| 769 | yield* updateResumeCursor(context); |
| 770 | }); |
| 771 | |
| 772 | const handleStreamEvent = ( |
| 773 | context: ClaudeSessionContext, |
| 774 | message: SDKMessage, |
| 775 | ): Effect.Effect<void> => |
| 776 | Effect.gen(function* () { |
| 777 | if (message.type !== "stream_event") { |
| 778 | return; |
| 779 | } |
| 780 | |
| 781 | const { event } = message; |
| 782 | |
| 783 | if (event.type === "content_block_delta") { |
| 784 | if ( |
| 785 | event.delta.type === "text_delta" && |
| 786 | event.delta.text.length > 0 && |
| 787 | context.turnState |
| 788 | ) { |
| 789 | if (!context.turnState.emittedTextDelta) { |
| 790 | context.turnState = { |
| 791 | ...context.turnState, |
| 792 | emittedTextDelta: true, |
| 793 | }; |
| 794 | } |
| 795 | const stamp = yield* makeEventStamp(); |
| 796 | yield* offerRuntimeEvent({ |
| 797 | type: "content.delta", |
| 798 | eventId: stamp.eventId, |
| 799 | provider: PROVIDER, |
| 800 | createdAt: stamp.createdAt, |
| 801 | threadId: context.session.threadId, |
| 802 | turnId: context.turnState.turnId, |
| 803 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 804 | payload: { |
| 805 | streamKind: streamKindFromDeltaType(event.delta.type), |
| 806 | delta: event.delta.text, |
| 807 | }, |
| 808 | providerRefs: { |
| 809 | ...providerThreadRef(context), |
| 810 | providerTurnId: context.turnState.turnId, |
| 811 | providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), |
| 812 | }, |
| 813 | raw: { |
| 814 | source: "claude.sdk.message", |
| 815 | method: "claude/stream_event/content_block_delta", |
| 816 | payload: message, |
| 817 | }, |
| 818 | }); |
| 819 | } |
| 820 | return; |
| 821 | } |
| 822 | |
| 823 | if (event.type === "content_block_start") { |
| 824 | const { index, content_block: block } = event; |
| 825 | if ( |
| 826 | block.type !== "tool_use" && |
| 827 | block.type !== "server_tool_use" && |
| 828 | block.type !== "mcp_tool_use" |
| 829 | ) { |
| 830 | return; |
| 831 | } |
| 832 | |
| 833 | const toolName = block.name; |
| 834 | const itemType = classifyToolItemType(toolName); |
| 835 | const toolInput = |
| 836 | typeof block.input === "object" && block.input !== null |
| 837 | ? (block.input as Record<string, unknown>) |
| 838 | : {}; |
| 839 | const itemId = block.id; |
| 840 | const detail = summarizeToolRequest(toolName, toolInput); |
| 841 | |
| 842 | const tool: ToolInFlight = { |
| 843 | itemId, |
| 844 | itemType, |
| 845 | toolName, |
| 846 | title: titleForTool(itemType), |
| 847 | detail, |
| 848 | }; |
| 849 | context.inFlightTools.set(index, tool); |
| 850 | |
| 851 | const stamp = yield* makeEventStamp(); |
| 852 | yield* offerRuntimeEvent({ |
| 853 | type: "item.started", |
| 854 | eventId: stamp.eventId, |
| 855 | provider: PROVIDER, |
| 856 | createdAt: stamp.createdAt, |
| 857 | threadId: context.session.threadId, |
| 858 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 859 | itemId: asRuntimeItemId(tool.itemId), |
| 860 | payload: { |
| 861 | itemType: tool.itemType, |
| 862 | status: "inProgress", |
| 863 | title: tool.title, |
| 864 | ...(tool.detail ? { detail: tool.detail } : {}), |
| 865 | data: { |
| 866 | toolName: tool.toolName, |
| 867 | input: toolInput, |
| 868 | }, |
| 869 | }, |
| 870 | providerRefs: { |
| 871 | ...providerThreadRef(context), |
| 872 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 873 | providerItemId: ProviderItemId.makeUnsafe(tool.itemId), |
| 874 | }, |
| 875 | raw: { |
| 876 | source: "claude.sdk.message", |
| 877 | method: "claude/stream_event/content_block_start", |
| 878 | payload: message, |
| 879 | }, |
| 880 | }); |
| 881 | return; |
| 882 | } |
| 883 | |
| 884 | if (event.type === "content_block_stop") { |
| 885 | const { index } = event; |
| 886 | const tool = context.inFlightTools.get(index); |
| 887 | if (!tool) { |
| 888 | return; |
| 889 | } |
| 890 | context.inFlightTools.delete(index); |
| 891 | |
| 892 | const stamp = yield* makeEventStamp(); |
| 893 | yield* offerRuntimeEvent({ |
| 894 | type: "item.completed", |
| 895 | eventId: stamp.eventId, |
| 896 | provider: PROVIDER, |
| 897 | createdAt: stamp.createdAt, |
| 898 | threadId: context.session.threadId, |
| 899 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 900 | itemId: asRuntimeItemId(tool.itemId), |
| 901 | payload: { |
| 902 | itemType: tool.itemType, |
| 903 | status: "completed", |
| 904 | title: tool.title, |
| 905 | ...(tool.detail ? { detail: tool.detail } : {}), |
| 906 | }, |
| 907 | providerRefs: { |
| 908 | ...providerThreadRef(context), |
| 909 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 910 | providerItemId: ProviderItemId.makeUnsafe(tool.itemId), |
| 911 | }, |
| 912 | raw: { |
| 913 | source: "claude.sdk.message", |
| 914 | method: "claude/stream_event/content_block_stop", |
| 915 | payload: message, |
| 916 | }, |
| 917 | }); |
| 918 | } |
| 919 | }); |
| 920 | |
| 921 | const handleAssistantMessage = ( |
| 922 | context: ClaudeSessionContext, |
| 923 | message: SDKMessage, |
| 924 | ): Effect.Effect<void> => |
| 925 | Effect.gen(function* () { |
| 926 | if (message.type !== "assistant") { |
| 927 | return; |
| 928 | } |
| 929 | |
| 930 | if (context.turnState) { |
| 931 | context.turnState.items.push(message.message); |
| 932 | const fallbackAssistantText = extractAssistantText(message); |
| 933 | if ( |
| 934 | fallbackAssistantText.length > 0 && |
| 935 | fallbackAssistantText !== context.turnState.fallbackAssistantText |
| 936 | ) { |
| 937 | context.turnState = { |
| 938 | ...context.turnState, |
| 939 | fallbackAssistantText, |
| 940 | }; |
| 941 | } |
| 942 | |
| 943 | const stamp = yield* makeEventStamp(); |
| 944 | yield* offerRuntimeEvent({ |
| 945 | type: "item.updated", |
| 946 | eventId: stamp.eventId, |
| 947 | provider: PROVIDER, |
| 948 | createdAt: stamp.createdAt, |
| 949 | threadId: context.session.threadId, |
| 950 | turnId: context.turnState.turnId, |
| 951 | itemId: asRuntimeItemId(context.turnState.assistantItemId), |
| 952 | payload: { |
| 953 | itemType: "assistant_message", |
| 954 | status: "inProgress", |
| 955 | title: "Assistant message", |
| 956 | data: message.message, |
| 957 | }, |
| 958 | providerRefs: { |
| 959 | ...providerThreadRef(context), |
| 960 | providerTurnId: context.turnState.turnId, |
| 961 | providerItemId: ProviderItemId.makeUnsafe(context.turnState.assistantItemId), |
| 962 | }, |
| 963 | raw: { |
| 964 | source: "claude.sdk.message", |
| 965 | method: "claude/assistant", |
| 966 | payload: message, |
| 967 | }, |
| 968 | }); |
| 969 | } |
| 970 | |
| 971 | context.lastAssistantUuid = message.uuid; |
| 972 | yield* updateResumeCursor(context); |
| 973 | }); |
| 974 | |
| 975 | const handleResultMessage = ( |
| 976 | context: ClaudeSessionContext, |
| 977 | message: SDKMessage, |
| 978 | ): Effect.Effect<void> => |
| 979 | Effect.gen(function* () { |
| 980 | if (message.type !== "result") { |
| 981 | return; |
| 982 | } |
| 983 | |
| 984 | const status = turnStatusFromResult(message); |
| 985 | const errorMessage = message.subtype === "success" ? undefined : message.errors[0]; |
| 986 | |
| 987 | if (status === "failed") { |
| 988 | yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed."); |
| 989 | } |
| 990 | |
| 991 | yield* completeTurn(context, status, errorMessage, message); |
| 992 | }); |
| 993 | |
| 994 | const handleSystemMessage = ( |
| 995 | context: ClaudeSessionContext, |
| 996 | message: SDKMessage, |
| 997 | ): Effect.Effect<void> => |
| 998 | Effect.gen(function* () { |
| 999 | if (message.type !== "system") { |
| 1000 | return; |
| 1001 | } |
| 1002 | |
| 1003 | const stamp = yield* makeEventStamp(); |
| 1004 | const base = { |
| 1005 | eventId: stamp.eventId, |
| 1006 | provider: PROVIDER, |
| 1007 | createdAt: stamp.createdAt, |
| 1008 | threadId: context.session.threadId, |
| 1009 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1010 | providerRefs: { |
| 1011 | ...providerThreadRef(context), |
| 1012 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), |
| 1013 | }, |
| 1014 | raw: { |
| 1015 | source: "claude.sdk.message" as const, |
| 1016 | method: sdkNativeMethod(message), |
| 1017 | messageType: `${message.type}:${message.subtype}`, |
| 1018 | payload: message, |
| 1019 | }, |
| 1020 | }; |
| 1021 | |
| 1022 | switch (message.subtype) { |
| 1023 | case "init": |
| 1024 | yield* offerRuntimeEvent({ |
| 1025 | ...base, |
| 1026 | type: "session.configured", |
| 1027 | payload: { |
| 1028 | config: message as Record<string, unknown>, |
| 1029 | }, |
| 1030 | }); |
| 1031 | return; |
| 1032 | case "status": |
| 1033 | yield* offerRuntimeEvent({ |
| 1034 | ...base, |
| 1035 | type: "session.state.changed", |
| 1036 | payload: { |
| 1037 | state: message.status === "compacting" ? "waiting" : "running", |
| 1038 | reason: `status:${message.status ?? "active"}`, |
| 1039 | detail: message, |
| 1040 | }, |
| 1041 | }); |
| 1042 | return; |
| 1043 | case "compact_boundary": |
| 1044 | yield* offerRuntimeEvent({ |
| 1045 | ...base, |
| 1046 | type: "thread.state.changed", |
| 1047 | payload: { |
| 1048 | state: "compacted", |
| 1049 | detail: message, |
| 1050 | }, |
| 1051 | }); |
| 1052 | return; |
| 1053 | case "hook_started": |
| 1054 | yield* offerRuntimeEvent({ |
| 1055 | ...base, |
| 1056 | type: "hook.started", |
| 1057 | payload: { |
| 1058 | hookId: message.hook_id, |
| 1059 | hookName: message.hook_name, |
| 1060 | hookEvent: message.hook_event, |
| 1061 | }, |
| 1062 | }); |
| 1063 | return; |
| 1064 | case "hook_progress": |
| 1065 | yield* offerRuntimeEvent({ |
| 1066 | ...base, |
| 1067 | type: "hook.progress", |
| 1068 | payload: { |
| 1069 | hookId: message.hook_id, |
| 1070 | output: message.output, |
| 1071 | stdout: message.stdout, |
| 1072 | stderr: message.stderr, |
| 1073 | }, |
| 1074 | }); |
| 1075 | return; |
| 1076 | case "hook_response": |
| 1077 | yield* offerRuntimeEvent({ |
| 1078 | ...base, |
| 1079 | type: "hook.completed", |
| 1080 | payload: { |
| 1081 | hookId: message.hook_id, |
| 1082 | outcome: message.outcome, |
| 1083 | output: message.output, |
| 1084 | stdout: message.stdout, |
| 1085 | stderr: message.stderr, |
| 1086 | ...(typeof message.exit_code === "number" ? { exitCode: message.exit_code } : {}), |
| 1087 | }, |
| 1088 | }); |
| 1089 | return; |
| 1090 | case "task_started": |
| 1091 | yield* offerRuntimeEvent({ |
| 1092 | ...base, |
| 1093 | type: "task.started", |
| 1094 | payload: { |
| 1095 | taskId: RuntimeTaskId.makeUnsafe(message.task_id), |
| 1096 | description: message.description, |
| 1097 | ...(message.task_type ? { taskType: message.task_type } : {}), |
| 1098 | }, |
| 1099 | }); |
| 1100 | return; |
| 1101 | case "task_progress": |
| 1102 | yield* offerRuntimeEvent({ |
| 1103 | ...base, |
| 1104 | type: "task.progress", |
| 1105 | payload: { |
| 1106 | taskId: RuntimeTaskId.makeUnsafe(message.task_id), |
| 1107 | description: message.description, |
| 1108 | ...(message.usage ? { usage: message.usage } : {}), |
| 1109 | ...(message.last_tool_name ? { lastToolName: message.last_tool_name } : {}), |
| 1110 | }, |
| 1111 | }); |
| 1112 | return; |
| 1113 | case "task_notification": |
| 1114 | yield* offerRuntimeEvent({ |
| 1115 | ...base, |
| 1116 | type: "task.completed", |
| 1117 | payload: { |
| 1118 | taskId: RuntimeTaskId.makeUnsafe(message.task_id), |
| 1119 | status: message.status, |
| 1120 | ...(message.summary ? { summary: message.summary } : {}), |
| 1121 | ...(message.usage ? { usage: message.usage } : {}), |
| 1122 | }, |
| 1123 | }); |
| 1124 | return; |
| 1125 | case "files_persisted": |
| 1126 | yield* offerRuntimeEvent({ |
| 1127 | ...base, |
| 1128 | type: "files.persisted", |
| 1129 | payload: { |
| 1130 | files: Array.isArray(message.files) |
| 1131 | ? message.files.map((file: { filename: string; file_id: string }) => ({ |
| 1132 | filename: file.filename, |
| 1133 | fileId: file.file_id, |
| 1134 | })) |
| 1135 | : [], |
| 1136 | ...(Array.isArray(message.failed) |
| 1137 | ? { |
| 1138 | failed: message.failed.map((entry: { filename: string; error: string }) => ({ |
| 1139 | filename: entry.filename, |
| 1140 | error: entry.error, |
| 1141 | })), |
| 1142 | } |
| 1143 | : {}), |
| 1144 | }, |
| 1145 | }); |
| 1146 | return; |
| 1147 | default: |
| 1148 | yield* emitRuntimeWarning( |
| 1149 | context, |
| 1150 | `Unhandled Claude system message subtype '${message.subtype}'.`, |
| 1151 | message, |
| 1152 | ); |
| 1153 | return; |
| 1154 | } |
| 1155 | }); |
| 1156 | |
| 1157 | const handleSdkTelemetryMessage = ( |
| 1158 | context: ClaudeSessionContext, |
| 1159 | message: SDKMessage, |
| 1160 | ): Effect.Effect<void> => |
| 1161 | Effect.gen(function* () { |
| 1162 | const stamp = yield* makeEventStamp(); |
| 1163 | const base = { |
| 1164 | eventId: stamp.eventId, |
| 1165 | provider: PROVIDER, |
| 1166 | createdAt: stamp.createdAt, |
| 1167 | threadId: context.session.threadId, |
| 1168 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1169 | providerRefs: { |
| 1170 | ...providerThreadRef(context), |
| 1171 | ...(context.turnState ? { providerTurnId: context.turnState.turnId } : {}), |
| 1172 | }, |
| 1173 | raw: { |
| 1174 | source: "claude.sdk.message" as const, |
| 1175 | method: sdkNativeMethod(message), |
| 1176 | messageType: message.type, |
| 1177 | payload: message, |
| 1178 | }, |
| 1179 | }; |
| 1180 | |
| 1181 | if (message.type === "tool_progress") { |
| 1182 | yield* offerRuntimeEvent({ |
| 1183 | ...base, |
| 1184 | type: "tool.progress", |
| 1185 | payload: { |
| 1186 | toolUseId: message.tool_use_id, |
| 1187 | toolName: message.tool_name, |
| 1188 | elapsedSeconds: message.elapsed_time_seconds, |
| 1189 | ...(message.task_id ? { summary: `task:${message.task_id}` } : {}), |
| 1190 | }, |
| 1191 | }); |
| 1192 | return; |
| 1193 | } |
| 1194 | |
| 1195 | if (message.type === "tool_use_summary") { |
| 1196 | yield* offerRuntimeEvent({ |
| 1197 | ...base, |
| 1198 | type: "tool.summary", |
| 1199 | payload: { |
| 1200 | summary: message.summary, |
| 1201 | ...(message.preceding_tool_use_ids.length > 0 |
| 1202 | ? { precedingToolUseIds: message.preceding_tool_use_ids } |
| 1203 | : {}), |
| 1204 | }, |
| 1205 | }); |
| 1206 | return; |
| 1207 | } |
| 1208 | |
| 1209 | if (message.type === "auth_status") { |
| 1210 | yield* offerRuntimeEvent({ |
| 1211 | ...base, |
| 1212 | type: "auth.status", |
| 1213 | payload: { |
| 1214 | isAuthenticating: message.isAuthenticating, |
| 1215 | output: message.output, |
| 1216 | ...(message.error ? { error: message.error } : {}), |
| 1217 | }, |
| 1218 | }); |
| 1219 | return; |
| 1220 | } |
| 1221 | |
| 1222 | if (message.type === "rate_limit_event") { |
| 1223 | yield* offerRuntimeEvent({ |
| 1224 | ...base, |
| 1225 | type: "account.rate-limits.updated", |
| 1226 | payload: { |
| 1227 | rateLimits: message, |
| 1228 | }, |
| 1229 | }); |
| 1230 | return; |
| 1231 | } |
| 1232 | }); |
| 1233 | |
| 1234 | const handleSdkMessage = ( |
| 1235 | context: ClaudeSessionContext, |
| 1236 | message: SDKMessage, |
| 1237 | ): Effect.Effect<void> => |
| 1238 | Effect.gen(function* () { |
| 1239 | yield* logNativeSdkMessage(context, message); |
| 1240 | yield* ensureThreadId(context, message); |
| 1241 | |
| 1242 | switch (message.type) { |
| 1243 | case "stream_event": |
| 1244 | yield* handleStreamEvent(context, message); |
| 1245 | return; |
| 1246 | case "user": |
| 1247 | return; |
| 1248 | case "assistant": |
| 1249 | yield* handleAssistantMessage(context, message); |
| 1250 | return; |
| 1251 | case "result": |
| 1252 | yield* handleResultMessage(context, message); |
| 1253 | return; |
| 1254 | case "system": |
| 1255 | yield* handleSystemMessage(context, message); |
| 1256 | return; |
| 1257 | case "tool_progress": |
| 1258 | case "tool_use_summary": |
| 1259 | case "auth_status": |
| 1260 | case "rate_limit_event": |
| 1261 | yield* handleSdkTelemetryMessage(context, message); |
| 1262 | return; |
| 1263 | default: |
| 1264 | yield* emitRuntimeWarning( |
| 1265 | context, |
| 1266 | `Unhandled Claude SDK message type '${message.type}'.`, |
| 1267 | message, |
| 1268 | ); |
| 1269 | return; |
| 1270 | } |
| 1271 | }); |
| 1272 | |
| 1273 | const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void> => |
| 1274 | Stream.fromAsyncIterable(context.query, (cause) => cause).pipe( |
| 1275 | Stream.takeWhile(() => !context.stopped), |
| 1276 | Stream.runForEach((message) => handleSdkMessage(context, message)), |
| 1277 | Effect.catchCause((cause) => |
| 1278 | Effect.gen(function* () { |
| 1279 | if (Cause.hasInterruptsOnly(cause) || context.stopped) { |
| 1280 | return; |
| 1281 | } |
| 1282 | const message = toMessage(Cause.squash(cause), "Claude runtime stream failed."); |
| 1283 | yield* emitRuntimeError(context, message, cause); |
| 1284 | yield* completeTurn(context, "failed", message); |
| 1285 | }), |
| 1286 | ), |
| 1287 | ); |
| 1288 | |
| 1289 | const stopSessionInternal = ( |
| 1290 | context: ClaudeSessionContext, |
| 1291 | options?: { readonly emitExitEvent?: boolean }, |
| 1292 | ): Effect.Effect<void> => |
| 1293 | Effect.gen(function* () { |
| 1294 | if (context.stopped) return; |
| 1295 | |
| 1296 | context.stopped = true; |
| 1297 | |
| 1298 | for (const [requestId, pending] of context.pendingApprovals) { |
| 1299 | yield* Deferred.succeed(pending.decision, "cancel"); |
| 1300 | const stamp = yield* makeEventStamp(); |
| 1301 | yield* offerRuntimeEvent({ |
| 1302 | type: "request.resolved", |
| 1303 | eventId: stamp.eventId, |
| 1304 | provider: PROVIDER, |
| 1305 | createdAt: stamp.createdAt, |
| 1306 | threadId: context.session.threadId, |
| 1307 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1308 | requestId: asRuntimeRequestId(requestId), |
| 1309 | payload: { |
| 1310 | requestType: pending.requestType, |
| 1311 | decision: "cancel", |
| 1312 | }, |
| 1313 | providerRefs: { |
| 1314 | ...providerThreadRef(context), |
| 1315 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1316 | providerRequestId: requestId, |
| 1317 | }, |
| 1318 | }); |
| 1319 | } |
| 1320 | context.pendingApprovals.clear(); |
| 1321 | |
| 1322 | if (context.turnState) { |
| 1323 | yield* completeTurn(context, "interrupted", "Session stopped."); |
| 1324 | } |
| 1325 | |
| 1326 | yield* Queue.shutdown(context.promptQueue); |
| 1327 | |
| 1328 | context.query.close(); |
| 1329 | |
| 1330 | const updatedAt = yield* nowIso; |
| 1331 | context.session = { |
| 1332 | ...context.session, |
| 1333 | status: "closed", |
| 1334 | activeTurnId: undefined, |
| 1335 | updatedAt, |
| 1336 | }; |
| 1337 | |
| 1338 | if (options?.emitExitEvent !== false) { |
| 1339 | const stamp = yield* makeEventStamp(); |
| 1340 | yield* offerRuntimeEvent({ |
| 1341 | type: "session.exited", |
| 1342 | eventId: stamp.eventId, |
| 1343 | provider: PROVIDER, |
| 1344 | createdAt: stamp.createdAt, |
| 1345 | threadId: context.session.threadId, |
| 1346 | payload: { |
| 1347 | reason: "Session stopped", |
| 1348 | exitKind: "graceful", |
| 1349 | }, |
| 1350 | providerRefs: {}, |
| 1351 | }); |
| 1352 | } |
| 1353 | |
| 1354 | sessions.delete(context.session.threadId); |
| 1355 | }); |
| 1356 | |
| 1357 | const requireSession = ( |
| 1358 | threadId: ThreadId, |
| 1359 | ): Effect.Effect<ClaudeSessionContext, ProviderAdapterError> => { |
| 1360 | const context = sessions.get(threadId); |
| 1361 | if (!context) { |
| 1362 | return Effect.fail( |
| 1363 | new ProviderAdapterSessionNotFoundError({ |
| 1364 | provider: PROVIDER, |
| 1365 | threadId, |
| 1366 | }), |
| 1367 | ); |
| 1368 | } |
| 1369 | if (context.stopped || context.session.status === "closed") { |
| 1370 | return Effect.fail( |
| 1371 | new ProviderAdapterSessionClosedError({ |
| 1372 | provider: PROVIDER, |
| 1373 | threadId, |
| 1374 | }), |
| 1375 | ); |
| 1376 | } |
| 1377 | return Effect.succeed(context); |
| 1378 | }; |
| 1379 | |
| 1380 | const startSession: ClaudeCodeAdapterShape["startSession"] = (input) => |
| 1381 | Effect.gen(function* () { |
| 1382 | if (input.provider !== undefined && input.provider !== PROVIDER) { |
| 1383 | return yield* new ProviderAdapterValidationError({ |
| 1384 | provider: PROVIDER, |
| 1385 | operation: "startSession", |
| 1386 | issue: `Expected provider '${PROVIDER}' but received '${input.provider}'.`, |
| 1387 | }); |
| 1388 | } |
| 1389 | |
| 1390 | const startedAt = yield* nowIso; |
| 1391 | const resumeState = readClaudeResumeState(input.resumeCursor); |
| 1392 | const threadId = input.threadId; |
| 1393 | |
| 1394 | const promptQueue = yield* Queue.unbounded<PromptQueueItem>(); |
| 1395 | const prompt = Stream.fromQueue(promptQueue).pipe( |
| 1396 | Stream.filter((item) => item.type === "message"), |
| 1397 | Stream.map((item) => item.message), |
| 1398 | Stream.toAsyncIterable, |
| 1399 | ); |
| 1400 | |
| 1401 | const pendingApprovals = new Map<ApprovalRequestId, PendingApproval>(); |
| 1402 | const inFlightTools = new Map<number, ToolInFlight>(); |
| 1403 | |
| 1404 | const contextRef = yield* Ref.make<ClaudeSessionContext | undefined>(undefined); |
| 1405 | |
| 1406 | const canUseTool: CanUseTool = (toolName, toolInput, callbackOptions) => |
| 1407 | Effect.runPromise( |
| 1408 | Effect.gen(function* () { |
| 1409 | const context = yield* Ref.get(contextRef); |
| 1410 | if (!context) { |
| 1411 | return { |
| 1412 | behavior: "deny", |
| 1413 | message: "Claude session context is unavailable.", |
| 1414 | } satisfies PermissionResult; |
| 1415 | } |
| 1416 | |
| 1417 | const runtimeMode = input.runtimeMode ?? "full-access"; |
| 1418 | if (runtimeMode === "full-access") { |
| 1419 | return { |
| 1420 | behavior: "allow", |
| 1421 | updatedInput: toolInput, |
| 1422 | } satisfies PermissionResult; |
| 1423 | } |
| 1424 | |
| 1425 | const requestId = ApprovalRequestId.makeUnsafe(yield* Random.nextUUIDv4); |
| 1426 | const requestType = classifyRequestType(toolName); |
| 1427 | const detail = summarizeToolRequest(toolName, toolInput); |
| 1428 | const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>(); |
| 1429 | const pendingApproval: PendingApproval = { |
| 1430 | requestType, |
| 1431 | detail, |
| 1432 | decision: decisionDeferred, |
| 1433 | ...(callbackOptions.suggestions |
| 1434 | ? { suggestions: callbackOptions.suggestions } |
| 1435 | : {}), |
| 1436 | }; |
| 1437 | |
| 1438 | const requestedStamp = yield* makeEventStamp(); |
| 1439 | yield* offerRuntimeEvent({ |
| 1440 | type: "request.opened", |
| 1441 | eventId: requestedStamp.eventId, |
| 1442 | provider: PROVIDER, |
| 1443 | createdAt: requestedStamp.createdAt, |
| 1444 | threadId: context.session.threadId, |
| 1445 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1446 | requestId: asRuntimeRequestId(requestId), |
| 1447 | payload: { |
| 1448 | requestType, |
| 1449 | detail, |
| 1450 | args: { |
| 1451 | toolName, |
| 1452 | input: toolInput, |
| 1453 | ...(callbackOptions.toolUseID ? { toolUseId: callbackOptions.toolUseID } : {}), |
| 1454 | }, |
| 1455 | }, |
| 1456 | providerRefs: { |
| 1457 | ...(context.session.threadId |
| 1458 | ? { providerThreadId: context.session.threadId } |
| 1459 | : {}), |
| 1460 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1461 | providerRequestId: requestId, |
| 1462 | }, |
| 1463 | raw: { |
| 1464 | source: "claude.sdk.permission", |
| 1465 | method: "canUseTool/request", |
| 1466 | payload: { |
| 1467 | toolName, |
| 1468 | input: toolInput, |
| 1469 | }, |
| 1470 | }, |
| 1471 | }); |
| 1472 | |
| 1473 | pendingApprovals.set(requestId, pendingApproval); |
| 1474 | |
| 1475 | const onAbort = () => { |
| 1476 | if (!pendingApprovals.has(requestId)) { |
| 1477 | return; |
| 1478 | } |
| 1479 | pendingApprovals.delete(requestId); |
| 1480 | Effect.runFork(Deferred.succeed(decisionDeferred, "cancel")); |
| 1481 | }; |
| 1482 | |
| 1483 | callbackOptions.signal.addEventListener("abort", onAbort, { |
| 1484 | once: true, |
| 1485 | }); |
| 1486 | |
| 1487 | const decision = yield* Deferred.await(decisionDeferred); |
| 1488 | pendingApprovals.delete(requestId); |
| 1489 | |
| 1490 | const resolvedStamp = yield* makeEventStamp(); |
| 1491 | yield* offerRuntimeEvent({ |
| 1492 | type: "request.resolved", |
| 1493 | eventId: resolvedStamp.eventId, |
| 1494 | provider: PROVIDER, |
| 1495 | createdAt: resolvedStamp.createdAt, |
| 1496 | threadId: context.session.threadId, |
| 1497 | ...(context.turnState ? { turnId: asCanonicalTurnId(context.turnState.turnId) } : {}), |
| 1498 | requestId: asRuntimeRequestId(requestId), |
| 1499 | payload: { |
| 1500 | requestType, |
| 1501 | decision, |
| 1502 | }, |
| 1503 | providerRefs: { |
| 1504 | ...(context.session.threadId |
| 1505 | ? { providerThreadId: context.session.threadId } |
| 1506 | : {}), |
| 1507 | ...(context.turnState ? { providerTurnId: String(context.turnState.turnId) } : {}), |
| 1508 | providerRequestId: requestId, |
| 1509 | }, |
| 1510 | raw: { |
| 1511 | source: "claude.sdk.permission", |
| 1512 | method: "canUseTool/decision", |
| 1513 | payload: { |
| 1514 | decision, |
| 1515 | }, |
| 1516 | }, |
| 1517 | }); |
| 1518 | |
| 1519 | if (decision === "accept" || decision === "acceptForSession") { |
| 1520 | return { |
| 1521 | behavior: "allow", |
| 1522 | updatedInput: toolInput, |
| 1523 | ...(decision === "acceptForSession" && pendingApproval.suggestions |
| 1524 | ? { updatedPermissions: [...pendingApproval.suggestions] } |
| 1525 | : {}), |
| 1526 | } satisfies PermissionResult; |
| 1527 | } |
| 1528 | |
| 1529 | return { |
| 1530 | behavior: "deny", |
| 1531 | message: |
| 1532 | decision === "cancel" |
| 1533 | ? "User cancelled tool execution." |
| 1534 | : "User declined tool execution.", |
| 1535 | } satisfies PermissionResult; |
| 1536 | }), |
| 1537 | ); |
| 1538 | |
| 1539 | const providerOptions = input.providerOptions?.claudeCode; |
| 1540 | const permissionMode = |
| 1541 | toPermissionMode(providerOptions?.permissionMode) ?? |
| 1542 | (input.runtimeMode === "full-access" ? "bypassPermissions" : undefined); |
| 1543 | |
| 1544 | const queryOptions: ClaudeQueryOptions = { |
| 1545 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 1546 | ...(input.model ? { model: input.model } : {}), |
| 1547 | ...(providerOptions?.binaryPath |
| 1548 | ? { pathToClaudeCodeExecutable: providerOptions.binaryPath } |
| 1549 | : {}), |
| 1550 | ...(permissionMode ? { permissionMode } : {}), |
| 1551 | ...(permissionMode === "bypassPermissions" |
| 1552 | ? { allowDangerouslySkipPermissions: true } |
| 1553 | : {}), |
| 1554 | ...(providerOptions?.maxThinkingTokens !== undefined |
| 1555 | ? { maxThinkingTokens: providerOptions.maxThinkingTokens } |
| 1556 | : {}), |
| 1557 | ...(resumeState?.resume ? { resume: resumeState.resume } : {}), |
| 1558 | ...(resumeState?.resumeSessionAt ? { resumeSessionAt: resumeState.resumeSessionAt } : {}), |
| 1559 | includePartialMessages: true, |
| 1560 | canUseTool, |
| 1561 | env: process.env, |
| 1562 | ...(input.cwd ? { additionalDirectories: [input.cwd] } : {}), |
| 1563 | }; |
| 1564 | |
| 1565 | const queryRuntime = yield* Effect.try({ |
| 1566 | try: () => |
| 1567 | createQuery({ |
| 1568 | prompt, |
| 1569 | options: queryOptions, |
| 1570 | }), |
| 1571 | catch: (cause) => |
| 1572 | new ProviderAdapterProcessError({ |
| 1573 | provider: PROVIDER, |
| 1574 | threadId, |
| 1575 | detail: toMessage(cause, "Failed to start Claude runtime session."), |
| 1576 | cause, |
| 1577 | }), |
| 1578 | }); |
| 1579 | |
| 1580 | const session: ProviderSession = { |
| 1581 | threadId, |
| 1582 | provider: PROVIDER, |
| 1583 | status: "ready", |
| 1584 | runtimeMode: input.runtimeMode, |
| 1585 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 1586 | ...(input.model ? { model: input.model } : {}), |
| 1587 | ...(threadId ? { threadId } : {}), |
| 1588 | resumeCursor: { |
| 1589 | ...(threadId ? { threadId } : {}), |
| 1590 | ...(resumeState?.resume ? { resume: resumeState.resume } : {}), |
| 1591 | ...(resumeState?.resumeSessionAt |
| 1592 | ? { resumeSessionAt: resumeState.resumeSessionAt } |
| 1593 | : {}), |
| 1594 | turnCount: resumeState?.turnCount ?? 0, |
| 1595 | }, |
| 1596 | createdAt: startedAt, |
| 1597 | updatedAt: startedAt, |
| 1598 | }; |
| 1599 | |
| 1600 | const context: ClaudeSessionContext = { |
| 1601 | session, |
| 1602 | promptQueue, |
| 1603 | query: queryRuntime, |
| 1604 | startedAt, |
| 1605 | resumeSessionId: resumeState?.resume, |
| 1606 | pendingApprovals, |
| 1607 | turns: [], |
| 1608 | inFlightTools, |
| 1609 | turnState: undefined, |
| 1610 | lastAssistantUuid: resumeState?.resumeSessionAt, |
| 1611 | lastThreadStartedId: undefined, |
| 1612 | stopped: false, |
| 1613 | }; |
| 1614 | yield* Ref.set(contextRef, context); |
| 1615 | sessions.set(threadId, context); |
| 1616 | |
| 1617 | const sessionStartedStamp = yield* makeEventStamp(); |
| 1618 | yield* offerRuntimeEvent({ |
| 1619 | type: "session.started", |
| 1620 | eventId: sessionStartedStamp.eventId, |
| 1621 | provider: PROVIDER, |
| 1622 | createdAt: sessionStartedStamp.createdAt, |
| 1623 | threadId, |
| 1624 | payload: input.resumeCursor !== undefined ? { resume: input.resumeCursor } : {}, |
| 1625 | providerRefs: {}, |
| 1626 | }); |
| 1627 | |
| 1628 | const configuredStamp = yield* makeEventStamp(); |
| 1629 | yield* offerRuntimeEvent({ |
| 1630 | type: "session.configured", |
| 1631 | eventId: configuredStamp.eventId, |
| 1632 | provider: PROVIDER, |
| 1633 | createdAt: configuredStamp.createdAt, |
| 1634 | threadId, |
| 1635 | payload: { |
| 1636 | config: { |
| 1637 | ...(input.model ? { model: input.model } : {}), |
| 1638 | ...(input.cwd ? { cwd: input.cwd } : {}), |
| 1639 | ...(permissionMode ? { permissionMode } : {}), |
| 1640 | ...(providerOptions?.maxThinkingTokens !== undefined |
| 1641 | ? { maxThinkingTokens: providerOptions.maxThinkingTokens } |
| 1642 | : {}), |
| 1643 | }, |
| 1644 | }, |
| 1645 | providerRefs: {}, |
| 1646 | }); |
| 1647 | |
| 1648 | const readyStamp = yield* makeEventStamp(); |
| 1649 | yield* offerRuntimeEvent({ |
| 1650 | type: "session.state.changed", |
| 1651 | eventId: readyStamp.eventId, |
| 1652 | provider: PROVIDER, |
| 1653 | createdAt: readyStamp.createdAt, |
| 1654 | threadId, |
| 1655 | payload: { |
| 1656 | state: "ready", |
| 1657 | }, |
| 1658 | providerRefs: {}, |
| 1659 | }); |
| 1660 | |
| 1661 | Effect.runFork(runSdkStream(context)); |
| 1662 | |
| 1663 | return { |
| 1664 | ...session, |
| 1665 | }; |
| 1666 | }); |
| 1667 | |
| 1668 | const sendTurn: ClaudeCodeAdapterShape["sendTurn"] = (input) => |
| 1669 | Effect.gen(function* () { |
| 1670 | const context = yield* requireSession(input.threadId); |
| 1671 | |
| 1672 | if (context.turnState) { |
| 1673 | return yield* new ProviderAdapterValidationError({ |
| 1674 | provider: PROVIDER, |
| 1675 | operation: "sendTurn", |
| 1676 | issue: `Thread '${input.threadId}' already has an active turn '${context.turnState.turnId}'.`, |
| 1677 | }); |
| 1678 | } |
| 1679 | |
| 1680 | if (input.model) { |
| 1681 | yield* Effect.tryPromise({ |
| 1682 | try: () => context.query.setModel(input.model), |
| 1683 | catch: (cause) => toRequestError(input.threadId, "turn/setModel", cause), |
| 1684 | }); |
| 1685 | } |
| 1686 | |
| 1687 | const turnId = TurnId.makeUnsafe(yield* Random.nextUUIDv4); |
| 1688 | const turnState: ClaudeTurnState = { |
| 1689 | turnId, |
| 1690 | assistantItemId: yield* Random.nextUUIDv4, |
| 1691 | startedAt: yield* nowIso, |
| 1692 | items: [], |
| 1693 | messageCompleted: false, |
| 1694 | emittedTextDelta: false, |
| 1695 | fallbackAssistantText: "", |
| 1696 | }; |
| 1697 | |
| 1698 | const updatedAt = yield* nowIso; |
| 1699 | context.turnState = turnState; |
| 1700 | context.session = { |
| 1701 | ...context.session, |
| 1702 | status: "running", |
| 1703 | activeTurnId: turnId, |
| 1704 | updatedAt, |
| 1705 | }; |
| 1706 | |
| 1707 | const turnStartedStamp = yield* makeEventStamp(); |
| 1708 | yield* offerRuntimeEvent({ |
| 1709 | type: "turn.started", |
| 1710 | eventId: turnStartedStamp.eventId, |
| 1711 | provider: PROVIDER, |
| 1712 | createdAt: turnStartedStamp.createdAt, |
| 1713 | threadId: context.session.threadId, |
| 1714 | turnId, |
| 1715 | payload: input.model ? { model: input.model } : {}, |
| 1716 | providerRefs: { |
| 1717 | providerTurnId: String(turnId), |
| 1718 | }, |
| 1719 | }); |
| 1720 | |
| 1721 | const message = buildUserMessage(input); |
| 1722 | |
| 1723 | yield* Queue.offer(context.promptQueue, { |
| 1724 | type: "message", |
| 1725 | message, |
| 1726 | }).pipe(Effect.mapError((cause) => toRequestError(input.threadId, "turn/start", cause))); |
| 1727 | |
| 1728 | return { |
| 1729 | threadId: context.session.threadId, |
| 1730 | turnId, |
| 1731 | ...(context.session.resumeCursor !== undefined |
| 1732 | ? { resumeCursor: context.session.resumeCursor } |
| 1733 | : {}), |
| 1734 | }; |
| 1735 | }); |
| 1736 | |
| 1737 | const interruptTurn: ClaudeCodeAdapterShape["interruptTurn"] = (threadId, _turnId) => |
| 1738 | Effect.gen(function* () { |
| 1739 | const context = yield* requireSession(threadId); |
| 1740 | yield* Effect.tryPromise({ |
| 1741 | try: () => context.query.interrupt(), |
| 1742 | catch: (cause) => toRequestError(threadId, "turn/interrupt", cause), |
| 1743 | }); |
| 1744 | }); |
| 1745 | |
| 1746 | const readThread: ClaudeCodeAdapterShape["readThread"] = (threadId) => |
| 1747 | Effect.gen(function* () { |
| 1748 | const context = yield* requireSession(threadId); |
| 1749 | return yield* snapshotThread(context); |
| 1750 | }); |
| 1751 | |
| 1752 | const rollbackThread: ClaudeCodeAdapterShape["rollbackThread"] = (threadId, numTurns) => |
| 1753 | Effect.gen(function* () { |
| 1754 | const context = yield* requireSession(threadId); |
| 1755 | const nextLength = Math.max(0, context.turns.length - numTurns); |
| 1756 | context.turns.splice(nextLength); |
| 1757 | yield* updateResumeCursor(context); |
| 1758 | return yield* snapshotThread(context); |
| 1759 | }); |
| 1760 | |
| 1761 | const respondToRequest: ClaudeCodeAdapterShape["respondToRequest"] = ( |
| 1762 | threadId, |
| 1763 | requestId, |
| 1764 | decision, |
| 1765 | ) => |
| 1766 | Effect.gen(function* () { |
| 1767 | const context = yield* requireSession(threadId); |
| 1768 | const pending = context.pendingApprovals.get(requestId); |
| 1769 | if (!pending) { |
| 1770 | return yield* new ProviderAdapterRequestError({ |
| 1771 | provider: PROVIDER, |
| 1772 | method: "item/requestApproval/decision", |
| 1773 | detail: `Unknown pending approval request: ${requestId}`, |
| 1774 | }); |
| 1775 | } |
| 1776 | |
| 1777 | context.pendingApprovals.delete(requestId); |
| 1778 | yield* Deferred.succeed(pending.decision, decision); |
| 1779 | }); |
| 1780 | |
| 1781 | const respondToUserInput: ClaudeCodeAdapterShape["respondToUserInput"] = ( |
| 1782 | threadId, |
| 1783 | requestId, |
| 1784 | _answers, |
| 1785 | ) => |
| 1786 | Effect.fail( |
| 1787 | new ProviderAdapterRequestError({ |
| 1788 | provider: PROVIDER, |
| 1789 | method: "item/tool/requestUserInput", |
| 1790 | detail: `Claude Code does not yet support structured user-input responses for thread '${threadId}' and request '${requestId}'.`, |
| 1791 | }), |
| 1792 | ); |
| 1793 | |
| 1794 | const stopSession: ClaudeCodeAdapterShape["stopSession"] = (threadId) => |
| 1795 | Effect.gen(function* () { |
| 1796 | const context = yield* requireSession(threadId); |
| 1797 | yield* stopSessionInternal(context, { |
| 1798 | emitExitEvent: true, |
| 1799 | }); |
| 1800 | }); |
| 1801 | |
| 1802 | const listSessions: ClaudeCodeAdapterShape["listSessions"] = () => |
| 1803 | Effect.sync(() => Array.from(sessions.values(), ({ session }) => ({ ...session }))); |
| 1804 | |
| 1805 | const hasSession: ClaudeCodeAdapterShape["hasSession"] = (threadId) => |
| 1806 | Effect.sync(() => { |
| 1807 | const context = sessions.get(threadId); |
| 1808 | return context !== undefined && !context.stopped; |
| 1809 | }); |
| 1810 | |
| 1811 | const stopAll: ClaudeCodeAdapterShape["stopAll"] = () => |
| 1812 | Effect.forEach( |
| 1813 | sessions, |
| 1814 | ([, context]) => |
| 1815 | stopSessionInternal(context, { |
| 1816 | emitExitEvent: true, |
| 1817 | }), |
| 1818 | { discard: true }, |
| 1819 | ); |
| 1820 | |
| 1821 | yield* Effect.addFinalizer(() => |
| 1822 | Effect.forEach( |
| 1823 | sessions, |
| 1824 | ([, context]) => |
| 1825 | stopSessionInternal(context, { |
| 1826 | emitExitEvent: false, |
| 1827 | }), |
| 1828 | { discard: true }, |
| 1829 | ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), |
| 1830 | ); |
| 1831 | |
| 1832 | return { |
| 1833 | provider: PROVIDER, |
| 1834 | capabilities: { |
| 1835 | sessionModelSwitch: "in-session", |
| 1836 | }, |
| 1837 | startSession, |
| 1838 | sendTurn, |
| 1839 | interruptTurn, |
| 1840 | readThread, |
| 1841 | rollbackThread, |
| 1842 | respondToRequest, |
| 1843 | respondToUserInput, |
| 1844 | stopSession, |
| 1845 | listSessions, |
| 1846 | hasSession, |
| 1847 | stopAll, |
| 1848 | streamEvents: Stream.fromQueue(runtimeEventQueue), |
| 1849 | } satisfies ClaudeCodeAdapterShape; |
| 1850 | }); |
| 1851 | } |
| 1852 | |
| 1853 | export const ClaudeCodeAdapterLive = Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter()); |
| 1854 | |
| 1855 | export function makeClaudeCodeAdapterLive(options?: ClaudeCodeAdapterLiveOptions) { |
| 1856 | return Layer.effect(ClaudeCodeAdapter, makeClaudeCodeAdapter(options)); |
| 1857 | } |