3070 lines · 95.0 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 ThreadTokenUsageSnapshot,  type ProviderUserInputAnswers,  type RuntimeContentStreamKind,  RuntimeItemId,  RuntimeRequestId,  RuntimeTaskId,  ThreadId,  TurnId,  type UserInputQuestion,  ClaudeCodeEffort,} from "@t3tools/contracts";import {  applyClaudePromptEffortPrefix,  resolveApiModelId,  resolveEffort,  trimOrNull,} 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 { ServerSettingsService } from "../../serverSettings.ts";import { getClaudeModelCapabilities } from "./ClaudeProvider.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;  currentApiModelId: string | 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;  lastKnownContextWindow: number | undefined;  lastKnownTokenUsage: ThreadTokenUsageSnapshot | 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 getEffectiveClaudeCodeEffort(  effort: ClaudeCodeEffort | null | undefined,): Exclude<ClaudeCodeEffort, "ultrathink"> | null {  if (!effort) {    return null;  }  return effort === "ultrathink" ? null : effort;} 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 maxClaudeContextWindowFromModelUsage(modelUsage: unknown): number | undefined {  if (!modelUsage || typeof modelUsage !== "object") {    return undefined;  }   let maxContextWindow: number | undefined;  for (const value of Object.values(modelUsage as Record<string, unknown>)) {    if (!value || typeof value !== "object") {      continue;    }    const contextWindow = (value as { contextWindow?: unknown }).contextWindow;    if (      typeof contextWindow !== "number" ||      !Number.isFinite(contextWindow) ||      contextWindow <= 0    ) {      continue;    }    maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow);  }   return maxContextWindow;} function normalizeClaudeTokenUsage(  usage: unknown,  contextWindow?: number,): ThreadTokenUsageSnapshot | undefined {  if (!usage || typeof usage !== "object") {    return undefined;  }   const record = usage as Record<string, unknown>;  const directUsedTokens =    typeof record.total_tokens === "number" && Number.isFinite(record.total_tokens)      ? record.total_tokens      : undefined;  const inputTokens =    (typeof record.input_tokens === "number" && Number.isFinite(record.input_tokens)      ? record.input_tokens      : 0) +    (typeof record.cache_creation_input_tokens === "number" &&    Number.isFinite(record.cache_creation_input_tokens)      ? record.cache_creation_input_tokens      : 0) +    (typeof record.cache_read_input_tokens === "number" &&    Number.isFinite(record.cache_read_input_tokens)      ? record.cache_read_input_tokens      : 0);  const outputTokens =    typeof record.output_tokens === "number" && Number.isFinite(record.output_tokens)      ? record.output_tokens      : 0;  const derivedUsedTokens = inputTokens + outputTokens;  const usedTokens = directUsedTokens ?? (derivedUsedTokens > 0 ? derivedUsedTokens : undefined);  if (usedTokens === undefined || usedTokens <= 0) {    return undefined;  }   return {    usedTokens,    lastUsedTokens: usedTokens,    ...(inputTokens > 0 ? { inputTokens } : {}),    ...(outputTokens > 0 ? { outputTokens } : {}),    ...(typeof contextWindow === "number" && Number.isFinite(contextWindow) && contextWindow > 0      ? { maxTokens: contextWindow }      : {}),    ...(typeof record.tool_uses === "number" && Number.isFinite(record.tool_uses)      ? { toolUses: record.tool_uses }      : {}),    ...(typeof record.duration_ms === "number" && Number.isFinite(record.duration_ms)      ? { durationMs: record.duration_ms }      : {}),  };} function asCanonicalTurnId(value: TurnId): TurnId {  return value;} function asRuntimeRequestId(value: ApprovalRequestId): RuntimeRequestId {  return RuntimeRequestId.makeUnsafe(value);} 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([

Lines 1–500 of 3070.