@dreb/coding-agent 2.18.0 → 2.19.1

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 (57) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +3 -2
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/cli/file-processor.d.ts.map +1 -1
  6. package/dist/cli/file-processor.js +3 -2
  7. package/dist/cli/file-processor.js.map +1 -1
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +6 -5
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/buddy/buddy-controller.d.ts.map +1 -1
  12. package/dist/core/buddy/buddy-controller.js +3 -2
  13. package/dist/core/buddy/buddy-controller.js.map +1 -1
  14. package/dist/core/event-bus.d.ts.map +1 -1
  15. package/dist/core/event-bus.js +2 -1
  16. package/dist/core/event-bus.js.map +1 -1
  17. package/dist/core/logger.d.ts +29 -0
  18. package/dist/core/logger.d.ts.map +1 -0
  19. package/dist/core/logger.js +54 -0
  20. package/dist/core/logger.js.map +1 -0
  21. package/dist/core/model-resolver.d.ts.map +1 -1
  22. package/dist/core/model-resolver.js +3 -2
  23. package/dist/core/model-resolver.js.map +1 -1
  24. package/dist/core/package-manager.d.ts.map +1 -1
  25. package/dist/core/package-manager.js +25 -2
  26. package/dist/core/package-manager.js.map +1 -1
  27. package/dist/core/stderr-guard.d.ts +37 -0
  28. package/dist/core/stderr-guard.d.ts.map +1 -0
  29. package/dist/core/stderr-guard.js +90 -0
  30. package/dist/core/stderr-guard.js.map +1 -0
  31. package/dist/core/timings.d.ts.map +1 -1
  32. package/dist/core/timings.js +5 -4
  33. package/dist/core/timings.js.map +1 -1
  34. package/dist/core/tools/subagent.d.ts +38 -1
  35. package/dist/core/tools/subagent.d.ts.map +1 -1
  36. package/dist/core/tools/subagent.js +192 -17
  37. package/dist/core/tools/subagent.js.map +1 -1
  38. package/dist/core/tools/terminal-render.d.ts.map +1 -1
  39. package/dist/core/tools/terminal-render.js +2 -1
  40. package/dist/core/tools/terminal-render.js.map +1 -1
  41. package/dist/core/tools/web-search-queue.d.ts.map +1 -1
  42. package/dist/core/tools/web-search-queue.js +2 -1
  43. package/dist/core/tools/web-search-queue.js.map +1 -1
  44. package/dist/core/tools/web.d.ts.map +1 -1
  45. package/dist/core/tools/web.js +6 -5
  46. package/dist/core/tools/web.js.map +1 -1
  47. package/dist/main.d.ts.map +1 -1
  48. package/dist/main.js +25 -24
  49. package/dist/main.js.map +1 -1
  50. package/dist/modes/interactive/interactive-mode.d.ts +5 -0
  51. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  52. package/dist/modes/interactive/interactive-mode.js +32 -2
  53. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  54. package/dist/modes/print-mode.d.ts.map +1 -1
  55. package/dist/modes/print-mode.js +3 -2
  56. package/dist/modes/print-mode.js.map +1 -1
  57. package/package.json +1 -1
@@ -3,11 +3,13 @@ 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";
9
10
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
10
11
  import { attachJsonlLineReader } from "../../modes/rpc/jsonl.js";
12
+ import { log } from "../logger.js";
11
13
  import { resolveCliModel } from "../model-resolver.js";
12
14
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
13
15
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
@@ -86,20 +88,20 @@ function loadAgentsFromDir(dir, agents) {
86
88
  const content = readFileSync(join(dir, file), "utf-8");
87
89
  const parsed = parseAgentFrontmatter(content);
88
90
  if (!parsed.ok) {
89
- console.error(`[subagent] Skipping agent file ${join(dir, file)}: ${parsed.error}`);
91
+ log.warn(`[subagent] Skipping agent file ${join(dir, file)}: ${parsed.error}`);
90
92
  }
91
93
  else {
92
94
  agents.set(parsed.config.name, parsed.config);
93
95
  }
94
96
  }
95
97
  catch (err) {
96
- console.error(`[subagent] Could not read agent file ${join(dir, file)}: ${err instanceof Error ? err.message : String(err)}`);
98
+ log.warn(`[subagent] Could not read agent file ${join(dir, file)}: ${err instanceof Error ? err.message : String(err)}`);
97
99
  }
98
100
  }
99
101
  }
100
102
  catch (err) {
101
103
  if (err.code !== "ENOENT") {
102
- console.error(`[subagent] Could not read agents directory ${dir}: ${err instanceof Error ? err.message : String(err)}`);
104
+ log.warn(`[subagent] Could not read agents directory ${dir}: ${err instanceof Error ? err.message : String(err)}`);
103
105
  }
104
106
  }
105
107
  }
@@ -140,7 +142,7 @@ function findDrebBinary() {
140
142
  }
141
143
  async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentProvider, sessionDir) {
142
144
  const drebBin = findDrebBinary();
143
- console.error(`[subagent] spawn: agent=${agentConfig.name} cwd=${cwd}`);
145
+ log.debug(`[subagent] spawn: agent=${agentConfig.name} cwd=${cwd}`);
144
146
  // Validate cwd exists — spawn() throws a misleading ENOENT blaming the
145
147
  // binary when the cwd is invalid, making the real cause hard to diagnose
146
148
  if (!existsSync(cwd)) {
@@ -226,12 +228,12 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
226
228
  }
227
229
  });
228
230
  proc.stderr?.on("error", (err) => {
229
- console.error(`[subagent] stderr stream error (agent=${agentConfig.name}): ${err.message}`);
231
+ log.warn(`[subagent] stderr stream error (agent=${agentConfig.name}): ${err.message}`);
230
232
  });
231
233
  // Parse JSONL events from stdout
232
234
  if (proc.stdout) {
233
235
  proc.stdout.on("error", (err) => {
234
- console.error(`[subagent] stdout stream error (agent=${agentConfig.name}): ${err.message}`);
236
+ log.warn(`[subagent] stdout stream error (agent=${agentConfig.name}): ${err.message}`);
235
237
  });
236
238
  attachJsonlLineReader(proc.stdout, (line) => {
237
239
  if (!line.trim())
@@ -247,7 +249,7 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
247
249
  // (e.g. startup errors printed before JSONL mode begins)
248
250
  plainStdoutLines.push(line.trim());
249
251
  if (line.trim().startsWith("{")) {
250
- console.error(`[subagent] Failed to parse JSONL event: ${line.slice(0, 200)}`);
252
+ log.warn(`[subagent] Failed to parse JSONL event: ${line.slice(0, 200)}`);
251
253
  }
252
254
  return;
253
255
  }
@@ -303,7 +305,7 @@ async function spawnSubagent(agentConfig, task, cwd, signal, onProgress, parentP
303
305
  signal?.removeEventListener("abort", onAbort);
304
306
  const exitCode = code ?? 1;
305
307
  const stderr = stderrChunks.join("");
306
- console.error(`[subagent] close: agent=${agentConfig.name} exit=${exitCode} messages=${collectedMessages.length}${exitCode !== 0 ? ` stderr=${stderr.slice(0, 200)} stdout=${plainStdoutLines.join("|").slice(0, 200)}` : ""}`);
308
+ log.debug(`[subagent] close: agent=${agentConfig.name} exit=${exitCode} messages=${collectedMessages.length}${exitCode !== 0 ? ` stderr=${stderr.slice(0, 200)} stdout=${plainStdoutLines.join("|").slice(0, 200)}` : ""}`);
307
309
  // Extract final text output from collected assistant messages
308
310
  const outputParts = [];
309
311
  for (const msg of collectedMessages) {
@@ -374,12 +376,12 @@ export function discoverSessionFile(sessionDir, agentName) {
374
376
  }
375
377
  }
376
378
  if (best) {
377
- console.error(`[subagent] session file: ${best.path} (agent=${agentName})`);
379
+ log.debug(`[subagent] session file: ${best.path} (agent=${agentName})`);
378
380
  return best.path;
379
381
  }
380
382
  }
381
383
  catch (err) {
382
- console.error(`[subagent] failed to discover session file (agent=${agentName}): ${err instanceof Error ? err.message : String(err)}`);
384
+ log.warn(`[subagent] failed to discover session file (agent=${agentName}): ${err instanceof Error ? err.message : String(err)}`);
383
385
  }
384
386
  return undefined;
385
387
  }
@@ -458,6 +460,173 @@ export function resolveModelStringSingle(modelStr, parentProvider, registry) {
458
460
  }
459
461
  return { ok: true, modelId: resolved.model.id, provider: resolved.model.provider };
460
462
  }
463
+ function compactErrorReason(reason) {
464
+ const singleLine = reason.replace(/\s+/g, " ").trim();
465
+ return singleLine.length > 180 ? `${singleLine.slice(0, 177)}...` : singleLine || "unknown error";
466
+ }
467
+ function reasonFromRuntimeError(value) {
468
+ if (value instanceof Error)
469
+ return value.message;
470
+ if (typeof value === "string")
471
+ return value;
472
+ if (value && typeof value === "object") {
473
+ const maybeMessage = value;
474
+ if (typeof maybeMessage.errorMessage === "string")
475
+ return maybeMessage.errorMessage;
476
+ if (typeof maybeMessage.message === "string")
477
+ return maybeMessage.message;
478
+ }
479
+ return String(value);
480
+ }
481
+ export function isRuntimeUnavailableError(value) {
482
+ if (value instanceof Error || typeof value === "string")
483
+ return true;
484
+ if (value && typeof value === "object") {
485
+ const maybeMessage = value;
486
+ return maybeMessage.stopReason === "error" || maybeMessage.stopReason === "aborted";
487
+ }
488
+ return false;
489
+ }
490
+ function makeProbeSignal(parentSignal, timeoutMs) {
491
+ const controller = new AbortController();
492
+ const timeoutError = new Error(`Model availability probe timed out after ${timeoutMs}ms`);
493
+ let timeout;
494
+ const timeoutPromise = new Promise((_, reject) => {
495
+ timeout = setTimeout(() => {
496
+ controller.abort(timeoutError);
497
+ reject(timeoutError);
498
+ }, timeoutMs);
499
+ });
500
+ const parentAbortHandler = () => controller.abort(parentSignal?.reason);
501
+ parentSignal?.addEventListener("abort", parentAbortHandler, { once: true });
502
+ if (parentSignal?.aborted)
503
+ controller.abort(parentSignal.reason);
504
+ return {
505
+ signal: controller.signal,
506
+ timeoutPromise,
507
+ cleanup: () => {
508
+ clearTimeout(timeout);
509
+ parentSignal?.removeEventListener("abort", parentAbortHandler);
510
+ },
511
+ };
512
+ }
513
+ export async function probeModelAvailability(model, options = {}) {
514
+ const { signal, registry, timeoutMs = 10_000 } = options;
515
+ if (signal?.aborted)
516
+ return { ok: false, reason: "Aborted before spawn", aborted: true };
517
+ const probeSignal = makeProbeSignal(signal, timeoutMs);
518
+ try {
519
+ const context = {
520
+ systemPrompt: "You are a model availability probe. Reply briefly.",
521
+ messages: [{ role: "user", content: "hi", timestamp: Date.now() }],
522
+ };
523
+ const apiKey = await Promise.race([
524
+ registry ? registry.getApiKey(model) : Promise.resolve(undefined),
525
+ probeSignal.timeoutPromise,
526
+ ]);
527
+ if (signal?.aborted)
528
+ return { ok: false, reason: "Aborted before spawn", aborted: true };
529
+ const result = await Promise.race([
530
+ complete(model, context, {
531
+ apiKey,
532
+ maxRetryDelayMs: 0,
533
+ maxTokens: 1,
534
+ signal: probeSignal.signal,
535
+ }),
536
+ probeSignal.timeoutPromise,
537
+ ]);
538
+ if (signal?.aborted)
539
+ return { ok: false, reason: "Aborted before spawn", aborted: true };
540
+ if (isRuntimeUnavailableError(result)) {
541
+ return { ok: false, reason: compactErrorReason(reasonFromRuntimeError(result)) };
542
+ }
543
+ return { ok: true };
544
+ }
545
+ catch (err) {
546
+ if (signal?.aborted)
547
+ return { ok: false, reason: "Aborted before spawn", aborted: true };
548
+ return { ok: false, reason: compactErrorReason(reasonFromRuntimeError(err)) };
549
+ }
550
+ finally {
551
+ probeSignal.cleanup();
552
+ }
553
+ }
554
+ export async function resolveModelForSubagentSpawn(models, parentProvider, registry, parentModel, signal) {
555
+ if (signal?.aborted)
556
+ return { ok: false, error: "Aborted before spawn", skippedModels: [] };
557
+ // Runtime probing only applies to agent definition fallback lists. Single
558
+ // models, per-invocation overrides, and registry-less environments keep the
559
+ // existing spawn-time resolution behavior exactly.
560
+ if (!Array.isArray(models) || !registry) {
561
+ const resolved = resolveModelWithFallbacks(models, parentProvider, registry, parentModel);
562
+ return { ...resolved, skippedModels: [] };
563
+ }
564
+ const skippedModels = [];
565
+ let lastError = "";
566
+ for (const modelStr of models) {
567
+ if (signal?.aborted)
568
+ return { ok: false, error: "Aborted before spawn", skippedModels };
569
+ const resolved = resolveModelStringSingle(modelStr, parentProvider, registry);
570
+ if (!resolved.ok) {
571
+ lastError = resolved.error;
572
+ const reason = compactErrorReason(resolved.error);
573
+ skippedModels.push({ model: modelStr, reason });
574
+ log.warn(`[subagent] Model "${modelStr}" unavailable (${reason}). Trying next fallback...`);
575
+ continue;
576
+ }
577
+ const modelObj = resolved.provider ? registry.find(resolved.provider, resolved.modelId) : undefined;
578
+ if (modelObj) {
579
+ const probe = await probeModelAvailability(modelObj, { signal, registry });
580
+ if (!probe.ok && probe.aborted) {
581
+ return { ok: false, error: "Aborted before spawn", skippedModels };
582
+ }
583
+ if (signal?.aborted)
584
+ return { ok: false, error: "Aborted before spawn", skippedModels };
585
+ if (!probe.ok) {
586
+ lastError = probe.reason;
587
+ skippedModels.push({ model: modelStr, reason: probe.reason });
588
+ log.warn(`[subagent] Model "${modelStr}" failed probe (${probe.reason}). Trying next fallback...`);
589
+ continue;
590
+ }
591
+ }
592
+ log.debug(`[subagent] Using model "${resolved.modelId}" for subagent.`);
593
+ return { ...resolved, skippedModels };
594
+ }
595
+ if (signal?.aborted)
596
+ return { ok: false, error: "Aborted before spawn", skippedModels };
597
+ if (parentModel) {
598
+ const parentResolved = resolveModelStringSingle(parentModel, parentProvider, registry);
599
+ if (parentResolved.ok) {
600
+ const warning = `Agent preferred models were unavailable. Falling back to parent model "${parentResolved.modelId}".`;
601
+ log.warn(`[subagent] ${warning}`);
602
+ return { ...parentResolved, warning, skippedModels };
603
+ }
604
+ lastError = parentResolved.error;
605
+ }
606
+ return {
607
+ ok: false,
608
+ skippedModels,
609
+ error: `None of the fallback models passed availability checks: ${[
610
+ ...models,
611
+ ...(parentModel ? [parentModel] : []),
612
+ ].join(", ")}. Last error: ${lastError || "all probes failed"}`,
613
+ };
614
+ }
615
+ export function formatModelFallbackSummary(skippedModels, selectedModel) {
616
+ if (skippedModels.length === 0)
617
+ return undefined;
618
+ const skipped = skippedModels.map((s) => `- ${s.model}: ${s.reason}`).join("\n");
619
+ return `[MODEL FALLBACK: skipped ${skippedModels.length} unavailable model(s); using "${selectedModel ?? "unknown"}".]\n${skipped}`;
620
+ }
621
+ export function prependModelFallbackSummary(output, skippedModels, selectedModel) {
622
+ const fallbackSummary = formatModelFallbackSummary(skippedModels, selectedModel);
623
+ return fallbackSummary ? `${fallbackSummary}\n\n${output}` : output;
624
+ }
625
+ function formatSkippedModelFailureDetails(skippedModels) {
626
+ if (skippedModels.length === 0)
627
+ return undefined;
628
+ return `Skipped models:\n${skippedModels.map((s) => `- ${s.model}: ${s.reason}`).join("\n")}`;
629
+ }
461
630
  const MAX_PARALLEL_TASKS = 8;
462
631
  const MAX_CONCURRENCY = 4;
463
632
  const MAX_TASK_LENGTH = 32_768; // 32 KB — prevent E2BIG from oversized argv
@@ -499,7 +668,7 @@ function clampCwd(defaultCwd, itemCwd) {
499
668
  }
500
669
  return { ok: true, cwd: resolved };
501
670
  }
502
- async function executeSingle(agents, agentName, task, cwd, signal, onProgress, modelOverride, parentProvider, registry, sessionDir, parentModel) {
671
+ export async function executeSingle(agents, agentName, task, cwd, signal, onProgress, modelOverride, parentProvider, registry, sessionDir, parentModel) {
503
672
  const name = agentName || DEFAULT_AGENT;
504
673
  const config = agents.get(name);
505
674
  if (!config) {
@@ -529,20 +698,25 @@ async function executeSingle(agents, agentName, task, cwd, signal, onProgress, m
529
698
  let effectiveConfig = modelOverride ? { ...config, model: modelOverride } : config;
530
699
  let resolvedProvider = parentProvider;
531
700
  let warning;
701
+ let skippedModels = [];
532
702
  // Resolve and validate the model against the registry before spawning.
533
703
  // This catches typos and invalid model names immediately instead of failing
534
704
  // silently in the child process. Also passes the canonical model ID to the
535
- // child, avoiding fuzzy matching entirely.
705
+ // child, avoiding fuzzy matching entirely. Agent definition fallback lists get
706
+ // an additional best-effort 1-token probe before spawn so runtime-unavailable
707
+ // models are skipped before committing to a child process.
536
708
  if (modelSpec) {
537
- const resolved = resolveModelWithFallbacks(modelSpec, parentProvider, registry, parentModel);
709
+ const resolved = await resolveModelForSubagentSpawn(modelSpec, parentProvider, registry, parentModel, signal);
710
+ skippedModels = resolved.skippedModels;
538
711
  if (!resolved.ok) {
712
+ const skippedDetails = formatSkippedModelFailureDetails(skippedModels);
539
713
  return {
540
714
  agent: name,
541
715
  task,
542
716
  exitCode: 1,
543
717
  output: "",
544
718
  stderr: "",
545
- errorMessage: resolved.error,
719
+ errorMessage: skippedDetails ? `${resolved.error}\n\n${skippedDetails}` : resolved.error,
546
720
  };
547
721
  }
548
722
  effectiveConfig = { ...effectiveConfig, model: resolved.modelId };
@@ -553,6 +727,7 @@ async function executeSingle(agents, agentName, task, cwd, signal, onProgress, m
553
727
  }
554
728
  onProgress?.(`Running ${name} agent...`);
555
729
  const result = await spawnSubagent(effectiveConfig, task, cwd, signal, onProgress, resolvedProvider, sessionDir);
730
+ result.output = prependModelFallbackSummary(result.output, skippedModels, result.model ?? effectiveConfig.model?.toString());
556
731
  if (warning) {
557
732
  result.output = `[WARNING: ${warning}]\n\n${result.output}`;
558
733
  }
@@ -845,7 +1020,7 @@ export function createSubagentToolDefinition(cwd, options) {
845
1020
  onBackgroundComplete(agentId, result, bgSignal.aborted);
846
1021
  }
847
1022
  catch (err) {
848
- console.error(`[subagent] onBackgroundComplete threw for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}. Background result lost.`);
1023
+ log.warn(`[subagent] onBackgroundComplete threw for agent ${agentId}: ${err instanceof Error ? err.message : String(err)}. Background result lost.`);
849
1024
  }
850
1025
  };
851
1026
  const run = async () => {
@@ -877,7 +1052,7 @@ export function createSubagentToolDefinition(cwd, options) {
877
1052
  }
878
1053
  };
879
1054
  run().catch((err) => {
880
- console.error(`[subagent] Unhandled background error (${agentId}): ${err instanceof Error ? err.message : String(err)}`);
1055
+ log.warn(`[subagent] Unhandled background error (${agentId}): ${err instanceof Error ? err.message : String(err)}`);
881
1056
  const entry = backgroundAgentRegistry.get(agentId);
882
1057
  if (entry && entry.status === "running")
883
1058
  entry.status = "failed";
@@ -893,7 +1068,7 @@ export function createSubagentToolDefinition(cwd, options) {
893
1068
  }, bgSignal.aborted);
894
1069
  }
895
1070
  catch (notifyErr) {
896
- console.error(`[subagent] CRITICAL: Last-resort notification failed for ${agentId}: ${notifyErr instanceof Error ? notifyErr.message : String(notifyErr)}`);
1071
+ log.error(`[subagent] CRITICAL: Last-resort notification failed for ${agentId}: ${notifyErr instanceof Error ? notifyErr.message : String(notifyErr)}`);
897
1072
  }
898
1073
  });
899
1074
  return agentId;