Agents
This chapter describes the agent configuration model in ARES v0.10.0. Every field here is read from the source files named in each section.
Agent behavior comes from TOML configuration. The root file ares.toml carries an [agents] table. You can also keep agents in TOON files under config/agents/. The config.agents_dir key in ares.toml sets that directory (crates/ares-http/src/overlay.rs, DynamicConfigPaths).
Agent fields
Each agent entry deserializes into AgentConfig (crates/ares-agent/src/config.rs):
| Field | Type | Default | Meaning |
|---|---|---|---|
model | string | required | Model name defined under [models]. Missing values fail deserialization. |
system_prompt | string | none | Personality and instructions for the agent, per the source doc comment. |
tools | list of strings | empty | Tool names this agent uses. |
allowed_tools | list of strings | all tools | Whitelist of permitted tool names. Absent means all tools are allowed. |
max_tool_iterations | integer | 10 | Maximum tool-calling rounds before the agent stops. |
parallel_tools | boolean | false | Run independent tool calls in parallel when possible. |
compaction_enabled | boolean | false | Turn on per-session history compaction through the LLM Compactor. Long conversations stay a bounded working set instead of a last-5 history slice. |
| (extra keys) | table | empty | Unknown keys pass through unchanged via #[serde(flatten)]. |
Deserialization details worth knowing when you write TOML:
- Only
modellacks a serde default. Every other field tolerates absence. allowed_toolscarriesskip_serializing_if = "Option::is_none". An absent key stays absent on round-trip, so "all tools" survives config rewrites.compaction_enabledis anOption<bool>. The engine readsunwrap_or(false), so omitting the key equalsfalse.- Extra keys land in
extra: HashMap<String, toml::Value>(#[serde(flatten)]). They never fail parsing. Runtime consumers read them by name.
Agent resolution order
The resolver (crates/ares-agent/src/resolver.rs) picks one agent definition from three tiers:
- Tenant database row (
tenant_db tenant_agents). - Community public agent.
- System agent from static TOML/TOON configuration.
The first tier that holds the name wins. The scope comes from the request isolate label, then a tenant context intercept, then a fallback id.
Skills configuration
The [skills] group deserializes into SkillsTomlConfig (crates/ares-agent/src/workflows_config.rs):
project_dir— project skills directory, for example./.claude/skills/.personal_dir— personal skills directory, for example~/.claude/skills/.plugin_dirs— extra directories to scan forSKILL.mdfiles.
All three keys are optional.
Workflows
A workflow defines how agents work together. Each entry under [workflows.<name>] deserializes into WorkflowConfig (crates/ares-agent/src/workflows_config.rs):
| Field | Type | Default | Meaning |
|---|---|---|---|
entry_agent | string | required | Agent that receives the initial request. |
fallback_agent | string | none | Agent used when routing fails or no match exists. |
max_depth | integer | 3 | Maximum depth for nested workflows. |
max_iterations | integer | 5 | Maximum iterations for iterative workflows. |
parallel_subagents | boolean | false | Execute sub-agent calls in parallel. |
The engine (crates/ares-agent/src/workflows/engine.rs) runs steps through the unified executor and records a WorkflowStep per hop: agent_name, input, output, timestamp, duration_ms. The result is a WorkflowOutput with final_response, steps_executed, agents_used, and reasoning_path. Workflows live on the context as a WorkflowSet; the HTTP overlay copies [workflows] from TOML into it.
Delegation flags
Skill delegation arguments parse into DelegationArgs (crates/ares-agent/src/skills/mod.rs). Parsing runs in two steps: tokenizing, then flag assignment.
Tokenizer rules
tokenize_delegation_args scans characters once:
- Whitespace outside quotes splits tokens. Empty tokens never appear.
- A double-quoted segment forms one token. It may contain spaces and bare
|. - Inside quotes only, a backslash escapes the next character. Outside quotes a backslash is an ordinary character.
- Quotes themselves disappear from the output token.
"delta echo"becomesdelta echo.
Flag assignment
parse_delegation_args walks the token list left to right:
--parallellatches split-per-token mode. After the flag, every plain token starts its own task and|separators are ignored. Tokens before the flag stay one sequential task.--model <value>consumes exactly one following token as the model override. A missing value returns the error--model consumes exactly one value; none given.--toolsenables the inner tool loop for delegated tasks. It takes no value.- Before the latch, bare
|separates sequential tasks. After the latch, bare|is dropped.
Parallel task cap
MAX_PARALLEL_DELEGATED_TASKS equals MAX_SKILL_CALL_DEPTH, which is 8 (crates/ares-agent/src/skills/mod.rs). Each parallel task becomes its own delegated skill call one level deeper, so the recursion depth is the natural ceiling. Excess tasks are truncated, not rejected.
Model resolution precedence
DelegationArgs::resolved_model(profile_default, global_default) applies this order:
- Explicit
--modelflag. - Profile default model, when non-empty.
- Global default model.
An empty profile default falls through to the global default.
Worked examples
Both examples come verbatim from the test suite (delegation_flag_tests, same file).
Example 1 — quoted pipe inside one sequential task:
translate "hello | world" --model fast-model --tools
Result: one task with tokens ["translate", "hello | world"], model = "fast-model", tools = true, parallel = false. The quoted pipe does not split the task because quoting happens at tokenize time, before any | handling.
Example 2 — latch mid-stream:
alpha | beta --parallel gamma "delta echo" | epsilon
Result: five tasks — alpha, beta, gamma, delta echo, epsilon. alpha and beta split on the first pipe. After --parallel latches, gamma starts a task, the quoted "delta echo" forms its own task despite the space, and the trailing | is ignored.
Review gate for delegated results
SkillsPluginConfig.review_delegated_results (crates/ares-agent/src/skills/mod.rs) opts nested skill results into a review micro-step before integration:
- Off by default. A missing or failing reviewer passes the original result through.
- On acceptance, the original result integrates unchanged.
- On rejection, the parent receives a structured rejection with review notes; the original stays intact in metadata for re-dispatch.
Verdict protocol
The reviewer prompt uses a fixed preamble (DELEGATED_REVIEW_TEMPLATE, same file). The preamble asks the reviewer to judge consistency between result and requested input. It requires the verdict word on the first line, followed by one sentence of notes. The preamble is byte-stable across calls so provider-side prompt caches hit on it.
parse_review_verdict applies these rules:
- Take the first non-empty line of the reply.
- Skip leading non-alphabetic characters, then read the first alphabetic run as the verdict word.
- Accept exactly
ACCEPTorREJECT. Anything else is a parse error. - Notes are the rest of the first line plus any further lines.
An empty reply or an unparsable verdict returns an error. Callers treat that error like a reviewer outage: the original result integrates unchanged. The gate never guesses.
Rejection payload shape
On rejection the parent receives this JSON structure instead of the original result:
{
"status": "rejected",
"review": { "accepted": false, "notes": "<reviewer text>" },
"metadata": {
"delegated_skill_id": "<nested skill id>",
"original_result": { "...": "unchanged original" }
}
}
The gate snapshots the delegated input before running the sub-execution. With the gate off, no snapshot allocation happens.
Without the postgres feature there is no reviewer, so the gate is permanently off and every result passes through.
Self-check rounds
SkillEngine::with_self_check_rounds(n) (crates/ares-agent/src/skills/engine.rs) adds critique rounds over each nested skill result before integration. One LLM call runs per round. Off by default; zero behaves identically to off.
Round flow
Each round follows this sequence:
- Extract the answer text. Structured results contribute their
contentfield; bare strings contribute themselves. Results with neither carry nothing checkable, so the loop skips them. - Build the prompt from a byte-stable template (
SELF_CHECK_TEMPLATE, same file). The template asks two questions: did the answer address the requested task, and are there obvious errors or omissions. The tail names the delegated skill id, the requested input, and the current answer. - Send one LLM call.
- On success, compare the reply with the current answer. A verbatim reply means no corrections were found, so the loop exits early.
- On error, stop the loop. The last good answer stays in place. No retry happens inside the loop.
- After the rounds end, fold the answer back. Structured results keep their shape; only
contentmoves. Bare strings are replaced whole. An unchanged answer leaves the result untouched.
flowchart TD
A[Nested skill result] --> B{Self-check on?}
B -- off --> H[Integrate result]
B -- on --> C[Extract answer]
C -- none found --> H
C --> D[Round: LLM call over stable template]
D -- error --> G[Keep last good answer]
G --> F
D -- verbatim reply --> F[Fold answer back]
D -- corrected reply --> E{Rounds left?}
E -- yes --> D
E -- no --> F
F --> H
The template prefix stays identical across rounds, so provider-side prompt caches hit on every round after the first.
Delegated sub-workflows accept only allowlisted step kinds. Nested tool rounds hard-cap at three (check_tool_round_cap). Anti-recursion blocks delegation inside a delegated sub-workflow (validate_delegated_step). Slash-command chatter lines are stripped from delegated text before it enters the parent context (sanitize_result_value).
Ambient enrichment
AmbientEnrichmentConfig.enabled (crates/ares-agent/src/skills/engine.rs) is off by default. When enabled, each LLM step completion fires two parallel micro calls after the answer: intent classification and keyword tagging. Outcomes attach as session metadata on the skill-step record under the ambient_enrichment key. Enrichment input truncates at 4000 characters. Failures log at debug level and never delay or fail the completion.
Emergency stop
EmergencyStop (crates/ares-agent/src/emergency_stop.rs) is a Cordis service holding one atomic boolean. set_active(true) turns the kill switch on; set_active(false) clears it. Startup provides a fresh instance set to inactive when none exists yet (crates/ares-agent/src/plugins.rs).
Propagation path
The switch reaches execution through the shared context:
- Plugins provide
EmergencyStopon the root context. - Skill execution resolves it with
ctx.get::<EmergencyStop>(). ensure_execution_active(crates/ares-agent/src/skills/engine.rs) checks the flag before every nested skill call. The check runs at call boundaries, so a stop request lands at the next boundary without cancelling in-flight work mid-call.- An active flag aborts with the stable marker
subtask_cancelledprefixed to the error text, so callers classify aborts without parsing prose. - The aborted subtask integrates nothing into the parent context.
Per-subtask cancel tokens work alongside the global switch. Tokens are sticky cancel flags keyed "{run_id}/{skill_id}"; registration is idempotent and there is no un-cancel. Execution reads the registry fresh at every boundary, so an external trigger racing a starting subtask still stops it at its first call boundary. Token aborts reuse the same subtask_cancelled marker.
Worked example
Every key below appears in the structures cited above. The file passes ares-server config --validate on the v0.10 tree:
[server]
host = "127.0.0.1"
port = 3000
log_level = "info"
# Required tables; every field inside defaults, so empty tables work.
[auth]
jwt_secret_env = "JWT_SECRET"
api_key_env = "API_KEY"
[database]
[providers.local]
type = "ollama"
base_url = "http://localhost:11434"
default_model = "llama3.1:8b"
[models.default]
provider = "local"
model = "llama3.1:8b"
temperature = 0.7
max_tokens = 512
[agents.helper]
model = "default"
system_prompt = "You answer questions about internal documents."
tools = ["calculator"]
allowed_tools = ["calculator"]
max_tool_iterations = 10
parallel_tools = false
compaction_enabled = true
[workflows.support]
entry_agent = "helper"
fallback_agent = "helper"
max_depth = 3
max_iterations = 5
parallel_subagents = false
[skills]
project_dir = "./.claude/skills/"
Validation output, captured from a real run (exercised):
$ ares-server config --validate
✓ Configuration is valid!
Configuration Summary
Config file: ares.toml
Server: 127.0.0.1:3000
Log level: info
Providers
• local
Models
• default
Agents
• helper
Omitting either [auth] or [database] fails with missing field 'auth' or missing field 'database'. The tables themselves are required even though every field inside them carries serde defaults.