@dreb/coding-agent 2.17.0 → 2.19.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.
- package/README.md +6 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/sdk.d.ts +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/index.d.ts +6 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +9 -2
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +44 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +206 -7
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/tools/wait.d.ts +40 -0
- package/dist/core/tools/wait.d.ts.map +1 -0
- package/dist/core/tools/wait.js +87 -0
- package/dist/core/tools/wait.js.map +1 -0
- package/docs/extensions.md +2 -2
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
|
+
import { complete } from "@dreb/ai";
|
|
6
7
|
import { Text } from "@dreb/tui";
|
|
7
8
|
import { Type } from "@sinclair/typebox";
|
|
8
9
|
import { CONFIG_DIR_NAME, getPackageDir, getSubagentSessionsDir } from "../../config.js";
|
|
@@ -108,6 +109,31 @@ function loadAgentsFromDir(dir, agents) {
|
|
|
108
109
|
// and process.argv[1] may return corrupted or truncated data.
|
|
109
110
|
const DREB_SCRIPT = process.argv[1] || "dreb";
|
|
110
111
|
const NODE_EXEC = process.execPath;
|
|
112
|
+
// Tools that must never be available to subagents — wait (subagents should
|
|
113
|
+
// never no-op; they have a task to complete) and subagent (no recursive spawning).
|
|
114
|
+
const SUBAGENT_EXCLUDED_TOOLS = ["wait", "subagent"];
|
|
115
|
+
// Default standard tools for subagents when no tools are specified in the agent
|
|
116
|
+
// definition. This is the set passed via --tools to the child process.
|
|
117
|
+
//
|
|
118
|
+
// NOTE: Always-active tools (search, skill, tasks_update, suggest_next) are NOT
|
|
119
|
+
// listed here — the child process adds them unconditionally regardless of --tools.
|
|
120
|
+
// Internal tools (tmp_read) are also excluded.
|
|
121
|
+
const SUBAGENT_DEFAULT_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls", "web_search", "web_fetch"];
|
|
122
|
+
/**
|
|
123
|
+
* Filter a comma-separated tools string, removing any tools in SUBAGENT_EXCLUDED_TOOLS.
|
|
124
|
+
* Returns the filtered tools as a comma-separated string (always non-empty — falls
|
|
125
|
+
* back to SUBAGENT_DEFAULT_TOOLS if all specified tools were excluded).
|
|
126
|
+
*/
|
|
127
|
+
export function filterSubagentTools(tools) {
|
|
128
|
+
if (!tools)
|
|
129
|
+
return SUBAGENT_DEFAULT_TOOLS.join(",");
|
|
130
|
+
const filtered = tools
|
|
131
|
+
.split(",")
|
|
132
|
+
.map((t) => t.trim())
|
|
133
|
+
.filter((t) => !SUBAGENT_EXCLUDED_TOOLS.includes(t))
|
|
134
|
+
.join(",");
|
|
135
|
+
return filtered || SUBAGENT_DEFAULT_TOOLS.join(",");
|
|
136
|
+
}
|
|
111
137
|
// TODO: Support PATH-based binary discovery.
|
|
112
138
|
// Currently returns the captured argv[1].
|
|
113
139
|
function findDrebBinary() {
|
|
@@ -147,9 +173,9 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
|
|
|
147
173
|
args.push("--provider", parentProvider);
|
|
148
174
|
}
|
|
149
175
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
176
|
+
// Always pass --tools to ensure wait/subagent are excluded from child processes.
|
|
177
|
+
// filterSubagentTools always returns a non-empty string.
|
|
178
|
+
args.push("--tools", filterSubagentTools(agentConfig.tools));
|
|
153
179
|
if (agentConfig.systemPrompt) {
|
|
154
180
|
args.push("--append-system-prompt", agentConfig.systemPrompt);
|
|
155
181
|
}
|
|
@@ -433,6 +459,173 @@ export function resolveModelStringSingle(modelStr, parentProvider, registry) {
|
|
|
433
459
|
}
|
|
434
460
|
return { ok: true, modelId: resolved.model.id, provider: resolved.model.provider };
|
|
435
461
|
}
|
|
462
|
+
function compactErrorReason(reason) {
|
|
463
|
+
const singleLine = reason.replace(/\s+/g, " ").trim();
|
|
464
|
+
return singleLine.length > 180 ? `${singleLine.slice(0, 177)}...` : singleLine || "unknown error";
|
|
465
|
+
}
|
|
466
|
+
function reasonFromRuntimeError(value) {
|
|
467
|
+
if (value instanceof Error)
|
|
468
|
+
return value.message;
|
|
469
|
+
if (typeof value === "string")
|
|
470
|
+
return value;
|
|
471
|
+
if (value && typeof value === "object") {
|
|
472
|
+
const maybeMessage = value;
|
|
473
|
+
if (typeof maybeMessage.errorMessage === "string")
|
|
474
|
+
return maybeMessage.errorMessage;
|
|
475
|
+
if (typeof maybeMessage.message === "string")
|
|
476
|
+
return maybeMessage.message;
|
|
477
|
+
}
|
|
478
|
+
return String(value);
|
|
479
|
+
}
|
|
480
|
+
export function isRuntimeUnavailableError(value) {
|
|
481
|
+
if (value instanceof Error || typeof value === "string")
|
|
482
|
+
return true;
|
|
483
|
+
if (value && typeof value === "object") {
|
|
484
|
+
const maybeMessage = value;
|
|
485
|
+
return maybeMessage.stopReason === "error" || maybeMessage.stopReason === "aborted";
|
|
486
|
+
}
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
function makeProbeSignal(parentSignal, timeoutMs) {
|
|
490
|
+
const controller = new AbortController();
|
|
491
|
+
const timeoutError = new Error(`Model availability probe timed out after ${timeoutMs}ms`);
|
|
492
|
+
let timeout;
|
|
493
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
494
|
+
timeout = setTimeout(() => {
|
|
495
|
+
controller.abort(timeoutError);
|
|
496
|
+
reject(timeoutError);
|
|
497
|
+
}, timeoutMs);
|
|
498
|
+
});
|
|
499
|
+
const parentAbortHandler = () => controller.abort(parentSignal?.reason);
|
|
500
|
+
parentSignal?.addEventListener("abort", parentAbortHandler, { once: true });
|
|
501
|
+
if (parentSignal?.aborted)
|
|
502
|
+
controller.abort(parentSignal.reason);
|
|
503
|
+
return {
|
|
504
|
+
signal: controller.signal,
|
|
505
|
+
timeoutPromise,
|
|
506
|
+
cleanup: () => {
|
|
507
|
+
clearTimeout(timeout);
|
|
508
|
+
parentSignal?.removeEventListener("abort", parentAbortHandler);
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
export async function probeModelAvailability(model, options = {}) {
|
|
513
|
+
const { signal, registry, timeoutMs = 10_000 } = options;
|
|
514
|
+
if (signal?.aborted)
|
|
515
|
+
return { ok: false, reason: "Aborted before spawn", aborted: true };
|
|
516
|
+
const probeSignal = makeProbeSignal(signal, timeoutMs);
|
|
517
|
+
try {
|
|
518
|
+
const context = {
|
|
519
|
+
systemPrompt: "You are a model availability probe. Reply briefly.",
|
|
520
|
+
messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
|
|
521
|
+
};
|
|
522
|
+
const apiKey = await Promise.race([
|
|
523
|
+
registry ? registry.getApiKey(model) : Promise.resolve(undefined),
|
|
524
|
+
probeSignal.timeoutPromise,
|
|
525
|
+
]);
|
|
526
|
+
if (signal?.aborted)
|
|
527
|
+
return { ok: false, reason: "Aborted before spawn", aborted: true };
|
|
528
|
+
const result = await Promise.race([
|
|
529
|
+
complete(model, context, {
|
|
530
|
+
apiKey,
|
|
531
|
+
maxRetryDelayMs: 0,
|
|
532
|
+
maxTokens: 1,
|
|
533
|
+
signal: probeSignal.signal,
|
|
534
|
+
}),
|
|
535
|
+
probeSignal.timeoutPromise,
|
|
536
|
+
]);
|
|
537
|
+
if (signal?.aborted)
|
|
538
|
+
return { ok: false, reason: "Aborted before spawn", aborted: true };
|
|
539
|
+
if (isRuntimeUnavailableError(result)) {
|
|
540
|
+
return { ok: false, reason: compactErrorReason(reasonFromRuntimeError(result)) };
|
|
541
|
+
}
|
|
542
|
+
return { ok: true };
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
if (signal?.aborted)
|
|
546
|
+
return { ok: false, reason: "Aborted before spawn", aborted: true };
|
|
547
|
+
return { ok: false, reason: compactErrorReason(reasonFromRuntimeError(err)) };
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
probeSignal.cleanup();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
export async function resolveModelForSubagentSpawn(models, parentProvider, registry, parentModel, signal) {
|
|
554
|
+
if (signal?.aborted)
|
|
555
|
+
return { ok: false, error: "Aborted before spawn", skippedModels: [] };
|
|
556
|
+
// Runtime probing only applies to agent definition fallback lists. Single
|
|
557
|
+
// models, per-invocation overrides, and registry-less environments keep the
|
|
558
|
+
// existing spawn-time resolution behavior exactly.
|
|
559
|
+
if (!Array.isArray(models) || !registry) {
|
|
560
|
+
const resolved = resolveModelWithFallbacks(models, parentProvider, registry, parentModel);
|
|
561
|
+
return { ...resolved, skippedModels: [] };
|
|
562
|
+
}
|
|
563
|
+
const skippedModels = [];
|
|
564
|
+
let lastError = "";
|
|
565
|
+
for (const modelStr of models) {
|
|
566
|
+
if (signal?.aborted)
|
|
567
|
+
return { ok: false, error: "Aborted before spawn", skippedModels };
|
|
568
|
+
const resolved = resolveModelStringSingle(modelStr, parentProvider, registry);
|
|
569
|
+
if (!resolved.ok) {
|
|
570
|
+
lastError = resolved.error;
|
|
571
|
+
const reason = compactErrorReason(resolved.error);
|
|
572
|
+
skippedModels.push({ model: modelStr, reason });
|
|
573
|
+
console.error(`[subagent] Model "${modelStr}" unavailable (${reason}). Trying next fallback...`);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const modelObj = resolved.provider ? registry.find(resolved.provider, resolved.modelId) : undefined;
|
|
577
|
+
if (modelObj) {
|
|
578
|
+
const probe = await probeModelAvailability(modelObj, { signal, registry });
|
|
579
|
+
if (!probe.ok && probe.aborted) {
|
|
580
|
+
return { ok: false, error: "Aborted before spawn", skippedModels };
|
|
581
|
+
}
|
|
582
|
+
if (signal?.aborted)
|
|
583
|
+
return { ok: false, error: "Aborted before spawn", skippedModels };
|
|
584
|
+
if (!probe.ok) {
|
|
585
|
+
lastError = probe.reason;
|
|
586
|
+
skippedModels.push({ model: modelStr, reason: probe.reason });
|
|
587
|
+
console.error(`[subagent] Model "${modelStr}" failed probe (${probe.reason}). Trying next fallback...`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
console.error(`[subagent] Using model "${resolved.modelId}" for subagent.`);
|
|
592
|
+
return { ...resolved, skippedModels };
|
|
593
|
+
}
|
|
594
|
+
if (signal?.aborted)
|
|
595
|
+
return { ok: false, error: "Aborted before spawn", skippedModels };
|
|
596
|
+
if (parentModel) {
|
|
597
|
+
const parentResolved = resolveModelStringSingle(parentModel, parentProvider, registry);
|
|
598
|
+
if (parentResolved.ok) {
|
|
599
|
+
const warning = `Agent preferred models were unavailable. Falling back to parent model "${parentResolved.modelId}".`;
|
|
600
|
+
console.error(`[subagent] ${warning}`);
|
|
601
|
+
return { ...parentResolved, warning, skippedModels };
|
|
602
|
+
}
|
|
603
|
+
lastError = parentResolved.error;
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
skippedModels,
|
|
608
|
+
error: `None of the fallback models passed availability checks: ${[
|
|
609
|
+
...models,
|
|
610
|
+
...(parentModel ? [parentModel] : []),
|
|
611
|
+
].join(", ")}. Last error: ${lastError || "all probes failed"}`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
export function formatModelFallbackSummary(skippedModels, selectedModel) {
|
|
615
|
+
if (skippedModels.length === 0)
|
|
616
|
+
return undefined;
|
|
617
|
+
const skipped = skippedModels.map((s) => `- ${s.model}: ${s.reason}`).join("\n");
|
|
618
|
+
return `[MODEL FALLBACK: skipped ${skippedModels.length} unavailable model(s); using "${selectedModel ?? "unknown"}".]\n${skipped}`;
|
|
619
|
+
}
|
|
620
|
+
export function prependModelFallbackSummary(output, skippedModels, selectedModel) {
|
|
621
|
+
const fallbackSummary = formatModelFallbackSummary(skippedModels, selectedModel);
|
|
622
|
+
return fallbackSummary ? `${fallbackSummary}\n\n${output}` : output;
|
|
623
|
+
}
|
|
624
|
+
function formatSkippedModelFailureDetails(skippedModels) {
|
|
625
|
+
if (skippedModels.length === 0)
|
|
626
|
+
return undefined;
|
|
627
|
+
return `Skipped models:\n${skippedModels.map((s) => `- ${s.model}: ${s.reason}`).join("\n")}`;
|
|
628
|
+
}
|
|
436
629
|
const MAX_PARALLEL_TASKS = 8;
|
|
437
630
|
const MAX_CONCURRENCY = 4;
|
|
438
631
|
const MAX_TASK_LENGTH = 32_768; // 32 KB — prevent E2BIG from oversized argv
|
|
@@ -474,7 +667,7 @@ function clampCwd(defaultCwd, itemCwd) {
|
|
|
474
667
|
}
|
|
475
668
|
return { ok: true, cwd: resolved };
|
|
476
669
|
}
|
|
477
|
-
async function executeSingle(agents, agentName, task, cwd, signal, onProgress, modelOverride, parentProvider, registry, sessionDir, parentModel) {
|
|
670
|
+
export async function executeSingle(agents, agentName, task, cwd, signal, onProgress, modelOverride, parentProvider, registry, sessionDir, parentModel) {
|
|
478
671
|
const name = agentName || DEFAULT_AGENT;
|
|
479
672
|
const config = agents.get(name);
|
|
480
673
|
if (!config) {
|
|
@@ -504,20 +697,25 @@ async function executeSingle(agents, agentName, task, cwd, signal, onProgress, m
|
|
|
504
697
|
let effectiveConfig = modelOverride ? { ...config, model: modelOverride } : config;
|
|
505
698
|
let resolvedProvider = parentProvider;
|
|
506
699
|
let warning;
|
|
700
|
+
let skippedModels = [];
|
|
507
701
|
// Resolve and validate the model against the registry before spawning.
|
|
508
702
|
// This catches typos and invalid model names immediately instead of failing
|
|
509
703
|
// silently in the child process. Also passes the canonical model ID to the
|
|
510
|
-
// child, avoiding fuzzy matching entirely.
|
|
704
|
+
// child, avoiding fuzzy matching entirely. Agent definition fallback lists get
|
|
705
|
+
// an additional best-effort 1-token probe before spawn so runtime-unavailable
|
|
706
|
+
// models are skipped before committing to a child process.
|
|
511
707
|
if (modelSpec) {
|
|
512
|
-
const resolved =
|
|
708
|
+
const resolved = await resolveModelForSubagentSpawn(modelSpec, parentProvider, registry, parentModel, signal);
|
|
709
|
+
skippedModels = resolved.skippedModels;
|
|
513
710
|
if (!resolved.ok) {
|
|
711
|
+
const skippedDetails = formatSkippedModelFailureDetails(skippedModels);
|
|
514
712
|
return {
|
|
515
713
|
agent: name,
|
|
516
714
|
task,
|
|
517
715
|
exitCode: 1,
|
|
518
716
|
output: "",
|
|
519
717
|
stderr: "",
|
|
520
|
-
errorMessage: resolved.error,
|
|
718
|
+
errorMessage: skippedDetails ? `${resolved.error}\n\n${skippedDetails}` : resolved.error,
|
|
521
719
|
};
|
|
522
720
|
}
|
|
523
721
|
effectiveConfig = { ...effectiveConfig, model: resolved.modelId };
|
|
@@ -528,6 +726,7 @@ async function executeSingle(agents, agentName, task, cwd, signal, onProgress, m
|
|
|
528
726
|
}
|
|
529
727
|
onProgress?.(`Running ${name} agent...`);
|
|
530
728
|
const result = await spawnSubagent(effectiveConfig, task, cwd, signal, onProgress, resolvedProvider, sessionDir);
|
|
729
|
+
result.output = prependModelFallbackSummary(result.output, skippedModels, result.model ?? effectiveConfig.model?.toString());
|
|
531
730
|
if (warning) {
|
|
532
731
|
result.output = `[WARNING: ${warning}]\n\n${result.output}`;
|
|
533
732
|
}
|