|
2 | 2 | "Tool for spawning subagents to perform focused tasks in isolated context." |
3 | 3 | (:require |
4 | 4 | [clojure.string :as str] |
| 5 | + [eca.config :as config] |
5 | 6 | [eca.features.tools.util :as tools.util] |
6 | 7 | [eca.logger :as logger] |
7 | 8 | [eca.messenger :as messenger])) |
|
81 | 82 | (catch Exception e |
82 | 83 | (logger/warn logger-tag (format "Error stopping subagent '%s': %s" agent-name (.getMessage e))))))) |
83 | 84 |
|
| 85 | +(defn ^:private available-model-names |
| 86 | + "Returns a sorted list of available model names from the runtime db." |
| 87 | + [db] |
| 88 | + (some->> (:models db) |
| 89 | + keys |
| 90 | + sort |
| 91 | + vec)) |
| 92 | + |
| 93 | +(defn ^:private available-variant-names |
| 94 | + "Returns a sorted union of all variant names across all available models." |
| 95 | + [config db] |
| 96 | + (let [model-keys (keys (:models db))] |
| 97 | + (when (seq model-keys) |
| 98 | + (let [all-variants (->> model-keys |
| 99 | + (mapcat (fn [^String full-model] |
| 100 | + (let [idx (.indexOf full-model "/")] |
| 101 | + (when (pos? idx) |
| 102 | + (let [provider (subs full-model 0 idx) |
| 103 | + model (subs full-model (inc idx)) |
| 104 | + user-variants (get-in config [:providers provider :models model :variants])] |
| 105 | + (keys (config/effective-model-variants config provider model user-variants))))))) |
| 106 | + (into (sorted-set)))] |
| 107 | + (when (seq all-variants) |
| 108 | + (vec all-variants)))))) |
| 109 | + |
| 110 | +(defn ^:private model-variant-names |
| 111 | + "Returns sorted variant names for a specific full model string (e.g. \"anthropic/claude-sonnet-4-6\")." |
| 112 | + [config ^String full-model] |
| 113 | + (when full-model |
| 114 | + (let [idx (.indexOf full-model "/")] |
| 115 | + (when (pos? idx) |
| 116 | + (let [provider (subs full-model 0 idx) |
| 117 | + model (subs full-model (inc idx)) |
| 118 | + user-variants (get-in config [:providers provider :models model :variants]) |
| 119 | + variants (config/effective-model-variants config provider model user-variants)] |
| 120 | + (when (seq variants) |
| 121 | + (vec (sort (keys variants))))))))) |
| 122 | + |
84 | 123 | (defn ^:private spawn-agent |
85 | 124 | "Handler for the spawn_agent tool. |
86 | 125 | Spawns a subagent to perform a focused task and returns the result." |
|
110 | 149 | ;; Create subagent chat session using deterministic id based on tool-call-id |
111 | 150 | subagent-chat-id (->subagent-chat-id tool-call-id) |
112 | 151 |
|
| 152 | + user-model (get arguments "model") |
| 153 | + _ (when user-model |
| 154 | + (let [available-models (:models db)] |
| 155 | + (when (and (seq available-models) |
| 156 | + (not (contains? available-models user-model))) |
| 157 | + (throw (ex-info (format "Model '%s' is not available. Available models: %s" |
| 158 | + user-model |
| 159 | + (str/join ", " (available-model-names db))) |
| 160 | + {:model user-model |
| 161 | + :available (available-model-names db)}))))) |
| 162 | + |
113 | 163 | parent-model (get-in db [:chats chat-id :model]) |
114 | | - subagent-model (or (:model subagent) parent-model)] |
| 164 | + subagent-model (or user-model (:model subagent) parent-model) |
| 165 | + |
| 166 | + ;; Variant validation: reject only when the resolved model has configured |
| 167 | + ;; variants and the user-specified one isn't among them. Models with no |
| 168 | + ;; configured variants accept any variant (the LLM API will reject if invalid). |
| 169 | + user-variant (get arguments "variant") |
| 170 | + _ (when user-variant |
| 171 | + (let [valid-variants (model-variant-names config subagent-model)] |
| 172 | + (when (and (seq valid-variants) |
| 173 | + (not (some #{user-variant} valid-variants))) |
| 174 | + (throw (ex-info (format "Variant '%s' is not available for model '%s'. Available variants: %s" |
| 175 | + user-variant subagent-model (str/join ", " valid-variants)) |
| 176 | + {:variant user-variant |
| 177 | + :model subagent-model |
| 178 | + :available valid-variants})))))] |
115 | 179 |
|
116 | | - (logger/info logger-tag (format "Spawning agent '%s' for task: %s" agent-name task)) |
| 180 | + (logger/info logger-tag (format "Spawning agent '%s' for task: %s (model: %s, variant: %s)" agent-name task subagent-model (or user-variant "default"))) |
117 | 181 |
|
118 | 182 | (let [max-steps-limit (max-steps subagent)] |
119 | 183 | (swap! db* assoc-in [:chats subagent-chat-id] |
|
132 | 196 | task max-steps-limit) |
133 | 197 | task)] |
134 | 198 | (chat-prompt |
135 | | - {:message task-prompt |
136 | | - :chat-id subagent-chat-id |
137 | | - :model subagent-model |
138 | | - :agent agent-name |
139 | | - :contexts []} |
| 199 | + (cond-> {:message task-prompt |
| 200 | + :chat-id subagent-chat-id |
| 201 | + :model subagent-model |
| 202 | + :agent agent-name |
| 203 | + :contexts []} |
| 204 | + user-variant (assoc :variant user-variant)) |
140 | 205 | db* |
141 | 206 | messenger |
142 | 207 | config |
|
204 | 269 | (str base-description agents-section))) |
205 | 270 |
|
206 | 271 | (defn definitions |
207 | | - [config] |
208 | | - {"spawn_agent" |
209 | | - {:description (build-description config) |
210 | | - :parameters {:type "object" |
211 | | - :properties {"agent" {:type "string" |
212 | | - :description "Name of the agent to spawn"} |
213 | | - "task" {:type "string" |
214 | | - :description "Clear description of what the agent should accomplish"} |
215 | | - "activity" {:type "string" |
216 | | - :description "Concise label (max 3-4 words) shown in the UI while the agent runs, e.g. \"exploring codebase\", \"reviewing changes\", \"analyzing tests\"."}} |
217 | | - :required ["agent" "task" "activity"]} |
218 | | - :handler #'spawn-agent |
219 | | - :summary-fn (fn [{:keys [args]}] |
220 | | - (if-let [agent-name (get args "agent")] |
221 | | - (let [activity (get args "activity" "working")] |
222 | | - (format "%s: %s" agent-name activity)) |
223 | | - "Spawning agent"))}}) |
| 272 | + [config db] |
| 273 | + (let [model-names (available-model-names db) |
| 274 | + variant-names (available-variant-names config db)] |
| 275 | + {"spawn_agent" |
| 276 | + {:description (build-description config) |
| 277 | + :parameters {:type "object" |
| 278 | + :properties {"agent" {:type "string" |
| 279 | + :description "Name of the agent to spawn"} |
| 280 | + "task" {:type "string" |
| 281 | + :description "Clear description of what the agent should accomplish"} |
| 282 | + "activity" {:type "string" |
| 283 | + :description "Concise label (max 3-4 words) shown in the UI while the agent runs, e.g. \"exploring codebase\", \"reviewing changes\", \"analyzing tests\"."} |
| 284 | + "model" (cond-> {:type "string" |
| 285 | + :description (str "Model to use for the subagent." |
| 286 | + "Pick the closest match from the enum when the user asks for a model by informal name (e.g. \"sonnet\" → latest sonnet available)." |
| 287 | + "Optional — defaults to the agent's configured model or the current conversation model.")} |
| 288 | + (seq model-names) (assoc :enum model-names)) |
| 289 | + "variant" (cond-> {:type "string" |
| 290 | + :description (str "Variant (Usually reasoning related) for the subagent." |
| 291 | + "Available variants are model-dependent; pick the closest match when the user asks informally (e.g. \"high reasoning\" → \"high\")." |
| 292 | + "Optional — defaults to the agent's or model's configured variant.")} |
| 293 | + (seq variant-names) (assoc :enum variant-names))} |
| 294 | + :required ["agent" "task" "activity"]} |
| 295 | + :handler #'spawn-agent |
| 296 | + :summary-fn (fn [{:keys [args]}] |
| 297 | + (if-let [agent-name (get args "agent")] |
| 298 | + (let [activity (get args "activity" "working")] |
| 299 | + (format "%s: %s" agent-name activity)) |
| 300 | + "Spawning agent"))}})) |
224 | 301 |
|
225 | 302 | (defmethod tools.util/tool-call-details-before-invocation :spawn_agent |
226 | 303 | [_name arguments _server {:keys [db config chat-id tool-call-id]}] |
227 | 304 | (let [agent-name (get arguments "agent") |
| 305 | + user-model (get arguments "model") |
| 306 | + user-variant (get arguments "variant") |
228 | 307 | subagent (when agent-name |
229 | 308 | (get-agent agent-name config)) |
230 | 309 | parent-model (get-in db [:chats chat-id :model]) |
231 | | - subagent-model (or (:model subagent) parent-model) |
| 310 | + subagent-model (or user-model (:model subagent) parent-model) |
232 | 311 | subagent-chat-id (when tool-call-id |
233 | 312 | (->subagent-chat-id tool-call-id))] |
234 | | - {:type :subagent |
235 | | - :subagent-chat-id subagent-chat-id |
236 | | - :model subagent-model |
237 | | - :agent-name agent-name |
238 | | - :step (get-in db [:chats subagent-chat-id :current-step] 1) |
239 | | - :max-steps (max-steps subagent)})) |
| 313 | + (cond-> {:type :subagent |
| 314 | + :subagent-chat-id subagent-chat-id |
| 315 | + :model subagent-model |
| 316 | + :agent-name agent-name |
| 317 | + :step (get-in db [:chats subagent-chat-id :current-step] 1) |
| 318 | + :max-steps (max-steps subagent)} |
| 319 | + user-variant (assoc :variant user-variant)))) |
240 | 320 |
|
241 | 321 | (defmethod tools.util/tool-call-details-after-invocation :spawn_agent |
242 | 322 | [_name _arguments before-details _result {:keys [db chat-id tool-call-id]}] |
|
0 commit comments