UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

438 lines (437 loc) • 21.2 kB
import { types } from "node:util"; import { resolveModel } from "../roles.js"; import { brainToRuntime } from "../runtime.js"; import { deliverEngineNotice } from "../engine-notice.js"; import { inlineUntrusted } from "../untrusted-text.js"; import { extensionWords } from "../untrusted-egress.js"; import { askApproverIdentity, describeThrown, ownArgs } from "../tool-policy.js"; import { admitMods, expandModPattern } from "./kernel/index.js"; import { validateOverlay } from "../../prompt-assembly/overlay.js"; import { modsInvalid } from "./kernel/registration.js"; import { isPlainRecord } from "./kernel/receive.js"; import { engineMethodBottoms } from "./dollar.js"; import { modSeatsOf, modsReportToOnError, registrationRefusedNotice } from "./host.js"; export const ENGINE_CORE_ANSWER = Object.freeze({}); export function engineCore() { return ENGINE_CORE_ANSWER; } export function agentIdOf(identity) { return identity?.isDelegatedChild === true ? identity.taskId : undefined; } export function stopEventOf(identity) { return identity?.isDelegatedChild === true ? "classic.SubagentStop" : "classic.Stop"; } export function judgedInputOf(args) { const owned = ownArgs(args); return owned.ok ? owned.value : args; } export function admittedChainOf(bus, spec, deps) { const offered = new Map(); for (const r of [...(deps.mods ?? []), ...(spec.mods ?? [])]) offered.set(r.name, r); const hung = bus.mounted(); const links = []; for (const rec of bus.records) { if ("refusedBy" in rec) continue; const registration = offered.get(rec.name); if (registration !== undefined) links.push(Object.freeze({ registration, handlers: Object.freeze(hung.filter((h) => h.plugin === rec.name)) })); } return Object.freeze(links); } const ADMITTED_CHAINS = new WeakMap(); function recordAdmittedChain(bus, seats) { const chain = admittedChainOf(bus, { ...(seats.task !== undefined ? { mods: [...seats.task] } : {}) }, { ...(seats.deployment !== undefined ? { mods: [...seats.deployment] } : {}) }); ADMITTED_CHAINS.set(bus, chain); return chain; } export function chainOfBus(bus) { return ADMITTED_CHAINS.get(bus); } const sameMatcher = (a, b) => { if (a === undefined || b === undefined) return a === b; const ka = Object.keys(a).sort(); const kb = Object.keys(b).sort(); return ka.length === kb.length && ka.every((k, i) => k === kb[i] && a[k] === b[k]); }; const sameHandler = (a, b) => a.seat === b.seat && a.pattern === b.pattern && a.observe === b.observe && a.caught === b.caught && sameMatcher(a.matcher, b.matcher); function sameModLink(a, b) { return a.registration === b.registration && a.handlers.length === b.handlers.length && a.handlers.every((h, j) => sameHandler(h, b.handlers[j])); } export function sameModChain(a, b) { return a.length === b.length && a.every((l, i) => sameModLink(l, b[i])); } const judgesCall = (p) => !p.observe; export function chainJudges(bus) { return bus.hasHandlers("classic.PreToolUse", judgesCall) || bus.hasHandlers("tool.check", judgesCall); } const SCREENING_CHAINS = new WeakMap(); export function recordScreeningChain(policy, chain) { SCREENING_CHAINS.set(policy, chain); } export function screeningChainOf(policy) { return SCREENING_CHAINS.get(policy); } export function sameInstallation(pc, leg) { const bus = screeningChainOf(pc.policy); const chain = bus !== undefined ? chainOfBus(bus) : undefined; return (chain !== undefined && sameModChain(chain, leg.chain) && askApproverIdentity(pc.onAsk) === askApproverIdentity(leg.onAsk) && (pc.durableMandate === true) === leg.durableMandate && (pc.contentMandate === true) === leg.contentMandate && pc.hookEnv === leg.hookEnv && pc.autoMode?.decider === leg.decider); } export function standsInFor(pc, leg, legChainJudges) { return legChainJudges && sameInstallation(pc, { ...leg, durableMandate: false, contentMandate: false }); } const judgesToolCheck = (h) => { const selected = expandModPattern(h.pattern); return !h.observe && "names" in selected && selected.names.includes("tool.check"); }; export function screeningInstallationsOf(entries) { return entries.flatMap((pc) => { const bus = screeningChainOf(pc.policy); return bus !== undefined || pc.mods !== undefined ? [{ bus, chain: pc.mods }] : []; }); } export function unraisableGuardsOf(installations) { const guards = installations.flatMap((i) => (i.bus !== undefined ? [] : (i.chain ?? []).filter((l) => l.handlers.some(judgesToolCheck)).map((l) => `"${inlineUntrusted(l.registration.name, 80)}"`))); return guards.length === 0 ? undefined : `an inherited screening entry names the tool.check guard of ${guards.join(", ")}, but its policy is not one createPreToolUseConstraintPolicy minted, so the guard has no chain to judge this leg's calls on — hand the minted policy back as it is`; } const judges = (p) => judgesCall(p); export function inheritedToolChecks(entries, standsIn, settleOf) { const unraisable = unraisableGuardsOf(screeningInstallationsOf(entries)); if (unraisable !== undefined) throw modsInvalid(unraisable); const checks = []; for (const pc of entries) { const bus = screeningChainOf(pc.policy); if (bus === undefined || standsIn(pc) || !bus.hasHandlers("tool.check", judges)) continue; checks.push(Object.freeze({ bus, judges, settle: settleOf(pc) })); } return Object.freeze(checks); } export function classicEventBase(identity, permissionMode) { const agentId = agentIdOf(identity); return { ...(identity !== undefined ? { identity } : {}), ...(agentId !== undefined ? { agentId } : {}), ...(permissionMode !== undefined ? { permissionMode } : {}), }; } export async function observeClassic(bus, event, e, signal) { if (!bus.hasHandlers(event)) return; try { await bus.dispatch(event, e, { bottom: engineCore, ...(signal !== undefined ? { signal } : {}) }); } catch { } } export async function dispatchClassic(bus, event, e, options) { if (!bus.hasHandlers(event)) return undefined; const check = options.checkAnswer; return await bus.dispatch(event, e, { bottom: engineCore, ...(options.signal !== undefined ? { signal: options.signal } : {}), ...(check !== undefined ? { checkAnswer: (a) => (a === ENGINE_CORE_ANSWER ? undefined : check(a)) } : {}), }); } export function stringMembersAnswer(event, members) { return (answer) => { if (answer === null || answer === false) return undefined; if (typeof answer !== "object") return `answered ${typeof answer}; ${event} takes { ${members.map((m) => `${m}?`).join(", ")} }`; for (const m of members) { const v = Reflect.get(answer, m); if (v !== undefined && typeof v !== "string") return `answered a ${typeof v} "${m}"; it is a string`; } return undefined; }; } export function stringMembersOf(answer, members) { const out = {}; if (typeof answer !== "object" || answer === null) return out; for (const m of members) { const v = Reflect.get(answer, m); if (typeof v === "string") out[m] = v; } return out; } const FAILURE_CLAUSE = Object.freeze({ threw: (who, why) => `crashed (extension "${who}" ${why})`, timeout: (who, why) => `did not answer (extension "${who}" ${why})`, violation: (who, why) => `gave an answer the station cannot take (extension "${who}" ${why})`, reentry: (who, why) => `was skipped on re-entry and its .catch did not answer (extension "${who}" ${why})`, }); export function failedHandlerClause(f) { return FAILURE_CLAUSE[f.cause](extensionWords(f.registration, 80), extensionWords(f.reason, 200)); } export async function observePermissionDenied(input) { const { payload, hostObserver } = input; const e = { ...payload, ...classicEventBase(payload.identity, payload.permissionMode) }; if (input.legBus?.hasHandlers("classic.PermissionDenied") === true) await observeClassic(input.legBus, "classic.PermissionDenied", e, input.signal); if (hostObserver === undefined) return; const own = await admitModChain({ deployment: [ { name: "toolGate.permissionDenied", tier: "user", uses: { events: ["classic.PermissionDenied"], calls: [] }, register: (on) => on("classic.PermissionDenied", async () => void (await hostObserver(payload)), { observe: true }), }, ], }, input.onHostObserverFailure); await observeClassic(own, "classic.PermissionDenied", e, input.signal); } export async function screenUserPrompt(bus, e, signal) { let outcome; try { outcome = await dispatchClassic(bus, "classic.UserPromptSubmit", e, { ...(signal !== undefined ? { signal } : {}), checkAnswer: stringMembersAnswer("classic.UserPromptSubmit", ["block", "additionalContext"]), }); } catch (err) { return { kind: "failed", registration: "engine", cause: "threw", reason: `could not raise the screen: ${describeThrown(err)}`, skipped: [] }; } if (outcome === undefined) return undefined; if (outcome.kind !== "answered") return outcome; return { kind: "answered", ...stringMembersOf(outcome.answer, ["block", "additionalContext"]) }; } export const TOOL_CHECK_DECISIONS = Object.freeze(["allow", "ask", "deny"]); const isToolCheckDecision = (v) => TOOL_CHECK_DECISIONS.some((d) => d === v); function readToolCheckAnswer(answer) { if (typeof answer !== "object" || answer === null) return `answered ${answer === null ? "null" : typeof answer}; tool.check takes { decision, reason? }`; const decision = Reflect.get(answer, "decision"); const reason = Reflect.get(answer, "reason"); if (!isToolCheckDecision(decision)) return `no { decision } (one of ${TOOL_CHECK_DECISIONS.join(", ")})`; if (reason !== undefined && typeof reason !== "string") return `answered a ${typeof reason} "reason"; it is a string`; return { decision, ...(reason !== undefined ? { reason } : {}) }; } export async function raiseToolCheck(bus, e, projection, signal, judges) { if (!bus.hasHandlers("tool.check", judges)) return undefined; let outcome; try { outcome = await bus.dispatch("tool.check", e, { bottom: () => projection, ...(judges !== undefined ? { participant: judges } : {}), ...(signal !== undefined ? { signal } : {}), checkAnswer: (a) => { const read = readToolCheckAnswer(a); return typeof read === "string" ? read : undefined; }, }); } catch (err) { return { kind: "failed", registration: "engine", cause: "threw", reason: `could not raise the check: ${describeThrown(err)}`, skipped: [] }; } if (outcome.kind !== "answered") return outcome; const read = readToolCheckAnswer(outcome.answer); if (typeof read === "string") return { kind: "failed", registration: outcome.answeredBy ?? "engine", cause: "violation", reason: read, skipped: outcome.skipped }; return { kind: "answered", ...read, ...(outcome.answeredBy !== undefined ? { by: outcome.answeredBy } : {}) }; } const NO_COMPOSE = Object.freeze({ deploy: Object.freeze([]), task: Object.freeze([]), warnings: Object.freeze([]) }); function composeOverlaysOf(answer) { if (!isPlainRecord(answer)) { return `answered ${answer === null ? "null" : Array.isArray(answer) ? "an array" : typeof answer === "object" ? "an object that is not a plain record" : typeof answer}; prompt.compose takes a plain { overlays }`; } const stray = Object.keys(answer).filter((k) => k !== "overlays"); if (stray.length > 0) return `the answer carries ${stray.slice(0, 5).map((k) => JSON.stringify(extensionWords(k, 40))).join(", ")} (overlays only)`; const overlays = answer["overlays"]; return Array.isArray(overlays) && Object.getPrototypeOf(overlays) === Array.prototype ? overlays : "the answer's overlays is not an ordinary array"; } export async function raisePromptCompose(bus, e, options) { if (!bus.hasHandlers("prompt.compose")) return NO_COMPOSE; const sourceOf = (seat) => (seat === "deployment" ? "deploy" : "task"); const bottom = Object.freeze({ overlays: Object.freeze([]) }); const taken = new WeakMap([[bottom, Object.freeze([])]]); const HEAD = "the answer does not carry what next() returned unchanged at its head (a handler adds overlays; it never rewrites or removes another's)"; let outcome; try { outcome = await bus.dispatch("prompt.compose", e, { bottom: () => bottom, ...(options.signal !== undefined ? { signal: options.signal } : {}), checkAnswer: (answer, _e, by, below) => { if (typeof answer === "object" && answer !== null && taken.has(answer)) return undefined; const mine = composeOverlaysOf(answer); if (typeof mine === "string") return mine; const returned = below[below.length - 1]; const lower = typeof returned === "object" && returned !== null ? taken.get(returned) : undefined; if (lower === undefined) { return below.length === 0 ? "it answered without next(e) — prompt.compose collects: a handler awaits next(e) and adds after what it returned" : "next() is still running; prompt.compose answers after it"; } if (mine.length < lower.length) return HEAD; const out = [...lower]; for (let i = 0; i < mine.length; i++) { const under = lower[i]; const v = validateOverlay(mine[i], { source: sourceOf(under?.by.seat ?? by.seat), event: "prompt.compose", lowerAdds: options.lowerAdds }); if (under !== undefined) { if ("violation" in v || JSON.stringify(v.overlay) !== JSON.stringify(under.overlay)) return HEAD; continue; } if ("violation" in v) return v.violation; out.push(Object.freeze({ overlay: v.overlay, by: Object.freeze({ plugin: by.plugin, seat: by.seat }) })); } taken.set(answer, Object.freeze(out)); return undefined; }, }); } catch (err) { return { deploy: [], task: [], warnings: [`prompt.compose could not be raised (${describeThrown(err)}); no mods overlay applies this run`] }; } const warnings = (outcome.kind === "aborted" ? [] : outcome.skipped).map((s) => `prompt.compose: extension "${extensionWords(s.registration, 80)}" ${s.cause === "violation" ? "gave an answer the station cannot take" : s.cause}: ${extensionWords(s.reason, 300)} — that answer is skipped; what is below it stands`); if (outcome.kind !== "answered") return { deploy: [], task: [], warnings }; const answer = outcome.answer; const contributions = typeof answer === "object" && answer !== null ? taken.get(answer) : undefined; if (contributions === undefined) return { deploy: [], task: [], warnings: [...warnings, "prompt.compose: the chain's answer is not one the station took; no mods overlay applies this run"] }; const blocks = []; for (const { overlay, by } of contributions) { const item = { overlay, origin: { source: sourceOf(by.seat), by: by.plugin } }; const last = blocks[blocks.length - 1]; if (last !== undefined && last.by.plugin === by.plugin && last.by.seat === by.seat) last.items.push(item); else blocks.push({ by, items: [item] }); } const inChainOrder = blocks.reverse().flatMap((b) => b.items); return { deploy: Object.freeze(inChainOrder.filter((o) => o.origin.source === "deploy")), task: Object.freeze(inChainOrder.filter((o) => o.origin.source === "task")), warnings, }; } function lateView() { let seats; const need = (method) => { if (seats === undefined) throw new Error(`${method}: the engine facade is not available before the leg is prepared`); return seats; }; const view = { env: { exec: (command, options) => need("process.run").exec(command, options) }, complete: (request, signal) => need("model.complete").complete(request, signal), book: (reply) => need("model.complete").book(reply), session: { id: () => need("session.id").sessionId, cwd: () => need("session.cwd").cwd(), root: () => need("session.root").root(), model: () => need("session.model").model(), messages: () => need("session.messages").messages(), }, now: () => need("clock.now").now(), tools: () => need("tool.list").tools(), agents: () => need("agent.list").agents(), }; return { view, bind: (s) => { seats = s; }, }; } export async function admitLegMods(input) { const late = lateView(); const seats = modSeatsOf(input.spec, input.deps); const bus = await admitMods({ side: "engine", seats, bottoms: engineMethodBottoms(late.view), report: modsReportToOnError(input.deps.onError, input.sessionId), describeThrown, isProxy: types.isProxy, }); for (const record of bus.records) { if ("refusedBy" in record) deliverEngineNotice(input.deps.onNotice, registrationRefusedNotice(record, input.sessionId)); } return { bus, chain: recordAdmittedChain(bus, seats), bindFacade: late.bind }; } function replyText(reply) { return reply.content.flatMap((c) => (c.type === "text" ? [c.text] : [])).join(""); } export function legFacadeSeats(input) { const runtime = brainToRuntime(input.deps.brain); const answeredBy = new Map(); const byUsage = new WeakMap(); return { exec: (command, options) => input.executionEnv.exec(command, options), complete: async (request, signal) => { const model = request.model !== undefined ? resolveModel(request.model, input.deps.models) : input.model; answeredBy.set(model.id, model); const auth = await input.spec.getApiKeyAndHeaders?.(model); const reply = await runtime.completeSimple(model, { ...request.context, messages: [...request.context.messages] }, { signal, maxTokens: request.maxTokens, ...(auth?.apiKey !== undefined ? { apiKey: auth.apiKey } : {}), ...(auth?.headers !== undefined ? { headers: auth.headers } : {}) }); if (reply.stopReason === "error" || reply.stopReason === "aborted") throw new Error(`model.complete: ${reply.errorMessage ?? reply.stopReason}`); if (reply.usage !== undefined) byUsage.set(reply.usage, { model, ...(reply.diagnostics !== undefined ? { diagnostics: reply.diagnostics } : {}) }); return Object.freeze({ text: replyText(reply), model: model.id, ...(reply.usage !== undefined ? { usage: reply.usage } : {}) }); }, sessionId: input.sessionId, book: (reply) => { const seen = reply.usage !== undefined ? byUsage.get(reply.usage) : undefined; const record = seen !== undefined && seen.model.id === reply.model ? seen : undefined; const model = record?.model ?? answeredBy.get(reply.model); if (model === undefined) throw new Error(`model.complete: the reply names model "${reply.model}", which no call of this leg resolved; it is not booked`); input.book(model, { usage: reply.usage, ...(record?.diagnostics !== undefined ? { diagnostics: record.diagnostics } : {}) }); }, cwd: () => input.cwdRef?.current ?? input.taskRoot, root: () => input.taskRoot, model: () => input.model.id, messages: () => input.lastBrainContext()?.messages ?? [], now: () => (input.deps.now ?? Date.now)(), tools: () => [...input.activeTools], agents: () => (input.agentListing?.entries ?? []).map((e) => e.name), }; } export async function admitModChain(seats, onError) { const bus = await admitMods({ side: "engine", seats, bottoms: engineMethodBottoms(lateView().view), report: (err) => { try { onError?.(err); } catch { } }, describeThrown, isProxy: types.isProxy, }); recordAdmittedChain(bus, seats); return bus; }