@aragon-agent/cli 0.6.38 → 0.6.40

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 (209) hide show
  1. package/CHANGELOG.md +263 -0
  2. package/README.md +544 -11
  3. package/dist/agent/controller.d.ts +245 -10
  4. package/dist/agent/controller.js +367 -22
  5. package/dist/agent/model-profile-settings.js +12 -1
  6. package/dist/agent/plan-prompt.d.ts +1 -3
  7. package/dist/agent/plan-prompt.js +13 -7
  8. package/dist/agent/reducer.d.ts +70 -2
  9. package/dist/agent/reducer.js +59 -6
  10. package/dist/agent/skill-triage-prompt.d.ts +42 -0
  11. package/dist/agent/skill-triage-prompt.js +74 -0
  12. package/dist/agent/system-prompt.d.ts +28 -2
  13. package/dist/agent/system-prompt.js +12 -4
  14. package/dist/cli.js +106 -32
  15. package/dist/commands/builtins.js +24 -3
  16. package/dist/commands/keys-command.d.ts +23 -0
  17. package/dist/commands/keys-command.js +38 -0
  18. package/dist/commands/registry.d.ts +39 -0
  19. package/dist/commands/skills.js +86 -1
  20. package/dist/commands/subagent-command.d.ts +25 -0
  21. package/dist/commands/subagent-command.js +242 -0
  22. package/dist/commands/task-command.d.ts +39 -0
  23. package/dist/commands/task-command.js +102 -0
  24. package/dist/compaction/command.js +16 -3
  25. package/dist/compaction/compactor.d.ts +25 -0
  26. package/dist/compaction/compactor.js +111 -10
  27. package/dist/compaction/digest.d.ts +24 -1
  28. package/dist/compaction/digest.js +139 -17
  29. package/dist/compaction/limits.d.ts +21 -0
  30. package/dist/compaction/limits.js +21 -0
  31. package/dist/compaction/reason-text.d.ts +36 -0
  32. package/dist/compaction/reason-text.js +78 -0
  33. package/dist/compaction/types.d.ts +82 -14
  34. package/dist/compaction/wiring.d.ts +13 -8
  35. package/dist/compaction/wiring.js +1 -0
  36. package/dist/config/cli-commands.d.ts +23 -0
  37. package/dist/config/cli-commands.js +36 -0
  38. package/dist/config/env.js +17 -4
  39. package/dist/config/load.js +55 -3
  40. package/dist/config/schema.d.ts +125 -10
  41. package/dist/config/schema.js +138 -14
  42. package/dist/config/store.js +23 -3
  43. package/dist/diagnostics/doctor.d.ts +9 -0
  44. package/dist/diagnostics/doctor.js +34 -0
  45. package/dist/diagnostics/info.d.ts +71 -0
  46. package/dist/diagnostics/info.js +91 -5
  47. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  48. package/dist/exec/bridge/caller-tools.js +213 -0
  49. package/dist/exec/bridge/decisions.d.ts +78 -0
  50. package/dist/exec/bridge/decisions.js +116 -0
  51. package/dist/exec/bridge/human-answers.d.ts +38 -0
  52. package/dist/exec/bridge/human-answers.js +164 -0
  53. package/dist/exec/bridge/limits.d.ts +68 -0
  54. package/dist/exec/bridge/limits.js +68 -0
  55. package/dist/exec/bridge/ndjson.d.ts +17 -0
  56. package/dist/exec/bridge/ndjson.js +83 -0
  57. package/dist/exec/bridge/pending.d.ts +70 -0
  58. package/dist/exec/bridge/pending.js +132 -0
  59. package/dist/exec/bridge/protocol.d.ts +346 -0
  60. package/dist/exec/bridge/protocol.js +256 -0
  61. package/dist/exec/bridge/server.d.ts +142 -0
  62. package/dist/exec/bridge/server.js +502 -0
  63. package/dist/exec/bridge/source.d.ts +58 -0
  64. package/dist/exec/bridge/source.js +145 -0
  65. package/dist/exec/bridge/writer.d.ts +26 -0
  66. package/dist/exec/bridge/writer.js +156 -0
  67. package/dist/exec/cli-commands.js +30 -2
  68. package/dist/exec/emitter.d.ts +16 -2
  69. package/dist/exec/emitter.js +26 -6
  70. package/dist/exec/events.d.ts +465 -10
  71. package/dist/exec/events.js +47 -1
  72. package/dist/exec/hooks/dispatch.d.ts +76 -0
  73. package/dist/exec/hooks/dispatch.js +169 -0
  74. package/dist/exec/hooks/index.d.ts +83 -0
  75. package/dist/exec/hooks/index.js +205 -0
  76. package/dist/exec/hooks/limits.d.ts +56 -0
  77. package/dist/exec/hooks/limits.js +49 -0
  78. package/dist/exec/hooks/registry.d.ts +49 -0
  79. package/dist/exec/hooks/registry.js +91 -0
  80. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  81. package/dist/exec/hooks/tool-wrapper.js +154 -0
  82. package/dist/exec/hooks/turn.d.ts +61 -0
  83. package/dist/exec/hooks/turn.js +59 -0
  84. package/dist/exec/hooks/validate.d.ts +85 -0
  85. package/dist/exec/hooks/validate.js +310 -0
  86. package/dist/exec/index.d.ts +130 -4
  87. package/dist/exec/index.js +501 -10
  88. package/dist/exec/options.d.ts +124 -2
  89. package/dist/exec/options.js +413 -6
  90. package/dist/exec/permission.d.ts +9 -1
  91. package/dist/exec/permission.js +7 -1
  92. package/dist/exec/request/limits.d.ts +51 -0
  93. package/dist/exec/request/limits.js +49 -0
  94. package/dist/exec/request/scope.d.ts +106 -0
  95. package/dist/exec/request/scope.js +149 -0
  96. package/dist/exec/result/limits.d.ts +74 -0
  97. package/dist/exec/result/limits.js +72 -0
  98. package/dist/exec/result/prompt.d.ts +28 -0
  99. package/dist/exec/result/prompt.js +74 -0
  100. package/dist/exec/result/schema.d.ts +99 -0
  101. package/dist/exec/result/schema.js +464 -0
  102. package/dist/exec/result/sink.d.ts +56 -0
  103. package/dist/exec/result/sink.js +50 -0
  104. package/dist/exec/result/tool.d.ts +37 -0
  105. package/dist/exec/result/tool.js +118 -0
  106. package/dist/exec/runner.d.ts +395 -1
  107. package/dist/exec/runner.js +860 -8
  108. package/dist/exec/threads/aggregate.d.ts +34 -0
  109. package/dist/exec/threads/aggregate.js +30 -0
  110. package/dist/exec/threads/capacity.d.ts +23 -0
  111. package/dist/exec/threads/capacity.js +38 -0
  112. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  113. package/dist/exec/threads/dispatch-source.js +106 -0
  114. package/dist/exec/threads/execute-threads.d.ts +53 -0
  115. package/dist/exec/threads/execute-threads.js +234 -0
  116. package/dist/exec/threads/gate.d.ts +42 -0
  117. package/dist/exec/threads/gate.js +81 -0
  118. package/dist/exec/threads/identity.d.ts +36 -0
  119. package/dist/exec/threads/identity.js +67 -0
  120. package/dist/exec/threads/interrupt.d.ts +29 -0
  121. package/dist/exec/threads/interrupt.js +79 -0
  122. package/dist/exec/threads/limits.d.ts +58 -0
  123. package/dist/exec/threads/limits.js +35 -0
  124. package/dist/exec/threads/outcome.d.ts +47 -0
  125. package/dist/exec/threads/outcome.js +26 -0
  126. package/dist/exec/threads/registry.d.ts +81 -0
  127. package/dist/exec/threads/registry.js +427 -0
  128. package/dist/exec/threads/runtime.d.ts +107 -0
  129. package/dist/exec/threads/runtime.js +523 -0
  130. package/dist/exec/threads/thread-source.d.ts +20 -0
  131. package/dist/exec/threads/thread-source.js +134 -0
  132. package/dist/fast/limits.d.ts +54 -0
  133. package/dist/fast/limits.js +54 -0
  134. package/dist/fast/summary-call.d.ts +72 -0
  135. package/dist/fast/summary-call.js +133 -0
  136. package/dist/fast/summary-prompt.d.ts +106 -0
  137. package/dist/fast/summary-prompt.js +320 -0
  138. package/dist/fast/task-summary-types.d.ts +66 -0
  139. package/dist/fast/task-summary-types.js +17 -0
  140. package/dist/logging/logger.d.ts +1 -1
  141. package/dist/proc/limits.d.ts +7 -0
  142. package/dist/proc/limits.js +7 -0
  143. package/dist/proc/log-mirror.d.ts +21 -4
  144. package/dist/proc/log-mirror.js +29 -7
  145. package/dist/proc/output-streams.d.ts +50 -0
  146. package/dist/proc/output-streams.js +54 -0
  147. package/dist/skills/service.d.ts +41 -0
  148. package/dist/skills/service.js +62 -0
  149. package/dist/skills/triage.d.ts +194 -0
  150. package/dist/skills/triage.js +393 -0
  151. package/dist/team/forced-dispatch.d.ts +95 -0
  152. package/dist/team/forced-dispatch.js +208 -0
  153. package/dist/team/limits.d.ts +21 -1
  154. package/dist/team/limits.js +21 -1
  155. package/dist/team/normalize.d.ts +15 -0
  156. package/dist/team/normalize.js +21 -4
  157. package/dist/team/prompt.d.ts +6 -3
  158. package/dist/team/prompt.js +9 -5
  159. package/dist/team/runtime.d.ts +9 -0
  160. package/dist/team/runtime.js +8 -0
  161. package/dist/team/subagent.d.ts +15 -0
  162. package/dist/team/subagent.js +45 -1
  163. package/dist/team/task-tool.js +11 -7
  164. package/dist/tools/bash-tool.d.ts +10 -0
  165. package/dist/tools/bash-tool.js +71 -6
  166. package/dist/tools/human-input.d.ts +35 -2
  167. package/dist/tools/human-input.js +38 -0
  168. package/dist/tools/index.d.ts +93 -1
  169. package/dist/tools/index.js +112 -3
  170. package/dist/tools/plan-tools.d.ts +20 -12
  171. package/dist/tools/plan-tools.js +24 -17
  172. package/dist/tools/tool-gate.d.ts +100 -0
  173. package/dist/tools/tool-gate.js +87 -0
  174. package/dist/ui/App.js +238 -12
  175. package/dist/ui/Header.d.ts +28 -16
  176. package/dist/ui/Header.js +21 -19
  177. package/dist/ui/StatusBar.d.ts +1 -0
  178. package/dist/ui/StatusBar.js +1 -0
  179. package/dist/ui/Transcript.js +1 -1
  180. package/dist/ui/entries/CompactionCard.d.ts +11 -1
  181. package/dist/ui/entries/CompactionCard.js +65 -4
  182. package/dist/ui/interaction-hints.js +6 -0
  183. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  184. package/dist/ui/layout/OverlayFrame.js +19 -4
  185. package/dist/ui/layout/header-hints.d.ts +55 -62
  186. package/dist/ui/layout/header-hints.js +59 -166
  187. package/dist/ui/layout/header-left.d.ts +115 -0
  188. package/dist/ui/layout/header-left.js +206 -0
  189. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  190. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  191. package/dist/ui/layout/status-detail-layout.js +5 -1
  192. package/dist/ui/layout/status-layout.d.ts +10 -0
  193. package/dist/ui/layout/status-layout.js +18 -1
  194. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  195. package/dist/ui/layout/task-summary-cell.js +65 -0
  196. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  197. package/dist/ui/overlays/HelpOverlay.js +29 -2
  198. package/dist/ui/overlays/SettingsScreen.js +29 -5
  199. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  200. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  201. package/dist/ui/overlays/TeamFanoutPage.d.ts +35 -0
  202. package/dist/ui/overlays/TeamFanoutPage.js +78 -0
  203. package/dist/ui/overlays/model-profile-state.d.ts +19 -1
  204. package/dist/ui/overlays/model-profile-state.js +12 -0
  205. package/dist/ui/overlays/team-fanout-state.d.ts +73 -0
  206. package/dist/ui/overlays/team-fanout-state.js +97 -0
  207. package/dist/ui/shortcuts.d.ts +81 -0
  208. package/dist/ui/shortcuts.js +221 -0
  209. package/package.json +2 -2
@@ -273,20 +273,23 @@ export function clampScrollResumeMs(v, fallback) {
273
273
  // nested-object merge is hand-written and capped at one level; three scalars do
274
274
  // not justify a second `clampXConfig` gate plus two more merge sites, and flat
275
275
  // keys mean `store.ts` needs ZERO changes.
276
+ //
277
+ // ONE OF THOSE KEYS IS GONE: `planModeMaxAskRounds` capped `ask_user` at four
278
+ // rounds per turn. There is no cap any more, so there is no number to store, no
279
+ // range to clamp and nothing to configure. It is retired through
280
+ // `RETIRED_CONFIG_KEYS` below rather than defaulted to a large number — a
281
+ // ninety-nine in the config file is the same second source of truth as a four.
276
282
  // ---------------------------------------------------------------------------
277
- /** `ask_user` calls allowed per user turn. */
278
- export const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
279
- const ASK_ROUNDS_RANGE = { min: 1, max: 10 };
280
283
  /**
281
284
  * Ceiling on ONE human wait, in ms. Thirty minutes is far outside "the user
282
285
  * went to get coffee", but a ceiling has to exist: an unattended terminal must
283
286
  * not wedge a CI job or a stale ssh session forever.
287
+ *
288
+ * THIS IS NOT A ROUND LIMIT and the distinction matters. It bounds a single wait
289
+ * (liveness); it says nothing about how many times the model may ask.
284
290
  */
285
291
  export const DEFAULT_PLAN_HUMAN_TIMEOUT_MS = 1_800_000;
286
292
  const HUMAN_TIMEOUT_RANGE = { min: 60_000, max: 7_200_000 };
287
- export function clampAskRounds(v, fallback) {
288
- return clampInt(v, fallback, ASK_ROUNDS_RANGE);
289
- }
290
293
  export function clampHumanTimeout(v, fallback) {
291
294
  return clampInt(v, fallback, HUMAN_TIMEOUT_RANGE);
292
295
  }
@@ -322,6 +325,9 @@ export const DEFAULT_SKILLS_CONFIG = {
322
325
  integrity: 'warn',
323
326
  usageTracking: true,
324
327
  toolPolicy: 'enforce',
328
+ // ON by default: the capability this key controls is the feature's whole point,
329
+ // and it costs zero model calls (local matching over in-memory records).
330
+ triage: true,
325
331
  };
326
332
  const CATALOG_BYTES_RANGE = { min: 500, max: 40_000 };
327
333
  /**
@@ -406,6 +412,7 @@ export function clampSkillsConfig(raw) {
406
412
  integrity: clampSkillsIntegrity(src.integrity, DEFAULT_SKILLS_CONFIG.integrity),
407
413
  usageTracking: bool(src.usageTracking, DEFAULT_SKILLS_CONFIG.usageTracking),
408
414
  toolPolicy: clampSkillsToolPolicy(src.toolPolicy, DEFAULT_SKILLS_CONFIG.toolPolicy),
415
+ triage: bool(src.triage, DEFAULT_SKILLS_CONFIG.triage),
409
416
  };
410
417
  }
411
418
  export const DEFAULT_SKILLS_RUNTIME = { forcedSkills: [], approveAll: false };
@@ -474,8 +481,12 @@ export function clampLogConfig(raw) {
474
481
  export const HARD_MAX_SUBAGENTS = TEAM_LIMITS.hardMaxSubagents;
475
482
  export const DEFAULT_TEAM_CONFIG = {
476
483
  enabled: true,
477
- maxSubagents: 5,
478
- maxConcurrent: 3,
484
+ // 8 IS THE DEFAULT, 10 IS THE CEILING, and they are not the same number
485
+ // (subagent-fanout D-1 / D-2). `maxConcurrent` moves WITH `maxSubagents`
486
+ // because a wider population behind a 3-wide pool shows the user nothing:
487
+ // children 4..8 simply queue (`team-bus.test.ts` records the queued phase).
488
+ maxSubagents: 8,
489
+ maxConcurrent: 8,
479
490
  // 0 = NO LIMIT. Subagents run under the same regime as the lead: no
480
491
  // wall clock, no turn cap; only the idle watchdog and the user's Esc.
481
492
  subagentTimeoutMs: 0,
@@ -534,6 +545,13 @@ export function clampTeamConfig(raw) {
534
545
  && [...src.modelProfileId].length <= 128
535
546
  ? src.modelProfileId
536
547
  : null,
548
+ // TRANSPARENT, and only ever `true` (subagent-fanout D-3 / R-6). The flag is
549
+ // not a setting: it is the one-shot marker the migration leaves behind, so a
550
+ // clamp that dropped it would re-migrate the user on every launch. Written
551
+ // as a spread rather than `=== true ? true : undefined` so the key stays
552
+ // ABSENT (not `undefined`) in the clamped result - `toEqual(DEFAULT_TEAM_CONFIG)`
553
+ // and the on-disk shape both depend on that.
554
+ ...(src.fanoutMigrated === true ? { fanoutMigrated: true } : {}),
537
555
  };
538
556
  }
539
557
  /**
@@ -574,6 +592,56 @@ export function legacyTeamCeilingPatch(raw) {
574
592
  }
575
593
  return matched ? patch : null;
576
594
  }
595
+ /**
596
+ * The fan-out defaults cli <= 0.6.x persisted as its OWN defaults
597
+ * (subagent-fanout F-2 / R-1).
598
+ *
599
+ * THE SAME TRAP AS THE CEILINGS ABOVE, ONE SECTION OVER. `mergePersistedConfig`
600
+ * writes the WHOLE `team` section on every save, so a user who has ever touched
601
+ * any setting carries this exact pair in `config.json` - changing
602
+ * `DEFAULT_TEAM_CONFIG` alone would never reach them, and they would keep
603
+ * seeing "only 3 subagents at once" for ever.
604
+ */
605
+ export const LEGACY_TEAM_FANOUT_DEFAULTS = {
606
+ maxSubagents: 5,
607
+ maxConcurrent: 3,
608
+ };
609
+ /**
610
+ * The `{ maxSubagents, maxConcurrent, fanoutMigrated }` patch for the legacy
611
+ * default pair, or `null` when nothing matches (subagent-fanout D-3).
612
+ *
613
+ * EXACT-EQUALITY ONLY, the rule `legacyTeamCeilingPatch` records: a value that
614
+ * differs was a deliberate user choice and survives untouched. Two differences
615
+ * from that function, both deliberate:
616
+ *
617
+ * - `fanoutMigrated: true` short-circuits to `null`. That is what makes "the
618
+ * user changed their mind and went back to 5/3" stick: without it the next
619
+ * launch would migrate them again, every launch, for ever.
620
+ * - `maxConcurrent` is allowed to be ABSENT as well as 3, because a
621
+ * hand-written section may name only one key and a half-written pair is
622
+ * still the legacy intent, not a user decision.
623
+ *
624
+ * Pure on purpose: `loadConfig` decides WHERE the patch is applied and
625
+ * persisted; this function only answers WHICH keys match, so the equality test
626
+ * lives beside the constants it reads.
627
+ */
628
+ export function legacyTeamFanoutPatch(raw) {
629
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
630
+ return null;
631
+ const src = raw;
632
+ if (src.fanoutMigrated === true)
633
+ return null;
634
+ if (src.maxSubagents !== LEGACY_TEAM_FANOUT_DEFAULTS.maxSubagents)
635
+ return null;
636
+ if (src.maxConcurrent !== undefined
637
+ && src.maxConcurrent !== LEGACY_TEAM_FANOUT_DEFAULTS.maxConcurrent)
638
+ return null;
639
+ return {
640
+ maxSubagents: DEFAULT_TEAM_CONFIG.maxSubagents,
641
+ maxConcurrent: DEFAULT_TEAM_CONFIG.maxConcurrent,
642
+ fanoutMigrated: true,
643
+ };
644
+ }
577
645
  export const DEFAULT_TODO_CONFIG = {
578
646
  enabled: true,
579
647
  panel: true,
@@ -966,7 +1034,6 @@ export function clampJobsConfig(raw) {
966
1034
  export const DEFAULT_PROMPTS_CONFIG = {
967
1035
  enabled: true,
968
1036
  insertMode: 'insert',
969
- hint: true,
970
1037
  };
971
1038
  const PROMPT_INSERT_MODES = ['insert', 'replace'];
972
1039
  /**
@@ -985,7 +1052,26 @@ export function clampPromptsConfig(raw) {
985
1052
  insertMode: PROMPT_INSERT_MODES.includes(mode)
986
1053
  ? mode
987
1054
  : DEFAULT_PROMPTS_CONFIG.insertMode,
988
- hint: bool(src.hint, DEFAULT_PROMPTS_CONFIG.hint),
1055
+ };
1056
+ }
1057
+ export const DEFAULT_TASK_SUMMARY_CONFIG = {
1058
+ enabled: true,
1059
+ fallback: true,
1060
+ };
1061
+ /**
1062
+ * THE single gate for every `taskSummary` read AND write, like the twelve sections
1063
+ * above it.
1064
+ *
1065
+ * BOTH DEFAULTS ARE `true`, AND THE DIRECTION MATTERS: a corrupt or hand-edited
1066
+ * section must not silently switch off a feature the user asked for, and - more
1067
+ * importantly - it must not switch off the FLOOR, which is the only thing standing
1068
+ * between a failed call and an empty header cell.
1069
+ */
1070
+ export function clampTaskSummaryConfig(raw) {
1071
+ const src = (raw && typeof raw === 'object' ? raw : {});
1072
+ return {
1073
+ enabled: bool(src.enabled, DEFAULT_TASK_SUMMARY_CONFIG.enabled),
1074
+ fallback: bool(src.fallback, DEFAULT_TASK_SUMMARY_CONFIG.fallback),
989
1075
  };
990
1076
  }
991
1077
  /**
@@ -1051,7 +1137,6 @@ export const DEFAULT_CONFIG = {
1051
1137
  keyboardEnhancement: true,
1052
1138
  scrollResumeMs: DEFAULT_SCROLL_RESUME_MS,
1053
1139
  planModeDefault: false,
1054
- planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
1055
1140
  planModeHumanTimeoutMs: DEFAULT_PLAN_HUMAN_TIMEOUT_MS,
1056
1141
  skills: DEFAULT_SKILLS_CONFIG,
1057
1142
  log: DEFAULT_LOG_CONFIG,
@@ -1093,14 +1178,22 @@ export const DEFAULT_CONFIG = {
1093
1178
  // rather than quietly disabling something.
1094
1179
  jobs: DEFAULT_JOBS_CONFIG,
1095
1180
  // And one section later again, for the seventh time, and this one has BOTH
1096
- // failure directions at once: `prompts.enabled` and `prompts.hint` are `true` by
1097
- // default, so a `config.json` predating this feature would resolve them to
1098
- // `undefined` (falsy) and arrive with the key and its hint silently off - while
1181
+ // failure directions at once: `prompts.enabled` is `true` by default, so a
1182
+ // `config.json` predating this feature would resolve it to `undefined` (falsy)
1183
+ // and arrive with the key silently off - while
1099
1184
  // `prompts.insertMode` would be `undefined`, which is neither `'insert'` nor
1100
1185
  // `'replace'` and would make `planPromptFill` fall through to its `replace`
1101
1186
  // branch. That branch discards the user's own words, so the miss would be a data
1102
1187
  // loss rather than a missing hint.
1103
1188
  prompts: DEFAULT_PROMPTS_CONFIG,
1189
+ // And one section later again, for the eighth time - and this one has a silent
1190
+ // DATA-LOSS-shaped miss: `taskSummary.enabled` and `.fallback` are both `true` by
1191
+ // default, and `App` reads `cfg.taskSummary?.fallback !== false` at call time. A
1192
+ // `config.json` predating this feature would resolve them to `undefined`, which
1193
+ // happens to behave like the defaults in those two readers but NOT in
1194
+ // `config list`, so the file would report a section it never wrote. One line
1195
+ // here, like the seven above it.
1196
+ taskSummary: DEFAULT_TASK_SUMMARY_CONFIG,
1104
1197
  };
1105
1198
  /**
1106
1199
  * Runtime state that used to be stored in `config.json` by mistake
@@ -1136,6 +1229,37 @@ export function stripLegacyStateKeys(raw) {
1136
1229
  delete out[key];
1137
1230
  return out;
1138
1231
  }
1232
+ /**
1233
+ * Config keys whose MEANING was removed, mapped to the explanation a user gets if
1234
+ * they still try to set one.
1235
+ *
1236
+ * SEPARATE FROM `LEGACY_STATE_KEYS`, because the disposition differs: those keys'
1237
+ * values were MOVED somewhere else, while these have nowhere to go — the feature
1238
+ * they configured no longer exists. Merging the two lists would erase that
1239
+ * difference at exactly the moment someone is reading the file to find out what
1240
+ * happened to their setting.
1241
+ *
1242
+ * Stripped on the READ side for the same reason `stripLegacyStateKeys` is:
1243
+ * `loadPersistedConfig()` merges `{ ...DEFAULT_CONFIG, ...partial }`, so a key
1244
+ * left on disk rides that spread into memory and `updatePersistedConfig` writes it
1245
+ * straight back. The user deletes the line, launches once, and watches it return.
1246
+ */
1247
+ export const RETIRED_CONFIG_KEYS = {
1248
+ planModeMaxAskRounds: 'ask_user has no round limit any more, so this key is gone. Nothing to set; ' +
1249
+ 'the next config write removes it from config.json.',
1250
+ };
1251
+ /**
1252
+ * THE single gate that keeps retired keys out of `config.json`. Applied on the
1253
+ * read side, so it is self-healing: every write is read-strip-merge-write, and a
1254
+ * hand-edited file, a downgrade to an older CLI and back, or a half-finished
1255
+ * migration all converge on a clean file at the next write.
1256
+ */
1257
+ export function stripRetiredConfigKeys(raw) {
1258
+ const out = { ...raw };
1259
+ for (const key of Object.keys(RETIRED_CONFIG_KEYS))
1260
+ delete out[key];
1261
+ return out;
1262
+ }
1139
1263
  // ---------------------------------------------------------------------------
1140
1264
  // Coercion / validation helpers
1141
1265
  // ---------------------------------------------------------------------------
@@ -17,7 +17,7 @@ import { getConfigPath, getSessionsDir } from './app-paths.js';
17
17
  import { registerProfileSecrets, registerSecretsFrom } from '../logging/secret-registry.js';
18
18
  import { adaptLegacyConnectionPatch, ModelProfileConfigError, readModelSettingsDisk } from './model-profile-store.js';
19
19
  import { normalizeModelProfiles } from './model-profiles.js';
20
- import { clampBashConfig, clampJobsConfig, clampPromptsConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, } from './schema.js';
20
+ import { clampBashConfig, clampJobsConfig, clampPromptsConfig, clampTaskSummaryConfig, CONFIG_VERSION, DEFAULT_CONFIG, clampCompactionConfig, clampFastConfig, clampLogConfig, clampMaxTokens, clampRetryConfig, clampSkillsConfig, clampTeamConfig, clampTodoConfig, clampContextWindow, clampUpdateConfig, isAutoToken, stripLegacyStateKeys, stripRetiredConfigKeys, } from './schema.js';
21
21
  /**
22
22
  * Normalize `maxTokens` for both directions of the config file.
23
23
  *
@@ -124,7 +124,12 @@ export function normalizePersistedConfig(input) {
124
124
  // on disk would otherwise ride the spread into `merged` and be written back
125
125
  // out by `updatePersistedConfig` — the "I deleted it and it grew back" bug
126
126
  // that `stripLegacyStateKeys` exists to make impossible (§4.5).
127
- const partial = stripLegacyStateKeys(input);
127
+ //
128
+ // TWO PASSES, DELIBERATELY: `stripLegacyStateKeys` handles keys whose value
129
+ // MOVED, `stripRetiredConfigKeys` handles keys whose FEATURE was removed
130
+ // (`planModeMaxAskRounds`). Same door, different reasons, and each list keeps
131
+ // growing on its own schedule.
132
+ const partial = stripRetiredConfigKeys(stripLegacyStateKeys(input));
128
133
  delete partial.fullscreen;
129
134
  return {
130
135
  ...DEFAULT_CONFIG,
@@ -200,6 +205,14 @@ export function normalizePersistedConfig(input) {
200
205
  // `'insert'` nor `'replace'`, so a fill into a non-empty draft would take the
201
206
  // branch that REPLACES what the user typed.
202
207
  prompts: clampPromptsConfig({ ...DEFAULT_CONFIG.prompts, ...(partial.prompts ?? {}) }),
208
+ // And the thirteenth, on the same line and for the same reason. Without it a
209
+ // `config.json` predating the header label resolves the whole section to
210
+ // `undefined`, and the two readers disagree: `App`'s `!== false` checks would
211
+ // treat it as ON while `config list` would report nothing at all.
212
+ taskSummary: clampTaskSummaryConfig({
213
+ ...DEFAULT_CONFIG.taskSummary,
214
+ ...(partial.taskSummary ?? {}),
215
+ }),
203
216
  };
204
217
  }
205
218
  /**
@@ -308,11 +321,18 @@ export function mergePersistedConfig(current, patch) {
308
321
  // user would never learn that their privacy switch had been turned back on.
309
322
  jobs: clampJobsConfig({ ...current.jobs, ...(patch.jobs ?? {}) }),
310
323
  // The twelfth section, and the seventh time this file records the same trap.
311
- // `aragon config set prompts.hint false` sends `{ prompts: { hint: false } }`;
324
+ // `aragon config set prompts.insertMode replace` sends `{ prompts: { insertMode } }`;
312
325
  // without this line the shallow top-level spread REPLACES the section and
313
326
  // `insertMode` is written absent - which reads back as neither `'insert'` nor
314
327
  // `'replace'`, i.e. the next fill overwrites the user's own draft.
315
328
  prompts: clampPromptsConfig({ ...current.prompts, ...(patch.prompts ?? {}) }),
329
+ // The thirteenth section, and the eighth time this file records the same trap.
330
+ // `aragon config set taskSummary.fallback false` sends
331
+ // `{ taskSummary: { fallback: false } }`; without this line the shallow spread
332
+ // REPLACES the section and `enabled` is written absent - and the one direction
333
+ // that must never happen is a FAILED summary call landing on "show nothing"
334
+ // because a switch the user never touched defaulted away.
335
+ taskSummary: clampTaskSummaryConfig({ ...current.taskSummary, ...(patch.taskSummary ?? {}) }),
316
336
  };
317
337
  return merged;
318
338
  }
@@ -27,3 +27,12 @@ export interface DoctorOptions {
27
27
  probe?: boolean;
28
28
  }
29
29
  export declare function runDoctorChecks(flags: CliFlags, opts?: DoctorOptions): Promise<DoctorReport>;
30
+ /**
31
+ * Is the optional JSON-Schema validator importable?
32
+ *
33
+ * ASKED ONCE, HERE, AND REUSED BY `aragon info` - because "how strict is my result
34
+ * schema validated" is a property of this MACHINE, not of the contract, and there must
35
+ * be exactly one answer to it. `createRequire().resolve()` rather than `import('ajv')`
36
+ * because `info` is synchronous: resolution is the question, not loading.
37
+ */
38
+ export declare function hasAjvValidator(): boolean;
@@ -10,6 +10,7 @@
10
10
  * the caller guess, which is the state this command exists to end.
11
11
  */
12
12
  import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
13
14
  import { join } from 'node:path';
14
15
  import process from 'node:process';
15
16
  import { initProviders, ModelRegistry } from '@aragon-agent/core';
@@ -26,10 +27,43 @@ export async function runDoctorChecks(flags, opts = {}) {
26
27
  checks.push(checkProviderKey(config));
27
28
  checks.push(checkModel(config));
28
29
  checks.push(checkSessionsDir());
30
+ checks.push(checkValidationStrength());
29
31
  if (opts.probe)
30
32
  checks.push(await probeProvider(config));
31
33
  return { ok: checks.every((c) => c.verdict !== 'fail'), checks };
32
34
  }
35
+ /**
36
+ * Is the optional JSON-Schema validator importable?
37
+ *
38
+ * ASKED ONCE, HERE, AND REUSED BY `aragon info` - because "how strict is my result
39
+ * schema validated" is a property of this MACHINE, not of the contract, and there must
40
+ * be exactly one answer to it. `createRequire().resolve()` rather than `import('ajv')`
41
+ * because `info` is synchronous: resolution is the question, not loading.
42
+ */
43
+ export function hasAjvValidator() {
44
+ try {
45
+ createRequire(import.meta.url).resolve('ajv');
46
+ return true;
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ /**
53
+ * `ajv: present | absent` - a `pass`, never a `warn`.
54
+ *
55
+ * BOTH ANSWERS ARE CORRECT INSTALLS. `ajv` is an OPTIONAL dependency of Core, and its
56
+ * absence degrades validation honestly (the CLI's own belt still enforces `required`,
57
+ * recursive `properties`, `items` and `additionalProperties:false`). The check exists
58
+ * because the STRENGTH differs and nothing else in the product says so out loud.
59
+ */
60
+ function checkValidationStrength() {
61
+ if (hasAjvValidator()) {
62
+ return pass('ajv', 'present: full JSON Schema keywords are enforced');
63
+ }
64
+ return pass('ajv', 'absent: required/properties/items/additionalProperties are still enforced by the ' +
65
+ 'built-in validator; pattern/format/multipleOf are not');
66
+ }
33
67
  function pass(name, detail) {
34
68
  return { name, verdict: 'pass', detail, remedy: '' };
35
69
  }
@@ -26,5 +26,76 @@ export interface ExecInfo {
26
26
  inputFormats: string[];
27
27
  permissionModes: string[];
28
28
  features: string[];
29
+ /**
30
+ * The bridge protocol, as data (headless-host-bridge section 4.6).
31
+ *
32
+ * THE POINT OF THIS BLOCK IS THAT A CALLER SHOULD NOT HAVE TO PROBE. The
33
+ * protocol version, which decision kinds exist, which tool names are gated and
34
+ * the limits it will be held to are all answered here, because the alternative
35
+ * is "send a frame and see whether an error comes back" - discovery by damage.
36
+ */
37
+ bridge: {
38
+ protocolVersion: number;
39
+ capabilities: string[];
40
+ decisionKinds: string[];
41
+ gatedTools: string[];
42
+ limits: {
43
+ maxCallerTools: number;
44
+ maxFrameBytes: number;
45
+ defaultDecisionTimeoutMs: number;
46
+ };
47
+ /**
48
+ * The per-request ceilings (headless-request-scope-and-typed-result section 4.6),
49
+ * BESIDE `limits` rather than inside it: `limits` is what one FRAME may carry,
50
+ * these bound one REQUEST. Both read their own `*_LIMITS` constant, so neither
51
+ * restates a number the enforcement does not use.
52
+ */
53
+ requestLimits: {
54
+ maxRequestTurns: number;
55
+ maxRequestTimeoutMs: number;
56
+ };
57
+ /**
58
+ * The thread ceilings (headless-threads-and-lifecycle-hooks section 4.6).
59
+ *
60
+ * A SIBLING OF `limits` / `requestLimits` RATHER THAN A MEMBER, because the three answer
61
+ * three different questions: what one frame carries, what one request may spend, and how
62
+ * many conversations this process can host.
63
+ */
64
+ threadLimits: {
65
+ maxOpenThreads: number;
66
+ defaultMaxOpenThreads: number;
67
+ defaultConcurrency: number;
68
+ maxThreadIdLength: number;
69
+ };
70
+ /** The hook ceilings and the four points, so a wrapper need not probe for them. */
71
+ hookLimits: {
72
+ maxHooks: number;
73
+ maxHooksPerPoint: number;
74
+ defaultTimeoutMs: number;
75
+ minTimeoutMs: number;
76
+ maxTimeoutMs: number;
77
+ allowModeMaxTimeoutMs: number;
78
+ points: string[];
79
+ };
80
+ };
81
+ /**
82
+ * The structured-result contract, as data (section 4.6).
83
+ *
84
+ * A NEW BLOCK, so an older consumer simply ignores it. It exists because the
85
+ * STRENGTH of the validation is machine-dependent (`ajv` is an optional dependency
86
+ * of Core), and "which strength am I on" is not something a caller can discover by
87
+ * sending a frame.
88
+ */
89
+ result: {
90
+ schemaBytes: number;
91
+ schemaDepth: number;
92
+ schemaRefs: number;
93
+ validationNodes: number;
94
+ maxViolations: number;
95
+ valueBytes: number;
96
+ defaultRepairTurns: number;
97
+ maxRepairTurns: number;
98
+ ajv: boolean;
99
+ };
29
100
  }
30
101
  export declare function buildInfo(flags: CliFlags, version: string): ExecInfo;
@@ -15,8 +15,15 @@ import { readFileSync } from 'node:fs';
15
15
  import process from 'node:process';
16
16
  import { getConfigPath, getSessionsDir, getUserDataDir } from '../config/app-paths.js';
17
17
  import { loadConfig, makeGetApiKey } from '../config/load.js';
18
- import { HOST_TOOL_NAMES } from '../tools/index.js';
19
- import { EXEC_SCHEMA_VERSION } from '../exec/events.js';
18
+ import { CALLER_GATED_TOOLS, HOST_TOOL_NAMES } from '../tools/index.js';
19
+ import { BRIDGE_CAPABILITIES, BRIDGE_PROTOCOL_VERSION } from '../exec/bridge/protocol.js';
20
+ import { BRIDGE_LIMITS } from '../exec/bridge/limits.js';
21
+ import { EXEC_SCHEMA_VERSION, execCapabilities } from '../exec/events.js';
22
+ import { RESULT_LIMITS } from '../exec/result/limits.js';
23
+ import { REQUEST_LIMITS } from '../exec/request/limits.js';
24
+ import { THREAD_LIMITS } from '../exec/threads/limits.js';
25
+ import { HOOK_LIMITS, HOOK_POINTS } from '../exec/hooks/limits.js';
26
+ import { hasAjvValidator } from './doctor.js';
20
27
  /**
21
28
  * Everything this build can do, as names a wrapper can test for.
22
29
  *
@@ -52,11 +59,90 @@ export function buildInfo(flags, version) {
52
59
  // `HOST_TOOL_NAMES` means and exactly what `--allow-tool` validates against.
53
60
  tools: [...HOST_TOOL_NAMES],
54
61
  outputFormats: ['text', 'json', 'stream-json'],
55
- inputFormats: ['text', 'stream-json'],
56
- permissionModes: ['auto', 'plan', 'strict'],
57
- features: [...FEATURES],
62
+ // THESE TWO LINES ARE THE ANSWER TO "WHAT DOES THIS BUILD ACCEPT", and this is
63
+ // the only place that answer exists in machine-readable form. Leaving them at
64
+ // three and three while `system/init` announces `bridge` and a run accepts
65
+ // `--permission-mode ask` would be two machine-readable answers that contradict
66
+ // each other, with nothing to tell a caller which one is true.
67
+ inputFormats: ['text', 'stream-json', 'bridge'],
68
+ permissionModes: ['auto', 'plan', 'strict', 'ask'],
69
+ features: buildFeatures(),
70
+ bridge: {
71
+ protocolVersion: BRIDGE_PROTOCOL_VERSION,
72
+ capabilities: [...BRIDGE_CAPABILITIES],
73
+ decisionKinds: ['tool', 'questions', 'plan', 'skill_install'],
74
+ // FROM THE CONSTANT THE GATE USES, so a caller never has to read the docs to
75
+ // learn that `skill_create` also asks it - and so this list cannot drift from
76
+ // the one enforcement is built on.
77
+ gatedTools: [...CALLER_GATED_TOOLS],
78
+ limits: {
79
+ maxCallerTools: BRIDGE_LIMITS.maxCallerTools,
80
+ maxFrameBytes: BRIDGE_LIMITS.maxFrameBytes,
81
+ defaultDecisionTimeoutMs: BRIDGE_LIMITS.defaultDecisionTimeoutMs,
82
+ },
83
+ requestLimits: {
84
+ maxRequestTurns: REQUEST_LIMITS.maxRequestTurns,
85
+ maxRequestTimeoutMs: REQUEST_LIMITS.maxRequestTimeoutMs,
86
+ },
87
+ // THREADS AND HOOKS (headless-threads-and-lifecycle-hooks section 4.6). REFERENCED
88
+ // from `THREAD_LIMITS` / `HOOK_LIMITS`, never restated: this block and the runtime's
89
+ // clamps have to be the same numbers, and a second literal is how they stop being.
90
+ threadLimits: {
91
+ maxOpenThreads: THREAD_LIMITS.maxOpenThreads,
92
+ defaultMaxOpenThreads: THREAD_LIMITS.defaultMaxOpenThreads,
93
+ defaultConcurrency: THREAD_LIMITS.defaultConcurrency,
94
+ maxThreadIdLength: THREAD_LIMITS.maxThreadIdLength,
95
+ },
96
+ hookLimits: {
97
+ maxHooks: HOOK_LIMITS.maxHooks,
98
+ maxHooksPerPoint: HOOK_LIMITS.maxHooksPerPoint,
99
+ defaultTimeoutMs: HOOK_LIMITS.defaultTimeoutMs,
100
+ minTimeoutMs: HOOK_LIMITS.minTimeoutMs,
101
+ maxTimeoutMs: HOOK_LIMITS.maxTimeoutMs,
102
+ allowModeMaxTimeoutMs: HOOK_LIMITS.allowModeMaxTimeoutMs,
103
+ points: [...HOOK_POINTS],
104
+ },
105
+ },
106
+ result: {
107
+ schemaBytes: RESULT_LIMITS.maxSchemaBytes,
108
+ schemaDepth: RESULT_LIMITS.maxSchemaDepth,
109
+ schemaRefs: RESULT_LIMITS.maxSchemaRefs,
110
+ validationNodes: RESULT_LIMITS.maxValidationNodes,
111
+ maxViolations: RESULT_LIMITS.maxViolations,
112
+ valueBytes: RESULT_LIMITS.maxResultValueBytes,
113
+ defaultRepairTurns: RESULT_LIMITS.defaultRepairTurns,
114
+ maxRepairTurns: RESULT_LIMITS.maxRepairTurns,
115
+ ajv: hasAjvValidator(),
116
+ },
58
117
  };
59
118
  }
119
+ /**
120
+ * `features` = this build's own names PLUS the capability strings of one
121
+ * fully-featured run.
122
+ *
123
+ * DERIVED FROM `execCapabilities()`, NOT WRITTEN OUT BY HAND (review P0-4). The whole
124
+ * point of `info` is that a caller can read it BEFORE deciding which frames to send;
125
+ * if this list and `system/init.capabilities` were two hand-maintained literals, the
126
+ * first one to gain a member would silently make that strategy wrong.
127
+ * `{ bridge: true, structuredResult: true }` is the right argument here because this
128
+ * command answers "what can this build do", not "what did one run do".
129
+ */
130
+ function buildFeatures() {
131
+ const features = [...FEATURES];
132
+ for (const capability of execCapabilities({
133
+ bridge: true,
134
+ structuredResult: true,
135
+ // THE SUPERSET ARGUMENT AGAIN: `info` answers "what can this build do", so the two new
136
+ // capabilities are declared here and only HERE - a run that does not open `--threads`
137
+ // still reports them in its own `init` as absent, from the same derivation point.
138
+ threads: true,
139
+ hooks: true,
140
+ })) {
141
+ if (!features.includes(capability))
142
+ features.push(capability);
143
+ }
144
+ return features;
145
+ }
60
146
  /**
61
147
  * The installed `@aragon-agent/core` version, or `'unknown'`.
62
148
  *
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Caller-served tools: what a `tool_register` frame becomes, and how it runs.
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * ## I-12: THESE TOOLS LIVE OUTSIDE THE HOST'S JURISDICTION, ON PURPOSE
7
+ *
8
+ * Everything below is built HERE and handed straight to the registry. That means a
9
+ * caller tool is NOT covered by the plan gate, NOT covered by a skill's
10
+ * `allowed-tools` ceiling, and has no job-notice tail - because it never passes
11
+ * through `createBuiltinTools`, which is where all three wrappers are applied.
12
+ *
13
+ * This is a decision, not an oversight, and it is written down because the next
14
+ * person to read this file will otherwise assume the wrappers are there. The
15
+ * defences that DO apply are: plan mode refuses the whole `tool_register` frame
16
+ * (`plan_mode_read_only`), and a caller tool may not shadow any host, team or
17
+ * skill tool name (`caller_tool_conflict`). Both are refusals at registration
18
+ * time, which is the only moment this module can still see the caller's intent.
19
+ *
20
+ * ## Why `validateCallerTools` REFUSES WHOLE FRAMES
21
+ *
22
+ * A partially accepted registration would leave the registry holding a mixture of
23
+ * two declarations - the caller's old `db_query` and its new `db_write` - with
24
+ * nothing anywhere saying which of the two the model is now using. The caller's
25
+ * natural recovery ("just send the fixed list again") only works if the previous
26
+ * frame left no trace, so every check below runs BEFORE the first write.
27
+ */
28
+ import { type AgentTool, type ToolResult } from '@aragon-agent/core';
29
+ import type { BridgeServer } from './server.js';
30
+ import type { CallerToolSpec } from './protocol.js';
31
+ export interface CallerToolValidation {
32
+ ok: true;
33
+ specs: CallerToolSpec[];
34
+ /** Non-fatal notes for `ack.warnings` - a clipped description, so far. */
35
+ warnings: string[];
36
+ }
37
+ export interface CallerToolRejection {
38
+ ok: false;
39
+ code: string;
40
+ message: string;
41
+ }
42
+ export type CallerToolValidationResult = CallerToolValidation | CallerToolRejection;
43
+ /**
44
+ * Narrow whatever arrived in `tool_register.tools` into usable specs.
45
+ *
46
+ * `reserved` is a PARAMETER rather than an import so the caller decides what a
47
+ * name may not collide with - and so the test can hand over a two-name set instead
48
+ * of modelling the whole host.
49
+ */
50
+ export declare function validateCallerTools(rawTools: readonly unknown[], reserved: ReadonlySet<string>): CallerToolValidationResult;
51
+ /**
52
+ * The effective ceiling for one call: `min(spec.timeoutMs, --caller-tool-timeout)`.
53
+ *
54
+ * MIN, NOT MAX, because both numbers are ceilings on the same thing: the caller's
55
+ * own declaration is a promise about its implementation ("this query takes at most
56
+ * 20 s") and the flag is the run's budget. Taking the larger of the two would let
57
+ * a declared 600 s override a 10 s budget.
58
+ */
59
+ export declare function resolveCallerToolTimeout(spec: CallerToolSpec | undefined, defaultTimeoutMs: number): number;
60
+ /**
61
+ * Turn specs into `AgentTool`s that round-trip through the server.
62
+ *
63
+ * EVERY PATH RETURNS A `ToolResult`. Nothing here throws: `ToolExecutor` would
64
+ * convert a throw into an error result of its own wording, and that wording says
65
+ * nothing about which of the four refusals happened.
66
+ */
67
+ export declare function createCallerTools(specs: readonly CallerToolSpec[], server: BridgeServer, defaultTimeoutMs: number): AgentTool[];
68
+ /**
69
+ * Coerce a `tool_result.content` into text, or refuse it.
70
+ *
71
+ * TEXT ONLY, THIS ROUND. An image part would need `imageResult` and the provider's
72
+ * multimodal path, which is a separate feature with its own budget questions; a
73
+ * half-support that silently dropped the image would be worse than a named
74
+ * refusal (`bridge_unsupported_content`).
75
+ */
76
+ export declare function contentToText(content: unknown): {
77
+ ok: true;
78
+ text: string;
79
+ } | {
80
+ ok: false;
81
+ };
82
+ /**
83
+ * Clip to `maxCallerToolResultBytes` and SAY SO.
84
+ *
85
+ * The footer is not decoration: `ToolExecutor` already clips model-visible output
86
+ * at its own ceiling, so a silent second clip here would look like the tool
87
+ * returned less than it did, and the model would draw conclusions from a truncated
88
+ * table without being told it was truncated.
89
+ */
90
+ export declare function clipCallerToolText(text: string): string;
91
+ /** Build the result a `tool_result` frame becomes, or a named refusal. */
92
+ export declare function callerToolResult(content: unknown, isError: boolean): ToolResult;
93
+ /** Thrown only so `server.ts` can map it to one error code in one place. */
94
+ export declare class UnsupportedContentError extends Error {
95
+ constructor();
96
+ }