Agent
A user asks “what’s the weather in Paris?”. Your code can’t just hit a weather API — the LLM has to decide which tool to call, with what args, then read the result, then decide whether to call another tool or respond. That decide → act → observe → repeat is what makes an Agent an Agent, not a function call.
Agent = ReAct
Section titled “Agent = ReAct”The Agent primitive is the ReAct loop (Yao 2022). One iteration:
LLM call → route → [tool calls → loop] OR [final answer]Each iteration: the LLM produces text + optional tool calls. If tools were called, the framework executes them, appends results to the message history, and starts another iteration. If no tools were called (or maxIterations is hit), the loop exits with the final text.
If it doesn’t loop-with-tools, it isn’t an Agent — it’s an LLMCall.
Build an agent
Section titled “Build an agent”Agent.create({ provider, model }) → builder. .system(...) sets the system prompt. .tool(...) registers tools (each with a JSON schema). .build() finalizes:
const agent = Agent.create({ provider: provider ?? exampleProvider('feature', { respond: weatherRespond }), model: 'mock', maxIterations: 5,}) .system('You answer weather questions using the `weather` tool.') .tool({ schema: { name: 'weather', description: 'Get current weather for a city.', inputSchema: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'], }, }, execute: async (args) => `${(args as { city: string }).city}: sunny, 72°F`, }) .build();The framework owns the iteration loop. You declare what tools the agent has; the LLM decides when (and with what args) to call them; the framework dispatches and feeds results back.
Observe the loop
Section titled “Observe the loop”Because the framework owns the loop, observability is just attaching listeners — no SDK, no agent-instrumentation wrapper:
agent.on('agentfootprint.stream.tool_start', (e) => console.log(`→ tool ${e.payload.toolName}(${JSON.stringify(e.payload.args)})`),);agent.on('agentfootprint.stream.tool_end', (e) => console.log(`← tool result: ${e.payload.result}`),);47 typed events fire across 13 domains during a single agent.run(). See the Observability guide for the full taxonomy.
maxIterations
Section titled “maxIterations”Every Agent has a maxIterations cap (default 10). The loop exits when:
- The LLM returns text with no tool calls (normal completion)
maxIterationsis reached (forced exit; final iteration’s text becomes the result)- A tool throws (propagates as an error; subject to
withRetryif wrapped) - The agent pauses via
askHuman/pauseHere(returns aPauseResultinstead of a string)
Tune maxIterations to your tool budget. Tool-heavy agents (research, code-gen) commonly run 15–30; chat agents 3–5.
Identity for multi-tenant memory
Section titled “Identity for multi-tenant memory”If your agent uses memory (.memory(...)), every .run() call must include an identity so memory is scoped per tenant / principal / conversation:
await agent.run({ message: 'How long do refunds take?', identity: { tenant: 'acme', principal: 'alice', conversationId: 'thread-42' },});Without an identity, memory falls back to a global namespace — fine for single-user prototypes, dangerous in production multi-tenant apps. See Memory guide.
Composition with other Agents
Section titled “Composition with other Agents”An Agent is a runner, just like LLMCall, Sequence, Parallel, Conditional. Use them as steps in larger graphs:
import { Sequence } from 'agentfootprint';
const research = Sequence.create() .step('plan', plannerLLM) .step('execute', researchAgent) .step('summarize', summarizerLLM) .build();There is no separate “multi-agent” class — agents are the building blocks. See Patterns for Reflexion / ToT / Debate / Map-Reduce / Swarm recipes.
Anti-patterns
Section titled “Anti-patterns”- Don’t subclass Agent for a “smarter” agent — use
.skill()/.instruction()to inject behavior. Subclassing breaks composition. - Don’t put async I/O in tool
execute’s synchronous setup — it runs every iteration. Cache outside; pure dispatch inside. - Don’t bypass
maxIterationsto “let the agent decide” — every loop has a cost ceiling. Set it explicitly; observeagentfootprint.agent.iteration_endto track.
Next steps
Section titled “Next steps”- Tools guide —
defineTool, MCP integration, permission gating - Memory guide —
.memory(defineMemory({...}))for cross-run state - Skills, explained — LLM-activated body + tools