Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- Truncate oversized failed tool output, preserving full logs on disk instead of overflowing the model context.
- Honor model output limits in OpenAI-compatible Chat Completions and Ollama requests while preserving `extraPayload` overrides.
- Respect `maxSteps` in Markdown agent definitions.
- Support `variant` in Markdown agent frontmatter.

## 0.147.3

Expand Down
3 changes: 3 additions & 0 deletions docs/config/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Subagents can be configured in config or markdown and support/require these fiel
- `inherit` (optional): name of another agent to inherit all settings from. The subagent's own fields are merged on top.
- `spawnableBy` (optional): one primary agent ID or a collection of primary agent IDs allowed to discover and spawn this subagent. When omitted or empty, the subagent remains available to every primary agent.
- `model` (optional): which full model to use for this subagent, using primary agent model if not specified.
- `variant` (optional): default model variant; ignored when unavailable for the selected model. See [Variants](variants.md#agent-default-variant).
- `tools` (optional): same as ECA tool approval logic to control what tools are allowed/askable/denied.
- `disabledTools` (optional): tools to hide from this agent entirely. Same matching as the global [`disabledTools`](tools.md#disabled-tools): a builtin tool name or regex (no `eca__` prefix needed), an exact MCP server name (all its tools), or a regex against the tool full name `server__tool`.
- `maxSteps` (optional): set a max limit of turns/steps that his subagent must finish and return an answer.
Expand Down Expand Up @@ -186,6 +187,7 @@ The `/config` command intentionally remains an administrative, raw resolved-conf
mode: subagent
description: You sleep one second when asked
model: ${env:MY_MODEL:anthropic/sonnet-4.5}
variant: high
tools:
byDefault: ask
deny:
Expand Down Expand Up @@ -241,6 +243,7 @@ The `/config` command intentionally remains an administrative, raw resolved-conf
"description": "You sleep one second when asked",
"systemPrompt": "You should run sleep 1 and return \"I slept 1 second\"",
"defaultModel": "anthropic/sonnet-4.5",
"variant": "high",
"toolCall": {...},
"maxSteps": 25 // Optional: to limit turns in subagent
}
Expand Down
35 changes: 25 additions & 10 deletions docs/config/variants.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,32 @@ To disable a specific built-in variant, set it to `{}`:

## Agent Default Variant

You can set a default variant for an agent so it starts with that variant pre-selected:
Set a default variant for an agent:

```javascript title="~/.config/eca/config.json"
{
"agent": {
"code": {
"variant": "medium"
=== "JSON"

```javascript title="~/.config/eca/config.json"
{
"agent": {
"code": {
"variant": "medium"
}
}
}
}
}
```
```

=== "Markdown"

```markdown title="~/.config/eca/agents/reviewer.md"
---
mode: subagent
description: Review code changes
model: openai/gpt-5.4
variant: high
---

Review the changes for correctness and regressions.
```

If the configured variant doesn't exist for the current model, it will be ignored.
Unavailable variants are ignored. An explicit chat or `spawn_agent` variant overrides the agent default. See [Agents](agents.md) for the complete agent specification.

11 changes: 9 additions & 2 deletions src/eca/features/agents.clj
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
(when-not (string/blank? normalized)
normalized))))

(defn ^:private normalize-agent-variant
[variant]
(when (string? variant)
(not-empty (string/trim variant))))

(defn ^:private normalize-spawnable-by
[spawnable-by]
(cond
Expand Down Expand Up @@ -105,8 +110,9 @@
nil)))

(defn ^:private md->agent-config
[{:keys [description mode model maxSteps steps tools body inherit spawnableBy disabledTools]}]
(let [max-steps (or maxSteps steps)
[{:keys [description mode model variant maxSteps steps tools body inherit spawnableBy disabledTools]}]
(let [agent-variant (normalize-agent-variant variant)
max-steps (or maxSteps steps)
tools-map (normalize-tools tools)
spawnable-by (normalize-spawnable-by spawnableBy)
disabled-tools (normalize-disabled-tools disabledTools)]
Expand All @@ -119,6 +125,7 @@
(mapv str mode)
(str mode)))
model (assoc :defaultModel (str model))
agent-variant (assoc :variant agent-variant)
max-steps (assoc :maxSteps (long max-steps))
(seq body) (assoc :systemPrompt body)
tools-map (assoc :toolCall
Expand Down
10 changes: 10 additions & 0 deletions test/eca/config_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,16 @@
(is (= ["duel"] (get-in resolved ["inherited-worker" :spawnableBy])))
(is (= ["other"] (get-in resolved ["overridden-worker" :spawnableBy])))))

(testing "variant follows normal inheritance and child override behavior"
(let [agents {"worker" {:mode "subagent"
:variant "medium"}
"inherited-worker" {:inherit "worker"}
"overridden-worker" {:inherit "worker"
:variant "high"}}
resolved (#'config/resolve-agent-inheritance agents)]
(is (= "medium" (get-in resolved ["inherited-worker" :variant])))
(is (= "high" (get-in resolved ["overridden-worker" :variant])))))

(testing "child values override parent values"
(let [agents {"code" {:mode "primary"
:disabledTools ["preview_file_change"]
Expand Down
17 changes: 17 additions & 0 deletions test/eca/features/agents_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"description: You sleep one second when asked\n"
"mode: subagent\n"
"model: my-org-anthropic/sonnet-4.5\n"
"variant: High\n"
"maxSteps: 5\n"
"tools:\n"
" byDefault: ask\n"
Expand All @@ -77,13 +78,22 @@
(is (match? {:description "You sleep one second when asked"
:mode "subagent"
:defaultModel "my-org-anthropic/sonnet-4.5"
:variant "High"
:maxSteps 5
:systemPrompt "You should run sleep 1 and return \"I sleeped 1 second\""
:toolCall {:approval {:byDefault "ask"
:allow {"eca__shell_command" {}}
:deny {"foo" {}}}}}
config))))

(testing "variant is trimmed without changing case"
(is (= "XHigh"
(:variant (#'agents/md->agent-config {:variant " XHigh "})))))

(testing "blank or malformed variant is ignored"
(doseq [variant [nil "" " " 42 [] {}]]
(is (nil? (:variant (#'agents/md->agent-config {:variant variant}))))))

(testing "maxSteps takes precedence over the legacy steps alias"
(is (= 7
(:maxSteps (#'agents/md->agent-config {:maxSteps 7
Expand Down Expand Up @@ -264,6 +274,7 @@
"description: Reviews code changes\n"
"mode: subagent\n"
"model: anthropic/sonnet-4.5\n"
"variant: high\n"
"steps: 10\n"
"---\n\n"
"You are a code reviewer."))
Expand All @@ -281,6 +292,7 @@
(is (match? ["reviewer" {:description "Reviews code changes"
:mode "subagent"
:defaultModel "anthropic/sonnet-4.5"
:variant "high"
:maxSteps 10
:systemPrompt "You are a code reviewer."}]
reviewer))
Expand Down Expand Up @@ -323,30 +335,35 @@
(str "---\n"
"description: From markdown\n"
"mode: subagent\n"
"variant: medium\n"
"---\n\n"
"I am from markdown."))
;; Agent defined in JSON config should take precedence over MD agent
(spit (fs/file local-agents-dir "json-override.md")
(str "---\n"
"description: MD version\n"
"mode: subagent\n"
"variant: low\n"
"---\n\n"
"MD prompt."))

(reset! config/initialization-config*
{:pureConfig false
:agent {"json-override" {:mode "subagent"
:description "JSON version"
:variant "high"
:systemPrompt "JSON prompt."}}})
(let [db {:workspace-folders [{:uri (shared/filename->uri (str tmp-dir))}]}
result (#'config/all* db)]
(testing "markdown agent is present in config"
(is (match? {:description "From markdown"
:mode "subagent"
:variant "medium"
:systemPrompt "I am from markdown."}
(get-in result [:agent "md-agent"]))))
(testing "JSON config agent takes precedence over same-named MD agent"
(is (= "JSON version" (get-in result [:agent "json-override" :description])))
(is (= "high" (get-in result [:agent "json-override" :variant])))
(is (= "JSON prompt." (get-in result [:agent "json-override" :systemPrompt])))))
(finally
(fs/delete-tree tmp-dir)))))
Expand Down
Loading