Skip to Content
ProductionConfig file

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

KeyTypeNotes
instructionsstrRequired. The system prompt.
modelstr or dictRequired. String spec ("claude-opus-4-7", "gpt-4o", "echo") or dict form {"name": ..., "effort": ..., "strict_effort": ...}.
architecturestr"react", "plan_and_execute", "reflexion", etc. See Architectures.
max_turnsintCap on agent-loop iterations.
auto_consolidateboolRun the consolidator after each turn.
auto_extractboolAuto-extract structured facts to memory.
response_tonestrPreset or free-form. See response_tone.
effortstrReasoning-effort dial. See Reasoning effort.
strict_effortboolHard-fail when the model can’t honour effort.
timeoutfloatWhole-run ceiling in seconds. See Wall-clock timeout.
snip_windowintKeep only the last N turns before each pass (0, the default, disables it). See snip_window.
prompt_cachingbool or tabletrue, or a table with enabled, ttl ("5m" / "1h") and cache_key. Unknown keys in the table raise ConfigError. See Prompt caching.
run_untilstr or tableGoal-loop condition. See Goal loops.
living_planbool, str or tableSame 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-specific

Or 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 entry

Default 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 cap

These 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" # optional

HTTP 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
Last updated on