UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

90 lines (89 loc) • 6.16 kB
import { valveLeaves } from "../../agents/tool-filter.js"; import { instructionsTaught, instructionsTold, tellInstructions, toolNamesOfServer, withdrawInstructionAdds } from "../mounted-mcp.js"; import { deliverEngineNotice } from "../types.js"; import { thrownText } from "../thrown-value.js"; export function rosterHolding(input, serverName) { const mounted = input.declared.some((d) => d.name === serverName) ? toolNamesOfServer(input.roster.tools.map((t) => t.name), serverName) : []; return { mounted, added: [], removed: [], excluded: [] }; } export function settledOnRoster(result, reading, roster) { const settled = { ...result, toolCount: reading.mounted.length, added: reading.added, removed: reading.removed }; if (result.tools === undefined) return settled; const held = new Set(reading.mounted); return { ...settled, tools: roster.tools.filter((t) => held.has(t.name)), ...(result.axes !== undefined ? { axes: result.axes.filter((a) => held.has(a.name)) } : {}) }; } function settleInstructions(lane, delta, taught) { if (!taught) return withdrawInstructionAdds(lane, delta.server); if (delta.instructions !== undefined && !instructionsTold(lane, delta.server) && !lane.pendingAdds.some((e) => e.server === delta.server)) tellInstructions(lane, delta.instructions); } export function applyMcpRosterDelta(input, delta) { const { spec, toolFaceSnapshot, withFaces, remoteToolOffload, foldProtocolAxes, toolEffects, axisExplicitNegatives, roster } = input; const domainNow = () => toolNamesOfServer(roster.tools.map((t) => t.name), delta.server); const before = domainNow(); const leaves = valveLeaves(spec, toolFaceSnapshot.selector); const pushable = delta.tools.filter(leaves); const valveExcluded = delta.tools.filter((t) => !leaves(t)).map((t) => t.name); const domainSnapshot = (m) => new Map([...m].filter(([name]) => name.startsWith(delta.prefix))); const restoreDomain = (m, snap) => { for (const name of [...m.keys()]) if (name.startsWith(delta.prefix)) m.delete(name); for (const [name, v] of snap) m.set(name, v); }; const priorDomainEffects = domainSnapshot(toolEffects); const priorDomainNegatives = domainSnapshot(axisExplicitNegatives); for (const name of priorDomainEffects.keys()) toolEffects.delete(name); for (const name of priorDomainNegatives.keys()) axisExplicitNegatives.delete(name); try { foldProtocolAxes(delta.axes.filter((a) => !valveExcluded.includes(a.name)), "MCP"); } catch (foldErr) { restoreDomain(toolEffects, priorDomainEffects); restoreDomain(axisExplicitNegatives, priorDomainNegatives); settleInstructions(input.instructionsDelta, delta, instructionsTaught(leaves, delta.server, roster.tools, delta.tools)); return { applied: false, foldError: thrownText(foldErr), ...rosterHolding(input, delta.server) }; } roster.replaceDomain(delta.prefix, withFaces(pushable, delta.server).map((t) => remoteToolOffload(t)), { source: "mcp", mountedBy: "optional", origin: { peer: delta.server } }); const mounted = domainNow(); const heldNow = new Set(mounted); const heldBefore = new Set(before); settleInstructions(input.instructionsDelta, delta, instructionsTaught(leaves, delta.server, roster.tools, delta.tools)); return { applied: true, mounted, added: mounted.filter((n) => !heldBefore.has(n)), removed: before.filter((n) => !heldNow.has(n)), excluded: delta.tools.map((t) => t.name).filter((n) => !heldNow.has(n)) }; } export async function commitMcpRosterChanges(input) { await input.rebuildHarnessToolsRef.current?.(); input.rosterSeat.republish(); if (!input.toolCallGateArmedRef.armed && (input.irreversibleTools.size > 0 || input.egressTools.size > 0)) { return "WARNING: refreshed tools declare irreversible/egress safety hints, but this task started with no approval gate registered (no policy/hooks and no gated tools at start) — these hints cannot arm a gate mid-task (RB-46); the refreshed tools run ungated on this deployment shape."; } return undefined; } export async function redialMcpServer(input, server) { const listed = await input.mcp.reconnect(server); let result = settledOnRoster(listed, rosterHolding(input, listed.server), input.roster); if (listed.tools !== undefined) { const outcome = applyMcpRosterDelta(input, { server: listed.server, prefix: listed.prefix, tools: listed.tools, axes: listed.axes ?? [], dropped: listed.dropped ?? [] }); result = settledOnRoster(listed, outcome, input.roster); if (!outcome.applied) input.deps.onError?.(Object.assign(new Error(`MCP re-dial of "${server}" connected but its safety-axis fold refused, so its tools were not mounted: ${outcome.foldError}`), { code: "mcp.server_unavailable" }), { phase: "mcp", sessionId: input.sessionId }); const ungated = await commitMcpRosterChanges(input); if (ungated !== undefined) input.deps.onError?.(Object.assign(new Error(ungated), { code: "mcp.server_unavailable" }), { phase: "mcp", sessionId: input.sessionId }); } for (const w of result.warnings ?? []) input.deps.onError?.(Object.assign(new Error(w), { code: "mcp.server_unavailable" }), { phase: "mcp", sessionId: input.sessionId }); deliverEngineNotice(input.deps.onNotice, { code: "mcp.server_redialed", message: result.outcome === "reconnected" ? `MCP server "${server}" was re-dialed at the host's request: ${result.toolCount} tool${result.toolCount === 1 ? "" : "s"} mounted (${result.added.length} added, ${result.removed.length} removed)` : `MCP server "${server}" was NOT re-dialed (${result.outcome})${result.reason !== undefined ? `: ${result.reason}` : ""}`, detail: { server, outcome: result.outcome, added: result.added.length, removed: result.removed.length, ...(result.reason !== undefined ? { reason: result.reason } : {}), sessionId: input.sessionId, runId: input.runId }, }); return result; }