@sema-agent/core
Version:
Stateless, task-oriented AI agent core
99 lines (98 loc) • 5.49 kB
JavaScript
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 ?? [])]));
}