Config file (0.9.37+)
You can build an Agent from a TOML file or a parsed dict instead
of constructing it inline. The reason this exists isn’t ergonomics.
It’s that production agents tend to have three different audiences
who all want a say in the wiring:
- Ops wants to flip backends per environment. SQLite locally, Postgres in prod, without rebuilding the image.
- SRE wants budget caps and permission policies in a reviewed config file, not buried in Python.
- Compliance wants the audit-log path declared declaratively so auditors can find it without reading code.
Agent.from_config(path) reads a TOML file. Agent.from_dict(cfg)
takes the parsed dict directly. Both accept the same shape, so
you can drive the same agent from a Pydantic BaseSettings, a Helm
chart’s env vars, a YAML file plus yaml.safe_load, whatever you
already have.
from loomflow import Agent
agent = Agent.from_config("./agent.toml")
result = await agent.run("hello", user_id="alice")A complete agent.toml
This covers every block the resolver understands:
# ---------- top-level scalars ---------------------------
instructions = "You are a research assistant."
model = "claude-opus-4-7"
architecture = "react"
max_turns = 100
auto_consolidate = true
auto_extract = true
response_tone = "concise"
effort = "medium"
strict_effort = false
# ---------- backend tables ------------------------------
[memory]
backend = "sqlite"
path = "./memory.db"
[runtime]
backend = "sqlite"
path = "./journal.db"
[telemetry]
backend = "file"
path = "./spans.jsonl"
[audit_log]
name = "./audit.jsonl"
scope_full = true # opts into verbatim capture
secret = "my-org-hmac-key"
[permissions]
backend = "standard"
mode = "default"
denied_tools = ["bash"]
[budget]
max_tokens = 200_000
max_cost_usd = 5.0
max_wall_clock_minutes = 10
soft_warning_at = 0.8
# ---------- arrays of tables ----------------------------
[[skills]]
path = "./skills/research"
label = "Bundled"
[[mcp]]
name = "git"
transport = "stdio"
command = "uvx"
args = ["mcp-server-git", "--repo", "."]
[[mcp]]
name = "remote"
transport = "http"
url = "https://example.com/mcp"
headers = { Authorization = "Bearer ..." }Then:
agent = Agent.from_config("./agent.toml")Every backend table goes through the same resolver Agent(...) uses
for its kwargs. Anything you can build via Agent(memory="sqlite: ./mem.db", ...) you can declare here, and vice-versa.
The same config as a dict
from loomflow import Agent
cfg = {
"instructions": "You are a helpful assistant.",
"model": "echo",
"max_turns": 5,
"memory": {"backend": "sqlite", "path": "./m.db"},
"telemetry": "memory", # string form
"permissions": "strict",
"budget": {"max_tokens": 10_000},
}
agent = Agent.from_dict(cfg)Reach for this when your config already lives somewhere structured.
Pydantic BaseSettings, a service config endpoint, env-var loaders,
a YAML file you’ve already parsed. The dict form skips the TOML
parse and goes straight to the resolver.
Reference: every key
Top-level scalars
| Key | Type | Notes |
|---|---|---|
instructions | str | Required. The system prompt. |
model | str or dict | Required. String spec ("claude-opus-4-7", "gpt-4o", "echo") or dict form {"name": ..., "effort": ..., "strict_effort": ...}. |
architecture | str | "react", "plan_and_execute", "reflexion", etc. See Architectures. |
max_turns | int | Cap on agent-loop iterations. |
auto_consolidate | bool | Run the consolidator after each turn. |
auto_extract | bool | Auto-extract structured facts to memory. |
response_tone | str | Preset or free-form. See response_tone. |
effort | str | Reasoning-effort dial. See Reasoning effort. |
strict_effort | bool | Hard-fail when the model can’t honour effort. |
Backend tables
Each accepts the same string shorthand or dict form the matching
Agent(...) kwarg accepts:
[memory]
[memory]
backend = "sqlite" # "inmemory" / "sqlite" / "postgres" / "redis" / "chroma"
path = "./memory.db" # backend-specificOr use the string shorthand directly:
memory = "sqlite:./memory.db"[runtime]
[runtime]
backend = "sqlite" # "inproc" / "sqlite" / "postgres"
path = "./journal.db"[telemetry]
[telemetry]
backend = "file" # "none" / "memory" / "console" / "file" / "otel"
path = "./spans.jsonl" # required when backend = "file"Or shorthand:
telemetry = "console"
telemetry = "file:./spans.jsonl"[audit_log]
[audit_log]
name = "./audit.jsonl" # path; omit for in-memory
scope_full = true # opt into verbatim capture (default: false)
secret = "my-key" # optional, HMAC-signs every entryDefault is compliance-friendly: prompts truncated at 500 chars,
outputs not recorded, tool results stripped to ok/denied/error/
reason. See Audit log attribution
for the verbatim-capture story.
[permissions]
[permissions]
backend = "standard" # "allow_all" / "strict" / "accept_edits" / "bypass" / "standard"
mode = "default" # for standard backend
allowed_tools = ["search", "fetch"]
denied_tools = ["bash", "delete_file"]Shorthand:
permissions = "strict"[budget]
[budget]
max_tokens = 200_000
max_input_tokens = 150_000
max_output_tokens = 50_000
max_cost_usd = 5.0
max_wall_clock_minutes = 10
soft_warning_at = 0.8 # log a warning at 80% of any capSee Per-user budget caps.
Arrays of tables
[[skills]] — load one or more skill bundles:
[[skills]]
path = "./skills/research"
label = "Research" # optional; defaults to the directory name
[[skills]]
path = "./skills/concise"[[mcp]] — connect to MCP servers. Each entry needs a name
and transport ("stdio" or "http").
Stdio transport:
[[mcp]]
name = "git"
transport = "stdio"
command = "uvx"
args = ["mcp-server-git", "--repo", "."]
env = { GIT_AUTHOR_NAME = "Loom" } # optional
description = "Git operations" # optionalHTTP transport:
[[mcp]]
name = "remote"
transport = "http"
url = "https://example.com/mcp"
headers = { Authorization = "Bearer abc" }The MCP entries get wrapped in an MCPRegistry and passed to
tools=. If you also supply tools= as a kwarg, the kwarg wins
and mcp = [...] raises a ConfigError. Pick one.
What you can’t put in TOML
TOML / JSON / YAML can’t express Python callables, custom hook objects, or stateful instances. These still come through kwargs:
from loomflow import Agent, tool
@tool
async def search(query: str) -> str: ...
agent = Agent.from_config(
"agent.toml",
tools=[search], # real callable
hooks=my_hook_registry, # custom HookRegistry instance
retry_policy=my_retry_policy, # custom RetryPolicy
approval_handler=handler, # custom approval handler
secrets=my_secret_store, # custom Secrets implementation
)When a kwarg and a config entry both target the same backend, the kwarg wins. That’s the override path. Use a shared base TOML and layer per-environment Python overrides on top.
TOML requires Python 3.11+. Loom uses the stdlib tomllib
module, which only exists from 3.11. On older Pythons, parse the
file yourself (with tomli, tomlkit, whatever you have) and pass
the result to Agent.from_dict(cfg).
Worked example
The framework ships examples/15_config_file.py
writing a complete TOML, building an agent from it, and asserting
every backend resolved to the expected concrete class. Runs offline
against EchoModel. No API key needed.
python examples/15_config_file.py