agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
139 lines (138 loc) • 6.42 kB
TypeScript
/**
* 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.',
* });
*/
import type { WireRole } from '../../provider-contract/types.js';
import type { Injection, InjectionContext } from '../types.js';
import type { CachePolicy } from '../../cache/types.js';
interface DefineInstructionBase {
readonly id: string;
readonly description?: string;
/**
* Predicate to decide activation. Synchronous; side-effect free.
* If omitted, the instruction is always active (effectively a
* Steering doc, but tagged with `'instructions'` flavor for
* narrative semantics — use `defineSteering` for clearer intent).
* Predicates that throw are skipped (fail-open) and reported via
* `agentfootprint.context.evaluated`.
*/
readonly activeWhen?: (ctx: InjectionContext) => boolean;
/**
* Cache policy for this instruction. Defaults to `'never'` —
* instructions are typically rule-based (volatile per-iter
* `activeWhen` predicates, on-tool-return reminders). Override to
* `'always'` only for instructions you know are stable per-turn
* (e.g., a static safety rule wrapped as `defineInstruction` for
* narrative tagging — though `defineSteering` is the cleaner choice
* for that case).
*/
readonly cache?: CachePolicy;
}
/**
* `slot` and `role` are ONE decision, so the type makes them one choice —
* see {@link DefineFactOptions} for the full reasoning. In short:
* `slot: 'messages'` requires a `role` with no default (who appears to speak
* is the app's meaning to set), and `slot: 'system-prompt'` forbids one.
*/
/**
* The instruction's words — a fixed string, or a template that names run-time
* facts. Exactly one, because they are the same decision spelled two ways and
* a definition carrying both would leave the library picking one.
*/
export type DefineInstructionWords = {
/** Instruction text, exactly as the model will read it. */
readonly prompt: string;
readonly promptTemplate?: never;
} | {
readonly prompt?: never;
/**
* Instruction text with named run-time facts in it (9.57.0), rendered
* fresh on every action:
*
* ```ts
* promptTemplate:
* 'You are on action {{action}} of {{actionBudget}}; ' +
* '{{actionsRemaining}} remain. Finish what you have rather than start something new.',
* ```
*
* The vocabulary is CLOSED and has three words — `{{action}}`,
* `{{actionBudget}}`, `{{actionsRemaining}}` — and a name outside it is
* refused here, at define time, rather than rendering as a literal in
* front of the model. It is closed rather than a `(ctx) => string`
* because a function makes ABSENCE the author's problem: given one, an
* author writes `${ctx.maxIterations}` and ships "23 of undefined", or
* writes `?? 0` and ships a fabricated denominator nothing can tell
* from a real zero. With named slots the library owns absence and
* applies one rule — if any named fact is unavailable the whole
* instruction is skipped, by name, on the record.
*
* Two things a template may not be, both refused at define time:
* `slot: 'messages'` (the delivery ledger keys by content, so a
* re-render would deliver a NEW message on every action), and any
* `cache` other than `'never'` (a marker asserting a stable prefix
* through bytes that change every call is a guaranteed miss plus the
* cache-write premium, with the trace claiming a stable prefix).
*
* Declare templated instructions LAST: a never-cacheable injection
* truncates the cached prefix at its declaration position.
*/
readonly promptTemplate: string;
};
export type DefineInstructionOptions = DefineInstructionBase & DefineInstructionWords & ({
/**
* The default. Appended to the system prompt — the one placement
* every provider delivers. An `on-tool-return` instruction already
* lands on exactly the turn it matters; this is a system prompt that
* says it on that one turn, rather than on every turn.
*/
readonly slot?: 'system-prompt';
readonly role?: never;
} | {
/**
* Deliver the instruction into the conversation itself — it enters
* `scope.history` at the injection-engine boundary, at the END of the
* window, so the model reads it at the recency this option is
* reaching for. If its role would collide with the turn already at
* the end, delivery defers to the next boundary and says so on
* `messagesDelivery.deferred` rather than reordering anything.
*
* In a tool-using loop the window ends on the user's turn or on tool
* results, so a `'user'` role typically never gets a slot. Use
* `'assistant'`, use `'system'` on a provider that carries it, or
* return the words from the tool whose result they are about.
*/
readonly slot: 'messages';
/**
* Who appears to say it. Required — no default. Refused at run start
* when the attached provider does not carry it inside `messages`.
*/
readonly role: WireRole;
});
export declare function defineInstruction(opts: DefineInstructionOptions): Injection;
export {};