UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

152 lines (151 loc) • 9.32 kB
/** * The LIVE MCP roster lane: applying one server's intake result to a RUNNING roster, and the * host-driven re-dial that is the second caller of it. * * Two faces change a task's MCP roster after prepare has finished — the model's `RefreshMcpTools` * tool and the run handle's `reconnectMcpServer` verb — and what they must do to the roster is the * same list in the same order: drop the deployment-excluded names, re-fold the safety axes (with the * non-monotone restore arm on refusal), replace the server's whole name-prefix domain at its own * anchor, rebuild the harness toolset, and re-mint the roster delta. That list lived inside the * refresh tool's execute closure, where the second face could only have copied it; a copy would have * been a second answer to "what does a mid-task roster change consist of". * * It is machinery, not a phase: the protocol-tools phase reaches DOWN for it to build the refresh * tool, and the run's verb wiring reaches down for the same functions. */ import type { AgentTool } from "../../internal/harness.js"; import type { McpDroppedTool, McpReconnectResult, MaterializedMcp, McpToolAxis } from "../mcp.js"; import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js"; import type { IrreversibilityTier, ToolFaceSnapshot } from "./contracts.js"; import type { RosterBuilder, ToolRosterDeltaSeat } from "../tool-roster.js"; import type { McpInstructionsEntry } from "../prompt-section-seat.js"; /** One server's assertion about its own prefix domain — the shape both faces reduce to. `tools` is * the roster the domain must END UP with (empty withdraws it); `added`/`removed` are the reporting * half and never drive the swap. `instructions`: the server's current connection's instructions entry, when the face that produced * the delta hands it back (a refresh does; a re-dial queues its new text itself). */ export interface McpRosterDelta { server: string; prefix: string; tools: AgentTool[]; axes: McpToolAxis[]; dropped: McpDroppedTool[]; instructions?: McpInstructionsEntry; } /** The seats a roster application writes through. Every one of them is borrowed from the phase that * minted it — this module owns nothing. */ export interface McpRosterApplyInput { /** borrowed-readonly — the run's spec; only `excludeAllTools` is read: the valve's whole-face switch decides what a live roster * change may mount, beside the selector (`valveLeaves`). */ spec: Pick<TaskSpec, "excludeAllTools">; /** borrowed-readonly — the frozen tool face; its selector keeps a tool the leg may not use from ever * being mounted by a face the model or a host can trigger. */ toolFaceSnapshot: Pick<ToolFaceSnapshot, "selector">; /** borrowed-readonly — the per-server face overlay (the phase's `withFaces`). */ withFaces: (tools: AgentTool[], server: string) => AgentTool[]; /** borrowed-readonly — the large-result wrapper every protocol tool is mounted through. */ remoteToolOffload: (tool: AgentTool) => AgentTool; /** borrowed-readonly — the phase's axis fold (throws on a config contradiction). */ foldProtocolAxes: (axes: McpToolAxis[], label: string) => void; /** borrowed-mutable — the two NON-MONOTONE tables the fold writes and this applier snapshots. */ toolEffects: Map<string, ToolEffect>; axisExplicitNegatives: Map<string, { irreversible?: false; egress?: false; }>; /** borrowed-mutable — the run's shared roster. */ roster: RosterBuilder; /** borrowed-readonly — the run's frozen MCP server declarations: a roster reading is of a declared server, and a name none is declared under holds nothing. */ declared: ReadonlyArray<{ readonly name: string; }>; /** borrowed-mutable — the materialization's instructions delta lane; a roster change settles the server's adds on it. */ instructionsDelta: MaterializedMcp["instructionsDelta"]; } /** What one application did to the server's domain on the roster — read off the roster before and after the swap, the ONE source of * "how many of this server's tools are mounted" for the re-dial's notice and receipt and the refresh tool's receipt. A name the * valve left out, a name the deployment's `toolFaces` overlay refused and a name the server listed but the roster did not take are * all simply absent from `mounted`. */ export interface McpRosterReading { /** The names of the server's tools the roster holds after the application, in wire order. */ mounted: string[]; /** The names `mounted` holds that the roster did not hold before. */ added: string[]; /** The names the roster held before that `mounted` no longer holds. */ removed: string[]; /** The names the server listed that the roster did not take (the valve's, the overlay's). */ excluded: string[]; } /** What one application did, for the caller's receipt. `applied:false` means the axis fold refused * and the domain was left exactly as it was (the tables restored) — no roster write happened, so `mounted` is what it held. */ export type McpRosterApplyOutcome = ({ applied: true; } & McpRosterReading) | ({ applied: false; foldError: string; } & McpRosterReading); /** What the roster holds of one server with no swap made — the reading of an arm that asserted no roster (a failed refresh, a refused re-dial, a fold refusal). * A name no server is declared under holds nothing, even when it mints a declared server's prefix. */ export declare function rosterHolding(input: Pick<McpRosterApplyInput, "roster" | "declared">, serverName: string): McpRosterReading; /** A refresh or re-dial result read off the roster instead of the server's own listing: `toolCount` is the tools mounted, `added` / * `removed` the names that came onto / left the roster, and — on a result that asserts a roster — `tools` the roster's tools of the * server (with `axes` cut to them), so `toolCount === tools.length`. One meaning, the one `MaterializedMcp.refresh` / `reconnect` give * for their own handles. */ export declare function settledOnRoster<T extends { toolCount: number; added: string[]; removed: string[]; tools?: AgentTool[]; axes?: McpToolAxis[]; }>(result: T, reading: McpRosterReading, roster: Pick<RosterBuilder, "tools">): T; /** * Apply one server's delta to the live roster, and settle the server's instruction adds on the delta lane: the teaching answer over the * roster after the swap, against the lane's ledger of what the model holds (both faces — the model's refresh and the host's re-dial — * through this one step). */ export declare function applyMcpRosterDelta(input: McpRosterApplyInput, delta: McpRosterDelta): McpRosterApplyOutcome; /** The seats the post-application commit writes through. */ export interface McpRosterCommitInput { rebuildHarnessToolsRef: { current?: () => Promise<void>; }; rosterSeat: ToolRosterDeltaSeat; toolCallGateArmedRef: { armed: boolean; }; irreversibleTools: ReadonlySet<string>; egressTools: ReadonlySet<string>; } /** * Publish whatever the applications above changed: rebuild the harness toolset (the content-origin * re-wrap runs inside it), then re-mint the roster delta so the stream's rows describe the LIVE * objects. Batched deliberately — a refresh spanning several servers rebuilds once. * * Returns the gate-honesty line when one is due. The built-in integrity policies are composed * UNCONDITIONALLY, so `armed` is always true in this engine and the line never fires today; the arm * stays as a zero-cost guard for a refactor that would make the composition conditional. */ export declare function commitMcpRosterChanges(input: McpRosterCommitInput): Promise<string | undefined>; /** The re-dial's own seats: the handle it drives, the two operator sinks, and the run identity the * disclosure names. */ export interface McpRedialInput extends McpRosterApplyInput, McpRosterCommitInput { mcp: MaterializedMcp; deps: Pick<RunnerDeps, "onError" | "onNotice">; sessionId: string; runId: string; irreversibilityTier: IrreversibilityTier; } /** * The run handle's `reconnectMcpServer` verb, phase-side: re-dial one server, put whatever it * asserted about its prefix domain onto the live roster, and disclose. * * The roster is written IFF the result carries `tools` — the one rule this lane reads. That covers * both directions with no second condition: a successful re-dial asserts its fresh roster, a failed * DIAL asserts the empty one (the connection it tore down is gone, so leaving its names mounted would * leave the model holding tools that can only fail), and an arm that never touched the connection — * unknown name, revoked server, re-dial already in flight, disposed materialization — asserts nothing * and the mounted roster is left alone. * * The result it hands back counts what the ROSTER took: `toolCount` is the server's tools mounted after the swap, `added` / * `removed` the names that came onto / left the roster, and the notice reads the same three — one source for "how many are mounted". */ export declare function redialMcpServer(input: McpRedialInput, server: string): Promise<McpReconnectResult>;