@sema-agent/core
Version:
Stateless, task-oriented AI agent core
69 lines (68 loc) • 5.07 kB
TypeScript
import type { ProtocolNamespace } from "./protocol-table.js";
/** The provider's tool-name pattern is `^[a-zA-Z0-9_-]{1,64}$` — the limit applies to the ASSEMBLED name,
* not to its segments (design/116 W5-5, CC 2.1.187 parity `buildMcpToolName`). */
export declare const TOOL_NAME_MAX_CHARS = 64;
/** Reserved for the tool segment when budgeting the peer segment: a peer kept at the full name budget
* overflows the assembled name on its own (prefix + 64 + separator = 71 > 64), which is how the tool
* segment used to be handed a NEGATIVE budget. A tool name shorter than this is not recognizable. */
export declare const MINTED_TOOL_SEGMENT_MIN_CHARS = 16;
/**
* design/116 W5-5 (CC 2.1.187 parity) — normalize a peer/tool segment to the API tool-name charset by
* replacing every other character with `_`. CC does exactly this on BOTH segments when composing
* `mcp__<server>__<tool>` (services/mcp/normalization.ts `normalizeNameForMCP` + mcpStringUtils.ts
* `buildMcpToolName`), so a peer advertising a dotted/spaced/unicode name can't mint a name the provider
* rejects. NOTE: only the MODEL-FACING namespaced name is normalized — the raw remote name still goes on
* the wire and still keys the caller-facing maps (`allowTools`/`toolAxes`). The collision property is
* inherent (two remote names that normalize to the same string mint the same string; CC accepts this at
* the same layer) — what this engine does NOT accept is two PEERS colliding, because a peer prefix is a
* live domain key here (the refresh splice). That case is decided by
* {@link findNamespacePrefixCollision} and refused at the mount, loudly; the mint stays total.
*
* The pattern this enforces is `{1,64}`, and only the charset half was
* enforced. Both other halves matter for the same reason the charset does — an over-long or empty segment
* produces a name the provider rejects, which 400s the WHOLE model request, the exact failure the intake
* gate exists to contain ("a single misconfigured server never bricks the whole task").
*/
export declare function normalizeNameSegment(name: string): string;
/** Force a normalized segment into `{1,max}`, keeping long names distinct via a stable digest. */
export declare function clampNameSegment(seg: string, max?: number): string;
/**
* The registered-name prefix for `peer` in `ns` — i.e. the set `name.startsWith(prefix)` that a consumer
* uses as its splice domain when swapping one peer's tools wholesale.
*
* The peer segment is kept as long as the assembled name allows (it is how an operator recognizes which
* peer a tool came from) and absorbs the clamp, digest-suffixed, when it does not fit.
*
* NOT `ns.makeName`: that leg REFUSES a peer carrying the separator, while this pipeline SANITIZES its
* inputs (the peer name is deployment/remote data, not a caller-authored identifier) and must not turn a
* long or exotic peer name into a materialization failure. SANITIZE, however, means the sanitized result
* must be sound: {@link settlePeerSegment} owes the same round-trip guarantee `makeName` gets by refusing
* (ticket #10), and the property test holds the two legs to the identical law.
*/
export declare function mintNamespacePrefix(ns: ProtocolNamespace, peer: string): string;
/**
* The first pair of peers in `peers` that mint the SAME {@link mintNamespacePrefix} — i.e. that would
* register into one indistinguishable namespace domain — or `undefined` when every peer owns its own.
*
* Two spellings collide whenever charset normalization, the length clamp or the separator fold maps
* them together: `"prod.db"` and `"prod_db"` both mint `mcp__prod_db__`, and the same name listed twice
* collides trivially. The mint itself cannot refuse (its input is deployment/remote data and a dotted
* name must not become a materialization failure — see {@link normalizeNameSegment}), so the refusal
* belongs to the MOUNT, which is the layer that knows the whole peer list. This function is that
* layer's decision procedure: pure name arithmetic, decidable before any I/O.
*
* Why a collision cannot be tolerated downstream: the prefix IS the domain key. A refresh splices
* `name.startsWith(prefix)` out and re-inserts only the refreshed peer's listing, so refreshing one of
* two colliding peers silently unmounts the other's tools; equal tool names additionally shadow each
* other last-write-wins in the harness map. Both failures are invisible at the moment they happen.
*/
export declare function findNamespacePrefixCollision(ns: ProtocolNamespace, peers: readonly string[]): {
prefix: string;
peers: [string, string];
} | undefined;
/**
* The full registered name for `(peer, tool)` in `ns`. Always starts with {@link mintNamespacePrefix}'s
* answer for the same peer (the invariant ticket #9's pins hold), and never exceeds
* {@link TOOL_NAME_MAX_CHARS}: whatever the peer segment leaves over is the tool segment's budget.
*/
export declare function mintNamespacedToolName(ns: ProtocolNamespace, peer: string, tool: string): string;