UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

106 lines • 5.68 kB
"use strict"; /** * defineInstruction — sugar for rule-based system-prompt Injections. * * The most flexible Instruction-style flavor: a predicate decides * activation each iteration. Use for "if condition X is true, give * the LLM this guidance". Compared to: * - Steering (always-on, no predicate) * - Skill (LLM-activated via `read_skill`) * - on-tool-return (specific tool just ran — Dynamic ReAct) * * Produces an `Injection` with: * - flavor: `'instructions'` * - trigger: `{ kind: 'rule', activeWhen }` (or `'always'` if omitted) * - inject: `{ systemPrompt: prompt }`, or `{ messages: [{ role, content }] }` * when `slot: 'messages'` names the role it speaks as * * @example * const calmTone = defineInstruction({ * id: 'calm-tone', * description: 'Use a calm, empathetic tone with frustrated users.', * activeWhen: (ctx) => /upset|angry|frustrated/.test(ctx.userMessage), * prompt: 'Acknowledge feelings before facts. Avoid corporate jargon.', * }); * * const piiAfterRedact = defineInstruction({ * id: 'pii-after-redact', * activeWhen: (ctx) => ctx.lastToolResult?.toolName === 'redact_pii', * prompt: 'PII has been redacted. Do not include emails or phone numbers.', * }); */ Object.defineProperty(exports, "__esModule", { value: true }); exports.defineInstruction = void 0; const messagesSlotRefusal_js_1 = require("../messagesSlotRefusal.js"); const promptTemplate_js_1 = require("../promptTemplate.js"); const applyCachePolicy_js_1 = require("../../cache/applyCachePolicy.js"); function defineInstruction(opts) { if (!opts.id || opts.id.trim().length === 0) { throw new Error('defineInstruction: `id` is required and must be non-empty.'); } const words = opts; if (words.prompt !== undefined && words.promptTemplate !== undefined) { throw new Error(`defineInstruction('${opts.id}'): pass \`prompt\` OR \`promptTemplate\`, not both. ` + `They are the same decision spelled two ways, and the library will not pick one for you.`); } const template = words.promptTemplate; const text = template ?? words.prompt; if (!text || text.length === 0) { throw new Error(`defineInstruction(${opts.id}): \`prompt\` is required — or \`promptTemplate\`, when ` + `the words name a run-time fact (${promptTemplate_js_1.TEMPLATE_FACTS.map((f) => `{{${f}}}`).join(', ')}).`); } // A template is refused HERE for everything it can be refused for, because // everything below this line happens on somebody's paid run. const templated = template !== undefined && (0, promptTemplate_js_1.isTemplate)(template); if (template !== undefined) (0, promptTemplate_js_1.assertKnownFacts)(template, `defineInstruction('${opts.id}')`); // Refused at run time as well as in the type, read through a widened view — // see defineFact for why the widening is deliberate. const raw = opts; const toMessages = raw.slot === 'messages'; if (toMessages && !raw.role) { throw new Error(`defineInstruction('${opts.id}'): \`slot: 'messages'\` requires a \`role\` — ` + `'user', 'assistant', or 'system' on a provider that carries it. There is no ` + `default: who appears to speak is a meaning your app owns, not one the library picks.`); } if (toMessages && raw.role === 'tool') { throw new Error((0, messagesSlotRefusal_js_1.messagesToolRoleRefusal)(`defineInstruction('${opts.id}')`)); } if (templated && toMessages) { throw new Error(`defineInstruction('${opts.id}'): \`promptTemplate\` cannot target ` + `\`slot: 'messages'\`. The messages slot delivers each piece ONCE per run, and its ` + `ledger keys by the content itself — so a template that renders differently every ` + `action would deliver a new message every action, for the whole run. Use the ` + `system-prompt slot (the default), which is re-composed per action by design.`); } if (templated && opts.cache !== undefined && opts.cache !== 'never') { throw new Error(`defineInstruction('${opts.id}'): \`promptTemplate\` cannot be cached ` + `(\`cache: '${String(opts.cache)}'\`). A cache marker asserts a STABLE PREFIX, and ` + `these bytes change on every action — you would pay the cache-write premium for a ` + `guaranteed miss, and the trace would claim a stable prefix that never existed. ` + `Leave \`cache\` unset, or say \`cache: 'never'\` out loud.`); } const trigger = opts.activeWhen ? { kind: 'rule', activeWhen: opts.activeWhen } : { kind: 'always' }; const inject = toMessages ? { messages: [{ role: raw.role, content: text }] } : { systemPrompt: text }; const cacheMetadata = (0, applyCachePolicy_js_1.cachePolicyMetadata)('instruction', opts.cache); return Object.freeze({ id: opts.id, ...(opts.description && { description: opts.description }), flavor: 'instructions', trigger, inject, // TOP-LEVEL, not `metadata` (9.57.0): at least one live path rebuilds // `metadata` wholesale, and a lost marker means the literal // `{{actionsRemaining}}` reaches the model — the exact class of failure // this feature exists to prevent. Top-level fields survive every // `{...injection}` spread in the repo. ...(templated && { templated: true }), metadata: Object.freeze(cacheMetadata), }); } exports.defineInstruction = defineInstruction; //# sourceMappingURL=defineInstruction.js.map