@dreb/coding-agent 2.45.4 → 2.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +10 -4
  2. package/dist/core/agent-session.d.ts +10 -2
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +94 -23
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/context-buffer.d.ts +2 -0
  7. package/dist/core/context-buffer.d.ts.map +1 -1
  8. package/dist/core/context-buffer.js +8 -0
  9. package/dist/core/context-buffer.js.map +1 -1
  10. package/dist/core/dispatch-arbiter.d.ts +94 -0
  11. package/dist/core/dispatch-arbiter.d.ts.map +1 -0
  12. package/dist/core/dispatch-arbiter.js +316 -0
  13. package/dist/core/dispatch-arbiter.js.map +1 -0
  14. package/dist/core/extensions/index.d.ts +1 -1
  15. package/dist/core/extensions/index.d.ts.map +1 -1
  16. package/dist/core/extensions/index.js.map +1 -1
  17. package/dist/core/extensions/runner.d.ts +7 -0
  18. package/dist/core/extensions/runner.d.ts.map +1 -1
  19. package/dist/core/extensions/runner.js +10 -0
  20. package/dist/core/extensions/runner.js.map +1 -1
  21. package/dist/core/extensions/types.d.ts +35 -0
  22. package/dist/core/extensions/types.d.ts.map +1 -1
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/core/git-repo-state.d.ts +2 -0
  25. package/dist/core/git-repo-state.d.ts.map +1 -1
  26. package/dist/core/git-repo-state.js +19 -0
  27. package/dist/core/git-repo-state.js.map +1 -1
  28. package/dist/core/model-routing-guide.d.ts +15 -0
  29. package/dist/core/model-routing-guide.d.ts.map +1 -0
  30. package/dist/core/model-routing-guide.js +186 -0
  31. package/dist/core/model-routing-guide.js.map +1 -0
  32. package/dist/core/sdk.d.ts.map +1 -1
  33. package/dist/core/sdk.js +1 -0
  34. package/dist/core/sdk.js.map +1 -1
  35. package/dist/core/session-manager.d.ts.map +1 -1
  36. package/dist/core/session-manager.js +38 -2
  37. package/dist/core/session-manager.js.map +1 -1
  38. package/dist/core/settings-manager.d.ts +18 -0
  39. package/dist/core/settings-manager.d.ts.map +1 -1
  40. package/dist/core/settings-manager.js +33 -0
  41. package/dist/core/settings-manager.js.map +1 -1
  42. package/dist/core/thinking.d.ts +14 -0
  43. package/dist/core/thinking.d.ts.map +1 -1
  44. package/dist/core/thinking.js +35 -2
  45. package/dist/core/thinking.js.map +1 -1
  46. package/dist/core/tools/ask-user.d.ts +45 -0
  47. package/dist/core/tools/ask-user.d.ts.map +1 -0
  48. package/dist/core/tools/ask-user.js +210 -0
  49. package/dist/core/tools/ask-user.js.map +1 -0
  50. package/dist/core/tools/index.d.ts +18 -1
  51. package/dist/core/tools/index.d.ts.map +1 -1
  52. package/dist/core/tools/index.js +8 -0
  53. package/dist/core/tools/index.js.map +1 -1
  54. package/dist/core/tools/subagent.d.ts +53 -6
  55. package/dist/core/tools/subagent.d.ts.map +1 -1
  56. package/dist/core/tools/subagent.js +343 -53
  57. package/dist/core/tools/subagent.js.map +1 -1
  58. package/dist/modes/interactive/components/ask-user.d.ts +59 -0
  59. package/dist/modes/interactive/components/ask-user.d.ts.map +1 -0
  60. package/dist/modes/interactive/components/ask-user.js +240 -0
  61. package/dist/modes/interactive/components/ask-user.js.map +1 -0
  62. package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
  63. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  64. package/dist/modes/interactive/components/settings-selector.js +99 -1
  65. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  66. package/dist/modes/interactive/interactive-mode.d.ts +13 -0
  67. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode.js +167 -6
  69. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  70. package/dist/modes/rpc/rpc-mode.d.ts +18 -3
  71. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  72. package/dist/modes/rpc/rpc-mode.js +177 -59
  73. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  74. package/dist/modes/rpc/rpc-types.d.ts +32 -0
  75. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  76. package/dist/modes/rpc/rpc-types.js.map +1 -1
  77. package/docs/agent-models.md +33 -1
  78. package/docs/dashboard.md +9 -3
  79. package/docs/extensions.md +33 -3
  80. package/docs/json.md +31 -5
  81. package/docs/rpc.md +105 -15
  82. package/docs/session.md +8 -2
  83. package/docs/settings.md +33 -1
  84. package/docs/skills.md +38 -0
  85. package/package.json +1 -1
  86. package/skills/model-routing-guide/SKILL.md +182 -0
@@ -11,11 +11,12 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
11
11
  import { attachJsonlLineReader } from "../../modes/rpc/jsonl.js";
12
12
  import { log } from "../logger.js";
13
13
  import { resolveCliModel } from "../model-resolver.js";
14
- import { resolveEffectiveThinkingLevel, thinkingLevelToReasoning } from "../thinking.js";
14
+ import { resolveEffectiveThinkingLevel, thinkingLevelToReasoning, validateThinkingLevelForModel } from "../thinking.js";
15
15
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
16
16
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
17
17
  import { DEFAULT_MAX_BYTES, formatSize } from "./truncate.js";
18
18
  const DEFAULT_AGENT = "Explore";
19
+ const SUBAGENT_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
19
20
  export const DEFAULT_MODEL_AVAILABILITY_PROBE_TIMEOUT_MS = 120_000;
20
21
  export function parseAgentFrontmatter(content) {
21
22
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
@@ -123,6 +124,7 @@ const SUBAGENT_EXCLUDED_TOOLS = ["wait", "subagent", "suggest_next"];
123
124
  // the child process adds them unconditionally regardless of --tools.
124
125
  // Internal tools (tmp_read) are also excluded.
125
126
  const SUBAGENT_DEFAULT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "web_search", "web_fetch"];
127
+ const SUBAGENT_ALWAYS_ACTIVE_TOOLS = ["search", "skill", "tasks_update"];
126
128
  /**
127
129
  * Filter a comma-separated tools string, removing any tools in SUBAGENT_EXCLUDED_TOOLS.
128
130
  * Returns the filtered tools as a comma-separated string (always non-empty — falls
@@ -138,11 +140,38 @@ export function filterSubagentTools(tools) {
138
140
  .join(",");
139
141
  return filtered || SUBAGENT_DEFAULT_TOOLS.join(",");
140
142
  }
143
+ function summarizeAgentsForArbitration(agents, getAgentModelsForAgent) {
144
+ return [...agents.values()].map((agent) => {
145
+ const settingsModels = getAgentModelsForAgent?.(agent.name);
146
+ return {
147
+ name: agent.name,
148
+ description: agent.description,
149
+ tools: [
150
+ ...new Set([
151
+ ...filterSubagentTools(agent.tools).split(",").filter(Boolean),
152
+ ...SUBAGENT_ALWAYS_ACTIVE_TOOLS,
153
+ ]),
154
+ ],
155
+ modelDefaults: settingsModels && settingsModels.length > 0
156
+ ? [...settingsModels]
157
+ : agent.model
158
+ ? Array.isArray(agent.model)
159
+ ? [...agent.model]
160
+ : [agent.model]
161
+ : [],
162
+ };
163
+ });
164
+ }
141
165
  // TODO: Support PATH-based binary discovery.
142
166
  // Currently returns the captured argv[1].
143
167
  function findDrebBinary() {
144
168
  return DREB_SCRIPT;
145
169
  }
170
+ function canonicalModelRef(provider, modelId) {
171
+ if (!provider || modelId.startsWith(`${provider}/`))
172
+ return modelId;
173
+ return `${provider}/${modelId}`;
174
+ }
146
175
  /**
147
176
  * Handle one line of a subagent child's JSONL stdout. Parses the line and
148
177
  * dispatches to the sinks: full-event relay, assistant-message collection,
@@ -175,8 +204,15 @@ export function handleChildJsonlLine(line, sinks) {
175
204
  if (typeof event.type === "string") {
176
205
  sinks.onEvent?.(event);
177
206
  }
178
- if (event.type === "agent_start" && event.model) {
179
- sinks.onModel(event.model.id);
207
+ if (event.type === "agent_start") {
208
+ if (typeof event.model?.id === "string") {
209
+ const provider = typeof event.model.provider === "string" ? event.model.provider : undefined;
210
+ sinks.onModel(canonicalModelRef(provider, event.model.id));
211
+ }
212
+ if (typeof event.thinkingLevel === "string" &&
213
+ SUBAGENT_THINKING_LEVELS.includes(event.thinkingLevel)) {
214
+ sinks.onThinking?.(event.thinkingLevel);
215
+ }
180
216
  }
181
217
  if (event.type === "message_end" && event.message?.role === "assistant") {
182
218
  sinks.onAssistantMessage(event.message);
@@ -189,7 +225,7 @@ export function handleChildJsonlLine(line, sinks) {
189
225
  sinks.onProgress(`${sinks.toolNameRef.current} done`);
190
226
  }
191
227
  }
192
- async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentProvider, sessionDir, parentSessionFile, onChildEvent) {
228
+ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentProvider, sessionDir, parentSessionFile, onChildEvent, thinkingOverride) {
193
229
  const drebBin = findDrebBinary();
194
230
  log.debug(`[subagent] spawn: agent=${agentConfig.name} cwd=${cwd}`);
195
231
  // Validate cwd exists — spawn() throws a misleading ENOENT blaming the
@@ -216,13 +252,16 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
216
252
  const modelStr = Array.isArray(agentConfig.model) ? agentConfig.model[0] : agentConfig.model;
217
253
  if (modelStr) {
218
254
  args.push("--model", modelStr);
219
- // When the model string doesn't already specify a provider (no "/"),
220
- // inherit the parent's provider to prevent fuzzy matching from picking
221
- // an unauthenticated provider (e.g. Bedrock instead of Anthropic).
222
- if (parentProvider && !modelStr.includes("/")) {
255
+ // executeSingle resolves the model and provider independently. Always pass
256
+ // that exact provider because raw model IDs may themselves contain slashes
257
+ // (for example, OpenRouter IDs such as "openai/gpt-oss-120b").
258
+ if (parentProvider) {
223
259
  args.push("--provider", parentProvider);
224
260
  }
225
261
  }
262
+ if (thinkingOverride) {
263
+ args.push("--thinking", thinkingOverride);
264
+ }
226
265
  // Always pass --tools to ensure wait/subagent/suggest_next are excluded from child processes.
227
266
  // filterSubagentTools always returns a non-empty string.
228
267
  args.push("--tools", filterSubagentTools(agentConfig.tools));
@@ -272,6 +311,7 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
272
311
  const plainStdoutLines = [];
273
312
  const toolNameRef = { current: "" };
274
313
  let resolvedModel;
314
+ let resolvedThinking;
275
315
  // Drain stderr concurrently to avoid pipe deadlock (capped to prevent OOM from verbose subagents)
276
316
  proc.stderr?.on("data", (chunk) => {
277
317
  if (stderrSize < MAX_STDERR_BYTES) {
@@ -293,8 +333,11 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
293
333
  onEvent: onChildEvent,
294
334
  onAssistantMessage: (message) => collectedMessages.push(message),
295
335
  onProgress,
296
- onModel: (modelId) => {
297
- resolvedModel = modelId;
336
+ onModel: (modelRef) => {
337
+ resolvedModel = modelRef;
338
+ },
339
+ onThinking: (thinkingLevel) => {
340
+ resolvedThinking = thinkingLevel;
298
341
  },
299
342
  onPlainLine: (plain) => plainStdoutLines.push(plain),
300
343
  toolNameRef,
@@ -390,15 +433,13 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
390
433
  }
391
434
  // Discover the session file written by the child process
392
435
  const sessionFile = sessionDir ? discoverSessionFile(sessionDir, agentConfig.name) : undefined;
436
+ const configuredModel = Array.isArray(agentConfig.model) ? agentConfig.model[0] : agentConfig.model;
393
437
  resolvePromise({
394
438
  agent: agentConfig.name,
395
439
  task,
396
440
  model: resolvedModel ??
397
- (exitCode === 0
398
- ? Array.isArray(agentConfig.model)
399
- ? agentConfig.model[0]
400
- : agentConfig.model
401
- : undefined),
441
+ (exitCode === 0 && configuredModel ? canonicalModelRef(parentProvider, configuredModel) : undefined),
442
+ thinking: resolvedThinking ?? (exitCode === 0 ? thinkingOverride : undefined),
402
443
  exitCode,
403
444
  output,
404
445
  stderr: stderr.slice(0, 2000), // cap stderr
@@ -747,14 +788,17 @@ logPrefix = "[subagent]") {
747
788
  ].join(", ")}. Last error: ${lastError || "all probes failed"}`,
748
789
  };
749
790
  }
750
- export function formatModelFallbackSummary(skippedModels, selectedModel) {
791
+ export function formatModelFallbackSummary(skippedModels, proposalModel, finalModel) {
751
792
  if (skippedModels.length === 0)
752
793
  return undefined;
753
794
  const skipped = skippedModels.map((s) => `- ${s.model}: ${s.reason}`).join("\n");
754
- return `[MODEL FALLBACK: skipped ${skippedModels.length} unavailable model(s); using "${selectedModel ?? "unknown"}".]\n${skipped}`;
795
+ const routeSummary = finalModel && finalModel !== proposalModel
796
+ ? `proposal resolved to "${proposalModel ?? "unknown"}" before arbitration selected "${finalModel}".`
797
+ : `using "${finalModel ?? proposalModel ?? "unknown"}".`;
798
+ return `[MODEL FALLBACK: skipped ${skippedModels.length} unavailable model(s); ${routeSummary}]\n${skipped}`;
755
799
  }
756
- export function prependModelFallbackSummary(output, skippedModels, selectedModel) {
757
- const fallbackSummary = formatModelFallbackSummary(skippedModels, selectedModel);
800
+ export function prependModelFallbackSummary(output, skippedModels, proposalModel, finalModel) {
801
+ const fallbackSummary = formatModelFallbackSummary(skippedModels, proposalModel, finalModel);
758
802
  return fallbackSummary ? `${fallbackSummary}\n\n${output}` : output;
759
803
  }
760
804
  function formatSkippedModelFailureDetails(skippedModels) {
@@ -765,6 +809,10 @@ function formatSkippedModelFailureDetails(skippedModels) {
765
809
  const MAX_PARALLEL_TASKS = 8;
766
810
  const MAX_CONCURRENCY = 4;
767
811
  const MAX_TASK_LENGTH = 32_768; // 32 KB — prevent E2BIG from oversized argv
812
+ /** Resolve per-task thinking precedence for parallel and chain modes. */
813
+ export function resolveSubagentThinkingOverride(taskThinking, topLevelThinking) {
814
+ return taskThinking ?? topLevelThinking;
815
+ }
768
816
  // Semaphore for background task concurrency — shared across all background launches
769
817
  let bgRunning = 0;
770
818
  const bgWaiters = [];
@@ -804,9 +852,9 @@ function clampCwd(defaultCwd, itemCwd) {
804
852
  }
805
853
  return { ok: true, cwd: resolved };
806
854
  }
807
- export async function executeSingle(agents, agentName, task, cwd, signal, onProgress, modelOverride, parentProvider, registry, sessionDir, parentModel, agentModels, parentSessionFile, onChildEvent) {
808
- const name = agentName || DEFAULT_AGENT;
809
- const config = agents.get(name);
855
+ export async function executeSingle(agents, agentName, task, cwd, signal, onProgress, modelOverride, parentProvider, registry, sessionDir, parentModel, agentModels, parentSessionFile, onChildEvent, thinkingOverride, arbitration) {
856
+ let name = agentName || DEFAULT_AGENT;
857
+ let config = agents.get(name);
810
858
  if (!config) {
811
859
  return {
812
860
  agent: name,
@@ -817,7 +865,6 @@ export async function executeSingle(agents, agentName, task, cwd, signal, onProg
817
865
  errorMessage: `Unknown agent type "${name}". Available: ${[...agents.keys()].join(", ")}. If you expected "${name}" to exist, check the .md file in ~/.dreb/agents/ or .dreb/agents/ for syntax errors.`,
818
866
  };
819
867
  }
820
- // Validate task length for all modes (single, parallel items, chain steps)
821
868
  if (task.length > MAX_TASK_LENGTH) {
822
869
  return {
823
870
  agent: name,
@@ -828,21 +875,18 @@ export async function executeSingle(agents, agentName, task, cwd, signal, onProg
828
875
  errorMessage: `Task prompt too long (${task.length} chars, max ${MAX_TASK_LENGTH}). Shorten the prompt.`,
829
876
  };
830
877
  }
831
- // Per-invocation model override takes precedence over agent settings, which take precedence over agent definition model.
832
- // Override is always a single string; agentModels and agent config may be arrays.
833
- const modelSpec = modelOverride || (agentModels && agentModels.length > 0 ? agentModels : undefined) || config.model;
878
+ // Phase 1: resolve the parent's proposal with the existing precedence and
879
+ // fallback behavior so the arbiter receives one concrete canonical route.
880
+ const configuredModelSpec = modelOverride || (agentModels && agentModels.length > 0 ? agentModels : undefined) || config.model;
881
+ const modelSpec = configuredModelSpec || parentModel;
834
882
  let effectiveConfig = modelOverride ? { ...config, model: modelOverride } : config;
835
883
  let resolvedProvider = parentProvider;
884
+ let resolvedModel;
836
885
  let warning;
837
886
  let skippedModels = [];
838
- // Resolve and validate the model against the registry before spawning.
839
- // This catches typos and invalid model names immediately instead of failing
840
- // silently in the child process. Also passes the canonical model ID to the
841
- // child, avoiding fuzzy matching entirely. Agent definition fallback lists get
842
- // an additional best-effort 1-token probe before spawn so runtime-unavailable
843
- // models are skipped before committing to a child process.
844
887
  if (modelSpec) {
845
- const resolved = await resolveModelForSubagentSpawn(modelSpec, parentProvider, registry, parentModel, signal);
888
+ const parentFallback = configuredModelSpec ? parentModel : undefined;
889
+ const resolved = await resolveModelForSubagentSpawn(modelSpec, parentProvider, registry, parentFallback, signal);
846
890
  skippedModels = resolved.skippedModels;
847
891
  if (!resolved.ok) {
848
892
  const skippedDetails = formatSkippedModelFailureDetails(skippedModels);
@@ -856,21 +900,201 @@ export async function executeSingle(agents, agentName, task, cwd, signal, onProg
856
900
  };
857
901
  }
858
902
  effectiveConfig = { ...effectiveConfig, model: resolved.modelId };
859
- if (resolved.provider) {
903
+ if (resolved.provider)
860
904
  resolvedProvider = resolved.provider;
861
- }
905
+ if (registry && resolvedProvider)
906
+ resolvedModel = registry.find(resolvedProvider, resolved.modelId);
862
907
  warning = resolved.warning;
863
908
  }
909
+ if (thinkingOverride && thinkingOverride !== "off" && !modelSpec) {
910
+ return {
911
+ agent: name,
912
+ task,
913
+ exitCode: 1,
914
+ output: "",
915
+ stderr: "",
916
+ errorMessage: `Cannot validate thinking level "${thinkingOverride}" because agent "${name}" has no configured model and no parent model is available. Set a model on the agent or pass a per-call model override.`,
917
+ };
918
+ }
919
+ const proposalModelId = Array.isArray(effectiveConfig.model) ? effectiveConfig.model[0] : effectiveConfig.model;
920
+ const proposalSelectedModel = proposalModelId ? canonicalModelRef(resolvedProvider, proposalModelId) : undefined;
921
+ let finalThinking = thinkingOverride;
922
+ let arbitrationEnabled = false;
923
+ if (arbitration) {
924
+ const proposed = {
925
+ agent: name,
926
+ model: proposalSelectedModel ?? "",
927
+ thinking: resolveEffectiveThinkingLevel(resolvedModel, thinkingOverride, arbitration.defaultThinkingLevel),
928
+ };
929
+ let arbitrationResult;
930
+ try {
931
+ arbitrationResult = await arbitration.arbitrate({
932
+ task,
933
+ cwd,
934
+ proposed,
935
+ agents: summarizeAgentsForArbitration(agents, arbitration.getAgentModelsForAgent),
936
+ parentSessionFile,
937
+ step: arbitration.step,
938
+ }, signal);
939
+ }
940
+ catch {
941
+ const errorMessage = "Dispatch arbiter failed internally before child spawn.";
942
+ try {
943
+ arbitration.onRecord({
944
+ status: "failure",
945
+ proposed,
946
+ final: null,
947
+ changed: [],
948
+ step: arbitration.step,
949
+ errorCode: "internal_error",
950
+ errorMessage,
951
+ });
952
+ }
953
+ catch { }
954
+ return {
955
+ agent: name,
956
+ task,
957
+ exitCode: 1,
958
+ output: "",
959
+ stderr: "",
960
+ errorMessage,
961
+ };
962
+ }
963
+ if (arbitrationResult.enabled) {
964
+ arbitrationEnabled = true;
965
+ if (!arbitrationResult.ok) {
966
+ const record = {
967
+ status: "failure",
968
+ proposed,
969
+ final: null,
970
+ changed: [],
971
+ step: arbitration.step,
972
+ errorCode: arbitrationResult.code,
973
+ errorMessage: arbitrationResult.error,
974
+ };
975
+ try {
976
+ arbitration.onRecord(record);
977
+ }
978
+ catch {
979
+ // The original failure still prevents spawn; do not replace it with an observer error.
980
+ }
981
+ return {
982
+ agent: name,
983
+ task,
984
+ exitCode: 1,
985
+ output: "",
986
+ stderr: "",
987
+ errorMessage: `Dispatch arbitration failed: ${arbitrationResult.error}`,
988
+ };
989
+ }
990
+ const decision = arbitrationResult.decision;
991
+ const selectedConfig = agents.get(decision.agent);
992
+ const slash = decision.model.indexOf("/");
993
+ const selectedProvider = slash > 0 ? decision.model.slice(0, slash) : "";
994
+ const selectedModelId = slash > 0 ? decision.model.slice(slash + 1) : "";
995
+ const selectedModel = registry?.find(selectedProvider, selectedModelId);
996
+ if (!selectedConfig || !selectedModel) {
997
+ const errorMessage = !selectedConfig
998
+ ? "Arbiter selected an unknown agent."
999
+ : "Arbiter selected an unavailable model.";
1000
+ const record = {
1001
+ status: "failure",
1002
+ proposed,
1003
+ final: null,
1004
+ changed: [],
1005
+ step: arbitration.step,
1006
+ errorCode: !selectedConfig ? "unknown_agent" : "out_of_scope_model",
1007
+ errorMessage,
1008
+ };
1009
+ try {
1010
+ arbitration.onRecord(record);
1011
+ }
1012
+ catch { }
1013
+ return {
1014
+ agent: name,
1015
+ task,
1016
+ exitCode: 1,
1017
+ output: "",
1018
+ stderr: "",
1019
+ errorMessage: `Dispatch arbitration failed: ${errorMessage}`,
1020
+ };
1021
+ }
1022
+ const finalThinkingValidation = validateThinkingLevelForModel(selectedModel, decision.thinking);
1023
+ if (!finalThinkingValidation.ok) {
1024
+ const record = {
1025
+ status: "failure",
1026
+ proposed,
1027
+ final: null,
1028
+ changed: [],
1029
+ step: arbitration.step,
1030
+ errorCode: "unsupported_thinking",
1031
+ errorMessage: finalThinkingValidation.error,
1032
+ };
1033
+ try {
1034
+ arbitration.onRecord(record);
1035
+ }
1036
+ catch { }
1037
+ return {
1038
+ agent: name,
1039
+ task,
1040
+ exitCode: 1,
1041
+ output: "",
1042
+ stderr: "",
1043
+ errorMessage: `Dispatch arbitration failed: ${finalThinkingValidation.error}`,
1044
+ };
1045
+ }
1046
+ name = decision.agent;
1047
+ config = selectedConfig;
1048
+ effectiveConfig = { ...config, model: selectedModel.id };
1049
+ resolvedProvider = selectedModel.provider;
1050
+ resolvedModel = selectedModel;
1051
+ finalThinking = decision.thinking;
1052
+ try {
1053
+ arbitration.onRecord({
1054
+ status: "success",
1055
+ proposed,
1056
+ final: decision,
1057
+ changed: arbitrationResult.changed,
1058
+ step: arbitration.step,
1059
+ });
1060
+ }
1061
+ catch (error) {
1062
+ return {
1063
+ agent: name,
1064
+ task,
1065
+ exitCode: 1,
1066
+ output: "",
1067
+ stderr: "",
1068
+ errorMessage: `Dispatch arbitration observability failed before spawn: ${error instanceof Error ? error.message : String(error)}`,
1069
+ };
1070
+ }
1071
+ }
1072
+ }
1073
+ if (finalThinking) {
1074
+ const validation = validateThinkingLevelForModel(resolvedModel, finalThinking);
1075
+ if (!validation.ok) {
1076
+ return {
1077
+ agent: name,
1078
+ task,
1079
+ exitCode: 1,
1080
+ output: "",
1081
+ stderr: "",
1082
+ errorMessage: validation.error,
1083
+ };
1084
+ }
1085
+ }
864
1086
  const usedModel = effectiveConfig.model?.toString();
865
1087
  onProgress?.(`Running ${name} agent${usedModel ? ` (${usedModel})` : ""}...`);
866
- const result = await spawnSubagent(effectiveConfig, task, cwd, signal, onProgress, resolvedProvider, sessionDir, parentSessionFile, onChildEvent);
867
- result.output = prependModelFallbackSummary(result.output, skippedModels, result.model ?? usedModel);
1088
+ const result = await spawnSubagent(effectiveConfig, task, cwd, signal, onProgress, resolvedProvider, sessionDir, parentSessionFile, onChildEvent, finalThinking);
1089
+ const finalSelectedModel = result.model ?? (usedModel ? canonicalModelRef(resolvedProvider, usedModel) : undefined);
1090
+ result.output = prependModelFallbackSummary(result.output, skippedModels, arbitrationEnabled ? proposalSelectedModel : finalSelectedModel, arbitrationEnabled ? finalSelectedModel : undefined);
868
1091
  if (warning) {
869
- result.output = `[WARNING: ${warning}]\n\n${result.output}`;
1092
+ const warningContext = arbitrationEnabled ? `Proposal resolution: ${warning}` : warning;
1093
+ result.output = `[WARNING: ${warningContext}]\n\n${result.output}`;
870
1094
  }
871
1095
  return result;
872
1096
  }
873
- async function executeChain(agents, chain, defaultCwd, signal, onProgress, parentProvider, registry, sessionBaseDir, defaultAgent, defaultModel, parentModel, getAgentModelsForAgentFn, parentSessionFile, onChildEvent) {
1097
+ async function executeChain(agents, chain, defaultCwd, signal, onProgress, parentProvider, registry, sessionBaseDir, defaultAgent, defaultModel, defaultThinking, parentModel, getAgentModelsForAgentFn, parentSessionFile, onChildEvent, arbitration) {
874
1098
  const results = [];
875
1099
  let previousOutput = "";
876
1100
  for (let i = 0; i < chain.length; i++) {
@@ -907,7 +1131,7 @@ async function executeChain(agents, chain, defaultCwd, signal, onProgress, paren
907
1131
  const stepSessionDir = sessionBaseDir ? join(sessionBaseDir, `step-${i + 1}`) : undefined;
908
1132
  const stepAgentName = step.agent || defaultAgent || DEFAULT_AGENT;
909
1133
  const stepMach6Models = getAgentModelsForAgentFn?.(stepAgentName);
910
- const result = await executeSingle(agents, step.agent || defaultAgent, task, cwdResult.cwd, signal, onProgress, step.model || defaultModel, parentProvider, registry, stepSessionDir, parentModel, stepMach6Models, parentSessionFile, onChildEvent);
1134
+ const result = await executeSingle(agents, step.agent || defaultAgent, task, cwdResult.cwd, signal, onProgress, step.model || defaultModel, parentProvider, registry, stepSessionDir, parentModel, stepMach6Models, parentSessionFile, onChildEvent, resolveSubagentThinkingOverride(step.thinking, defaultThinking), arbitration ? { ...arbitration, step: i + 1 } : undefined);
911
1135
  results.push(result);
912
1136
  if (result.exitCode !== 0) {
913
1137
  break; // stop chain on error
@@ -1201,13 +1425,19 @@ export function rehydrateBackgroundAgentsFromDisk(parentSessionFile, subagentSes
1201
1425
  }
1202
1426
  return registered;
1203
1427
  }
1428
+ function cloneBackgroundAgentInfo(info) {
1429
+ return {
1430
+ ...info,
1431
+ arbitrations: info.arbitrations?.map((record) => structuredClone(record)),
1432
+ };
1433
+ }
1204
1434
  /** Get a snapshot of all tracked background agents (running and recently completed). Returns readonly clones. */
1205
1435
  export function getBackgroundAgents() {
1206
- return [...backgroundAgentRegistry.values()].map((a) => ({ ...a }));
1436
+ return [...backgroundAgentRegistry.values()].map(cloneBackgroundAgentInfo);
1207
1437
  }
1208
1438
  /** Get only currently running background agents. Returns readonly clones. */
1209
1439
  export function getRunningBackgroundAgents() {
1210
- return [...backgroundAgentRegistry.values()].filter((a) => a.status === "running").map((a) => ({ ...a }));
1440
+ return [...backgroundAgentRegistry.values()].filter((a) => a.status === "running").map(cloneBackgroundAgentInfo);
1211
1441
  }
1212
1442
  /** Abort all running background agents. */
1213
1443
  export function abortBackgroundAgents() {
@@ -1233,6 +1463,14 @@ export function pruneBackgroundAgents(maxAgeMs = 5 * 60 * 1000) {
1233
1463
  // ---------------------------------------------------------------------------
1234
1464
  // Tool schema and definition
1235
1465
  // ---------------------------------------------------------------------------
1466
+ const thinkingLevelSchema = Type.Union([
1467
+ Type.Literal("off"),
1468
+ Type.Literal("minimal"),
1469
+ Type.Literal("low"),
1470
+ Type.Literal("medium"),
1471
+ Type.Literal("high"),
1472
+ Type.Literal("xhigh"),
1473
+ ], { description: "Thinking level override for the child model." });
1236
1474
  const taskItemSchema = Type.Object({
1237
1475
  agent: Type.Optional(Type.String({ description: "Agent type name (default: 'Explore')" })),
1238
1476
  task: Type.String({ description: "The task prompt for this subagent" }),
@@ -1242,6 +1480,7 @@ const taskItemSchema = Type.Object({
1242
1480
  model: Type.Optional(Type.String({
1243
1481
  description: "Model override for this task. Takes precedence over agent definition model. Note: a single-string override discards the agent's fallback list.",
1244
1482
  })),
1483
+ thinking: Type.Optional(thinkingLevelSchema),
1245
1484
  });
1246
1485
  const subagentSchema = Type.Object({
1247
1486
  agent: Type.Optional(Type.String({ description: "Agent type name (default: 'Explore')" })),
@@ -1249,6 +1488,16 @@ const subagentSchema = Type.Object({
1249
1488
  model: Type.Optional(Type.String({
1250
1489
  description: "Model override. Takes precedence over agent definition model. Note: a single-string override discards the agent's fallback list. For parallel/chain, set per-task instead.",
1251
1490
  })),
1491
+ thinking: Type.Optional(Type.Union([
1492
+ Type.Literal("off"),
1493
+ Type.Literal("minimal"),
1494
+ Type.Literal("low"),
1495
+ Type.Literal("medium"),
1496
+ Type.Literal("high"),
1497
+ Type.Literal("xhigh"),
1498
+ ], {
1499
+ description: "Thinking level override. Per-task values take precedence in parallel and chain modes. Omit to preserve child defaults.",
1500
+ })),
1252
1501
  tasks: Type.Optional(Type.Array(taskItemSchema, {
1253
1502
  description: "Array of tasks to run in parallel (max 8)",
1254
1503
  minItems: 1,
@@ -1322,7 +1571,13 @@ function formatSubagentResult(result, options, theme, showImages) {
1322
1571
  return text;
1323
1572
  }
1324
1573
  export function formatSingleResult(result) {
1325
- let text = `## Agent: ${result.agent}${result.model ? ` (model: ${result.model})` : ""}\n`;
1574
+ const metadata = [
1575
+ result.model ? `model: ${result.model}` : undefined,
1576
+ result.thinking ? `thinking: ${result.thinking}` : undefined,
1577
+ ]
1578
+ .filter((value) => value !== undefined)
1579
+ .join(", ");
1580
+ let text = `## Agent: ${result.agent}${metadata ? ` (${metadata})` : ""}\n`;
1326
1581
  if (result.exitCode !== 0) {
1327
1582
  text += `**Error** (exit ${result.exitCode}): ${result.errorMessage || "Unknown error"}\n`;
1328
1583
  if (result.stderr) {
@@ -1353,6 +1608,9 @@ export function createSubagentToolDefinition(cwd, options) {
1353
1608
  const getParentSessionFile = options?.parentSessionFile ?? (() => undefined);
1354
1609
  const modelRegistry = options?.modelRegistry;
1355
1610
  const getAgentModelsForAgent = options?.getAgentModelsForAgent;
1611
+ const arbitrate = options?.arbitrate;
1612
+ const onArbitration = options?.onArbitration;
1613
+ const getDefaultThinkingLevel = options?.defaultThinkingLevel;
1356
1614
  // Discover agents at definition time to build the prompt guidelines.
1357
1615
  // This is cheap (reads .md files) and the same call happens on every execute().
1358
1616
  const knownAgents = discoverAgentTypes(cwd);
@@ -1382,6 +1640,7 @@ export function createSubagentToolDefinition(cwd, options) {
1382
1640
  "Each agent notifies independently when done — completion messages include a list of any still-running agents. If you need their results before proceeding, end your current turn with no tool calls (as if you were asking the user a question and waiting for their reply). This emits `agent_end` and lets the framework deliver the completion as a new message that resumes your turn automatically. Do not call `sleep` or any other waiting action, and do not launch filler work.",
1383
1641
  "Agent definitions specify a `model` field with a provider fallback list (comma-separated or YAML list). The spawner tries each in order and uses the first one that resolves for the current provider. This makes agents portable across providers.",
1384
1642
  "Per-invocation `model` overrides take precedence but **discard the entire fallback list** — if the single override model isn't available on the current provider, the agent fails. Only override when you have a specific reason (e.g. escalating to a stronger tier for a complex task).",
1643
+ "Optional `thinking` overrides accept off/minimal/low/medium/high/xhigh. Per-task values override a top-level value; unsupported levels fail before spawn. Omit thinking to preserve the child's configured default.",
1385
1644
  "**Model routing** — agent definitions already specify the right tier for their role. Most subagent tasks (exploration, file discovery, grep, navigation, summarization) are handled well by the defaults. Do not override the model unless the task genuinely requires a different capability tier than what the agent definition provides.",
1386
1645
  "**Model identity** — Your current model is stated in the system prompt as `You are running on: provider/id`. Use this for explicit routing decisions — e.g. delegate vision tasks if you're on a text-only model, or use a differently-architected model as a critic for tasks where diverse model perspectives improve reliability.",
1387
1646
  ],
@@ -1426,9 +1685,9 @@ export function createSubagentToolDefinition(cwd, options) {
1426
1685
  * Shared lifecycle for all background launches: generates agent ID,
1427
1686
  * sets up registry/abort/notification, gates on the concurrency
1428
1687
  * semaphore, and handles errors. The caller provides the actual
1429
- * work via `runFn(signal, onChildEvent)` which must return a
1430
- * SubagentResult; `onChildEvent` (when defined) must be forwarded to
1431
- * the spawn so the child's JSONL events reach the relay.
1688
+ * work via `runFn(signal, onChildEvent, onArbitrationRecord)` which must
1689
+ * return a SubagentResult. Child events are relayed live; arbitration
1690
+ * records update the registry before being persisted/emitted.
1432
1691
  */
1433
1692
  const launchBackgroundLifecycle = (agentName, taskSummary, sessionDir, agentCwd, runFn) => {
1434
1693
  const agentId = generateAgentId();
@@ -1456,6 +1715,16 @@ export function createSubagentToolDefinition(cwd, options) {
1456
1715
  }
1457
1716
  }
1458
1717
  : undefined;
1718
+ const onArbitrationRecord = (record) => {
1719
+ const entry = backgroundAgentRegistry.get(agentId);
1720
+ if (entry) {
1721
+ entry.arbitrations ??= [];
1722
+ entry.arbitrations.push(record);
1723
+ if (record.status === "success" && record.final)
1724
+ entry.agentType = record.final.agent;
1725
+ }
1726
+ onArbitration?.({ type: "subagent_arbitration", agentId, ...record });
1727
+ };
1459
1728
  const bgSignal = bgAbort.signal;
1460
1729
  const safeNotify = (result) => {
1461
1730
  try {
@@ -1468,7 +1737,7 @@ export function createSubagentToolDefinition(cwd, options) {
1468
1737
  const run = async () => {
1469
1738
  await bgAcquire();
1470
1739
  try {
1471
- const result = await runFn(bgSignal, onChildEvent);
1740
+ const result = await runFn(bgSignal, onChildEvent, onArbitrationRecord);
1472
1741
  const entry = backgroundAgentRegistry.get(agentId);
1473
1742
  if (entry && !bgSignal.aborted)
1474
1743
  entry.status = result.exitCode === 0 ? "completed" : "failed";
@@ -1519,18 +1788,25 @@ export function createSubagentToolDefinition(cwd, options) {
1519
1788
  };
1520
1789
  // Helper to launch a single background task
1521
1790
  const subagentSessionsBase = getSubagentSessionsDir();
1522
- const launchBackgroundTask = (agentName, task, taskLabel, taskCwd, modelOverride) => {
1791
+ const launchBackgroundTask = (agentName, task, taskLabel, taskCwd, modelOverride, thinkingOverride) => {
1523
1792
  const resolvedCwd = taskCwd ?? cwd;
1524
1793
  // Each background agent gets its own session subdirectory
1525
1794
  const sessionId = generateAgentId();
1526
1795
  const sessionDir = join(subagentSessionsBase, sessionId);
1527
1796
  const agentModels = getAgentModelsForAgent?.(agentName || DEFAULT_AGENT);
1528
- return launchBackgroundLifecycle(agentName, taskLabel, sessionDir, resolvedCwd, (signal, onChildEvent) => executeSingle(agents, agentName === DEFAULT_AGENT ? undefined : agentName, task, resolvedCwd, signal, undefined, modelOverride, getParentProvider(), modelRegistry, sessionDir, getParentModel(), agentModels, getParentSessionFile(), onChildEvent));
1797
+ return launchBackgroundLifecycle(agentName, taskLabel, sessionDir, resolvedCwd, (signal, onChildEvent, onArbitrationRecord) => executeSingle(agents, agentName === DEFAULT_AGENT ? undefined : agentName, task, resolvedCwd, signal, undefined, modelOverride, getParentProvider(), modelRegistry, sessionDir, getParentModel(), agentModels, getParentSessionFile(), onChildEvent, thinkingOverride, arbitrate
1798
+ ? {
1799
+ arbitrate,
1800
+ onRecord: onArbitrationRecord,
1801
+ defaultThinkingLevel: getDefaultThinkingLevel?.(),
1802
+ getAgentModelsForAgent,
1803
+ }
1804
+ : undefined));
1529
1805
  };
1530
1806
  if (params.task) {
1531
1807
  // Single background task
1532
1808
  const agentName = params.agent || DEFAULT_AGENT;
1533
- const agentId = launchBackgroundTask(agentName, params.task, `${agentName} task`, undefined, params.model);
1809
+ const agentId = launchBackgroundTask(agentName, params.task, `${agentName} task`, undefined, params.model, params.thinking);
1534
1810
  return {
1535
1811
  content: [
1536
1812
  {
@@ -1554,7 +1830,7 @@ export function createSubagentToolDefinition(cwd, options) {
1554
1830
  skipped.push({ taskText: item.task, error: cwdResult.error });
1555
1831
  continue;
1556
1832
  }
1557
- const agentId = launchBackgroundTask(agentName, item.task, `${agentName} task ${i + 1}/${params.tasks.length}`, cwdResult.cwd, item.model || params.model);
1833
+ const agentId = launchBackgroundTask(agentName, item.task, `${agentName} task ${i + 1}/${params.tasks.length}`, cwdResult.cwd, item.model || params.model, resolveSubagentThinkingOverride(item.thinking, params.thinking));
1558
1834
  launched.push({ id: agentId, agentName, taskText: item.task });
1559
1835
  }
1560
1836
  const listing = launched
@@ -1590,8 +1866,15 @@ export function createSubagentToolDefinition(cwd, options) {
1590
1866
  const taskSummary = `${params.chain.length}-step chain`;
1591
1867
  const chainSteps = params.chain;
1592
1868
  const chainSessionDir = join(subagentSessionsBase, `chain-${generateAgentId()}`);
1593
- const agentId = launchBackgroundLifecycle(agentName, taskSummary, chainSessionDir, cwd, async (signal, onChildEvent) => {
1594
- const results = await executeChain(agents, chainSteps, cwd, signal, undefined, getParentProvider(), modelRegistry, chainSessionDir, params.agent, params.model, getParentModel(), getAgentModelsForAgent, getParentSessionFile(), onChildEvent);
1869
+ const agentId = launchBackgroundLifecycle(agentName, taskSummary, chainSessionDir, cwd, async (signal, onChildEvent, onArbitrationRecord) => {
1870
+ const results = await executeChain(agents, chainSteps, cwd, signal, undefined, getParentProvider(), modelRegistry, chainSessionDir, params.agent, params.model, params.thinking, getParentModel(), getAgentModelsForAgent, getParentSessionFile(), onChildEvent, arbitrate
1871
+ ? {
1872
+ arbitrate,
1873
+ onRecord: onArbitrationRecord,
1874
+ defaultThinkingLevel: getDefaultThinkingLevel?.(),
1875
+ getAgentModelsForAgent,
1876
+ }
1877
+ : undefined);
1595
1878
  const resultText = results
1596
1879
  .map((r, i) => `### Step ${i + 1}\n${formatSingleResult(r)}`)
1597
1880
  .join("\n\n---\n\n");
@@ -1600,6 +1883,13 @@ export function createSubagentToolDefinition(cwd, options) {
1600
1883
  return {
1601
1884
  agent: agentName,
1602
1885
  task: taskSummary,
1886
+ steps: results.map((result, index) => ({
1887
+ step: index + 1,
1888
+ agent: result.agent,
1889
+ success: result.exitCode === 0,
1890
+ model: result.model,
1891
+ thinking: result.thinking,
1892
+ })),
1603
1893
  exitCode: failed.length > 0 ? 1 : 0,
1604
1894
  output: resultText,
1605
1895
  stderr: "",