Package Exports
- agentfootprint
- agentfootprint/explain
- agentfootprint/instructions
- agentfootprint/observe
- agentfootprint/providers
- agentfootprint/resilience
- agentfootprint/security
- agentfootprint/stream
Readme
agentfootprint
The explainable agent framework
Most agent frameworks give you execution. agentfootprint gives you connected evidence — grounded, auditable, LLM-readable. The LLM can explain its own decisions. You can verify it wasn't hallucinating.
npm install agentfootprintImport what you need — each capability is a subpath:
import { Agent, defineTool } from 'agentfootprint'; // Build agents
import { mock, anthropic } from 'agentfootprint/providers'; // Connect providers
import { defineInstruction } from 'agentfootprint/instructions'; // Smart behavior
import { agentObservability } from 'agentfootprint/observe'; // Monitor execution
import { withRetry } from 'agentfootprint/resilience'; // Reliability
import { gatedTools } from 'agentfootprint/security'; // Tool safety
import { ExplainRecorder } from 'agentfootprint/explain'; // Grounding analysis
import { SSEFormatter } from 'agentfootprint/stream'; // Real-time eventsStart Simple, Compose Up
Five concepts. Each adds one capability. No upfront graph DSL — start with a function call and grow.
import { Agent, defineTool, mock } from 'agentfootprint';
const agent = Agent.create({ provider: mock([...]) })
.system('You are a research assistant.')
.tool(searchTool)
.build();
const result = await agent.run('Find AI trends');
console.log(result.content);
console.log(agent.getNarrative()); // connected execution trace| Concept | What it adds | Use case |
|---|---|---|
| LLMCall | Single LLM invocation | Summarization, classification |
| Agent | + Tool use loop (ReAct) | Research, code generation |
| RAG | + Retrieval | Q&A over documents |
| FlowChart | + Sequential pipeline | Approval flows, ETL |
| Swarm | + LLM-driven routing | Customer support, triage |
All five share one interface: .build() → .run(), .getNarrative(), .getSnapshot().
Adapter-Swap Testing
Write tests with mock(). Deploy with anthropic(). Same code. $0 test runs.
// test — deterministic, free, instant
const provider = mock([{ content: 'Paris.' }]);
// production — swap one line
const provider = createProvider(anthropic('claude-sonnet-4-20250514'));
// Same agent. Same tools. Same flowchart.
const agent = Agent.create({ provider }).system('Geography expert.').tool(searchTool).build();Works with Anthropic, OpenAI, Bedrock, Ollama. No lock-in.
Instructions — Conditional Context Injection
One concept. Three LLM API positions. Define a rule once — it injects into system prompt, tools, AND tool-result recency window. Driven by accumulated state.
import { defineInstruction, Agent, AgentPattern } from 'agentfootprint';
const refund = defineInstruction({
id: 'refund-handling',
activeWhen: (d) => d.orderStatus === 'denied',
prompt: 'Handle denied orders with empathy. Follow refund policy.',
tools: [processRefund],
onToolResult: [{ id: 'empathy', text: 'Do NOT promise reversal.' }],
});
const agent = Agent.create({ provider })
.tool(lookupOrder)
.instruction(refund)
.decision({ orderStatus: null })
.pattern(AgentPattern.Dynamic) // re-evaluate each iteration
.build();Tool results update the decision scope via decide(). Next iteration, different instructions activate. Progressive tool authorization, context-aware prompts, state-driven behavior — all declarative.
See Instructions Guide.
LLM Narrative — Connected Evidence
Not disconnected spans. Not logs. Connected entries with key, value, stageId — collected during the single traversal pass. The LLM can read its own trace and answer follow-up questions.
const result = await agent.run('Check order ORD-1003');
// Human-readable narrative
agent.getNarrative();
// [
// "[Seed] Initialized agent state",
// "[CallLLM] claude-sonnet-4 (127in / 45out)",
// "[ExecuteToolCalls] lookup_order({orderId: 'ORD-1003'})",
// " Tool results: {status: 'denied', amount: 5000}",
// "[CallLLM] claude-sonnet-4 (312in / 89out)",
// "[Finalize] Your order was denied..."
// ]
// Structured entries for programmatic access
agent.getNarrativeEntries();
// Each entry: { type, text, key, rawValue, stageId, subflowId }Grounding Analysis
Compare what tools returned vs what the LLM said. Hallucination detection without a separate eval pipeline.
import { ExplainRecorder } from 'agentfootprint/explain';
const explain = new ExplainRecorder();
const agent = Agent.create({ provider }).tool(orderTool).recorder(explain).build();
await agent.run('Check order status');
const report = explain.explain();
report.sources; // what tools returned (ground truth)
report.claims; // what the LLM said (to verify)
report.decisions; // what tool calls the LLM madeDynamic ReAct
All three slots (system prompt, tools, messages) re-evaluate each iteration. Instructions re-evaluate against updated decision scope. Progressive tool authorization:
Turn 1: basic tools → LLM calls verify_identity → decision.verified = true
Turn 2: InstructionsToLLM re-evaluates → admin tools unlocked → refund tools available
Turn 3: LLM sees admin tools → can process refundThe LLM's capabilities change based on what happened — not what you hardcoded.
Pausable — Human-in-the-Loop
Long-running agent pauses, serializes state to JSON, resumes hours later on a different server.
import { Agent, askHuman } from 'agentfootprint';
const agent = Agent.create({ provider })
.tool(askHuman()) // special tool that pauses execution
.build();
const result = await agent.run('Process my refund');
if (result.paused) {
// Store checkpoint in Redis/Postgres/anywhere
const checkpoint = result.pauseData;
// ... hours later, different server ...
const final = await agent.resume(humanResponse);
}Streaming Lifecycle Events
9-event discriminated union. Build any UX — CLI, web, mobile. Tool lifecycle fires even without streaming mode.
await agent.run('Check order', {
onEvent: (event) => {
switch (event.type) {
case 'token': process.stdout.write(event.content); break;
case 'tool_start': console.log(`Running ${event.toolName}...`); break;
case 'tool_end': console.log(`Done (${event.latencyMs}ms)`); break;
case 'llm_end': console.log(`[${event.model}, ${event.latencyMs}ms]`); break;
}
},
});Events: turn_start · llm_start · thinking · token · llm_end · tool_start · tool_end · turn_end · error
SSE for web backends: res.write(SSEFormatter.format(event))
Recorders — Passive Observation
Observe without shaping behavior. Collect during traversal.
import { TokenRecorder, CostRecorder, QualityRecorder, GuardrailRecorder } from 'agentfootprint';
const tokens = new TokenRecorder();
const cost = new CostRecorder({ pricingTable: { 'claude-sonnet': { input: 3, output: 15 } } });
const agent = Agent.create({ provider })
.recorder(tokens)
.recorder(cost)
.build();
await agent.run('Hello');
tokens.getStats(); // { totalCalls, totalInputTokens, totalOutputTokens, ... }
cost.getTotalCost(); // USD amount| Recorder | What it tracks |
|---|---|
TokenRecorder |
Input/output tokens per LLM call |
CostRecorder |
USD cost per model |
ToolUsageRecorder |
Tool call counts, latency, errors |
QualityRecorder |
Score each response via custom judge |
GuardrailRecorder |
Flag policy violations via custom check |
PermissionRecorder |
Blocked/denied/allowed tool events |
Tool Gating — Defense-in-Depth
The LLM never sees tools it can't use. Can't hallucinate a tool it never saw.
import { gatedTools, PermissionPolicy } from 'agentfootprint';
const policy = PermissionPolicy.fromRoles({
user: ['search', 'calc'],
admin: ['search', 'calc', 'delete-user'],
}, 'user');
const agent = Agent.create({ provider })
.toolProvider(gatedTools(allTools, policy.checker()))
.build();
// Upgrade mid-conversation
policy.setRole('admin');Two layers: resolve-time filtering (hidden from LLM) + execute-time rejection (hallucinated names caught).
Safety Instructions
defineInstruction({
id: 'compliance',
safety: true, // fail-closed: fires even when predicate throws
prompt: 'GDPR compliance required.',
});Safety instructions: unsuppressable, fail-closed, sorted last (highest LLM attention position).
Orchestration
import { withRetry, withFallback, withCircuitBreaker, resilientProvider } from 'agentfootprint';
const reliable = withRetry(agent, { maxRetries: 3 });
const resilient = withFallback(primaryAgent, cheapAgent);
const guarded = withCircuitBreaker(agent, { failureThreshold: 5 });
// Cross-family provider failover: Claude → GPT-4o → local Ollama
const provider = resilientProvider([anthropicAdapter, openaiAdapter, ollamaAdapter]);26 Samples
test/samples/ — runnable with vitest:
| # | Sample | What it demonstrates |
|---|---|---|
| 01-16 | Core patterns | LLMCall, Agent, RAG, FlowChart, Swarm, recorders, tools, security, errors, multi-modal |
| 17 | Instructions | defineInstruction, decide(), conditional activation, Decision Scope |
| 18 | Streaming Events | AgentStreamEvent lifecycle, tool events, SSE |
| 19 | Security | gatedTools, PermissionPolicy, role-based tool access |
| 20 | Grounding | ExplainRecorder — sources, claims, decisions |
| 21 | SSE Server | Express SSE endpoint with SSEFormatter |
| 22 | Resilience | withRetry, withFallback, provider failover |
| 23 | Memory Stores | redisStore, postgresStore, dynamoStore adapters |
| 24 | Structured Output | outputSchema, Zod auto-convert, zodToJsonSchema |
| 25 | OTel Recorder | OpenTelemetry spans with mock tracer |
| 26 | Explain Recorder | ExplainRecorder: sources, claims, decisions during traversal |
Architecture
src/
├── concepts/ → LLMCall, Agent, RAG, FlowChart, Swarm (builders + runners)
├── lib/ → Instructions, narrative (grounding helpers), loop (buildAgentLoop), slots, call stages
├── adapters/ → LLM adapters (Anthropic, OpenAI, Bedrock, Mock) + protocol (MCP, A2A)
├── providers/ → Prompt/tool/message strategies
├── recorders/ → AgentRecorders (Token, Cost, Quality, Guardrail, Permission)
├── streaming/ → AgentStreamEvent, StreamEmitter, SSEFormatter
├── tools/ → ToolRegistry, defineTool
├── compositions/ → withRetry, withFallback, withCircuitBreaker
└── types/ → All type definitionsBuilt on footprintjs — the flowchart pattern for backend code.