UNPKG

agentfootprint

Version:

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

132 lines • 6.41 kB
/** * Injection Engine — types. * * THE primitive that unifies every form of context engineering in the * library. Skills, Steering docs, Instructions, RAG, Memory, custom * Context — all reduce to one shape: an `Injection` with a `trigger` * (when), `inject` (what — one or more slot targets), and a `flavor` * (observability tag). * * Pattern: Strategy (GoF) — each Injection's trigger is a strategy for * "should I activate this iteration?". Each Injection's * `inject` is the Memento (GoF) carrying content to slots. * Role: Layer-3 context engineering primitive in the stack. * Sits below the slot subflows. * Emits: Engine emits `agentfootprint.context.evaluated` once per * iteration. Slot subflows emit `agentfootprint.context.injected` * for each InjectionRecord they place. */ import { isSaidByPerson } from '../lib/saidByPerson.js'; import { renderTemplate } from './promptTemplate.js'; // FOLD · the one rule for "may this skill activate for this caller" — role visibility // consumers read this and never re-derive it: the evaluator (activation), the skill-graph cursor // resolver (position) and the turn-start cascade's tier 1 (`skillIntent.ts` · firstRuleMatch). /** * Is `id` a skill this caller's role may not see, on this pass? * * Hidden means hidden on EVERY activation path: model and user text are * untrusted, so a keyword in the message, a declared route, a tool proposal, * a lease or a `read_skill` pick must never surface a skill the caller's role * may not read. Every place that decides whether a skill becomes active asks * this function, with the set resolved once per pass on the context. */ export function isHiddenSkill(ctx, id) { return ctx.hiddenSkillIds !== undefined && ctx.hiddenSkillIds.includes(id); } /** * The iteration's tool-result batch, in call order — with the singular * fallback applied (9.16.0). THE one reader for batch-aware evaluation: * `ctx.toolResults` when the runtime supplied it, else `[ctx.lastToolResult]` * for contexts built by callers that only know the singular, else `[]`. * Both the evaluator's `on-tool-return` arm and the skill-graph cursor * resolver go through here, so the two can never disagree about what the * batch was. */ export function toolResultsOf(ctx) { return ctx.toolResults ?? (ctx.lastToolResult ? [ctx.lastToolResult] : []); } /** * The messages in `ctx.history` a PERSON actually wrote, in order (9.84.0). * THE one reader for a predicate that judges what was said. * * Seven kinds of `role: 'user'` message in that list came from this library, * not from anybody: a compaction frame, a drop notice, the two in-loop * corrections, the out-of-budget wrap-up instruction, the stepped-skill nudge, * and a message an Injection delivered. They are ours, they are in the * person's voice, two of them name tools and two quote text the model or a * validator produced — so a rule written as * * ```ts * activeWhen: (ctx) => ctx.history.some((m) => m.content.includes('refund')) * ``` * * fires on a summary of a refund conversation and on a notice saying the * refund tool's result was dropped. Written as * `saidByPerson(ctx).some(...)`, it fires only when somebody said it. * * The test itself is `isSaidByPerson` in `lib/saidByPerson.ts` — the same * function the window's refusal engine uses to decide which message it may * never drop, imported rather than restated so the two answers cannot differ. * * @param ctx the iteration context a trigger or entry rule was handed */ export function saidByPerson(ctx) { return ctx.history.filter(isSaidByPerson); } /** * Project a full Injection (with functions) into a scope-safe POJO. * * `ctx` (9.57.0) is THE rendering seam: it is the one moment the context and * the content are both in scope, and every downstream reader — the slots, the * cache decision, Deliver, the ledger, the recorders, a foreign host — * continues to see the same static `ActiveInjection` it always saw. Optional, * so a caller from before 9.57.0 still compiles. * * A TEMPLATED injection projected with no usable ctx comes back with NO * `systemPrompt` at all. Absence is honest; a literal `{{actionsRemaining}}` * in front of the model is not. Through the engine that path is unreachable — * `evaluateInjections` has already skipped such an injection by name. */ export function projectActiveInjection(inj, ctx) { // Project per-skill metadata that slot subflows need to dispatch on. // `surfaceMode` drives the system-prompt-suppression decision. // `autoActivate` drives runtime tool gating: it tells the tools slot that // this skill's tools are not in the static registry and must be readmitted // while it is active. const meta = inj.metadata; // Rendered HERE, once, so the projection every reader shares is already // plain text. `undefined` = a template whose facts this ctx cannot supply, // and then the piece is absent rather than gappy. const systemPrompt = inj.templated === true && inj.inject.systemPrompt !== undefined ? ctx === undefined ? undefined : renderTemplate(inj.inject.systemPrompt, ctx) : inj.inject.systemPrompt; const out = { id: inj.id, flavor: inj.flavor, ...(inj.description && { description: inj.description }), ...(meta?.surfaceMode && { surfaceMode: meta.surfaceMode }), ...(meta?.autoActivate && { autoActivate: meta.autoActivate, }), // The cache directive rides across too — see the field's note. Without it // the cache decision reads `undefined` for every injection and defaults to // "never cacheable", which silently discards what the consumer declared. ...(meta?.cache !== undefined && { cache: meta.cache, }), inject: { ...(systemPrompt !== undefined && systemPrompt.length > 0 && { systemPrompt }), ...(inj.inject.messages && { messages: inj.inject.messages.map((m) => ({ ...m })) }), ...(inj.inject.tools && { tools: inj.inject.tools.map((t) => ({ schema: { ...t.schema }, injectionId: inj.id, })), }), }, }; return out; } //# sourceMappingURL=types.js.map