UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

122 lines • 6.17 kB
/** * mcpClientPool — a client per conversation, for one MCP server. * * const orders = mcpClientPool({ * name: 'orders', * transport: () => * agentCoreRuntimeMcpTransport({ agentRuntimeArn, region, session: 'reuse', client: aws }), * }); * const agent = Agent.create({ provider, model }).toolProvider(orders.toolProvider()).build(); * await agent.run({ message, identity: { tenant, principal, conversationId } }); * * Pattern: Facade over one state machine (`pool.ts`), its accounting (`work.ts`), * its definitions (`definitions.ts`) and its bound tools (`boundTool.ts`). * Role: `docs/design/2026-10-mcp-client-pool.md`. A `'reuse'` session costs a * cold start once per conversation instead of once per call — but only * if each conversation has a client of its own, opened once, bounded, * never closed while in use, ended remotely and said to be. This is * the one place that does all of it: * * - one client per authenticated conversation, keyed by the library's * identity encoding of the identity the CALLER named; * - one open per key, under the pool's own signal and `initTimeoutMs`; * - three budgets — open clients, endings, outstanding operations — each * charged until the work under it has SETTLED, and `PoolFullError` at once * when one is full: there is no queue; * - tool definitions per client incarnation by default, shared only under the * app's declared contract, every tool re-checked after each wait and at the * send gate; * - every ending through `terminate()`, with the remote outcome reported, and * `close()` still only local. * * What it does not do: repeat a call, share anything across processes, bound a * remote session it could not end, or make a server enforce per-user * permissions. */ import type { ToolProvider } from '../../tools/toolProvider.js'; import type { McpTerminateOutcome } from '../types.js'; import { type McpPoolLease } from './lease.js'; import { type McpClientPoolOptions, type McpPoolIdentity } from './options.js'; import { type McpPoolStats } from './pool.js'; /** The scope `terminate` and `permissionsChanged` name. */ export interface McpPoolScope { /** Matched exactly, always: an absent tenant is "no tenant", never "every tenant". */ readonly tenant?: string; /** Absent: every principal of the tenant (`permissionsChanged` only). */ readonly principal?: string; /** Absent: every conversation of the principal. */ readonly conversationId?: string; } /** One pool per MCP server: a client per conversation. */ export interface McpClientPool { /** The pool's name: the server. */ readonly name: string; /** * The agent path: a `ToolProvider` (id `mcp-pool:<name>`) whose `list(ctx)` * takes the run's caller identity (`ToolDispatchContext.callerIdentity`) and * returns that conversation's tools, bound to it. A bound tool's `execute` * takes a lease on the key's client for one call, opening the client first * when the key has none; nothing is held between calls. A run with no caller * identity is refused (`no-identity`) — a derived one counts as none. */ toolProvider(): ToolProvider; /** * Hold `identity`'s conversation client — opened or reused — until * `release()`, at most `maxLeaseMs`. The only way to protect a client between * calls (a host holds one around `agent.run()`). * * @throws `McpPoolRefusedError` (`no-identity`, `incomplete-identity`, * `init-timeout`, `init-failed`, `closed`) or `PoolFullError` */ acquire(identity: McpPoolIdentity, options?: { readonly signal?: AbortSignal; }): Promise<McpPoolLease>; /** * End the conversations `scope` names — one (`{ tenant, principal, * conversationId }`) or every one of a principal (`{ tenant, principal }`). * New leases stop at once; the next acquire for a key opens a new client. * In-flight work is waited for up to 5 s; then each client is ended through * `terminate()` once an ending slot and an operation slot are reserved. A * client still leased, or whose slots cannot be reserved yet, is reported * deferred (`onEvent`), never as ended, and is ended when it can be. * * @returns one `McpTerminateOutcome` per client it ended */ terminate(scope: McpPoolScope): Promise<readonly McpTerminateOutcome[]>; /** * Shut down: refuse new work, revoke app leases, abort in-flight calls * through the pool's own signal, then end every client as slots can be * reserved, at most `maxEnding` at a time. Resolves after a bounded wait; * the clients it could not end yet are reported deferred and the work still * unsettled stays charged until it settles (`stats()`). Idempotent. * * @returns one `McpTerminateOutcome` per client it ended */ close(): Promise<readonly McpTerminateOutcome[]>; /** Resolves once every charged operation and every ending has settled. */ settled(): Promise<void>; /** * The app's permission signal: every tool list in the scope stops being * served, and every tool bound to one is refused (`stale-tools`) before it is * sent. Per process: a fleet sends it to every process, or versions its * `authorization` names. */ permissionsChanged(scope: { readonly tenant?: string; readonly principal?: string; }): void; /** The server changed (a deploy hook): every tool list stops being served. */ toolsChanged(): void; /** Run the lazy sweep now: revoked leases, idle ends, deferred ends. No timer is ever installed. */ sweep(): void; /** Counts only — never keys. They report; the budgets enforce. */ stats(): McpPoolStats; } /** * Build a pool for one MCP server. * * @throws TypeError when an option is missing, mistyped or out of range, or when * `connection` is given — here, when the pool is built */ export declare function mcpClientPool(options: McpClientPoolOptions): McpClientPool; //# sourceMappingURL=mcpClientPool.d.ts.map