import type { EventHandler as H3EventHandler } from "h3"; import type { Task } from "../a2a/types.js"; import { type ActionAutomationContext, type ActionCaller } from "../action.js"; import { fireInternalDispatch } from "../server/self-dispatch.js"; import { type ReasoningEffort } from "../shared/reasoning-effort.js"; import { PROVIDER_TO_ENV } from "./engine/provider-env-vars.js"; import type { AgentEngine, EngineTool, EngineMessage, EngineContentPart } from "./engine/types.js"; import { type Processor } from "./processors.js"; import { subscribeToRun, getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably } from "./run-manager.js"; import type { ActiveRun } from "./run-manager.js"; import { insertRun, isTurnAborted, markRunAborted, updateRunHeartbeat, updateRunStatusIfRunning, setRunError, setRunTerminalReason, claimBackgroundRun, readBackgroundRunClaim, recordRunDiagnostic, countRunsForTurn } from "./run-store.js"; import type { ActionTool, AgentChatAttachment, AgentChatEvent, AgentChatReference, AgentChatStructuredMessage } from "./types.js"; export { PROVIDER_TO_ENV }; /** * Grace window + poll interval for the foreground circuit-breaker that confirms * a background worker actually CLAIMED a 202-dispatched run before recovering * inline. The grace must cover the worker's cold-start + per-request init before * it reaches `claimBackgroundRun`: light apps win the claim in ~1-2s, but heavy * apps (e.g. analytics) were observed in prod taking >8s, so an 8s grace made * their worker lose the race every time and always fall back to inline (adding * ~8s latency with no background budget). 15s covers the slow apps while staying * well within the foreground's ~40s soft-timeout. */ export declare const BACKGROUND_CLAIM_GRACE_MS = 15000; /** * Safety margin subtracted from the unclaimed-reaper grace when deciding how * long the foreground may keep waiting for a slow-but-live worker to claim. The * foreground recovers the run inline this many ms BEFORE `reapUnclaimedBackgroundRun` * would error an unclaimed row, so the foreground always wins the race to claim * and the two never collide — see `resolveBackgroundDispatchOutcome`. */ export declare const BACKGROUND_REAPER_SAFETY_MARGIN_MS = 2000; export declare const BACKGROUND_CLAIM_POLL_MS = 400; export type BackgroundDispatchOutcome = { action: "stream"; } | { action: "subscribe"; } | { action: "inline"; reason: "dispatch-failed" | "worker-never-claimed" | "no-row"; }; /** * Decide what the foreground should do after attempting a durable background * dispatch. A Netlify async background function returns 202 the instant it * ENQUEUES the invocation — that is NOT proof the worker executed. If the * generated wrapper fails to import/hand off to the route, the worker never * reaches `claimBackgroundRun` and the run is reaped as "worker never claimed". * * So after a successful dispatch we poll briefly for the worker to CLAIM the run: * - claimed within grace → "stream" (subscribe to the worker) * - dispatch failed OR no claim → recover inline by atomically claiming the * run ourselves: if we win → "inline"; if a (delayed) worker already won * it → "subscribe" (never double-run). * * Pure except for the injected `readClaim`/`claim`/`now`/`sleep` deps, so each * branch is unit-testable. */ export declare function resolveBackgroundDispatchOutcome(opts: { dispatched: boolean; backgroundRowInserted: boolean; runId: string; graceMs: number; /** * The unclaimed-run reaper's grace (`UNCLAIMED_BACKGROUND_RUN_GRACE_MS`). When * provided, the foreground may keep waiting PAST `graceMs` while the worker is * provably alive and still in setup — but it recovers inline before the run has * been unclaimed this long (minus the safety margin), so it always claims * before the reaper can fire. Omit to disable the extension (behaves exactly * like the base grace). */ reaperGraceMs?: number; /** Margin subtracted from `reaperGraceMs` (default `BACKGROUND_REAPER_SAFETY_MARGIN_MS`). */ reaperSafetyMarginMs?: number; pollIntervalMs: number; readClaim: (runId: string) => Promise<{ dispatchMode: string | null; status: string | null; diagStage?: string | null; /** COALESCE(heartbeat_at, started_at) — the reaper's liveness basis. */ lastLivenessAt?: number | null; } | null>; claim: (runId: string) => Promise; now?: () => number; sleep?: (ms: number) => Promise; }): Promise; /** * Look up a user's persisted API key for the given provider. Returns * `undefined` for unauthenticated callers. * * Read order: * 1. `app_secrets` — encrypted user override, then active org/workspace. * 2. Legacy `user-api-key::` settings row — pre-migration * data that hasn't been backfilled yet. Surfaced for compat only; * writes always go to app_secrets now. */ export declare function getOwnerApiKey(provider: string, ownerEmail: string | null | undefined): Promise; /** * Derive the provider name from the active engine setting. * "ai-sdk:openai" → "openai", "anthropic" → "anthropic" */ export declare function engineToProvider(engineName: string): string; /** * Resolve the active engine's provider and look up the user's API key for it. * * If the owner has no scoped key, fall back to provider keys supplied by the * hosting environment only when the current request can safely use deploy-level * credentials. This is a read from process-level config, not a request-scoped * write to `process.env`. * * Callers that layer another deployment-key fallback after this should keep the * same precedence: scoped key first, host-provided env key second. */ export declare function getOwnerActiveApiKey(ownerEmail: string | null | undefined): Promise; /** @deprecated Use getOwnerApiKey("anthropic", ownerEmail) instead */ export declare function getOwnerAnthropicApiKey(ownerEmail: string | null | undefined): Promise; /** * Context passed as the optional second argument to an action's `run`. * Defined in `../action.js` (cycle-free home) and re-exported here so existing * importers (e.g. `scripts/call-agent.ts`) keep their import path. */ export type { ActionRunContext, ActionCaller } from "../action.js"; export interface ActionEntry { tool: ActionTool; run: (args: any, context?: import("../action.js").ActionRunContext) => Promise | any; /** Standard Schema input validator when declared through defineAction. */ schema?: unknown; /** HTTP exposure config. `false` = agent-only. Omitted = auto-inferred from name. */ http?: import("../action.js").ActionHttpConfig | false; /** Whether HTTP/frontend action calls must have an authenticated owner. * Defaults to true; false lets safe metadata/read actions run with * `ctx.userEmail` undefined when auth resolution returns 401/403. */ requiresAuth?: boolean; /** Max HTTP request body in bytes; the route 413s on `Content-Length` before * parsing. For public, no-auth POST actions. */ maxBodyBytes?: number; /** Whether the action is exposed to the agent as a callable tool. Only an * explicit `false` hides it from every agent tool surface (in-app assistant, * MCP, A2A, job/trigger runners) while leaving it frontend/HTTP-callable. * Set by `defineAction`'s `agentTool` option. */ agentTool?: boolean; /** Explicit opt-in metadata for public agent protocols. Public routes never * imply public tool exposure; MCP/A2A/OpenAPI surfaces must filter for this. */ publicAgent?: import("../action.js").PublicAgentActionConfig; /** If true, completion does NOT trigger a screen-refresh change event. * Set automatically by `defineAction` when `http.method === "GET"`. */ readOnly?: boolean; /** False keeps a read-only tool available in Act mode but hides/blocks it in * Plan mode. Use for tools that perform substantive work even without * mutating state. */ allowInPlanMode?: boolean; /** If true, this action can run concurrently with other same-turn * read-only/parallel-safe tool calls. Only use for actions that handle * their own write ordering and idempotency. */ parallelSafe?: boolean; /** Set false to exempt a read-only tool from the duplicate read-only * tool-call guard (per-turn result cache + repeat-kill). Default true. Use * for volatile/polling reads that are expected to return a different * result on each identical call. See `defineAction`'s `dedupe` option. */ dedupe?: boolean; /** Whether this action may be invoked from the tools-iframe bridge. * **Default-allow opt-out**: only an explicit `false` returns 403. * - `true` / `undefined` — allow. * - `false` — explicit deny; the tools bridge returns 403. * See `defineAction` (`packages/core/src/action.ts`) and audit H5 in * `security-audit/05-tools-sandbox.md`. */ toolCallable?: boolean; /** Optional deep-link builder. When set, MCP/A2A surfaces append an * "Open in →" link built from the call's args + result. Pure, sync, * best-effort. See `defineAction` and the `external-agents` skill. */ link?: import("../action.js").ActionLinkBuilder; /** Optional MCP Apps UI resource for hosts that support inline interactive * app iframes. CLI/non-UI hosts still receive the normal tool result and * any deep link from `link`. */ mcpApp?: import("../action.js").ActionMcpAppConfig; /** Optional native Agent-Native chat renderer for this action's result. */ chatUI?: import("../action-ui.js").ActionChatUIConfig; /** * Per-tool timeout override in milliseconds. When set, the agent loop uses * this value instead of the global TOOL_TIMEOUT_MS (60 s) for this action. * Useful for long-running tools such as sandboxed code execution. */ timeoutMs?: number; /** * Per-tool max-result-chars override. When set, the agent loop truncates * the result to this many characters instead of the global 50 000 cap. */ maxResultChars?: number; /** * Opt-in human-in-the-loop approval gate (default off). When truthy (or a * predicate that resolves truthy for the call's args), the loop emits * `approval_required` and stops the turn instead of executing this action, * until a human approves the specific call. Set by `defineAction`'s * `needsApproval` option. See `packages/core/docs/content/actions.mdx`. */ needsApproval?: boolean | ((args: any, ctx?: import("../action.js").ActionRunContext) => boolean | Promise); } /** @deprecated Use `ActionEntry` instead */ export type ScriptEntry = ActionEntry; export type AgentExecutionMode = "act" | "plan"; export declare const PLAN_MODE_SYSTEM_PROMPT = "## Plan Mode Active\n\nYou are in Plan mode. This turn is for research, clarification, and a proposed approach only.\n\nHard rules:\n- Use only read-only tools. Do not edit files, write resources, run mutating bash commands, mutate SQL rows, navigate the UI, send notifications, create jobs, create tools, call external agents, or change external systems.\n- If a needed detail is unclear, ask a concise clarifying question before proposing a plan.\n- When ready, present a concrete plan with the files/tools you expect to touch, the intended changes, validation steps, and notable risks.\n- Do not treat approval as implicit while Plan mode is still active. Tell the user to switch to Act mode with the mode selector or /act before implementation."; export declare function isPlanModeToolCallAllowed(name: string, input: unknown, entry: ActionEntry): boolean; export declare function createPlanModeActionRegistry(actions: Record): Record; export interface ProductionAgentOptions { /** Action entries for the agent. Use `actions` (preferred) or `scripts` (deprecated alias). */ actions?: Record; /** @deprecated Use `actions` instead */ scripts?: Record; /** Static system prompt string, or async function called per-request with the H3 event */ systemPrompt: string | ((event: any) => string | Promise); /** Falls back to ANTHROPIC_API_KEY env var. Ignored when `engine` is provided. */ apiKey?: string; /** Agent engine to use. Defaults to the "anthropic" engine. */ engine?: AgentEngine | string | { name: string; config: Record; }; /** Model to use. Defaults to the resolved engine's default model. */ model?: string; /** App/template id used for org-scoped per-app model defaults. */ appId?: string; /** Default reasoning effort for requests that do not supply an override. */ reasoningEffort?: ReasoningEffort; /** Provider-specific options passed through to the engine */ providerOptions?: EngineMessage extends never ? never : any; /** Called when a run completes (for server-side thread persistence) */ onRunComplete?: (run: ActiveRun, threadId: string | undefined) => void; /** Called after request validation but before a run is started. */ onRunPrepared?: (details: { runId: string; threadId: string | undefined; message: string; attachments?: AgentChatAttachment[]; }) => void | Promise; /** * Optional per-template request normalizer. Runs after owner resolution and * before system/context assembly so templates can materialize uploaded chat * attachments or append app-specific, non-visible instructions. */ prepareRequest?: (details: { event: any; ownerEmail: string | null; message: string; displayMessage?: string; attachments: AgentChatAttachment[]; references: AgentChatReference[]; threadId?: string; internalContinuation?: boolean; mode: AgentExecutionMode; }) => void | { message?: string; displayMessage?: string; attachments?: AgentChatAttachment[]; } | Promise; /** Optional per-app agent run chunk budget in milliseconds. Defaults to * AGENT_RUN_SOFT_TIMEOUT_MS when set, otherwise no framework-imposed * timeout. When reached, the client receives an internal auto-continuation * signal instead of a user-facing warning. */ runSoftTimeoutMs?: number; /** Optional no-progress watchdog override for this app's runs. */ runNoProgressTimeoutMs?: number; /** * Opt this app into durable Netlify background-function agent-chat runs. This * is a runtime opt-in layered on top of the hosted-runtime + A2A_SECRET gates; * single-template Netlify deploys must also enable the deploy-time * `AGENT_CHAT_DURABLE_BACKGROUND` flag so the background function is emitted. */ durableBackgroundRuns?: boolean; /** Called when a run starts, with the send function for emitting events and the threadId */ onRunStart?: (send: (event: AgentChatEvent) => void, threadId: string) => void | Promise; /** * Called after the engine + model are resolved for this request. Used by * the plugin layer to thread the parent's choices into sub-agents so * delegated tasks don't default back to Anthropic + Claude. */ onEngineResolved?: (engine: AgentEngine, model: string) => void; /** Resolve the owner email from the H3 event (for usage tracking) */ resolveOwnerEmail?: (event: any) => string | Promise; /** * Optional final-answer guard. If it returns a message after a text-only * assistant turn, the loop clears that draft once and asks the model to * continue with the returned corrective instruction before allowing a final. */ finalResponseGuard?: AgentLoopFinalResponseGuard; /** * Skip auto-injecting the workspace files/skills/agents inventory on the * first message of a conversation. Useful for minimal/voice apps where * the ~2KB inventory of unrelated resources is noise, not signal. * Default: false (inventory is injected). */ skipFilesContext?: boolean; /** * Optional starter tool catalog. When set, the first model request includes * only these tool schemas plus `tool-search`; the full action registry remains * searchable, and matching tool schemas from `tool-search` results are added * to the next model request. This keeps first-token latency low without * forcing rarely used capabilities into every prompt. The framework also * promotes common provider/corpus/code-execution tools when the current * app/mode registry exposes them, so prompts that teach broad integrations do * not describe tools that require an extra discovery turn before use. */ initialToolNames?: string[]; /** * App-level default tool limits. Each action's own `timeoutMs` / * `maxResultChars` takes precedence; this sets the fallback for actions * that don't declare their own limits. */ toolLimits?: { timeoutMs?: number; maxResultChars?: number; }; } export declare function resolveAgentOwnerEmail(options: Pick, event: any): Promise; declare function generateRunId(): string; /** Check if an error is transient and should be retried * @internal exported for unit tests only */ export declare function isContextTooLongError(err: unknown): boolean; /** @internal exported for unit tests only */ export declare function isRetryableError(err: unknown): boolean; /** * Attempt one aggressive trim of old tool-result content to reduce the * context window usage. Tool-result messages older than the last * {@link CONTEXT_TRIM_KEEP_TAIL} messages have their text replaced with a * short stub. All user/assistant text messages and the recent tail are * preserved exactly. * * Returns a new array — the original is not mutated. * Returns `null` when there are no trimable tool results (so the caller can * skip the retry). */ export declare function trimOldToolResults(messages: EngineMessage[], keepTail?: number): EngineMessage[] | null; /** * Wall-clock left in this invocation's soft-timeout budget, measured from * `startedAt`. `Infinity` off the hosted soft-timeout regime (local dev, * self-hosted), where runs are genuinely unbounded. Uses the same ceiling * `startRun` sizes the round to, so budget math here can't disagree with the * soft timeout that actually aborts the run. */ export declare function remainingRunBudgetMs(startedAt: number): number; export declare function buildUserContentWithAttachments(opts: { text: string; attachments?: AgentChatAttachment[]; }): EngineContentPart[]; export declare function structuredHistoryToEngineMessages(history: AgentChatStructuredMessage[] | undefined): EngineMessage[] | null; export declare function resolveSkillReferenceContent(ref: AgentChatReference): Promise; export declare function createConnectedAgentReferenceEventRelay(input: { agent: string; send: (event: AgentChatEvent) => void; agentCallId?: string; now?: () => number; }): { agentCallId: string; start(): void; observeActivity: (task: Task) => void; observePollUpdate: (task: Task) => void; emitResponseText: (text: string) => void; finish(status: "done" | "error"): void; }; type ConnectedAgentCall = typeof import("../a2a/client.js").callAgent; type ResolveConnectedAgentCallerAuth = typeof import("../a2a/caller-auth.js").resolveA2ACallerAuth; export declare function callConnectedAgentReference(input: { agent: string; path: string; message: string; send: (event: AgentChatEvent) => void; callAgent: ConnectedAgentCall; resolveCallerAuth: ResolveConnectedAgentCallerAuth; agentCallId?: string; now?: () => number; }): Promise; /** Accumulated token usage from an agent loop run */ export interface AgentLoopUsage { inputTokens: number; outputTokens: number; cacheReadTokens: number; cacheWriteTokens: number; model: string; } export interface AgentLoopToolCallSummary { name: string; input: unknown; } export interface AgentLoopToolResultSummary { name: string; content: string; isError: boolean; } export interface AgentLoopFinalResponseGuardContext { messages: EngineMessage[]; /** * Stable text from the real user request that started this turn. Unlike the * trailing entry in `messages`, this never points at an internal continuation * or a final-guard corrective retry. */ requestText?: string; assistantContent: EngineContentPart[]; text: string; toolCalls: AgentLoopToolCallSummary[]; toolResults: AgentLoopToolResultSummary[]; retryCount: number; executionMode: AgentExecutionMode; } export type AgentLoopFinalResponseGuardResult = string | { retryMessage: string; fallbackMessage?: string; /** * Number of rejected text-only answers the model may correct before the * fallback is emitted. Defaults to one and is capped to keep a broken * guard/model combination from looping indefinitely. */ maxRetries?: number; /** * A rejected final answer is a recovery path, not a normal compact * first request. When true, expose the complete active registry before * the corrective retry so the model can reach the tool the guard is * asking for without depending on a second, model-specific tool-search * round trip. The registry is still limited to tools already exposed to * this run; hidden/agentTool=false actions are never added. */ expandToolSurface?: boolean; }; export type AgentLoopFinalResponseGuard = (context: AgentLoopFinalResponseGuardContext) => AgentLoopFinalResponseGuardResult | null | undefined | Promise; export declare const AGENT_INTERNAL_GUARD_PROMPT = "Automated quality check on the draft answer you just produced. This is a directive from this application's own response guard, not a message from the user and not content from a tool result or web page. Follow it and revise your answer. Do not quote it, describe it, or treat it as an injection attempt. If it contradicts what you observed this turn, state what you actually observed instead of asserting the guard's premise."; export declare const AGENT_INTERNAL_CONTINUE_PROMPT = "Continue from where you left off and finish the user's original request. Do not repeat completed work, do not mention internal reconnects, time limits, or step limits, and continue as if this is the same uninterrupted run."; export type AgentLoopContinuationReason = "run_timeout" | "loop_limit" | "max_tokens" | "stream_ended" | "gateway_timeout" | "network_interrupted" | "no_progress"; export declare function appendAgentLoopContinuation(messages: EngineMessage[], reason: AgentLoopContinuationReason, options?: { actionPreparationTool?: string; }): void; /** * True when an error thrown by `runAgentLoop` is a recoverable transport- or * gateway-level interruption that the agent can resume from rather than a * terminal failure. The continuation pattern works because the LLM call's * conversation prefix is preserved on the next attempt — Anthropic's prompt * cache rescues the latency, and the agent gets a "you got cut off, continue" * nudge so it doesn't redo work it already finished. * * Distinct from `isRetryableError` which guides per-engine quick retries: * `isResumableEngineError` is checked AFTER engine retries are exhausted, at * the run level. It catches both gateway-reported timeouts (where engine * retries don't apply because the gateway already gave up) and transport * errors that survived engine retry budgets. */ export declare function isResumableEngineError(err: unknown): boolean; /** * Map a resumable error to the most descriptive continuation reason. Used * when surfacing the resume to the agent and to clients via the * `auto_continue` event. */ export declare function continuationReasonForResumableError(err: unknown): "gateway_timeout" | "network_interrupted"; /** Resolve the real user request behind any internal continuation messages. */ export declare function resolveFinalResponseGuardRequestText(messages: EngineMessage[]): string | undefined; /** * Whether a cached read-only tool result is still something the model can * actually see in `contextMessages` — the trimmed/summarized view the engine * is streamed (NOT the raw, ever-growing `messages` array the cache is keyed * off of). Context-xray `evict` drops tool-result parts entirely, `summarize` * replaces their content with a placeholder, and observational-memory * trimming drops whole older messages once active. When the cached result * has fallen out of that view, re-serving "use the previous result" is not * actionable — the model has nothing to point back to. * * A visible result must belong to the same tool name + normalized input and * contain either the exact cached body or the exact wrapper used when that * body was re-served after trimming. Matching only by a content suffix is too * loose: short results such as "ok" can also end unrelated tool output. */ export declare function isCachedToolResultVisibleInContext(contextMessages: EngineMessage[], toolCall: { name: string; input: unknown; }, cachedResult: string): boolean; /** * Identical (tool, arguments) invocations tolerated in one turn before the turn * is stopped, whether or not they errored. * * This is the guard that distinguishes a spiral from deep work. Volume ceilings * cannot: prod contains a legitimate 117-tool-call analysis alongside turns that * issued 39 identical `run-code` webFetches and 43 identical `docs-search` * calls. Repetition is what separates them, so repetition is what we bound — * leaving genuinely long analyses free to keep making NEW calls. * * Above `MAX_IDENTICAL_TOOL_ERRORS` (3) because a repeated SUCCESS is weaker * evidence of a loop than a repeated failure: a few identical reads can be a * legitimate re-check after a write. */ export declare const MAX_IDENTICAL_TOOL_CALLS = 8; /** * Convert ActionEntry registry to EngineTool array. */ export declare function actionsToEngineTools(actions: Record): EngineTool[]; export type AgentToolCallExecutionResult = { status: "completed"; output: string; completedSideEffect?: boolean; } | { status: "approval_required"; output: string; approvalKey: string; } | { status: "failed"; output: string; }; export interface ExecuteAgentToolCallOptions { actions: Record; name: string; input?: unknown; callId: string; signal?: AbortSignal; ownerEmail?: string | null; orgId?: string | null; /** Audit/action attribution for this externally selected call. */ caller?: ActionCaller; networkProtocol?: "a2a" | "mcp" | "provider-api"; networkId?: string; networkPeer?: string; threadId?: string; turnId?: string; approvedToolCalls?: string[]; send?: (event: AgentChatEvent) => void; } /** * Execute one externally-selected tool through the exact same guarded runtime * as a normal model-selected tool call. Realtime voice and other duplex * transports use this instead of calling `ActionEntry.run` directly, which * would bypass approvals, schema validation, timeouts, the tool journal, * result redaction, mutation ordering, and refresh notifications. */ export declare function executeAgentToolCall(options: ExecuteAgentToolCallOptions): Promise; export declare function filterInitialEngineTools(tools: EngineTool[], initialToolNames?: string[]): EngineTool[]; export declare function buildFirstRequestPayloadDetail(input: { isFirstRequest: boolean; systemPrompt: string; messages: EngineMessage[]; tools: EngineTool[]; availableToolCount: number; }): string; export declare function toolCallCacheKey(toolName: string, input: unknown): string; export declare function repeatedSourceSweepGuardMessage(opts: { toolName: string; priorCalls: number; threshold?: number; scope?: "tool" | "aggregate"; }): string; export declare function shouldGuardRepeatedSourceSweep(opts: { toolName: string; entry: ActionEntry | undefined; priorToolCalls: readonly AgentLoopToolCallSummary[]; actions?: Record; threshold?: number; }): { toolName: string; priorCalls: number; message: string; } | null; /** * The core agent loop — calls the engine iteratively until no more tool calls. * Decoupled from HTTP transport so it can run in the background. * Returns accumulated token usage for cost tracking. */ export declare function runAgentLoop(opts: { engine: AgentEngine; model: string; systemPrompt: string; tools: EngineTool[]; availableTools?: EngineTool[]; messages: EngineMessage[]; systemSections?: import("../shared/context-xray.js").ContextManifestSystemSection[]; actions: Record; send: (event: AgentChatEvent) => void; signal: AbortSignal; /** Observe usage as each model response reports it, including aborted loops. */ onUsage?: (usage: AgentLoopUsage) => void; ownerEmail?: string | null; orgId?: string | null; /** Action invocation attribution. Defaults to the normal agent tool loop. */ actionCaller?: ActionCaller; /** Trusted trigger lineage for automation-dispatched action calls. */ automation?: ActionAutomationContext; /** Concrete execution id used for cross-app trace correlation. */ runId?: string; /** * Wall-clock anchor for run-budget math (engine-retry backoff, in-process * resume). Defaults to loop entry; a continuation wrapper passes the round's * start so later attempts don't each believe they have a fresh budget. */ budgetStartedAt?: number; /** Verified/telemetry-only delegated lineage supplied by the transport. */ networkProtocol?: "a2a" | "mcp" | "provider-api"; networkId?: string; networkPeer?: string; /** * Attachments submitted with this turn (pasted text, files, images), passed * through to each tool's `ActionRunContext.attachments` so actions can * consume a large pasted artifact by reference instead of having the model * re-emit it as a tool argument. See `create-extension`'s * `contentFromAttachment`. */ attachments?: AgentChatAttachment[]; reasoningEffort?: ReasoningEffort; providerOptions?: any; maxOutputTokens?: number; executionMode?: AgentExecutionMode; maxIterations?: number; /** * Per-TURN input-token ceiling. Iteration count and wall clock do not bound * spend — retained tool results are re-sent every iteration, so cost is * quadratic in tool-call count. Crossing this stops the turn outright * (unlike `maxIterations`, which hands off to the next chunk). */ maxRunInputTokens?: number; /** * Input tokens already consumed by EARLIER chunks of this same logical turn. * Without it every chained chunk would get a fresh allowance and 25 chunks * would multiply the ceiling by 25. */ priorTurnInputTokens?: number; finalResponseGuard?: AgentLoopFinalResponseGuard; /** * Stable real-user request text preserved across internal continuation * attempts. Callers normally omit this; continuation wrappers freeze it * before appending their synthetic user messages. */ finalResponseGuardRequestText?: string; threadId?: string; turnId?: string; /** * App-level default limits applied to every tool call unless the individual * ActionEntry overrides them with its own timeoutMs / maxResultChars. */ toolLimits?: { timeoutMs?: number; maxResultChars?: number; }; /** * Stable approval keys granted by a human for actions declared * `needsApproval`. A call whose key is present here runs even though the * action requires approval; otherwise the loop pauses with * `approval_required`. See `AgentChatRequest.approvedToolCalls`. */ approvedToolCalls?: string[]; /** * In-loop processor seam (see `processors.ts`). Each processor can observe * streamed chunks, observe model responses around tool execution, and * `abort()` the run. Loop-internal config, NOT a tool/authoring surface — * processors only observe/mutate-stream/abort; they never define app * behavior or replace actions. When omitted or empty, none of the seam code * runs and the loop is byte-for-byte unchanged (zero overhead). */ processors?: Processor[]; }): Promise; export declare function lastUnfinishedPreparingActionToolFromEvents(events: readonly AgentChatEvent[]): string | undefined; export declare function backgroundContinuationReasonForRun(run: ActiveRun): AgentLoopContinuationReason; export declare function runAgentLoopWithMainChatInternalContinuations(opts: Parameters[0] & { /** * Resume a thrown `isResumableEngineError` (gateway drop / transport * interruption surviving engine retries) in-process UNCONDITIONALLY — the * same "continue from where you left off" treatment applied to in-loop * `auto_continue` events below. * * Set it only for a proven durable-background worker * (`isInBackgroundFunctionRuntime()`), which has minutes of budget left on * this invocation AND cannot fall back to `chainServerDrivenContinuation` * (a background function cannot invoke its own public URL from inside a * live invocation — that self-dispatch 404s). * * When omitted, the resume still happens but only while enough wall-clock * remains (`SELF_CHAIN_MIN_CONTINUATION_BUDGET_MS`); below that the error * propagates to `startRun`'s outer catch and the cross-invocation recovery * paths (foreground self-chain, client `auto_continue` re-POST) as before. */ resumeResumableErrorsInProcess?: boolean; /** * Override for the per-invocation continuation attempt cap. Defaults to * `MAIN_CHAT_INTERNAL_CONTINUATION_LIMIT` (6) when omitted — unchanged for * every existing caller. A proven background-function worker (15-min * budget) can pass a higher cap (e.g. `MAX_BACKGROUND_RUN_CONTINUATIONS`) * so a run that needs several resumable-error recoveries within one * 13-minute chunk isn't cut off at the foreground-sized limit. */ maxContinuations?: number; }): Promise>>; /** * Hard cap on server-driven background→background continuation chunks for a * single logical turn. A `backgroundFunction` run gets a ~13-min soft timeout, * so reaching this boundary at all is the rare exception (most turns finish in * one chunk). The cap bounds a pathological turn that would otherwise chain * background invocations forever, mirroring `MAX_AGENT_TEAM_CONTINUATIONS`. */ export declare const MAX_BACKGROUND_RUN_CONTINUATIONS = 20; /** * Whether this run should self-fire the next server-driven continuation chunk * instead of depending on the client to re-POST `auto_continue`. True for * either of two independently-gated cases, both requiring a recoverable * unfinished boundary (not aborted/stopped) and a chain still under its * budget: * - a durable-background WORKER run (`isBackgroundWorker`) — unconditional, * unchanged from before; or * - a FOREGROUND run when the resolved `foregroundSelfChainEligible` gate is * true (see `isAgentChatForegroundSelfChainEnabled` in * `durable-background.ts`) AND this specific run was never dispatched to * the durable background worker (`dispatchedToBackground` false) — a run * already headed to the durable background path chains via the * `isBackgroundWorker` branch above, never both. * Aborted / user-stopped runs do NOT chain either way. */ export declare function shouldChainBackgroundContinuation(opts: { isBackgroundWorker: boolean; run: ActiveRun; continuationCount: number; /** * Opt-in: allow a FOREGROUND (non-background-worker) run to also * self-chain server-side. Default false — preserves the exact prior * behavior (foreground runs never chain; the client's `auto_continue` * re-POST is the only continuation path) when omitted. */ foregroundSelfChainEligible?: boolean; /** * True when this run was dispatched to the durable background worker * (`dispatchToBackground` in the handler). When true, foreground self-chain * is never eligible — the run either IS the background worker (chains via * `isBackgroundWorker`) or is a foreground POST whose recovery is already * owned by the background circuit-breaker, not this path. */ dispatchedToBackground?: boolean; }): boolean; /** * Minimum remaining budget (ms) a synchronous self-chain continuation chunk * must have left — after subtracting the wall-clock this invocation has * ALREADY spent on its own pre-`startRun` setup (auth, DB reads, marker * validation) — before it is safe to let it start real agent-loop work at * all. Below this, even a reduced soft-timeout risks the run making an LLM * call that then gets cut off by Netlify's ~60s synchronous-function hard * kill mid-stream instead of checkpointing cleanly — exactly the "run wedged * until stale-run recovery" failure `resolveSelfChainContinuationBudget` * hardens against. 8s leaves enough runway for a small model round trip (or * the soft-timeout timer itself) to still land cleanly before the hard wall. */ export declare const SELF_CHAIN_MIN_CONTINUATION_BUDGET_MS = 8000; /** Result of `resolveSelfChainContinuationBudget`. */ export interface SelfChainContinuationBudget { /** * True when there isn't enough wall-clock left in THIS invocation to * safely start real agent-loop work. The caller should skip straight to a * synthetic `run_timeout` boundary (the same one a normal soft-timeout * produces) instead of invoking the loop, so the existing continuation * machinery (`chainServerDrivenContinuation` / the client's `auto_continue` * re-POST fallback) hands off to a fresh invocation instead of risking a * mid-stream kill. */ skipToBoundary: boolean; /** * The soft-timeout budget to pass into `startRun` when NOT skipping — the * chunk's normal ceiling minus wall-clock already spent on setup. Always * `<= chunkCeilingMs` and `>= minContinuationBudgetMs` when `skipToBoundary` * is false. */ softTimeoutMs: number; } /** * Budgets a synchronous (non-`-background`-function) self-chain continuation * chunk against the wall-clock ALREADY spent on this invocation's own setup * before `startRun` is ever called, instead of handing it a fresh * `chunkCeilingMs` window that ignores that setup cost entirely. * * Root cause this hardens against: when `AGENT_CHAT_FOREGROUND_SELF_CHAIN` is * enabled, a continuation is dispatched to the `_process-run` route running as * a REGULAR synchronous serverless function (hard ~60s wall on Netlify). The * handler burns setup time (DB reads, auth, marker validation) before ever * calling `startRun()`, which — unaware of that elapsed time — grants a fresh * ~40s run ceiling. Total handler time (setup + a fresh 40s) can then exceed * the 60s wall, so the function is killed mid-stream instead of checkpointing, * and the run sits "active" until stale-run recovery (~90s later) instead of * cleanly handing off. * * Pure function — no I/O, unit-testable in isolation. */ export declare function resolveSelfChainContinuationBudget(elapsedSinceHandlerEntryMs: number, chunkCeilingMs: number, minContinuationBudgetMs?: number): SelfChainContinuationBudget; export declare function markBackgroundContinuationChunkTerminal(opts: { runId: string; continuationReason: AgentLoopContinuationReason; terminalEvent?: AgentChatEvent; deps?: { updateRunStatusIfRunning?: typeof updateRunStatusIfRunning; setRunError?: typeof setRunError; setRunTerminalReason?: typeof setRunTerminalReason; }; }): Promise; export declare function claimBackgroundWorkerRunEarly(opts: { runId: string; threadId?: string | null; markerTurnId?: string | null; requestTurnId?: string | null; continuationCount: number; runsInBackgroundFunction: boolean; backgroundRuntimeDetail?: string; deps?: { recordRunDiagnostic?: typeof recordRunDiagnostic; insertRun?: typeof insertRun; claimBackgroundRun?: typeof claimBackgroundRun; updateRunHeartbeat?: typeof updateRunHeartbeat; isTurnAborted?: typeof isTurnAborted; markRunAborted?: typeof markRunAborted; }; }): Promise<{ claimed: true; } | { claimed: false; skipped: string; }>; /** * Wall-clock ceiling on a single logical turn. The run-count ledger alone is * not a time bound: in durable mode each of the ~25 permitted chunks may burn * ~780s, so the ledger's real worst case is over five hours (production has an * observed 2h34m turn). Nobody is waiting that long, and every minute past * this point is spend on a request the user has abandoned. */ export declare const MAX_TURN_WALL_CLOCK_MS: number; /** * Request-body field carrying the turn's running input-token total across * chunks. It rides the BODY (not the background-run marker) because the marker * is stripped from `continuationBody` on every handoff while the body is the * successor's persisted rehydration payload. */ export declare const AGENT_CHAT_TURN_INPUT_TOKENS_FIELD = "__agentNativeTurnInputTokens"; /** * First `started_at` for a logical turn — the turn's true wall-clock origin * (`agent_runs.started_at` is never bumped by heartbeats). Returns null when * the turn has no rows or the read fails, in which case the deadline simply * does not apply and the run-count ledger remains the only bound. * * Costs one extra round trip per chain. Folding `MIN(started_at)` into * `countRunsForTurn`'s existing SELECT in run-store.ts would remove it. */ declare function readTurnStartedAt(threadId: string, turnId: string): Promise; /** * Append a user-visible assistant text event to a live run from outside the * agent loop. The budget-exhaustion paths below terminate a turn that has * already done real work; without this the user sees only a bare * `turn_continuation_budget_exhausted` error string and every completed tool * result looks discarded. Best-effort — a failed ledger write must not turn a * budget stop into a thrown error. */ declare function emitRunText(run: ActiveRun, text: string): Promise; /** Injectable dependencies for `chainServerDrivenContinuation` (unit tests). */ export interface ChainServerDrivenContinuationDeps { countRunsForTurn?: typeof countRunsForTurn; readTurnStartedAt?: typeof readTurnStartedAt; isTurnAborted?: typeof isTurnAborted; emitRunText?: typeof emitRunText; insertRun?: typeof insertRun; fireInternalDispatch?: typeof fireInternalDispatch; readBackgroundRunClaim?: typeof readBackgroundRunClaim; updateRunHeartbeat?: typeof updateRunHeartbeat; updateRunStatusIfRunning?: typeof updateRunStatusIfRunning; markRunAborted?: typeof markRunAborted; setRunTerminalReason?: typeof setRunTerminalReason; recordRunDiagnostic?: typeof recordRunDiagnostic; markBackgroundContinuationChunkTerminal?: typeof markBackgroundContinuationChunkTerminal; generateRunId?: typeof generateRunId; sleep?: (ms: number) => Promise; } /** Retry-budget sizing for one `chainServerDrivenContinuation` dispatch. */ export interface ContinuationDispatchBudget { maxDispatchAttempts: number; dispatchResponseTimeoutMs: number; /** Cap (ms) on the per-gap exponential backoff. `Infinity` preserves the * original uncapped `500 * 2 ** attempt` schedule for the two budgets * that predate this cap (their attempt counts are low enough it never * mattered); only the larger proven-in-background-function budget uses a * real cap so its extra attempts don't add unbounded backoff. */ backoffCapMs: number; } /** * Sizes the dispatch retry budget by *remaining wall-clock capacity*, not by * dispatch TARGET — the two are independent concerns. `chainViaDurableBackground` * only picks where the dispatch goes (see `continuationDispatchPath` above); * this picks how hard to retry getting it there. * * Three cases: * - `chainViaDurableBackground === true`: the durable worker chain dispatching * to the Netlify background function url. Unchanged: 3 attempts / 15s. * - `chainViaDurableBackground === false` and * `workerProvenInBackgroundFunction === true`: a worker PROVEN (by runtime * function name, see `shouldUseBackgroundFunctionTimeoutForWorker`) to * already be executing inside a real `-background` function — it was * forced onto the regular-function dispatch target only because a * background function cannot invoke its own URL from within a live * invocation (see the caller's `&& !runsInBackgroundFunction` comment), * NOT because it lacks time or a connected-client fallback. This worker * has up to `BACKGROUND_SOFT_TIMEOUT_CEILING_MS` (13min) of soft-timeout * budget behind it and up to Netlify's ~15min hard function limit ahead — * roughly 2 minutes of remaining wall clock — and, being a background * worker, NO connected client to fall back on if the handoff is dropped. * Demoting it to the foreground's 2-attempt/10s budget is exactly the bug * this type documents: 5 attempts / 15s response timeout, with backoff * capped at 4s/gap (500ms, 1s, 2s, 4s across the 4 gaps ≈ 7.5s total). * Worst case: 5 × 15s dispatch + ~7.5s backoff ≈ 82.5s, safely inside the * ~2min of remaining budget before the function's hard kill. * - Otherwise: a true foreground caller not proven in a background * function. Unchanged: 2 attempts / 10s — it has a live connected client * as a fallback (the terminal `auto_continue` event still reaches it). */ export declare function resolveContinuationDispatchBudget(opts: { chainViaDurableBackground: boolean; workerProvenInBackgroundFunction: boolean; }): ContinuationDispatchBudget; /** * True when a `fireInternalDispatch` failure is Netlify's Functions platform * loop-protection response (`HTTP 508 Loop Detected`), matched against the * exact message `self-dispatch.ts`'s `dispatchResponseError` constructs * (`Self-dispatch to ${path} returned HTTP ${res.status} ${res.statusText}...`). * Matches on the status code alone (not the reason phrase text) so it is * robust to any wording Netlify's edge puts in `statusText`. * * VERIFIED: production `agent_runs` diagnostics show exactly this failure mode * — 8 successful `chain_dispatch_sent` self-dispatches followed by a 9th/10th * that dies with `HTTP 508 Loop Detected`, terminal reason * `background_continuation_dispatch_failed`. UNVERIFIED (Netlify does not * document the mechanism — checked the Functions overview, Functions API * reference, Background Functions overview, Status Codes reference, and * Request Chain troubleshooting docs, none mention it): the ONLY public * confirmation is a Netlify staff forum reply — "we prevent multiple * executions after one-another as that's a 'loop' ... I believe we enforce * this after 9 or 10 self-invocations" * (https://answers.netlify.com/t/self-invoke-background-function-via-post-requests/146012) * — which also confirms Background Functions do NOT escape the limit (the * reporter's case was already a `-background` function self-invoking). See * `MAX_NESTED_SELF_DISPATCH_DEPTH` for how this classification is used. */ export declare function isLoopProtectionDispatchError(err: unknown): boolean; /** * Conservative safety margin on NESTED self-dispatch chaining — a * continuation dispatched directly from within the live invocation that is * about to finish, as opposed to being picked up later by the * unclaimed-background-run sweep (`agent-chat-plugin.ts`), which fires its * redispatch from an unrelated, timer-driven invocation rather than from * inside the chain's own execution. * * Netlify's loop-protection ceiling is undocumented and platform-reported * only approximately ("I believe... 9 or 10" — see * `isLoopProtectionDispatchError`), so hard-coding that exact number here * would be pinning behavior to a number Netlify itself won't commit to, and * production evidence shows it can trigger by the 9th self-dispatch. Rather * than only reacting after triggering that undocumented limit, * `chainServerDrivenContinuation` proactively hands a chunk to the sweep once * `backgroundContinuationCount` (nested hops since the last chain break) * reaches this value — comfortably below the observed ~9-10 failure point — * instead of attempting another nested dispatch. This is the SAME deferred- * recovery path already used when a dispatch fails outright: the row is left * `status='running', dispatch_mode='background'` for the sweep to redispatch, * never silently dropped. * * The sweep's redispatch marker intentionally omits `continuationCount` (see * the "Unclaimed background-run sweep" in `agent-chat-plugin.ts`), so a * sweep-recovered chunk's own `backgroundContinuationCount` resets to 0 — * this constant therefore bounds each NESTED segment between chain breaks, * not the whole turn. The TRUE ceiling on total turn length is unaffected by * that reset: it is the durable per-turn SQL ledger (`countRunsForTurn`, * checked above in this function) plus `MAX_BACKGROUND_RUN_CONTINUATIONS` — * both counted independently of this in-marker value — so a legitimately * long turn keeps making progress in bounded nested segments, each recovered * by the sweep, until it genuinely exhausts the intentional overall budget * and fails loud with `turn_continuation_budget_exhausted`. */ export declare const MAX_NESTED_SELF_DISPATCH_DEPTH = 6; /** * Server-driven continuation handoff for a chunk that hit its soft-timeout * boundary still unfinished: mint the next chunk's runId, PRE-INSERT its run * row (so `/runs/active` never shows an idle gap and a lost dispatch is * reaped loudly instead of hanging silently), fire the HMAC-signed * `_process-run` self-dispatch carrying ids only (the body is persisted on * the row as `dispatch_payload`), fully AWAIT the dispatch acknowledgment * with retries, and mark this chunk terminal only after the handoff landed. * On failure this chunk always goes terminal loudly (diag stage + terminal * reason recorded — never a silent loss), but the successor row's fate * depends on WHY the dispatch failed: * - the pre-insert itself failed (no successor row exists) — nothing for a * sweep to find, so this is unrecoverable: fail loud immediately with * `background_continuation_dispatch_failed`. * - every dispatch attempt failed but the successor row DOES exist with its * `dispatch_payload` intact — this is RECOVERABLE: the row is left alone * (`status='running', dispatch_mode='background'`) instead of being * errored, so the unclaimed-background-run sweep in `agent-chat-plugin.ts` * can redispatch it once `UNCLAIMED_BACKGROUND_RUN_GRACE_MS` has passed. * This chunk is flipped to errored with the distinct, honest * `background_continuation_dispatch_deferred` reason — the TURN is * deferred, not dead. The sweep still bounds this by * `UNCLAIMED_BACKGROUND_RUN_REDISPATCH_BOUND_MS`: past that it falls back * to the existing loud reap, so a genuinely dead handoff never hangs * silently forever. (For a FOREGROUND self-chain the client additionally * still receives the terminal `auto_continue` event — run-manager emits * it after this callback — so the existing client re-POST path is a * second, faster fallback alongside the sweep.) * * `chainViaDurableBackground` selects the dispatch target: * - true → the durable-background worker chain (unchanged behavior): the * resolved background-function url on hosted Netlify (15-min budget), 3 * attempts, 15s response timeout (Netlify async functions 202 on enqueue). * - false → the OPT-IN foreground self-chain: the framework `_process-run` * route on the REGULAR function. With `AGENT_CHAT_DURABLE_BACKGROUND` * off the `-background` function is never emitted into the deploy * output, so the regular function is the only guaranteed target; the * successor keeps the 40s chunk clamp (`backgroundFunctionRuntimeExpected` * is false for this path). The regular function responds only after the * successor chunk FINISHES (~40s), so a response timeout is NOT proof of * a dead handoff — after a failed/timed-out attempt the successor's * ATOMIC CLAIM is consulted (`readBackgroundRunClaim`): a row that * flipped to `background-processing` (or already went terminal) proves * the handoff landed, so no retry is fired. * * `chainViaDurableBackground` does NOT alone determine the retry BUDGET — * see `resolveContinuationDispatchBudget` and `workerProvenInBackgroundFunction` * below: a worker forced onto this same `false` target because it is proven * to already be inside a real background function gets a materially larger * budget than a true foreground caller, since it has no connected-client * fallback and minutes of remaining wall clock instead of seconds. * * Never throws — all failure paths are handled (recorded + marked) inside. */ export declare function chainServerDrivenContinuation(opts: { event: unknown; run: ActiveRun; effectiveThreadId: string; effectiveTurnId: string; /** The current chunk's request body — the successor's rehydration payload * is derived from it (marker stripped, `internalContinuation` set). */ requestBody: Record; backgroundContinuationCount: number; /** * Input tokens this logical turn has consumed across every chunk so far, * carried on the successor's body so the per-turn token ceiling is a real * turn budget instead of a fresh allowance per chunk. */ turnInputTokens?: number; chainViaDurableBackground: boolean; /** True only when this worker is PROVEN (by runtime function name) to * already be executing inside a real Netlify `-background` function — * distinct from `chainViaDurableBackground`, which is forced `false` for * this exact worker (it cannot dispatch to its own function URL from a * live invocation). Widens the retry budget; does NOT change the dispatch * target. See `resolveContinuationDispatchBudget`. Defaults to `false`. */ workerProvenInBackgroundFunction?: boolean; deps?: ChainServerDrivenContinuationDeps; }): Promise; export declare function resolveAgentRequestReasoningEffort({ model, requestEffort, configuredEffort, }: { model: string; requestEffort?: unknown; configuredEffort?: ReasoningEffort; }): ReasoningEffort | undefined; export declare function createProductionAgentHandler(options: ProductionAgentOptions): H3EventHandler; export { getActiveRunForThread, getActiveRunForThreadAsync, getRun, abortRun, abortRunDurably, subscribeToRun, }; //# sourceMappingURL=production-agent.d.ts.map