Monitor

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.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.

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 done

The 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 row

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

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_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.

Anti-patterns

  • 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.

Next steps

On this page