@sema-agent/core
Version:
Stateless, task-oriented AI agent core
195 lines (194 loc) • 12.3 kB
JavaScript
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 },
}),
};
}