UNPKG

agentfootprint

Version:

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

150 lines • 7.07 kB
"use strict"; /** * listTools — one conversation's tools, bound to it: one listing operation. * * Pattern: one admitted operation per call, the provider's and the lease's alike. * Role: § 3.2 and § 6 of the design. A listing is one `list(ctx)` for one * conversation, admitted against `maxOperations`. Its setup — the open * of the key's client, the `tools/list` — runs inside its slot. * * - PER CLIENT: the conversation's own client is opened (or reused) and its own * list served, listed again when it cannot be served as it stands. * - SHARED, under the app's contract: the plan is resolved and the context * computed WITHOUT opening anything; a current entry is served touching no * client (`client: 'not-needed'`); a listing already under way for it is * joined; otherwise the listing runs on THIS conversation's own client — * never in another conversation's session. * - THE CALLER'S CONTEXT AFTER EVERY WAIT: the app's names are asked again * after a joined listing and after this one's own (`Pool.ensureListed`); a * list is served only under the context the caller has at that moment, and * a moved context is resolved again — at most three rounds, then refused * (`binding-changed`). * * A refused listing never reaches the model as a tool result: it fails that * iteration's tool discovery (`agentfootprint.tools.discovery_failed`), as any * failing `ToolProvider` does. A list that cannot be read to its end * (`McpToolListIncompleteError`, from the client's own discovery) is never * served cut short. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.listTools = void 0; const abort_js_1 = require("../../lib/abort.js"); const toolSessions_js_1 = require("../../tools/toolSessions.js"); const sessionEnded_js_1 = require("./sessionEnded.js"); const boundTool_js_1 = require("./boundTool.js"); const plan_js_1 = require("./plan.js"); /** List `key`'s conversation's tools, bound to it, under one admitted operation. */ async function listTools(pool, key, signal, facts) { facts.keyHash = key.keyHash; pool.interact(); const operation = pool.admit('listing', 'listing', signal); try { for (let round = 0; round < 3; round += 1) { const current = pool.clients.currentFor(key.key); let plan; if (current === undefined) { try { plan = (0, plan_js_1.resolvePlan)(pool.settings, key); } catch (cause) { throw pool.refused('init-failed', cause); } } // A key with a client: the app's names asked again, so a changed // authorization reaches this conversation as it would a new one. const definitions = current === undefined ? plan : pool.refreshContext(current); if (definitions.shared) { const served = await sharedWithoutClient(pool, key, definitions.context, operation.signal); if (served !== undefined) { // After that wait, the caller's names once more: served only under // the context they give now — otherwise resolved again. if (!callerStillIn(pool, key, plan, served.context)) continue; facts.client = 'not-needed'; facts.definitions = 'shared'; facts.definitionsAgeMs = Math.max(0, pool.now() - served.list.listedAt); return bindAll(pool, key, served, undefined); } } const client = await pool.leaseClient(key, operation, facts, plan); let served; try { // Under the client's context of now, checked with no wait since. served = await pool.ensureListed(client, operation); } catch (error) { throw classifiedListing(pool, client, facts, error); } facts.definitions = served.shared ? 'shared' : 'listed'; if (served.shared) facts.definitionsAgeMs = Math.max(0, pool.now() - served.list.listedAt); return bindAll(pool, key, served, client); } // The app's names kept moving under this listing. throw pool.refused('binding-changed'); } finally { operation.finish(); } } exports.listTools = listTools; /** * Do the caller's names still give `context` — asked now? Through the key's * client when it has one by now, else over the plan's configuration. False * when `share` cannot answer: the next round says why. */ function callerStillIn(pool, key, plan, context) { const current = pool.clients.currentFor(key.key); if (current !== undefined) return pool.contextHolds(current, context); if (plan === undefined) return false; try { return (0, plan_js_1.contextFor)(pool.settings, key, plan.configHash).context === context; } catch { return false; } } /** * The shared entry for `plan`, when it can be served without touching a client * — at once, or once a listing already under way for it is done. */ async function sharedWithoutClient(pool, key, context, signal) { const entry = pool.shared.entryFor(context, key.tenant); const servable = () => entry.list.servable(pool.now(), pool.settings.definitionTtlMs); if (!servable() && entry.list.listing !== undefined) { // Joined inside this operation's own slot; if it fails, this one lists itself. await (0, abort_js_1.untilAborted)(entry.list.listing, signal).catch(() => undefined); } return servable() ? { list: entry.list, shared: true, context } : undefined; } /** Every definition of `served`, bound to `key` and to the list it came from. */ function bindAll(pool, key, served, client) { const base = { key, context: served.context, shared: served.shared, listId: served.list.id, generation: served.list.generation, ...(!served.shared && client !== undefined && { clientId: client.clientId }), }; return served.list.all().map((definition) => (0, boundTool_js_1.boundTool)(pool, { ...base, definition })); } /** A listing that failed because the client's session expired or its handle was lost. */ function classifiedListing(pool, client, facts, error) { if (client.lost === 'expired') { facts.client = 'expired'; facts.evidence = 'server-404'; return new sessionEnded_js_1.ServerEndedSessionError(pool.settings.name, client.clientId, error); } if (client.lost === 'handle-lost') { facts.client = 'handle-lost'; facts.evidence = 'transport-forgot-handle'; return (0, toolSessions_js_1.isLocalSessionHandleLost)(error) ? error : new toolSessions_js_1.LocalSessionHandleLostError({ cause: error }); } return error; } //# sourceMappingURL=listTools.js.map