agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
95 lines (94 loc) • 5.03 kB
TypeScript
/**
* terminate — ending a client's remote session, and saying what the remote side
* answered.
*
* Pattern: a pass-through `fetch` that reads one status + one bounded procedure.
* Role: `McpClient.terminate()` (`mcpClient.ts`). The MCP SDK's
* `StreamableHTTPClientTransport.close()` only aborts the requests in
* flight; `terminateSession()` is the one method that sends the
* Streamable HTTP `DELETE` carrying `Mcp-Session-Id`. It treats a 405
* as success, so the status is read here, by a pass-through `fetch`
* the library installs on every transport it builds — it changes no
* request and reads only a `DELETE`'s answer.
*
* The procedure, at most once per client:
*
* 1. no session id was ever issued → `'no-session'`, nothing is sent;
* 2. otherwise the transport's own `terminateSession()`, bounded by
* `TOOL_TEARDOWN_TIMEOUT_MS` (the tool-session tier's `withTimeout`, shared):
* at the deadline the request is aborted and the outcome is `'timed-out'`;
* 3. a local close, whatever happened.
*
* ONE DEADLINE ANSWERS THE CALLER: the remote answer and the local close are
* waited for together for at most `TOOL_TEARDOWN_TIMEOUT_MS` of ELAPSED time,
* measured by timers started together at the start — never by reading the wall
* clock, so a clock stepped back cannot stretch it. A close that finishes in
* time is finished before the caller hears; one that hangs does not hold the
* answer. The work's own settlement — the `DELETE`, a `fetch` that ignores its
* abort, the close — may come later, and is returned apart (`settled`): the MCP
* client pool keeps an ending charged until then. The clock (`now`) only
* measures `durationMs`.
*
* Never vendor-aware: the one port is the Streamable HTTP `DELETE`. A transport
* that turns it into something else (AgentCore Runtime's `StopRuntimeSession`)
* answers it like a server would.
*/
import type { McpClient, McpTerminateOutcome } from './types.js';
/** An `McpClient` that can end its remote session. */
export type TerminableMcpClient = McpClient & {
terminate(): Promise<McpTerminateOutcome>;
};
/**
* Can `client` end its remote session? Reads the member, never assumes it —
* the feature-detection law (`provider-contract/types.ts` · `canStageCodeInputs`).
* True for `mcpClient()` and `mockMcpClient()`; false for a hand-written client
* without `terminate`.
*/
export declare function canTerminate(client: unknown): client is TerminableMcpClient;
/** What ending one client came to: the caller's answer, and when the work under it settled. */
export interface Ending {
/** Answers by `TOOL_TEARDOWN_TIMEOUT_MS`. Never rejects. */
readonly outcome: Promise<McpTerminateOutcome>;
/** Resolves once every piece of work the ending started has settled. Never rejects. */
readonly settled: Promise<void>;
}
/** The slice of the SDK's Streamable HTTP transport the procedure uses. */
export interface TerminableTransport {
readonly sessionId?: string;
terminateSession(): Promise<void>;
}
/**
* What the pass-through `fetch` saw of a transport's `DELETE` requests: the
* status of the latest answer, and the signal it links into each one while an
* ending is under way.
*/
export declare class DeleteWatch {
private status;
private cut;
/** Arm for one ending: forget the last status, link `cut` into its `DELETE`. */
arm(cut: AbortSignal): void;
/** The status the latest `DELETE` was answered with, if one was. */
get answered(): number | undefined;
/**
* `inner` (the global `fetch` when `undefined`, looked up at call time),
* passed through untouched except that a `DELETE`'s answer status is read and
* an armed ending's signal is linked into it.
*/
fetch(inner: ((input: string | URL, init?: RequestInit) => Promise<Response>) | undefined): (input: string | URL, init?: RequestInit) => Promise<Response>;
}
/** The outcome for an ending that sent nothing. */
export declare function outcomeWithout(remote: 'no-session' | 'unavailable', durationMs?: number): McpTerminateOutcome;
/**
* An ending that sends nothing — `remote` says why — only the local close:
* the caller is answered when the close finishes or at `timeoutMs`, whichever
* comes first; `settled` waits for the close itself.
*/
export declare function endLocally(remote: 'no-session' | 'unavailable', close: () => Promise<void>, now?: () => number, timeoutMs?: number): Ending;
/**
* End `transport`'s session, then close locally with `close` — the procedure
* in the module note. `watch` is the `DeleteWatch` installed on the transport's
* fetch. `now` is the clock the duration is measured with.
*/
export declare function endSession(transport: TerminableTransport, watch: DeleteWatch, close: () => Promise<void>, now?: () => number, timeoutMs?: number): Ending;
/** A failure's class name, never its message. */
export declare function classOf(error: unknown): string;