Skip to content

Context engineering recorder

Your agent emits a context.injected event every time content lands in any slot — including the user’s message, every tool result, the static system prompt anchor. For Lens UIs, evals, cost attribution, debug logging, the baseline flow is noise. contextEngineering(agent) gives you two filtered subscriptions: just-engineered + just-baseline. Same events, cleaner streams.

import { Agent, contextEngineering } from 'agentfootprint';
const agent = Agent.create({...})
.system('You answer questions.')
.instruction(beFriendly)
.skills(supportRegistry)
.memory(recentMemory)
.build();
const ce = contextEngineering(agent);
// Just the engineered injections
ce.onEngineered((e) => {
console.log(`[${e.payload.source}] ${e.payload.contentSummary}`);
// → [skill] billing skill body
// → [memory] last 3 user turns
// → [instructions] be-friendly rule
});
// Just the baseline message-history flow
ce.onBaseline((e) => {
console.log(`[baseline:${e.payload.source}]`);
// → [baseline:user]
// → [baseline:tool-result]
});
await agent.run({ message: 'help me with a refund' });
ce.detach(); // remove all subscriptions when done

agentfootprint.context.injected events carry a source: ContextSource field. The library classifies sources into two disjoint sets:

Engineered (the context-engineering work)Baseline (the message flow)
'rag' — retrieval-augmented chunks'user' — user input
'skill' — activated skill bodies'tool-result' — tool returns
'memory' — memory subsystem injections'assistant' — prior LLM turns
'instructions' — rule-based guidance'base' — static system prompt anchor
'steering' — always-on policy'registry' — static tool catalog
'fact' — developer-supplied facts
'custom' — consumer-defined

Two filter helpers expose the classifier as pure functions for ad-hoc use without the wrapper:

import { isEngineeredSource, isBaselineSource, ENGINEERED_SOURCES } from 'agentfootprint';
isEngineeredSource('rag'); // → true
isEngineeredSource('user'); // → false
isBaselineSource('tool-result'); // → true
[...ENGINEERED_SOURCES]; // → ['rag','skill','memory','instructions','steering','fact','custom']

1. Lens UI — render engineered injections in the “context bin”

Section titled “1. Lens UI — render engineered injections in the “context bin””

Lens shows the engineered set as cards on the agent’s iteration timeline; the baseline flow appears as edges between iterations. Without contextEngineering, the UI would conflate “the user said hello” with “RAG retrieved 4 product-doc chunks” — visually noisy, semantically different.

2. Eval pipelines — what entered the prompt for this query?

Section titled “2. Eval pipelines — what entered the prompt for this query?”
const counts: Record<string, number> = {};
const ce = contextEngineering(agent);
ce.onEngineered((e) => {
counts[e.payload.source] = (counts[e.payload.source] ?? 0) + 1;
});
await agent.run({ message: evalQuery });
ce.detach();
// counts: { rag: 3, skill: 1, instructions: 2 } — eval row

3. Cost attribution — token spend per engineered source

Section titled “3. Cost attribution — token spend per engineered source”
const tokensBySource: Record<string, number> = {};
ce.onEngineered((e) => {
const t = e.payload.budgetSpent?.tokens ?? 0;
tokensBySource[e.payload.source] = (tokensBySource[e.payload.source] ?? 0) + t;
});

Pair with costRecorder to know what fraction of spend was engineered vs baseline.

4. Debug logging — tail engineered signals during dev

Section titled “4. Debug logging — tail engineered signals during dev”
ce.onEngineered((e) => {
console.log(`[${e.payload.slot}/${e.payload.source}/${e.payload.sourceId}] ${e.payload.reason}`);
});

Spot surprising activations without drowning in user-message noise.

Forward-compat behavior — unknown sources

Section titled “Forward-compat behavior — unknown sources”

The ContextSource union is open-extensible (new flavors can be added as a non-breaking change). When an event arrives with a source that’s in NEITHER ENGINEERED_SOURCES nor BASELINE_SOURCES, the wrapper routes it to NEITHER stream — preferring under-fire over miscategorization.

If your agent uses a forward-future source the library doesn’t yet know about, subscribe to the raw event for completeness:

agent.on('agentfootprint.context.injected', (e) => {
if (!isEngineeredSource(e.payload.source) && !isBaselineSource(e.payload.source)) {
// forward-future source — handle as you wish
}
});
contextEngineering(agent)Returns a ContextEngineeringHandle
handle.onEngineered(cb)Subscribe to engineered injections; returns unsub
handle.onBaseline(cb)Subscribe to baseline injections; returns unsub
handle.detach()Unsubscribes ALL listeners registered through the handle (idempotent)
isEngineeredSource(source)Pure classifier
isBaselineSource(source)Pure classifier
ENGINEERED_SOURCESReadonlySet<ContextSource>
BASELINE_SOURCESReadonlySet<ContextSource>

The wrapper accepts any runner that implements agent.on('agentfootprint.context.injected', cb)Agent, LLMCall, Sequence, Parallel, Conditional, Loop. Each runner emits its own context events; pass the right one.

  • Don’t subscribe via contextEngineering AND raw agent.on('agentfootprint.context.injected', ...) for the same purpose. The raw subscription fires for ALL sources; layering breaks the filter intent. Pick one.
  • Don’t forget to detach() long-lived handles. Each onEngineered / onBaseline registers a listener on the agent; without detach, listeners accumulate across runs and leak memory.
  • Don’t classify raw ContextSource strings yourself. Use isEngineeredSource / isBaselineSource so future additions to the union are routed consistently across consumers.