UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

195 lines (194 loc) • 12.3 kB
import { Type } from "typebox"; import { materializeMcpTools } from "../mcp.js"; import { applyMcpRosterDelta, commitMcpRosterChanges, redialMcpServer } from "./mcp-redial.js"; import { materializeA2aTools } from "../a2a.js"; import { deliverEngineNotice } from "../types.js"; import { RosterBuilder } from "../tool-roster.js"; import { TOOL_WIRE_NAME_MAX_CHARS } from "../tool-face.js"; import { REFRESH_MCP_TOOLS_TOOL_NAME, toolFace } from "../tool-catalog-entries.js"; import { MCP_NAMESPACE, A2A_NAMESPACE } from "../protocol-table.js"; import { mintNamespacePrefix } from "../protocol-naming.js"; import { applyMcpToolFaces } from "./tool-face-overlay.js"; import { thrownText } from "../thrown-value.js"; export async function prepareProtocolTools(input) { const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, roster, rosterSeat, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, toolMarkSources, abortController, rollback } = input; const markedBy = (name, source) => { const set = toolMarkSources.get(name); if (set !== undefined) set.add(source); else toolMarkSources.set(name, new Set([source])); }; const overPeer = (lockedPreflight.mcp ?? []).find((s) => s.name.length > TOOL_WIRE_NAME_MAX_CHARS); if (overPeer !== undefined) { throw Object.assign(new Error(`MCP server name ${JSON.stringify(overPeer.name.slice(0, 40) + "…")} is ${overPeer.name.length} characters; a peer rides every roster row of its tools and the roster's bound on a name is ${TOOL_WIRE_NAME_MAX_CHARS}.`), { code: "config.tool_name_too_long" }); } const mcp = lockedPreflight.mcp?.length ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps, runId)) : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], reconnect: async (server) => ({ server, prefix: mintNamespacePrefix(MCP_NAMESPACE, server), outcome: "not_declared", reason: "this task declared no MCP servers", toolCount: 0, added: [], removed: [] }), dispose: async () => { } }; if (lockedPreflight.mcp?.length) rollback.push(function mcpDispose() { return mcp.dispose(); }); for (const w of mcp.warnings) onceLedger.onError(w, { phase: "mcp", sessionId }); for (const d of mcp.droppedTools) { onceLedger.onError(new Error(`MCP tool dropped at intake — ${d.tool} (server "${d.server}"): ${d.reason}`), { phase: "mcp", sessionId }); } onceLedger.pruneDroppedTools(mcp.droppedTools); { const callerNames = new Set((spec.tools ?? []).flatMap((t) => [t.name, ...(t.aliases ?? [])])); const clash = mcp.tools.find((t) => [t.name, ...(t.aliases ?? [])].some((name) => callerNames.has(name))); if (clash) { const e = new Error(`Tool name "${clash.name}" is reserved by an injected MCP tool — a caller tool of the same name would silently shadow it.`); e.code = "config.reserved_tool_name"; throw e; } } const faceAnnounced = new Set(); const withFaces = (tools, serverName) => { const server = (lockedPreflight.mcp ?? []).find((s) => s.name === serverName); return server === undefined ? [...tools] : applyMcpToolFaces({ tools, server, onNotice: deps.onNotice, announced: faceAnnounced, sessionId: sessionId ?? "", runId }); }; { const servers = (lockedPreflight.mcp ?? []).map((s) => ({ name: s.name, prefix: mintNamespacePrefix(MCP_NAMESPACE, s.name) })); const faced = new Map(); for (const server of servers) { const own = mcp.tools.filter((x) => x.name.startsWith(server.prefix)); for (const t of withFaces(own, server.name)) faced.set(t.name, t); } for (const t of mcp.tools) { const server = servers.find((s) => t.name.startsWith(s.prefix)); const mounted = server === undefined ? t : faced.get(t.name); if (mounted === undefined) continue; roster.mount(remoteToolOffload(mounted), { source: "mcp", mountedBy: "optional", origin: { peer: server?.name ?? MCP_NAMESPACE.parse(t.name)?.peer ?? t.name } }); } } const rebuildHarnessToolsRef = {}; const contentOriginWrapRef = {}; const toolCallGateArmedRef = { armed: false }; const rosterApply = { toolFaceSnapshot, withFaces, remoteToolOffload, foldProtocolAxes: (axes, label) => foldProtocolAxes(axes, label), toolEffects, axisExplicitNegatives, roster }; const rosterCommit = { rebuildHarnessToolsRef, rosterSeat, toolCallGateArmedRef, irreversibleTools, egressTools }; const redialInput = { ...rosterApply, ...rosterCommit, mcp, deps, sessionId, runId, irreversibilityTier }; const mcpRedial = lockedPreflight.mcp?.length ? (server) => redialMcpServer(redialInput, server) : undefined; if (lockedPreflight.mcp?.length) { roster.mount({ ...toolFace("refresh-mcp-tools"), name: REFRESH_MCP_TOOLS_TOOL_NAME, label: REFRESH_MCP_TOOLS_TOOL_NAME, description: "Refresh the tool list of connected MCP servers. Never dials or re-dials connections — it only re-reads the tool list over the existing connection. The refreshed tools are available immediately; you can call them on your next step. Use when a server's expected tool is missing, or the tool list looks stale after a connection recovered. Omit `server` to refresh every connected server.", parameters: Type.Object({ server: Type.Optional(Type.String({ description: "Name of a single MCP server to refresh; omit to refresh all connected servers." })), }), execute: async (_toolCallId, params) => { const serverArg = params?.server; const results = await mcp.refresh(typeof serverArg === "string" && serverArg.length > 0 ? serverArg : undefined); const lines = []; let changed = false; let anyActiveFailure = false; const excludedSet = new Set(toolFaceSnapshot.exclude ?? []); for (const r of results) { if (r.tools === undefined) { lines.push(`${r.server}: ${r.status}${r.error !== undefined ? ` (${r.error})` : ""}`); if (r.status === "failed") anyActiveFailure = true; continue; } const applied = applyMcpRosterDelta(rosterApply, { server: r.server, prefix: r.prefix, tools: r.tools, axes: r.axes ?? [], dropped: r.dropped ?? [] }); if (!applied.applied) { lines.push(`${r.server}: failed (${applied.foldError})`); anyActiveFailure = true; continue; } changed = true; const detail = []; const shownAdded = r.added.filter((n) => !excludedSet.has(n)); if (shownAdded.length > 0) detail.push(`added: ${shownAdded.join(", ")}`); if (r.removed.length > 0) detail.push(`removed: ${r.removed.join(", ")}`); if (applied.excluded.length > 0) detail.push(`excluded by deployment config (not mounted): ${applied.excluded.join(", ")}`); if ((r.dropped?.length ?? 0) > 0) detail.push(`dropped: ${r.dropped.map((d) => `${d.tool} (${d.reason.length > 90 ? `${d.reason.slice(0, 90)}…` : d.reason})`).join("; ")}`); lines.push(`${r.server}: refreshed — ${applied.pushed} tool${applied.pushed === 1 ? "" : "s"}${detail.length > 0 ? ` (${detail.join("; ")})` : ""}`); } if (changed) { const ungated = await commitMcpRosterChanges(rosterCommit); if (ungated !== undefined) lines.push(ungated); } const text = lines.length === 0 ? "No connected MCP servers to refresh." : lines.join("\n"); return { content: [{ type: "text", text }], details: { results }, terminate: false, ...(anyActiveFailure && !changed ? { isError: true } : {}), }; }, }, { source: "builtin", mountedBy: "optional" }); } const foldProtocolAxes = (axes, protocolLabel) => { const source = protocolLabel === "A2A" ? "a2a_default" : "mcp_annotation"; for (const axis of axes) { if (axis.irreversibility === "always") { irreversibilityTier.set(axis.name, "always"); irreversibleTools.add(axis.name); markedBy(axis.name, source); } if (axis.irreversibility === "never") { axisExplicitNegatives.set(axis.name, { ...axisExplicitNegatives.get(axis.name), irreversible: false }); } if (axis.egress === false) { axisExplicitNegatives.set(axis.name, { ...axisExplicitNegatives.get(axis.name), egress: false }); } if (axis.egress) { if (axis.effect !== undefined && axis.effect !== "write") { const e = new Error(`${protocolLabel} tool "${axis.name}" resolves to egress:true with effect:"${axis.effect}" — an egress tool (external write) must have effect:"write". Clear egress (toolAxes egress:false) if it is a pure read, or set effect:"write".`); e.code = "config.egress_requires_write_effect"; throw e; } egressTools.add(axis.name); markedBy(axis.name, source); } if (axis.effect !== undefined && !toolEffects.has(axis.name)) toolEffects.set(axis.name, axis.effect); } }; foldProtocolAxes(mcp.toolAxes, "MCP"); const a2aDeclared = spec.a2a?.length ? spec.a2a : undefined; const a2a = a2aDeclared ? await materializeA2aTools(a2aDeclared, spec.principal, abortController.signal) : { tools: [], toolAxes: [], warnings: [], statuses: [], refresh: async () => [], dispose: async () => { } }; if (a2aDeclared) rollback.push(function a2aDispose() { return a2a.dispose(); }); for (const w of a2a.warnings) deps.onError?.(w, { phase: "a2a", sessionId }); { const callerNames = new Set((spec.tools ?? []).flatMap((t) => [t.name, ...(t.aliases ?? [])])); const clash = a2a.tools.find((t) => callerNames.has(t.name)); if (clash) { const e = new Error(`Tool name "${clash.name}" is reserved by an injected A2A tool — a caller tool of the same name would silently shadow it.`); e.code = "config.reserved_tool_name"; throw e; } } for (const t of a2a.tools) roster.mount(remoteToolOffload(t), { source: "a2a", mountedBy: "optional", origin: { peer: A2A_NAMESPACE.parse(t.name)?.peer ?? t.name } }); foldProtocolAxes(a2a.toolAxes, "A2A"); return { mcp, a2a, rebuildHarnessToolsRef, contentOriginWrapRef, toolCallGateArmedRef, ...(mcpRedial !== undefined ? { mcpRedial } : {}) }; } function mcpRevocationWiring(deps, runId) { if (deps.mcpRevocations === undefined) return undefined; const ledger = deps.mcpRevocations; return { isRevoked: (name) => ledger.isRevoked(name), onProbeFailure: (e) => deliverEngineNotice(deps.onNotice, { code: "mcp.revocation_probe_failed", message: `the mcpRevocations.isRevoked probe threw — MCP dispatch fails OPEN (no server treated as revoked) until the probe recovers: ${thrownText(e)}`, detail: { message: thrownText(e), runId }, }), }; }