3876 lines · 121.5 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 SDKControlGetContextUsageResponse,  type SDKResultMessage,  type SettingSource,  type SDKUserMessage,  type ModelUsage,} from "@anthropic-ai/claude-agent-sdk";import { parseCliArgs } from "@t3tools/shared/cliArgs";import {  ApprovalRequestId,  type CanonicalItemType,  type CanonicalRequestType,  type ClaudeSettings,  EventId,  type ProviderApprovalDecision,  ProviderDriverKind,  ProviderInstanceId,  type ModelSelection,  ProviderItemId,  type ProviderRuntimeEvent,  type ProviderRuntimeTurnStatus,  type ProviderSendTurnInput,  type ProviderSession,  type ThreadTokenUsageSnapshot,  type ProviderUserInputAnswers,  type RuntimeContentStreamKind,  RuntimeItemId,  RuntimeRequestId,  RuntimeTaskId,  ThreadId,  TurnId,  type UserInputQuestion,} from "@t3tools/contracts";import {  applyClaudePromptEffortPrefix,  getModelSelectionBooleanOptionValue,  getModelSelectionStringOptionValue,  getProviderOptionDescriptors,  resolvePromptInjectedEffort,} from "@t3tools/shared/model";import * as Cause from "effect/Cause";import * as Crypto from "effect/Crypto";import * as DateTime from "effect/DateTime";import * as Deferred from "effect/Deferred";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Fiber from "effect/Fiber";import * as Path from "effect/Path";import * as Queue from "effect/Queue";import * as Ref from "effect/Ref";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream"; import { resolveAttachmentPath } from "../../attachmentStore.ts";import { ServerConfig } from "../../config.ts";import * as McpProviderSession from "../../mcp/McpProviderSession.ts";import { makeClaudeEnvironment } from "../Drivers/ClaudeHome.ts";import {  getClaudeModelCapabilities,  isClaudeUltracodeEffort,  normalizeClaudeCliEffort,  resolveClaudeApiModelId,  resolveClaudeEffort,} from "./ClaudeProvider.ts";import {  ProviderAdapterProcessError,  ProviderAdapterRequestError,  ProviderAdapterSessionClosedError,  ProviderAdapterSessionNotFoundError,  ProviderAdapterValidationError,  type ProviderAdapterError,} from "../Errors.ts";import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.UnknownFromJsonString);const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.UnknownFromJsonString); const PROVIDER = ProviderDriverKind.make("claudeAgent");type ClaudeTextStreamKind = Extract<RuntimeContentStreamKind, "assistant_text" | "reasoning_text">;type ClaudeToolResultStreamKind = Extract<  RuntimeContentStreamKind,  "command_output" | "file_change_output">;type ClaudeSdkEffort = NonNullable<ClaudeQueryOptions["effort"]>; function encodeJsonStringForDiagnostics(input: unknown): string | undefined {  const result = encodeUnknownJsonStringExit(input);  return Exit.isSuccess(result) ? result.value : undefined;} 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;  /**   * True for turns auto-started by assistant output arriving without an   * active turn (background agent/subagent responses between user prompts).   * Synthetic turns are auto-closed by the next sendTurn; real turns are   * steered instead (the queued message continues the same turn).   */  readonly synthetic?: boolean;  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 ClaudeTaskState {  readonly id: string;  subject: string;  status: PlanStep["status"];  readonly blockedBy: Set<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>;  readonly claudeTasks: Map<string, ClaudeTaskState>;  turnState: ClaudeTurnState | undefined;  lastKnownContextWindow: number | undefined;  lastKnownTokenUsage: ThreadTokenUsageSnapshot | undefined;  lastKnownTotalProcessedTokens: number | 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 getContextUsage?: () => Promise<SDKControlGetContextUsageResponse>;  readonly close: () => void;} export interface ClaudeAdapterLiveOptions {  readonly instanceId?: ProviderInstanceId;  readonly environment?: NodeJS.ProcessEnv;  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 hasDurableClaudeSessionId(message: SDKMessage): boolean {  if (message.type !== "system") {    return true;  }   return (    message.subtype !== "hook_started" &&    message.subtype !== "hook_progress" &&    message.subtype !== "hook_response"  );} function toMessage(cause: unknown, fallback: string): string {  if (cause instanceof Error && cause.message.length > 0) {    return cause.message;  }  return fallback;} function toProcessError(  cause: unknown,  fallback: string,  threadId: ThreadId,): ProviderAdapterProcessError {  return new ProviderAdapterProcessError({    provider: PROVIDER,    threadId,    detail: toMessage(cause, fallback),    cause,  });} function normalizeClaudeStreamMessages(  cause: Cause.Cause<{ readonly message: string }>,): ReadonlyArray<string> {  const errors: Array<string> = [];  for (const error of Cause.prettyErrors(cause)) {    const message = error.message.trim();    if (message.length > 0) {      errors.push(message);    }  }  if (errors.length > 0) {    return errors;  }   const squashed = toMessage(Cause.squash(cause), "").trim();  return squashed.length > 0 ? [squashed] : [];} function getEffectiveClaudeAgentEffort(  effort: string | null | undefined,  model: string | null | undefined,): ClaudeSdkEffort | null {  const normalized = normalizeClaudeCliEffort(effort, model);  return normalized ? (normalized as ClaudeSdkEffort) : null;} 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<{ readonly message: string }>): boolean {  return (    Cause.hasInterruptsOnly(cause) ||    normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage)  );} function messageFromClaudeStreamCause(  cause: Cause.Cause<{ readonly message: string }>,  fallback: string,): string {  return normalizeClaudeStreamMessages(cause)[0] ?? fallback;} function interruptionMessageFromClaudeCause(  cause: Cause.Cause<{ readonly message: string }>,): 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.make(value);} function maxClaudeContextWindowFromModelUsage(  modelUsage: Record<string, ModelUsage> | undefined,): number | undefined {  if (!modelUsage) return undefined;   let maxContextWindow: number | undefined;  for (const value of Object.values(modelUsage)) {    const contextWindow = value.contextWindow;    maxContextWindow = Math.max(maxContextWindow ?? 0, contextWindow);  }   return maxContextWindow;} function selectedClaudeContextWindow(  modelSelection: ModelSelection | undefined,): number | undefined {  switch (modelSelection?.model) {    case "claude-opus-4-8":    case "claude-opus-4-7":      return 1_000_000;  }   const optionValue = getModelSelectionStringOptionValue(modelSelection, "contextWindow");  if (optionValue === "1m") {    return 1_000_000;  }  if (optionValue === "200k") {    return 200_000;  }  const caps = getClaudeModelCapabilities(modelSelection?.model);  const hasContextWindowOption = getProviderOptionDescriptors({ caps }).some(    (descriptor) => descriptor.type === "select" && descriptor.id === "contextWindow",  );  if (hasContextWindowOption) {    return 200_000;  }  return undefined;} function finiteNonNegativeInteger(value: unknown): number | undefined {  return typeof value === "number" && Number.isFinite(value) && value >= 0    ? Math.round(value)    : undefined;} function finitePositiveInteger(value: unknown): number | undefined {  return typeof value === "number" && Number.isFinite(value) && value > 0    ? Math.round(value)    : undefined;} function claudeUsageInputTokens(usage: Record<string, unknown>): number {  return (    (finiteNonNegativeInteger(usage.input_tokens) ?? 0) +    (finiteNonNegativeInteger(usage.cache_creation_input_tokens) ?? 0) +    (finiteNonNegativeInteger(usage.cache_read_input_tokens) ?? 0)  );} function claudeUsageOutputTokens(usage: Record<string, unknown>): number {  return finiteNonNegativeInteger(usage.output_tokens) ?? 0;} function lastClaudeUsageIteration(  value: Record<string, unknown>,): Record<string, unknown> | undefined {  const iterations = Array.isArray(value.iterations) ? value.iterations : [];  return iterations.findLast(    (iteration): iteration is Record<string, unknown> =>      iteration !== null && typeof iteration === "object" && !Array.isArray(iteration),  );} function claudeTotalProcessedTokens(value: unknown): number | undefined {  if (!value || typeof value !== "object" || Array.isArray(value)) {    return undefined;  }   const usage = value as Record<string, unknown>;  const explicitTotal = finiteNonNegativeInteger(usage.total_tokens);  if (explicitTotal !== undefined && explicitTotal > 0) {    return explicitTotal;  }   const total = claudeUsageInputTokens(usage) + claudeUsageOutputTokens(usage);  return total > 0 ? total : undefined;} function makeClaudeTokenUsageSnapshot(input: {  readonly activeTokens: number;  readonly inputTokens?: number;  readonly outputTokens?: number;  readonly contextWindow?: number;  readonly totalProcessedTokens?: number;  readonly lastUsedTokens?: number;  readonly compactsAutomatically?: boolean;}): ThreadTokenUsageSnapshot | undefined {  const activeTokens = finiteNonNegativeInteger(input.activeTokens);  if (activeTokens === undefined || activeTokens <= 0) {    return undefined;  }   const maxTokens = finitePositiveInteger(input.contextWindow);  const usedTokens = maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens;  const lastUsedTokens =    finiteNonNegativeInteger(input.lastUsedTokens) ??    (maxTokens !== undefined ? Math.min(activeTokens, maxTokens) : activeTokens);  const totalProcessedTokens = finiteNonNegativeInteger(input.totalProcessedTokens);  const inputTokens = finiteNonNegativeInteger(input.inputTokens);  const outputTokens = finiteNonNegativeInteger(input.outputTokens);   return {    usedTokens,    lastUsedTokens,    ...(totalProcessedTokens !== undefined && totalProcessedTokens > usedTokens      ? { totalProcessedTokens }      : {}),    ...(inputTokens !== undefined && inputTokens > 0 ? { inputTokens } : {}),    ...(outputTokens !== undefined && outputTokens > 0 ? { outputTokens } : {}),    ...(maxTokens !== undefined ? { maxTokens } : {}),    ...(input.compactsAutomatically !== undefined      ? { compactsAutomatically: input.compactsAutomatically }      : {}),  };} function normalizeClaudeActiveTokenUsage(  value: unknown,  contextWindow?: number,  totalProcessedTokens?: number,): ThreadTokenUsageSnapshot | undefined {  if (!value || typeof value !== "object") {    return undefined;  }   const usage = value as Record<string, unknown>;  const activeUsage = lastClaudeUsageIteration(usage) ?? usage;  const inputTokens = claudeUsageInputTokens(activeUsage);  const outputTokens = claudeUsageOutputTokens(activeUsage);  const activeTokens = claudeTotalProcessedTokens(activeUsage) ?? inputTokens + outputTokens;  if (activeTokens <= 0) {    return undefined;  }   return makeClaudeTokenUsageSnapshot({    activeTokens,    inputTokens,    outputTokens,    ...(contextWindow !== undefined ? { contextWindow } : {}),    ...(totalProcessedTokens !== undefined ? { totalProcessedTokens } : {}),  });} function normalizeClaudeContextUsageApiSnapshot(

Lines 1–500 of 3876.