@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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Every ceiling one request is held to (headless-request-scope-and-typed-result
3
+ * section 4.3).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * STRUCTURE, NOT POLICY, for the reason `exec/result/limits.ts` records: a request
8
+ * budget is a property of the WIRE (what one `request_end` can carry, what one
9
+ * timer may bound), not a user preference, so nothing here is persisted and nothing
10
+ * here is reachable through `config.json`.
11
+ */
12
+ export declare const REQUEST_LIMITS: {
13
+ /**
14
+ * The shortest request wall-clock budget worth having.
15
+ *
16
+ * Below a second the flag would only ever produce `timeout` before the first
17
+ * provider round trip finishes, which is a footgun rather than a budget.
18
+ */
19
+ readonly minRequestTimeoutMs: 1000;
20
+ /**
21
+ * The longest one request may be bounded to.
22
+ *
23
+ * SAME VALUE AS `BRIDGE_LIMITS.maxDecisionTimeoutMs` ON PURPOSE: both answer
24
+ * "how long may a caller make this process wait without hearing from it", and
25
+ * two different answers to that question in one protocol is a bug waiting for a
26
+ * caller to find it.
27
+ */
28
+ readonly maxRequestTimeoutMs: 1800000;
29
+ /** Turns one request may take. Far above any real prompt; a runaway guard. */
30
+ readonly maxRequestTurns: 200;
31
+ /**
32
+ * Request ids remembered as "already settled", for de-duplication.
33
+ *
34
+ * NOT the emitter of the event (see the two writers in `runner.ts`): it is the
35
+ * authority that makes "exactly one `request_end` per request" true. A FIFO
36
+ * because a long-lived process must not grow it without bound.
37
+ */
38
+ readonly maxTrackedRequests: 512;
39
+ /**
40
+ * `request_end.result` character ceiling.
41
+ *
42
+ * EVENT FRAME TRUNCATION, not the value ceiling - the full text is still on the
43
+ * `assistant` event, and the accepted structured value has its own limit in
44
+ * `RESULT_LIMITS.maxResultValueBytes`. Keeping the two ideas apart is what stops
45
+ * "the answer got cut" from being indistinguishable from "the answer was too
46
+ * big to accept".
47
+ */
48
+ readonly maxResultChars: 8192;
49
+ };
50
+ /** The union of every value in `REQUEST_LIMITS`; useful for tests and docs. */
51
+ export type RequestLimits = typeof REQUEST_LIMITS;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Every ceiling one request is held to (headless-request-scope-and-typed-result
3
+ * section 4.3).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * STRUCTURE, NOT POLICY, for the reason `exec/result/limits.ts` records: a request
8
+ * budget is a property of the WIRE (what one `request_end` can carry, what one
9
+ * timer may bound), not a user preference, so nothing here is persisted and nothing
10
+ * here is reachable through `config.json`.
11
+ */
12
+ export const REQUEST_LIMITS = {
13
+ /**
14
+ * The shortest request wall-clock budget worth having.
15
+ *
16
+ * Below a second the flag would only ever produce `timeout` before the first
17
+ * provider round trip finishes, which is a footgun rather than a budget.
18
+ */
19
+ minRequestTimeoutMs: 1_000,
20
+ /**
21
+ * The longest one request may be bounded to.
22
+ *
23
+ * SAME VALUE AS `BRIDGE_LIMITS.maxDecisionTimeoutMs` ON PURPOSE: both answer
24
+ * "how long may a caller make this process wait without hearing from it", and
25
+ * two different answers to that question in one protocol is a bug waiting for a
26
+ * caller to find it.
27
+ */
28
+ maxRequestTimeoutMs: 1_800_000,
29
+ /** Turns one request may take. Far above any real prompt; a runaway guard. */
30
+ maxRequestTurns: 200,
31
+ /**
32
+ * Request ids remembered as "already settled", for de-duplication.
33
+ *
34
+ * NOT the emitter of the event (see the two writers in `runner.ts`): it is the
35
+ * authority that makes "exactly one `request_end` per request" true. A FIFO
36
+ * because a long-lived process must not grow it without bound.
37
+ */
38
+ maxTrackedRequests: 512,
39
+ /**
40
+ * `request_end.result` character ceiling.
41
+ *
42
+ * EVENT FRAME TRUNCATION, not the value ceiling - the full text is still on the
43
+ * `assistant` event, and the accepted structured value has its own limit in
44
+ * `RESULT_LIMITS.maxResultValueBytes`. Keeping the two ideas apart is what stops
45
+ * "the answer got cut" from being indistinguishable from "the answer was too
46
+ * big to accept".
47
+ */
48
+ maxResultChars: 8_192,
49
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * One request, as an object with a budget, an account and a cancellation
3
+ * (headless-request-scope-and-typed-result section 2.4.3).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * ONE WRITER FOR EVERY REQUEST-LEVEL FACT. The turn count, the wall clock, the
8
+ * cancellation intent, the "already aborted for an accepted result" latch and the
9
+ * de-duplication table all live in here, so the runner only ever ASKS. A second
10
+ * writer anywhere is how "the request ended but nobody was told" happens.
11
+ *
12
+ * THE TIMER IS THE SUBTLE PART (review P1-4). A `setTimeout` that fires after the
13
+ * request it belongs to has ended would abort the NEXT request - one request's
14
+ * budget killing another, which is the single thing this whole file exists to
15
+ * prevent. Three latches, not one: `begin()` clears the previous timer and bumps a
16
+ * generation, the callback refuses to act unless its generation is still current,
17
+ * and `end()` clears the timer synchronously. This codebase has paid for the same
18
+ * lesson three times (`runGen`, `abortRequested`, `requestVersion`); the comment is
19
+ * here so the fourth time is cheaper.
20
+ */
21
+ import type { ExecCost, ExecRequestError, ExecRequestStopReason, ExecUsage } from '../events.js';
22
+ /** The process-level counters a request is measured against. */
23
+ export interface RequestTotals {
24
+ usage: {
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ };
28
+ cost: ExecCost;
29
+ }
30
+ export interface RequestScopeOptions {
31
+ /** `--request-max-turns`, or absent for "no ceiling". */
32
+ maxTurns?: number;
33
+ /** `--request-max-duration`, or absent. Bridge-only, like the turns ceiling. */
34
+ maxDurationMs?: number;
35
+ now: () => number;
36
+ /**
37
+ * Called when the wall-clock budget expires FOR THE CURRENT REQUEST ONLY.
38
+ *
39
+ * Takes the generation so a caller can assert what it received: a callback whose
40
+ * number is not the scope's current generation must do nothing.
41
+ */
42
+ onDurationExpired: (generation: number) => void;
43
+ /** The process-level totals to diff against; read at `begin()` and at `end()`. */
44
+ totals: () => RequestTotals;
45
+ }
46
+ /** What `end()` hands the runner so it can write one `request_end`. */
47
+ export interface RequestSettlement {
48
+ requestId: string;
49
+ requestSeq?: number;
50
+ stopReason: ExecRequestStopReason;
51
+ isError: boolean;
52
+ turns: number;
53
+ durationMs: number;
54
+ usage: ExecUsage;
55
+ cost: ExecCost;
56
+ error: ExecRequestError | null;
57
+ }
58
+ export interface RequestScope {
59
+ /** Start a request: resets everything except the process-level totals. */
60
+ begin(input: {
61
+ requestSeq: number;
62
+ requestId?: string;
63
+ }): void;
64
+ requestId(): string | undefined;
65
+ requestSeq(): number | undefined;
66
+ /** One turn finished (called from the same place the process counter is bumped). */
67
+ countTurn(): void;
68
+ turns(): number;
69
+ /** Turns exhausted? `false` when there is no ceiling. */
70
+ turnsExhausted(): boolean;
71
+ /** Wall clock over budget, checked lazily at turn boundaries. */
72
+ isOverDurationBudget(now: number): boolean;
73
+ /** Cancel this request. Idempotent; the first reason wins. */
74
+ cancel(reason: ExecRequestStopReason): void;
75
+ cancelledReason(): ExecRequestStopReason | null;
76
+ /** The I-S8 latch: has this request already been aborted for an accepted result? */
77
+ acceptanceAborted(): boolean;
78
+ markAcceptanceAborted(): void;
79
+ /**
80
+ * Finish the request and return its account. THROWS on the second call: a repeated
81
+ * `request_end` would make a caller de-duplicate by id and silently lose the
82
+ * second account.
83
+ *
84
+ * `stopReason` is the PROCESS-LEVEL reason and is used only when this request was
85
+ * not cancelled on its own terms (section 2.4.2). A process `interrupt`, a spent
86
+ * `--max-turns` and a `--max-duration` end the run underneath a live request, and
87
+ * that request still has to say why it ended - `completed` there is a lie a caller
88
+ * reads as a finished answer.
89
+ */
90
+ end(input: {
91
+ now: number;
92
+ error?: ExecRequestError | null;
93
+ stopReason?: ExecRequestStopReason;
94
+ }): RequestSettlement;
95
+ /** Differential account: what changed since `begin()`. */
96
+ usageDelta(): {
97
+ usage: ExecUsage;
98
+ cost: ExecCost;
99
+ };
100
+ /** Remember a settled request id. Returns `false` when it was already known. */
101
+ remember(requestId: string): boolean;
102
+ hasRemembered(requestId: string): boolean;
103
+ /** Clear the timer; safe to call twice. */
104
+ detach(): void;
105
+ }
106
+ export declare function createRequestScope(options: RequestScopeOptions): RequestScope;
@@ -0,0 +1,149 @@
1
+ import { REQUEST_LIMITS } from './limits.js';
2
+ const ZERO_TOTALS = {
3
+ usage: { inputTokens: 0, outputTokens: 0 },
4
+ cost: { amount: 0, currency: 'USD', known: false },
5
+ };
6
+ export function createRequestScope(options) {
7
+ let requestSeq;
8
+ let requestId;
9
+ let startedAt = 0;
10
+ let turnCount = 0;
11
+ let cancelReason = null;
12
+ let abortedForAcceptance = false;
13
+ let ended = true;
14
+ let baseline = ZERO_TOTALS;
15
+ let generation = 0;
16
+ let timer = null;
17
+ let detached = false;
18
+ /** Bounded FIFO of settled ids: the authority behind "exactly one request_end". */
19
+ const remembered = new Set();
20
+ const clearTimer = () => {
21
+ if (!timer)
22
+ return;
23
+ clearTimeout(timer);
24
+ timer = null;
25
+ };
26
+ const readTotals = () => {
27
+ try {
28
+ return options.totals();
29
+ }
30
+ catch {
31
+ // A controller that has already been disposed must not turn accounting into an
32
+ // exception on the way out of a run.
33
+ return ZERO_TOTALS;
34
+ }
35
+ };
36
+ const scope = {
37
+ begin(input) {
38
+ // CLEAR FIRST, THEN BUMP: a callback that is already queued on the event loop
39
+ // will observe the new generation and refuse to act.
40
+ clearTimer();
41
+ generation += 1;
42
+ requestSeq = input.requestSeq;
43
+ requestId = input.requestId;
44
+ startedAt = options.now();
45
+ turnCount = 0;
46
+ cancelReason = null;
47
+ abortedForAcceptance = false;
48
+ ended = false;
49
+ baseline = readTotals();
50
+ if (options.maxDurationMs === undefined)
51
+ return;
52
+ const mine = generation;
53
+ timer = setTimeout(() => {
54
+ // THE LATCH THAT MAKES THIS SAFE. Without it, a request whose budget expired
55
+ // while a long synchronous task held the loop would abort whichever request
56
+ // happened to be running by then.
57
+ if (detached || mine !== generation || ended)
58
+ return;
59
+ scope.cancel('timeout');
60
+ options.onDurationExpired(mine);
61
+ }, options.maxDurationMs);
62
+ timer.unref?.();
63
+ },
64
+ requestId: () => requestId,
65
+ requestSeq: () => requestSeq,
66
+ countTurn() {
67
+ turnCount += 1;
68
+ },
69
+ turns: () => turnCount,
70
+ turnsExhausted() {
71
+ return options.maxTurns !== undefined && turnCount >= options.maxTurns;
72
+ },
73
+ isOverDurationBudget(now) {
74
+ if (options.maxDurationMs === undefined)
75
+ return false;
76
+ return now - startedAt >= options.maxDurationMs;
77
+ },
78
+ cancel(reason) {
79
+ if (cancelReason === null)
80
+ cancelReason = reason;
81
+ },
82
+ cancelledReason: () => cancelReason,
83
+ acceptanceAborted: () => abortedForAcceptance,
84
+ markAcceptanceAborted() {
85
+ abortedForAcceptance = true;
86
+ },
87
+ end(input) {
88
+ if (ended) {
89
+ throw new Error('RequestScope.end() called twice for one request');
90
+ }
91
+ ended = true;
92
+ clearTimer();
93
+ const delta = scope.usageDelta();
94
+ // THE REQUEST'S OWN CANCELLATION WINS; the process-level reason is only a
95
+ // fallback, so "the caller cancelled this one" is never overwritten by "the run
96
+ // was interrupted a moment later".
97
+ const reason = cancelReason ?? input.stopReason ?? 'completed';
98
+ return {
99
+ requestId: requestId ?? 'unknown',
100
+ ...(requestSeq !== undefined ? { requestSeq } : {}),
101
+ stopReason: reason,
102
+ isError: input.error !== null && input.error !== undefined,
103
+ turns: turnCount,
104
+ durationMs: Math.max(0, input.now - startedAt),
105
+ usage: delta.usage,
106
+ cost: delta.cost,
107
+ error: input.error ?? null,
108
+ };
109
+ },
110
+ usageDelta() {
111
+ const current = readTotals();
112
+ return {
113
+ usage: {
114
+ inputTokens: current.usage.inputTokens - baseline.usage.inputTokens,
115
+ outputTokens: current.usage.outputTokens - baseline.usage.outputTokens,
116
+ totalTokens: current.usage.inputTokens + current.usage.outputTokens -
117
+ (baseline.usage.inputTokens + baseline.usage.outputTokens),
118
+ },
119
+ // A DIFFERENCE, NOT A COPY. `known` rides along from the current reading: a
120
+ // model with no price table must report "unknown" for the request exactly as
121
+ // it does for the process, instead of a confident zero.
122
+ cost: {
123
+ amount: current.cost.amount - baseline.cost.amount,
124
+ currency: 'USD',
125
+ known: current.cost.known,
126
+ },
127
+ };
128
+ },
129
+ remember(id) {
130
+ if (remembered.has(id))
131
+ return false;
132
+ remembered.add(id);
133
+ // A FIFO, because a long-lived process serves an unbounded number of requests
134
+ // and the set only exists to answer "did this one already settle".
135
+ if (remembered.size > REQUEST_LIMITS.maxTrackedRequests) {
136
+ const oldest = remembered.values().next();
137
+ if (!oldest.done)
138
+ remembered.delete(oldest.value);
139
+ }
140
+ return true;
141
+ },
142
+ hasRemembered: (id) => remembered.has(id),
143
+ detach() {
144
+ detached = true;
145
+ clearTimer();
146
+ },
147
+ };
148
+ return scope;
149
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Every ceiling `--result-schema` is held to (headless-request-scope-and-typed-result
3
+ * section 4.3).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * STRUCTURE, NOT POLICY. These numbers are what a protocol frame and a
8
+ * model-visible string can PHYSICALLY carry, not a user preference, so they do not
9
+ * live in `config.json` and are not tunable per run. That is the same split
10
+ * `exec/bridge/limits.ts` records for the decision channel, and the same one
11
+ * `CLAUDE.md` states between a subsystem's `limits.ts` and `config/schema.ts`.
12
+ *
13
+ * ONE AUTHORITY PER NUMBER. `diagnostics/info.ts` reads this object rather than
14
+ * restating the numbers, so a caller that asks "how big may my schema be" gets the
15
+ * number the validator actually enforces.
16
+ */
17
+ export declare const RESULT_LIMITS: {
18
+ /**
19
+ * One schema file.
20
+ *
21
+ * WHY 64 KiB AND NOT THE 256 KiB VALUE CAP: a schema is a HUMAN-AUTHORED
22
+ * contract that a person reads and reviews; a value is a MACHINE PRODUCT a model
23
+ * emits. The two have no reason to share a ceiling, and the smaller one is what
24
+ * keeps `--result-schema` from becoming "paste your whole type system here".
25
+ */
26
+ readonly maxSchemaBytes: 65536;
27
+ /**
28
+ * Nesting depth of the schema itself.
29
+ *
30
+ * 12 is deeper than any contract this feature is for (an object of objects of
31
+ * arrays of objects is depth 6) and shallow enough that the depth walk in
32
+ * `schema.ts` cannot be turned into a stack overflow by a hostile file.
33
+ */
34
+ readonly maxSchemaDepth: 12;
35
+ /** `$ref` occurrences. A schema that needs more than 64 is a type system. */
36
+ readonly maxSchemaRefs: 64;
37
+ /**
38
+ * Nodes visited by one validation. Exceeding it REJECTS the value.
39
+ *
40
+ * WITHOUT THIS, a deep or wildly wide object makes a headless process spin
41
+ * inside its own validator - a denial of service the model can trigger by
42
+ * accident and a caller cannot see. 5000 is far above any real contract.
43
+ */
44
+ readonly maxValidationNodes: 5000;
45
+ /** Violations RETURNED TO THE MODEL, and the width of one path. */
46
+ readonly maxViolations: 12;
47
+ readonly maxViolationPathChars: 120;
48
+ /**
49
+ * Bytes of the accepted value, after canonicalization.
50
+ *
51
+ * `result.result` uses this same number when the value is serialized: a second,
52
+ * differently sized ceiling would only give the next reader a fork in the road
53
+ * about which one applies.
54
+ */
55
+ readonly maxResultValueBytes: 262144;
56
+ /** The repair loop: default, hard ceiling, and the message it may produce. */
57
+ readonly defaultRepairTurns: 1;
58
+ readonly maxRepairTurns: 2;
59
+ readonly maxRepairPromptChars: 4000;
60
+ };
61
+ /**
62
+ * The value's own recursion guard.
63
+ *
64
+ * IN `RESULT_LIMITS` WITH EVERY OTHER CEILING (implementation note): the design
65
+ * listed eight numbers and this is the ninth. It belongs here because it bounds
66
+ * exactly what `maxValidationNodes` bounds - how much work one untrusted value can
67
+ * cause - and because a second, differently-placed guard is how two limits end up
68
+ * disagreeing. 64 is far deeper than any contract (a JSON document from a model
69
+ * that nests 64 levels is pathological) and shallow enough that the recursive walk
70
+ * cannot exhaust the stack.
71
+ */
72
+ export declare const RESULT_MAX_VALUE_DEPTH = 64;
73
+ /** The union of every value in `RESULT_LIMITS`; useful for tests and docs. */
74
+ export type ResultLimits = typeof RESULT_LIMITS;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Every ceiling `--result-schema` is held to (headless-request-scope-and-typed-result
3
+ * section 4.3).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * STRUCTURE, NOT POLICY. These numbers are what a protocol frame and a
8
+ * model-visible string can PHYSICALLY carry, not a user preference, so they do not
9
+ * live in `config.json` and are not tunable per run. That is the same split
10
+ * `exec/bridge/limits.ts` records for the decision channel, and the same one
11
+ * `CLAUDE.md` states between a subsystem's `limits.ts` and `config/schema.ts`.
12
+ *
13
+ * ONE AUTHORITY PER NUMBER. `diagnostics/info.ts` reads this object rather than
14
+ * restating the numbers, so a caller that asks "how big may my schema be" gets the
15
+ * number the validator actually enforces.
16
+ */
17
+ export const RESULT_LIMITS = {
18
+ /**
19
+ * One schema file.
20
+ *
21
+ * WHY 64 KiB AND NOT THE 256 KiB VALUE CAP: a schema is a HUMAN-AUTHORED
22
+ * contract that a person reads and reviews; a value is a MACHINE PRODUCT a model
23
+ * emits. The two have no reason to share a ceiling, and the smaller one is what
24
+ * keeps `--result-schema` from becoming "paste your whole type system here".
25
+ */
26
+ maxSchemaBytes: 65_536,
27
+ /**
28
+ * Nesting depth of the schema itself.
29
+ *
30
+ * 12 is deeper than any contract this feature is for (an object of objects of
31
+ * arrays of objects is depth 6) and shallow enough that the depth walk in
32
+ * `schema.ts` cannot be turned into a stack overflow by a hostile file.
33
+ */
34
+ maxSchemaDepth: 12,
35
+ /** `$ref` occurrences. A schema that needs more than 64 is a type system. */
36
+ maxSchemaRefs: 64,
37
+ /**
38
+ * Nodes visited by one validation. Exceeding it REJECTS the value.
39
+ *
40
+ * WITHOUT THIS, a deep or wildly wide object makes a headless process spin
41
+ * inside its own validator - a denial of service the model can trigger by
42
+ * accident and a caller cannot see. 5000 is far above any real contract.
43
+ */
44
+ maxValidationNodes: 5_000,
45
+ /** Violations RETURNED TO THE MODEL, and the width of one path. */
46
+ maxViolations: 12,
47
+ maxViolationPathChars: 120,
48
+ /**
49
+ * Bytes of the accepted value, after canonicalization.
50
+ *
51
+ * `result.result` uses this same number when the value is serialized: a second,
52
+ * differently sized ceiling would only give the next reader a fork in the road
53
+ * about which one applies.
54
+ */
55
+ maxResultValueBytes: 262_144,
56
+ /** The repair loop: default, hard ceiling, and the message it may produce. */
57
+ defaultRepairTurns: 1,
58
+ maxRepairTurns: 2,
59
+ maxRepairPromptChars: 4_000,
60
+ };
61
+ /**
62
+ * The value's own recursion guard.
63
+ *
64
+ * IN `RESULT_LIMITS` WITH EVERY OTHER CEILING (implementation note): the design
65
+ * listed eight numbers and this is the ninth. It belongs here because it bounds
66
+ * exactly what `maxValidationNodes` bounds - how much work one untrusted value can
67
+ * cause - and because a second, differently-placed guard is how two limits end up
68
+ * disagreeing. 64 is far deeper than any contract (a JSON document from a model
69
+ * that nests 64 levels is pathological) and shallow enough that the recursive walk
70
+ * cannot exhaust the stack.
71
+ */
72
+ export const RESULT_MAX_VALUE_DEPTH = 64;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bumped when the block's WORDING changes in a way an evaluation would notice.
3
+ *
4
+ * It is stamped into the block itself, so a transcript shows which instructions the
5
+ * model was actually given - the difference between "the model ignored it" and "the
6
+ * model was told something else" is otherwise unanswerable after the fact.
7
+ */
8
+ export declare const STRUCTURED_RESULT_BLOCK_VERSION = "v1-2026-10";
9
+ export interface StructuredResultBlockOptions {
10
+ /** `'submit_result'`. A parameter rather than a literal so the two cannot drift. */
11
+ toolName: string;
12
+ /** `true` when the caller's schema carried `definitions` / `$defs`. */
13
+ hasDefinitions: boolean;
14
+ /** `--result-repair-turns`: how many automatic retries the caller allowed. */
15
+ repairTurns: number;
16
+ }
17
+ /** The block, or `''` when the caller supplied no schema (I-S1: byte-identical). */
18
+ export declare function buildStructuredResultBlock(options: StructuredResultBlockOptions): string;
19
+ /**
20
+ * The message the CLI sends on the caller's behalf when a request ended without an
21
+ * accepted result (section 2.3.5).
22
+ *
23
+ * IT QUOTES THE FIELD PATHS, not the whole value: the model already has the value in
24
+ * its own tool call, and what it does not have is which fields failed. Bounded by
25
+ * `maxRepairPromptChars` for the reason every model-visible string here is bounded -
26
+ * an unbounded line would ride into every later provider request.
27
+ */
28
+ export declare function buildResultRepairMessage(violations: string[], attempts: number): string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The `<structured_result>` prompt block and the repair message
3
+ * (headless-request-scope-and-typed-result section 2.3.8).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * ONE SOURCE FOR BOTH STRINGS, so the block a model reads and the message it gets
8
+ * when it ignores the block cannot drift apart. The version constant exists for the
9
+ * reason every other `*_BLOCK_VERSION` in this package does: a wording change becomes
10
+ * greppable from a behaviour report instead of an argument.
11
+ */
12
+ import { RESULT_LIMITS } from './limits.js';
13
+ /**
14
+ * Bumped when the block's WORDING changes in a way an evaluation would notice.
15
+ *
16
+ * It is stamped into the block itself, so a transcript shows which instructions the
17
+ * model was actually given - the difference between "the model ignored it" and "the
18
+ * model was told something else" is otherwise unanswerable after the fact.
19
+ */
20
+ export const STRUCTURED_RESULT_BLOCK_VERSION = 'v1-2026-10';
21
+ /** The block, or `''` when the caller supplied no schema (I-S1: byte-identical). */
22
+ export function buildStructuredResultBlock(options) {
23
+ const lines = [
24
+ `<structured_result version="${STRUCTURED_RESULT_BLOCK_VERSION}">`,
25
+ `Your final answer must be submitted with the \`${options.toolName}\` tool, exactly once.`,
26
+ `The tool's \`result\` parameter is validated against the schema in its definition; a`,
27
+ `value that does not match is REJECTED and you get the field paths that failed.`,
28
+ '',
29
+ 'Rules:',
30
+ `- Do not also print the answer as prose. The submission IS the answer.`,
31
+ `- Call \`${options.toolName}\` when you are done, then stop.`,
32
+ `- If it returns an error, fix the fields it names and call it again with the whole object.`,
33
+ `- A second successful call changes nothing: the first accepted value is kept.`,
34
+ ];
35
+ if (options.repairTurns > 0) {
36
+ lines.push(`- If you never submit, you will be asked at most ${options.repairTurns} more time(s).`);
37
+ }
38
+ else {
39
+ lines.push('- If you never submit, this run fails: there is no automatic retry.');
40
+ }
41
+ lines.push('- If the schema genuinely cannot be satisfied, say why and stop. That is a', ' recognisable failure (`result_schema_unsatisfied`) and is better than prose', ' that looks like success.');
42
+ if (options.hasDefinitions) {
43
+ // THE ONE THING A CALLER'S `$ref` NEEDS SAID OUT LOUD. The definitions were
44
+ // lifted to the parameter schema's root so `#/definitions/X` still resolves; a
45
+ // model that "helpfully" re-inlines them wastes tokens and risks a mismatch.
46
+ lines.push('', 'The schema uses `$ref` with `#/definitions/...` or `#/$defs/...`. Those resolve', 'inside the tool definition - do not expand or rewrite them.');
47
+ }
48
+ lines.push('</structured_result>');
49
+ return lines.join('\n');
50
+ }
51
+ /**
52
+ * The message the CLI sends on the caller's behalf when a request ended without an
53
+ * accepted result (section 2.3.5).
54
+ *
55
+ * IT QUOTES THE FIELD PATHS, not the whole value: the model already has the value in
56
+ * its own tool call, and what it does not have is which fields failed. Bounded by
57
+ * `maxRepairPromptChars` for the reason every model-visible string here is bounded -
58
+ * an unbounded line would ride into every later provider request.
59
+ */
60
+ export function buildResultRepairMessage(violations, attempts) {
61
+ const listed = violations.length > 0
62
+ ? violations.join('\n')
63
+ : 'result: required (nothing was submitted)';
64
+ const text = [
65
+ `Your answer was not submitted as a structured result.`,
66
+ `Attempts so far: ${attempts}.`,
67
+ `The last rejection named these fields:`,
68
+ listed,
69
+ '',
70
+ `Fix them and call the submit tool again with the WHOLE object, then stop.`,
71
+ ].join('\n');
72
+ const max = RESULT_LIMITS.maxRepairPromptChars;
73
+ return text.length <= max ? text : `${text.slice(0, max)}\n[truncated]`;
74
+ }