Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Support `variant` in Markdown agent frontmatter.

## 0.147.3

- Fix remote server silently falling back to HTTP when the TLS private key is in SEC1 EC format; validate keys before caching and log HTTP fallback. #521
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 steps tools body inherit spawnableBy disabledTools]}]
(let [tools-map (normalize-tools tools)
[{:keys [description mode model variant steps tools body inherit spawnableBy disabledTools]}]
(let [agent-variant (normalize-agent-variant variant)
tools-map (normalize-tools tools)
spawnable-by (normalize-spawnable-by spawnableBy)
disabled-tools (normalize-disabled-tools disabledTools)]
(cond-> {}
Expand All @@ -118,6 +124,7 @@
(mapv str mode)
(str mode)))
model (assoc :defaultModel (str model))
agent-variant (assoc :variant agent-variant)
steps (assoc :maxSteps (long 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"
"steps: 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 "tool entries with regex patterns"
(let [parsed {:tools {"byDefault" "ask"
"allow" ["eca__shell_command(npm run .*)"
Expand Down Expand Up @@ -259,6 +269,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 @@ -276,6 +287,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 @@ -318,30 +330,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