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