From 91df372bb56aa596d481fa04a3ff8f966d2ecb8d Mon Sep 17 00:00:00 2001 From: Juha Itkonen Date: Fri, 17 Jul 2026 09:56:36 +0300 Subject: [PATCH] Support variants in Markdown agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow Markdown agent definitions to select the same default model variants as JSON-configured agents, with validation coverage and aligned documentation. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent --- CHANGELOG.md | 2 ++ docs/config/agents.md | 3 +++ docs/config/variants.md | 35 ++++++++++++++++++++++--------- src/eca/features/agents.clj | 11 ++++++++-- test/eca/config_test.clj | 10 +++++++++ test/eca/features/agents_test.clj | 17 +++++++++++++++ 6 files changed, 66 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b93db1744..e13c89498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/config/agents.md b/docs/config/agents.md index 84fcfffd4..96c33720f 100644 --- a/docs/config/agents.md +++ b/docs/config/agents.md @@ -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. @@ -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: @@ -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 } diff --git a/docs/config/variants.md b/docs/config/variants.md index af8b358f9..e48af48fa 100644 --- a/docs/config/variants.md +++ b/docs/config/variants.md @@ -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. diff --git a/src/eca/features/agents.clj b/src/eca/features/agents.clj index 1a964feb0..9af38a2d7 100644 --- a/src/eca/features/agents.clj +++ b/src/eca/features/agents.clj @@ -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 @@ -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-> {} @@ -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 diff --git a/test/eca/config_test.clj b/test/eca/config_test.clj index bf47f0589..23363ff3e 100644 --- a/test/eca/config_test.clj +++ b/test/eca/config_test.clj @@ -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"] diff --git a/test/eca/features/agents_test.clj b/test/eca/features/agents_test.clj index 9ceaf1f22..4508f4aa8 100644 --- a/test/eca/features/agents_test.clj +++ b/test/eca/features/agents_test.clj @@ -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" @@ -77,6 +78,7 @@ (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" @@ -84,6 +86,14 @@ :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 .*)" @@ -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.")) @@ -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)) @@ -318,6 +330,7 @@ (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 @@ -325,6 +338,7 @@ (str "---\n" "description: MD version\n" "mode: subagent\n" + "variant: low\n" "---\n\n" "MD prompt.")) @@ -332,16 +346,19 @@ {: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)))))