Build

Grounding

Reduce hallucination by giving the LLM the source material — and recording what it produced vs what it was given. The trace IS the grounding evidence.

Your agent answers a customer's policy question. The customer screenshots the answer and emails your CEO: "I asked your bot whether refunds were available; it said no. Now I'm seeing they ARE available — what's going on?" You go to look. The agent might have hallucinated. Without grounding evidence captured at the moment of the answer, you cannot tell. agentfootprint's typed event stream + memory snapshots make grounding evidence a first-class artifact, not a thing you reconstruct.

What grounding means here

Grounding = constraining the LLM to answer from supplied source material, not from training-data memory. Two complementary mechanisms:

  1. Source injection — the source material (docs, policy text, retrieved chunks, prior decisions) lands in the system-prompt slot via RAG, Memory, or defineFact. The LLM sees it as supplied context, above the conversation.
  2. Source observation — the framework records observed injections (agentfootprint.context.injected), model-call results (agentfootprint.stream.llm_end, with text withheld when an output policy applies), and a delivered answer when there is one (agentfootprint.agent.turn_end). Keep those different observations distinct in an audit.

Together they provide evidence for checking an answer against its observed sources. They do not themselves prove every claim correct or make a withheld model response available in an event-only export.

RAG as grounding

The RAG guide is the standard pattern. defineRAG retrieves top-K matching chunks above a strict threshold; if the threshold is missed, NO chunks inject (the LLM gets no context). This is intentional — garbage low-confidence chunks make hallucination MORE likely, not less.

const docs = defineRAG({
  id: 'policy-docs',
  store, embedder,
  topK: 3, threshold: 0.7,  // strict — no chunks if nothing clears 0.7
});
agent.rag(docs);

When chunks DO match, they land in the system-prompt slot — one system message carrying every match as a citation block. Observability surfaces show exactly which chunks were retrieved per turn.

Causal memory as grounding for follow-ups

For cross-run grounding (the customer asks "why did you tell me that earlier?"), Causal memory persists snapshots of past runs. New questions cosine-match past queries; matching snapshots inject the stored run. The follow-up answer is grounded in what actually happened, not reconstruction.

This is the differentiator no other framework has. See Memory guide § Causal memory.

Recording what was injected

Subscribe to agentfootprint.context.injected to capture every injection in real time:

agent.on('agentfootprint.context.injected', (e) => {
  // e.payload — { source, slot, contentSummary, rawContent?, asRole?, sourceId?, reason, ... }
  // source: 'steering' | 'instructions' | 'skill' | 'fact' | 'memory' | 'rag' | ...
  // rawContent is the full injected source (may be redacted); contentSummary is always present.
  const content = e.payload.rawContent ?? e.payload.contentSummary;
  audit.log({ when: Date.now(), source: e.payload.source, content });
});

Every flavor of context engineering routes through the SAME event channel — the audit log is uniform across Skill, Steering, Instruction, Fact, Memory, RAG. One event taxonomy for context observability; no per-feature integration glue.

Recording what the LLM produced

agentfootprint.stream.llm_end carries the LLMEndPayload — content, toolCallCount, usage, stopReason, durationMs, iteration. Keep each call's event, rather than selecting the first one: a turn may contain multiple calls.

With output middleware, this event carries contentWithheld: true and empty content: the draft is withheld, not absent. Use agent.turn_end.finalContent for the delivered answer, and distinguish it from raw model output. A refused answer has no turn_end. Raw drafts remain in the run's audit state, which needs separate access and retention protection; do not copy them into a public event stream to replace the withheld field.

Capture the observations available for each run:

For events belonging to one run:
  injected_chunks = all context.injected events
  model_calls = every stream.llm_end event, keeping iteration and usage:
    if contentWithheld is true:
      output = { status: 'withheld' }  // never infer an empty model answer
    otherwise:
      output = { status: 'observed', text: content }
  delivered_answer = agent.turn_end.finalContent, only if that event exists
  evidence = { question, injected_chunks, model_calls, delivered_answer }

This record distinguishes available model text from withheld drafts and from the answer actually delivered. Persist it under an appropriate access and retention policy. A missing turn_end means no delivered answer was observed; it does not by itself explain whether the run paused, failed, or the event capture was incomplete. An event-only record cannot reconstruct text that the output policy withheld.

Anti-patterns

  • Don't describe an event-only record as complete when response text was withheld or capture was incomplete. Keep that limitation visible rather than treating blank llm_end.content as an empty answer.
  • Don't trust the LLM to cite sources from memory. It will hallucinate URLs / doc IDs. Cite by injecting the source AND requiring the LLM to repeat the injected source ID in its answer (verifiable via comparison).
  • Don't mix high-confidence and low-confidence sources without flagging. If RAG returns chunks at score 0.9 and 0.5, the LLM treats them equally. Either threshold strictly OR weight injection role to mark uncertainty.

Next steps

On this page