Skip to Content
FAQ

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:

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_EDITS approves only file edits, MCP tools that two servers share are named server__tool, retry jitter is full jitter, journals are JSON and kept per user, and effort= 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_hooks and a few others now go in Agent(..., tuning=Tuning(...)). Passing them directly still works but emits a DeprecationWarning.
  • agent.resume() takes session_id as a keyword. Call await 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).

Last updated on