diff --git a/CHANGELOG.md b/CHANGELOG.md index e5b978224..f6a8025c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 ddb396fcb..0a1fb066b 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 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)] @@ -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 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 1851197ad..0f3ba391e 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" "maxSteps: 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 "maxSteps takes precedence over the legacy steps alias" (is (= 7 (:maxSteps (#'agents/md->agent-config {:maxSteps 7 @@ -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.")) @@ -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)) @@ -323,6 +335,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 @@ -330,6 +343,7 @@ (str "---\n" "description: MD version\n" "mode: subagent\n" + "variant: low\n" "---\n\n" "MD prompt.")) @@ -337,16 +351,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)))))