What is an Agent
An Agent is a configured loop driver. It carries:
- Instructions. The system prompt.
- An architecture. The loop strategy (ReAct by default).
- A Dependencies bundle. Every protocol the architecture might need: model, memory, runtime, tool host, budget, permissions, hooks, telemetry, audit log.
from loomflow import Agent
agent = Agent(
"You are a helpful assistant.", # instructions
model="claude-opus-4-7", # the Model protocol
memory="sqlite:./bot.db", # the Memory protocol
tools=[search, fetch], # the ToolHost protocol
budget=StandardBudget(BudgetConfig(...)),
permissions=StandardPermissions(...),
audit_log=FileAuditLog("./audit.jsonl"),
telemetry=OTelTelemetry(...),
runtime=SqliteRuntime("./journal.db"),
architecture="react", # default
)Each kwarg is a different protocol implementation. Most have no-op
defaults so a bare Agent("...", model="...") is a complete object.
What it’s not
- Not a base class. You don’t subclass
Agent. To customize the loop, pass a differentarchitecture=. To customize a backend, pass a different protocol implementation. - Not stateful at the class level. All per-run state lives in
AgentSession(constructed fresh peragent.run()). TheAgentitself is a configuration container. - Not single-tenant. One
Agentinstance serves N users viauser_id=onagent.run().
How agent.run() works
result = await agent.run(
"What's the weather in Tokyo?",
user_id="alice", # multi-tenant scope
session_id="conv_42", # conversation continuity
output_schema=MyPydanticModel, # optional structured output
)The Agent:
- Opens a runtime session (sets a contextvar).
- Opens a root telemetry span (
loom.run). - Writes a
run_startedaudit entry. - Seeds the message stack from memory (recent episodes, recent facts, working blocks).
- Hands control to the architecture’s
run()async generator. - Forwards events to the consumer (or
awaits through them foragent.run()). - Persists the new episode to memory.
- Triggers auto-extract for fact consolidation (if enabled).
- Writes a
run_completedaudit entry. - Returns a
RunResult.
For the full step-by-step including all four observability boundaries (events, telemetry, audit, runtime journal), see Architecture (internals).
What agent.stream() returns
stream() is the same loop, exposed as an async generator of Events:
async for event in agent.stream("..."):
if event.kind == "model_chunk":
print(event.payload["chunk"]["text"], end="")
elif event.kind == "tool_call":
log.info("calling tool %s", event.payload["call"]["tool"])Same loop, same RunResult (built from the same AgentSession at
the end). Pick run() when you only need the final answer; pick
stream() when you want backpressure-aware token output.
Re-using one Agent across users
You construct ONE agent per logical service. Pass user_id= per
call:
agent = Agent(
"...",
model="claude-opus-4-7",
memory="postgres://...",
audit_log=FileAuditLog("./audit.jsonl"),
)
# In a request handler:
async def handle(prompt, user_id, session_id):
return await agent.run(prompt, user_id=user_id, session_id=session_id)Memory partitions by user_id, audit attributes by user_id,
budgets cap per user_id. The Agent itself is shared.
Agent is a value object plus a run method. It carries no
mutable state across calls except for hook registrations and add_tool
/ remove_tool plugin operations. Two agent.run() calls in flight
concurrently against the same Agent are safe. Each gets its own
AgentSession.