import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices"; import type { Options as ClaudeQueryOptions, PermissionMode, PermissionResult, SDKMessage, SDKUserMessage, } from "@anthropic-ai/claude-agent-sdk"; import { ApprovalRequestId, ProviderItemId, ProviderRuntimeEvent, type RuntimeMode, ThreadId, } from "@t3tools/contracts"; import { assert, describe, it } from "@effect/vitest"; import { Effect, Fiber, Layer, Random, Stream } from "effect"; import { attachmentRelativePath } from "../../attachmentStore.ts"; import { ServerConfig } from "../../config.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; import { ProviderAdapterValidationError } from "../Errors.ts"; import { ClaudeAdapter } from "../Services/ClaudeAdapter.ts"; import { makeClaudeAdapterLive, type ClaudeAdapterLiveOptions } from "./ClaudeAdapter.ts"; class FakeClaudeQuery implements AsyncIterable { private readonly queue: Array = []; private readonly waiters: Array<{ readonly resolve: (value: IteratorResult) => void; readonly reject: (reason: unknown) => void; }> = []; private done = false; private failure: unknown | undefined; public readonly interruptCalls: Array = []; public readonly setModelCalls: Array = []; public readonly setPermissionModeCalls: Array = []; public readonly setMaxThinkingTokensCalls: Array = []; public closeCalls = 0; emit(message: SDKMessage): void { if (this.done) { return; } const waiter = this.waiters.shift(); if (waiter) { waiter.resolve({ done: false, value: message }); return; } this.queue.push(message); } fail(cause: unknown): void { if (this.done) { return; } this.done = true; this.failure = cause; for (const waiter of this.waiters.splice(0)) { waiter.reject(cause); } } finish(): void { if (this.done) { return; } this.done = true; this.failure = undefined; for (const waiter of this.waiters.splice(0)) { waiter.resolve({ done: true, value: undefined }); } } readonly interrupt = async (): Promise => { this.interruptCalls.push(undefined); }; readonly setModel = async (model?: string): Promise => { this.setModelCalls.push(model); }; readonly setPermissionMode = async (mode: PermissionMode): Promise => { this.setPermissionModeCalls.push(mode); }; readonly setMaxThinkingTokens = async (maxThinkingTokens: number | null): Promise => { this.setMaxThinkingTokensCalls.push(maxThinkingTokens); }; readonly close = (): void => { this.closeCalls += 1; this.finish(); }; [Symbol.asyncIterator](): AsyncIterator { return { next: () => { if (this.queue.length > 0) { const value = this.queue.shift(); if (value) { return Promise.resolve({ done: false, value, }); } } if (this.failure !== undefined) { const failure = this.failure; this.failure = undefined; return Promise.reject(failure); } if (this.done) { return Promise.resolve({ done: true, value: undefined, }); } return new Promise((resolve, reject) => { this.waiters.push({ resolve, reject, }); }); }, }; } } function makeHarness(config?: { readonly nativeEventLogPath?: string; readonly nativeEventLogger?: ClaudeAdapterLiveOptions["nativeEventLogger"]; readonly cwd?: string; readonly baseDir?: string; }) { const query = new FakeClaudeQuery(); let createInput: | { readonly prompt: AsyncIterable; readonly options: ClaudeQueryOptions; } | undefined; const adapterOptions: ClaudeAdapterLiveOptions = { createQuery: (input) => { createInput = input; return query; }, ...(config?.nativeEventLogger ? { nativeEventLogger: config.nativeEventLogger, } : {}), ...(config?.nativeEventLogPath ? { nativeEventLogPath: config.nativeEventLogPath, } : {}), }; return { layer: makeClaudeAdapterLive(adapterOptions).pipe( Layer.provideMerge( ServerConfig.layerTest( config?.cwd ?? "/tmp/claude-adapter-test", config?.baseDir ?? "/tmp", ), ), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(NodeServices.layer), ), query, getLastCreateQueryInput: () => createInput, }; } function makeDeterministicRandomService(seed = 0x1234_5678): { nextIntUnsafe: () => number; nextDoubleUnsafe: () => number; } { let state = seed >>> 0; const nextIntUnsafe = (): number => { state = (Math.imul(1_664_525, state) + 1_013_904_223) >>> 0; return state; }; return { nextIntUnsafe, nextDoubleUnsafe: () => nextIntUnsafe() / 0x1_0000_0000, }; } async function readFirstPromptText( input: | { readonly prompt: AsyncIterable; } | undefined, ): Promise { const iterator = input?.prompt[Symbol.asyncIterator](); if (!iterator) { return undefined; } const next = await iterator.next(); if (next.done) { return undefined; } if (typeof next.value.message.content === "string") { return next.value.message.content; } const content = next.value.message.content[0]; if (!content || content.type !== "text") { return undefined; } return content.text; } async function readFirstPromptMessage( input: | { readonly prompt: AsyncIterable; } | undefined, ): Promise { const iterator = input?.prompt[Symbol.asyncIterator](); if (!iterator) { return undefined; } const next = await iterator.next(); if (next.done) { return undefined; } return next.value; } const THREAD_ID = ThreadId.makeUnsafe("thread-claude-1"); const RESUME_THREAD_ID = ThreadId.makeUnsafe("thread-claude-resume"); describe("ClaudeAdapterLive", () => { it.effect("returns validation error for non-claude provider on startSession", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const result = yield* adapter .startSession({ threadId: THREAD_ID, provider: "codex", runtimeMode: "full-access" }) .pipe(Effect.result); assert.equal(result._tag, "Failure"); if (result._tag !== "Failure") { return; } assert.deepEqual( result.failure, new ProviderAdapterValidationError({ provider: "claudeAgent", operation: "startSession", issue: "Expected provider 'claudeAgent' but received 'codex'.", }), ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("derives bypass permission mode from full-access runtime policy", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); assert.equal(createInput?.options.permissionMode, "bypassPermissions"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("loads Claude filesystem settings sources for SDK sessions", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "approval-required", }); const createInput = harness.getLastCreateQueryInput(); assert.deepEqual(createInput?.options.settingSources, ["user", "project", "local"]); assert.equal(createInput?.options.permissionMode, undefined); assert.equal(createInput?.options.allowDangerouslySkipPermissions, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("uses bypass permissions for full-access claude sessions", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.permissionMode, "bypassPermissions"); assert.equal(createInput?.options.allowDangerouslySkipPermissions, true); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("forwards claude effort levels into query options", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", options: { effort: "max", }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.effort, "max"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("falls back to default effort when unsupported max is requested for Sonnet 4.6", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6", options: { effort: "max", }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.effort, "high"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("ignores adaptive effort for Haiku 4.5", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-haiku-4-5", options: { effort: "high", }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.effort, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("forwards Claude thinking toggle into SDK settings for Haiku 4.5", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-haiku-4-5", options: { thinking: false, }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.deepEqual(createInput?.options.settings, { alwaysThinkingEnabled: false, }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("ignores Claude thinking toggle for non-Haiku models", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6", options: { thinking: false, }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.settings, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("forwards claude fast mode into SDK settings", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", options: { fastMode: true, }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.deepEqual(createInput?.options.settings, { fastMode: true, }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("ignores claude fast mode for non-opus models", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6", options: { fastMode: true, }, }, runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.settings, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("treats ultrathink as a prompt keyword instead of a session effort", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6", options: { effort: "ultrathink", }, }, runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "Investigate the edge cases", attachments: [], modelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-6", options: { effort: "ultrathink", }, }, }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.effort, "high"); const promptText = yield* Effect.promise(() => readFirstPromptText(createInput)); assert.equal(promptText, "Ultrathink:\nInvestigate the edge cases"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("embeds image attachments in Claude user messages", () => { const baseDir = mkdtempSync(path.join(os.tmpdir(), "claude-attachments-")); const harness = makeHarness({ cwd: "/tmp/project-claude-attachments", baseDir, }); return Effect.gen(function* () { yield* Effect.addFinalizer(() => Effect.sync(() => rmSync(baseDir, { recursive: true, force: true, }), ), ); const adapter = yield* ClaudeAdapter; const { attachmentsDir } = yield* ServerConfig; const attachment = { type: "image" as const, id: "thread-claude-attachment-12345678-1234-1234-1234-123456789abc", name: "diagram.png", mimeType: "image/png", sizeBytes: 4, }; const attachmentPath = path.join(attachmentsDir, attachmentRelativePath(attachment)); mkdirSync(path.dirname(attachmentPath), { recursive: true }); writeFileSync(attachmentPath, Uint8Array.from([1, 2, 3, 4])); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "What's in this image?", attachments: [attachment], }); const createInput = harness.getLastCreateQueryInput(); const promptMessage = yield* Effect.promise(() => readFirstPromptMessage(createInput)); assert.isDefined(promptMessage); assert.deepEqual(promptMessage?.message.content, [ { type: "text", text: "What's in this image?", }, { type: "image", source: { type: "base64", media_type: "image/png", data: "AQIDBA==", }, }, ]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("maps Claude stream/runtime messages to canonical provider runtime events", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 10).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection: { provider: "claudeAgent", model: "claude-sonnet-4-5", }, runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-0", parent_tool_use_id: null, event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-1", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Hi", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-2", parent_tool_use_id: null, event: { type: "content_block_stop", index: 0, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-3", parent_tool_use_id: null, event: { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "tool-1", name: "Bash", input: { command: "ls", }, }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-1", uuid: "stream-4", parent_tool_use_id: null, event: { type: "content_block_stop", index: 1, }, } as unknown as SDKMessage); harness.query.emit({ type: "assistant", session_id: "sdk-session-1", uuid: "assistant-1", parent_tool_use_id: null, message: { id: "assistant-message-1", content: [{ type: "text", text: "Hi" }], }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-1", uuid: "result-1", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.completed", "item.started", "item.completed", "turn.completed", ], ); const turnStarted = runtimeEvents[3]; assert.equal(turnStarted?.type, "turn.started"); if (turnStarted?.type === "turn.started") { assert.equal(String(turnStarted.turnId), String(turn.turnId)); } const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); assert.equal(deltaEvent?.type, "content.delta"); if (deltaEvent?.type === "content.delta") { assert.equal(deltaEvent.payload.delta, "Hi"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); assert.equal(toolStarted?.type, "item.started"); if (toolStarted?.type === "item.started") { assert.equal(toolStarted.payload.itemType, "command_execution"); } const assistantCompletedIndex = runtimeEvents.findIndex( (event) => event.type === "item.completed" && event.payload.itemType === "assistant_message", ); const toolStartedIndex = runtimeEvents.findIndex((event) => event.type === "item.started"); assert.equal( assistantCompletedIndex >= 0 && toolStartedIndex >= 0 && assistantCompletedIndex < toolStartedIndex, true, ); const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; assert.equal(turnCompleted?.type, "turn.completed"); if (turnCompleted?.type === "turn.completed") { assert.equal(String(turnCompleted.turnId), String(turn.turnId)); assert.equal(turnCompleted.payload.state, "completed"); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("maps Claude reasoning deltas, streamed tool inputs, and tool results", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 11).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "stream_event", session_id: "sdk-session-tool-streams", uuid: "stream-thinking", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "thinking_delta", thinking: "Let", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-tool-streams", uuid: "stream-tool-start", parent_tool_use_id: null, event: { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "tool-grep-1", name: "Grep", input: {}, }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-tool-streams", uuid: "stream-tool-input-1", parent_tool_use_id: null, event: { type: "content_block_delta", index: 1, delta: { type: "input_json_delta", partial_json: '{"pattern":"foo","path":"src"}', }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-tool-streams", uuid: "stream-tool-stop", parent_tool_use_id: null, event: { type: "content_block_stop", index: 1, }, } as unknown as SDKMessage); harness.query.emit({ type: "user", session_id: "sdk-session-tool-streams", uuid: "user-tool-result", parent_tool_use_id: null, message: { role: "user", content: [ { type: "tool_result", tool_use_id: "tool-grep-1", content: "src/example.ts:1:foo", }, ], }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-tool-streams", uuid: "result-tool-streams", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.started", "item.updated", "item.updated", "item.completed", "turn.completed", ], ); const reasoningDelta = runtimeEvents.find( (event) => event.type === "content.delta" && event.payload.streamKind === "reasoning_text", ); assert.equal(reasoningDelta?.type, "content.delta"); if (reasoningDelta?.type === "content.delta") { assert.equal(reasoningDelta.payload.delta, "Let"); assert.equal(String(reasoningDelta.turnId), String(turn.turnId)); } const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); assert.equal(toolStarted?.type, "item.started"); if (toolStarted?.type === "item.started") { assert.equal(toolStarted.payload.itemType, "dynamic_tool_call"); } const toolInputUpdated = runtimeEvents.find( (event) => event.type === "item.updated" && (event.payload.data as { input?: { pattern?: string; path?: string } } | undefined)?.input ?.pattern === "foo", ); assert.equal(toolInputUpdated?.type, "item.updated"); if (toolInputUpdated?.type === "item.updated") { assert.deepEqual(toolInputUpdated.payload.data, { toolName: "Grep", input: { pattern: "foo", path: "src", }, }); } const toolResultUpdated = runtimeEvents.find( (event) => event.type === "item.updated" && (event.payload.data as { result?: { tool_use_id?: string } } | undefined)?.result ?.tool_use_id === "tool-grep-1", ); assert.equal(toolResultUpdated?.type, "item.updated"); if (toolResultUpdated?.type === "item.updated") { assert.equal( ( toolResultUpdated.payload.data as { result?: { content?: string }; } ).result?.content, "src/example.ts:1:foo", ); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("classifies Claude Task tool invocations as collaboration agent work", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "delegate this", attachments: [], }); harness.query.emit({ type: "stream_event", session_id: "sdk-session-task", uuid: "stream-task-1", parent_tool_use_id: null, event: { type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "tool-task-1", name: "Task", input: { description: "Review the database layer", prompt: "Audit the SQL changes", subagent_type: "code-reviewer", }, }, }, } as unknown as SDKMessage); harness.query.emit({ type: "assistant", session_id: "sdk-session-task", uuid: "assistant-task-1", parent_tool_use_id: null, message: { id: "assistant-message-task-1", content: [{ type: "text", text: "Delegated" }], }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-task", uuid: "result-task-1", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); const toolStarted = runtimeEvents.find((event) => event.type === "item.started"); assert.equal(toolStarted?.type, "item.started"); if (toolStarted?.type === "item.started") { assert.equal(toolStarted.payload.itemType, "collab_agent_tool_call"); assert.equal(toolStarted.payload.title, "Subagent task"); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("treats user-aborted Claude results as interrupted without a runtime error", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "result", subtype: "error_during_execution", is_error: false, errors: ["Error: Request was aborted."], stop_reason: "tool_use", session_id: "sdk-session-abort", uuid: "result-abort", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "turn.completed", ], ); const turnCompleted = runtimeEvents[runtimeEvents.length - 1]; assert.equal(turnCompleted?.type, "turn.completed"); if (turnCompleted?.type === "turn.completed") { assert.equal(String(turnCompleted.turnId), String(turn.turnId)); assert.equal(turnCompleted.payload.state, "interrupted"); assert.equal(turnCompleted.payload.errorMessage, "Error: Request was aborted."); assert.equal(turnCompleted.payload.stopReason, "tool_use"); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("closes the session when the Claude stream aborts after a turn starts", () => { const harness = makeHarness(); return Effect.gen(function* () { const services = yield* Effect.services(); const runFork = Effect.runForkWith(services); const adapter = yield* ClaudeAdapter; const runtimeEvents: Array = []; const runtimeEventsFiber = runFork( Stream.runForEach(adapter.streamEvents, (event) => Effect.sync(() => { runtimeEvents.push(event); }), ), ); yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: THREAD_ID, input: "hello", attachments: [], }); harness.query.fail(new Error("All fibers interrupted without error")); yield* Effect.yieldNow; yield* Effect.yieldNow; yield* Effect.yieldNow; runtimeEventsFiber.interruptUnsafe(); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "turn.completed", "session.exited", ], ); const turnCompleted = runtimeEvents[4]; assert.equal(turnCompleted?.type, "turn.completed"); if (turnCompleted?.type === "turn.completed") { assert.equal(String(turnCompleted.turnId), String(turn.turnId)); assert.equal(turnCompleted.payload.state, "interrupted"); assert.equal(turnCompleted.payload.errorMessage, "Claude runtime interrupted."); } const sessionExited = runtimeEvents[5]; assert.equal(sessionExited?.type, "session.exited"); assert.equal(yield* adapter.hasSession(THREAD_ID), false); const sessions = yield* adapter.listSessions(); assert.equal(sessions.length, 0); assert.equal(harness.query.closeCalls, 1); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("stopSession does not throw into the SDK prompt consumer", () => { // The SDK consumes user messages via `for await (... of prompt)`. // Stopping a session must end that loop cleanly — not throw an error. // // FakeClaudeQuery.close() masks this by resolving pending iterators // before the shutdown propagates. Override it to match real SDK behavior // where close() does not resolve the prompt consumer. const query = new FakeClaudeQuery(); (query as { close: () => void }).close = () => { query.closeCalls += 1; }; let promptConsumerError: unknown = undefined; const layer = makeClaudeAdapterLive({ createQuery: (input) => { // Simulate the SDK consuming the prompt iterable (async () => { try { for await (const _message of input.prompt) { /* SDK processes user messages */ } } catch (error) { promptConsumerError = error; } })(); return query; }, }).pipe( Layer.provideMerge(ServerConfig.layerTest("/tmp/claude-adapter-test", "/tmp")), Layer.provideMerge(ServerSettingsService.layerTest()), Layer.provideMerge(NodeServices.layer), ); return Effect.gen(function* () { const services = yield* Effect.services(); const runFork = Effect.runForkWith(services); const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = runFork( Stream.runForEach(adapter.streamEvents, () => Effect.void), ); yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.stopSession(THREAD_ID); yield* Effect.yieldNow; yield* Effect.yieldNow; yield* Effect.yieldNow; yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 50))); runtimeEventsFiber.interruptUnsafe(); assert.equal( promptConsumerError, undefined, `Prompt consumer should not receive a thrown error on session stop, ` + `but got: "${promptConsumerError instanceof Error ? promptConsumerError.message : String(promptConsumerError)}"`, ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(layer), ); }); it.effect("forwards Claude task progress summaries for subagent updates", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( Stream.runCollect, Effect.forkChild, ); yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); harness.query.emit({ type: "system", subtype: "task_progress", task_id: "task-subagent-1", description: "Running background teammate", summary: "Code reviewer checked the migration edge cases.", usage: { total_tokens: 123, tool_uses: 4, duration_ms: 987, }, session_id: "sdk-session-task-summary", uuid: "task-progress-1", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); const progressEvent = runtimeEvents.find((event) => event.type === "task.progress"); assert.equal(progressEvent?.type, "task.progress"); if (progressEvent?.type === "task.progress") { assert.equal( progressEvent.payload.summary, "Code reviewer checked the migration edge cases.", ); assert.equal(progressEvent.payload.description, "Running background teammate"); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("emits thread token usage updates from Claude task progress", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 6).pipe( Stream.runCollect, Effect.forkChild, ); yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); harness.query.emit({ type: "system", subtype: "task_progress", task_id: "task-usage-1", description: "Thinking through the patch", usage: { total_tokens: 321, tool_uses: 2, duration_ms: 654, }, session_id: "sdk-session-task-usage", uuid: "task-usage-progress-1", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated"); const progressEvent = runtimeEvents.find((event) => event.type === "task.progress"); assert.equal(usageEvent?.type, "thread.token-usage.updated"); if (usageEvent?.type === "thread.token-usage.updated") { assert.deepEqual(usageEvent.payload, { usage: { usedTokens: 321, lastUsedTokens: 321, toolUses: 2, durationMs: 654, }, }); } assert.equal(progressEvent?.type, "task.progress"); if (usageEvent && progressEvent) { assert.notStrictEqual(usageEvent.eventId, progressEvent.eventId); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("emits Claude context window on result completion usage snapshots", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 7).pipe( Stream.runCollect, Effect.forkChild, ); yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: THREAD_ID, input: "hello", attachments: [], }); harness.query.emit({ type: "result", subtype: "success", is_error: false, duration_ms: 1234, duration_api_ms: 1200, num_turns: 1, result: "done", stop_reason: "end_turn", session_id: "sdk-session-result-usage", usage: { input_tokens: 4, cache_creation_input_tokens: 2715, cache_read_input_tokens: 21144, output_tokens: 679, }, modelUsage: { "claude-opus-4-6": { contextWindow: 200000, maxOutputTokens: 64000, }, }, } as unknown as SDKMessage); harness.query.finish(); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); const usageEvent = runtimeEvents.find((event) => event.type === "thread.token-usage.updated"); assert.equal(usageEvent?.type, "thread.token-usage.updated"); if (usageEvent?.type === "thread.token-usage.updated") { assert.deepEqual(usageEvent.payload, { usage: { usedTokens: 24542, lastUsedTokens: 24542, inputTokens: 23863, outputTokens: 679, maxTokens: 200000, }, }); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect( "emits completion only after turn result when assistant frames arrive before deltas", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "assistant", session_id: "sdk-session-early-assistant", uuid: "assistant-early", parent_tool_use_id: null, message: { id: "assistant-message-early", content: [ { type: "tool_use", id: "tool-early", name: "Read", input: { path: "a.ts" } }, ], }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-early-assistant", uuid: "stream-early", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Late text", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-early-assistant", uuid: "result-early", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.completed", "turn.completed", ], ); const deltaIndex = runtimeEvents.findIndex((event) => event.type === "content.delta"); const completedIndex = runtimeEvents.findIndex((event) => event.type === "item.completed"); assert.equal(deltaIndex >= 0 && completedIndex >= 0 && deltaIndex < completedIndex, true); const deltaEvent = runtimeEvents[deltaIndex]; assert.equal(deltaEvent?.type, "content.delta"); if (deltaEvent?.type === "content.delta") { assert.equal(deltaEvent.payload.delta, "Late text"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }, ); it.effect("creates a fresh assistant message when Claude reuses a text block index", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 9).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "stream_event", session_id: "sdk-session-reused-text-index", uuid: "stream-reused-start-1", parent_tool_use_id: null, event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-reused-text-index", uuid: "stream-reused-delta-1", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "First", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-reused-text-index", uuid: "stream-reused-stop-1", parent_tool_use_id: null, event: { type: "content_block_stop", index: 0, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-reused-text-index", uuid: "stream-reused-start-2", parent_tool_use_id: null, event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-reused-text-index", uuid: "stream-reused-delta-2", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Second", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-reused-text-index", uuid: "stream-reused-stop-2", parent_tool_use_id: null, event: { type: "content_block_stop", index: 0, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-reused-text-index", uuid: "result-reused-text-index", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.completed", "content.delta", "item.completed", ], ); const assistantDeltas = runtimeEvents.filter( (event) => event.type === "content.delta" && event.payload.streamKind === "assistant_text", ); assert.equal(assistantDeltas.length, 2); if (assistantDeltas.length !== 2) { return; } const [firstAssistantDelta, secondAssistantDelta] = assistantDeltas; assert.equal(firstAssistantDelta?.type, "content.delta"); assert.equal(secondAssistantDelta?.type, "content.delta"); if ( firstAssistantDelta?.type !== "content.delta" || secondAssistantDelta?.type !== "content.delta" ) { return; } assert.equal(firstAssistantDelta.payload.delta, "First"); assert.equal(secondAssistantDelta.payload.delta, "Second"); assert.notEqual(firstAssistantDelta.itemId, secondAssistantDelta.itemId); const assistantCompletions = runtimeEvents.filter( (event) => event.type === "item.completed" && event.payload.itemType === "assistant_message", ); assert.equal(assistantCompletions.length, 2); assert.equal(String(assistantCompletions[0]?.itemId), String(firstAssistantDelta.itemId)); assert.equal(String(assistantCompletions[1]?.itemId), String(secondAssistantDelta.itemId)); assert.notEqual( String(assistantCompletions[0]?.itemId), String(assistantCompletions[1]?.itemId), ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("falls back to assistant payload text when stream deltas are absent", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 8).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "assistant", session_id: "sdk-session-fallback-text", uuid: "assistant-fallback", parent_tool_use_id: null, message: { id: "assistant-message-fallback", content: [{ type: "text", text: "Fallback hello" }], }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-fallback-text", uuid: "result-fallback", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.completed", "turn.completed", ], ); const deltaEvent = runtimeEvents.find((event) => event.type === "content.delta"); assert.equal(deltaEvent?.type, "content.delta"); if (deltaEvent?.type === "content.delta") { assert.equal(deltaEvent.payload.delta, "Fallback hello"); assert.equal(String(deltaEvent.turnId), String(turn.turnId)); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("segments Claude assistant text blocks around tool calls", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 13).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-text-1-start", parent_tool_use_id: null, event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-text-1-delta", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "First message.", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-text-1-stop", parent_tool_use_id: null, event: { type: "content_block_stop", index: 0, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-tool-start", parent_tool_use_id: null, event: { type: "content_block_start", index: 1, content_block: { type: "tool_use", id: "tool-interleaved-1", name: "Grep", input: { pattern: "assistant", path: "src", }, }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-tool-stop", parent_tool_use_id: null, event: { type: "content_block_stop", index: 1, }, } as unknown as SDKMessage); harness.query.emit({ type: "user", session_id: "sdk-session-interleaved", uuid: "user-tool-result-interleaved", parent_tool_use_id: null, message: { role: "user", content: [ { type: "tool_result", tool_use_id: "tool-interleaved-1", content: "src/example.ts:1:assistant", }, ], }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-text-2-start", parent_tool_use_id: null, event: { type: "content_block_start", index: 2, content_block: { type: "text", text: "", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-text-2-delta", parent_tool_use_id: null, event: { type: "content_block_delta", index: 2, delta: { type: "text_delta", text: "Second message.", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "stream_event", session_id: "sdk-session-interleaved", uuid: "stream-text-2-stop", parent_tool_use_id: null, event: { type: "content_block_stop", index: 2, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-interleaved", uuid: "result-interleaved", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", "content.delta", "item.completed", "item.started", "item.updated", "item.completed", "content.delta", "item.completed", "turn.completed", ], ); const assistantTextDeltas = runtimeEvents.filter( (event) => event.type === "content.delta" && event.payload.streamKind === "assistant_text", ); assert.equal(assistantTextDeltas.length, 2); if (assistantTextDeltas.length !== 2) { return; } const [firstAssistantDelta, secondAssistantDelta] = assistantTextDeltas; if (!firstAssistantDelta || !secondAssistantDelta) { return; } assert.notEqual(String(firstAssistantDelta.itemId), String(secondAssistantDelta.itemId)); const firstAssistantCompletedIndex = runtimeEvents.findIndex( (event) => event.type === "item.completed" && event.payload.itemType === "assistant_message" && String(event.itemId) === String(firstAssistantDelta.itemId), ); const toolStartedIndex = runtimeEvents.findIndex((event) => event.type === "item.started"); const secondAssistantDeltaIndex = runtimeEvents.findIndex( (event) => event.type === "content.delta" && event.payload.streamKind === "assistant_text" && String(event.itemId) === String(secondAssistantDelta.itemId), ); assert.equal( firstAssistantCompletedIndex >= 0 && toolStartedIndex >= 0 && secondAssistantDeltaIndex >= 0 && firstAssistantCompletedIndex < toolStartedIndex && toolStartedIndex < secondAssistantDeltaIndex, true, ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("does not fabricate provider thread ids before first SDK session_id", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const runtimeEventsFiber = yield* Stream.take(adapter.streamEvents, 5).pipe( Stream.runCollect, Effect.forkChild, ); const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); assert.equal(session.threadId, THREAD_ID); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); assert.equal(turn.threadId, THREAD_ID); harness.query.emit({ type: "stream_event", session_id: "sdk-thread-real", uuid: "stream-thread-real", parent_tool_use_id: null, event: { type: "message_start", message: { id: "msg-thread-real", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-thread-real", uuid: "result-thread-real", } as unknown as SDKMessage); const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber)); assert.deepEqual( runtimeEvents.map((event) => event.type), [ "session.started", "session.configured", "session.state.changed", "turn.started", "thread.started", ], ); const sessionStarted = runtimeEvents[0]; assert.equal(sessionStarted?.type, "session.started"); if (sessionStarted?.type === "session.started") { assert.equal(sessionStarted.threadId, THREAD_ID); } const threadStarted = runtimeEvents[4]; assert.equal(threadStarted?.type, "thread.started"); if (threadStarted?.type === "thread.started") { assert.equal(threadStarted.threadId, THREAD_ID); assert.deepEqual(threadStarted.payload, { providerThreadId: "sdk-thread-real", }); } }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("bridges approval request/response lifecycle through canUseTool", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "approval-required", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); yield* adapter.sendTurn({ threadId: session.threadId, input: "approve this", attachments: [], }); yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); harness.query.emit({ type: "stream_event", session_id: "sdk-session-approval-1", uuid: "stream-approval-thread", parent_tool_use_id: null, event: { type: "message_start", message: { id: "msg-approval-thread", }, }, } as unknown as SDKMessage); const threadStarted = yield* Stream.runHead(adapter.streamEvents); assert.equal(threadStarted._tag, "Some"); if (threadStarted._tag !== "Some" || threadStarted.value.type !== "thread.started") { return; } const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } const permissionPromise = canUseTool( "Bash", { command: "pwd" }, { signal: new AbortController().signal, suggestions: [ { type: "setMode", mode: "default", destination: "session", }, ], toolUseID: "tool-use-1", }, ); const requested = yield* Stream.runHead(adapter.streamEvents); assert.equal(requested._tag, "Some"); if (requested._tag !== "Some") { return; } assert.equal(requested.value.type, "request.opened"); if (requested.value.type !== "request.opened") { return; } assert.deepEqual(requested.value.providerRefs, { providerItemId: ProviderItemId.makeUnsafe("tool-use-1"), }); const runtimeRequestId = requested.value.requestId; assert.equal(typeof runtimeRequestId, "string"); if (runtimeRequestId === undefined) { return; } yield* adapter.respondToRequest( session.threadId, ApprovalRequestId.makeUnsafe(runtimeRequestId), "accept", ); const resolved = yield* Stream.runHead(adapter.streamEvents); assert.equal(resolved._tag, "Some"); if (resolved._tag !== "Some") { return; } assert.equal(resolved.value.type, "request.resolved"); if (resolved.value.type !== "request.resolved") { return; } assert.equal(resolved.value.requestId, requested.value.requestId); assert.equal(resolved.value.payload.decision, "accept"); assert.deepEqual(resolved.value.providerRefs, { providerItemId: ProviderItemId.makeUnsafe("tool-use-1"), }); const permissionResult = yield* Effect.promise(() => permissionPromise); assert.equal((permissionResult as PermissionResult).behavior, "allow"); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("classifies Agent tools and read-only Claude tools correctly for approvals", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "approval-required", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } const agentPermissionPromise = canUseTool( "Agent", {}, { signal: new AbortController().signal, toolUseID: "tool-agent-1", }, ); const agentRequested = yield* Stream.runHead(adapter.streamEvents); assert.equal(agentRequested._tag, "Some"); if (agentRequested._tag !== "Some" || agentRequested.value.type !== "request.opened") { return; } assert.equal(agentRequested.value.payload.requestType, "dynamic_tool_call"); yield* adapter.respondToRequest( session.threadId, ApprovalRequestId.makeUnsafe(String(agentRequested.value.requestId)), "accept", ); yield* Stream.runHead(adapter.streamEvents); yield* Effect.promise(() => agentPermissionPromise); const grepPermissionPromise = canUseTool( "Grep", { pattern: "foo", path: "src" }, { signal: new AbortController().signal, toolUseID: "tool-grep-approval-1", }, ); const grepRequested = yield* Stream.runHead(adapter.streamEvents); assert.equal(grepRequested._tag, "Some"); if (grepRequested._tag !== "Some" || grepRequested.value.type !== "request.opened") { return; } assert.equal(grepRequested.value.payload.requestType, "file_read_approval"); yield* adapter.respondToRequest( session.threadId, ApprovalRequestId.makeUnsafe(String(grepRequested.value.requestId)), "accept", ); yield* Stream.runHead(adapter.streamEvents); yield* Effect.promise(() => grepPermissionPromise); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("passes Claude resume ids without pinning a stale assistant checkpoint", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: RESUME_THREAD_ID, provider: "claudeAgent", resumeCursor: { threadId: "resume-thread-1", resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, }, runtimeMode: "full-access", }); assert.equal(session.threadId, RESUME_THREAD_ID); assert.deepEqual(session.resumeCursor, { threadId: RESUME_THREAD_ID, resume: "550e8400-e29b-41d4-a716-446655440000", resumeSessionAt: "assistant-99", turnCount: 3, }); const createInput = harness.getLastCreateQueryInput(); assert.equal(createInput?.options.resume, "550e8400-e29b-41d4-a716-446655440000"); assert.equal(createInput?.options.sessionId, undefined); assert.equal(createInput?.options.resumeSessionAt, undefined); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("uses an app-generated Claude session id for fresh sessions", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const createInput = harness.getLastCreateQueryInput(); const sessionResumeCursor = session.resumeCursor as { threadId?: string; resume?: string; turnCount?: number; }; assert.equal(sessionResumeCursor.threadId, THREAD_ID); assert.equal(typeof sessionResumeCursor.resume, "string"); assert.equal(sessionResumeCursor.turnCount, 0); assert.match( sessionResumeCursor.resume ?? "", /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, ); assert.equal(createInput?.options.resume, undefined); assert.equal(createInput?.options.sessionId, sessionResumeCursor.resume); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect( "supports rollbackThread by trimming in-memory turns and preserving earlier turns", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const firstTurn = yield* adapter.sendTurn({ threadId: session.threadId, input: "first", attachments: [], }); const firstCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-rollback", uuid: "result-first", } as unknown as SDKMessage); const firstCompleted = yield* Fiber.join(firstCompletedFiber); assert.equal(firstCompleted._tag, "Some"); if (firstCompleted._tag === "Some" && firstCompleted.value.type === "turn.completed") { assert.equal(String(firstCompleted.value.turnId), String(firstTurn.turnId)); } const secondTurn = yield* adapter.sendTurn({ threadId: session.threadId, input: "second", attachments: [], }); const secondCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-rollback", uuid: "result-second", } as unknown as SDKMessage); const secondCompleted = yield* Fiber.join(secondCompletedFiber); assert.equal(secondCompleted._tag, "Some"); if (secondCompleted._tag === "Some" && secondCompleted.value.type === "turn.completed") { assert.equal(String(secondCompleted.value.turnId), String(secondTurn.turnId)); } const threadBeforeRollback = yield* adapter.readThread(session.threadId); assert.equal(threadBeforeRollback.turns.length, 2); const rolledBack = yield* adapter.rollbackThread(session.threadId, 1); assert.equal(rolledBack.turns.length, 1); assert.equal(rolledBack.turns[0]?.id, firstTurn.turnId); const threadAfterRollback = yield* adapter.readThread(session.threadId); assert.equal(threadAfterRollback.turns.length, 1); assert.equal(threadAfterRollback.turns[0]?.id, firstTurn.turnId); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }, ); it.effect("updates model on sendTurn when model override is provided", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", }, attachments: [], }); assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6"]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect( "does not re-set the Claude model when the session already uses the same effective API model", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const modelSelection = { provider: "claudeAgent" as const, model: "claude-opus-4-6", }; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", modelSelection, runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", modelSelection, attachments: [], }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello again", modelSelection, attachments: [], }); assert.deepEqual(harness.query.setModelCalls, []); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }, ); it.effect("re-sets the Claude model when the effective API model changes", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", options: { contextWindow: "1m", }, }, attachments: [], }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello again", modelSelection: { provider: "claudeAgent", model: "claude-opus-4-6", }, attachments: [], }); assert.deepEqual(harness.query.setModelCalls, ["claude-opus-4-6[1m]", "claude-opus-4-6"]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("sets plan permission mode on sendTurn when interactionMode is plan", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "plan this for me", interactionMode: "plan", attachments: [], }); assert.deepEqual(harness.query.setPermissionModeCalls, ["plan"]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect.each<{ runtimeMode: RuntimeMode; expectedBase: PermissionMode }>([ { runtimeMode: "full-access", expectedBase: "bypassPermissions" }, { runtimeMode: "approval-required", expectedBase: "default" }, { runtimeMode: "auto-accept-edits", expectedBase: "acceptEdits" }, ])( "restores $expectedBase permission mode after plan turn ($runtimeMode)", ({ runtimeMode, expectedBase }) => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode, }); // First turn in plan mode yield* adapter.sendTurn({ threadId: session.threadId, input: "plan this", interactionMode: "plan", attachments: [], }); // Complete the turn so we can send another const turnCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: `sdk-session-${runtimeMode}`, uuid: `result-${runtimeMode}`, } as unknown as SDKMessage); yield* Fiber.join(turnCompletedFiber); // Second turn back to default yield* adapter.sendTurn({ threadId: session.threadId, input: "now do it", interactionMode: "default", attachments: [], }); assert.deepEqual(harness.query.setPermissionModeCalls, ["plan", expectedBase]); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }, ); it.effect("does not call setPermissionMode when interactionMode is absent", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); assert.deepEqual(harness.query.setPermissionModeCalls, []); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("captures ExitPlanMode as a proposed plan and denies auto-exit", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); yield* adapter.sendTurn({ threadId: session.threadId, input: "plan this", interactionMode: "plan", attachments: [], }); yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } const permissionPromise = canUseTool( "ExitPlanMode", { plan: "# Ship it\n\n- one\n- two", allowedPrompts: [{ tool: "Bash", prompt: "run tests" }], }, { signal: new AbortController().signal, toolUseID: "tool-exit-1", }, ); const proposedEvent = yield* Stream.runHead(adapter.streamEvents); assert.equal(proposedEvent._tag, "Some"); if (proposedEvent._tag !== "Some") { return; } assert.equal(proposedEvent.value.type, "turn.proposed.completed"); if (proposedEvent.value.type !== "turn.proposed.completed") { return; } assert.equal(proposedEvent.value.payload.planMarkdown, "# Ship it\n\n- one\n- two"); assert.deepEqual(proposedEvent.value.providerRefs, { providerItemId: ProviderItemId.makeUnsafe("tool-exit-1"), }); const permissionResult = yield* Effect.promise(() => permissionPromise); assert.equal((permissionResult as PermissionResult).behavior, "deny"); const deniedResult = permissionResult as PermissionResult & { message?: string; }; assert.equal(deniedResult.message?.includes("captured your proposed plan"), true); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("extracts proposed plans from assistant ExitPlanMode snapshots", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); yield* adapter.sendTurn({ threadId: session.threadId, input: "plan this", interactionMode: "plan", attachments: [], }); yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); const proposedEventFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.proposed.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "assistant", session_id: "sdk-session-exit-plan", uuid: "assistant-exit-plan", parent_tool_use_id: null, message: { model: "claude-opus-4-6", id: "msg-exit-plan", type: "message", role: "assistant", content: [ { type: "tool_use", id: "tool-exit-2", name: "ExitPlanMode", input: { plan: "# Final plan\n\n- capture it", }, }, ], stop_reason: null, stop_sequence: null, usage: {}, }, } as unknown as SDKMessage); const proposedEvent = yield* Fiber.join(proposedEventFiber); assert.equal(proposedEvent._tag, "Some"); if (proposedEvent._tag !== "Some") { return; } assert.equal(proposedEvent.value.type, "turn.proposed.completed"); if (proposedEvent.value.type !== "turn.proposed.completed") { return; } assert.equal(proposedEvent.value.payload.planMarkdown, "# Final plan\n\n- capture it"); assert.deepEqual(proposedEvent.value.providerRefs, { providerItemId: ProviderItemId.makeUnsafe("tool-exit-2"), }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("handles AskUserQuestion via user-input.requested/resolved lifecycle", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; // Start session in approval-required mode so canUseTool fires. const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "approval-required", }); // Drain the session startup events (started, configured, state.changed). yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); yield* adapter.sendTurn({ threadId: session.threadId, input: "question turn", attachments: [], }); yield* Stream.take(adapter.streamEvents, 1).pipe(Stream.runDrain); harness.query.emit({ type: "stream_event", session_id: "sdk-session-user-input-1", uuid: "stream-user-input-thread", parent_tool_use_id: null, event: { type: "message_start", message: { id: "msg-user-input-thread", }, }, } as unknown as SDKMessage); const threadStarted = yield* Stream.runHead(adapter.streamEvents); assert.equal(threadStarted._tag, "Some"); if (threadStarted._tag !== "Some" || threadStarted.value.type !== "thread.started") { return; } const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } // Simulate Claude calling AskUserQuestion with structured questions. const askInput = { questions: [ { question: "Which framework?", header: "Framework", options: [ { label: "React", description: "React.js" }, { label: "Vue", description: "Vue.js" }, ], multiSelect: false, }, ], }; const permissionPromise = canUseTool("AskUserQuestion", askInput, { signal: new AbortController().signal, toolUseID: "tool-ask-1", }); // The adapter should emit a user-input.requested event. const requestedEvent = yield* Stream.runHead(adapter.streamEvents); assert.equal(requestedEvent._tag, "Some"); if (requestedEvent._tag !== "Some") { return; } assert.equal(requestedEvent.value.type, "user-input.requested"); if (requestedEvent.value.type !== "user-input.requested") { return; } const requestId = requestedEvent.value.requestId; assert.equal(typeof requestId, "string"); assert.equal(requestedEvent.value.payload.questions.length, 1); assert.equal(requestedEvent.value.payload.questions[0]?.question, "Which framework?"); assert.deepEqual(requestedEvent.value.providerRefs, { providerItemId: ProviderItemId.makeUnsafe("tool-ask-1"), }); // Respond with the user's answers. yield* adapter.respondToUserInput( session.threadId, ApprovalRequestId.makeUnsafe(requestId!), { "Which framework?": "React" }, ); // The adapter should emit a user-input.resolved event. const resolvedEvent = yield* Stream.runHead(adapter.streamEvents); assert.equal(resolvedEvent._tag, "Some"); if (resolvedEvent._tag !== "Some") { return; } assert.equal(resolvedEvent.value.type, "user-input.resolved"); if (resolvedEvent.value.type !== "user-input.resolved") { return; } assert.deepEqual(resolvedEvent.value.payload.answers, { "Which framework?": "React", }); assert.deepEqual(resolvedEvent.value.providerRefs, { providerItemId: ProviderItemId.makeUnsafe("tool-ask-1"), }); // The canUseTool promise should resolve with the answers in SDK format. const permissionResult = yield* Effect.promise(() => permissionPromise); assert.equal((permissionResult as PermissionResult).behavior, "allow"); const updatedInput = (permissionResult as { updatedInput: Record }) .updatedInput; assert.deepEqual(updatedInput.answers, { "Which framework?": "React" }); // Original questions should be passed through. assert.deepEqual(updatedInput.questions, askInput.questions); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("routes AskUserQuestion through user-input flow even in full-access mode", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; // In full-access mode, regular tools are auto-approved. // AskUserQuestion should still go through the user-input flow. const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } const askInput = { questions: [ { question: "Deploy to which env?", header: "Env", options: [ { label: "Staging", description: "Staging environment" }, { label: "Production", description: "Production environment" }, ], multiSelect: false, }, ], }; const permissionPromise = canUseTool("AskUserQuestion", askInput, { signal: new AbortController().signal, toolUseID: "tool-ask-2", }); // Should still get user-input.requested even in full-access mode. const requestedEvent = yield* Stream.runHead(adapter.streamEvents); assert.equal(requestedEvent._tag, "Some"); if (requestedEvent._tag !== "Some" || requestedEvent.value.type !== "user-input.requested") { assert.fail("Expected user-input.requested event"); return; } const requestId = requestedEvent.value.requestId; yield* adapter.respondToUserInput( session.threadId, ApprovalRequestId.makeUnsafe(requestId!), { "Deploy to which env?": "Staging" }, ); // Drain the resolved event. yield* Stream.runHead(adapter.streamEvents); const permissionResult = yield* Effect.promise(() => permissionPromise); assert.equal((permissionResult as PermissionResult).behavior, "allow"); const updatedInput = (permissionResult as { updatedInput: Record }) .updatedInput; assert.deepEqual(updatedInput.answers, { "Deploy to which env?": "Staging" }); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("denies AskUserQuestion when the waiting turn is aborted", () => { const harness = makeHarness(); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "approval-required", }); yield* Stream.take(adapter.streamEvents, 3).pipe(Stream.runDrain); const createInput = harness.getLastCreateQueryInput(); const canUseTool = createInput?.options.canUseTool; assert.equal(typeof canUseTool, "function"); if (!canUseTool) { return; } const controller = new AbortController(); const permissionPromise = canUseTool( "AskUserQuestion", { questions: [ { question: "Continue?", header: "Continue", options: [{ label: "Yes", description: "Proceed" }], multiSelect: false, }, ], }, { signal: controller.signal, toolUseID: "tool-ask-abort", }, ); const requestedEvent = yield* Stream.runHead(adapter.streamEvents); assert.equal(requestedEvent._tag, "Some"); if (requestedEvent._tag !== "Some" || requestedEvent.value.type !== "user-input.requested") { assert.fail("Expected user-input.requested event"); return; } assert.equal(requestedEvent.value.threadId, session.threadId); controller.abort(); const resolvedEvent = yield* Stream.runHead(adapter.streamEvents); assert.equal(resolvedEvent._tag, "Some"); if (resolvedEvent._tag !== "Some" || resolvedEvent.value.type !== "user-input.resolved") { assert.fail("Expected user-input.resolved event"); return; } assert.deepEqual(resolvedEvent.value.payload.answers, {}); const permissionResult = yield* Effect.promise(() => permissionPromise); assert.deepEqual(permissionResult, { behavior: "deny", message: "User cancelled tool execution.", } satisfies PermissionResult); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); it.effect("writes provider-native observability records when enabled", () => { const nativeEvents: Array<{ event?: { provider?: string; method?: string; threadId?: string; turnId?: string; }; }> = []; const nativeThreadIds: Array = []; const harness = makeHarness({ nativeEventLogger: { filePath: "memory://claude-native-events", write: (event, threadId) => { nativeEvents.push(event as (typeof nativeEvents)[number]); nativeThreadIds.push(threadId ?? null); return Effect.void; }, close: () => Effect.void, }, }); return Effect.gen(function* () { const adapter = yield* ClaudeAdapter; const session = yield* adapter.startSession({ threadId: THREAD_ID, provider: "claudeAgent", runtimeMode: "full-access", }); const turn = yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [], }); const turnCompletedFiber = yield* Stream.filter( adapter.streamEvents, (event) => event.type === "turn.completed", ).pipe(Stream.runHead, Effect.forkChild); harness.query.emit({ type: "stream_event", session_id: "sdk-session-native-log", uuid: "stream-native-log", parent_tool_use_id: null, event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "hi", }, }, } as unknown as SDKMessage); harness.query.emit({ type: "result", subtype: "success", is_error: false, errors: [], session_id: "sdk-session-native-log", uuid: "result-native-log", } as unknown as SDKMessage); const turnCompleted = yield* Fiber.join(turnCompletedFiber); assert.equal(turnCompleted._tag, "Some"); assert.equal(nativeEvents.length > 0, true); assert.equal( nativeEvents.some((record) => record.event?.provider === "claudeAgent"), true, ); assert.equal( nativeEvents.some( (record) => String( (record.event as { readonly providerThreadId?: string } | undefined) ?.providerThreadId, ) === "sdk-session-native-log", ), true, ); assert.equal( nativeEvents.some((record) => String(record.event?.turnId) === String(turn.turnId)), true, ); assert.equal( nativeEvents.some( (record) => record.event?.method === "claude/stream_event/content_block_delta/text_delta", ), true, ); assert.equal( nativeThreadIds.every((threadId) => threadId === String(THREAD_ID)), true, ); }).pipe( Effect.provideService(Random.Random, makeDeterministicRandomService()), Effect.provide(harness.layer), ); }); });