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
JavaScript
;
/**
* 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