@sema-agent/core
Version:
Stateless, task-oriented AI agent core
157 lines (156 loc) • 8.23 kB
JavaScript
import { AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
import { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./auto-mode-prompt-assets.js";
import { cutEnginePrefix, cutEngineSegments, flattenableUserText } from "./untrusted-text.js";
import { readDenyPatternRun } from "../tools/fs/read-deny.js";
export { AUTO_MODE_DEFAULTS_SENTINEL } from "./auto-mode-defaults.js";
function mergeRuleSection(user, defaultsInner) {
if (!user?.length)
return defaultsInner;
const out = [];
let spliced = false;
for (const rule of user) {
if (rule === AUTO_MODE_DEFAULTS_SENTINEL) {
if (!spliced) {
if (defaultsInner.length > 0)
out.push(defaultsInner);
spliced = true;
}
continue;
}
out.push(`- ${rule}`);
}
return out.join("\n");
}
const DENY_LINE_PATTERN_MAX = 512;
const NOT_STATED = /[\s\p{Z}\p{Cf}`<>(){}[\]?\\\u0000-\u001f\u007f-\u009f]/u;
const notStated = (ch) => NOT_STATED.test(ch) || NOT_STATED.test(ch.normalize("NFKC"));
export function readDenyClassifierRules(entries, rules = []) {
const out = [];
const seen = new Set();
const push = (line) => {
const key = `${line.rule}\u0000${line.anyCase === true}`;
if (seen.has(key))
return;
seen.add(key);
out.push(line);
};
for (const { pattern, caseSensitive } of entries) {
const { homeAnchored, run } = readDenyPatternRun(pattern);
const segments = run.split("/").filter((seg) => seg.length > 0);
if (segments.length === 0)
continue;
const stated = statedPattern(segments.join("/"));
push({ rule: homeAnchored ? `Read(~/${stated})` : `Read(//**/${stated})`, ...(caseSensitive !== true && /[A-Za-z]/.test(stated) ? { anyCase: true } : {}) });
}
for (const { tool, pattern } of rules) {
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(tool) || pattern.length === 0)
continue;
push({ rule: `${tool}(${pattern.split("/").map((seg) => (seg === "**" ? seg : statedPattern(seg))).join("/")})` });
}
return out;
}
function statedPattern(pattern) {
const chars = [...pattern].map((ch) => (notStated(ch) ? "*" : ch));
const cut = chars.length > DENY_LINE_PATTERN_MAX ? [...chars.slice(0, DENY_LINE_PATTERN_MAX), "*"] : chars;
return cut.join("").replace(/\*+/g, "*");
}
export function renderClassifierDenyRules(input) {
return renderSettingsDenyRules(readDenyClassifierRules(input.readDeny ?? [], input.readDenyRules ?? []), input.settingsDenyRules ?? []);
}
function renderSettingsDenyRules(derived, settings) {
const items = [...derived.map((d) => `\`${d.rule}\`${d.anyCase === true ? " (any ASCII letter case)" : ""}`), ...settings.map((r) => `\`${r}\``)];
if (items.length === 0)
return "";
return (`- User Deny Rules: The user has configured these permission deny rules: ${items.join(", ")}. Each rule names a tool and (optionally) an argument pattern that is already hard-blocked for that tool. ` +
"Block the action if it accomplishes the same effect via a different tool — e.g. using Bash with " +
"`python -c`, `sed -i`, `cat >`, heredocs, or similar to write or edit a file that an Edit/Write/MultiEdit deny rule covers, or otherwise routing around a deny rule by switching tools. The named tool itself is enforced separately; your job here is to catch circumvention.");
}
const PAIRED_SECTIONS = [
["allow", /<user_allow_rules_to_replace>([\s\S]*?)<\/user_allow_rules_to_replace>/],
["softDeny", /<user_soft_deny_rules_to_replace>([\s\S]*?)<\/user_soft_deny_rules_to_replace>/],
["hardDeny", /<user_hard_deny_rules_to_replace>([\s\S]*?)<\/user_hard_deny_rules_to_replace>/],
["environment", /<user_environment_to_replace>([\s\S]*?)<\/user_environment_to_replace>/],
];
export function buildAutoModePrompt(options) {
const crossSessionRule = options?.crossSessionMessagesRule ?? "";
let out = AUTO_MODE_BASE_PROMPT.replace("<permissions_template>", () => AUTO_MODE_PERMISSIONS_EXTERNAL).replace("<cross_session_messages_rule>", () => crossSessionRule);
for (const [key, re] of PAIRED_SECTIONS) {
out = out.replace(re, (_m, inner) => mergeRuleSection(options?.rules?.[key], inner));
}
out = out.replace("<settings_deny_rules>", () => renderSettingsDenyRules(options?.derivedDenyRules ?? [], options?.settingsDenyRules ?? []));
const context = options?.sessionContext ?? [];
if (context.length > 0) {
out += `\n\n## Session Context\n\n${context.map((f) => `- ${f}`).join("\n")}`;
}
return out;
}
function excerpt(text, cap) {
if (text.length <= cap)
return text;
return `${text.slice(0, cap)} [… ${text.length - cap} chars truncated]`;
}
function excerptTailInclusive(text, cap) {
if (text.length <= cap)
return text;
const half = Math.floor(cap / 2);
const marker = ` [… ${text.length - 2 * half} chars not shown …] `;
const out = `${text.slice(0, half)}${marker}${text.slice(text.length - half)}`;
return out.length >= text.length ? text : out;
}
function renderEntry(m, cap) {
if (m.role === "user") {
const raw = typeof m.content === "string" ? m.content : m.content.map((c) => (c.type === "text" ? c.text : `[${c.type}]`)).join("\n");
if (m.provenance === "engine-note") {
return `[engine note]\n${excerpt(raw, cap)}`;
}
if (m.engineMinted === true) {
return `[user]\n(engine-injected <system-reminder> message omitted — not user speech)`;
}
const flat = flattenableUserText(m.content);
if (flat === undefined) {
return `[user]\n${excerpt(raw, cap)}`;
}
const bySegments = cutEngineSegments(flat, m.engineSegments);
if (bySegments.malformed) {
return `[user]\n${excerpt(flat, cap)}`;
}
const { text, cut } = bySegments.cut ? bySegments : cutEnginePrefix(flat, m.enginePrefixChars);
const note = cut
? bySegments.cut
? "(engine-injected <system-reminder> segments omitted — not user speech)"
: "(engine-injected <system-reminder> prefix omitted — not user speech)"
: undefined;
if (text.length === 0)
return `[user]\n${note ?? ""}`;
return note === undefined ? `[user]\n${excerpt(text, cap)}` : `[user]\n${note}\n${excerpt(text, cap)}`;
}
if (m.role === "toolResult") {
const text = m.content.map((c) => (c.type === "text" ? c.text : `[${c.type}]`)).join("\n");
return `[tool_result ${m.toolName}${m.isError ? " (error)" : ""}]\n${excerpt(text, cap)}`;
}
const parts = [];
for (const c of m.content) {
if (c.type === "text")
parts.push(excerpt(c.text, cap));
else if (c.type === "toolCall")
parts.push(`[tool_call] ${c.name} ${excerpt(JSON.stringify(c.arguments ?? {}), cap)}`);
}
return `[assistant]\n${parts.join("\n")}`;
}
export function renderAutoModeWindow(messages, options) {
const maxEntries = options?.maxEntries ?? AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES;
const cap = options?.maxCharsPerEntry ?? AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS;
const window = maxEntries > 0 ? messages.slice(-maxEntries) : [];
if (messages.length === 0)
return "(transcript empty — this is the run's first gated action)\n";
const dropped = messages.length - window.length;
const head = dropped > 0 ? `(… ${dropped} earlier transcript entries omitted)\n` : "";
if (window.length === 0)
return head;
return `${head}${head === "" ? "" : "\n"}${window.map((m) => renderEntry(m, cap)).join("\n\n")}\n`;
}
export function renderAutoModeAction(input) {
const ask = input.askMessage ? `\npermission gate: ${input.askMessage}` : "";
return (`\n## New action to classify (the agent's most recent action — evaluate THIS)\n\n` +
`[tool_call] ${input.req.toolName} ${excerptTailInclusive(JSON.stringify(input.req.args ?? {}), 48_000)}${ask}\n`);
}