agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
214 lines (213 loc) • 12.1 kB
TypeScript
/**
* toolExtras — agentfootprint's own tool DECLARATIONS, carried over MCP.
*
* A `Tool` is two different things at once. Half of it is EXECUTION — the
* handler, the credential it needs, the human it wants to check in with. The
* other half is DECLARATION — flat, inert facts about the tool that no code
* runs and every consumer-side rail READS: where its arguments come from, what
* kind of artifact its result is, who owns it, what class of answer it gives,
* how big a result is too big.
*
* Over MCP only the execution half used to cross. A tool served by `mcpServe`
* arrived at a client as a name, a description and an input schema, so every
* rail that reads a declaration went quiet for it: the dangling-reference and
* unsupported-argument checks never armed, placement minted
* `tool-result/<name>` that no `wants` could spend, and the identity joins had
* no subject. An MCP server could be somebody's whole tool catalogue and still
* be a second-class citizen of every check this library ships.
*
* ── The carrier ─────────────────────────────────────────────────────────
* MCP's own extensibility bag: `Tool._meta`, under ONE namespaced key
* ({@link MCP_TOOL_EXTRAS_KEY}). `_meta` is declared on the spec's `Tool`
* object (`@modelcontextprotocol/sdk` types it as
* `z.record(z.string(), z.unknown()).optional()` on `ToolSchema`), so it
* survives the SDK's own `tools/list` validation in both directions and needs
* no new protocol, no side channel and no version negotiation. A server that
* has never heard of agentfootprint sends no bag; a client that has never
* heard of it ignores the key.
*
* ── What may travel, and the bar ────────────────────────────────────────
* **A declaration a consumer-side check or rail reads; nothing that governs
* execution.** Every field below passes it. `needs` (credentials), `checkIn`
* (human consent) and the session hooks do NOT and never will: they govern how
* a tool RUNS, and the tool runs on the server. Sending them would tell a
* client to hold a gate that the only executor of the tool has already held,
* or — worse — that nobody is holding at all. `mcpServe` already refuses to
* serve a `checkIn` tool for exactly that reason; this list is the same law,
* stated on the way out.
*
* ── The asymmetry that matters ──────────────────────────────────────────
* SERVING validates nothing new: the tool was built by `defineTool`, which
* already refused every malformed declaration at definition time.
*
* INGEST validates everything and THROWS NOTHING. A bag comes from a server
* this process does not control, and one server's typo must not take down a
* bulk register of forty tools. So each field is judged by the SAME rule
* `defineTool` enforces (literally the same exported assert), and a field that
* fails is warned about once — naming the server, the tool, the field and the
* rule — and DROPPED. The tool still registers, without that one declaration.
* That is the honest outcome: the rail it would have armed stays unarmed, and
* the developer is told which rail and why.
*/
import { type Tool, type ToolOwner, type ToolResultCeiling, type ToolResultColumns } from '../tools/tools.js';
import type { ToolResultClass } from '../semantics/types.js';
import { type AskOrAssume, type ToolPeriod } from '../tools/declare.js';
import { type ToolScript } from '../tools/script.js';
import type { ToolSearch } from '../semantics/coverage/search/types.js';
import { type TimeContext } from '../time/wire.js';
/**
* The single `_meta` key every agentfootprint declaration travels under.
*
* Namespaced so it can never collide with another vendor's `_meta` entry, and
* a CONSTANT so there is exactly one spelling in the codebase — the
* `branchSegment.ts` precedent upstream: one owner of the grammar, and nobody
* else may write the string.
*/
export declare const MCP_TOOL_EXTRAS_KEY = "agentfootprint";
/**
* The declarations that cross an MCP boundary — the plain-data half of a
* `Tool`, in the shape the receiving side puts straight back onto a `Tool`.
*
* Every field is optional and ABSENT MEANS ABSENT: a tool that declares
* nothing sends no bag at all, and a tool that declares two sends two keys,
* never five with three empty placeholders. "Said nothing" and "said empty"
* are different statements everywhere else in this library, and they are
* different here.
*
* @see MCP_TOOL_EXTRAS_KEY for the `_meta` key it rides under.
*/
export interface McpToolExtras {
/** Where this tool's arguments come from — see {@link Tool.argumentsFrom}.
* Arms the dangling-reference and unsupported-argument checks. */
readonly argumentsFrom?: readonly string[];
/** The artifact kind a PLACED result is minted under — see
* {@link Tool.resultKind}. What makes a placed result spendable by `wants`. */
readonly resultKind?: string;
/**
* What this tool's ROWS contain (9.78.0) — see {@link Tool.resultColumns}.
* Arms the write seam's `column-type-mismatch` and `missing-column` checks.
*
* It clears the bar this file states, and by the widest margin of the eight.
* It is a flat, inert fact about the RESULT that a consumer-side check
* reads; it governs nothing about how the tool runs, and the tool runs on
* the server. It is also the field case for carrying declarations at all: a
* remote catalogue of rowset tools is exactly where a numeric column
* arriving as text goes unnoticed, because the consumer holding the chart
* has no way to know what the producer meant. Leaving it behind would arm
* the check for local tools and leave every MCP tool a second-class citizen
* of it — the whole defect `toolExtras` was built to end.
*/
readonly resultColumns?: ToolResultColumns;
/** The identity edge integrity checks join on — see {@link Tool.owner}. */
readonly owner?: ToolOwner;
/** The declared class of this tool's results — see {@link Tool.resultClass}. */
readonly resultClass?: ToolResultClass;
/** The author's refusing ceiling on this tool's result — see
* {@link Tool.resultCeiling}. */
readonly resultCeiling?: ToolResultCeiling;
/** The named ingredient tools this tool is composed of (9.76.0) — see
* {@link Tool.composedOf}. Feeds the receiving agent's build-time drift
* gate. */
readonly composedOf?: readonly string[];
/** Whether this tool's procedure can raise an approval gate (9.76.0) — see
* {@link Tool.gates}. Read by composition-time checks that must keep a
* gating tool out of a fan-out branch. */
readonly gates?: boolean;
/**
* The per-argument rules (the inputs layer, honesty layer 2) — see
* {@link Tool.askOrAssume}. It passes this file's bar: the fill and the
* batch ask happen in the CLIENT's own loop before the call
* is sent — exactly as the client applies a served `resultCeiling` — and
* nothing about how the server runs the tool changes. The FIRST extra
* judged against the tool's schema, which is why the origin carries the
* listed tool's `inputSchema`. A client that ignores `_meta` sees the
* author's `required` and sends the value, or the server applies its own
* default.
*/
readonly askOrAssume?: AskOrAssume;
/** Which arguments set the period the answer covers, the forms they take
* and the facts about the source — see {@link Tool.period}. Judged
* against the listed tool's own `inputSchema` and the KEPT `askOrAssume`
* (every argument a form names must carry a kept rule). A host tool
* written in another language declares the same object, field for field. */
readonly period?: ToolPeriod;
/**
* What one call of this tool searches — see {@link Tool.search}. It passes
* this file's bar: composition is a consumer-side reading of the call's
* arguments and governs nothing about how the server runs the tool. Judged
* against the listed tool's own `inputSchema` and the KEPT `period`. A kept
* remote declaration composes rows marked `composed: 'remote'`, which
* establish nothing: the server's words are untrusted.
*/
readonly search?: ToolSearch;
/**
* This tool RUNS a program, carried by its one string argument `argument`
* — see {@link Tool.script}. It passes this file's bar: a consumer-side
* record reads it (which program a call submitted, matched against what the
* answer shows) and it governs nothing about how the server runs the tool.
* Judged against the listed tool's own `inputSchema`.
*/
readonly script?: ToolScript;
}
/**
* The `_meta` bag a `tools/call` request carries for a call handed
* `ctx.time` (the time layer, `time/wire.ts` · `TimeContext`): the
* same object, under the one namespaced key — so a host tool in another
* process can declare the `period.queried` its read covered without parsing
* its own argument.
*/
export declare function callMetaOf(time: TimeContext): Readonly<Record<string, unknown>>;
/**
* The call's time a `tools/call` request carried in its `_meta` bag, read —
* or `undefined` when it carried none, or one this runtime cannot read (a
* different version, a malformed field): never repaired, never guessed. What
* `mcpServe` hands a served tool as `ctx.time`.
*/
export declare function callTimeOf(meta: unknown): TimeContext | undefined;
/**
* The declarations on `tool`, ready to be written into an MCP `_meta` bag —
* or `undefined` when the tool declares none, so `mcpServe` can leave `_meta`
* off the listing entirely rather than sending an empty object.
*
* Copied verbatim, never inferred or defaulted. This is the SERVE side, and
* the values were already vetted by `defineTool`.
*/
export declare function toolExtrasOf(tool: Tool): McpToolExtras | undefined;
/** Where a rejected declaration came from, for the warning that names it. */
export interface McpToolExtrasOrigin {
/** The client's logical name — `mcpClient({ name })`. */
readonly server: string;
/** The tool the bag was attached to. */
readonly tool: string;
/**
* The listed tool's own `inputSchema` — what `askOrAssume` and `period` are
* judged against (each ruled property by its OWN schema). Absent → those
* two fields are judged against an empty schema, so every rule is dropped
* with a warning rather than trusted unjudged.
*/
readonly inputSchema?: Readonly<Record<string, unknown>>;
}
/**
* Read an MCP `Tool._meta` bag into declarations a `Tool` can carry.
*
* **Never throws.** Everything here is somebody else's data:
*
* - no `_meta`, no `agentfootprint` key, or a non-object under it → `{}`,
* and the ingest is byte-identical to every release before this existed;
* - a field the receiving side does not know → ignored in silence (a NEWER
* server talking to an OLDER client is not an error, and warning about it
* would make every upgrade noisy at the wrong end);
* - a field this library DOES know but cannot honour → warned once and
* dropped, and the rest of the bag still lands.
*
* The rule each field is judged by is the exported assert `defineTool` itself
* calls, so a declaration that would have been refused at definition time is
* refused here too, with the same sentence.
*/
export declare function readToolExtras(meta: unknown, origin: McpToolExtrasOrigin): McpToolExtras;
/**
* Forget every warning issued so far.
* @internal test seam — the ledger is process-wide and warn-ONCE, so a test
* proving the warning fires must be able to start from silence.
*/
export declare function _resetToolExtrasWarnings(): void;