Context engineering recorder
Filter the firehose of context.injected events into engineered (RAG / Skills / Memory / Instructions / Steering / Facts) vs baseline (user / tool-result / assistant). The first-class handle on what your context engineering is actually doing.
Your agent emits a
context.injectedevent 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.
What it does
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 doneThe engineered / baseline split
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']Use cases
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?
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 row3. 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
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
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
}
});API
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_SOURCES | ReadonlySet<ContextSource> |
BASELINE_SOURCES | ReadonlySet<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.
Anti-patterns
- Don't subscribe via
contextEngineeringAND rawagent.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. EachonEngineered/onBaselineregisters a listener on the agent; without detach, listeners accumulate across runs and leak memory. - Don't classify raw
ContextSourcestrings yourself. UseisEngineeredSource/isBaselineSourceso future additions to the union are routed consistently across consumers.
Next steps
- Observability guide — the broader event taxonomy
- Dynamic ReAct guide — what makes per-iteration recomposition produce a stream of engineered injections
- RAG, Memory, Skills, Instructions — the engineered-source primitives
Exporters: AgentCore & OTEL
Step-by-step — ship the agent's typed event trace to AWS AgentCore Observability (CloudWatch GenAI) and to OpenTelemetry. Observability is a port; pick an exporter strategy and mount it with agent.enable.observability().
Streaming
Token-by-token output via provider.stream(). The same response is delivered as a stream for UI feedback AND as the authoritative LLMResponse for the ReAct loop — single round-trip, no double call.
