UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

99 lines (98 loc) • 5.49 kB
import { carriesNamespaceSeparator, protocolOf } from "./protocol-table.js"; import { mintNamespacePrefix } from "./protocol-naming.js"; export const RETIRED_TOOL_NAMES = new Map([ ["read_file", 'renamed "Read"'], ["edit_file", 'renamed "Edit"'], ["write_file", 'renamed "Write"'], ["bash", 'renamed "Bash"'], ["bash_readonly", 'folded into "Bash" (read-only posture rides handsReadOnly/shellGate, not a tool name)'], ["glob", 'renamed "Glob"'], ["grep", 'renamed "Grep"'], ["web_fetch", 'renamed "WebFetch"'], ["tool_search", 'renamed "ToolSearch"'], ["skill", 'renamed "Skill"'], ["present_plan", 'renamed "ExitPlanMode"'], ["enter_plan_mode", 'renamed "EnterPlanMode"'], ["spawn_subagent", 'renamed "Agent"'], ["CronCancel", 'renamed "CronDelete"'], ["lsp", 'renamed "LSP"'], ["submit_output", 'renamed "StructuredOutput"'], ["repo_map", 'renamed "RepoMap"'], ["read_tool_result", 'renamed "ReadToolResult"'], ["report_blocked", 'renamed "ReportBlocked"'], ["declare_done", 'renamed "DeclareDone"'], ["run_workflow", 'renamed "Workflow"'], ["RunWorkflow", 'renamed "Workflow"'], ["Task", 'renamed "Agent" (a deployment-defined tool named "Task" is fine — this fires only when the name matches nothing in the roster)'], ["BashOutput", 'renamed "TaskOutput"'], ["AgentOutputTool", 'renamed "TaskOutput"'], ["BashOutputTool", 'renamed "TaskOutput"'], ["AgentOutput", 'renamed "TaskOutput"'], ["WorkflowStatus", 'folded into "TaskOutput" (poll by runId)'], ["KillShell", 'renamed "TaskStop"'], ["KillBash", 'renamed "TaskStop"'], ["ListMcpResources", 'renamed "ListMcpResourcesTool"'], ["ReadMcpResource", 'renamed "ReadMcpResourceTool"'], ["ReadMcpResourceDir", 'renamed "ReadMcpResourceDirTool"'], ["MultiEdit", 'removed — "Edit" accepts the edits[] shape'], ["Fork", 'removed — use Agent with subagent_type: "fork"'], ["Sleep", "removed — schedule a re-wake with CronCreate({schedule:{kind:\"delay\"}}) or wait with Monitor"], ["Remember", "removed — memory is file-based; write the memory directory with the ordinary file tools"], ["remember", "removed — memory is file-based; write the memory directory with the ordinary file tools"], ["Recall", "removed — memory is file-based; read the memory directory with the ordinary file tools"], ["recall", "removed — memory is file-based; read the memory directory with the ordinary file tools"], ]); export const RULE_TOOL_NAME_VERDICTS = Object.freeze(["current", "retired", "unprefixed_namespace", "unmintable_segment"]); const CURRENT = Object.freeze({ kind: "current" }); const UNPREFIXED = Object.freeze({ kind: "unprefixed_namespace" }); const NAMESPACE_SEPARATOR = "__"; export function splitNamespacedName(name) { const ns = protocolOf(name); if (ns === undefined) return undefined; const [peer = "", ...rest] = name.slice(ns.prefix.length).split(NAMESPACE_SEPARATOR); return rest.length > 0 ? { ns, peer, tool: rest.join(NAMESPACE_SEPARATOR) } : { ns, peer }; } function unmintableSegmentOf({ ns, peer, tool }) { if (peer === "") return { kind: "unmintable_segment", guidance: `the ${ns.id} name has an empty peer segment — no tool is minted under it` }; if (tool === "") return { kind: "unmintable_segment", guidance: `the ${ns.id} name has an empty tool segment — no tool is minted under it; write ${ns.prefix}<peer> to cover every tool of a peer, or ${ns.prefix}<peer>${NAMESPACE_SEPARATOR}<tool> for one tool` }; const minted = mintNamespacePrefix(ns, peer).slice(ns.prefix.length, -NAMESPACE_SEPARATOR.length); if (minted === peer) return undefined; return { kind: "unmintable_segment", guidance: `the ${ns.id} peer segment ${JSON.stringify(peer)} is not a spelling the engine mints — its tools are named ${ns.prefix}${minted}${NAMESPACE_SEPARATOR}<tool>, and a rule's peer segment must be spelled the way its tools are`, currentName: `${ns.prefix}${minted}${tool !== undefined ? `${NAMESPACE_SEPARATOR}${tool}` : ""}`, }; } export function judgeRuleToolName(name, roster) { if (roster.has(name)) return CURRENT; const guidance = RETIRED_TOOL_NAMES.get(name); if (guidance !== undefined) { const currentName = /^renamed "([^"]+)"/.exec(guidance)?.[1]; return currentName !== undefined ? { kind: "retired", guidance, currentName } : { kind: "retired", guidance }; } const split = splitNamespacedName(name); if (split === undefined) return carriesNamespaceSeparator(name) ? UNPREFIXED : CURRENT; return unmintableSegmentOf(split) ?? CURRENT; } export const TOOL_MISS_KINDS = Object.freeze(["unknown", "expected-absent"]); const [UNKNOWN_MISS, ABSENT_MISS] = TOOL_MISS_KINDS; export function judgeToolNameMiss(name, roster) { const v = judgeRuleToolName(name, roster); if (v.kind !== "retired") return { kind: UNKNOWN_MISS }; const next = v.currentName; if (next === undefined) return { kind: ABSENT_MISS, detail: v.guidance }; if (!roster.has(next)) return { kind: ABSENT_MISS, detail: `renamed to "${next}", which this run does not mount` }; return { kind: ABSENT_MISS, detail: `renamed to "${next}"`, successor: next }; } export function mountedToolNames(tools) { return new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])])); }