What can this answer support?
A tool returned no rows. Before an agent says "there are no incidents", the app needs to know which account was searched, what period the read covered, whether it finished, and what it could not reach. A successful empty query and a failed query support different answers.
For example, the bounded claim is: "The search returned no matching incidents for this account and time window. Email reports were not checked." That is an illustration of scope-aware wording, not a claim that no incident exists anywhere. When the evidence cannot support the requested conclusion, the app should make that uncertainty explicit or withhold the answer.
Agentfootprint supplies the execution record and configurable checks for that decision. Your app defines the comparisons and delivery policy. Tool declarations must describe the query truthfully: recording a source's assertion does not prove the source is correct.
How evidence reaches the answer
- Record what ran. FootprintJS connects execution steps and data dependencies; Agentfootprint records the context supplied, tools called and results returned.
- Preserve the limits. Tools declare absence, coverage and time boundaries. Configure those limits to travel with the answer instead of relying on the model to repeat them.
- Check before delivery. Opt-in answer validation compares the candidate with evidence under app-authored rules. Enforce mode withholds failed or unverified candidates; the host decides how to explain the refusal to the user.
- Inspect the same run. Review recorded inputs, results, decisions and checks, or replay the execution to investigate a failure.
Execution provenance describes what ran and what was supplied. It does not expose the model's internal reasoning or independently establish that an external fact is true. Withholding an answer does not undo tool actions already performed.
Checks you can enable
Installing the framework does not make every answer verified. These mechanisms have different jobs and must be configured for the claims your app needs to check.
The links above describe this branch. Features marked unreleased in the capability index are not yet available in the published package; check its availability column and the release notes for your installed version. The basic quick start below does not enable these checks.
How this compares with LangChain and Mastra
These frameworks all support agent building and runtime controls. The useful distinction is what each emphasizes, rather than whether it has guardrails.
Choose Agentfootprint when a central requirement is to connect an answer to what the agent actually read, the limits of that evidence, and the checks applied before delivery. Skills, tools and orchestration build the agent; the execution record also supports its answer policy. This comparison describes design emphasis, not exclusive capabilities or measured superiority in answer accuracy.
Quick start — runs offline, no API key
npm install agentfootprint footprintjs
import { Agent, defineTool } from 'agentfootprint';
import { mock } from 'agentfootprint/providers';
const weather = defineTool({
name: 'weather',
description: 'Get current weather for a city.',
inputSchema: {
type: 'object',
properties: { city: { type: 'string' } },
required: ['city'],
},
execute: async ({ city }: { city: string }) => `${city}: 72°F, sunny`,
});
const agent = Agent.create({
provider: mock({
replies: [
{ toolCalls: [{ id: 'weather-1', name: 'weather', args: { city: 'Paris' } }] },
{ content: 'The demo weather tool returned 72°F and sunny for Paris.' },
],
}),
model: 'mock',
})
.system('You answer weather questions using the weather tool.')
.tool(weather)
.build();
const result = await agent.run({ message: 'Weather in Paris?' });
console.log(result); // → "The demo weather tool returned 72°F and sunny for Paris."
The tool runs locally and the mock scripts its call and reply. The temperature is fixture data, not a live weather reading; this example demonstrates wiring, not answer validation. For a configured delivery check, run the mock answer-validation example.
To use a real model, replace the scripted mock with a provider from
agentfootprint/providers — anthropic(...) / openai(...) / bedrock(...) /
gemini(...) / ollama(...) — and configure its model and any required credentials.
The agent and tool wiring stays the same. Every provider, including mock, lives
on that subpath so the main barrel stays free of optional provider SDK imports.
No cloud account? ollama('llama3.2') from agentfootprint/providers runs the same agent against a local model for $0 — the free rung between the mock and the bill. Full recipes: Ollama · OpenAI-compatible endpoints.
What a tool returns — three helpers, one rule
Whatever a tool returns is what the model reads next. When that value has limits the model must not lose, return it through one of three helpers — each is the tool's response; nothing is added to the system prompt or the tool's schema:
Never wrap one in another. Add .limitsTravelWithTheAnswer() and the declared coverage is appended to the final answer — or, for a typed .outputSchema() answer, returned beside it as data by agent.answerCoverage(). Provenance and grain never are.
import { absent, coverage, describedResult } from 'agentfootprint';
execute: ({ vm, summary }) => {
const { exportedAt, source, runs: all } = backupExport; // your system of record
const runs = all.filter((r) => r.vm === vm);
const ground = { checked: [`every backup job in the export of ${exportedAt}`] };
if (runs.length === 0) return absent({ what: `backup runs for ${vm}`, ...ground }); // nothing matched
if (summary) return coverage(`${vm}: ${runs.length} backup runs`, ground); // a verdict with limits
return describedResult({ // rows from the record
facts: runs.map((r) => ({ entity: vm, day: r.day, ok: r.ok === 1 })),
provenance: { measuredAt: exportedAt, source }, // the time comes from the data
coverage: ground,
});
},
What the model reads from each, exactly: Tools → what execute returns.
Building agents — context, tools and orchestration
Skills, steering, RAG, facts, memory, guardrails — every name for context does one thing: it injects into one of three LLM slots. So we abstracted the injection itself. The abstraction is three rules:
- Three slots are fixed.
system,messages,tools— the LLM API surface. - N flavors are open. You declare what you have. Tomorrow's flavor (few-shot, reflection, persona, A2A handoff…) plugs in the same way.
- Rules decide where and when. You provide the rules. We collect your data, fire the right one, land it in the right slot at the right iteration.
That's the whole model: Injection = slot × trigger × cache.
- Slot — which of the 3 LLM API regions the content lands in (
system/messages/tools). - Trigger — when the content fires (see below).
- Cache — how stable the content is across iterations. The framework places provider cache markers for you — stable content gets 80–90% cheaper prefixes.
The 4 triggers
3 slots × 4 triggers × N flavors = the entire context-engineering surface.
Inspect and replay the evidence
The recorded run also supports debugging. These examples illustrate different kinds of evidence; a similarity score or recorded dependency alone is not proof of why a model chose an answer.
Q: Were refund_full and refund_partial described similarly?
A: margin 0.02 — ⚠ NARROW: the two tool descriptions read nearly identical
(toolChoiceRecorder — and the catalog lint flags the pair before you ever run)
Q: Why was this loan declined?
A: decision ← [control: "DTI above the 0.43 affordability ceiling"] ← dti 0.52 ← monthlyDebt / income
(decide() evidence + the causal slice — every hop is a real recorded edge)
Q: Did removing a piece of context change the outcome in these reruns?
A: ablating fact 'vip-override' flipped the outcome in 3/3 seeded reruns
(localizeContextBug — evidence from those counterfactual trials)
Q: Does this exported bundle pass its integrity check?
A: verifyAuditBundle → valid: false, brokenAt: #16 — the tampered record, named
(hash-chained audit export, offline verification)
The trace toolpack also lets a debugger model inspect the record: in one run it found a planted bug while reading 9.5% of the trace (self-explain). That is one debugging result, not an answer-accuracy benchmark. Integrity checks establish properties of the exported record; they do not establish that its source data was true.
Pick your door
Mocks first, production second
Build the entire app against in-memory mocks with zero API cost, then swap real infrastructure one boundary at a time.
The flowchart, recorders, and tests don't change between dev and prod.
Any AG-UI frontend — no glue code
AG-UI frontends (CopilotKit, the AG-UI Dojo, any @ag-ui/client app) all speak one protocol: POST a RunAgentInput, read a stream of typed events. An agentfootprint run is already a stream of typed events, so the only missing piece is the adapter between them — agUi(). Hand it to streamHandler, a plain (request) => Promise<Response> that mounts in Next.js, Hono, Bun, Deno or Workers, and the frontend shows streamed text, tool calls with their results, steps and sub-agents.
A browser request is untrusted, so the handler decides nothing on its word: it builds a fresh agent for every request, binds the thread to the caller your verifier proves (verifiedThreads — a checked bearer token or sign-in, never a header read as a name), keeps the conversation on the server (sessions) and takes only the person's newest message from the page. It reads only what the run serves, so anything your redaction policy hides never reaches the browser.
// npm install agentfootprint @ag-ui/core @ag-ui/encoder
import { Agent } from 'agentfootprint';
import { mock } from 'agentfootprint/providers';
import { memorySessions, streamHandler, verifiedThreads } from 'agentfootprint/hosting';
import { agUi } from 'agentfootprint/ag-ui';
// Who is calling — PROVEN, not claimed. A real app: jwksIdentity({ jwksUrl, issuer, audience })
// from 'agentfootprint/security'. This demo verifier knows one token.
const verify = async (token: string) => {
if (token !== 'demo-token-for-ada') throw new Error('unknown token');
return { userId: 'ada' };
};
const handler = streamHandler({
// A fresh agent per request — nothing one caller's run leaves behind reaches another.
agent: () =>
Agent.create({ provider: mock({ reply: 'Hello from agentfootprint.' }), model: 'mock' })
.system('Be brief.')
.build(),
adapter: agUi(),
threads: verifiedThreads({ verify }), // no valid token → 401; the thread is the caller's own
sessions: memorySessions(), // the conversation lives on the server (one turn per thread, per process)
}); // Next.js: export const POST = handler;
// What an AG-UI frontend sends, and what it reads back:
const response = await handler(
new Request('http://localhost/agent', {
method: 'POST',
headers: { 'content-type': 'application/json', authorization: 'Bearer demo-token-for-ada' },
body: JSON.stringify({ threadId: 't1', runId: 'r1', messages: [{ id: 'u1', role: 'user', content: 'Hi' }] }),
}),
);
console.log(await response.text()); // data: {"type":"RUN_STARTED",…} … data: {"type":"RUN_FINISHED",…}
Two turns with a tool call, end to end: examples/deploy/ag-ui.ts · frontend tools, interrupts, what a page may not decide, and what goes out: AG-UI frontends.
Building with an AI coding assistant
agentfootprint is written to be used — and maintained — by coding agents.
- Teach your assistant the API:
npx agentfootprint-setupinstalls the agentfootprint skill for Claude Code (and rule files for Cursor, Copilot, Windsurf, Cline, Kiro). It loads only when the task is about agentfootprint. - Before building something, search CAPABILITIES.md — an index of what already ships, keyed by what you would call it. It is in the npm package too.
- Every example runs:
examples/holds 160+ runnable programs, each executed end to end in CI against mock providers.
Where to next
Or jump into the examples gallery — every example is also an end-to-end CI test.
Tree-shakeable & ESM-first
- Dual build, true ESM — CommonJS and real ES modules with TypeScript types; loads natively under Node, Vite, Next, Deno and Bun.
- Per-file modules, honest
sideEffects—import { defineTool }doesn't pull in the Agent runtime, memory stores or providers. - Entry points named for what you're doing —
agentfootprintplus/providers,/memory,/rag,/observe,/context,/resilience,/security,/hosting,/events,/cacheand a few more;package.jsonexportsis the complete list. - Lazy peer-deps — vendor SDKs (Anthropic, AWS, Redis, MCP…) load only when you instantiate that adapter, and optional families like the self-explain toolpack load only when enabled.
Proven by tests, not promised: test/esm-packaging.test.ts and test/trace-toolpack/browserGraph.test.ts.
Built on
footprintjs — the flowchart pattern for backend code. agentfootprint's decision-evidence capture, narrative recording, and time-travel checkpointing are footprintjs primitives at the runtime layer.
You don't need to learn footprintjs to use agentfootprint — but if you want to build your own primitives at this depth, start there.
Citing
agentfootprint is part of a research program on making software systems explain themselves — every run records why it did what it did, as a causal trace. Researching agent transparency, observability, or explainable AI? The ecosystem map is a good starting point.
If you use agentfootprint in academic work, please cite it (or use the "Cite this repository" button on GitHub):
@software{anbalagan_agentfootprint,
author = {Anbalagan, Sanjay Krishna},
title = {agentfootprint: the explainable AI agent framework},
url = {https://github.com/footprintjs/agentfootprint},
license = {MIT},
year = {2025}
}