Configuration Guide

Define your entire agent in a config file — model, tools, compaction, limits — and construct it with two lines of Rust:

use phi_core::{parse_config_file, agent_from_config, Agent};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = parse_config_file(Path::new("agent.toml"))?;
    let agent = agent_from_config(&config)?;

    // agent is Arc<dyn Agent> — ready to prompt
    println!("Agent model: {:?}", agent.model_config().unwrap().id);
    Ok(())
}

Overview

The configuration system replaces scattered Rust builder calls with a declarative config file. Instead of this:

#![allow(unused)]
fn main() {
let agent = BasicAgent::new(ModelConfig::anthropic("claude-sonnet-4-20250514", "Sonnet", &key))
    .with_system_prompt("You are a coding assistant.")
    .with_thinking(ThinkingLevel::High)
    .with_temperature(0.2)
    .with_execution_limits(ExecutionLimits { max_turns: 50, .. })
    .with_context_config(ContextConfig { .. });
}

You write a TOML file:

[agent]
system_prompt = "You are a coding assistant."

[agent.profile]
thinking_level = "high"
temperature = 0.2

[provider]
model = "claude-sonnet-4-20250514"
api_key = "${ANTHROPIC_API_KEY}"

[execution]
max_turns = 50

Three formats supported: TOML (primary, Rust-idiomatic), JSON (programmatic generation), YAML (human-friendly alternative).

Pipeline: Config file → parse_config_file()AgentConfig struct → agent_from_config()Arc<dyn Agent>


Quick Start

1. Create agent.toml:

[provider]
model = "claude-sonnet-4-20250514"
api_key = "${ANTHROPIC_API_KEY}"

2. Load and use it:

use phi_core::{parse_config_file, agent_from_config, Agent};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = parse_config_file(Path::new("agent.toml"))?;
    let agent = agent_from_config(&config)?;

    // The agent is an Arc<dyn Agent> wrapping a BasicAgent internally.
    // Access configuration through trait methods:
    let model = agent.model_config().unwrap();
    println!("Using model: {} via {}", model.id, model.provider);

    Ok(())
}

Only the [provider] section is required. Everything else has sensible defaults.


Config Formats

The primary format. Clean, readable, Rust-idiomatic.

#![allow(unused)]
fn main() {
use phi_core::config::{parse_config, ConfigFormat};

let toml_str = r#"
[provider]
model = "claude-sonnet-4-20250514"
api_key = "sk-..."
"#;
let config = parse_config(toml_str, ConfigFormat::Toml)?;
}

JSON

Useful when generating config programmatically.

#![allow(unused)]
fn main() {
let json_str = r#"{ "provider": { "model": "gpt-4o", "api_key": "sk-...", "api": "openai" } }"#;
let config = parse_config(json_str, ConfigFormat::Json)?;
}

YAML

Human-friendly alternative.

#![allow(unused)]
fn main() {
let yaml_str = "provider:\n  model: claude-sonnet-4-20250514\n  api_key: sk-...";
let config = parse_config(yaml_str, ConfigFormat::Yaml)?;
}

Auto-Detection

parse_config_file detects format from the file extension:

ExtensionFormat
.tomlTOML
.jsonJSON
.yaml, .ymlYAML

parse_config_auto tries all formats in order (TOML → JSON → YAML) and returns the first successful parse.


Environment Variable Substitution

Any string field in the config can reference environment variables with ${VAR}:

[provider]
api_key = "${ANTHROPIC_API_KEY}"
base_url = "${CUSTOM_API_URL}"

[agent]
system_prompt = "Running in ${ENVIRONMENT} mode."

How it works:

  • Substitution happens before parsing (pre-parse text replacement)
  • Works in all three formats (TOML, JSON, YAML)
  • Missing variables produce ConfigError::MissingEnvVar
  • Malformed patterns like ${UNCLOSED are passed through literally
  • Empty ${} is passed through literally

Agent Profile

An AgentProfile is a reusable blueprint that defines default configuration. Multiple agent instances can share the same profile while overriding specific fields.

[agent.profile]
name = "coding-agent"
description = "An agent specialized for code generation and review"
system_prompt = "You are an expert software engineer."
thinking_level = "high"
temperature = 0.2
max_tokens = 16384
config_id = "coder"
skills = ["code-review", "debugging"]

System Prompt Resolution

The system prompt is resolved through a priority chain. The first non-empty value wins:

  1. [agent].system_prompt — explicit agent-level override (highest priority)
  2. Profile instance system_prompt — when an agent instance references a profile via {{agent_profile.name}}
  3. [agent.profile].system_prompt — base inline profile fallback
  4. Empty string (no system prompt)

Inline Text

The simplest form — write the prompt directly:

[agent.profile]
system_prompt = "You are an expert software engineer."

Agent-level overrides the profile:

[agent.profile]
system_prompt = "You are a general assistant."   # default from blueprint

[agent]
system_prompt = "You are a Python specialist."   # overrides the profile

File Reference (file: prefix)

Load the prompt from a file. Relative paths resolve from the agent's workspace directory:

[agent]
workspace = "workspace"

[agent.profile]
system_prompt = "file:system_prompt.md"        # resolves to workspace/system_prompt.md

Absolute paths are used as-is:

[agent.profile]
system_prompt = "file:/etc/phi/prompts/coder.md"

The file: prefix works at all levels: [agent].system_prompt, [agent.profile].system_prompt, and [[agent.profile.instances]].system_prompt.

Strategy Reference ({{...}} protocol)

For advanced multi-block prompt composition, reference a system prompt instance. This uses a 3-entity chain: strategy (block template) → prompt instance (block content) → agent reference.

# 1. Define the strategy template — block structure with ordering and size limits
[[system_prompt_strategy.instances]]
id = "{{coding_strategy}}"

[[system_prompt_strategy.instances.blocks]]
name = "identity"
order = 0
max_length = 2000

[[system_prompt_strategy.instances.blocks]]
name = "instructions"
order = 1
max_length = 3000

[[system_prompt_strategy.instances.blocks]]
name = "constraints"
order = 2
max_length = 1000

# 2. Define the prompt instance — fills content into the strategy's blocks
#    Block values can be inline text or file: references (relative to workspace)
[[system_prompt.instances]]
id = "{{coding_prompt}}"
description = "System prompt for coding agents"
type = "{{system_prompt_strategy.coding_strategy}}"
identity = "You are an expert software engineer at a fintech company."
instructions = "file:prompts/coding_instructions.md"
constraints = "Never modify production databases. Always write tests."

# 3. Reference the prompt instance from the agent
[agent]
system_prompt = "{{system_prompt.coding_prompt}}"
workspace = "workspace"

The builder resolves the chain: finds the prompt instance → finds its strategy → sorts blocks by order → resolves file: paths → truncates each block to max_length → joins with double newlines.

See the Field Reference for [system_prompt_strategy] and [system_prompt] sections.

Profile Instance Override

When using named profile instances, the instance's system_prompt participates in resolution. The system_prompt field on a profile instance supports all three modes — inline text, file: path, or {{...}} reference to a system_prompt instance:

# ── System prompt strategy + instance (reusable prompt definition) ───
[[system_prompt_strategy.instances]]
id = "{{simple}}"

[[system_prompt_strategy.instances.blocks]]
name = "identity"
order = 0
max_length = 5000

[[system_prompt.instances]]
id = "{{coder_prompt}}"
type = "{{system_prompt_strategy.simple}}"
identity = "file:prompts/coder.md"

[[system_prompt.instances]]
id = "{{reviewer_prompt}}"
type = "{{system_prompt_strategy.simple}}"
identity = "file:prompts/reviewer.md"

# ── Profile instances reference system_prompt instances ──────────────
[agent.profile]
name = "base"
system_prompt = "You are a general assistant."   # base fallback

[[agent.profile.instances]]
id = "{{coder}}"
system_prompt = "{{system_prompt.coder_prompt}}"   # profile → system_prompt instance
temperature = 0.2
max_tokens = 16384

[[agent.profile.instances]]
id = "{{reviewer}}"
system_prompt = "{{system_prompt.reviewer_prompt}}" # profile → system_prompt instance
temperature = 0.1
max_tokens = 8192

# ── Agent instances reference profile instances ──────────────────────
[[agent.instances]]
name = "code-writer"
agent_profile = "{{agent_profile.coder}}"          # agent → profile → system_prompt

[[agent.instances]]
name = "code-reviewer"
agent_profile = "{{agent_profile.reviewer}}"       # agent → profile → system_prompt

[[agent.instances]]
name = "generalist"
# no agent_profile → uses base [agent.profile].system_prompt

Full reference chain: agent.instancesagent.profile.instances (via agent_profile) → system_prompt.instances (via system_prompt) → system_prompt_strategy.instances (via type). Each layer can override or inherit from the one above.

When an agent instance omits agent_profile, it is built using the base [agent.profile] directly (no instance override). The base profile's system_prompt, temperature, and other fields apply as defaults.

Workspace-relative Resolution

The file: prefix resolves relative to the agent's workspace directory. Each agent instance can set its own workspace, so the same file: reference resolves to different files per agent:

[agent.profile]
name = "base"

[[agent.profile.instances]]
id = "{{copywriter}}"
system_prompt = "file:system_prompt.md"   # same file ref, different workspace resolution
temperature = 0.7

[[agent.instances]]
name = "alpha-writer"
agent_profile = "{{agent_profile.copywriter}}"
workspace = "projects/alpha"              # reads projects/alpha/system_prompt.md

[[agent.instances]]
name = "beta-writer"
agent_profile = "{{agent_profile.copywriter}}"
workspace = "projects/beta"               # reads projects/beta/system_prompt.md

Workspace resolution order:

  1. [[agent.instances]].workspace — per-agent instance (highest priority)
  2. [agent].workspace — shared agent-level
  3. default_workspace — global default
  4. "." — current directory

Thinking Level

Controls depth of model reasoning. Specified as a string in config:

Config ValueRust EnumDescription
"off"ThinkingLevel::OffNo chain-of-thought (default)
"minimal"ThinkingLevel::MinimalLightweight reasoning
"low"ThinkingLevel::LowSome reasoning
"medium"ThinkingLevel::MediumModerate reasoning
"high"ThinkingLevel::HighDeep reasoning before responding

Parsing is case-insensitive: "High", "HIGH", "high" all work.

Skills vs Tools

skills in the profile are skill names loaded via SkillSet from SKILL.md files (per the AgentSkills standard). They are NOT tools. See Skills for details.


Profile Instances

Profile instances are named variations of the profile blueprint. Each instance inherits the profile defaults and overrides specific fields. This lets you define a single profile and then create specialized variants without duplicating the entire configuration.

Use [[agent.profile.instances]] to define instances. Each instance requires an id field using the {{...}} ID reference protocol (see below). Instance fields override the corresponding profile defaults; any field not specified falls through to the profile value.

Agent instances reference a profile instance via the agent_profile field, using either a qualified reference ({{agent_profile.name}}) or an unqualified reference ({{name}}) if the name is unique across all namespaces.

Example

# ── Profile defaults ──────────────────────────────────────────
[agent.profile]
name = "coding-agent"
description = "An agent specialized for code tasks"
system_prompt = "You are an expert software engineer."
thinking_level = "high"
temperature = 0.2
max_tokens = 16384

# ── Profile instances (override specific fields) ─────────────
[[agent.profile.instances]]
id = "{{%coder%}}"
description = "A code generation specialist"
thinking_level = "high"
temperature = 0.2
max_tokens = 16384
config_id = "coder"

[[agent.profile.instances]]
id = "{{%reviewer%}}"
description = "A code review specialist"
thinking_level = "high"
temperature = 0.1
max_tokens = 8192
config_id = "reviewer"

# ── Agent instances referencing profile instances ─────────────
[[agent.instances]]
name = "code-writer"
agent_profile = "{{agent_profile.coder}}"
system_prompt = "You write clean, well-tested code. Follow existing patterns."

[[agent.instances]]
name = "code-reviewer"
agent_profile = "{{agent_profile.reviewer}}"
system_prompt = "You review code for bugs, security issues, and style violations."

The code-writer agent inherits all profile defaults and applies the coder instance overrides. The code-reviewer agent uses the reviewer instance, which sets a lower temperature and smaller token budget for more focused review output.


ID Reference Protocol

The {{...}} syntax is a lightweight reference protocol for linking configuration entities (providers, profile instances, sub-agents) by name. It appears in id fields (to declare an entity) and in reference fields like provider and agent_profile (to point to an entity).

Syntax

PatternMeaning
{{type.name}}Qualified reference, recreate if invoked
{{%type.name%}}Qualified reference, no recreation if already exists
{{name}}Unqualified reference (unique resolve), recreate if invoked
{{%name%}}Unqualified reference, no recreation if already exists
{{#system_id#}}Literal system ID, no recreation

Namespaces

References are resolved within namespaces. The three namespaces are:

  • agent_profile -- Profile instances declared in [[agent.profile.instances]]
  • provider -- Provider instances declared in [[provider.instances]]
  • sub_agent -- Sub-agent instances declared in [[sub_agents.instances]]

Resolution

Qualified references ({{type.name}}) include the namespace prefix and always resolve unambiguously. Use these when multiple namespaces could contain the same name.

Unqualified references ({{name}}) omit the namespace. The system searches all namespaces and resolves the reference only if the name is unique. If multiple entities share the same name across namespaces, an unqualified reference is ambiguous and will produce an error.

Recreation Semantics

The % sigil controls whether an entity is recreated when referenced:

  • Without % ({{name}} or {{type.name}}): The entity is recreated each time it is resolved. Use this when you want fresh instances.
  • With % ({{%name%}} or {{%type.name%}}): The entity is reused if it already exists (matched by latest creation date). Use this for shared singletons like provider connections.

The {{#system_id#}} form references a literal system-generated ID and never triggers recreation.

Usage in ID Fields

When declaring an entity, the id field establishes the entity's name within its namespace:

[[provider.instances]]
id = "{{%openai%}}"          # declares "openai" in the provider namespace
model = "gpt-4o"

Usage in Reference Fields

When referencing an entity from another section, use the reference syntax:

[[agent.instances]]
name = "my-agent"
provider = "{{provider.openai}}"       # qualified reference
agent_profile = "{{reviewer}}"         # unqualified (must be unique)

Provider Configuration

The [provider] section defines the LLM model, API credentials, and protocol.

[provider]
model = "claude-sonnet-4-20250514"    # Model ID sent to the API
api_key = "${ANTHROPIC_API_KEY}"      # API credential
api = "anthropic_messages"            # API protocol
provider = "anthropic"                # Provider name
name = "Claude Sonnet 4"             # Human-friendly display name
reasoning = true                      # Model supports thinking
context_window = 200000               # Context window in tokens
max_tokens = 8192                     # Default max output tokens

API Protocols

Config ValueAliasesProtocol
"anthropic_messages""anthropic"Anthropic Messages API
"openai_completions""openai"OpenAI Chat Completions
"openai_responses"OpenAI Responses API
"azure_openai_responses""azure"Azure OpenAI
"google_generative_ai""google", "gemini"Google Gemini
"google_vertex""vertex"Google Vertex AI
"bedrock_converse_stream""bedrock"Amazon Bedrock

Default base URLs are set automatically per protocol when base_url is omitted:

  • Anthropic: https://api.anthropic.com
  • OpenAI: https://api.openai.com
  • Google: https://generativelanguage.googleapis.com
  • Others: empty (uses provider defaults)

Important: The API protocol is NOT auto-detected from the model name. If you set model = "gpt-4o", you must also set api = "openai" explicitly.

Cost Rates

Enable cost tracking by setting per-token rates:

[provider.cost]
input_per_million = 3.0       # $ per million input tokens
output_per_million = 15.0     # $ per million output tokens
cache_read_per_million = 0.3  # $ per million cache-read tokens
cache_write_per_million = 3.75

Cost is tracked automatically after each turn. Combine with [execution].max_cost to enforce a budget.

Custom Headers

[provider]
model = "my-model"

[provider.headers]
"X-Custom-Header" = "value"
"Authorization" = "Bearer ${CUSTOM_TOKEN}"

Multiple Providers

Use [[provider.instances]] to define named providers alongside the default. Each instance uses the {{...}} ID reference protocol to declare its name in the provider namespace. The url field is an alias for base_url.

# Default provider — Anthropic (used unless overridden)
[provider]
model = "claude-sonnet-4-20250514"
name = "Claude Sonnet 4"
api_key = "${ANTHROPIC_API_KEY}"
api = "anthropic_messages"
provider = "anthropic"

[provider.cost]
input_per_million = 3.0
output_per_million = 15.0
cache_read_per_million = 0.3
cache_write_per_million = 3.75

# OpenAI
[[provider.instances]]
id = "{{%openai%}}"
description = "OpenAI GPT-4o provider"
name = "GPT-4o"
model = "gpt-4o"
api_key = "${OPENAI_API_KEY}"
api = "openai_completions"
url = "https://api.openai.com/v1"

# OpenRouter
[[provider.instances]]
id = "{{%openrouter%}}"
description = "OpenRouter multi-model gateway"
name = "OpenRouter"
model = "anthropic/claude-sonnet-4"
api_key = "${OPENROUTER_API_KEY}"
api = "openai_completions"
url = "https://openrouter.ai/api/v1"
provider = "openrouter"

# Google Gemini
[[provider.instances]]
id = "{{%gemini%}}"
description = "Google Gemini 2.5 Flash provider"
name = "Gemini 2.5 Flash"
model = "gemini-2.5-flash"
api_key = "${GOOGLE_API_KEY}"
api = "google_generative_ai"

# Ollama (local)
[[provider.instances]]
id = "{{%ollama%}}"
description = "Local Ollama instance for development"
name = "Ollama Llama 3.2"
model = "llama3.2"
api = "openai_completions"
url = "http://localhost:11434/v1"
api_key = "not-needed"
provider = "ollama"

Agent instances and sub-agents reference these via the ID protocol (e.g., provider = "{{provider.openai}}" or provider = "{{ollama}}" if unique).


Session Configuration

The [session] section controls session scope.

[session]
scope = "persistent"       # "ephemeral" (default) or "persistent"

Session Scope

ValueBehavior
"ephemeral"Session exists only in memory for the process lifetime (default)
"persistent"Session data is written to a store and survives restarts

Note: Setting scope = "persistent" declares intent but does not automatically configure a storage backend. The caller must set up session persistence using the session recorder.

Thinking level and temperature are configured per-loop via LoopConfigSnapshot (captured on each AgentStart event) rather than at the session level. Set them on the agent profile or AgentLoopConfig.


Tools

The [tools] section declares which tools the agent can use and how they execute.

[tools]
enabled = ["bash", "file_read", "file_write", "search"]
tool_strategy = "parallel"   # "sequential", "parallel", or "batched"
batch_size = 3               # Only used when strategy is "batched"

Tool Execution Strategies

StrategyBehavior
"sequential"One tool at a time; checks steering queue between each
"parallel"All tool calls concurrent; check steering after all complete (default)
"batched"Run N concurrent, wait, check steering, next batch

Context Pruning

Enable model-directed context pruning with with_prun_tool(). This lets the model surgically remove irrelevant inrun content (its own messages, tool calls, tool results) from the working context to reclaim space in the context window. User messages are never pruned. See Context Pruning for details.

#![allow(unused)]
fn main() {
let agent = BasicAgent::new(model_config)
    .with_default_tools()
    .with_prun_tool();
}

Or via config:

[tools]
enabled = ["bash", "read_file", "write_file", "prun"]

Registering Tools at Runtime

Tools are NOT instantiated from the config file. The config specifies tool names only. You must register tool instances after constructing the agent:

#![allow(unused)]
fn main() {
use phi_core::{parse_config_file, agent_from_config, Agent};
use phi_core::tools::{BashTool, ReadFileTool, WriteFileTool, SearchTool};
use std::sync::Arc;

let config = parse_config_file(Path::new("agent.toml"))?;
let agent = agent_from_config(&config)?;

// Cast to mutable and register tools
let agent_mut = Arc::get_mut(&mut agent).unwrap();
agent_mut.set_tools(vec![
    Arc::new(BashTool::default()),
    Arc::new(ReadFileTool::new()),
    Arc::new(WriteFileTool::new()),
    Arc::new(SearchTool::new()),
]);
}

Tool Registry

Instead of manually registering tools after construction, use agent_from_config_with_registry() to resolve tool names from the config automatically:

#![allow(unused)]
fn main() {
use phi_core::{parse_config_file, agent_from_config_with_registry, Agent};
use phi_core::tools::ToolRegistry;
use std::path::Path;

let config = parse_config_file(Path::new("agent.toml"))?;

// Create a registry with the 6 built-in tools
let registry = ToolRegistry::new().with_defaults();

// Tools listed in config.tools.enabled are resolved through the registry
let agent = agent_from_config_with_registry(&config, &registry)?;
}

The default registry includes all 6 built-in tools: bash, read_file, write_file, edit_file, list_files, search. You can also register custom tools:

#![allow(unused)]
fn main() {
let mut registry = ToolRegistry::new().with_defaults();
registry.register("my_tool", || Arc::new(MyCustomTool::new()));

let agent = agent_from_config_with_registry(&config, &registry)?;
}

Unknown tool names in tools.enabled are silently skipped. Use registry.contains(name) to check availability before construction if needed.


Context & Compaction

The [compaction] section controls automatic context management. When the conversation grows too long, compaction summarizes older messages to stay within the model's context window.

[compaction]
max_context_tokens = 200000     # Model's context window
system_prompt_tokens = 4000     # Tokens reserved for system prompt
compact_at_pct = 0.85           # Start measuring at 85% capacity
compact_budget_threshold_pct = 0.05  # Compact when < 5% headroom remains
keep_first_turns = 2            # Keep first 2 turns verbatim
keep_recent_turns = 4           # Keep last 4 turns verbatim
max_summary_tokens = 2000       # Token budget for the summarized middle
tool_output_max_lines = 50      # Truncate tool outputs to 50 lines

Compaction must be explicitly enabled by setting max_context_tokens. If omitted, compaction is disabled entirely.

How Compaction Works

  1. Before each LLM turn, the loop estimates current token usage
  2. If usage exceeds the trigger threshold, compaction fires
  3. First N turns are kept verbatim (preserves initial context)
  4. Middle turns are summarized (aggressive token reduction)
  5. Last M turns are kept verbatim (preserves recent history)
  6. Tool outputs in kept turns are truncated to max_lines

See Context Compaction for the full algorithm.

Focused Compaction

The focus_message field steers what the compaction summary emphasizes. Compaction instances let you define named variations that agent profiles can reference.

[compaction]
max_context_tokens = 200000
focus_message = "Retain key decisions and code changes."

# Named compaction instances
[[compaction.instances]]
id = "{{%coding%}}"
focus_message = "Focus on file paths, function signatures, and design rationale."
keep_recent_turns = 6
max_summary_tokens = 3000

[[compaction.instances]]
id = "{{%research%}}"
focus_message = "Preserve citations, data sources, and methodology."
keep_first_turns = 3
max_summary_tokens = 4000

Profiles reference a compaction instance via compaction = "{{compaction.coding}}":

[agent.profile]
name = "coding-agent"
compaction = "{{compaction.coding}}"

See Focused Compaction for full details.


Execution Limits

The [execution] section sets safety guards that prevent runaway loops and budget overruns.

[execution]
max_turns = 50              # Maximum LLM turns (default: 50)
max_total_tokens = 1000000  # Total token budget (default: 1,000,000)
max_duration_secs = 600     # Wall-clock timeout in seconds (default: 600)
max_cost = 5.0              # Dollar cost cap (requires [provider.cost] rates)

Cost Tracking

Cost enforcement requires both cost rates and a budget:

[provider.cost]
input_per_million = 3.0
output_per_million = 15.0

[execution]
max_cost = 5.0   # Stop when accumulated cost reaches $5

Without cost rates (all zeros), max_cost has no effect. Token usage is always tracked regardless.

Retry Configuration

Automatic retry for transient provider errors (rate limits, network issues):

[execution.retry]
max_retries = 3           # Retry attempts (default: 3, 0 = disabled)
initial_delay_ms = 1000   # First retry delay in ms
backoff_multiplier = 2.0  # Exponential backoff multiplier
max_delay_ms = 30000      # Maximum delay cap

Only RateLimited and Network errors are retried. Invalid requests and context overflows fail immediately.

Cache Configuration

Control prompt caching behavior:

[execution.cache]
enabled = true        # Master switch (default: true)
strategy = "auto"     # "auto" or "disabled"

Sub-Agents

Define sub-agents that run their own agent loops when invoked as tools:

[[sub_agents.instances]]
name = "researcher"
description = "Searches the web for information"
system_prompt = "You are a research assistant. Search thoroughly."
model = "claude-haiku-4-5-20251001"
max_turns = 10
tools = ["web_search"]

[[sub_agents.instances]]
name = "code_writer"
description = "Writes and edits code files"
system_prompt = "You are a code generation expert."
provider = "openai"    # References a [[provider.instances]] by name
max_turns = 20
tools = ["bash", "file_write"]

Sub-agents do NOT inherit the parent agent's configuration. Each sub-agent is fully independent — set all needed fields explicitly.


Multi-Agent Configurations

For complex setups, combine named providers with named agent instances:

# Providers
[provider]
model = "claude-sonnet-4-20250514"
api_key = "${ANTHROPIC_API_KEY}"

[[provider.instances]]
name = "fast"
model = "claude-haiku-4-5-20251001"
api_key = "${ANTHROPIC_API_KEY}"

# Agent instances
[[agent.instances]]
name = "planner"
system_prompt = "You are an architect. Plan the approach."
provider = "fast"

[[agent.instances]]
name = "executor"
system_prompt = "You are an implementer. Write the code."

Agent Workspace

The workspace field sets the working directory for an agent. Tools that interact with the filesystem (bash, file read/write, etc.) use this as their base path.

There are two levels of workspace configuration:

  • default_workspace (top-level config field): Sets the default workspace for all agents. If omitted, the current working directory is used.
  • workspace (per-agent field on [agent.profile] or [[agent.instances]]): Overrides default_workspace for a specific agent.
default_workspace = "/home/user/projects"

[agent.profile]
workspace = "/home/user/projects/my-app"   # overrides default_workspace for this agent

Callbacks & Hooks

The config schema accepts [callbacks] and [hooks] sections for lifecycle hooks:

[callbacks]
before_loop = "my_plugin::before_loop"
after_turn = "my_plugin::after_turn"
before_task = "./scripts/on_task_start.sh"
after_task = "python3 scripts/after_task.py"

[hooks]
transform_context = "my_plugin::transform"

Script-based callbacks (shell scripts, Python scripts) are supported. The agent spawns the script as a subprocess, passing context via environment variables. Exit code 0 means continue; non-zero aborts the action (for Before* hooks). WASM plugin loading for Rust-native callbacks is planned for Phase 2.

Session-Level Callbacks

before_task and after_task are session-level callbacks configured on SessionRecorderConfig:

  • before_task: Fires on the first AgentStart event with a new session_id. Use for task-level setup, metrics initialization, or audit logging.
  • after_task: Fires on flush(). Use for task-level teardown, billing, or summary generation.

Programmatic Hooks

To set hooks programmatically, use the Agent trait setter methods after construction:

#![allow(unused)]
fn main() {
let agent = agent_from_config(&config)?;
let agent_mut = Arc::get_mut(&mut agent).unwrap();
agent_mut.set_before_loop(Some(Arc::new(|msgs, n| {
    println!("Loop starting with {} messages", msgs.len());
    true // return false to abort
})));
}

Complete Example

A full coding agent configuration using every section:

# ── Agent identity ────────────────────────────────────────────
[agent]
system_prompt = "You are an expert software engineer."

[agent.profile]
name = "coding-agent"
description = "Full-featured coding assistant"
thinking_level = "high"
temperature = 0.2
max_tokens = 16384
config_id = "coder-v1"
skills = ["code-review"]

# ── Provider ──────────────────────────────────────────────────
[provider]
model = "claude-sonnet-4-20250514"
api_key = "${ANTHROPIC_API_KEY}"
reasoning = true
context_window = 200000

[provider.cost]
input_per_million = 3.0
output_per_million = 15.0
cache_read_per_million = 0.3
cache_write_per_million = 3.75

# ── Session ───────────────────────────────────────────────────
[session]
scope = "persistent"

# ── Tools ─────────────────────────────────────────────────────
[tools]
enabled = ["bash", "file_read", "file_write", "search", "edit_file"]
tool_strategy = "parallel"

# ── Context management ────────────────────────────────────────
[compaction]
max_context_tokens = 200000
system_prompt_tokens = 4000
compact_at_pct = 0.85
keep_first_turns = 2
keep_recent_turns = 4
max_summary_tokens = 2000
tool_output_max_lines = 50

# ── Execution limits ──────────────────────────────────────────
[execution]
max_turns = 100
max_total_tokens = 2000000
max_duration_secs = 1800
max_cost = 10.0

[execution.retry]
max_retries = 3
initial_delay_ms = 1000
backoff_multiplier = 2.0

[execution.cache]
enabled = true
strategy = "auto"

# ── Sub-agents ────────────────────────────────────────────────
[[sub_agents.instances]]
name = "researcher"
description = "Searches for information and documentation"
system_prompt = "Find relevant information. Be thorough."
model = "claude-haiku-4-5-20251001"
max_turns = 10
tools = ["web_search"]

Field Reference

[agent]

FieldTypeDefaultDescription
system_promptstringNoneAgent-level system prompt (overrides profile). Supports: inline text, file:path (relative to workspace), or {{...}} reference to a [[system_prompt.instances]] entry.
profiletable(empty)Profile blueprint (see below)
workspacestringNoneWorkspace directory for file: resolution and tool paths
instancesarray[]Named agent instances

[agent.profile]

FieldTypeDefaultDescription
profile_idstringUUIDUnique profile identifier
namestringNoneHuman-readable name
descriptionstringNoneProfile description
system_promptstringNoneDefault system prompt. Supports: inline text, file:path, or {{...}} reference.
thinking_levelstringNone"off", "minimal", "low", "medium", "high"
temperaturefloatNoneLLM temperature (0.0-2.0)
max_tokensintegerNoneMax output tokens
config_idstringNoneStable identity for loop_id generation
skillsarray[]Skill names (SKILL.md, not tools)
instancesarray[]Named profile instances (see ProfileInstanceSection)

ProfileInstanceSection

Each entry in [[agent.profile.instances]]:

FieldTypeDefaultDescription
idstringrequired{{...}} ID in the agent_profile namespace
descriptionstringNoneHuman-readable description of this variant
namestring(from profile)Override name
system_promptstring(from profile)Override system prompt (supports inline, file:, or {{...}})
thinking_levelstring(from profile)Override thinking level
temperaturefloat(from profile)Override temperature
max_tokensinteger(from profile)Override max output tokens
config_idstringNoneStable identity for loop_id generation
skillsarray(from profile)Override skill names

AgentInstanceSection

Each entry in [[agent.instances]]:

FieldTypeDefaultDescription
namestring"unnamed"Instance name
agent_profilestringNoneProfile instance reference ({{...}} syntax)
profiletableNoneInline profile override (not a reference)
system_promptstringNoneInstance-specific system prompt
providerstring(default provider)Provider reference ({{...}} syntax)
workspacestringNonePer-instance workspace directory (overrides [agent].workspace)

[provider]

FieldTypeDefaultDescription
modelstring"unknown"Model ID sent to API
api_keystring""API credential (supports ${VAR})
apistring"anthropic_messages"API protocol
base_urlstring(per protocol)API base URL (url is an accepted alias)
providerstring"anthropic"Provider name
namestringmodel valueDisplay name
reasoningboolfalseSupports thinking/reasoning
context_windowinteger200000Context window tokens
max_tokensinteger8192Default max output tokens

ProviderInstanceSection

Each entry in [[provider.instances]] accepts all fields from [provider] above, plus:

FieldTypeDefaultDescription
idstringNone{{...}} ID in the provider namespace
descriptionstringNoneHuman-readable description of this provider
urlstringNoneAlias for base_url

[provider.cost]

FieldTypeDefaultDescription
input_per_millionfloat0.0Input token rate
output_per_millionfloat0.0Output token rate
cache_read_per_millionfloat0.0Cache read rate
cache_write_per_millionfloat0.0Cache write rate

[session]

FieldTypeDefaultDescription
scopestring"ephemeral""ephemeral" or "persistent"

[tools]

FieldTypeDefaultDescription
enabledarray[]Tool names (resolved by caller)
tool_strategystring"parallel""sequential", "parallel", "batched"
batch_sizeinteger3Batch size for "batched" strategy

[compaction]

FieldTypeDefaultDescription
max_context_tokensintegerNoneContext window (must set to enable compaction)
system_prompt_tokensinteger4000Reserved system prompt tokens
compact_at_pctfloat0.90Measurement threshold
compact_budget_threshold_pctfloat0.05Compaction trigger
keep_first_turnsinteger2Verbatim turns from start
keep_recent_turnsinteger10Verbatim turns from end
max_summary_tokensinteger2000Summary token budget
tool_output_max_linesinteger50Tool output line cap

[system_prompt_strategy]

Strategy templates define block structure for multi-block system prompts.

[[system_prompt_strategy.instances]]

FieldTypeDefaultDescription
idstringrequired{{...}} ID for this strategy template
descriptionstringNoneHuman-readable description
blocksarray[]Block definitions (see below)

[[system_prompt_strategy.instances.blocks]]

FieldTypeDefaultDescription
namestringrequiredBlock name (e.g., "identity", "instructions", "constraints")
orderinteger0Assembly order — lower appears first in the composed prompt
max_lengthintegerunlimitedMaximum character budget for this block

[system_prompt]

Prompt instances fill content into a strategy's blocks.

[[system_prompt.instances]]

FieldTypeDefaultDescription
idstringrequired{{...}} ID for this prompt instance
descriptionstringNoneHuman-readable description
typestringNone{{...}} reference to a strategy instance (e.g., "{{system_prompt_strategy.coding}}")
(block names)stringEach block defined in the strategy gets a field here. Value is inline text or "file:path" (relative to workspace).

Note: Block content fields use #[serde(flatten)] — they appear as top-level keys on the instance, not nested under a blocks table.

[execution]

FieldTypeDefaultDescription
max_turnsinteger50Maximum LLM turns
max_total_tokensinteger1000000Total token budget
max_duration_secsinteger600Wall-clock timeout (seconds)
max_costfloatNoneDollar cost cap

[execution.retry]

FieldTypeDefaultDescription
max_retriesinteger3Retry attempts (0 = disabled)
initial_delay_msinteger1000First retry delay (ms)
backoff_multiplierfloat2.0Exponential backoff factor
max_delay_msinteger30000Maximum delay cap (ms)

[execution.cache]

FieldTypeDefaultDescription
enabledbooltrueMaster switch
strategystring"auto""auto" or "disabled"

Error Handling

agent_from_config() and the parse functions return ConfigError:

VariantCauseFix
Parse(msg)Invalid TOML/JSON/YAML syntaxCheck syntax; the message includes the parser error
MissingEnvVar { var }${VAR} references an unset env varSet the variable or remove the reference
InvalidField { field, value, expected }Invalid enum value (e.g., thinking_level = "extreme")Use one of the expected values
Io(err)File not found or not readableCheck file path and permissions

Common Mistakes

Forgetting to set the API protocol for non-Anthropic models:

# Wrong — defaults to anthropic_messages, fails at runtime
[provider]
model = "gpt-4o"
api_key = "${OPENAI_API_KEY}"

# Correct
[provider]
model = "gpt-4o"
api_key = "${OPENAI_API_KEY}"
api = "openai"

Setting max_cost without cost rates:

# max_cost is ignored — no rates to compute cost from
[execution]
max_cost = 5.0

# Correct — set rates AND budget
[provider.cost]
input_per_million = 3.0
output_per_million = 15.0

[execution]
max_cost = 5.0

Expecting tools to be instantiated from config:

[tools]
enabled = ["bash", "file_read"]
# These are names only — you must call agent.set_tools() in Rust

Programmatic Usage

Using AgentConfig Directly

You can construct AgentConfig in Rust without a file:

#![allow(unused)]
fn main() {
use phi_core::config::schema::{AgentConfig, ProviderSection, ProfileSection, AgentSection};

let config = AgentConfig {
    provider: ProviderSection {
        model: Some("claude-sonnet-4-20250514".into()),
        api_key: Some(std::env::var("ANTHROPIC_API_KEY")?),
        ..Default::default()
    },
    agent: AgentSection {
        system_prompt: Some("You are helpful.".into()),
        profile: ProfileSection {
            thinking_level: Some("high".into()),
            ..Default::default()
        },
        ..Default::default()
    },
    ..Default::default()
};

let agent = agent_from_config(&config)?;
}

Mixing Config with Programmatic Overrides

After agent_from_config(), use Agent trait methods to add hooks, tools, or modify settings:

#![allow(unused)]
fn main() {
use phi_core::{parse_config_file, agent_from_config, Agent};
use std::sync::Arc;

let config = parse_config_file(Path::new("agent.toml"))?;
let mut agent = agent_from_config(&config)?;

// Get mutable access to add tools and hooks
let a = Arc::get_mut(&mut agent).unwrap();
a.set_tools(vec![Arc::new(phi_core::tools::BashTool::default())]);
a.set_before_loop(Some(Arc::new(|msgs, _| {
    println!("Starting with {} messages", msgs.len());
    true
})));
}

Reading Config Through the Agent Trait

All configuration is accessible through Agent trait methods:

#![allow(unused)]
fn main() {
let agent = agent_from_config(&config)?;

// Config accessors (all have defaults)
agent.model_config();       // Option<&ModelConfig>
agent.profile();            // Option<&AgentProfile>
agent.system_prompt();      // &str
agent.thinking_level();     // ThinkingLevel
agent.temperature();        // Option<f32>
agent.max_tokens();         // Option<u32>
agent.context_config();     // Option<&ContextConfig>
agent.execution_limits();   // Option<&ExecutionLimits>
agent.cache_config();       // CacheConfig
agent.tool_execution();     // ToolExecutionStrategy
agent.retry_config();       // RetryConfig
agent.session();            // Option<&Session>
agent.build_config();       // Result<AgentLoopConfig, AgentBuildError>
                            // Default impl returns Err(MissingModelConfig)
                            // if model_config() returns None. BasicAgent's
                            // override always returns Ok(...).
}