Common Configuration Patterns

Real-world Soothe recipes — and the reasoning behind each one. Use these as starting points, not gospel; the goal is to understand why a config looks the way it does so you can adapt it.

How to Think About Config

Soothe config has three axes you tune independently:

  • Capability — which providers, tools, and subagents are available.
  • Behavior — how the agent loops, when it stops, how it parallelizes.
  • Environment — where state lives (SQLite vs Postgres), where logs go, how secrets enter.

Most patterns below change only one axis. The minimal config changes capability (add a provider) and relies on defaults for the other two. Production configs change all three deliberately.

Zero-Config (No YAML)

export OPENAI_API_KEY=sk-...
soothed start && soothe

Soothe bootstraps an OpenAI provider from the environment when providers is empty. Default routing uses the built-in profile (openai:gpt-4o-mini). Vector stores default to embedded sqlite_vec.

Change the default model without YAML:

export SOOTHE_ROUTER_PROFILES='[{"name":"default","router":{"default":"openai:gpt-4o"}}]'
export SOOTHE_EMBEDDING_PROFILE='[{"model_role":"openai:text-embedding-3-small","embedding_dims":1536}]'

(SOOTHE_ROUTER_DEFAULT does not work — router is derived from profiles.) Copy config/config.template.yml when you need multi-provider routing, Postgres, Langfuse, or other overrides.

Minimal to Development

The jump from “it runs” to “it’s pleasant to develop with” is small: add a second model for reasoning, bump verbosity, and bound the loop so a runaway agent doesn’t burn tokens.

router_profiles:
  - name: default
    router:
      default: openai:gpt-4o-mini
      think: openai:gpt-4o      # reasoning tasks use the bigger model

active_router_profile: default

embedding_profile:
  - model_role: openai:text-embedding-3-small
    embedding_dims: 1536

agent:
  loop:
    max_iterations: 10        # hard stop during dev

observability:
  verbosity: debug
  console:
    enabled: true

debug: true at the top level enables LangGraph debug tracing — useful when a loop behaves unexpectedly, noisy otherwise.

Production Mindset

Production configs differ from dev configs in three ways: state becomes durable (Postgres), cost is bounded (rate limits, cheaper default model), and observability is structured (Langfuse, not console). The essential moves: set persistence.default_backend: postgresql with a postgres_base_dsn; add agent.loop.llm_rate_limit.rpm_limit / concurrent_limit; and switch observability from console to langfuse with ${LANGFUSE_*} keys.

Decision points: Use Postgres when you need thread durability across restarts or multiple workers — SQLite locks under concurrent writers (WAL mode helps but doesn’t eliminate it). Set rpm_limit below your provider’s tier ceiling; Soothe retries on 429 with exponential backoff, but staying under the limit avoids the latency hit.

Multi-Provider & Hybrid Routing

The router’s role-based design exists precisely so you can mix providers without rewriting agent logic. Define an ollama provider (local) and an openai provider (cloud), then route default to the local model and think/image to cloud models.

Routing logic: unset roles fall back to default. So a hybrid config can set only default (local) and think (cloud) — everything else reuses the local model. This is cheaper than it looks: classification, routing, and memory extraction use the fast role, which inherits default, so they all run locally.

Gotcha: provider_type: openai with a custom api_base_url works for any OpenAI-compatible API (Qwen/DashScope, OpenRouter, vLLM, local oMLX). Non-OpenAI endpoints automatically receive compatibility wrappers for reasoning_content and tool_choice quirks.

Tool Scoping by Use Case

The tools section is where you shape what the agent can do. Disabling tools is a security and cost measure, not just a preference.

Use case Disable Enable
Research agent execution, file_ops wizsearch, deepxiv
Code assistant wizsearch, http_requests execution, file_ops
Safe assistant execution, http_requests file_ops (with path policy)

Each tool group is a block with an enabled: bool; set the ones you want off to false and configure the ones you keep (e.g. wizsearch.default_engines: [tavily, duckduckgo]).

Why disable rather than restrict: a disabled tool never enters the model’s tool array, so the model never wastes tokens considering it. Path policies (see Security) are a second layer for tools you keep enabled.

Autonomous Mode Tradeoffs

Autonomous mode turns Soothe into a 24/7 goal-runner. The knobs are all ceilings — they exist to prevent runaway cost and infinite loops, not to enable features. Set enabled: true, then bound max_iterations (per goal), max_parallel_goals, and max_loops (loop pool size). Set goal_deadline_seconds (default 14 days / 1209600) for autopilot hang detection. For interactive CLI/TUI turns, the daemon enforces the same wall-clock bound via daemon.ymlthread_pool.request_timeout_seconds (also default 14 days). Enable dreaming_enabled for long-running agents that should consolidate memory when idle; leave it off for short-lived ones. Raise protocols.durability.thread_inactivity_timeout_hours (e.g. 168 for a week) so threads survive between runs.

Key decisions:

  • max_parallel_goals × max_iterations ≈ your worst-case concurrent LLM load. Size checkpointer_pool_size accordingly.
  • dreaming_enabled runs memory consolidation when goals complete — useful for long-running agents, pure overhead for short-lived ones.
  • Autonomous mode requires durable persistence (Postgres). SQLite will bottleneck under parallel goals.

For a “controlled” variant, lower every ceiling and disable dreaming/scheduling — this gives you autonomous capability with manual cadence.

Vector Stores by Scale

For pgvector, define a pgvector provider with a dsn and index_type: hnsw, then route via vector_store_router.default: <name>:<collection>. See Provider Setup for the snippet.

Selection guide:

  • SQLite Vec (default): zero deps, fine for single-user dev and <100k vectors.
  • pgvector: production. Use hnsw for low-latency queries, ivfflat for very large static datasets, none only during development.
  • Weaviate Cloud: when you want managed vector storage decoupled from your Postgres.
  • in_memory: tests and ephemeral sessions only — data is lost on restart.

embedding_profile.embedding_dims must match your embedding model’s output (1536 for text-embedding-3-small, 3072 for -large, 1024 for DashScope). A mismatch causes silent corruption or errors at insert time.

Subagent Model Assignment

Subagents inherit the fast router role by default. Override per-subagent when a role needs different strength:

subagents:
  deep_research:
    config:
      effort: thorough
  academic_research:
    config:
      effort: normal
  planner:
    model_role: think  # router role; optional model: provider:model overrides role

Rule of thumb: research subagents use the fast role by default; planner wants reasoning (use the think role); browser_use wants vision-capable models. Disabling all subagents puts you in single-agent mode — simpler but no parallelism or specialized planning.

MCP Servers

A stdio MCP server entry needs a name, command, args, and (for servers needing auth) an env map with ${VAR}-interpolated tokens. Set defer: true (the default) on every entry unless you have a specific reason to eagerly load tools.

defer keeps MCP tools out of the model’s default tool array — they’re loaded on demand. This matters because a server exposing 50 tools would otherwise consume context budget on every turn. Use tool_filter (glob allowlist) to further trim noisy servers. See YAML Reference for the field list.

Logging Levels

Setting When to use
verbosity: debug + debug: true Tracing a misbehaving loop
verbosity: normal Everyday use
verbosity: quiet Production / headless automation
thread_logging_enabled: true Multi-thread debugging (off in prod to save disk)

Rotate logs with log_file_max_bytes / log_file_backup_count; defaults (5 MB, 3 backups) are reasonable for dev, increase retention for production auditing.

Testing Configs

For unit tests, use a mock provider (provider_type: openai, api_key: mock-key), route to mock:gpt-4o-mini, and point SQLite checkpoints at a /tmp path. No network, no real keys.

For integration tests, point at a real provider but cap global_max_llm_calls to bound cost, and set langfuse.environment: testing to keep traces out of production dashboards.

Common Mistakes

  • Mismatched embedding_profile.embedding_dims — the most common silent failure. Verify against your embedding model’s spec.
  • Forgetting think/fast — leaving them null makes everything use default, which is often the most expensive model.
  • SQLite under parallel autonomous goals — works for one goal, deadlocks under concurrency. Switch to Postgres.
  • Hardcoding keys in YAML — use ${VAR}. See Environment Variables.
  • Setting rpm_limit above provider tier — you’ll hit 429s; Soothe retries, but latency compounds.

See Also