@wrongstack/core 0.299.0 → 0.300.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 (50) hide show
  1. package/dist/coordination/director.d.ts +8 -0
  2. package/dist/coordination/fleet-manager.d.ts +48 -3
  3. package/dist/coordination/ifleet-manager.d.ts +2 -0
  4. package/dist/coordination/index.js +120 -20
  5. package/dist/coordination/multi-agent-coordinator.d.ts +1 -0
  6. package/dist/core/fallback-model.d.ts +48 -0
  7. package/dist/core/index.d.ts +3 -2
  8. package/dist/core/index.js +226 -26
  9. package/dist/core/instruction-template.d.ts +80 -0
  10. package/dist/core/system-prompt-blocks.d.ts +10 -1
  11. package/dist/core/system-prompt-builder.d.ts +35 -1
  12. package/dist/defaults/index.js +238 -99
  13. package/dist/execution/autonomy-brain.d.ts +7 -0
  14. package/dist/execution/council-brain.d.ts +11 -0
  15. package/dist/execution/council-orchestrator.d.ts +23 -4
  16. package/dist/execution/council-prompts.d.ts +12 -1
  17. package/dist/execution/index.js +355 -138
  18. package/dist/fleet-notifier.d.ts +9 -2
  19. package/dist/hooks/index.js +8 -4
  20. package/dist/hq/index.js +18 -4
  21. package/dist/hq/protocol/fleet.d.ts +20 -0
  22. package/dist/hq/protocol.js +10 -0
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1512 -707
  25. package/dist/kernel/events/brain-events.d.ts +9 -0
  26. package/dist/kernel/events/provider-events.d.ts +42 -1
  27. package/dist/models/index.js +1 -1
  28. package/dist/plugin/api.d.ts +6 -0
  29. package/dist/plugin/config.d.ts +55 -0
  30. package/dist/plugin/index.d.ts +1 -1
  31. package/dist/plugin/index.js +134 -21
  32. package/dist/security/index.d.ts +1 -1
  33. package/dist/security/index.js +157 -42
  34. package/dist/security/permission-helpers.d.ts +23 -6
  35. package/dist/security/permission-policy.d.ts +16 -0
  36. package/dist/security/totp.d.ts +14 -0
  37. package/dist/storage/director-state.d.ts +7 -0
  38. package/dist/storage/index.js +33 -8
  39. package/dist/tools/fallback-system-config-view-tool.d.ts +1 -1
  40. package/dist/tools/index.js +388 -102
  41. package/dist/types/council.d.ts +11 -0
  42. package/dist/types/index.d.ts +1 -1
  43. package/dist/types/multi-agent.d.ts +10 -0
  44. package/dist/types/one-shot-llm.d.ts +9 -0
  45. package/dist/types/plugin.d.ts +28 -0
  46. package/dist/worktree/index.js +4 -4
  47. package/instructions/system-lite.md +81 -3
  48. package/instructions/system-pro.md +275 -90
  49. package/instructions/system.md +228 -81
  50. package/package.json +3 -3
@@ -3979,7 +3979,11 @@ async function completeBrainLlm(target, input) {
3979
3979
  return (await completeBrainLlmDetailed(target, input)).text;
3980
3980
  }
3981
3981
  async function completeBrainLlmDetailed(target, input) {
3982
- const signal = AbortSignal.timeout(input.timeoutMs);
3982
+ if (input.signal?.aborted) {
3983
+ throw new DOMException("Brain call aborted before it started.", "AbortError");
3984
+ }
3985
+ const timeoutSignal = AbortSignal.timeout(input.timeoutMs);
3986
+ const signal = input.signal ? AbortSignal.any([input.signal, timeoutSignal]) : timeoutSignal;
3983
3987
  const response = await target.provider.complete(
3984
3988
  {
3985
3989
  model: target.model,
@@ -5174,22 +5178,29 @@ function buildCouncilJudgeUserPrompt(question, votes, opts = {}) {
5174
5178
  "</council-ballots>"
5175
5179
  ].filter(Boolean).join("\n\n");
5176
5180
  }
5177
- function normalizeOptions(options) {
5178
- if (!options) return [];
5181
+ function validateCouncilOptions(options) {
5182
+ const errors = [];
5179
5183
  const seen = /* @__PURE__ */ new Set();
5180
- return options.map((option) => {
5184
+ for (const option of options ?? []) {
5181
5185
  const id = option.id.trim();
5182
- const label = option.label.trim();
5183
- if (!id) throw new Error("buildCouncilQuestionPrompt: option id must not be empty.");
5184
- if (!label) throw new Error(`buildCouncilQuestionPrompt: option "${id}" needs a label.`);
5185
- if (seen.has(id)) throw new Error(`buildCouncilQuestionPrompt: duplicate option id "${id}".`);
5186
+ if (!id) errors.push("Every option must have a non-empty `id`.");
5187
+ if (!option.label.trim()) errors.push(`Option "${id || "<empty>"}" must have a label.`);
5188
+ if (seen.has(id)) errors.push(`Duplicate option id "${id}".`);
5186
5189
  seen.add(id);
5187
- return {
5188
- id,
5189
- label,
5190
- ...option.consequence?.trim() ? { consequence: option.consequence.trim() } : {}
5191
- };
5192
- });
5190
+ }
5191
+ return errors;
5192
+ }
5193
+ function normalizeOptions(options) {
5194
+ if (!options) return [];
5195
+ const errors = validateCouncilOptions(options);
5196
+ if (errors.length > 0) {
5197
+ throw new Error(`buildCouncilQuestionPrompt: ${errors.join("; ")}`);
5198
+ }
5199
+ return options.map((option) => ({
5200
+ id: option.id.trim(),
5201
+ label: option.label.trim(),
5202
+ ...option.consequence?.trim() ? { consequence: option.consequence.trim() } : {}
5203
+ }));
5193
5204
  }
5194
5205
  function requiredInstruction(path17) {
5195
5206
  const text = readBundledInstructionText(path17);
@@ -5297,6 +5308,8 @@ function requireFraction(value, label) {
5297
5308
  var COUNCIL_REFUSAL_OPTION_ID = "council_refuse";
5298
5309
  var DEFAULT_COUNCIL_MAX_CONCURRENCY = 3;
5299
5310
  var MAX_COUNCIL_CONCURRENCY = 8;
5311
+ var OVERALL_TIMEOUT_REASON = "Council overall timeout exceeded.";
5312
+ var CALL_CANCELLED_REASON = "Cancelled.";
5300
5313
  var CouncilOrchestrator = class {
5301
5314
  caller;
5302
5315
  personas;
@@ -5307,6 +5320,17 @@ var CouncilOrchestrator = class {
5307
5320
  fallbackProfileManager;
5308
5321
  seatCaller;
5309
5322
  judgeCaller;
5323
+ /**
5324
+ * Normalized ad-hoc profiles keyed by the caller's config object identity.
5325
+ * The Brain adapter reuses ONE profile object for every decision, so this
5326
+ * avoids re-validating + re-freezing it on every ask() without caching
5327
+ * string-keyed registry lookups (those are already O(1)).
5328
+ *
5329
+ * Hosts must treat ad-hoc profile configs as IMMUTABLE once passed to
5330
+ * ask(): the cache is keyed by object identity and never invalidated, so
5331
+ * mutating a cached profile would silently serve the first snapshot.
5332
+ */
5333
+ profileCache = /* @__PURE__ */ new WeakMap();
5310
5334
  constructor(opts) {
5311
5335
  if (!opts.caller && !opts.seatCaller && !opts.judgeCaller) {
5312
5336
  throw new Error(
@@ -5325,13 +5349,33 @@ var CouncilOrchestrator = class {
5325
5349
  this.seatCaller = opts.seatCaller;
5326
5350
  this.judgeCaller = opts.judgeCaller;
5327
5351
  }
5328
- async ask(question) {
5329
- const startedAt = Date.now();
5330
- const profile = resolveCouncilProfile(question.profile, {
5352
+ /**
5353
+ * Resolve the effective profile for a question. String ids and the default
5354
+ * go through the registry (already O(1)); ad-hoc config objects are
5355
+ * normalized once per stable object identity and cached, because hosts such
5356
+ * as the Brain adapter pass the same profile object on every ask().
5357
+ */
5358
+ resolveProfile(profile) {
5359
+ if (typeof profile === "string" || profile === void 0) {
5360
+ return resolveCouncilProfile(profile, {
5361
+ registry: this.profiles,
5362
+ personas: this.personas,
5363
+ defaultProfile: this.defaultProfile
5364
+ });
5365
+ }
5366
+ const cached = this.profileCache.get(profile);
5367
+ if (cached) return cached;
5368
+ const resolved = resolveCouncilProfile(profile, {
5331
5369
  registry: this.profiles,
5332
5370
  personas: this.personas,
5333
5371
  defaultProfile: this.defaultProfile
5334
5372
  });
5373
+ this.profileCache.set(profile, resolved);
5374
+ return resolved;
5375
+ }
5376
+ async ask(question) {
5377
+ const startedAt = Date.now();
5378
+ const profile = this.resolveProfile(question.profile);
5335
5379
  validateRefusalCollision(question, this.refusalOptionId);
5336
5380
  const timeoutSignal = AbortSignal.timeout(profile.overallTimeoutMs);
5337
5381
  const signal = question.signal ? AbortSignal.any([question.signal, timeoutSignal]) : timeoutSignal;
@@ -5348,11 +5392,16 @@ var CouncilOrchestrator = class {
5348
5392
  try {
5349
5393
  return await this.callSeat(question, profile, seat, i, signal, usage);
5350
5394
  } catch (error) {
5395
+ const timedOut = signal.aborted && !question.signal?.aborted;
5351
5396
  return {
5352
5397
  seatId: seat.id,
5353
5398
  persona: seat.persona,
5354
- status: signal.aborted ? "cancelled" : "failed",
5355
- error: errorMessage(error)
5399
+ // Only the caller's own cancel is a "cancelled" vote; the overall
5400
+ // budget expiring is a failure (timeout), matching the envelope.
5401
+ status: question.signal?.aborted ? "cancelled" : "failed",
5402
+ // Canonical text for aborted-by-budget or cancelled seats, so one
5403
+ // event does not surface a different string per code path.
5404
+ error: question.signal?.aborted ? CALL_CANCELLED_REASON : timedOut ? OVERALL_TIMEOUT_REASON : errorMessage(error)
5356
5405
  };
5357
5406
  }
5358
5407
  }
@@ -5362,7 +5411,7 @@ var CouncilOrchestrator = class {
5362
5411
  if (question.signal?.aborted) {
5363
5412
  return resultEnvelope({
5364
5413
  status: "cancelled",
5365
- reason: "Council call cancelled.",
5414
+ reason: CALL_CANCELLED_REASON,
5366
5415
  resolution: "none",
5367
5416
  votes,
5368
5417
  profile,
@@ -5375,14 +5424,17 @@ var CouncilOrchestrator = class {
5375
5424
  if (timeoutSignal.aborted) {
5376
5425
  return resultEnvelope({
5377
5426
  status: "failed",
5378
- reason: "Council overall timeout exceeded.",
5427
+ reason: OVERALL_TIMEOUT_REASON,
5379
5428
  resolution: "none",
5380
5429
  votes,
5381
5430
  profile,
5382
5431
  usage,
5383
5432
  startedAt,
5384
5433
  warnings,
5385
- errors: [...errors, "Council overall timeout exceeded."]
5434
+ // Seat-prefixed errors normally carry the canonical timeout text, but
5435
+ // a signal-blind caller can resolve valid votes even after the budget
5436
+ // expired — append the standalone entry only when nothing carries it.
5437
+ errors: errors.some((entry) => entry.includes(OVERALL_TIMEOUT_REASON)) ? errors : [...errors, OVERALL_TIMEOUT_REASON]
5386
5438
  });
5387
5439
  }
5388
5440
  if (!question.options || question.options.length === 0) {
@@ -5409,7 +5461,17 @@ var CouncilOrchestrator = class {
5409
5461
  );
5410
5462
  }
5411
5463
  async callSeat(question, profile, seat, seatIndex, signal, usage) {
5412
- if (signal.aborted) return cancelledVote(seat);
5464
+ if (signal.aborted) {
5465
+ return question.signal?.aborted ? cancelledVote(seat) : {
5466
+ seatId: seat.id,
5467
+ persona: seat.persona,
5468
+ status: "failed",
5469
+ ...seat.target?.providerId ? { provider: seat.target.providerId } : {},
5470
+ ...seat.target?.model ? { model: seat.target.model } : {},
5471
+ durationMs: 0,
5472
+ error: OVERALL_TIMEOUT_REASON
5473
+ };
5474
+ }
5413
5475
  let persona;
5414
5476
  try {
5415
5477
  persona = this.personas.require(seat.persona);
@@ -5435,12 +5497,15 @@ var CouncilOrchestrator = class {
5435
5497
  });
5436
5498
  const metadata = callMetadata(result);
5437
5499
  if (result.error) {
5500
+ const timedOut = signal.aborted && !question.signal?.aborted;
5438
5501
  return {
5439
5502
  seatId: seat.id,
5440
5503
  persona: seat.persona,
5441
- status: signal.aborted ? "cancelled" : "failed",
5504
+ status: question.signal?.aborted ? "cancelled" : "failed",
5442
5505
  ...metadata,
5443
- error: result.error
5506
+ // Canonical text for cancelled or aborted-by-budget seats, so one
5507
+ // cancel/timeout event does not surface a raw provider string.
5508
+ error: question.signal?.aborted ? CALL_CANCELLED_REASON : timedOut ? OVERALL_TIMEOUT_REASON : result.error
5444
5509
  };
5445
5510
  }
5446
5511
  const parsed = parseVote(result.text, question, this.refusalOptionId);
@@ -5539,9 +5604,27 @@ var CouncilOrchestrator = class {
5539
5604
  signal,
5540
5605
  usage
5541
5606
  );
5607
+ if (signal.aborted) {
5608
+ const cancelled = question.signal?.aborted === true;
5609
+ const reason = cancelled ? CALL_CANCELLED_REASON : OVERALL_TIMEOUT_REASON;
5610
+ return resultEnvelope({
5611
+ status: cancelled ? "cancelled" : "failed",
5612
+ reason,
5613
+ resolution: "none",
5614
+ votes,
5615
+ profile,
5616
+ usage,
5617
+ startedAt,
5618
+ warnings,
5619
+ errors: errors.some((entry) => entry.includes(reason)) ? errors : [...errors, reason],
5620
+ judgeUsed: true
5621
+ });
5622
+ }
5542
5623
  if (!judged.ok) {
5543
5624
  return resultEnvelope({
5544
- status: signal.aborted ? "cancelled" : "abstained",
5625
+ // User cancel -> cancelled; overall budget expired mid-judge -> failed;
5626
+ // otherwise the judge simply failed -> abstained (can't decide).
5627
+ status: question.signal?.aborted ? "cancelled" : signal.aborted ? "failed" : "abstained",
5545
5628
  reason: judged.error,
5546
5629
  resolution: "none",
5547
5630
  votes,
@@ -5601,6 +5684,19 @@ var CouncilOrchestrator = class {
5601
5684
  });
5602
5685
  }
5603
5686
  if (!profile.judge) {
5687
+ if (divergentStances(valid)) {
5688
+ return resultEnvelope({
5689
+ status: "abstained",
5690
+ reason: "Council produced multiple distinct stances and has no judge to reconcile them.",
5691
+ resolution: "none",
5692
+ votes,
5693
+ profile,
5694
+ usage,
5695
+ startedAt,
5696
+ warnings,
5697
+ errors
5698
+ });
5699
+ }
5604
5700
  const first = valid[0];
5605
5701
  if (!first) {
5606
5702
  return resultEnvelope({
@@ -5637,9 +5733,30 @@ var CouncilOrchestrator = class {
5637
5733
  signal,
5638
5734
  usage
5639
5735
  );
5736
+ if (signal.aborted) {
5737
+ const cancelled = question.signal?.aborted === true;
5738
+ const reason = cancelled ? CALL_CANCELLED_REASON : OVERALL_TIMEOUT_REASON;
5739
+ return resultEnvelope({
5740
+ status: cancelled ? "cancelled" : "failed",
5741
+ reason,
5742
+ resolution: "none",
5743
+ votes,
5744
+ profile,
5745
+ usage,
5746
+ startedAt,
5747
+ warnings,
5748
+ errors: errors.some((entry) => entry.includes(reason)) ? errors : [...errors, reason],
5749
+ judgeUsed: true
5750
+ });
5751
+ }
5640
5752
  if (!judged.ok) {
5641
5753
  return resultEnvelope({
5642
- status: signal.aborted ? "cancelled" : "failed",
5754
+ // User cancel -> cancelled; overall budget expired mid-judge -> failed;
5755
+ // otherwise the judge simply failed -> failed (open questions cannot
5756
+ // abstain for a judge failure — 'abstained' is reserved for quorum
5757
+ // failure and stance divergence; the option path maps this same
5758
+ // plain-judge-failure case to 'abstained' instead).
5759
+ status: question.signal?.aborted ? "cancelled" : "failed",
5643
5760
  reason: judged.error,
5644
5761
  resolution: "none",
5645
5762
  votes,
@@ -5678,18 +5795,28 @@ var CouncilOrchestrator = class {
5678
5795
  signal,
5679
5796
  usage
5680
5797
  });
5681
- if (result.error) return { ok: false, error: result.error };
5798
+ if (result.error) {
5799
+ if (signal.aborted && !question.signal?.aborted) {
5800
+ return { ok: false, error: OVERALL_TIMEOUT_REASON };
5801
+ }
5802
+ if (question.signal?.aborted) {
5803
+ return { ok: false, error: CALL_CANCELLED_REASON };
5804
+ }
5805
+ return { ok: false, error: result.error };
5806
+ }
5682
5807
  return parseJudge(result.text, question, this.refusalOptionId);
5683
5808
  }
5684
5809
  /**
5685
- * Resolve the effective LLM caller for a call. Voter seats use
5686
- * `seatCaller(seatIndex)` when wired. Judge seats (seatIndex undefined)
5687
- * use `judgeCaller` if set, otherwise `seatCaller(0)` if set, otherwise
5688
- * the shared `caller`.
5810
+ * Resolve the effective LLM caller for a call. Voter seats (defined
5811
+ * seatIndex) use `seatCaller(seatIndex)` when wired, otherwise the shared
5812
+ * `caller` a seat never falls through to the judge caller. Judge seats
5813
+ * (seatIndex undefined) use `judgeCaller` if set, otherwise `seatCaller(0)`
5814
+ * if set, otherwise the shared `caller`.
5689
5815
  */
5690
5816
  resolveCaller(seatIndex) {
5691
- if (seatIndex !== void 0 && this.seatCaller) {
5692
- return this.seatCaller(seatIndex);
5817
+ if (seatIndex !== void 0) {
5818
+ if (this.seatCaller) return this.seatCaller(seatIndex);
5819
+ return this.caller ?? this.judgeCaller;
5693
5820
  }
5694
5821
  if (this.judgeCaller) return this.judgeCaller;
5695
5822
  if (this.seatCaller) return this.seatCaller(0);
@@ -5698,6 +5825,7 @@ var CouncilOrchestrator = class {
5698
5825
  async safeCall(input) {
5699
5826
  const effectiveCaller = this.resolveCaller(input.seatIndex);
5700
5827
  const resolvedTarget = this.resolveCouncilTarget(input.target);
5828
+ const startedAt = Date.now();
5701
5829
  try {
5702
5830
  const result = await effectiveCaller.call({
5703
5831
  system: input.system,
@@ -5714,8 +5842,17 @@ var CouncilOrchestrator = class {
5714
5842
  addUsage(input.usage, result);
5715
5843
  return result;
5716
5844
  } catch (error) {
5717
- input.usage.calls += 1;
5718
- return emptyCallResult(errorMessage(error));
5845
+ const failed = {
5846
+ text: "",
5847
+ model: resolvedTarget?.model ?? "",
5848
+ provider: resolvedTarget?.providerId ?? "",
5849
+ tokens: { input: 0, output: 0, total: 0 },
5850
+ durationMs: Math.max(0, Date.now() - startedAt),
5851
+ fromFallback: false,
5852
+ error: errorMessage(error)
5853
+ };
5854
+ addUsage(input.usage, failed);
5855
+ return failed;
5719
5856
  }
5720
5857
  }
5721
5858
  /**
@@ -5747,47 +5884,77 @@ var CouncilOrchestrator = class {
5747
5884
  };
5748
5885
  }
5749
5886
  };
5750
- function parseVote(text, question, refusalOptionId) {
5887
+ function parseCouncilResponse(text, question, refusalOptionId, opts) {
5888
+ const roleLabel = opts.role === "judge" ? "Judge" : "Voter";
5751
5889
  const parsed = parseObject(text);
5752
5890
  if (!parsed.ok && (!question.options || question.options.length === 0)) {
5753
5891
  const fallback = text.trim();
5754
- if (fallback) return { ok: true, vote: { stance: fallback } };
5755
- return { ok: false, error: "Voter returned an empty response." };
5892
+ if (fallback) return { ok: true, value: { [opts.freeTextField]: fallback } };
5893
+ return { ok: false, error: `${roleLabel} returned an empty response.` };
5756
5894
  }
5757
5895
  if (!parsed.ok) return parsed;
5758
5896
  const rationale = optionalString(parsed.value["rationale"]);
5759
5897
  if (question.options && question.options.length > 0) {
5760
5898
  const optionId = optionalString(parsed.value["optionId"]);
5761
- const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
5899
+ const allowed = /* @__PURE__ */ new Set([
5900
+ ...question.options.map((option) => option.id.trim()),
5901
+ refusalOptionId
5902
+ ]);
5762
5903
  if (!optionId || !allowed.has(optionId)) {
5763
- return { ok: false, error: "Voter returned an unknown or missing optionId." };
5904
+ return { ok: false, error: `${roleLabel} returned an unknown or missing optionId.` };
5764
5905
  }
5765
- return { ok: true, vote: { optionId, ...rationale ? { rationale } : {} } };
5906
+ return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
5907
+ }
5908
+ const freeText = optionalString(parsed.value[opts.freeTextField]);
5909
+ if (!freeText) {
5910
+ return {
5911
+ ok: false,
5912
+ error: `${roleLabel} returned an empty or missing ${opts.freeTextField}.`
5913
+ };
5766
5914
  }
5767
- const stance = optionalString(parsed.value["stance"]);
5768
- if (!stance) return { ok: false, error: "Voter returned an empty or missing stance." };
5769
- return { ok: true, vote: { stance, ...rationale ? { rationale } : {} } };
5915
+ return { ok: true, value: { [opts.freeTextField]: freeText, ...rationale ? { rationale } : {} } };
5770
5916
  }
5771
- function parseJudge(text, question, refusalOptionId) {
5772
- const parsed = parseObject(text);
5773
- if (!parsed.ok && (!question.options || question.options.length === 0)) {
5774
- const fallback = text.trim();
5775
- if (fallback) return { ok: true, value: { answer: fallback } };
5776
- return { ok: false, error: "Judge returned an empty response." };
5917
+ function divergentStances(valid) {
5918
+ const seen = /* @__PURE__ */ new Set();
5919
+ for (const vote of valid) {
5920
+ const normalized = vote.stance.trim().replace(/^["'`]+|["'`]+$/g, "").toLowerCase().replace(/[.!?;:,]+$/g, "").replace(/\s+/g, " ").trim();
5921
+ if (!normalized) return true;
5922
+ seen.add(normalized);
5923
+ if (seen.size > 1) return true;
5777
5924
  }
5925
+ return false;
5926
+ }
5927
+ function parseVote(text, question, refusalOptionId) {
5928
+ const parsed = parseCouncilResponse(text, question, refusalOptionId, {
5929
+ role: "voter",
5930
+ freeTextField: "stance"
5931
+ });
5778
5932
  if (!parsed.ok) return parsed;
5779
- const rationale = optionalString(parsed.value["rationale"]);
5780
- if (question.options && question.options.length > 0) {
5781
- const optionId = optionalString(parsed.value["optionId"]);
5782
- const allowed = /* @__PURE__ */ new Set([...question.options.map((option) => option.id.trim()), refusalOptionId]);
5783
- if (!optionId || !allowed.has(optionId)) {
5784
- return { ok: false, error: "Judge returned an unknown or missing optionId." };
5933
+ const { optionId, stance, rationale } = parsed.value;
5934
+ return {
5935
+ ok: true,
5936
+ vote: {
5937
+ ...optionId ? { optionId } : {},
5938
+ ...stance ? { stance } : {},
5939
+ ...rationale ? { rationale } : {}
5785
5940
  }
5786
- return { ok: true, value: { optionId, ...rationale ? { rationale } : {} } };
5787
- }
5788
- const answer = optionalString(parsed.value["answer"]);
5789
- if (!answer) return { ok: false, error: "Judge returned an empty or missing answer." };
5790
- return { ok: true, value: { answer, ...rationale ? { rationale } : {} } };
5941
+ };
5942
+ }
5943
+ function parseJudge(text, question, refusalOptionId) {
5944
+ const parsed = parseCouncilResponse(text, question, refusalOptionId, {
5945
+ role: "judge",
5946
+ freeTextField: "answer"
5947
+ });
5948
+ if (!parsed.ok) return parsed;
5949
+ const { optionId, answer, rationale } = parsed.value;
5950
+ return {
5951
+ ok: true,
5952
+ value: {
5953
+ ...optionId ? { optionId } : {},
5954
+ ...answer ? { answer } : {},
5955
+ ...rationale ? { rationale } : {}
5956
+ }
5957
+ };
5791
5958
  }
5792
5959
  function parseObject(text) {
5793
5960
  const trimmed = text.trim();
@@ -5831,7 +5998,7 @@ function callMetadata(result) {
5831
5998
  };
5832
5999
  }
5833
6000
  function addUsage(usage, result) {
5834
- usage.calls += 1;
6001
+ usage.calls += Math.max(1, result.attempts ?? 1);
5835
6002
  usage.inputTokens += result.tokens.input;
5836
6003
  usage.outputTokens += result.tokens.output;
5837
6004
  usage.totalTokens += result.tokens.total;
@@ -5840,21 +6007,40 @@ function usageResult(usage, startedAt) {
5840
6007
  return Object.freeze({ ...usage, durationMs: Math.max(0, Date.now() - startedAt) });
5841
6008
  }
5842
6009
  function cancelledVote(seat) {
5843
- return { seatId: seat.id, persona: seat.persona, status: "cancelled", error: "Cancelled." };
6010
+ return {
6011
+ seatId: seat.id,
6012
+ persona: seat.persona,
6013
+ status: "cancelled",
6014
+ ...seat.target?.providerId ? { provider: seat.target.providerId } : {},
6015
+ ...seat.target?.model ? { model: seat.target.model } : {},
6016
+ durationMs: 0,
6017
+ error: CALL_CANCELLED_REASON
6018
+ };
5844
6019
  }
5845
6020
  function distinctTargetCount(votes, profile) {
5846
- const keys = votes.filter((vote) => vote.status === "valid").map(
5847
- (vote) => profile.distinctness === "provider" ? vote.provider : `${vote.provider ?? ""}/${vote.model ?? ""}`
5848
- ).filter(Boolean);
5849
- return new Set(keys).size;
6021
+ return new Set(distinctTargetKeys(votes, profile)).size;
6022
+ }
6023
+ function distinctTargetKeys(votes, profile) {
6024
+ const keys = [];
6025
+ for (const vote of votes) {
6026
+ if (vote.status !== "valid") continue;
6027
+ const provider = vote.provider?.trim() ?? "";
6028
+ const model = vote.model?.trim() ?? "";
6029
+ if (profile.distinctness === "provider") {
6030
+ if (provider) keys.push(provider);
6031
+ } else if (provider || model) {
6032
+ keys.push(`${provider}/${model}`);
6033
+ }
6034
+ }
6035
+ return keys;
5850
6036
  }
5851
6037
  function distinctnessWarnings(votes, profile) {
5852
6038
  if (profile.distinctness === "none") return [];
5853
- const valid = votes.filter((vote) => vote.status === "valid");
5854
- const distinct = distinctTargetCount(valid, profile);
5855
- if (valid.length > 1 && distinct < valid.length) {
6039
+ const keys = distinctTargetKeys(votes, profile);
6040
+ const distinct = new Set(keys).size;
6041
+ if (keys.length > 1 && distinct < keys.length) {
5856
6042
  return [
5857
- `Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${valid.length} valid vote(s).`
6043
+ `Council distinctness policy "${profile.distinctness}" was not met: ${distinct} distinct target(s) served ${keys.length} valid vote(s).`
5858
6044
  ];
5859
6045
  }
5860
6046
  return [];
@@ -5890,17 +6076,6 @@ async function mapConcurrent(items, concurrency, worker) {
5890
6076
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => run()));
5891
6077
  return results;
5892
6078
  }
5893
- function emptyCallResult(error) {
5894
- return {
5895
- text: "",
5896
- model: "",
5897
- provider: "",
5898
- tokens: { input: 0, output: 0, total: 0 },
5899
- durationMs: 0,
5900
- fromFallback: false,
5901
- error
5902
- };
5903
- }
5904
6079
  function optionalString(value) {
5905
6080
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
5906
6081
  }
@@ -5947,52 +6122,58 @@ function resolvePersonaRegistry(voters) {
5947
6122
  });
5948
6123
  return { registry, personaIds };
5949
6124
  }
6125
+ function makeSeatCallerForVoter(target) {
6126
+ return {
6127
+ async call(input) {
6128
+ const startedAt = Date.now();
6129
+ try {
6130
+ const result = await completeBrainLlmDetailed(
6131
+ { provider: target.provider, model: input.model ?? target.model },
6132
+ {
6133
+ system: typeof input.system === "string" ? input.system : Array.isArray(input.system) ? input.system.map((b) => b.text).join("\n") : "",
6134
+ user: input.userPrompt ?? "",
6135
+ timeoutMs: input.timeoutMs ?? 15e3,
6136
+ maxTokens: input.maxTokens,
6137
+ // Forward the orchestrator's signal (overall budget + caller
6138
+ // cancellation) so an in-flight seat call is interrupted when
6139
+ // the council times out or is cancelled instead of running to
6140
+ // its per-call timeout. Previously this was dropped and a
6141
+ // cancelled council waited for every seat to finish.
6142
+ signal: input.signal
6143
+ }
6144
+ );
6145
+ const inputTokens = result.usage?.input ?? 0;
6146
+ const outputTokens = result.usage?.output ?? 0;
6147
+ return {
6148
+ text: result.text,
6149
+ model: target.model,
6150
+ provider: target.provider.id,
6151
+ tokens: {
6152
+ input: inputTokens,
6153
+ output: outputTokens,
6154
+ total: inputTokens + outputTokens
6155
+ },
6156
+ durationMs: Date.now() - startedAt,
6157
+ fromFallback: false
6158
+ };
6159
+ } catch (error) {
6160
+ return {
6161
+ text: "",
6162
+ model: target.model,
6163
+ provider: target.provider.id,
6164
+ tokens: { input: 0, output: 0, total: 0 },
6165
+ durationMs: 0,
6166
+ fromFallback: false,
6167
+ error: error instanceof Error ? error.message : String(error)
6168
+ };
6169
+ }
6170
+ }
6171
+ };
6172
+ }
5950
6173
  function createCouncilBrainArbiter(opts) {
5951
6174
  if (opts.voters.length === 0) {
5952
6175
  throw new Error("createCouncilBrainArbiter: at least one voter is required.");
5953
6176
  }
5954
- function makeSeatCallerForVoter(target) {
5955
- return {
5956
- async call(input) {
5957
- const startedAt = Date.now();
5958
- try {
5959
- const result = await completeBrainLlmDetailed(
5960
- { provider: target.provider, model: input.model ?? target.model },
5961
- {
5962
- system: typeof input.system === "string" ? input.system : Array.isArray(input.system) ? input.system.map((b) => b.text).join("\n") : "",
5963
- user: input.userPrompt ?? "",
5964
- timeoutMs: input.timeoutMs ?? 15e3,
5965
- maxTokens: input.maxTokens
5966
- }
5967
- );
5968
- const inputTokens = result.usage?.input ?? 0;
5969
- const outputTokens = result.usage?.output ?? 0;
5970
- return {
5971
- text: result.text,
5972
- model: target.model,
5973
- provider: target.provider.id,
5974
- tokens: {
5975
- input: inputTokens,
5976
- output: outputTokens,
5977
- total: inputTokens + outputTokens
5978
- },
5979
- durationMs: Date.now() - startedAt,
5980
- fromFallback: false
5981
- };
5982
- } catch (error) {
5983
- return {
5984
- text: "",
5985
- model: target.model,
5986
- provider: target.provider.id,
5987
- tokens: { input: 0, output: 0, total: 0 },
5988
- durationMs: 0,
5989
- fromFallback: false,
5990
- error: error instanceof Error ? error.message : String(error)
5991
- };
5992
- }
5993
- }
5994
- };
5995
- }
5996
6177
  const seatCaller = (seatIndex) => {
5997
6178
  const voter = opts.voters[seatIndex];
5998
6179
  if (!voter) {
@@ -6030,13 +6211,23 @@ function createCouncilBrainArbiter(opts) {
6030
6211
  model: opts.judge.model,
6031
6212
  role: "judge"
6032
6213
  } : false;
6214
+ const perCallTimeoutMs = opts.decisionTimeoutMs ?? 15e3;
6215
+ const effectiveConcurrency = Math.max(
6216
+ 1,
6217
+ Math.min(opts.maxConcurrency ?? DEFAULT_COUNCIL_MAX_CONCURRENCY, seats.length)
6218
+ );
6219
+ const overallTimeoutMs = Math.max(
6220
+ DEFAULT_COUNCIL_OVERALL_TIMEOUT_MS,
6221
+ perCallTimeoutMs * (Math.ceil(seats.length / effectiveConcurrency) + 1)
6222
+ );
6033
6223
  const profile = {
6034
6224
  id: "brain-council-adapter",
6035
6225
  seats,
6036
6226
  judge: judgeTarget,
6037
6227
  quorumFraction: opts.quorumFraction ?? 0.5,
6038
6228
  approvalFraction: opts.approvalFraction ?? 0.5,
6039
- perCallTimeoutMs: opts.decisionTimeoutMs ?? 15e3,
6229
+ perCallTimeoutMs,
6230
+ overallTimeoutMs,
6040
6231
  ...opts.judgeMaxTokens !== void 0 ? { judgeMaxTokens: opts.judgeMaxTokens } : {},
6041
6232
  distinctness: opts.distinctness ?? "none"
6042
6233
  };
@@ -15965,8 +16156,17 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
15965
16156
  durationMs: 0
15966
16157
  };
15967
16158
  this.completedResults.push(synthetic);
16159
+ this.trimCompletedResults();
15968
16160
  this.emit("task.completed", { task, result: synthetic });
15969
16161
  }
16162
+ trimCompletedResults() {
16163
+ if (this.completedResults.length > _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS) {
16164
+ this.completedResults.splice(
16165
+ 0,
16166
+ this.completedResults.length - _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS
16167
+ );
16168
+ }
16169
+ }
15970
16170
  async runDispatched(subagentId, task) {
15971
16171
  const subagent = this.subagents.get(subagentId);
15972
16172
  if (!subagent) return;
@@ -16100,12 +16300,7 @@ var DefaultMultiAgentCoordinator = class _DefaultMultiAgentCoordinator extends E
16100
16300
  }
16101
16301
  recordCompletion(result) {
16102
16302
  this.completedResults.push(result);
16103
- if (this.completedResults.length > _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS) {
16104
- this.completedResults.splice(
16105
- 0,
16106
- this.completedResults.length - _DefaultMultiAgentCoordinator.MAX_COMPLETED_RESULTS
16107
- );
16108
- }
16303
+ this.trimCompletedResults();
16109
16304
  this.totalIterations += result.iterations;
16110
16305
  if (this.inFlight > 0) {
16111
16306
  this.inFlight--;
@@ -20411,6 +20606,7 @@ var OneShotOrchestrator = class {
20411
20606
  tokens: { input: 0, output: 0, total: 0 },
20412
20607
  durationMs: Math.round(performance.now() - startedAt),
20413
20608
  fromFallback: false,
20609
+ attempts: 0,
20414
20610
  error: "No provider or model could be resolved. Check your config."
20415
20611
  };
20416
20612
  }
@@ -20425,6 +20621,7 @@ var OneShotOrchestrator = class {
20425
20621
  tokens: { input: 0, output: 0, total: 0 },
20426
20622
  durationMs: Math.round(performance.now() - startedAt),
20427
20623
  fromFallback: false,
20624
+ attempts: 0,
20428
20625
  error: `Cannot build provider "${target.providerId}": ${err instanceof Error ? err.message : String(err)}`
20429
20626
  };
20430
20627
  }
@@ -20437,11 +20634,13 @@ var OneShotOrchestrator = class {
20437
20634
  let fromFallback = false;
20438
20635
  let lastError;
20439
20636
  let fallbackEligible = false;
20637
+ let attempts = 0;
20440
20638
  if (tracker && !tracker.isAvailable(target.providerId, target.model) || !evaluateModelCalendar(config.modelAvailabilitySchedule, target.providerId, target.model).allowed) {
20441
20639
  this.opts.logger?.debug(
20442
20640
  `one-shot: primary "${target.providerId}/${target.model}" is blocked \u2014 trying fallback`
20443
20641
  );
20444
20642
  } else {
20643
+ attempts += 1;
20445
20644
  const primaryAttempt = await this.tryCall(
20446
20645
  provider,
20447
20646
  request,
@@ -20456,10 +20655,17 @@ var OneShotOrchestrator = class {
20456
20655
  tracker?.recordSuccess(target.providerId, target.model);
20457
20656
  servingProviderId = provider.id;
20458
20657
  servingModel = target.model;
20459
- return this.buildResult(result, servingProviderId, servingModel, false, startedAt);
20658
+ return this.buildResult(result, servingProviderId, servingModel, false, startedAt, attempts);
20460
20659
  }
20461
20660
  if (!fallbackEligible || chain.length === 0) {
20462
- return this.buildErrorResult(lastError, target.providerId, target.model, false, startedAt);
20661
+ return this.buildErrorResult(
20662
+ lastError,
20663
+ target.providerId,
20664
+ target.model,
20665
+ false,
20666
+ startedAt,
20667
+ attempts
20668
+ );
20463
20669
  }
20464
20670
  }
20465
20671
  const estimatedTokens = estimateRequestTokens(request.messages, request.system, []).total;
@@ -20487,6 +20693,7 @@ var OneShotOrchestrator = class {
20487
20693
  }
20488
20694
  servingProviderId = fbProvider.id;
20489
20695
  servingModel = entry.model;
20696
+ attempts += 1;
20490
20697
  const attempt = await this.tryCall(
20491
20698
  fbProvider,
20492
20699
  this.buildRequest(input, entry.model),
@@ -20497,7 +20704,14 @@ var OneShotOrchestrator = class {
20497
20704
  if (attempt.response) {
20498
20705
  tracker?.recordSuccess(entry.providerId, entry.model);
20499
20706
  fromFallback = true;
20500
- return this.buildResult(attempt.response, servingProviderId, servingModel, true, startedAt);
20707
+ return this.buildResult(
20708
+ attempt.response,
20709
+ servingProviderId,
20710
+ servingModel,
20711
+ true,
20712
+ startedAt,
20713
+ attempts
20714
+ );
20501
20715
  }
20502
20716
  lastError = attempt.error;
20503
20717
  }
@@ -20506,7 +20720,8 @@ var OneShotOrchestrator = class {
20506
20720
  servingProviderId,
20507
20721
  servingModel,
20508
20722
  fromFallback,
20509
- startedAt
20723
+ startedAt,
20724
+ attempts
20510
20725
  );
20511
20726
  }
20512
20727
  // ── Private helpers ─────────────────────────────────────────────
@@ -20630,7 +20845,7 @@ var OneShotOrchestrator = class {
20630
20845
  }
20631
20846
  }
20632
20847
  /** Build a success result from a provider Response. */
20633
- buildResult(response, servingProviderId, servingModel, fromFallback, startedAt) {
20848
+ buildResult(response, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
20634
20849
  const textBlocks = response.content.filter(isTextBlock);
20635
20850
  const text = textBlocks.map((b) => b.text).join("\n").trim();
20636
20851
  return {
@@ -20644,11 +20859,12 @@ var OneShotOrchestrator = class {
20644
20859
  },
20645
20860
  durationMs: Math.round(performance.now() - startedAt),
20646
20861
  fromFallback,
20862
+ attempts,
20647
20863
  stopReason: response.stopReason
20648
20864
  };
20649
20865
  }
20650
20866
  /** Build a total-failure error result. */
20651
- buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt) {
20867
+ buildErrorResult(error, servingProviderId, servingModel, fromFallback, startedAt, attempts) {
20652
20868
  return {
20653
20869
  text: "",
20654
20870
  model: servingModel,
@@ -20656,6 +20872,7 @@ var OneShotOrchestrator = class {
20656
20872
  tokens: { input: 0, output: 0, total: 0 },
20657
20873
  durationMs: Math.round(performance.now() - startedAt),
20658
20874
  fromFallback,
20875
+ attempts,
20659
20876
  error: error instanceof Error ? error.message : String(error ?? "Unknown error")
20660
20877
  };
20661
20878
  }