@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
@@ -21,6 +21,7 @@ import { parseModelPattern } from "../../core/model-resolver.js";
21
21
  import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js";
22
22
  import { SessionManager } from "../../core/session-manager.js";
23
23
  import { TabTitleGenerator } from "../../core/tab-title.js";
24
+ import { validateThinkingLevelForModel } from "../../core/thinking.js";
24
25
  import { discoverAgentTypes, getBackgroundAgents, rehydrateBackgroundAgentsFromDisk, } from "../../core/tools/subagent.js";
25
26
  import { theme } from "../interactive/theme/theme.js";
26
27
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
@@ -155,6 +156,7 @@ export function toRpcBackgroundAgentInfo(a) {
155
156
  sessionDir: a.sessionDir,
156
157
  sessionFile: a.sessionFile,
157
158
  cwd: a.cwd,
159
+ arbitrations: a.arbitrations?.map((record) => structuredClone(record)),
158
160
  };
159
161
  }
160
162
  /** Discover available subagent types for RPC clients, sorted by display name. */
@@ -191,6 +193,7 @@ export function getSettingsForRpc(settingsManager) {
191
193
  transport: settingsManager.getTransport(),
192
194
  hideThinkingBlock: settingsManager.getHideThinkingBlock(),
193
195
  agentModels: settingsManager.getAgentModels(),
196
+ subagentArbiter: settingsManager.getGlobalSubagentArbiterSettings(),
194
197
  };
195
198
  }
196
199
  function formatSettingsErrors(errors) {
@@ -257,6 +260,7 @@ const SETTINGS_UPDATE_KEYS = [
257
260
  "transport",
258
261
  "hideThinkingBlock",
259
262
  "agentModels",
263
+ "subagentArbiter",
260
264
  ];
261
265
  const QUEUE_MODES = ["all", "one-at-a-time"];
262
266
  const TRANSPORT_SETTINGS = ["sse", "websocket", "auto"];
@@ -534,6 +538,64 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
534
538
  return validation;
535
539
  }
536
540
  }
541
+ let subagentArbiter = update.subagentArbiter === null ? undefined : update.subagentArbiter;
542
+ if (subagentArbiter !== undefined) {
543
+ if (!isPlainObject(subagentArbiter)) {
544
+ return { ok: false, error: "subagentArbiter must be an object or null" };
545
+ }
546
+ const validKeys = ["enabled", "model", "thinking", "guidePath"];
547
+ const unknownArbiterKeys = Object.keys(subagentArbiter).filter((key) => !validKeys.includes(key));
548
+ if (unknownArbiterKeys.length > 0) {
549
+ return { ok: false, error: `Unknown subagentArbiter key(s): ${unknownArbiterKeys.join(", ")}` };
550
+ }
551
+ if (subagentArbiter.enabled !== undefined && typeof subagentArbiter.enabled !== "boolean") {
552
+ return { ok: false, error: "subagentArbiter.enabled must be a boolean" };
553
+ }
554
+ if (subagentArbiter.enabled === false) {
555
+ // Explicit disablement is the recovery path for fail-closed policies.
556
+ // Preserve retained fields without validating them so malformed manual
557
+ // edits cannot trap users in a policy that blocks every child spawn.
558
+ subagentArbiter = { ...subagentArbiter };
559
+ }
560
+ else {
561
+ if (subagentArbiter.model !== undefined &&
562
+ (typeof subagentArbiter.model !== "string" || !subagentArbiter.model.trim())) {
563
+ return { ok: false, error: "subagentArbiter.model must be a non-empty exact provider/model string" };
564
+ }
565
+ if (subagentArbiter.thinking !== undefined &&
566
+ (typeof subagentArbiter.thinking !== "string" || !isValidThinkingLevel(subagentArbiter.thinking))) {
567
+ return {
568
+ ok: false,
569
+ error: `Invalid subagentArbiter.thinking: ${JSON.stringify(subagentArbiter.thinking)}. Valid values: ${VALID_THINKING_LEVELS.join(", ")}`,
570
+ };
571
+ }
572
+ if (subagentArbiter.guidePath !== undefined &&
573
+ (typeof subagentArbiter.guidePath !== "string" || !subagentArbiter.guidePath.trim())) {
574
+ return { ok: false, error: "subagentArbiter.guidePath must be a non-empty string" };
575
+ }
576
+ if (subagentArbiter.enabled === true && !subagentArbiter.model) {
577
+ return { ok: false, error: "Enabling subagentArbiter requires an exact provider/model" };
578
+ }
579
+ if (subagentArbiter.model) {
580
+ const slash = subagentArbiter.model.indexOf("/");
581
+ if (slash <= 0 || slash === subagentArbiter.model.length - 1 || /\s/.test(subagentArbiter.model)) {
582
+ return { ok: false, error: "subagentArbiter.model must be an exact provider/model" };
583
+ }
584
+ const provider = subagentArbiter.model.slice(0, slash);
585
+ const modelId = subagentArbiter.model.slice(slash + 1);
586
+ const models = await modelRegistry.getAvailable();
587
+ const model = models.find((candidate) => candidate.provider === provider && candidate.id === modelId);
588
+ if (!model)
589
+ return { ok: false, error: `Arbiter model not found: ${subagentArbiter.model}` };
590
+ if (subagentArbiter.thinking !== undefined) {
591
+ const validation = validateThinkingLevelForModel(model, subagentArbiter.thinking);
592
+ if (!validation.ok)
593
+ return validation;
594
+ }
595
+ }
596
+ subagentArbiter = { ...subagentArbiter };
597
+ }
598
+ }
537
599
  let trustedContextFolders;
538
600
  if (update.trustedContextFolders !== undefined) {
539
601
  try {
@@ -619,6 +681,9 @@ export async function setSettingsForRpc(settingsManager, modelRegistry, update)
619
681
  settingsManager.setHideThinkingBlock(update.hideThinkingBlock);
620
682
  }
621
683
  const warnings = [];
684
+ if (update.subagentArbiter !== undefined) {
685
+ settingsManager.setGlobalSubagentArbiterSettings(subagentArbiter);
686
+ }
622
687
  if (update.agentModels !== undefined) {
623
688
  for (const [agentName, models] of Object.entries(update.agentModels)) {
624
689
  if (settingsManager.hasProjectAgentModelOverride(agentName)) {
@@ -790,77 +855,105 @@ export async function navigateTreeForRpc(session, targetId, options) {
790
855
  ...(result.editorText !== undefined ? { editorText: result.editorText } : {}),
791
856
  };
792
857
  }
793
- /**
794
- * Run in RPC mode.
795
- * Listens for JSON commands on stdin, outputs events and responses on stdout.
796
- */
797
- export async function runRpcMode(session, modelFallbackMessage) {
798
- takeOverStdout();
799
- const output = (obj) => {
800
- writeRawStdout(serializeJsonLine(obj));
801
- };
802
- const success = (id, command, data) => {
803
- if (data === undefined) {
804
- return { id, type: "response", command, success: true };
805
- }
806
- return { id, type: "response", command, success: true, data };
807
- };
808
- const error = (id, command, message) => {
809
- return { id, type: "response", command, success: false, error: message };
810
- };
811
- if (session.sessionFile && session.messages.length > 0) {
812
- const rehydratedCount = rehydrateBackgroundAgentsFromDisk(session.sessionFile);
813
- if (rehydratedCount > 0) {
814
- console.error(`[rpc] Rehydrated ${rehydratedCount} background subagent${rehydratedCount === 1 ? "" : "s"} from disk`);
815
- }
816
- }
817
- // Pending extension UI requests waiting for response
818
- const pendingExtensionRequests = new Map();
819
- // Shutdown request flag
820
- let shutdownRequested = false;
821
- let dailyCostTracker;
822
- let dailyCostTrackerPrimed = false;
858
+ export function createRpcExtensionUIContext(output, pendingExtensionRequests, onAskStop = () => { }) {
823
859
  /** Helper for dialog methods with signal/timeout support */
824
- function createDialogPromise(opts, defaultValue, request, parseResponse) {
860
+ function createDialogPromise(opts, defaultValue, request, parseResponse, onTimeout) {
825
861
  if (opts?.signal?.aborted)
826
862
  return Promise.resolve(defaultValue);
827
863
  const id = crypto.randomUUID();
828
864
  return new Promise((resolve, reject) => {
829
865
  let timeoutId;
866
+ let settled = false;
830
867
  const cleanup = () => {
868
+ if (settled)
869
+ return false;
870
+ settled = true;
831
871
  if (timeoutId)
832
872
  clearTimeout(timeoutId);
833
873
  opts?.signal?.removeEventListener("abort", onAbort);
834
874
  pendingExtensionRequests.delete(id);
875
+ output({ type: "extension_ui_response_handled", id });
876
+ return true;
835
877
  };
836
878
  const onAbort = () => {
837
- cleanup();
879
+ if (!cleanup())
880
+ return;
838
881
  resolve(defaultValue);
839
882
  };
840
883
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
841
884
  if (opts?.timeout) {
842
885
  timeoutId = setTimeout(() => {
843
- cleanup();
886
+ if (!cleanup())
887
+ return;
888
+ onTimeout?.();
844
889
  resolve(defaultValue);
845
890
  }, opts.timeout);
846
891
  }
892
+ const rpcRequest = {
893
+ type: "extension_ui_request",
894
+ id,
895
+ ...request,
896
+ };
847
897
  pendingExtensionRequests.set(id, {
898
+ request: rpcRequest,
848
899
  resolve: (response) => {
849
- cleanup();
850
- resolve(parseResponse(response));
900
+ if (!cleanup())
901
+ return;
902
+ try {
903
+ resolve(parseResponse(response));
904
+ }
905
+ catch (error) {
906
+ reject(error);
907
+ }
908
+ },
909
+ reject: (error) => {
910
+ if (!cleanup())
911
+ return;
912
+ reject(error);
851
913
  },
852
- reject,
853
914
  });
854
- output({ type: "extension_ui_request", id, ...request });
915
+ output(rpcRequest);
855
916
  });
856
917
  }
857
918
  /**
858
919
  * Create an extension UI context that uses the RPC protocol.
859
920
  */
860
- const createExtensionUIContext = () => ({
921
+ return {
861
922
  select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
862
923
  confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
863
924
  input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
925
+ ask: (request, opts) => createDialogPromise(opts, undefined, {
926
+ method: "ask",
927
+ title: request.title ?? "Question",
928
+ question: request.question,
929
+ options: request.options,
930
+ allowFreeText: request.allowFreeText,
931
+ multiSelect: request.multiSelect,
932
+ multiline: request.multiline,
933
+ timeout: opts?.timeout,
934
+ // Preserve the authoritative deadline across Dashboard reload,
935
+ // resync, and drill-in hydration instead of restarting the full
936
+ // duration whenever the question component remounts.
937
+ expiresAt: opts?.timeout ? Date.now() + opts.timeout : undefined,
938
+ }, (response) => {
939
+ if ("cancelled" in response && response.cancelled) {
940
+ onAskStop();
941
+ return undefined;
942
+ }
943
+ const selected = response.selected;
944
+ const customText = response.customText;
945
+ if (!Array.isArray(selected) || !selected.every((value) => typeof value === "string")) {
946
+ throw new Error("Invalid RPC ask response: selected must be an array of strings");
947
+ }
948
+ if (customText !== undefined && typeof customText !== "string") {
949
+ throw new Error("Invalid RPC ask response: customText must be a string");
950
+ }
951
+ if (selected.length === 0 && !customText?.trim()) {
952
+ onAskStop();
953
+ return undefined;
954
+ }
955
+ return { selected, customText };
956
+ }, onAskStop),
864
957
  notify(message, type) {
865
958
  // Fire and forget - no response needed
866
959
  output({
@@ -939,26 +1032,11 @@ export async function runRpcMode(session, modelFallbackMessage) {
939
1032
  // Host should track editor state locally if needed
940
1033
  return "";
941
1034
  },
942
- async editor(title, prefill) {
943
- const id = crypto.randomUUID();
944
- return new Promise((resolve, reject) => {
945
- pendingExtensionRequests.set(id, {
946
- resolve: (response) => {
947
- if ("cancelled" in response && response.cancelled) {
948
- resolve(undefined);
949
- }
950
- else if ("value" in response) {
951
- resolve(response.value);
952
- }
953
- else {
954
- resolve(undefined);
955
- }
956
- },
957
- reject,
958
- });
959
- output({ type: "extension_ui_request", id, method: "editor", title, prefill });
960
- });
961
- },
1035
+ editor: (title, prefill) => createDialogPromise(undefined, undefined, { method: "editor", title, prefill }, (response) => "cancelled" in response && response.cancelled
1036
+ ? undefined
1037
+ : "value" in response
1038
+ ? response.value
1039
+ : undefined),
962
1040
  setEditorComponent() {
963
1041
  // Custom editor components not supported in RPC mode
964
1042
  },
@@ -982,7 +1060,45 @@ export async function runRpcMode(session, modelFallbackMessage) {
982
1060
  setToolsExpanded(_expanded) {
983
1061
  // Tool expansion not supported in RPC mode - no TUI
984
1062
  },
985
- });
1063
+ };
1064
+ }
1065
+ /** Cancel every pending dialog during RPC host teardown. */
1066
+ export function cancelPendingRpcExtensionRequests(pendingExtensionRequests) {
1067
+ for (const [id, pending] of [...pendingExtensionRequests]) {
1068
+ pending.resolve({ type: "extension_ui_response", id, cancelled: true });
1069
+ }
1070
+ }
1071
+ export async function runRpcMode(session, modelFallbackMessage) {
1072
+ takeOverStdout();
1073
+ const output = (obj) => {
1074
+ writeRawStdout(serializeJsonLine(obj));
1075
+ };
1076
+ const success = (id, command, data) => {
1077
+ if (data === undefined) {
1078
+ return { id, type: "response", command, success: true };
1079
+ }
1080
+ return { id, type: "response", command, success: true, data };
1081
+ };
1082
+ const error = (id, command, message) => {
1083
+ return { id, type: "response", command, success: false, error: message };
1084
+ };
1085
+ if (session.sessionFile && session.messages.length > 0) {
1086
+ const rehydratedCount = rehydrateBackgroundAgentsFromDisk(session.sessionFile);
1087
+ if (rehydratedCount > 0) {
1088
+ console.error(`[rpc] Rehydrated ${rehydratedCount} background subagent${rehydratedCount === 1 ? "" : "s"} from disk`);
1089
+ }
1090
+ }
1091
+ // Pending extension UI requests waiting for response. Keep the emitted
1092
+ // payload alongside its callbacks so recovery snapshots can reconstruct the
1093
+ // answer UI after a Dashboard reload or replay gap.
1094
+ const pendingExtensionRequests = new Map();
1095
+ // Shutdown request flag
1096
+ let shutdownRequested = false;
1097
+ let dailyCostTracker;
1098
+ let dailyCostTrackerPrimed = false;
1099
+ // Extension UI context uses the RPC protocol; built by a module-scope
1100
+ // factory so the dialog round trip is unit-testable (see createRpcExtensionUIContext).
1101
+ const createExtensionUIContext = () => createRpcExtensionUIContext(output, pendingExtensionRequests, () => void session.abort());
986
1102
  // Set up extensions with RPC-based UI context
987
1103
  await session.bindExtensions({
988
1104
  uiContext: createExtensionUIContext(),
@@ -1121,6 +1237,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
1121
1237
  state: getStateForRpc(session, modelFallbackMessage),
1122
1238
  messages: getDashboardMessagesForRpc(session),
1123
1239
  backgroundAgents: getBackgroundAgents().map(toRpcBackgroundAgentInfo),
1240
+ pendingExtensionUiRequests: [...pendingExtensionRequests.values()].map(({ request }) => request),
1124
1241
  };
1125
1242
  return success(id, "get_dashboard_snapshot", data);
1126
1243
  }
@@ -1450,6 +1567,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
1450
1567
  */
1451
1568
  let detachInput = () => { };
1452
1569
  async function shutdown() {
1570
+ cancelPendingRpcExtensionRequests(pendingExtensionRequests);
1453
1571
  const currentRunner = session.extensionRunner;
1454
1572
  if (currentRunner?.hasHandlers("session_shutdown")) {
1455
1573
  await currentRunner.emit({ type: "session_shutdown" });