Config file
You can build an Agent from a TOML file or a parsed dict instead
of constructing it inline. Use this when the wiring (backends,
budgets, permissions, MCP servers) should change per environment or
be reviewed by people who don’t read the Python. Production agents
tend to have three 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, or 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
snip_window = 20 # keep the last 20 turns
prompt_caching = true # or a [prompt_caching] table
# ---------- 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", "--repository", "."]
[[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. |
timeout | float | Whole-run ceiling in seconds. See Wall-clock timeout. |
snip_window | int | Keep only the last N turns before each pass (0, the default, disables it). See snip_window. |
prompt_caching | bool or table | true, or a table with enabled, ttl ("5m" / "1h") and cache_key. Unknown keys in the table raise ConfigError. See Prompt caching. |
run_until | str or table | Goal-loop condition. See Goal loops. |
living_plan | bool, str or table | Same forms as the living_plan= kwarg. See Living plan. |
snip_window and prompt_caching are read from config as of 0.17;
earlier releases ignored them.
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 capThese six are the only keys [budget] accepts; anything else raises
ConfigError. Per-user caps aren’t available from config. See
Per-user budget caps.
Arrays of tables
[[skills]] loads one or more skill bundles:
[[skills]]
path = "./skills/research"
label = "Research" # optional; defaults to the directory name
[[skills]]
path = "./skills/concise"[[mcp]] connects 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", "--repository", "."]
env = { GIT_AUTHOR_NAME = "Loomflow" } # 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.
Typos fail loudly
Agent.from_config and Agent.from_dict reject keys they don’t read,
at the top level, in [budget] and in a prompt_caching table. The
ConfigError names the closest known key, so a typo doesn’t silently
drop a setting:
from loomflow import Agent, ConfigError
try:
Agent.from_dict({"instructions": "...", "model": "echo", "max_turn": 5})
except ConfigError as exc:
print(exc)
# Agent.from_dict: unknown key(s) 'max_turn' (did you mean 'max_turns'?). Known keys: ...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+. Loomflow 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. It runs
offline against EchoModel, so no API key is needed.
python examples/15_config_file.py