UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

771 lines • 53.9 kB
import { type ProtocolId } from "./protocol-table.js";
import { type McpDelivered, type McpFailureKind, type McpServerLiveness } from "./mcp-failure.js";
import type { AgentTool } from "../internal/harness-types.js";
import type { ImageContent, TextContent } from "../internal/llm.js";
import { type McpImageResizer } from "./image-downsample.js";
import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
import { type ReminderDisclosureCounts } from "./reminder-disclosure.js";
export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
/**
 * The safety axes (design/77 §4 irreversibility, design/70 egress) derived from one materialized MCP
 * tool's server-advertised `annotations`. These ride alongside the {@link AgentTool} (which is vendored
 * and carries no axis fields) so `prepare-task` can fold them into the SAME irreversibleTools/egressTools
 * collection that `spec.tools` feeds — otherwise a destructive MCP tool would register no gate and a
 * no-policy deployment would silently AUTO-ALLOW it (MAJOR-1). The `name` is the namespaced tool name
 * (`<server>__<tool>`), matching the keys the gate looks up.
 *
 * Trust note (design F): a server's `annotations` are SERVER-controlled and "not guaranteed faithful" (MCP
 * spec), so they may only ever TIGHTEN — `destructiveHint` adds an `ask`/suspend, `openWorldHint` adds an
 * egress tighten — never LOWER. A server's `readOnlyHint` does NOT lower `effect` (that would let an adversarial
 * server lie on a mutating tool to escape repeat-safety + path-confinement). The ONLY trusted way to LOWER an
 * MCP tool below the fail-closed `write` default is the caller's {@link McpServerSpec.toolAxes} override
 * (caller = trust root). The synthetic first-party resource tools are the one exception — core sets their
 * `read` effect itself because it authored them.
 */
export interface McpToolAxis {
    /** Namespaced tool name (`mcp__<server>__<tool>` — design/108 ① CC parity). */
    name: string;
    /** design/77 §4: `destructiveHint === true` → `"always"` (the gate always tightens allow→ask/suspend).
     *  `"never"` appears ONLY via an explicit caller override — enforcement ignores it (tighten-only),
     *  the ask's risk-axes report face consumes it (an explicit negative is a judgment, not silence). */
    irreversibility?: "always" | "never";
    /** design/70: a non-readOnly tool with `openWorldHint === true` → egress (external write — never
     *  auto-allowed). `false` appears ONLY via an explicit caller override — same posture as above:
     *  enforcement ignores it, the report face consumes it. */
    egress?: boolean;
    /**
     * The repeat-safety / blast-radius class (NOT the data-trust axis — untrusted resource *content* is fenced
     * separately by delimitUntrusted). `read`/`idempotent` here means "not write-capable", which keeps the tool
     * off the LOUD ungated-write warning and out of path-confinement under a skill. Source is restricted by trust
     * (design F): the SYNTHETIC first-party resource tools set `read` as a fact core controls, and the caller's
     * {@link McpServerSpec.toolAxes} override may set any effect — a server's own `readOnlyHint` never sets it.
     */
    effect?: ToolEffect;
}
/**
 * REF-D17: one MCP tool dropped at intake (server/tool/reason) — the SINGLE shape {@link
 * MaterializedMcp.droppedTools}, {@link McpRefreshResult.dropped}, and `turn-attachments.ts`'s
 * disclosure rendering/batching all reference. MCP trust/disclosure is a security-relevant surface
 * (the trust model was BREAKING-redone historically) — a drift between the connect-time and
 * refresh-time shapes would make a real field change invisible at one of the two intake paths.
 */
export interface McpDroppedTool {
    server: string;
    tool: string;
    reason: string;
}
/**
 * The completeness verdict of a walked STANDARD MCP list method (`tools/list`, `resources/list` — see
 * {@link walkMcpListPages}). ABSENT means the walk reached the end of pagination and the aggregated
 * listing IS the server's full list. PRESENT means it is not, and the listing must not be presented —
 * or DIFFED — as if it were: an entry missing from a truncated listing is unproven-absent, not
 * withdrawn (that inference is exactly what made a paginating server's second-page tools read as
 * `removed`).
 *  - `page_cap` — {@link MAX_MCP_LIST_PAGES} pages were walked with a cursor still pending;
 *  - `cursor_loop` — the server handed back a cursor already used in this walk (a paginator that does
 *    not advance); the walk stops rather than spinning to the cap re-appending the same page;
 *  - `cursor_invalid` — the server sent a `nextCursor` that is not a string (`null` excepted: that is
 *    JSON's absent optional). It cannot be followed and it is not proof of the end, so the walk stops
 *    and says so instead of certifying a short listing as complete;
 *  - `page_error` — a CONTINUATION page failed. The pages already retrieved are kept and the failure
 *    text carried. A FIRST-page failure is never this: it still throws (a caller's "no listing at all"
 *    must not degrade into "an empty listing");
 *  - `budget_exhausted` — CORE stopped the walk: the time this listing is allowed to spend
 *    ({@link walkMcpListPages}'s `budgetMs`) had too little left to fund another request
 *    ({@link MIN_MCP_PAGE_BUDGET_MS}), so none was sent. Distinct from `page_error` on purpose — the
 *    server neither failed nor was asked. Issuing that request anyway would hand the SDK a
 *    near-zero timeout, and its -32001 would arrive here wearing the server's failure clothes.
 * `error` is SERVER-authored text — neutralize it before it rides any model/operator face.
 */
export interface McpListingIncomplete {
    reason: "page_cap" | "cursor_loop" | "cursor_invalid" | "page_error" | "budget_exhausted";
    /** Pages actually retrieved before the walk stopped (≥ 1 — a zero-page walk throws instead). */
    pages: number;
    error?: string;
    /** The walk's total time budget in ms. Present ONLY on `budget_exhausted`, where it is the whole
     *  actionable content of the verdict (which bound to raise); every other reason leaves it absent
     *  rather than reporting a budget that had nothing to do with the stop. */
    budgetMs?: number;
}
/** Tools materialized from one or more MCP servers, plus a disposer to disconnect them. */
export interface MaterializedMcp {
    tools: AgentTool[];
    /**
     * Per-tool safety axes derived from each tool's MCP `annotations` (design/77 §4 / design/70). One entry
     * per materialized tool that carries a tightening hint; `prepare-task` folds these into its
     * irreversibleTools/egressTools sets so a destructive/open-world MCP tool REGISTERS the gate + tightens
     * even on a no-policy deployment. Empty when no server advertised a relevant annotation.
     */
    toolAxes: McpToolAxis[];
    /**
     * MCP server `instructions` (design/64 §17.3, CC `# MCP Server Instructions`): the optional guidance a
     * connected server returns from `initialize`. The Runner injects these into the (stable) system prompt so
     * the model uses the server's tools correctly. One entry per server that provided non-empty instructions.
     */
    serverInstructions: Array<{
        server: string;
        text: string;
    }>;
    /**
     * G1 通告层续批 (CC `mcp_instructions_delta` parity) — the run-scoped instruction-delta ref the run
     * loop drains at turn boundaries (`spec.attachments.mcpInstructions` opt-in; `newTools` consumption
     * discipline: drained only when the announcement survived the byte cap intact).
     * - `pendingRemovals`: an INSTRUCTION-BEARING server whose transport closed mid-task (the CC
     *   "servers have disconnected — their instructions above no longer apply" copy is instruction-scoped,
     *   so instruction-less servers are not tracked). `dispose()` disarms first: task teardown is not a
     *   disconnect.
     * - `pendingAdds`: the CC add lane ("# MCP Server Instructions" reminder). Structurally present for a
     *   dynamic mid-task connect face; TODAY nothing populates it — sema connects all servers at prepare
     *   and carries their initial instructions in the stable system prompt (design/64 §17.3, deliberate
     *   CC deviation: prefix-cache-correct).
     */
    instructionsDelta: {
        pendingAdds: Array<{
            server: string;
            text: string;
        }>;
        pendingRemovals: string[];
    };
    /**
     * The OPERATOR warning lane for this materialization (the Runner forwards each entry to
     * `onError(phase:"mcp")`). Empty when every server connected and listed completely. Two populations,
     * distinguishable by the `code` each Error carries — a consumer must NOT read mere presence as "this
     * server was skipped":
     *  - `mcp.server_unavailable` — a server that failed to connect / list its tools and was SKIPPED
     *    (fail-open, design/29): the task proceeds with the healthy servers' tools, because a single bad
     *    server (missing stdio command, unreachable URL) must never brick every task in the scenario;
     *  - `mcp.listing_incomplete` — a server that DID connect but whose `tools/list` pagination walk did
     *    not reach the end ({@link McpListingIncomplete}): its tools past the stopping point are not
     *    mounted this task, which is a capability loss the operator must see rather than infer from a
     *    short tool list.
     */
    warnings: Error[];
    /**
     * [cc216] Tools DROPPED at intake because the server advertised a schema no provider accepts
     * ({@link mcpToolSchemaProblem}) — without the drop, one malformed tool schema riding the wire
     * fails the WHOLE model request (provider 400), bricking the task for every healthy tool
     * (CC 2.1.216 ships the same intake prune, `tengu_mcp_drop_invalid_tool_schemas`).
     *
     * HONEST REACH (pinned in mcp-dropped-tools.test.ts): listings arrive through the lenient reader
     * (`listToolsLenient` — raw request, no SDK-client zod pass), so a string-root or otherwise odd
     * schema DOES reach this gate and is judged per tool; healthy siblings survive. This per-tool gate
     * is therefore the PRIMARY adjudicator for schema shape, not a fallback behind SDK validation.
     *
     * The drop is NEVER silent, two faces (CC parity — its `mcp_dropped_tools_delta` system reminder):
     * - operator: prepare-task forwards each entry to `onError(phase:"mcp")` alongside `warnings`;
     * - model: the run loop announces the entries ONCE at a turn boundary (`mcp_dropped_tools`
     *   attachment, `mcpInstructions` opt-in lane) and DRAINS this array on intact delivery — same
     *   survival-gated consumption discipline as `instructionsDelta`.
     * `tool` is the server-advertised name, neutralized at intake (`inlineUntrusted`); `reason` is
     * core-TEMPLATED but its structural-error paths may embed server-controlled property names, so
     * the whole string is likewise neutralized + bounded at intake (single-line, ≤240 cps, fences
     * defused) — the bound also keeps one rendered announce entry inside a single frame.
     */
    droppedTools: McpDroppedTool[];
    /**
     * design/99 §E9 — per-server status, a PURE PROJECTION of THIS materialization (no new connection logic).
     * One entry per declared server. **Semantics = the moment of this task leg's materialize** — core never
     * persists MCP connections, so it is "did this leg connect", NOT a live session-health signal between tasks.
     * A consumer (service) surfacing it as session status must understand that. `toolNames` are the namespaced
     * (`<server>__<tool>`) names exposed.
     *
     * READ THIS AS A SNAPSHOT, NOT A HEARTBEAT. `status` is the connect-time verdict and is never
     * rewritten: a server that dies mid-task still reads `connected` here, because that IS what happened at
     * materialize. The one live bit is {@link McpServerStatus.transportClosed} (set if and when core observes
     * the transport go away — see its own note for the reach). For an authoritative answer about a server's
     * health RIGHT NOW, call {@link refresh} — it is the only face that touches the connection.
     */
    statuses: McpServerStatus[];
    /**
     * RefreshMcpTools (CC 2.1.218 parity, anchored to the CLI form) — re-read the tool list of one server (or
     * every CONNECTED server when `server` is omitted) over the EXISTING connection. NEVER dials or
     * re-dials: a dead/never-connected server reports `not_connected`; a live listTools failure
     * reports `failed` (the connection is left as-is). A refreshed entry carries the NEW intake
     * result (tools/axes/dropped — the same schema-gate/namespacing/axis pipeline as connect) plus
     * the added/removed namespaced tool names relative to the previous listing; the caller (the
     * RefreshMcpTools tool mount in prepare-task) swaps them into the harness toolset.
     *
     * LEG-LOCAL fact (review, documented): refresh results do NOT ride a checkpoint — a durable
     * resume re-materializes and takes a FRESH first listing, so refreshed tools may be absent on the
     * resumed leg until RefreshMcpTools is called again (an honest not-found, never a silent
     * misroute).
     */
    refresh: (server?: string) => Promise<McpRefreshResult[]>;
    /**
     * RE-DIAL one named server — the action {@link refresh} deliberately is not. It closes
     * whatever handle the server still has and dials it again through the SAME `connectServer` the
     * materialization used, so the schema gate, the namespacing, the axis derivation and the metadata
     * cache seeding cannot drift between a first connect and a re-dial: one function, not one
     * convention.
     *
     * ONE SERVER, NAMED — CC 2.1.259's control-channel request carries a REQUIRED `serverName` and has
     * no all-servers form, and an all-servers arm would close HEALTHY connections to re-dial them (a
     * stdio server owning a device or a port is exactly the one that cannot be re-dialed while its
     * predecessor still holds the device). CLOSE-THEN-DIAL, so a re-dial that FAILS leaves the server
     * DISCONNECTED until another re-dial or the next leg's materialize, and calls in flight on the
     * replaced connection fail with it — inherent to re-dialing, and CC's behavior too.
     *
     * NOT A MODEL FACE (CC has no such tool either): a verb that can leave a connection down is a host
     * act, and its consumer entrance is the run handle's `reconnectMcpServer`. LEG-LOCAL like
     * {@link refresh}: the result rides no checkpoint, and a durable resume takes a fresh listing.
     */
    reconnect: (server: string) => Promise<McpReconnectResult>;
    dispose: () => Promise<void>;
}
/** One per-server entry of {@link MaterializedMcp.refresh}. `added`/`removed` are namespaced
 *  (`mcp__<server>__<tool>`) names; `tools`/`axes`/`dropped` are present only on `"refreshed"`.
 *  The model-facing refresh receipt renders each `dropped` entry with ITS OWN `reason` (schema
 *  gate, same-listing name collision, …) — never a blanket label; a consumer matching that receipt
 *  text should key on the tool name, not on any fixed reason wording. Note the startup-time
 *  sibling: two DECLARED servers whose names normalize to one prefix refuse the whole mount at
 *  connect (`config.mcp_server_name_collision`, both original spellings named) — a deployment
 *  carrying such a pair learns at the first materialize, with the rename spelled out in the error. */
export interface McpRefreshResult {
    server: string;
    /** The server's namespaced-name prefix (`mcp__<normalized-server>__`) — the consumer's SPLICE
     *  DOMAIN for the swap (review F4: a prefix domain is self-healing and decoupled from the diff
     *  baseline, which advances even when a consumer skips a swap). Present on every entry. */
    prefix: string;
    /** `"revoked"` (design/338): the host ledger marks this server revoked — the refresh did NOT
     *  contact it (no tools/list round trip; the engine must not hand a severed server a request
     *  channel or splice its post-revocation text into the model catalog). */
    status: "refreshed" | "not_connected" | "failed" | "revoked";
    toolCount: number;
    added: string[];
    /**
     * Names that were mounted before this refresh and are no longer in the roster it returns — i.e. an
     * assertion that the server withdrew them. It is therefore EMPTY whenever {@link listingIncomplete}
     * is present, and empty as a FACT rather than a policy: an incomplete walk cannot prove absence, so
     * it retains what it could not prove gone (see `listingIncomplete`) and nothing leaves. `added` is
     * unaffected — a name that IS in the partial listing was really listed, whether or not the walk
     * finished (#381).
     */
    removed: string[];
    tools?: AgentTool[];
    axes?: McpToolAxis[];
    dropped?: McpDroppedTool[];
    /**
     * Set on `not_connected` / `failed` / `revoked`; ALSO set on a `"refreshed"` entry whose listing walk
     * did not finish (see {@link listingIncomplete}) — a refresh that produced usable tools is not a
     * failure, so the status stays `refreshed` and the incompleteness rides here.
     */
    error?: string;
    /**
     * Present when this entry's `tools/list` walk did NOT reach the end of pagination. `tools` is
     * then a MERGE: everything the partial listing did return, plus the previously-mounted entries whose
     * absence it cannot prove (so the consumer's replace-the-whole-prefix swap withdraws nothing, and
     * `removed: []` describes a roster that really lost nothing). Absent on a complete walk and on every
     * non-`refreshed` status.
     *
     * RESIDUAL, honest: the model-facing `RefreshMcpTools` receipt renders a refreshed entry's line from
     * added/removed/excluded/dropped only, so this fact reaches the STRUCTURED face (`details.results`)
     * and the operator, not the receipt text — rendering it there is a prepare-task change, filed rather
     * than smuggled in here.
     */
    listingIncomplete?: McpListingIncomplete;
}
/**
 * The result of {@link MaterializedMcp.reconnect} — ONE server, because the face takes one.
 *
 * `outcome` is the whole top level, three words, so a consumer mapping it onto a three-state wire
 * answer needs no table: `reconnected` is the only success, and everything that did not reconnect is
 * either `refused` (the engine declined, or the dial/first listing failed — `reason` says which) or
 * `not_declared` (no server of that name in this leg's spec). Sub-reasons ride `reason`; a fourth
 * word would be a new state for every downstream to learn.
 *
 * @contract mcp.redial.roster_assertion — a re-dial result carries `tools` IF AND ONLY IF it asserts a
 * roster for its `prefix` domain, and that one rule is what every consumer of it reads: the fresh intake
 * on `reconnected`, the EMPTY roster on a dial that failed after the old connection was already closed
 * (the server is down, so its tools are withdrawn rather than left mounted and unusable), and NOTHING at
 * all on an arm that never touched the connection — an unknown name, a revoked server, a re-dial already
 * in flight, a disposed materialization — whose mounted roster is left exactly as it was.
 */
export interface McpReconnectResult {
    server: string;
    /** The consumer's splice domain, same meaning and same mint as {@link McpRefreshResult.prefix}. */
    prefix: string;
    outcome: "reconnected" | "refused" | "not_declared";
    /** Why, on every non-`reconnected` outcome (neutralized + bounded when it embeds remote text).
     *  Never the only machine-readable fact: see {@link status}. */
    reason?: string;
    toolCount: number;
    /** Namespaced names this re-dial added / took away relative to what the server had mounted a moment
     *  ago — a first successful dial adds its whole roster, a failed dial removes it. */
    added: string[];
    removed: string[];
    /** See the type docstring: present iff this result asserts a roster for `prefix`. */
    tools?: AgentTool[];
    axes?: McpToolAxis[];
    dropped?: McpDroppedTool[];
    /** The OPERATOR lines this re-dial produced (an incomplete listing walk, a kept-but-defective tool
     *  schema, a spec naming tools a resources-only server cannot serve). Also appended to
     *  {@link MaterializedMcp.warnings}, which the runner drains ONCE at prepare — so the copy here is
     *  what the run handle's verb forwards to `onError(phase:"mcp")`. One production, two drains. */
    warnings?: string[];
    /** The NEW connect-time status record (also swapped into {@link MaterializedMcp.statuses}), present
     *  whenever the dial was attempted: a consumer keys its next action off `errorCode` / `httpStatus`
     *  (a 401 is a re-authorization door, a `spawn_failed` a missing binary), not off {@link reason}. */
    status?: McpServerStatus;
    /** The re-dial's own `tools/list` walk did not finish — {@link McpRefreshResult.listingIncomplete}
     *  minus the retention arm: a re-dial has no previous listing on this connection to retain from. */
    listingIncomplete?: McpListingIncomplete;
}
/** design/99 §E9 — projected per-server MCP status (see {@link MaterializedMcp.statuses}). NOTE: `serverInfo`
 *  and `error` are SERVER-controlled strings (verbatim from the remote) — UNTRUSTED; a consumer rendering them
 *  into a TUI/log must treat them as such (a misbehaving server could embed ANSI/break-out sequences). `toolNames`
 *  are namespaced with a core-controlled `mcp__<server>__` prefix (design/108 ①). */
export interface McpServerStatus {
    name: string;
    /** The CONNECT-TIME verdict for this leg — "did this server connect and list its tools". Deliberately
     *  frozen: a later death does not rewrite it (see {@link transportClosed} and `MaterializedMcp.refresh`). */
    status: "connected" | "failed";
    serverInfo?: {
        name: string;
        version: string;
    };
    toolNames?: string[];
    error?: string;
    /** Present iff `status === "failed"` — the connect-time failure record, spread flat: {@link McpFailure.kind}
     *  as `errorCode`, {@link McpFailure.delivered} as `delivered`, and `httpStatus` when the kind is
     *  `http_status`. A consumer keys an action off THESE (a `http_status` 401 is a re-authorization door,
     *  a `spawn_failed` is a missing binary) instead of parsing the neutralized `error` text. */
    errorCode?: McpFailureKind;
    delivered?: McpDelivered;
    httpStatus?: number;
    /**
     * Set to `true` once core OBSERVES this server's transport close during the task; absent
     * otherwise (never `false`: absence means "no close was observed", which is not the same as "alive").
     * It mirrors exactly the flag core's own fail-fast path keys on, so `transportClosed === true` also
     * means every further call to this server's tools/resources is refused without dialing.
     *
     * REACH (honest, and the reason `refresh()` stays the authority): this needs a transport-level close
     * NOTIFICATION. A stdio server has one — the child process exiting closes the pipe. A streamable-HTTP
     * endpoint does NOT: the SDK's HTTP transport raises `onclose` only when the client itself closes it,
     * so an endpoint that dies is discovered per-REQUEST (as a translated transport failure carrying the
     * server name and the outcome) and this field stays absent.
     */
    transportClosed?: boolean;
    /**
     * The engine's last liveness OBSERVATION about this server ({@link McpServerLiveness}) — a different
     * question from all three of its neighbours, and the note each of them carries says so:
     *  · {@link status} is the CONNECT-TIME verdict of this leg, frozen at the dial — it is not this;
     *  · {@link transportClosed} is one EVENT (a close notification arrived), on the transports that have
     *    one — it is not this (this record also carries an `unreachable` that no close ever announced);
     *  · `McpReconnectResult.outcome` is one ACTION's judgment — it is not this (it says what a re-dial
     *    did, this says what the connection layer last learned).
     * Written where the connection layer establishes or loses a connection (the dial, a re-dial, a close,
     * a request that comes back with the transport gone), never on a timer and never by a probe: the
     * engine adds no traffic of its own to answer it.
     *
     * ABSENT = no observation ({@link import("./mcp-failure.js").MCP_LIVENESS_STATES}): a declaration that could not be dialed
     * (`invalid_config`) never touched a transport, and an older / foreign record predates the field. A
     * consumer must read absence as indeterminate — never as healthy, never as off.
     */
    liveness?: McpServerLiveness;
}
/**
 * design/108 ① (CC 2.1.187 parity) — the GLOBAL `mcp__` marker prefix. CC names every MCP tool
 * `mcp__<server>__<tool>` so a consumer (permission allowlist, bypass-exempt, the flywheel's whitelist) can
 * identify an MCP-originated tool by prefix ALONE, without knowing the per-deployment server names. Exported so
 * downstream consumers share one source (the durable-resume canonicalizer is retired — RB-476-A).
 * The value is OWNED by the protocol table (protocol-table.ts) — this export is the public face.
 *
 * The type is the LITERAL `"mcp__"`, not `string`: a downstream literal-typed position (a template-literal
 * type, a keyed lookup) compiles against the literal and stops compiling the moment this widens — and the
 * export-surface snapshot is name → kind, so it cannot see a widening at all. Annotated rather than
 * asserted, so a table-side change to the prefix surfaces as a compile error here instead of being
 * silently re-narrowed by a cast.
 */
export declare const MCP_PREFIX: "mcp__";
/**
 * Apply the token gate to a mapped MCP content array (CC `$Mp`/`BMp` shape): text blocks consume a
 * cumulative char budget of `tokens×4`; past the budget they are sliced/dropped head-keep, and the CC
 * guidance note is appended as a final text block. Non-text blocks (images) pass through untouched —
 * same convention as the offload wrapper. Exported for tests.
 */
export declare function gateMcpOutput(content: Array<TextContent | ImageContent>, limitTokens?: number): Array<TextContent | ImageContent>;
export declare function structuredContentErrorLine(structuredContent: unknown, collectedText: string): string | undefined;
/** Head+tail bound for MCP error text (exported for tests). */
export declare function truncateMcpErrorText(s: string): string;
/**
 * Default per-call MCP tool timeout. The SDK's own default (60s) silently kills any long-running MCP tool
 * with a -32001 and offers the deployment no knob; a very large default (matching the behavior of the
 * reference agent client) lets the task-level budget/abort govern instead, while `MCP_TOOL_TIMEOUT` (ms)
 * remains available to tighten it per deployment.
 */
export declare const MCP_TOOL_TIMEOUT_DEFAULT_MS = 100000000;
/**
 * The bounds EVERY `MCP_*` millisecond knob resolves into — CC 223-same (`MNs` @354067 and the idle leg
 * `Pyd` @354076 both clamp into `[1000, 2147483647]`; the constants were already ported here, the clamp
 * was not).
 *
 * The ceiling is not cosmetic: `setTimeout` stores its delay in a signed 32-bit field, so a larger value
 * is silently rewritten to `1` — an operator WIDENING a bound past 24.85 days (the shape of "basically
 * turn this watchdog off") gets the exact opposite: a bound that fires ~immediately on every call. The
 * floor closes the other end of the same unit trap — a bare `30` is what someone thinking in seconds
 * types, and a 30ms MCP bound is never anyone's intent.
 */
export declare const MCP_ENV_MS_MIN = 1000;
export declare const MCP_ENV_MS_MAX = 2147483647;
/** Test seam kept under the name its callers already use; the ledger it clears is the shared one
 *  ({@link __resetEnvKnobAnnouncements} — the MCP knobs are not a family of their own, they are
 *  process-level env knobs like the output-length pair). Never called by production code. */
export declare function __resetMcpEnvAnnouncements(): void;
/** Resolve the per-call tool timeout in ms (env `MCP_TOOL_TIMEOUT`, else the large default). Exported for tests. */
export declare function mcpToolTimeoutMs(): number;
/**
 * Resolve the TOTAL wall-clock ceiling for one tool call in ms (env `MCP_TOOL_TIMEOUT_TOTAL`, else the
 * per-call timeout). The SDK's `resetTimeoutOnProgress` only re-arms the SDK's OWN per-call timer
 * (`mcpToolTimeoutMs()` above — a different mechanism from {@link armMcpIdleWatchdog}'s idle watchdog
 * below, despite the name-adjacent terms; disambiguated here on purpose, see that function's docstring
 * for why sema needs both). It enforces a total bound ONLY when `maxTotalTimeout` is provided (protocol.js
 * `_resetTimeout`), so without this a
 * misbehaving/stuck server emitting periodic progress notifications could extend a call forever. Defaulting
 * the ceiling to the per-call timeout means progress can keep an active call alive only within the same
 * window the deployment already accepted; the resulting failure is the same -32001 timeout frame. Env
 * parsing follows the `MCP_TOOL_TIMEOUT` discipline (unset/unparseable/non-positive → default). Exported
 * for tests.
 */
export declare function mcpToolTotalTimeoutMs(perCallMs: number): number;
/**
 * RB-199 FO-1 (form-one audit, CC 2.1.220): a server that goes SILENT — alive, transport open, no error,
 * no close, just never responds and never emits progress — has no bound tighter than the (deliberately
 * enormous, ~27.8h-default) total ceiling above. CC bounds this with a SEPARATE idle watchdog distinct
 * from its total ceiling (`j7u` @345484: stdio 30min / http-sse 5min).
 *
 * Narrower than the audit's original framing: a CLOSED transport is already handled correctly by the SDK
 * itself — `Protocol._onclose` (shared/protocol.js) rejects every in-flight response handler immediately
 * with `ConnectionClosed` the moment `onclose` fires, so this watchdog exists for the SILENT-but-open case
 * only, not as a substitute for connection-close handling.
 */
export declare const MCP_IDLE_TIMEOUT_STDIO_DEFAULT_MS: number;
export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
/** Resolve the idle-watchdog bound in ms for a transport kind (env `MCP_IDLE_TIMEOUT_STDIO` /
 *  `MCP_IDLE_TIMEOUT_HTTP`, else the CC-matched default). Exported for tests. */
export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
/**
 * Startup-phase (connect + initial listTools) timeout override in ms (env `MCP_TIMEOUT`); undefined keeps
 * the SDK default. Startup deliberately stays bounded: a hung server at materialize time should fail open
 * into a skipped-server warning quickly, not stall task preparation indefinitely.
 */
export declare function mcpStartupTimeoutMs(): number | undefined;
/** The protocol-range condition name for a JSON-RPC error code, or `undefined` for every other code
 *  (see {@link MCP_SPEC_ERROR_CODE_NAMES}). */
export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
/**
 * The SDK stamps `MCP error <code>: ` onto an McpError's message IN ITS CONSTRUCTOR, and it
 * does so at BOTH ends of the wire: a server built on an MCP SDK throws an McpError, its already-stamped
 * `.message` goes out as the JSON-RPC `error.message`, and the client constructs its own McpError over
 * that string — so the failure reaches the model as `MCP error -32602: MCP error -32602: …`. Collapse a
 * repeat of the SAME code (a leading stamp carrying a DIFFERENT code is server data, not a duplicate of
 * ours, and is left alone). Exported for unit testing; not part of the package index.
 */
export declare function collapseMcpErrorPrefix(message: string): string;
export { normalizeNameSegment as normalizeMcpName, clampNameSegment } from "./protocol-naming.js";
export * from "./image-downsample.js";
/**
 * design/116 W5-2 — compact, jq-friendly type signature for a `structuredContent` value, e.g.
 * `{title: string, items: [{id: number}]}`. Port of CC 2.1.187 `inferCompactSchema` (services/mcp/client.ts,
 * beside the structuredContent branch at client.ts:2676-2683): depth-2, first array element as the element
 * type, ≤10 object entries then `, ...`.
 */
export declare function inferCompactSchema(value: unknown, depth?: number): string;
/** The per-peer HTTP auth declaration every protocol spec carries in the same two fields — the static
 *  headers the deployment configured, and the header name the per-task principal rides on (design/62).
 *  Named rather than inlined because it is a cross-module signature: {@link McpServerSpec}'s http
 *  transport and A2A's peer spec both satisfy it, and the resolver below serves both. */
export interface ProtocolHttpAuth {
    headers?: Record<string, string>;
    principalHeader?: string;
}
/**
 * Resolve the HTTP request headers for one remote PEER of `protocol`, injecting the per-task end-user
 * principal (design/62). The principal is added **only** when the spec declares a `principalHeader` AND a
 * `principal` is present; it is injected **last** so it overrides any same-named static header; an absent
 * principal sends no such header (the remote must then default to deny/public, never admin). Pure +
 * exported for testing and so a consumer can verify what its config sends.
 *
 * Shared across protocols (A2A reuses it verbatim), which is why the failure names its protocol from the
 * ARGUMENT rather than from a hard-coded literal: a copy of this guard per protocol is exactly the
 * "second mint" shape whose copies stop tracking each other. Callers must invoke it INSIDE their per-peer
 * boundary — the throw is one peer's configuration defect, and the surrounding per-peer catch is what
 * turns it into a skipped peer instead of a failed task.
 */
export declare function resolveProtocolHttpHeaders(protocol: ProtocolId, t: ProtocolHttpAuth, principal?: string): Record<string, string> | undefined;
/** `kind` is the dial's ONE read of `spec.transport.kind` (taken by the caller beside the dial and threaded
 *  here, to the idle bucket and to the failure classifier alike): a nested `transport` object is the
 *  caller's, and a getter that answered differently to two readers would otherwise connect over one
 *  transport and be judged under another. An unknown kind takes the non-stdio branch, as it always did. */
/**
 * The MCP HTTP transport's `fetch`: every request the transport makes — the POST, the SSE GET (initial and
 * resumed), the DELETE, the auth flows — goes to the endpoint the declaration NAMED and follows no redirect.
 * `fetch`'s default follows a 3xx silently and then reports only the last hop, which is the one shape under
 * which the delivery verdict would lie (a connect refusal on the redirect target reading "never sent" while
 * the declared endpoint had received the POST); a 3xx now surfaces as the SDK's HTTP-status error, and a
 * body or a credential never travels to a host the declaration did not name. Exported for its pin only.
 */
export declare const declaredEndpointFetch: typeof fetch;
interface McpContentItem {
    type: string;
    text?: string;
    data?: string;
    mimeType?: string;
    /** `resource_link` block: a pointer to a server resource. */
    uri?: string;
    name?: string;
    description?: string;
    /** `resource` block: an embedded resource (text or binary). */
    resource?: {
        uri?: string;
        mimeType?: string;
        text?: string;
        blob?: string;
    };
}
export declare function mapContent(content: Array<McpContentItem>, serverName?: string, imageResizer?: McpImageResizer): Promise<Array<TextContent | ImageContent>>;
/**
 * Connect to each MCP server, list its tools, and wrap them as AgentTools.
 * Tools are namespaced `mcp__<server>__<tool>` (CC parity) to avoid collisions. The model sees each
 * tool's real JSON-Schema (`inputSchema`), which the agent loop validates natively.
 *
 * Fail-open (design/29): a server that fails to connect or list tools is SKIPPED — its error is
 * returned in `warnings`, the healthy servers still materialize. A single misconfigured server
 * (bad stdio command, unreachable URL) never bricks the whole task.
 *
 * Call `dispose()` when the task finishes — connections are task-scoped, never persisted.
 */
/**
 * Invisible / format / private-use / unassigned characters stripped out of every model-facing string a
 * server advertises — CC `MZg` :144247. CC spells this as the property class BELOW followed by five
 * explicit ranges (zero-width U+200B–U+200F, bidi embedding U+202A–U+202E, bidi isolates U+2066–U+2069,
 * the BOM, and the BMP private-use area); on this engine's Unicode data every one of those code points
 * is already `Cf` or `Co`, so the class alone is byte-equivalent to CC's expression and the source stays
 * free of literal invisible characters (a file carrying them is a grep blind spot, and this repo has
 * paid for that once already). The equivalence is asserted by a pin, not assumed — a Unicode-data change
 * that moved any of those ranges out of the class would open exactly the hole this strips.
 *
 * `\p{Cc}` is deliberately NOT here: newlines and tabs are legitimate description formatting.
 */
export declare const MCP_INVISIBLE_TEXT_RE: RegExp;
/** One tool's schema-normalization outcome (CC 220 `zyo` @336616-336654's return shape). */
export type McpSchemaNormalizeResult = {
    outcome: "unchanged";
} | {
    outcome: "normalized";
    schema: Record<string, unknown>;
    note: string;
    combinators: string[];
} | {
    outcome: "drop";
    reason: string;
};
/**
 * RB-199 FO-2 (form-one audit, CC 2.1.220 `zyo` @336616-336654): flatten a root-level `anyOf`/`oneOf`/
 * `allOf` combinator into a plain `{type:"object", properties, required}` shape a provider's tool-schema
 * validator accepts, INSTEAD of leaving `mcpToolSchemaProblem` below to drop the whole tool. Without this,
 * a server advertising the common "zod union" combinator shape (structurally VALID JSON Schema — the
 * existing gate's root-type/structural checks both pass it) gets forwarded as-is and a strict provider
 * (Anthropic) 400s the ENTIRE model request at the wire — the exact failure `mcpToolSchemaProblem`'s own
 * docstring promises to contain ("a single misconfigured server never bricks the whole task"). A schema
 * with no top-level combinator key is passed through `{outcome:"unchanged"}` — the vast majority of tools
 * never reach the union-handling code below at all (fail-open by construction, matching CC's own early exit).
 * Properties are collected from the top-level `properties` FIRST (so a sibling `properties` block — legal
 * alongside a combinator, and semantically load-bearing — is never shadowed by a same-named combinator-
 * branch property), then from every combinator branch present, in declaration order; the FIRST property of
 * a given name wins (matches CC's own `!(d in r)` guard) — differing branches disagreeing on the SAME
 * property's shape is a real ambiguity CC accepts too, not a sema gap. `required` is unioned from the
 * top-level `required` PLUS `allOf` branches ONLY (never `anyOf`/`oneOf` branches — those are alternatives;
 * unioning their `required` would demand fields the caller may legitimately omit by picking a different
 * branch, exactly the correctness bug CC's own asymmetry avoids). Any exception during the walk (a $ref
 * cycle degenerate enough to defeat the local-only resolver, e.g.) is caught and mapped to `outcome:"drop"`
 * — a normalizer crash is evidence the schema is pathological, not a reason to propagate an exception out
 * of the per-tool intake loop into the per-SERVER catch (same isolation principle `mcpToolSchemaProblem`'s
 * own docstring already states for structural validation).
 */
export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormalizeResult;
/**
 * [cc216] Deterministic intake gate for a server-advertised tool `inputSchema` — returns a
 * CORE-authored reason string when the schema would be rejected at the provider wire, else
 * `undefined` (tool is kept). Deliberately CONSERVATIVE (fail-open): only shapes that are
 * *structurally* unusable are dropped — anything a provider might accept rides through. An ABSENT
 * schema is fine (intake defaults it to `{type:"object"}` below). CC 2.1.216 gates the same prune
 * per-server via remote config (its rollout mechanism, not the mechanism itself); sema's gate is
 * the deterministic predicate alone. RB-199 FO-2: run {@link normalizeMcpToolSchema} on the schema
 * FIRST — a root-level anyOf/oneOf/allOf reaching this gate unflattened is exactly the shape this
 * gate's own docstring promise ("a single misconfigured server never bricks the whole task") existed
 * to catch, and this gate alone does not catch it (no type key ⇒ passes the root-type check; the
 * combinator's own structure is legal JSON Schema ⇒ passes validateJsonSchemaShape too).
 */
/**
 * A schema defect the operator must HEAR about but that must not cost the tool its mount — returns a
 * core-authored sentence, or `undefined` when there is nothing to say.
 *
 * Today that is exactly one thing: a TOP-LEVEL parameter name outside `[a-zA-Z0-9_.-]{1,64}`. A name
 * like `"user name"` is rejected at the Anthropic wire, and because the tool table rides EVERY request
 * it takes the whole request down with it — while the operator previously got nothing at all pointing
 * at the cause (no warning, no drop record, just a task that stopped working). CC checks the identical
 * names against the identical expression (`EX_`/`_xo` :325793/:325824, reported as `check:"propertyKey"`).
 *
 * WHY AN ADVISORY AND NOT A DROP, stated because the sibling structural gate above does drop: CC's own
 * disposition here is keep-and-warn — its drop arm sits behind a rollout flag that ships OFF (`Xyd` /
 * `Yyd` :354641/:354622, an empty remote-config list ⇒ false), so CC's shipped behavior is to keep the
 * tool and warn "requests that include it may fail". The difference from the structural half is real
 * and not a technicality: a schema with no object root is unusable everywhere, whereas this charset is
 * ONE provider's rule, and this engine is bring-your-own-model. Dropping here would delete a working
 * tool from a deployment whose provider accepts the name — a Chinese- or Japanese-named parameter is
 * the ordinary case, not an adversarial one. Detection was the gap; removal was never the mandate.
 *
 * TOP-LEVEL only, like CC: a nested property name is not what the provider validates.
 */
export declare function mcpToolSchemaAdvisory(schema: unknown): string | undefined;
export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer, // design/116 CONFIRM-1 seam: deployment-injected; default = auto-detected sharp
reminderDisclosure?: {
    reminderMark?: string;
    counts?: ReminderDisclosureCounts;
}, mcpRevocations?: {
    isRevoked(serverName: string): boolean;
    onProbeFailure?: (error: unknown) => void;
}): Promise<MaterializedMcp>;
/**
 * Fold the caller's AUTHORITATIVE per-tool override (design F: caller = trust root) over the server-hint axis.
 * Unlike server hints, the caller may RAISE or LOWER any axis: `effect` sets the repeat-safety class (lower to
 * `read`/`idempotent` or raise to `write`); `egress: false` / `irreversibility: "never"` REMOVE a server-added
 * tighten the caller knows is wrong. Returns `undefined` when nothing remains set.
 */
/** Exported for tests — the override algebra above is a contract face (negatives must survive). */
export declare function applyCallerAxisOverride(name: string, hint: McpToolAxis | undefined, override: {
    effect?: ToolEffect;
    egress?: boolean;
    irreversibility?: "always" | "never";
} | undefined): McpToolAxis | undefined;
/** Read a first-page `-32602` message as either meaning (see {@link DIR_READ_NOT_A_DIRECTORY_RE}). Exported
 *  for the discrimination nails; the message is SERVER-authored and is only ever classified here, never
 *  trusted as instructions. */
export declare function classifyDirReadInvalidParams(message: string): "not_found" | "not_directory";
/**
 * The content key {@link walkMcpListPages}'s cursor-loop arm uses to recognize an entry it already
 * collected: the entry's own JSON, which is what the server sent and what a re-served page repeats
 * verbatim. `undefined` for anything unserializable — an entry that cannot be keyed is never treated as
 * a repeat, so the failure direction is "kept, and disclosed by the lane that judges it" rather than
 * "silently gone".
 *
 * NOT a semantic identity: two spellings of the same entry (different key order, a description the
 * server re-rendered) read as different entries here and both survive. That is the safe direction —
 * this key only ever authorizes a DELETION, so it must be exact and never clever.
 *
 * Exported for the walker's own unit pins (same reason {@link walkMcpListPages} is): its bound is a
 * behavior, and a pin that re-declared the bound locally would pass while the real one drifted.
 */
export declare function listEntryFingerprint(entry: unknown): string | undefined;
/**
 * Walk a paginated MCP list method to the END (`{cursor?}` → `{items, nextCursor?}`), bounded.
 *
 * Every spec-standard MCP list method is paginated the same way: absence of `nextCursor` means "that
 * was the last page". A single request therefore returns the WHOLE list only for servers that choose
 * not to paginate — and a server may start paginating at any time, with no protocol change and no
 * malice, the moment its list outgrows its own page size. Treating page one as the complete list is
 * both a capability loss (the rest is never mounted) and, at any call site that DIFFS two listings, an
 * active falsehood about the server's inventory.
 *
 * Contract:
 *  - page ONE is fetched with the caller's own historic parameters (`cursor === undefined`), so a
 *    non-paginating server's wire traffic is byte-identical to before;
 *  - a FIRST-page failure propagates unchanged — every caller's failure shape (skipped server,
 *    `failed` refresh, per-server attributed aggregate error) is built on that throw;
 *  - a CONTINUATION failure keeps the pages already retrieved and reports {@link McpListingIncomplete}
 *    (`page_error`) — fail-open WITH disclosure, the same law {@link readDirViaExtension} follows —
 *    except an ABORT, which still propagates (a cancelled walk is not a short listing);
 *  - a repeated cursor stops the walk (`cursor_loop`) instead of spinning to the cap, AND undoes the
 *    entries that page re-served (a paginator that does not advance is typically one answering with a
 *    page already collected, and keeping it is how one entry gets listed twice) — judged by
 *    `identityOf` where the caller can key its entries, whole-page otherwise; an unreadable cursor
 *    stops the walk as `cursor_invalid` rather than passing for the end of the listing;
 *  - {@link MAX_MCP_LIST_PAGES} pages with a cursor still pending stops it too (`page_cap`);
 *  - a remainder too short to fund a real request stops it as `budget_exhausted` — core's own doing,
 *    named as such (see {@link MIN_MCP_PAGE_BUDGET_MS}).
 * SIZE, stated rather than assumed: a single page has never been bounded here (a server could always
 * answer with an arbitrarily long list), and walking multiplies that existing exposure by at most the
 * page cap — a bounded multiplier, not a new unbounded surface. Per-entry admission stays where it
 * already is (the intake schema gate, which also drops a name a later page repeats).
 *
 * TIME, the reason `budgetMs` exists (confirmed): the page cap bounds REQUESTS, not
 * latency. Giving every page its own full request timeout would let a server that stalls each page hold
 * a connect (or a refresh) for up to twenty timeouts — twenty minutes at the SDK's 60s default, where
 * an unpaginated listing cost one. So the WALK gets the budget a single listing used to have: page one
 * is issued with the full amount (byte-identical to the pre-walk request) and each continuation gets
 * only what is left. ACCEPTED COST, stated: a genuinely slow server whose pages together outlast that
 * bound is truncated where a single-request listing would not have been — but that truncation is
 * DISCLOSED, while the alternative is an undisclosed twenty-fold latency ceiling on the task's startup
 * path. The bound is the caller's own configured request timeout, not a new knob, so a deployment that
 * needs more already has the dial. `fetchPage` receives the remaining budget and MUST spend it as that
 * request's timeout — the deadline is cooperative, since only the caller can pass a timeout to its own
 * transport — and must REPORT that same number if the request then times out: a page cut off at 3ms
 * whose failure text names the whole listing budget describes a wait that never happened.
 *
 * NOT merged with {@link readDirViaExtension}, which walks the same way but owns CC-anchored
 * first-page `InvalidParams` discrimination (not-a-directory vs not-found) and its own three-state
 * flags; folding it in here would put that CC semantics into the standard-method path where it has no
 * meaning.
 *
 * Exported for the walker's own unit pins — its arms are failure/deadline states that no fixture server
 * can drive deterministically. Every production consumer goes through the three call sites in this file.
 */
export declare function walkMcpListPages<T>(fetchPage: (cursor: string | undefined, remainingMs: number) => Promise<{
    items: T[];
    nextCursor?: string;
    cursorInvalid?: boolean;
}>, opts: {
    budgetMs: number;
    onPage?: () => void;
    signal?: AbortSignal;
    /** A key for one entry's CONTENT — equal keys mean the same listing entry, served again.
     *  `undefined` for an entry the caller cannot key. Used ONLY by the cursor-loop arm, to tell a
     *  re-served page from a page that merely arrived with a broken cursor; absent ⇒ that arm cannot
     *  tell them apart and falls back to dropping the whole page (see it for the trade).
     *
     *  CONTENT, not name: an entry's name is what the CALLERS' admission gates key on, and they judge
     *  each entry on its own (a tool whose schema they refuse is dropped, and a later entry of the same
     *  name is then free to mount). Keying this on the name would let this walk delete the second
     *  entry before any of that ran — the only admissible definition of a tool, removed on the theory
     *  that a name cannot appear twice, which is a rule the layer below owns and applies differently. */
    fingerprintOf?: (item: T) => string | undefined;
}): Promise<{
    items: T[];
    incomplete?: McpListingIncomplete;
}>;
/**
 * RB-408 G12 — `tools/call` result parse that accepts the MCP `2026-07-28` widening of
 * `structuredContent` from "a JSON object" to ANY JSON value (the same revision that relaxes
 * `inputSchema`/`outputSchema` to the full JSON Schema 2020-12 keyword set).
 *
 * SDK 1.30.0's `CallToolResultSchema` still types the field as a record, so a spec-legal array / string /
 * number / boolean / null fails validation for the WHOLE result — and the failure takes the `content`
 * blocks down with it: a server returning perfectly usable text plus a scalar `structuredContent`
 * produced nothing but `Invalid tools/call result: expected record, received number`. That is a silent
 * capability loss dressed as a protocol error, and it gets WORSE as servers adopt the revision.
 *
 * The widening is deliberately surgical: `structuredContent` is lifted out, the SDK's own strict schema
 * runs over the remainder (so content-block validation, the `content` default and the loose passthrough
 * of unknown top-level keys are byte-for-byte what they were), and the lifted value is re-attached
 * VERBATIM — same reference, no rebuild. An object-valued `structuredContent` therefore behaves exactly
 * as before, and a malformed `content` array is still rejected: this must not become a validation bypass.
 * Same duck-typing posture as {@link LenientListToolsResultSchema} (no direct `zod` dependency — the SDK's
 * `zod-compat` shim calls `.safeParse(data)` on any object lacking a Zod-4 `_zod` marker).
 *
 * RESIDUAL, SDK-side and deliberately not widened here: `Client.callTool` guards its `outputSchema`
 * validation with a TRUTHINESS test on `result.structuredContent`, so a tool that declares an
 * `outputSchema` and returns a falsy scalar (`0`, `""`, `false`, `null`) is still rejected by the SDK with
 * "has an output schema but did not return structured content". That path is LOUD (never a silent drop),
 * is bounded to outputSchema-declaring tools, and widening it means replacing `callTool` itself — which
 * would mean re-implementing the two guards it exists for (the output validator and the
 * `taskSupport:"required"` refusal, both keyed off SDK-private caches).
 */
export declare function parseCallToolResultLenient(data: unknown): {
    success: true;
    data: unknown;
} | {
    success: false;
    error: unknown;
};