Build

Agent

Agent = ReAct. The loop primitive that thinks, acts (tool call), observes the result, and repeats until done.

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

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

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,  // reactMode: 'dynamic-grouped' wraps the LLM turn in an sf-llm-call subflow,  // so Lens renders the agent's reasoning as an LLM group with its context  // slots (system-prompt / messages / tools) nested inside — the SAME shape  // the LLMCall primitive shows — instead of a bare "Final · RUNNER" card.  reactMode: 'dynamic-grouped',})  .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

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}`),);

65 typed events fire across 18 domains during a single agent.run(). See the Observability guide for the full taxonomy.

maxIterations

Every Agent has a maxIterations cap (default 10). The loop exits when:

  1. The LLM returns text with no tool calls (normal completion)
  2. maxIterations is reached (forced exit; final iteration's text becomes the result)
  3. A tool throws (propagates as an error; subject to withRetry if wrapped)
  4. The agent pauses via askHuman / pauseHere (run() returns a RunnerPauseOutcome instead of a string — narrow it with the isPaused() guard)

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

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

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

  • 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 maxIterations to "let the agent decide" — every loop has a cost ceiling. Set it explicitly; observe agentfootprint.agent.iteration_end to track.

Next steps

On this page