Skip to content

Commit f32d5c9

Browse files
committed
Support inherit key in agent config to inherit settings from another agent
1 parent b8b499e commit f32d5c9

5 files changed

Lines changed: 94 additions & 2 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ ECA Agent Guide (AGENTS.md)
3131
- Avoid adding too many comments, only add essential or when you think is really important to mention something.
3232
- ECA's protocol specification of client <-> server lives in docs/protocol.md
3333
- If changing ECA config structure, remember to update its docs/config.json
34-
- When adding support to a new feature or fixing a existing github issue, add a entry to Unreleased in CHANGELOG.md if not already there, be concise like the rest.
34+
- When adding support to a new feature or fixing a existing github issue, add a entry to Unreleased in CHANGELOG.md if not already there as last entry, be concise like the rest.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- Plugins can provide skills, MCP servers, agents, commands, hooks, rules, and arbitrary config overrides.
77
- Add commands `/plugins` and `/plugin-install`.
88
- Fix race condition where stopping a prompt and immediately sending a new one could cause two concurrent prompts with no way to stop the older one.
9+
- Support `inherit` key in agent config to inherit settings from another agent.
910

1011
## 0.111.0
1112

docs/config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,12 @@
567567
"description": "A named agent configuration.",
568568
"markdownDescription": "A named agent configuration.",
569569
"properties": {
570+
"inherit": {
571+
"type": "string",
572+
"description": "Name of another agent to inherit configuration from. The current agent's settings are deep-merged on top of the inherited agent's settings.",
573+
"markdownDescription": "Name of another agent to inherit configuration from. The current agent's settings are deep-merged on top of the inherited agent's settings.",
574+
"examples": ["code", "plan", "explorer"]
575+
},
570576
"mode": {
571577
"type": "string",
572578
"description": "Agent mode. 'primary' for standalone agents, 'subagent' for agent-spawnable agents.",

src/eca/config.clj

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,31 @@
316316
(last args)))
317317
maps))
318318

319+
(defn ^:private resolve-agent-inheritance
320+
"Resolves :inherit keys in agent configs. When an agent has :inherit \"other\",
321+
its config is deep-merged on top of the parent agent's config (child wins).
322+
The :inherit key is stripped from the resolved config."
323+
[agents]
324+
(reduce-kv
325+
(fn [result agent-name agent-config]
326+
(if-let [parent-name (:inherit agent-config)]
327+
(let [parent-config (get agents parent-name)]
328+
(cond
329+
(= parent-name agent-name)
330+
(do (logger/warn logger-tag (format "Agent '%s' inherits from itself, ignoring inherit" agent-name))
331+
(assoc result agent-name (dissoc agent-config :inherit)))
332+
333+
(nil? parent-config)
334+
(do (logger/warn logger-tag (format "Agent '%s' inherits from unknown agent '%s', ignoring inherit" agent-name parent-name))
335+
(assoc result agent-name (dissoc agent-config :inherit)))
336+
337+
:else
338+
(assoc result agent-name (deep-merge (dissoc parent-config :inherit)
339+
(dissoc agent-config :inherit)))))
340+
(assoc result agent-name agent-config)))
341+
{}
342+
agents))
343+
319344
(defn ^:private eca-version* []
320345
(string/trim (slurp (io/resource "ECA_VERSION"))))
321346

@@ -493,7 +518,8 @@
493518
(if (or (seq md-agent-configs) (seq plugin-agents))
494519
(update config :agent (fn [existing]
495520
(merge md-agent-configs plugin-agents existing)))
496-
config))))))
521+
config)))
522+
(update :agent resolve-agent-inheritance))))
497523

498524
(def all (memoize/ttl all* :ttl/threshold ttl-cache-config-ms))
499525

test/eca/config_test.clj

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,65 @@
166166
(with-redefs [logger/warn (fn [_ _] nil)]
167167
(is (= "code" (config/validate-agent-name "anything" config)))))))
168168

169+
(deftest resolve-agent-inheritance-test
170+
(testing "basic inheritance copies parent fields to child"
171+
(let [agents {"plan" {:mode "primary"
172+
:disabledTools ["edit_file" "write_file"]
173+
:toolCall {:approval {:byDefault "ask"}}}
174+
"my-plan" {:inherit "plan"
175+
:description "custom plan"}}]
176+
(is (match?
177+
{"plan" {:mode "primary"
178+
:disabledTools ["edit_file" "write_file"]}
179+
"my-plan" {:mode "primary"
180+
:disabledTools ["edit_file" "write_file"]
181+
:toolCall {:approval {:byDefault "ask"}}
182+
:description "custom plan"}}
183+
(#'config/resolve-agent-inheritance agents)))))
184+
185+
(testing "child values override parent values"
186+
(let [agents {"code" {:mode "primary"
187+
:disabledTools ["preview_file_change"]
188+
:defaultModel "anthropic/claude-sonnet-4-6"}
189+
"my-code" {:inherit "code"
190+
:defaultModel "openai/gpt-5"}}]
191+
(is (match?
192+
{"my-code" {:mode "primary"
193+
:disabledTools ["preview_file_change"]
194+
:defaultModel "openai/gpt-5"}}
195+
(#'config/resolve-agent-inheritance agents)))))
196+
197+
(testing "missing parent is skipped with warning"
198+
(let [agents {"child" {:inherit "nonexistent"
199+
:mode "primary"}}]
200+
(is (match?
201+
{"child" {:mode "primary"}}
202+
(#'config/resolve-agent-inheritance agents)))
203+
(is (not (contains? (get (#'config/resolve-agent-inheritance agents) "child") :inherit)))))
204+
205+
(testing "self-inheritance is skipped"
206+
(let [agents {"self" {:inherit "self"
207+
:mode "primary"}}]
208+
(is (match?
209+
{"self" {:mode "primary"}}
210+
(#'config/resolve-agent-inheritance agents)))
211+
(is (not (contains? (get (#'config/resolve-agent-inheritance agents) "self") :inherit)))))
212+
213+
(testing "agent without inherit is unchanged"
214+
(let [agents {"code" {:mode "primary"
215+
:disabledTools ["preview_file_change"]}}]
216+
(is (match?
217+
{"code" {:mode "primary"
218+
:disabledTools ["preview_file_change"]}}
219+
(#'config/resolve-agent-inheritance agents)))))
220+
221+
(testing "inherit key is stripped from resolved config"
222+
(let [agents {"plan" {:mode "primary"}
223+
"child" {:inherit "plan"
224+
:description "my child"}}
225+
resolved (#'config/resolve-agent-inheritance agents)]
226+
(is (not (contains? (get resolved "child") :inherit))))))
227+
169228
(deftest diff-keeping-vectors-test
170229
(testing "like clojure.data/diff"
171230
(is (= {:b 3}

0 commit comments

Comments
 (0)