2919 lines · 94.6 KB · TypeScript
RawDownload
/** * ClaudeAdapterLive - Scoped live implementation for the Claude Agent provider adapter. * * Wraps `@anthropic-ai/claude-agent-sdk` query sessions behind the generic * provider adapter contract and emits canonical runtime events. * * @module ClaudeAdapterLive */import {  type CanUseTool,  query,  type Options as ClaudeQueryOptions,  type PermissionMode,  type PermissionResult,  type PermissionUpdate,  type SDKMessage,  type SDKResultMessage,  type SettingSource,  type SDKUserMessage,} from "@anthropic-ai/claude-agent-sdk";import {  ApprovalRequestId,  type CanonicalItemType,  type CanonicalRequestType,  EventId,  type ProviderApprovalDecision,  ProviderItemId,  type ProviderRuntimeEvent,  type ProviderRuntimeTurnStatus,  type ProviderSendTurnInput,  type ProviderSession,  type ProviderUserInputAnswers,  type RuntimeContentStreamKind,  RuntimeItemId,  RuntimeRequestId,  RuntimeTaskId,  ThreadId,  TurnId,  type UserInputQuestion,} from "@t3tools/contracts";import {  applyClaudePromptEffortPrefix,  getEffectiveClaudeCodeEffort,  getReasoningEffortOptions,  resolveReasoningEffortForProvider,  supportsClaudeFastMode,  supportsClaudeThinkingToggle,  supportsClaudeUltrathinkKeyword,} from "@t3tools/shared/model";import {  Cause,  DateTime,  Deferred,  Effect,  Exit,  FileSystem,  Fiber,  Layer,  Queue,  Random,  Ref,  Stream,} from "effect"; import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import {  ProviderAdapterProcessError,  ProviderAdapterRequestError,  ProviderAdapterSessionClosedError,  ProviderAdapterSessionNotFoundError,  ProviderAdapterValidationError,  type ProviderAdapterError,} from "../Errors.ts";import { ClaudeAdapter, type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; const PROVIDER = "claudeAgent" as const;type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">;type ClaudeToolResultStreamKind = Extract<  RuntimeContentStreamKind,  "command_output" | "file_change_output">; type PromptQueueItem =  | {      readonly type: "message";      readonly message: SDKUserMessage;    }  | {      readonly type: "terminate";    }; interface ClaudeResumeState {  readonly threadId?: ThreadId;  readonly resume?: string;  readonly resumeSessionAt?: string;  readonly turnCount?: number;} interface ClaudeTurnState {  readonly turnId: TurnId;  readonly startedAt: string;  readonly items: Array<unknown>;  readonly assistantTextBlocks: Map<number, AssistantTextBlockState>;  readonly assistantTextBlockOrder: Array<AssistantTextBlockState>;  readonly capturedProposedPlanKeys: Set<string>;  nextSyntheticAssistantBlockIndex: number;} interface AssistantTextBlockState {  readonly itemId: string;  readonly blockIndex: number;  emittedTextDelta: boolean;  fallbackText: string;  streamClosed: boolean;  completionEmitted: boolean;} interface PendingApproval {  readonly requestType: CanonicalRequestType;  readonly detail?: string;  readonly suggestions?: ReadonlyArray<PermissionUpdate>;  readonly decision: Deferred.Deferred<ProviderApprovalDecision>;} interface PendingUserInput {  readonly questions: ReadonlyArray<UserInputQuestion>;  readonly answers: Deferred.Deferred<ProviderUserInputAnswers>;} interface ToolInFlight {  readonly itemId: string;  readonly itemType: CanonicalItemType;  readonly toolName: string;  readonly title: string;  readonly detail?: string;  readonly input: Record<string, unknown>;  readonly partialInputJson: string;  readonly lastEmittedInputFingerprint?: string;} interface ClaudeSessionContext {  session: ProviderSession;  readonly promptQueue: Queue.Queue<PromptQueueItem>;  readonly query: ClaudeQueryRuntime;  streamFiber: Fiber.Fiber<void, Error> | undefined;  readonly startedAt: string;  readonly basePermissionMode: PermissionMode | undefined;  resumeSessionId: string | undefined;  readonly pendingApprovals: Map<ApprovalRequestId, PendingApproval>;  readonly pendingUserInputs: Map<ApprovalRequestId, PendingUserInput>;  readonly turns: Array<{    id: TurnId;    items: Array<unknown>;  }>;  readonly inFlightTools: Map<number, ToolInFlight>;  turnState: ClaudeTurnState | undefined;  lastAssistantUuid: string | undefined;  lastThreadStartedId: string | undefined;  stopped: boolean;} interface ClaudeQueryRuntime extends AsyncIterable<SDKMessage> {  readonly interrupt: () => Promise<void>;  readonly setModel: (model?: string) => Promise<void>;  readonly setPermissionMode: (mode: PermissionMode) => Promise<void>;  readonly setMaxThinkingTokens: (maxThinkingTokens: number | null) => Promise<void>;  readonly close: () => void;} export interface ClaudeAdapterLiveOptions {  readonly createQuery?: (input: {    readonly prompt: AsyncIterable<SDKUserMessage>;    readonly options: ClaudeQueryOptions;  }) => ClaudeQueryRuntime;  readonly nativeEventLogPath?: string;  readonly nativeEventLogger?: EventNdjsonLogger;} function isUuid(value: string): boolean {  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);} function isSyntheticClaudeThreadId(value: string): boolean {  return value.startsWith("claude-thread-");} function toMessage(cause: unknown, fallback: string): string {  if (cause instanceof Error && cause.message.length > 0) {    return cause.message;  }  return fallback;} function toError(cause: unknown, fallback: string): Error {  return cause instanceof Error ? cause : new Error(toMessage(cause, fallback));} function normalizeClaudeStreamMessages(cause: Cause.Cause<Error>): ReadonlyArray<string> {  const errors = Cause.prettyErrors(cause)    .map((error) => error.message.trim())    .filter((message) => message.length > 0);  if (errors.length > 0) {    return errors;  }   const squashed = toMessage(Cause.squash(cause), "").trim();  return squashed.length > 0 ? [squashed] : [];} function isClaudeInterruptedMessage(message: string): boolean {  const normalized = message.toLowerCase();  return (    normalized.includes("all fibers interrupted without error") ||    normalized.includes("request was aborted") ||    normalized.includes("interrupted by user")  );} function isClaudeInterruptedCause(cause: Cause.Cause<Error>): boolean {  return (    Cause.hasInterruptsOnly(cause) ||    normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage)  );} function messageFromClaudeStreamCause(cause: Cause.Cause<Error>, fallback: string): string {  return normalizeClaudeStreamMessages(cause)[0] ?? fallback;} function interruptionMessageFromClaudeCause(cause: Cause.Cause<Error>): string {  const message = messageFromClaudeStreamCause(cause, "Claude runtime interrupted.");  return isClaudeInterruptedMessage(message) ? "Claude runtime interrupted." : message;} function resultErrorsText(result: SDKResultMessage): string {  return "errors" in result && Array.isArray(result.errors)    ? result.errors.join(" ").toLowerCase()    : "";} function isInterruptedResult(result: SDKResultMessage): boolean {  const errors = resultErrorsText(result);  if (errors.includes("interrupt")) {    return true;  }   return (    result.subtype === "error_during_execution" &&    result.is_error === false &&    (errors.includes("request was aborted") ||      errors.includes("interrupted by user") ||      errors.includes("aborted"))  );} function asRuntimeItemId(value: string): RuntimeItemId {  return RuntimeItemId.makeUnsafe(value);} function asCanonicalTurnId(value: TurnId): TurnId {  return value;} function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId {  return RuntimeRequestId.makeUnsafe(value);} function toPermissionMode(value: unknown): PermissionMode | undefined {  switch (value) {    case "default":    case "acceptEdits":    case "bypassPermissions":    case "plan":    case "dontAsk":      return value;    default:      return undefined;  }} function readClaudeResumeState(resumeCursor: unknown): ClaudeResumeState | undefined {  if (!resumeCursor || typeof resumeCursor !== "object") {    return undefined;  }  const cursor = resumeCursor as {    threadId?: unknown;    resume?: unknown;    sessionId?: unknown;    resumeSessionAt?: unknown;    turnCount?: unknown;  };   const threadIdCandidate = typeof cursor.threadId === "string" ? cursor.threadId : undefined;  const threadId =    threadIdCandidate && !isSyntheticClaudeThreadId(threadIdCandidate)      ? ThreadId.makeUnsafe(threadIdCandidate)      : undefined;  const resumeCandidate =    typeof cursor.resume === "string"      ? cursor.resume      : typeof cursor.sessionId === "string"        ? cursor.sessionId        : undefined;  const resume = resumeCandidate && isUuid(resumeCandidate) ? resumeCandidate : undefined;  const resumeSessionAt =    typeof cursor.resumeSessionAt === "string" ? cursor.resumeSessionAt : undefined;  const turnCountValue = typeof cursor.turnCount === "number" ? cursor.turnCount : undefined;   return {    ...(threadId ? { threadId } : {}),    ...(resume ? { resume } : {}),    ...(resumeSessionAt ? { resumeSessionAt } : {}),    ...(turnCountValue !== undefined && Number.isInteger(turnCountValue) && turnCountValue >= 0      ? { turnCount: turnCountValue }      : {}),  };} function classifyToolItemType(toolName: string): CanonicalItemType {  const normalized = toolName.toLowerCase();  if (normalized.includes("agent")) {    return "collab_agent_tool_call";  }  if (    normalized === "task" ||    normalized === "agent" ||    normalized.includes("subagent") ||    normalized.includes("sub-agent")  ) {    return "collab_agent_tool_call";  }  if (    normalized.includes("bash") ||    normalized.includes("command") ||    normalized.includes("shell") ||    normalized.includes("terminal")  ) {    return "command_execution";  }  if (    normalized.includes("edit") ||    normalized.includes("write") ||    normalized.includes("file") ||    normalized.includes("patch") ||    normalized.includes("replace") ||    normalized.includes("create") ||    normalized.includes("delete")  ) {    return "file_change";  }  if (normalized.includes("mcp")) {    return "mcp_tool_call";  }  if (normalized.includes("websearch") || normalized.includes("web search")) {    return "web_search";  }  if (normalized.includes("image")) {    return "image_view";  }  return "dynamic_tool_call";} function isReadOnlyToolName(toolName: string): boolean {  const normalized = toolName.toLowerCase();  return (    normalized === "read" ||    normalized.includes("read file") ||    normalized.includes("view") ||    normalized.includes("grep") ||    normalized.includes("glob") ||    normalized.includes("search")  );} function classifyRequestType(toolName: string): CanonicalRequestType {  if (isReadOnlyToolName(toolName)) {    return "file_read_approval";  }  const itemType = classifyToolItemType(toolName);  return itemType === "command_execution"    ? "command_execution_approval"    : itemType === "file_change"      ? "file_change_approval"      : "dynamic_tool_call";} function summarizeToolRequest(toolName: string, input: Record<string, unknown>): string {  const commandValue = input.command ?? input.cmd;  const command = typeof commandValue === "string" ? commandValue : undefined;  if (command && command.trim().length > 0) {    return `${toolName}: ${command.trim().slice(0, 400)}`;  }   const serialized = JSON.stringify(input);  if (serialized.length <= 400) {    return `${toolName}: ${serialized}`;  }  return `${toolName}: ${serialized.slice(0, 397)}...`;} function titleForTool(itemType: CanonicalItemType): string {  switch (itemType) {    case "command_execution":      return "Command run";    case "file_change":      return "File change";    case "mcp_tool_call":      return "MCP tool call";    case "collab_agent_tool_call":      return "Subagent task";    case "web_search":      return "Web search";    case "image_view":      return "Image view";    case "dynamic_tool_call":      return "Tool call";    default:      return "Item";  }} const SUPPORTED_CLAUDE_IMAGE_MIME_TYPES = new Set([  "image/gif",  "image/jpeg",  "image/png",  "image/webp",]);const CLAUDE_SETTING_SOURCES = [  "user",  "project",  "local",] as const satisfies ReadonlyArray<SettingSource>; function buildPromptText(input: ProviderSendTurnInput): string {  const requestedEffort = resolveReasoningEffortForProvider(    "claudeAgent",    input.modelOptions?.claudeAgent?.effort ?? null,  );  const supportedEffortOptions = getReasoningEffortOptions("claudeAgent", input.model);  const promptEffort =    requestedEffort === "ultrathink" && supportsClaudeUltrathinkKeyword(input.model)      ? "ultrathink"      : requestedEffort && supportedEffortOptions.includes(requestedEffort)        ? requestedEffort        : null;  return applyClaudePromptEffortPrefix(input.input?.trim() ?? "", promptEffort);} function buildUserMessage(input: {  readonly sdkContent: Array<Record<string, unknown>>;}): SDKUserMessage {  return {    type: "user",    session_id: "",    parent_tool_use_id: null,    message: {      role: "user",      content: input.sdkContent,    },  } as SDKUserMessage;} function buildClaudeImageContentBlock(input: {  readonly mimeType: string;  readonly bytes: Uint8Array;}): Record<string, unknown> {  return {    type: "image",    source: {      type: "base64",      media_type: input.mimeType,      data: Buffer.from(input.bytes).toString("base64"),    },  };} function buildUserMessageEffect(  input: ProviderSendTurnInput,  dependencies: {    readonly fileSystem: FileSystem.FileSystem;    readonly attachmentsDir: string;  },): Effect.Effect<SDKUserMessage, ProviderAdapterRequestError> {  return Effect.gen(function* () {    const text = buildPromptText(input);    const sdkContent: Array<Record<string, unknown>> = [];     if (text.length > 0) {      sdkContent.push({ type: "text", text });    }     for (const attachment of input.attachments ?? []) {      if (attachment.type !== "image") {        continue;      }       if (!SUPPORTED_CLAUDE_IMAGE_MIME_TYPES.has(attachment.mimeType)) {        return yield* new ProviderAdapterRequestError({

Lines 1–500 of 2919.