import { createHash } from "node:crypto";
import {
  APICallError,
  generateText,
  streamText,
  tool as aiSdkTool,
  jsonSchema as aiSdkJsonSchema,
  type LanguageModelUsage,
} from "ai";
import {
  BestAgentError,
  isRecord,
  normalizeWhitespace,
  truncateText,
} from "@best-agent/foundation";
import {
  PLAN_TODO_KINDS,
  type PhaseResultCandidateVerdict,
  type PhaseResultPhaseName,
  type PlanStatus,
  type PlanTodoKind,
  type PlanTodoStatus,
  type ProviderConfig,
  type ProviderDecisionTransport,
  type ProviderKind,
  type ProviderTurnActionPolicy,
  type ToolEffect,
  type ToolInputValue,
} from "@best-agent/protocol";

type AiSdkGenerateTextInput = Parameters<typeof generateText>[0];
type AiSdkModel = AiSdkGenerateTextInput["model"];
type AiSdkToolSet = NonNullable<AiSdkGenerateTextInput["tools"]>;
type AiSdkToolValue = AiSdkToolSet[string];
type AiSdkToolChoice = AiSdkGenerateTextInput["toolChoice"];
type AiSdkProviderOptions = NonNullable<AiSdkGenerateTextInput["providerOptions"]>;
type OpenAiProviderOptions = NonNullable<AiSdkProviderOptions["openai"]>;
type OpenAiCompatibleProviderOptions = {
  reasoningEffort?: ProviderConfig["reasoningEffort"];
};
type BestAgentProviderOptions = AiSdkProviderOptions & {
  openaiCompatible?: OpenAiCompatibleProviderOptions;
};
type AiSdkGenerateTextResult = Awaited<ReturnType<typeof generateText>>;
export type ToolUsePolicy =
  | "answer-when-sufficient"
  // TODO: migrate persisted checkpoints off this legacy policy and remove the compatibility case.
  | "tool-first-implementation"
  | "required-tool-only";
import type { ProviderCapabilities } from "./provider-metadata.js";
import type { ConversationMessage } from "./provider-conversation.js";
import {
  isOpenAiCompatibleProviderKind,
  resolveOpenAiCompatibleMode,
} from "./provider-metadata.js";
import { deriveControlsForActionPolicy } from "./provider-action-controls.js";
import { mapConversationMessagesToAiSdk } from "./provider-message-mapping.js";
import {
  DELEGATE_TASKS_TOOL_NAME,
  PHASE_RESULT_TOOL_NAME,
  PLAN_UPDATE_TOOL_NAME,
  createProviderToolNameCodec,
  prepareProviderToolNameBindings,
  toRuntimeToolName,
  type ProviderToolName,
  type ProviderToolNameBinding,
  type RuntimeToolName,
} from "./provider-tool-names.js";
import { getToolInputJsonSchema } from "../tool/schema.js";
import { containsControlPayloadFinal } from "../shared/control-payload-classifier.js";

export type { ConversationMessage } from "./provider-conversation.js";
export { resolveOpenAiCompatibleMode } from "./provider-metadata.js";
export {
  DELEGATE_TASKS_TOOL_NAME,
  PHASE_RESULT_TOOL_NAME,
  PLAN_UPDATE_TOOL_NAME,
  isReservedProviderToolName,
  prepareProviderToolNameBindings,
  toRuntimeToolName,
  toProviderToolName,
  type ProviderToolName,
  type ProviderToolNameBinding,
  type RuntimeToolName,
} from "./provider-tool-names.js";

export interface ProviderGenerateInput {
  cacheControl?: ProviderCacheControl;
  messages: ConversationMessage[];
  stream?: ProviderTextStreamCallbacks;
  systemPrompt?: string;
  toolNameBindings?: readonly ProviderToolNameBinding[];
}

export interface ProviderTextStreamCallbacks {
  noActionTimeoutMs?: number;
  onReasoningDelta?: (event: { delta: string; streamId: string }) => Promise<void> | void;
  onReasoningEnd?: (event: { streamId: string }) => Promise<void> | void;
  onReasoningStart?: (event: { streamId: string }) => Promise<void> | void;
  onTextDelta?: (event: { delta: string; streamId: string }) => Promise<void> | void;
  onToolCallDelta?: (event: {
    inputTextDelta: string;
    toolCallId: string;
    toolName: string;
  }) => Promise<void> | void;
  onToolCallStarted?: (event: { toolCallId: string; toolName: string }) => Promise<void> | void;
}

export interface ProviderGenerateOutput {
  model: string;
  provider: ProviderKind;
  reasoningText?: string;
  text: string;
  usage?: ProviderUsageStats;
}

export interface ProviderWebSearchInput {
  domains?: string[];
  limit: number;
  query: string;
  recencyDays?: number;
}

export interface ProviderToolOption {
  description: string;
  effect: ToolEffect;
  inputSchema?: unknown;
  name: string;
}

export interface FunctionCallingToolOption extends ProviderToolOption {
  decodeFunctionArguments(input: unknown): ToolInputValue;
  functionCallingParameters: Record<string, unknown>;
  name: ProviderToolName;
  providerName: ProviderToolName;
  runtimeName: RuntimeToolName;
  strictFunctionCalling: boolean;
}

export interface ProviderCacheControl {
  stablePrefixText?: string;
  stablePrefixTokens?: number;
}

interface ResolveProviderOptionsInput {
  cacheControl?: ProviderCacheControl;
  includeReasoningSummary?: boolean;
  systemPrompt?: string;
}

export interface ProviderDecideInput extends ProviderGenerateInput {
  actionPolicy: ProviderTurnActionPolicy;
  decisionTransport: ProviderDecisionTransport;
  allowPhaseResult?: boolean;
  allowPlanUpdate?: boolean;
  phaseResultPhaseName?: PhaseResultPhaseName;
  toolUsePolicy?: ToolUsePolicy;
  tools: ProviderToolOption[];
}

export interface ProviderRespondDecision {
  kind: "respond";
  model: string;
  provider: ProviderKind;
  text: string;
  usage?: ProviderUsageStats;
}

export interface ProviderSelectedToolCall {
  input: ToolInputValue;
  providerToolCallId?: string;
  providerToolName?: string;
  toolName: string;
}

type ProviderToolDecisionBase = {
  kind: "tool";
  model: string;
  provider: ProviderKind;
  reasoning?: string;
  usage?: ProviderUsageStats;
};

type ProviderLegacyToolDecisionShape = {
  input: ToolInputValue;
  providerToolCallId?: string;
  toolCalls?: never;
  toolName: string;
};

type ProviderMultiToolDecisionShape = {
  input?: never;
  providerToolCallId?: never;
  toolCalls: ProviderSelectedToolCall[];
  toolName?: never;
};

export type ProviderToolDecision = ProviderToolDecisionBase &
  (ProviderLegacyToolDecisionShape | ProviderMultiToolDecisionShape);

function hasProviderToolCalls(
  decision: ProviderToolDecision,
): decision is ProviderToolDecisionBase & ProviderMultiToolDecisionShape {
  return "toolCalls" in decision && Array.isArray(decision.toolCalls);
}

export interface ProviderPlanUpdateTodo {
  content: string;
  kind?: PlanTodoKind;
  status?: PlanTodoStatus;
}

export interface ProviderPlanUpdatePayload {
  acceptanceCriteria: string[];
  affectedPaths: string[];
  evidenceRefs: string[];
  openQuestions: string[];
  risks: string[];
  status: Extract<PlanStatus, "draft" | "review_ready">;
  summary: string;
  title: string;
  todos: ProviderPlanUpdateTodo[];
}

export interface ProviderPlanUpdateDecision {
  kind: "plan_update";
  model: string;
  plan: ProviderPlanUpdatePayload;
  provider: ProviderKind;
  usage?: ProviderUsageStats;
}

export interface ProviderPhaseResultPayload {
  candidateVerdict?: PhaseResultCandidateVerdict;
  phaseName: PhaseResultPhaseName;
  summary: string;
}

export interface ProviderPhaseResultDecision {
  kind: "phase_result";
  model: string;
  phaseResult: ProviderPhaseResultPayload;
  provider: ProviderKind;
  usage?: ProviderUsageStats;
}

export type ProviderRepairableDecisionReasonCode =
  | "invalid-structured-control-combination"
  | "invalid-structured-control-input"
  | "invalid-native-tool-input"
  | "invalid-native-tool-selection"
  | "structured-control-not-allowed"
  | "textual-control-payload"
  | "leaked-control-payload";

export interface ProviderRepairableInvalidDecision {
  kind: "repairable_invalid";
  message: string;
  model: string;
  provider: ProviderKind;
  rawText?: string;
  reasonCode: ProviderRepairableDecisionReasonCode;
  selectedToolNames?: string[];
  usage?: ProviderUsageStats;
}

export type ProviderDecisionOutput =
  | ProviderPhaseResultDecision
  | ProviderPlanUpdateDecision
  | ProviderRepairableInvalidDecision
  | ProviderRespondDecision
  | ProviderToolDecision;

export interface ProviderDecisionParseOptions {
  actionPolicy?: ProviderTurnActionPolicy;
  allowPhaseResult?: boolean;
  allowPlanUpdate?: boolean;
  phaseResultPhaseName?: PhaseResultPhaseName;
  toolUsePolicy?: ToolUsePolicy;
}

export interface ProviderUsageStats {
  cacheReadTokens: number;
  cacheWriteTokens: number;
  inputTokens: number;
  outputTokens: number;
  reasoningTokens: number;
  totalTokens: number;
}

export interface ProviderPromptCacheDiagnostics {
  eligible: boolean;
  providerHintApplied: boolean;
}

export interface ModelProvider {
  readonly capabilities?: ProviderCapabilities;
  readonly config: ProviderConfig;
  decide(input: ProviderDecideInput): Promise<ProviderDecisionOutput>;
  generate(input: ProviderGenerateInput): Promise<ProviderGenerateOutput>;
  searchWeb?(input: ProviderWebSearchInput): Promise<string>;
}

export type ProviderOptions = AiSdkProviderOptions;

export interface ProviderTextResult {
  reasoningText?: string;
  sources?: unknown[];
  text: string;
  toolCalls?: Array<{ args: unknown; toolCallId: string; toolName: string }>;
  totalUsage?: LanguageModelUsage;
}

const FUNCTION_CALL_ARGUMENT_KEY = "value";
const FUNCTION_CALL_SCHEMA_TYPE_KEYS: Readonly<Record<string, readonly string[]>> = {
  array: ["items", "maxItems", "minItems", "prefixItems", "uniqueItems"],
  boolean: [],
  integer: ["exclusiveMaximum", "exclusiveMinimum", "maximum", "minimum", "multipleOf"],
  null: [],
  number: ["exclusiveMaximum", "exclusiveMinimum", "maximum", "minimum", "multipleOf"],
  object: [
    "additionalProperties",
    "maxProperties",
    "minProperties",
    "patternProperties",
    "properties",
    "required",
  ],
  string: ["contentEncoding", "contentMediaType", "format", "maxLength", "minLength", "pattern"],
};

const DEFAULT_PROVIDER_TIMEOUT_MS = 300_000;
export const MAX_RETRYABLE_PROVIDER_ATTEMPTS = 3;
const BASE_PROVIDER_RETRY_DELAY_MS = 1_000;
const MAX_PROVIDER_RETRY_DELAY_MS = 3_000;
const REASONING_DELTA_FLUSH_THRESHOLD_CHARS = 2_048;
const TOOL_CALL_DELTA_FLUSH_THRESHOLD_CHARS = 2_048;
export function resolveProviderTimeoutMs(config: ProviderConfig): number {
  return typeof config.timeoutMs === "number" &&
    Number.isFinite(config.timeoutMs) &&
    config.timeoutMs > 0
    ? config.timeoutMs
    : DEFAULT_PROVIDER_TIMEOUT_MS;
}

export function isProviderNoActionError(error: unknown): boolean {
  return (
    (error instanceof BestAgentError && error.code === "PROVIDER_NO_ACTION") ||
    (isRecord(error) &&
      (error.code === "PROVIDER_NO_ACTION" ||
        (error.code === "PROVIDER_REQUEST_FAILED" &&
          typeof error.message === "string" &&
          isProviderEmptyOutputMessage(error.message))))
  );
}

export function isProviderContextOverflowError(error: unknown): boolean {
  const message =
    error instanceof Error
      ? error.message
      : isRecord(error) && typeof error.message === "string"
        ? error.message
        : "";
  const normalized = message.toLowerCase();
  return (
    normalized.includes("context_length_exceeded") ||
    normalized.includes("context length exceeded") ||
    normalized.includes("maximum context length") ||
    (normalized.includes("context window") && normalized.includes("exceed")) ||
    normalized.includes("prompt is too long") ||
    normalized.includes("too many tokens") ||
    normalized.includes("reduce the length of the messages") ||
    (normalized.includes("input tokens") && normalized.includes("exceed"))
  );
}

function requireRecord(value: unknown, message: string): Record<string, unknown> {
  if (!isRecord(value)) {
    throw new BestAgentError("PROVIDER_DECISION_INVALID", message);
  }
  return value;
}

function cloneSchemaRecord(schema: Record<string, unknown>): Record<string, unknown> {
  return requireRecord(
    JSON.parse(JSON.stringify(schema)),
    "Function calling schema clone must remain an object.",
  );
}

function normalizeJsonSchemaForFunctionCalling(value: unknown): unknown {
  if (Array.isArray(value)) {
    return value.map(normalizeJsonSchemaForFunctionCalling);
  }
  if (!isRecord(value)) {
    return value;
  }

  const normalized: Record<string, unknown> = {};
  for (const [key, candidate] of Object.entries(value)) {
    if ((key === "properties" || key === "patternProperties") && isRecord(candidate)) {
      normalized[key] = Object.fromEntries(
        Object.entries(candidate).map(([propertyName, propertySchema]) => [
          propertyName,
          normalizeJsonSchemaForFunctionCalling(propertySchema),
        ]),
      );
      continue;
    }
    normalized[key] = normalizeJsonSchemaForFunctionCalling(candidate);
  }

  if (Array.isArray(normalized.type) && normalized.type.every((type) => typeof type === "string")) {
    const unionTypes = normalized.type as string[];
    const sharedEntries = Object.entries(normalized).filter(
      ([key]) =>
        key !== "type" &&
        !Object.values(FUNCTION_CALL_SCHEMA_TYPE_KEYS).some((keys) => keys.includes(key)),
    );
    return {
      anyOf: unionTypes.map((type) => ({
        ...Object.fromEntries(sharedEntries),
        ...Object.fromEntries(
          (FUNCTION_CALL_SCHEMA_TYPE_KEYS[type] ?? [])
            .filter((key) => key in normalized)
            .map((key) => [key, normalized[key]]),
        ),
        type,
      })),
    };
  }

  return normalized;
}

function hasObjectRootSchema(schema: Record<string, unknown>): boolean {
  if (schema.type === "object") {
    return true;
  }
  if (schema.type != null) {
    return false;
  }
  return (
    "properties" in schema ||
    "required" in schema ||
    "additionalProperties" in schema ||
    "patternProperties" in schema
  );
}

function buildFunctionCallingParameters(inputSchema: unknown): Readonly<{
  decodeFunctionArguments: (input: unknown) => ToolInputValue;
  functionCallingParameters: Record<string, unknown>;
  strictFunctionCalling: boolean;
}> {
  const jsonInputSchema = getToolInputJsonSchema(inputSchema);
  if (!isRecord(jsonInputSchema)) {
    return {
      decodeFunctionArguments: (input) =>
        assertNativeToolInputValue(input, "Function calling decode (no schema)"),
      functionCallingParameters: { type: "object" },
      strictFunctionCalling: false,
    };
  }

  const schema = requireRecord(
    normalizeJsonSchemaForFunctionCalling(cloneSchemaRecord(jsonInputSchema)),
    "Function calling schema normalization must produce an object.",
  );
  if (hasObjectRootSchema(schema)) {
    return {
      decodeFunctionArguments: (input) =>
        assertNativeToolInputValue(input, "Function calling decode (object root schema)"),
      functionCallingParameters:
        schema.type === "object"
          ? schema
          : {
              ...schema,
              type: "object",
            },
      strictFunctionCalling: isStrictFunctionCallingSchema(
        schema.type === "object"
          ? schema
          : {
              ...schema,
              type: "object",
            },
      ),
    };
  }

  const functionCallingParameters = {
    additionalProperties: false,
    properties: {
      [FUNCTION_CALL_ARGUMENT_KEY]: schema,
    },
    required: [FUNCTION_CALL_ARGUMENT_KEY],
    type: "object",
  };
  return {
    decodeFunctionArguments(input) {
      if (isRecord(input) && FUNCTION_CALL_ARGUMENT_KEY in input) {
        return assertNativeToolInputValue(
          input[FUNCTION_CALL_ARGUMENT_KEY],
          "Function calling decode (wrapped key)",
        );
      }
      return assertNativeToolInputValue(input, "Function calling decode (unwrapped)");
    },
    functionCallingParameters,
    strictFunctionCalling: isStrictFunctionCallingSchema(functionCallingParameters),
  };
}

function isStrictFunctionCallingSchema(schema: unknown): boolean {
  if (!isRecord(schema)) {
    return false;
  }
  if ("anyOf" in schema || "oneOf" in schema || "allOf" in schema) {
    return false;
  }
  const type = schema.type;
  if (Array.isArray(type)) {
    return false;
  }
  if (type === "object") {
    if (schema.additionalProperties !== false || !isRecord(schema.properties)) {
      return false;
    }
    const propertyNames = Object.keys(schema.properties).sort();
    const required = Array.isArray(schema.required)
      ? schema.required.filter((entry): entry is string => typeof entry === "string").sort()
      : [];
    if (
      propertyNames.length !== required.length ||
      propertyNames.some((propertyName, index) => propertyName !== required[index])
    ) {
      return false;
    }
    return Object.values(schema.properties).every(isStrictFunctionCallingSchema);
  }
  if (type === "array") {
    return isStrictFunctionCallingSchema(schema.items);
  }
  return typeof type === "string";
}

export function prepareToolsForFunctionCalling(
  tools: readonly ProviderToolOption[],
): FunctionCallingToolOption[] {
  const codec = createProviderToolNameCodec(tools);
  return tools.map((tool) => {
    const runtimeName = toRuntimeToolName(tool.name);
    const providerName = codec.toProvider(runtimeName);
    return {
      ...tool,
      ...buildFunctionCallingParameters(tool.inputSchema),
      name: providerName,
      providerName,
      runtimeName,
    };
  });
}

export function decodeFunctionCallingToolInput(
  tools: readonly FunctionCallingToolOption[],
  toolName: string,
  input: unknown,
): ToolInputValue {
  const matched = resolveFunctionCallingTool(tools, toolName);
  return matched.decodeFunctionArguments(input);
}

class UnknownFunctionCallingToolError extends BestAgentError {
  readonly toolName: string;

  constructor(toolName: string) {
    super("PROVIDER_DECISION_INVALID", `Provider selected unknown tool ${toolName}.`);
    this.name = "UnknownFunctionCallingToolError";
    this.toolName = toolName;
  }
}

function isUnknownFunctionCallingToolError(
  error: unknown,
): error is UnknownFunctionCallingToolError {
  return error instanceof UnknownFunctionCallingToolError;
}

function resolveFunctionCallingTool(
  tools: readonly FunctionCallingToolOption[],
  toolName: string,
): FunctionCallingToolOption {
  const matched = tools.find((tool) => tool.name === toolName);
  if (matched) {
    return matched;
  }
  throw new UnknownFunctionCallingToolError(toolName);
}

export function normalizeProviderToolDecision(
  decision: ProviderToolDecision,
): ProviderSelectedToolCall[] {
  if (hasProviderToolCalls(decision)) {
    return decision.toolCalls;
  }
  return [
    {
      input: decision.input,
      ...(decision.providerToolCallId ? { providerToolCallId: decision.providerToolCallId } : {}),
      toolName: decision.toolName,
    },
  ];
}

export async function decideWithAiSdk(
  config: ProviderConfig,
  input: ProviderDecideInput,
  getModel: (modelId: string) => AiSdkModel,
): Promise<ProviderDecisionOutput> {
  return decideWithNativeTools(config, input, getModel);
}

async function decideWithNativeTools(
  config: ProviderConfig,
  input: ProviderDecideInput,
  getModel: (modelId: string) => AiSdkModel,
): Promise<ProviderDecisionOutput> {
  const controls = deriveControlsForActionPolicy(
    {
      allowPhaseResult: input.allowPhaseResult,
      allowPlanUpdate: input.allowPlanUpdate,
      phaseResultPhaseName: input.phaseResultPhaseName,
    },
    input.actionPolicy,
  );
  const allFunctionCallingTools = prepareToolsForFunctionCalling(input.tools);
  const functionCallingTools = selectFunctionCallingToolsForActionPolicy(
    allFunctionCallingTools,
    input.actionPolicy,
  );
  const aiSdkTools = buildAiSdkTools(functionCallingTools, {
    allowPhaseResult: controls.allowPhaseResult,
    allowPlanUpdate: controls.allowPlanUpdate,
    phaseResultPhaseName: controls.phaseResultPhaseName,
  });
  const hasNativeTools = Object.keys(aiSdkTools).length > 0;
  const activeTools = Object.keys(aiSdkTools);
  const toolChoice = resolveAiSdkToolChoice(input.actionPolicy, functionCallingTools);
  const systemPrompt = mergeSystemPrompts(
    input.systemPrompt ?? config.systemPrompt,
    buildNativeDecisionSystemPrompt(functionCallingTools, {
      actionPolicy: input.actionPolicy,
      allowPhaseResult: controls.allowPhaseResult,
      allowPlanUpdate: controls.allowPlanUpdate,
      phaseResultPhaseName: controls.phaseResultPhaseName,
      toolUsePolicy: input.toolUsePolicy,
    }),
  );
  const aiSdkSystemPrompt = resolveAiSdkSystemPrompt(config, systemPrompt);
  const messages = await mapConversationMessagesToAiSdk(input.messages, allFunctionCallingTools);

  const result = await generateTextWithProviderResilience(
    config,
    {
      model: getModel(config.model),
      messages,
      ...(typeof config.maxOutputTokens === "number"
        ? { maxOutputTokens: config.maxOutputTokens }
        : {}),
      providerOptions: resolveProviderOptions(config, {
        cacheControl: input.cacheControl,
        includeReasoningSummary: true,
        systemPrompt,
      }),
      ...(aiSdkSystemPrompt ? { system: aiSdkSystemPrompt } : {}),
      ...(hasNativeTools
        ? {
            activeTools,
            ...(toolChoice ? { toolChoice } : {}),
            tools: aiSdkTools,
          }
        : {}),
    },
    mapProviderToolStreamCallbacks(input.stream, functionCallingTools),
  );

  const usage = createProviderUsageStats(result.totalUsage);

  if (result.toolCalls && result.toolCalls.length > 0) {
    const selectedToolCalls = result.toolCalls.map((toolCall) => {
      const selected = decodeNativeSelectedToolCallRepairable({
        functionCallingTools,
        rawText: result.text,
        toolCall,
        usage,
      });
      return {
        ...selected,
        ...(toolCall.toolCallId ? { providerToolCallId: toolCall.toolCallId } : {}),
      };
    });
    const planToolCall = selectedToolCalls.find(
      (toolCall) => toolCall.toolName === PLAN_UPDATE_TOOL_NAME,
    );
    const phaseResultToolCall = selectedToolCalls.find(
      (toolCall) => toolCall.toolName === PHASE_RESULT_TOOL_NAME,
    );
    if (planToolCall && phaseResultToolCall) {
      throw new ProviderRepairableDecisionError({
        message: "Provider returned incompatible structured control decisions in one turn.",
        rawText: result.text,
        reasonCode: "invalid-structured-control-combination",
        selectedToolNames: selectedToolCalls.map((toolCall) => toolCall.toolName),
        usage,
      });
    }
    if (planToolCall) {
      if (selectedToolCalls.length !== 1) {
        throw new ProviderRepairableDecisionError({
          message: "Provider returned an invalid structured plan update decision.",
          rawText: result.text,
          reasonCode: "invalid-structured-control-combination",
          selectedToolNames: selectedToolCalls.map((toolCall) => toolCall.toolName),
          usage,
        });
      }
      return attachDecisionUsage(
        {
          kind: "plan_update",
          model: config.model,
          plan: parsePlanUpdateToolInputRepairable({
            input: planToolCall.input,
            rawText: result.text,
            usage,
          }),
          provider: config.kind,
        },
        usage,
      );
    }
    if (phaseResultToolCall) {
      if (selectedToolCalls.length !== 1) {
        throw new ProviderRepairableDecisionError({
          message: "Provider returned an invalid structured phase result decision.",
          rawText: result.text,
          reasonCode: "invalid-structured-control-combination",
          selectedToolNames: selectedToolCalls.map((toolCall) => toolCall.toolName),
          usage,
        });
      }
      return attachDecisionUsage(
        {
          kind: "phase_result",
          model: config.model,
          phaseResult: parsePhaseResultToolInputRepairable({
            expectedPhaseName: controls.phaseResultPhaseName,
            input: phaseResultToolCall.input,
            rawText: result.text,
            usage,
          }),
          provider: config.kind,
        },
        usage,
      );
    }
    return attachDecisionUsage(
      {
        kind: "tool",
        model: config.model,
        provider: config.kind,
        reasoning: normalizeReasoningText(result.reasoningText),
        toolCalls: selectedToolCalls,
      },
      usage,
    );
  }

  if (result.text?.trim()) {
    return attachDecisionUsage(
      {
        kind: "respond",
        model: config.model,
        provider: config.kind,
        text: result.text,
      },
      usage,
    );
  }

  throw new BestAgentError("PROVIDER_NO_ACTION", "Provider returned neither text nor a tool call.");
}

function requiredRuntimeToolNamesForActionPolicy(policy: ProviderTurnActionPolicy): string[] {
  if (policy.required?.kind !== "tool") {
    return [];
  }
  const names = new Set<string>();
  for (const name of policy.required.toolNames ?? []) {
    names.add(name);
  }
  for (const name of policy.required.requiredAllToolNames ?? []) {
    names.add(name);
  }
  for (const name of policy.required.requiredAnyToolNames ?? []) {
    names.add(name);
  }
  for (const group of policy.required.requiredAnyToolGroups ?? []) {
    for (const name of group) {
      names.add(name);
    }
  }
  return [...names].filter(Boolean);
}

function isNativeToolOnlyActionPolicy(policy: ProviderTurnActionPolicy): boolean {
  return (
    policy.allowedActions.length === 1 &&
    policy.allowedActions[0] === "tool" &&
    policy.directResponseAllowed === false
  );
}

function selectFunctionCallingToolsForActionPolicy(
  tools: readonly FunctionCallingToolOption[],
  policy: ProviderTurnActionPolicy,
): FunctionCallingToolOption[] {
  if (!policy.allowedActions.includes("tool")) {
    return [];
  }
  if (!isNativeToolOnlyActionPolicy(policy)) {
    return [...tools];
  }
  const requiredRuntimeNames =
    policy.required?.kind === "tool" && policy.required.selectableToolNames?.length
      ? policy.required.selectableToolNames
      : requiredRuntimeToolNamesForActionPolicy(policy);
  if (requiredRuntimeNames.length === 0) {
    return [...tools];
  }
  const required = new Set(requiredRuntimeNames);
  const selected = tools.filter((tool) => required.has(String(tool.runtimeName)));
  if (selected.length === 0) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Action policy requires unavailable tool(s): ${requiredRuntimeNames.join(", ")}.`,
    );
  }
  return selected;
}

function resolveAiSdkToolChoice(
  policy: ProviderTurnActionPolicy,
  tools: readonly FunctionCallingToolOption[],
): AiSdkToolChoice | undefined {
  if (tools.length === 0) {
    return undefined;
  }
  if (!isNativeToolOnlyActionPolicy(policy)) {
    return undefined;
  }
  if (tools.length === 1) {
    const [tool] = tools;
    return {
      type: "tool",
      toolName: tool.name,
    };
  }
  return "required";
}

function decodeNativeSelectedToolCall(
  functionCallingTools: FunctionCallingToolOption[],
  toolName: string,
  args: unknown,
): Pick<ProviderSelectedToolCall, "input" | "providerToolName" | "toolName"> {
  if (toolName === PLAN_UPDATE_TOOL_NAME || toolName === PHASE_RESULT_TOOL_NAME) {
    return {
      input: assertNativeToolInputValue(args, `Function calling decode (${toolName})`),
      providerToolName: toolName,
      toolName,
    };
  }
  const matched = resolveFunctionCallingTool(functionCallingTools, toolName);
  return {
    input: matched.decodeFunctionArguments(args),
    providerToolName: matched.name,
    toolName: matched.runtimeName,
  };
}

function nativeToolCallInput(toolCall: { args?: unknown; input?: unknown }): unknown {
  return Object.prototype.hasOwnProperty.call(toolCall, "input") ? toolCall.input : toolCall.args;
}

function decodeNativeSelectedToolCallRepairable(input: {
  functionCallingTools: FunctionCallingToolOption[];
  rawText?: string;
  toolCall: { args?: unknown; input?: unknown; toolName: string };
  usage?: ProviderUsageStats;
}): Pick<ProviderSelectedToolCall, "input" | "providerToolName" | "toolName"> {
  try {
    return decodeNativeSelectedToolCall(
      input.functionCallingTools,
      input.toolCall.toolName,
      nativeToolCallInput(input.toolCall),
    );
  } catch (error) {
    const structuredControl =
      input.toolCall.toolName === PLAN_UPDATE_TOOL_NAME ||
      input.toolCall.toolName === PHASE_RESULT_TOOL_NAME;
    const unknownNativeTool = isUnknownFunctionCallingToolError(error);
    throw toRepairableDecisionError({
      error,
      messagePrefix: unknownNativeTool
        ? "The provider selected an unknown native tool"
        : structuredControl
          ? `The provider returned an invalid ${input.toolCall.toolName} payload`
          : "The provider returned an invalid native tool input",
      rawText: input.rawText,
      reasonCode: unknownNativeTool
        ? "invalid-native-tool-selection"
        : structuredControl
          ? "invalid-structured-control-input"
          : "invalid-native-tool-input",
      selectedToolNames: [input.toolCall.toolName],
      usage: input.usage,
    });
  }
}

function parsePlanUpdateToolInputRepairable(input: {
  input: ToolInputValue;
  rawText?: string;
  usage?: ProviderUsageStats;
}): ProviderPlanUpdatePayload {
  try {
    return parsePlanUpdateToolInput(input.input);
  } catch (error) {
    throw toRepairableDecisionError({
      error,
      messagePrefix: `The provider returned an invalid ${PLAN_UPDATE_TOOL_NAME} payload`,
      rawText: input.rawText,
      reasonCode: "invalid-structured-control-input",
      selectedToolNames: [PLAN_UPDATE_TOOL_NAME],
      usage: input.usage,
    });
  }
}

function parsePhaseResultToolInputRepairable(input: {
  expectedPhaseName?: PhaseResultPhaseName;
  input: ToolInputValue;
  rawText?: string;
  usage?: ProviderUsageStats;
}): ProviderPhaseResultPayload {
  try {
    return parsePhaseResultToolInput(input.input, input.expectedPhaseName);
  } catch (error) {
    throw toRepairableDecisionError({
      error,
      messagePrefix: `The provider returned an invalid ${PHASE_RESULT_TOOL_NAME} payload`,
      rawText: input.rawText,
      reasonCode: "invalid-structured-control-input",
      selectedToolNames: [PHASE_RESULT_TOOL_NAME],
      usage: input.usage,
    });
  }
}

function resolveRuntimeToolName(
  functionCallingTools: readonly FunctionCallingToolOption[],
  toolName: string,
): string {
  if (toolName === PLAN_UPDATE_TOOL_NAME || toolName === PHASE_RESULT_TOOL_NAME) {
    return toolName;
  }
  return functionCallingTools.find((tool) => tool.name === toolName)?.runtimeName ?? toolName;
}

function mapProviderToolStreamCallbacks(
  callbacks: ProviderTextStreamCallbacks | undefined,
  functionCallingTools: readonly FunctionCallingToolOption[],
): ProviderTextStreamCallbacks | undefined {
  if (!callbacks) {
    return undefined;
  }
  return {
    ...callbacks,
    onToolCallDelta: callbacks.onToolCallDelta
      ? (event) =>
          callbacks.onToolCallDelta?.({
            ...event,
            toolName: resolveRuntimeToolName(functionCallingTools, event.toolName),
          })
      : undefined,
    onToolCallStarted: callbacks.onToolCallStarted
      ? (event) =>
          callbacks.onToolCallStarted?.({
            ...event,
            toolName: resolveRuntimeToolName(functionCallingTools, event.toolName),
          })
      : undefined,
  };
}

/** Convert ProviderToolOption[] to AI SDK native tools record. */
function buildAiSdkTools(
  tools: FunctionCallingToolOption[],
  options: ProviderDecisionParseOptions = {},
): AiSdkToolSet {
  const result: Record<string, AiSdkToolValue> = {};
  for (const t of tools) {
    result[t.name] = aiSdkTool({
      description: t.description,
      inputSchema: aiSdkJsonSchema(t.functionCallingParameters),
      ...(t.strictFunctionCalling ? { strict: true } : {}),
    });
  }
  if (options.allowPlanUpdate) {
    result[PLAN_UPDATE_TOOL_NAME] = aiSdkTool({
      description: "Structured plan_update action for plan-authoring turns.",
      inputSchema: aiSdkJsonSchema({
        type: "object" as const,
        properties: {
          title: { type: "string", description: "Plan title" },
          summary: { type: "string", description: "Plan summary" },
          status: { type: "string", enum: ["draft", "review_ready"] },
          todos: {
            type: "array",
            items: {
              type: "object",
              properties: {
                content: { type: "string" },
                status: { type: "string" },
                kind: { type: "string", enum: PLAN_TODO_KINDS },
              },
              required: ["content"],
            },
          },
          risks: { type: "array", items: { type: "string" } },
          openQuestions: { type: "array", items: { type: "string" } },
          acceptanceCriteria: { type: "array", items: { type: "string" } },
          affectedPaths: { type: "array", items: { type: "string" } },
          evidenceRefs: { type: "array", items: { type: "string" } },
        },
        required: ["title", "summary", "status", "todos"],
      }),
    });
  }
  if (options.allowPhaseResult && options.phaseResultPhaseName) {
    result[PHASE_RESULT_TOOL_NAME] = aiSdkTool({
      description:
        "Structured phase_result action for completing the current phase. Verify phases include candidate verdict support.",
      inputSchema: aiSdkJsonSchema({
        type: "object" as const,
        properties: {
          phaseName: {
            type: "string",
            enum: [options.phaseResultPhaseName],
          },
          summary: {
            type: "string",
            description: "Compact phase-completion summary for the parent workflow or controller.",
          },
          candidateVerdict: {
            type: "object",
            properties: {
              blockingReason: { type: "string" },
              status: {
                type: "string",
                enum: ["satisfied", "unsatisfied", "inconclusive", "blocked"],
              },
              summary: { type: "string" },
            },
            required: ["status", "summary"],
          },
        },
        required:
          options.phaseResultPhaseName === "verify"
            ? ["phaseName", "summary", "candidateVerdict"]
            : ["phaseName", "summary"],
      }),
    });
  }
  return result;
}

function parsePlanUpdateToolInput(input: ToolInputValue): ProviderPlanUpdatePayload {
  if (!input || typeof input !== "object" || Array.isArray(input)) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider plan_update tool inputs must be objects.",
    );
  }
  return parsePlanUpdatePayload(input);
}

function parsePhaseResultToolInput(
  input: ToolInputValue,
  expectedPhaseName?: PhaseResultPhaseName,
): ProviderPhaseResultPayload {
  if (!input || typeof input !== "object" || Array.isArray(input)) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result tool inputs must be objects.",
    );
  }
  return parsePhaseResultPayload(input, expectedPhaseName);
}

/** System prompt for native tool calling — behavioral guidance only, no JSON format instructions. */
function buildNativeDecisionSystemPrompt(
  tools: readonly ProviderToolOption[],
  options: ProviderDecisionParseOptions = {},
): string {
  const toolUsePolicy = options.toolUsePolicy ?? "answer-when-sufficient";
  const actionPolicy = options.actionPolicy ?? defaultProviderTurnActionPolicy(tools, options);
  const availableToolLines = tools.map((tool) => `- ${tool.name}: ${tool.description}`);
  const renamedToolLines = tools.flatMap((tool) => {
    const runtimeName = "runtimeName" in tool ? tool.runtimeName : undefined;
    return typeof runtimeName === "string" && runtimeName !== tool.name
      ? [`- ${runtimeName} is exposed to the provider as ${tool.name}.`]
      : [];
  });
  if (options.allowPlanUpdate) {
    availableToolLines.push(`- ${PLAN_UPDATE_TOOL_NAME}: Structured plan update action.`);
  }
  if (options.allowPhaseResult && options.phaseResultPhaseName) {
    availableToolLines.push(
      `- ${PHASE_RESULT_TOOL_NAME}: Structured ${options.phaseResultPhaseName} phase result action${options.phaseResultPhaseName === "verify" ? " with candidate verdict support" : ""}.`,
    );
  }
  const structuredActionLines = actionPolicy.directResponseAllowed
    ? []
    : ["Direct prose is not an allowed action for this turn."];
  return [
    "You are deciding the next runtime action for a single turn.",
    renderActionPolicyPrompt(actionPolicy),
    ...nativeDecisionToolPolicyLines(toolUsePolicy, actionPolicy),
    "Use only listed runtime actions and tools. Do not invent tool names.",
    "Assistant-visible text must not contain raw tool-call markup, XML-like arg tags, or JSON control payloads.",
    availableToolLines.length > 0 ? "Available tools:" : undefined,
    ...availableToolLines,
    renamedToolLines.length > 0 ? "Provider tool name bindings:" : undefined,
    ...renamedToolLines,
    ...structuredActionLines,
  ]
    .filter(Boolean)
    .join("\n");
}

function defaultProviderTurnActionPolicy(
  tools: readonly ProviderToolOption[],
  options: ProviderDecisionParseOptions,
): ProviderTurnActionPolicy {
  if (options.allowPlanUpdate) {
    return {
      allowedActions: tools.length > 0 ? ["tool", "plan_update"] : ["plan_update"],
      directResponseAllowed: false,
      required: {
        kind: "plan_update",
        reason: "plan-authoring-turn",
      },
    };
  }
  if (options.allowPhaseResult) {
    return {
      allowedActions: tools.length > 0 ? ["tool", "phase_result"] : ["phase_result"],
      directResponseAllowed: false,
      required: {
        kind: "phase_result",
        phaseName: options.phaseResultPhaseName ?? "current",
        reason: "phase-completion-turn",
      },
    };
  }
  return {
    allowedActions: tools.length > 0 ? ["respond", "tool"] : ["respond"],
    directResponseAllowed: true,
  };
}

function renderActionPolicyPrompt(policy: ProviderTurnActionPolicy): string {
  const lines = [`Allowed actions: ${policy.allowedActions.join(", ")}.`];
  if (policy.directResponseAllowed) {
    lines.push(
      "A direct prose response is allowed only when respond is one of the allowed actions.",
    );
  } else {
    lines.push("Direct prose is not an allowed runtime action for this turn.");
  }
  if (!policy.required) {
    return lines.join("\n");
  }
  if (policy.required.kind === "tool") {
    lines.push("Runtime tool action is available; the runtime does not require a specific tool name.");
    return lines.join("\n");
  }
  if (policy.required.kind === "plan_update") {
    lines.push(`Required runtime action: ${PLAN_UPDATE_TOOL_NAME}.`);
    return lines.join("\n");
  }
  lines.push(`Required runtime action: ${PHASE_RESULT_TOOL_NAME}.`);
  return lines.join("\n");
}

function nativeDecisionToolPolicyLines(
  policy: ToolUsePolicy,
  actionPolicy: ProviderTurnActionPolicy,
): string[] {
  switch (policy) {
    case "answer-when-sufficient":
    case "tool-first-implementation":
      return actionPolicy.directResponseAllowed
        ? [
            "Runtime action policy: tools are available; direct response is allowed when grounded by current context.",
          ]
        : ["Runtime action policy: direct response is not available for this turn."];
    case "required-tool-only":
      return ["Runtime action policy: direct response is not available for this turn."];
  }
}

function formatDecisionOutputExcerpt(text: string): string {
  const trimmed = text.trim();
  if (!trimmed) {
    return "(empty)";
  }
  return JSON.stringify(truncateText(normalizeWhitespace(trimmed), 400));
}

export function decorateInvalidStructuredDecision(
  message: string,
  rawText?: string,
): BestAgentError {
  const decorated = new BestAgentError(
    "PROVIDER_DECISION_INVALID",
    rawText
      ? `${message} Raw provider output excerpt: ${formatDecisionOutputExcerpt(rawText)}`
      : message,
  );
  if (rawText) {
    (decorated as BestAgentError & { rawDecisionText?: string }).rawDecisionText = rawText;
  }
  return decorated;
}

function toRepairableDecisionError(input: {
  error: unknown;
  messagePrefix: string;
  rawText?: string;
  reasonCode: ProviderRepairableDecisionReasonCode;
  selectedToolNames?: string[];
  usage?: ProviderUsageStats;
}): ProviderRepairableDecisionError {
  const message =
    input.error instanceof Error && input.error.message
      ? `${input.messagePrefix}: ${input.error.message}`
      : input.messagePrefix;
  return new ProviderRepairableDecisionError({
    message,
    rawText: input.rawText,
    reasonCode: input.reasonCode,
    selectedToolNames: input.selectedToolNames,
    usage: input.usage,
  });
}

export class ProviderRepairableDecisionError extends BestAgentError {
  readonly providerRepairableDecision = true;
  readonly rawText?: string;
  readonly reasonCode: ProviderRepairableDecisionReasonCode;
  readonly selectedToolNames?: string[];
  readonly usage?: ProviderUsageStats;

  constructor(input: {
    message: string;
    rawText?: string;
    reasonCode: ProviderRepairableDecisionReasonCode;
    selectedToolNames?: string[];
    usage?: ProviderUsageStats;
  }) {
    super(
      "PROVIDER_DECISION_INVALID",
      input.rawText
        ? `${input.message} Raw provider output excerpt: ${formatDecisionOutputExcerpt(input.rawText)}`
        : input.message,
    );
    this.name = "ProviderRepairableDecisionError";
    this.reasonCode = input.reasonCode;
    this.rawText = input.rawText;
    this.selectedToolNames = input.selectedToolNames;
    this.usage = input.usage;
  }
}

export function isProviderRepairableDecisionError(
  error: unknown,
): error is ProviderRepairableDecisionError {
  return (
    error instanceof ProviderRepairableDecisionError ||
    (error instanceof BestAgentError &&
      error.code === "PROVIDER_DECISION_INVALID" &&
      (error as Partial<ProviderRepairableDecisionError>).providerRepairableDecision === true)
  );
}

function getStringField(source: Record<string, unknown>, keys: string[]): string | undefined {
  for (const key of keys) {
    if (!(key in source)) {
      continue;
    }
    const value = source[key];
    if (typeof value === "string" && value.trim().length > 0) {
      return value;
    }
  }

  return undefined;
}

export function parsePlanUpdatePayload(
  plan: Record<string, unknown> | undefined,
): ProviderPlanUpdatePayload {
  if (!plan) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider plan_update decisions must include a plan object.",
    );
  }
  const allowedKeys = [
    "acceptanceCriteria",
    "affectedPaths",
    "evidenceRefs",
    "openQuestions",
    "risks",
    "status",
    "summary",
    "title",
    "todos",
  ];
  assertAllowedKeys(plan, allowedKeys);

  const title = getStringField(plan, ["title"]);
  const summary = getStringField(plan, ["summary"]);
  const status = normalizeProviderPlanStatus(getStringField(plan, ["status"]));
  const todos = parsePlanUpdateTodos(plan.todos);
  if (!title) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider plan_update payloads must include title.",
    );
  }
  if (!summary) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider plan_update payloads must include summary.",
    );
  }
  if (!status) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      'Provider plan_update payload status must be "draft" or "review_ready".',
    );
  }

  return {
    acceptanceCriteria: parsePlanUpdateStringList(plan.acceptanceCriteria, "acceptanceCriteria"),
    affectedPaths: parsePlanUpdateStringList(plan.affectedPaths, "affectedPaths"),
    evidenceRefs: parsePlanUpdateStringList(plan.evidenceRefs, "evidenceRefs"),
    openQuestions: parsePlanUpdateStringList(plan.openQuestions, "openQuestions"),
    risks: parsePlanUpdateStringList(plan.risks, "risks"),
    status,
    summary,
    title,
    todos,
  };
}

export function parsePhaseResultPayload(
  payload: Record<string, unknown> | undefined,
  expectedPhaseName?: PhaseResultPhaseName,
): ProviderPhaseResultPayload {
  if (!payload) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result decisions must include a payload object.",
    );
  }
  const allowedKeys = ["candidateVerdict", "phaseName", "summary"];
  assertAllowedKeys(payload, allowedKeys);

  const phaseName = normalizePhaseResultPhaseName(getStringField(payload, ["phaseName"]));
  const summary = getStringField(payload, ["summary"]);
  const candidateVerdict = parsePhaseResultCandidateVerdict(payload.candidateVerdict);
  if (!phaseName) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result payloads must include phaseName.",
    );
  }
  if (expectedPhaseName && phaseName !== expectedPhaseName) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Provider phase_result payload phaseName must be ${expectedPhaseName}.`,
    );
  }
  if (!summary) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result payloads must include summary.",
    );
  }
  if (phaseName === "verify" && !candidateVerdict) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result payloads for verify must include candidateVerdict.",
    );
  }
  if (phaseName !== "verify" && candidateVerdict) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result payloads may only include candidateVerdict for verify.",
    );
  }
  return {
    ...(candidateVerdict ? { candidateVerdict } : {}),
    phaseName,
    summary,
  };
}

function parsePhaseResultCandidateVerdict(value: unknown): PhaseResultCandidateVerdict | undefined {
  if (value === undefined) {
    return undefined;
  }
  const verdict = requireRecord(value, "Provider phase_result candidateVerdict must be an object.");
  const allowedKeys = ["blockingReason", "status", "summary"];
  assertAllowedKeys(verdict, allowedKeys);
  const status = normalizePhaseResultVerdictStatus(getStringField(verdict, ["status"]));
  const summary = getStringField(verdict, ["summary"]);
  const blockingReason = getStringField(verdict, ["blockingReason"]);
  if (!status) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result candidateVerdict must include a valid status.",
    );
  }
  if (!summary) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider phase_result candidateVerdict must include summary.",
    );
  }
  return {
    ...(blockingReason ? { blockingReason } : {}),
    status,
    summary,
  };
}

function parsePlanUpdateTodos(value: unknown): ProviderPlanUpdateTodo[] {
  if (!Array.isArray(value)) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      "Provider plan_update payloads must include todos as an array.",
    );
  }
  return value.map((entry, index) => parsePlanUpdateTodo(entry, index));
}

function parsePlanUpdateTodo(value: unknown, index: number): ProviderPlanUpdateTodo {
  const todo = requireRecord(value, `Provider plan_update todo ${index + 1} must be an object.`);
  const allowedKeys = ["content", "kind", "status"];
  assertAllowedKeys(todo, allowedKeys);
  const content = getStringField(todo, ["content"]);
  const kind = normalizeProviderPlanTodoKind(getStringField(todo, ["kind"]));
  const status = normalizeProviderPlanTodoStatus(getStringField(todo, ["status"]));
  if (!content) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Provider plan_update todo ${index + 1} must include content.`,
    );
  }
  if ("kind" in todo && !kind) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Provider plan_update todo ${index + 1} uses an invalid kind.`,
    );
  }
  if ("status" in todo && !status) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Provider plan_update todo ${index + 1} uses an invalid status.`,
    );
  }
  return {
    content,
    ...(kind ? { kind } : {}),
    ...(status ? { status } : {}),
  };
}

function parsePlanUpdateStringList(value: unknown, fieldName: string): string[] {
  if (value === undefined) {
    return [];
  }
  if (!Array.isArray(value)) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Provider plan_update payload ${fieldName} must be an array of strings.`,
    );
  }
  const invalidIndex = value.findIndex((entry) => typeof entry !== "string");
  if (invalidIndex !== -1) {
    throw new BestAgentError(
      "PROVIDER_DECISION_INVALID",
      `Provider plan_update payload ${fieldName} entry ${invalidIndex + 1} must be a string.`,
    );
  }
  return value.filter((entry) => entry.trim().length > 0).map((entry) => entry.trim());
}

function normalizeProviderPlanStatus(
  value: string | undefined,
): Extract<PlanStatus, "draft" | "review_ready"> | undefined {
  switch ((value ?? "").trim().toLowerCase()) {
    case "draft":
      return "draft";
    case "review_ready":
      return "review_ready";
    default:
      return undefined;
  }
}

function normalizeProviderPlanTodoKind(value: string | undefined): PlanTodoKind | undefined {
  const normalized = (value ?? "").trim().toLowerCase();
  if (!normalized) {
    return undefined;
  }
  return isPlanTodoKind(normalized) ? normalized : undefined;
}

function isPlanTodoKind(value: string): value is PlanTodoKind {
  return (PLAN_TODO_KINDS as readonly string[]).includes(value);
}

function normalizeProviderPlanTodoStatus(value: string | undefined): PlanTodoStatus | undefined {
  switch ((value ?? "").trim().toLowerCase()) {
    case "":
      return undefined;
    case "pending":
      return "pending";
    case "in_progress":
      return "in_progress";
    case "blocked":
      return "blocked";
    case "done":
      return "done";
    case "cancelled":
      return "cancelled";
    default:
      return undefined;
  }
}

function normalizePhaseResultPhaseName(
  value: string | undefined,
): PhaseResultPhaseName | undefined {
  switch ((value ?? "").trim().toLowerCase()) {
    case "inspect":
      return "inspect";
    case "plan":
      return "plan";
    case "change":
      return "change";
    case "explore":
      return "explore";
    case "build":
      return "build";
    case "verify":
      return "verify";
    default:
      return undefined;
  }
}

function normalizePhaseResultVerdictStatus(
  value: string | undefined,
): PhaseResultCandidateVerdict["status"] | undefined {
  switch ((value ?? "").trim().toLowerCase()) {
    case "satisfied":
      return "satisfied";
    case "unsatisfied":
      return "unsatisfied";
    case "inconclusive":
      return "inconclusive";
    case "blocked":
      return "blocked";
    default:
      return undefined;
  }
}

function isToolInputValueStrict(value: unknown): value is ToolInputValue {
  if (value === null) return true;
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
    return true;
  }
  if (Array.isArray(value)) {
    return value.every(isToolInputValueStrict);
  }
  if (value && typeof value === "object") {
    return Object.values(value).every(isToolInputValueStrict);
  }
  return false;
}

function assertNativeToolInputValue(value: unknown, context: string): ToolInputValue {
  if (isToolInputValueStrict(value)) {
    return value;
  }

  throw new BestAgentError(
    "PROVIDER_DECISION_INVALID",
    `${context}: provider returned tool input that is not a valid ToolInputValue.`,
  );
}

export function decisionLeakToolNames(
  tools: readonly ProviderToolOption[],
  allowPlanUpdate = false,
  allowPhaseResult = false,
): string[] {
  const names = tools.map((tool) => tool.name);
  if (allowPlanUpdate) {
    names.push(PLAN_UPDATE_TOOL_NAME);
  }
  if (allowPhaseResult) {
    names.push(PHASE_RESULT_TOOL_NAME);
  }
  return names;
}

/** @deprecated Use containsControlPayloadFinal from shared/control-payload-classifier. */
export function containsDecisionControlLeak(
  text: string,
  toolNames: readonly string[] = [],
): boolean {
  return containsControlPayloadFinal(text, toolNames);
}

function assertAllowedKeys(parsed: Record<string, unknown>, allowedKeys: readonly string[]): void {
  for (const key of Object.keys(parsed)) {
    if (!allowedKeys.includes(key)) {
      throw new BestAgentError(
        "PROVIDER_DECISION_INVALID",
        `Provider structured decisions may not include unexpected key ${key}.`,
      );
    }
  }
}

export function mergeSystemPrompts(...parts: Array<string | undefined>): string | undefined {
  const content = parts
    .map((value) => value?.trim())
    .filter((value): value is string => Boolean(value && value.length > 0));
  if (content.length === 0) {
    return undefined;
  }

  return content.join("\n\n");
}

export function resolveProviderOptions(
  config: ProviderConfig,
  input: ResolveProviderOptionsInput | undefined,
): BestAgentProviderOptions | undefined {
  const providerOptions: BestAgentProviderOptions = {};
  const cacheControl = input?.cacheControl;
  const systemPrompt = input?.systemPrompt?.trim();

  if (cacheControl?.stablePrefixText && config.kind === "anthropic") {
    providerOptions.anthropic = {
      cacheControl: {
        type: "ephemeral",
      },
    };
  }

  const openAiOptions: OpenAiProviderOptions = {};
  if (systemPrompt && supportsOpenAiResponsesInstructions(config)) {
    openAiOptions.instructions = systemPrompt;
    openAiOptions.systemMessageMode = "remove";
  }
  if (cacheControl?.stablePrefixText && supportsNativeOpenAiPromptCache(config)) {
    openAiOptions.promptCacheKey = createPromptCacheKey(config, cacheControl.stablePrefixText);
  }
  if (supportsOpenAiReasoningControls(config)) {
    const reasoningEffort = resolveOpenAiReasoningEffort(config, input);
    if (reasoningEffort) {
      openAiOptions.reasoningEffort = reasoningEffort;
    }
    if (input?.includeReasoningSummary) {
      openAiOptions.reasoningSummary = "auto";
    }
  }

  const finalizedOpenAiOptions = finalizeOpenAiProviderOptions(openAiOptions);
  if (finalizedOpenAiOptions) {
    providerOptions.openai = finalizedOpenAiOptions;
  }

  if (
    isOpenAiCompatibleProviderKind(config.kind) &&
    resolveOpenAiCompatibleMode(config) === "compatible"
  ) {
    const openAiCompatibleOptions: OpenAiCompatibleProviderOptions = {};
    if (config.reasoningEffort) {
      openAiCompatibleOptions.reasoningEffort = config.reasoningEffort;
    }
    const finalizedOpenAiCompatibleOptions =
      finalizeOpenAiCompatibleProviderOptions(openAiCompatibleOptions);
    if (finalizedOpenAiCompatibleOptions) {
      providerOptions.openaiCompatible = finalizedOpenAiCompatibleOptions;
    }
  }

  return nonEmptyObject(providerOptions);
}

function finalizeOpenAiProviderOptions(
  options: OpenAiProviderOptions,
): OpenAiProviderOptions | undefined {
  return nonEmptyObject(options);
}

function finalizeOpenAiCompatibleProviderOptions(
  options: OpenAiCompatibleProviderOptions,
): OpenAiCompatibleProviderOptions | undefined {
  return nonEmptyObject(options);
}

function nonEmptyObject<T extends object>(value: T): T | undefined {
  return Object.keys(value).length > 0 ? value : undefined;
}

export function resolveAiSdkSystemPrompt(
  config: ProviderConfig,
  systemPrompt: string | undefined,
): string | undefined {
  const trimmed = systemPrompt?.trim();
  if (!trimmed) {
    return undefined;
  }

  return supportsOpenAiResponsesInstructions(config) ? undefined : trimmed;
}

export function resolveProviderPromptCacheDiagnostics(
  config: ProviderConfig,
  cacheControl: ProviderCacheControl | undefined,
): ProviderPromptCacheDiagnostics {
  if (!cacheControl?.stablePrefixText) {
    return {
      eligible: false,
      providerHintApplied: false,
    };
  }

  const eligible = supportsNativeProviderPromptCache(config);
  return {
    eligible,
    providerHintApplied: eligible && Boolean(resolveProviderOptions(config, { cacheControl })),
  };
}

export function attachDecisionUsage(
  decision: ProviderDecisionOutput,
  usage: ProviderUsageStats | undefined,
): ProviderDecisionOutput {
  return usage ? { ...decision, usage } : decision;
}

export function createProviderUsageStats(
  usage: LanguageModelUsage | undefined,
): ProviderUsageStats | undefined {
  if (!usage) {
    return undefined;
  }

  return {
    cacheReadTokens: usage.inputTokenDetails.cacheReadTokens ?? usage.cachedInputTokens ?? 0,
    cacheWriteTokens: usage.inputTokenDetails.cacheWriteTokens ?? 0,
    inputTokens: usage.inputTokens ?? 0,
    outputTokens: usage.outputTokens ?? 0,
    reasoningTokens: usage.outputTokenDetails.reasoningTokens ?? usage.reasoningTokens ?? 0,
    totalTokens: usage.totalTokens ?? 0,
  };
}

function supportsNativeOpenAiPromptCache(config: ProviderConfig): boolean {
  if (config.kind !== "openai" && config.kind !== "openai-responses") {
    return false;
  }

  if (config.compatibilityMode === "compatible") {
    return false;
  }

  if (!config.baseURL) {
    return true;
  }

  try {
    return new URL(config.baseURL).hostname === "api.openai.com";
  } catch {
    return false;
  }
}

function supportsNativeProviderPromptCache(config: ProviderConfig): boolean {
  return config.kind === "anthropic" || supportsNativeOpenAiPromptCache(config);
}

function supportsOpenAiReasoningControls(config: ProviderConfig): boolean {
  return (
    isOpenAiCompatibleProviderKind(config.kind) &&
    isOpenAiReasoningModel(config.model) &&
    resolveOpenAiCompatibleMode(config) === "native"
  );
}

function supportsOpenAiResponsesInstructions(config: ProviderConfig): boolean {
  return (
    isOpenAiCompatibleProviderKind(config.kind) && resolveOpenAiCompatibleMode(config) === "native"
  );
}

function isOpenAiReasoningModel(modelId: string): boolean {
  return (
    modelId.startsWith("o1") ||
    modelId.startsWith("o3") ||
    modelId.startsWith("o4-mini") ||
    (modelId.startsWith("gpt-5") && !modelId.startsWith("gpt-5-chat"))
  );
}

function resolveOpenAiReasoningEffort(
  config: ProviderConfig,
  input: ResolveProviderOptionsInput | undefined,
): ProviderConfig["reasoningEffort"] | undefined {
  if (config.reasoningEffort) {
    return config.reasoningEffort;
  }
  return input?.includeReasoningSummary ? "medium" : undefined;
}

function normalizeReasoningText(reasoningText: string | undefined): string | undefined {
  const normalized = reasoningText?.trim();
  return normalized ? normalized : undefined;
}

function createPromptCacheKey(config: ProviderConfig, stablePrefixText: string): string {
  const digest = createHash("sha1").update(stablePrefixText).digest("hex");
  return [
    "best-agent",
    config.kind,
    config.model,
    resolvePromptCacheCompatibilityFingerprint(config),
    resolvePromptCacheBaseUrlFingerprint(config),
    digest,
  ].join(":");
}

function resolvePromptCacheCompatibilityFingerprint(config: ProviderConfig): string {
  if (config.kind !== "openai" && config.kind !== "openai-responses") {
    return config.compatibilityMode ?? "default";
  }
  if (config.compatibilityMode === "compatible") {
    return "compatible";
  }
  if (config.compatibilityMode === "native") {
    return "native";
  }
  if (!config.baseURL) {
    return "native";
  }
  try {
    return new URL(config.baseURL).hostname === "api.openai.com" ? "native" : "compatible";
  } catch {
    return "native";
  }
}

function resolvePromptCacheBaseUrlFingerprint(config: ProviderConfig): string {
  if (!config.baseURL) {
    return "default";
  }
  try {
    return new URL(config.baseURL).toString().replace(/\/$/u, "");
  } catch {
    return config.baseURL;
  }
}

async function withProviderDiagnostics<T>(
  config: ProviderConfig,
  operation: () => Promise<T>,
): Promise<T> {
  try {
    return await operation();
  } catch (error) {
    throw decorateProviderError(error, config);
  }
}

export async function generateTextWithProviderResilience(
  config: ProviderConfig,
  input: Parameters<typeof generateText>[0],
  stream?: ProviderTextStreamCallbacks,
): Promise<ProviderTextResult> {
  const timeoutMs = resolveProviderTimeoutMs(config);
  return withProviderDiagnostics(config, async () => {
    let lastError: unknown;
    for (let attempt = 0; attempt < MAX_RETRYABLE_PROVIDER_ATTEMPTS; attempt += 1) {
      try {
        if (!stream && !shouldUseStreamingTransport(config)) {
          const result = await generateText({
            ...input,
            timeout: { totalMs: timeoutMs },
          });
          const nativeToolCalls = extractNativeToolCalls(result);
          return {
            reasoningText: result.reasoningText,
            sources: result.sources,
            text: result.text,
            toolCalls: nativeToolCalls,
            totalUsage: result.totalUsage,
          };
        }

        return await streamTextWithProviderResilience(config, input, stream ?? {});
      } catch (error) {
        lastError = error;
        if (!isRetryableProviderError(error) || attempt + 1 >= MAX_RETRYABLE_PROVIDER_ATTEMPTS) {
          throw error;
        }
        await sleep(resolveProviderRetryDelayMs(attempt, error));
      }
    }

    throw lastError;
  });
}

function shouldUseStreamingTransport(config: ProviderConfig): boolean {
  return (
    config.kind === "openai-responses" ||
    config.kind === "xai-responses" ||
    config.kind === "aihubmix-responses"
  );
}

async function streamTextWithProviderResilience(
  config: ProviderConfig,
  input: Parameters<typeof generateText>[0],
  callbacks: ProviderTextStreamCallbacks,
): Promise<ProviderTextResult> {
  const timeoutMs = resolveProviderTimeoutMs(config);
  const noActionTimeoutMs =
    typeof callbacks.noActionTimeoutMs === "number" &&
    Number.isFinite(callbacks.noActionTimeoutMs) &&
    callbacks.noActionTimeoutMs > 0
      ? callbacks.noActionTimeoutMs
      : undefined;
  const openReasoningIds = new Set<string>();
  const pendingReasoningDeltas = new Map<string, string>();
  const openToolCalls = new Map<string, string>(); // toolCallId → toolName
  const pendingToolInputDeltas = new Map<string, string>();
  const idleAbortController = new AbortController();
  let sawReasoningDelta = false;
  let noActionReject: ((error: BestAgentError) => void) | undefined;
  let noActionTimer: ReturnType<typeof setTimeout> | undefined;
  let idleTimer: ReturnType<typeof setTimeout> | undefined;
  const noActionPromise = noActionTimeoutMs
    ? new Promise<never>((_resolve, reject) => {
        noActionReject = reject;
      })
    : undefined;

  const clearIdleTimer = () => {
    if (idleTimer) {
      clearTimeout(idleTimer);
      idleTimer = undefined;
    }
  };

  const resetIdleTimer = () => {
    clearIdleTimer();
    idleTimer = setTimeout(() => {
      idleAbortController.abort(new Error(`Provider stream idle timeout after ${timeoutMs}ms.`));
    }, timeoutMs);
  };

  const clearNoActionTimer = () => {
    if (noActionTimer) {
      clearTimeout(noActionTimer);
      noActionTimer = undefined;
    }
  };

  const resetNoActionTimer = () => {
    clearNoActionTimer();
    if (!noActionTimeoutMs) {
      return;
    }
    noActionTimer = setTimeout(() => {
      const error = new BestAgentError(
        "PROVIDER_NO_ACTION",
        `Provider streamed without selecting a tool or final text within ${noActionTimeoutMs}ms.`,
      );
      idleAbortController.abort(error);
      noActionReject?.(error);
      noActionReject = undefined;
    }, noActionTimeoutMs);
  };

  const closeReasoning = async (streamId: string) => {
    if (!openReasoningIds.delete(streamId)) {
      return;
    }
    const bufferedDelta = pendingReasoningDeltas.get(streamId);
    if (bufferedDelta) {
      pendingReasoningDeltas.delete(streamId);
      await callbacks.onReasoningDelta?.({ delta: bufferedDelta, streamId });
    }
    await callbacks.onReasoningEnd?.({ streamId });
  };

  const closeAllReasoning = async () => {
    const ids = [...openReasoningIds];
    for (const streamId of ids) {
      await closeReasoning(streamId);
    }
  };

  const flushToolInputDelta = async (toolCallId: string) => {
    const bufferedDelta = pendingToolInputDeltas.get(toolCallId);
    if (!bufferedDelta) {
      return;
    }
    pendingToolInputDeltas.delete(toolCallId);
    await callbacks.onToolCallDelta?.({
      inputTextDelta: bufferedDelta,
      toolCallId,
      toolName: openToolCalls.get(toolCallId) ?? "",
    });
  };

  const flushAllToolInputDeltas = async () => {
    for (const toolCallId of pendingToolInputDeltas.keys()) {
      await flushToolInputDelta(toolCallId);
    }
  };

  const result = streamText({
    ...input,
    onChunk: async ({ chunk }) => {
      resetIdleTimer();
      if (chunk.type === "reasoning-delta") {
        resetNoActionTimer();
        sawReasoningDelta = true;
        if (!openReasoningIds.has(chunk.id)) {
          openReasoningIds.add(chunk.id);
          await callbacks.onReasoningStart?.({ streamId: chunk.id });
        }
        const nextDelta = `${pendingReasoningDeltas.get(chunk.id) ?? ""}${chunk.text}`;
        pendingReasoningDeltas.set(chunk.id, nextDelta);
        if (nextDelta.length >= REASONING_DELTA_FLUSH_THRESHOLD_CHARS) {
          pendingReasoningDeltas.delete(chunk.id);
          await callbacks.onReasoningDelta?.({ delta: nextDelta, streamId: chunk.id });
        }
        return;
      }

      if (openReasoningIds.size > 0) {
        await closeAllReasoning();
      }

      if (chunk.type === "text-delta") {
        clearNoActionTimer();
        await callbacks.onTextDelta?.({ delta: chunk.text, streamId: chunk.id });
        return;
      }

      if (chunk.type === "tool-input-start") {
        clearNoActionTimer();
        openToolCalls.set(chunk.id, chunk.toolName);
        await callbacks.onToolCallStarted?.({ toolCallId: chunk.id, toolName: chunk.toolName });
        return;
      }

      if (chunk.type === "tool-input-delta") {
        clearNoActionTimer();
        const nextDelta = `${pendingToolInputDeltas.get(chunk.id) ?? ""}${chunk.delta}`;
        pendingToolInputDeltas.set(chunk.id, nextDelta);
        if (nextDelta.length >= TOOL_CALL_DELTA_FLUSH_THRESHOLD_CHARS) {
          await flushToolInputDelta(chunk.id);
        }
        return;
      }
    },
    abortSignal: idleAbortController.signal,
    timeout: { totalMs: timeoutMs },
  });

  resetNoActionTimer();
  resetIdleTimer();
  try {
    const resultPromise = Promise.all([
      result.text,
      result.sources,
      result.totalUsage,
      result.reasoningText,
      result.toolCalls,
    ]);
    const [text, sources, totalUsage, reasoningText, toolCalls] = noActionPromise
      ? await Promise.race([resultPromise, noActionPromise])
      : await resultPromise;
    await flushAllToolInputDeltas();
    if (!sawReasoningDelta && typeof reasoningText === "string" && reasoningText.trim()) {
      const streamId = "reasoning-final";
      await callbacks.onReasoningStart?.({ streamId });
      await callbacks.onReasoningDelta?.({ delta: reasoningText, streamId });
      await callbacks.onReasoningEnd?.({ streamId });
    }
    return {
      reasoningText,
      sources,
      text: text ?? "",
      toolCalls: extractNativeToolCalls({ toolCalls }),
      totalUsage,
    };
  } finally {
    clearIdleTimer();
    clearNoActionTimer();
    await flushAllToolInputDeltas();
    await closeAllReasoning();
  }
}

export function isRetryableProviderError(error: unknown): boolean {
  if (isProviderNoActionError(error)) {
    return false;
  }
  if (!(error instanceof Error)) {
    return false;
  }
  const normalized = error.message.toLowerCase();
  return (
    error.name === "AbortError" ||
    normalized.includes("fetch failed") ||
    normalized.includes("timed out") ||
    normalized.includes("timeout") ||
    normalized.includes("time-out") ||
    normalized.includes("aborted") ||
    normalized.includes("connection reset") ||
    normalized.includes("econnreset") ||
    normalized.includes("rate limit") ||
    normalized.includes("429") ||
    normalized.includes("500") ||
    normalized.includes("502") ||
    normalized.includes("503") ||
    normalized.includes("504")
  );
}

export function resolveProviderRetryDelayMs(attempt: number, error: unknown): number {
  const normalized = error instanceof Error ? error.message.toLowerCase() : "";
  const multiplier =
    normalized.includes("429") || normalized.includes("rate limit") ? attempt + 2 : attempt + 1;
  return Math.min(BASE_PROVIDER_RETRY_DELAY_MS * multiplier, MAX_PROVIDER_RETRY_DELAY_MS);
}

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/** Extract tool calls from an AI SDK result into our normalized shape. */
function extractNativeToolCalls(result: {
  toolCalls?: AiSdkGenerateTextResult["toolCalls"];
}): Array<{ args: unknown; toolCallId: string; toolName: string }> | undefined {
  if (!result.toolCalls?.length) return undefined;

  return result.toolCalls.map((tc) => {
    if (typeof tc.toolName !== "string" || !tc.toolName.trim()) {
      throw new BestAgentError(
        "PROVIDER_DECISION_INVALID",
        "Native tool call is missing a valid toolName.",
      );
    }

    return {
      args: nativeToolCallInput(tc),
      toolCallId: typeof tc.toolCallId === "string" ? tc.toolCallId : "",
      toolName: tc.toolName,
    };
  });
}

export function decorateProviderError(error: unknown, config: ProviderConfig): Error {
  if (error instanceof BestAgentError) {
    return error;
  }
  const message = error instanceof Error ? error.message : String(error);
  if (isProviderEmptyOutputMessage(message)) {
    return new BestAgentError("PROVIDER_NO_ACTION", message);
  }
  const detailedMessage = APICallError.isInstance(error)
    ? [message, formatProviderApiCallDiagnostics(error)].join("\n\n")
    : message;
  if (isOpenAiCompatibleProviderKind(config.kind) && message.includes("/v1/responses")) {
    const hint =
      resolveOpenAiCompatibleMode(config) === "native"
        ? "The current endpoint may only support /chat/completions. Try compatibilityMode='compatible' or --compatibility compatible."
        : "The current endpoint rejected the OpenAI Responses API path. The runtime can use chat-completions mode with compatibilityMode='compatible'.";
    return new BestAgentError("PROVIDER_REQUEST_FAILED", `${detailedMessage}\n\n${hint}`);
  }

  return new BestAgentError("PROVIDER_REQUEST_FAILED", detailedMessage);
}

function isProviderEmptyOutputMessage(message: string): boolean {
  const normalized = message.toLowerCase();
  return normalized.includes("no output generated") && normalized.includes("stream for errors");
}

function formatProviderApiCallDiagnostics(error: APICallError): string {
  const lines = ["AI SDK provider diagnostics:"];
  if (typeof error.statusCode === "number") {
    lines.push(`- statusCode: ${error.statusCode}`);
  }
  lines.push(`- url: ${error.url}`);
  if (error.responseHeaders && Object.keys(error.responseHeaders).length > 0) {
    lines.push(`- responseHeaders: ${clipProviderDiagnosticValue(error.responseHeaders, 600)}`);
  }
  if (typeof error.responseBody === "string" && error.responseBody.trim()) {
    lines.push(`- responseBody: ${clipProviderDiagnosticValue(error.responseBody, 1_200)}`);
  }
  if (error.data !== undefined) {
    lines.push(`- data: ${clipProviderDiagnosticValue(error.data, 1_200)}`);
  }
  if (error.cause !== undefined) {
    lines.push(`- cause: ${clipProviderDiagnosticValue(error.cause, 800)}`);
  }
  lines.push(`- requestBodyValues: ${clipProviderDiagnosticValue(error.requestBodyValues, 1_200)}`);
  return lines.join("\n");
}

function clipProviderDiagnosticValue(value: unknown, limit: number): string {
  const serialized = serializeProviderDiagnosticValue(value);
  if (serialized.length <= limit) {
    return serialized;
  }
  return `${serialized.slice(0, Math.max(0, limit - 15))}… [truncated]`;
}

function serializeProviderDiagnosticValue(value: unknown): string {
  if (typeof value === "string") {
    return value;
  }
  if (value instanceof Error) {
    return value.stack?.trim() || value.message;
  }
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
}
