2575 lines · 84.3 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 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, Layer, Queue, Random, Ref, Stream } from "effect"; 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<number>;  nextSyntheticAssistantBlockIndex: number;} interface AssistantTextBlockState {  readonly itemId: string;  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;  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 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";  }} function buildUserMessage(input: ProviderSendTurnInput): SDKUserMessage {  const fragments: string[] = [];   if (input.input && input.input.trim().length > 0) {    fragments.push(input.input.trim());  }   for (const attachment of input.attachments ?? []) {    if (attachment.type === "image") {      fragments.push(        `Attached image: ${attachment.name} (${attachment.mimeType}, ${attachment.sizeBytes} bytes).`,      );    }  }   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;  const text = applyClaudePromptEffortPrefix(fragments.join("\n\n"), promptEffort);   return {    type: "user",    session_id: "",    parent_tool_use_id: null,    message: {      role: "user",      content: [{ type: "text", text }],    },  } as SDKUserMessage;} function turnStatusFromResult(result: SDKResultMessage): ProviderRuntimeTurnStatus {  if (result.subtype === "success") {    return "completed";  }   const errors = result.errors.join(" ").toLowerCase();  if (errors.includes("interrupt")) {    return "interrupted";  }  if (errors.includes("cancel")) {    return "cancelled";  }  return "failed";} function streamKindFromDeltaType(deltaType: string): ClaudeTextStreamKind {  return deltaType.includes("thinking") ? "reasoning_text" : "assistant_text";} function nativeProviderRefs(  _context: ClaudeSessionContext,  options?: {    readonly providerItemId?: string | undefined;  },): NonNullable<ProviderRuntimeEvent["providerRefs"]> {  if (options?.providerItemId) {    return {      providerItemId: ProviderItemId.makeUnsafe(options.providerItemId),    };  }  return {};} function extractAssistantTextBlocks(message: SDKMessage): Array<string> {  if (message.type !== "assistant") {    return [];  }   const content = (message.message as { content?: unknown } | undefined)?.content;  if (!Array.isArray(content)) {    return [];  }   const fragments: string[] = [];  for (const block of content) {    if (!block || typeof block !== "object") {      continue;    }    const candidate = block as { type?: unknown; text?: unknown };    if (      candidate.type === "text" &&      typeof candidate.text === "string" &&      candidate.text.length > 0    ) {      fragments.push(candidate.text);    }  }   return fragments;} function extractContentBlockText(block: unknown): string {  if (!block || typeof block !== "object") {    return "";  }   const candidate = block as { type?: unknown; text?: unknown };  return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";} function extractTextContent(value: unknown): string {  if (typeof value === "string") {    return value;  }   if (Array.isArray(value)) {    return value.map((entry) => extractTextContent(entry)).join("");  }   if (!value || typeof value !== "object") {    return "";  }   const record = value as {    text?: unknown;    content?: unknown;  };   if (typeof record.text === "string") {    return record.text;  }   return extractTextContent(record.content);} function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {  try {    const parsed = JSON.parse(value);    return parsed && typeof parsed === "object" && !Array.isArray(parsed)      ? (parsed as Record<string, unknown>)      : undefined;  } catch {    return undefined;  }} function toolInputFingerprint(input: Record<string, unknown>): string | undefined {  try {    return JSON.stringify(input);  } catch {    return undefined;  }} function toolResultStreamKind(itemType: CanonicalItemType): ClaudeToolResultStreamKind | undefined {  switch (itemType) {    case "command_execution":      return "command_output";    case "file_change":

Lines 1–500 of 2575.