UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

164 lines (163 loc) • 8.82 kB
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"; import { renderContentBlock, renderUserAttachmentView, userAttachmentOf } from "../internal/llm.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; } const LAUNCHING_AGENT_FRAME = "(this agent was started by another agent: every entry under [launching agent] reached it through that agent, and none is vouched for as the user's own message)\n"; function renderEntry(m, cap, user) { if (m.role === "user") { const raw = typeof m.content === "string" ? m.content : m.content.map(renderContentBlock).join("\n"); const attachment = userAttachmentOf(m); if (attachment !== undefined) return `[attachment]\n${renderUserAttachmentView(m, attachment, cap)}`; 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(renderContentBlock).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, run) { const maxEntries = options?.maxEntries ?? AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES; const cap = options?.maxCharsPerEntry ?? AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS; const user = run?.delegated === true ? "[launching agent]" : "[user]"; const frame = run?.delegated === true ? LAUNCHING_AGENT_FRAME : ""; const window = maxEntries > 0 ? messages.slice(-maxEntries) : []; if (messages.length === 0) return `${frame}(transcript empty — this is the run's first gated action)\n`; const dropped = messages.length - window.length; const head = `${frame}${dropped > 0 ? `(… ${dropped} earlier transcript entries omitted)\n` : ""}`; if (window.length === 0) return head; return `${head}${head === "" ? "" : "\n"}${window.map((m) => renderEntry(m, cap, user)).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`); }