FAQ
Setup
How is this different from LangChain / LangGraph?
LangChain is a building-block library; LangGraph is a graph DSL on
top. Loomflow is a harness: one configured Agent with a typed
protocol for each part (memory, tools, runtime, permissions), plus a
Workflow for graphs you define yourself. One kwarg switches the
agent loop among twelve built-in strategies.
The biggest concrete difference: user_id is a typed primitive in
Loomflow. In LangGraph it’s a string in config["configurable"]
and typoing it leaks data across tenants. See
Migrating from LangGraph.
How is this different from CrewAI / AutoGen / OpenAI Agents SDK?
Those frameworks give each team one coordination shape. Loomflow supports recursive composition: architectures wrap each other. Reflexion wrapped around a Supervisor is a team that learns from its own failed attempts, which a single fixed shape can’t express. See Recursive composition.
Why Python 3.11+?
Strict typing (Self, Generic[T] quirks resolved), better
asyncio cancellation semantics, and StrEnum. The pyproject.toml
declares requires-python = ">=3.11".
Does it support sync code?
Yes. Sync @tool functions are dispatched to a worker thread via
anyio.to_thread.run_sync so they don’t block the event loop. The
agent loop itself is async, so agent.run(...) is awaited. From
sync code, wrap it with asyncio.run(...):
import asyncio
from loomflow import Agent
agent = Agent("...", model="claude-opus-4-7")
result = asyncio.run(agent.run("..."))Will my OpenAI / Anthropic SDK code keep working?
Yes. Under the hood, OpenAIModel wraps AsyncOpenAI and
AnthropicModel wraps AsyncAnthropic. Pass your own client
instance for custom config:
from openai import AsyncOpenAI
from loomflow.model.openai import OpenAIModel
from loomflow import Agent
client = AsyncOpenAI(timeout=30.0, max_retries=0)
agent = Agent("...", model=OpenAIModel("gpt-4o", client=client))See Migrating from the OpenAI SDK.
Cost
How do I limit cost per user?
StandardBudget(BudgetConfig(per_user_max_*)). See
Per-user budget caps.
What does auto_extract cost?
A small extraction LLM call after every agent.run(). With
gpt-4o-mini it’s ~$0.0001 per run. Disable for cost-sensitive
bulk processing: Agent(..., auto_extract=False). See
Auto-extract observability.
Does the framework retry expensive calls?
Yes. Transient model errors (429, 408, 409, 5xx, network blips)
retry automatically with exponential backoff, honouring the
provider’s Retry-After. An OpenAI insufficient_quota 429 (out of
credit) isn’t retried, since waiting won’t help. The RetryPolicy is
tunable; disable retries entirely with RetryPolicy.disabled(). See
RetryPolicy.
Multi-tenancy
How do I make the agent multi-tenant?
Pass user_id= on every agent.run(). The framework partitions
memory, tracks per-user budgets, attributes audit log entries, and
exposes user_id to tools via get_run_context() automatically.
See Multi-tenancy.
Can different tenants use different models?
Yes. Either construct a different Agent per tenant, or use a
Router that dispatches to specialists
with different models. To give tenants different tool access, use
PerUserPermissions.
How do I delete a user’s data (GDPR)?
await agent.memory.forget(user_id="alice")See GDPR ops. The audit log is intentionally immutable; for compliance, redact at query time.
Memory
What’s the difference between Memory and a Vector store?
Memory holds conversational state: episodes, facts and working
blocks built up across runs. VectorStore holds arbitrary
corpora: documents you retrieve from but don’t grow. See
Episode vs Fact and RAG.
Why bi-temporal facts?
Production systems need to answer “what did the agent believe at time T?” for audit, compliance and debugging. A plain “last value wins” store can’t. See Bi-temporal facts.
Can I write facts manually?
Yes. await agent.memory.facts.append(Fact(subject=..., predicate=..., object=...))
goes through the same supersession logic as auto-extracted facts: an
older fact with the same subject and predicate stops being current.
Tools
How do I make the agent ask before destructive tool calls?
Mark the tool destructive and use Mode.DEFAULT:
from loomflow import Agent, Mode, StandardPermissions, tool
@tool(destructive=True)
async def delete_record(record_id: str) -> str:
"""Delete a record permanently."""
...
async def my_approver(call, user_id) -> bool:
# Ask a human however you like: Slack, a ticket, a review UI.
return await ask_reviewer(call.tool, call.args, user_id)
agent = Agent(
"...",
model="claude-opus-4-7",
permissions=StandardPermissions(mode=Mode.DEFAULT),
approval_handler=my_approver,
tools=[delete_record],
)See Approval handlers.
Can tools see who’s calling them?
Yes. Inside a tool, get_run_context() returns the live
RunContext with user_id, session_id, run_id and metadata, so
you don’t need to pass scope through tool signatures. See Run vs Session vs RunContext.
How do I parallelize tool calls?
You don’t have to. The agent loop runs every tool call from the
same model turn concurrently in one anyio.create_task_group. The
results are added to the conversation in the order the model made
the calls. See ReAct.
Architectures
Which architecture should I default to?
ReAct. Swap it when:
- Cost matters and the steps are predictable → Plan-and-Execute or ReWOO.
- Quality matters more than cost → ActorCritic or Reflexion.
- Combinatorial reasoning → Tree of Thoughts.
- Multiple specialists → Router / Supervisor.
See the cheat sheet on Architectures.
Can I write my own architecture?
Yes. Implement the Architecture protocol: a name attribute, an
async-generator run(session, deps, prompt) method, and
declared_workers(). See Custom architectures.
Do architectures compose?
Yes. Reflexion(base=Supervisor(workers=...)) is a team that
learns. See Recursive composition.
Production
How do I survive a process crash mid-run?
Use a durable runtime and turn on checkpoints:
from loomflow import Agent, Tuning
from loomflow.runtime import SqliteRuntime
agent = Agent(
"...",
model="claude-opus-4-7",
runtime=SqliteRuntime("./journal.db"),
tuning=Tuning(checkpoint=True),
)After a restart, call await agent.resume(session_id="...") with the
session id of the interrupted run. See Checkpoints
and Replay and resume.
How do I get traces / metrics into Datadog / Honeycomb?
Wire OTelTelemetry(tracer_provider=...), and every span and
metric flows through OTel. For local dev, the built-in ConsoleTelemetry
/ FileTelemetry / InMemoryTelemetry sinks need no collector.
See Telemetry.
How do I audit everything for compliance?
Pass audit_log=FileAuditLog("./audit.jsonl", secret="prod-secret").
Every tool call and run-lifecycle entry is written with an HMAC
signature that covers user_id and chains to the entry before it;
await audit.verify() checks the whole log. See
Audit log attribution.
When should I run the bench/multi_tenant.py benchmark?
If you contribute to loomflow, run it before any release that touches the agent loop, memory or budget. It catches isolation regressions that unit tests miss. See Load testing isolation.
Migrations
I’m upgrading from an older release. What should I check?
The changes most likely to affect existing code:
- 0.17 changed several defaults:
ACCEPT_EDITSapproves only file edits, MCP tools that two servers share are namedserver__tool, retry jitter is full jitter, journals are JSON and kept per user, andeffort=now reaches every current Claude model. See 0.16 → 0.17. - Rarely-used knobs moved to
Tuning.retry_policy,secrets,response_tone,auto_consolidate,stop_hooksand a few others now go inAgent(..., tuning=Tuning(...)). Passing them directly still works but emits aDeprecationWarning. agent.resume()takessession_idas a keyword. Callawait agent.resume(session_id="..."); the prompt is optional.model=is required. There is no default model.
See Migrations for the version-by-version guides, including 0.9 → 0.10.
Stuck
Where do I file a bug?
github.com/Anurich/LoomFlow/issues .
How do I read the source efficiently?
The architecture tour suggests a 1500-line reading path that covers the framework end to end. See Architecture (internals).