@aragon-agent/cli 0.6.38 → 0.6.39

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 (156) hide show
  1. package/CHANGELOG.md +2523 -2326
  2. package/README.md +3614 -3173
  3. package/dist/agent/controller.d.ts +142 -1
  4. package/dist/agent/controller.js +171 -3
  5. package/dist/agent/reducer.d.ts +41 -1
  6. package/dist/agent/reducer.js +25 -0
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +5 -0
  9. package/dist/cli.js +85 -27
  10. package/dist/commands/builtins.js +13 -1
  11. package/dist/commands/keys-command.d.ts +23 -0
  12. package/dist/commands/keys-command.js +38 -0
  13. package/dist/commands/registry.d.ts +39 -0
  14. package/dist/commands/task-command.d.ts +39 -0
  15. package/dist/commands/task-command.js +102 -0
  16. package/dist/config/cli-commands.d.ts +23 -0
  17. package/dist/config/cli-commands.js +36 -0
  18. package/dist/config/env.js +17 -4
  19. package/dist/config/load.js +6 -1
  20. package/dist/config/schema.d.ts +43 -4
  21. package/dist/config/schema.js +31 -5
  22. package/dist/config/store.js +17 -2
  23. package/dist/diagnostics/doctor.d.ts +9 -0
  24. package/dist/diagnostics/doctor.js +34 -0
  25. package/dist/diagnostics/info.d.ts +71 -0
  26. package/dist/diagnostics/info.js +91 -5
  27. package/dist/exec/bridge/caller-tools.d.ts +96 -0
  28. package/dist/exec/bridge/caller-tools.js +213 -0
  29. package/dist/exec/bridge/decisions.d.ts +78 -0
  30. package/dist/exec/bridge/decisions.js +116 -0
  31. package/dist/exec/bridge/human-answers.d.ts +38 -0
  32. package/dist/exec/bridge/human-answers.js +164 -0
  33. package/dist/exec/bridge/limits.d.ts +68 -0
  34. package/dist/exec/bridge/limits.js +68 -0
  35. package/dist/exec/bridge/ndjson.d.ts +17 -0
  36. package/dist/exec/bridge/ndjson.js +83 -0
  37. package/dist/exec/bridge/pending.d.ts +70 -0
  38. package/dist/exec/bridge/pending.js +132 -0
  39. package/dist/exec/bridge/protocol.d.ts +346 -0
  40. package/dist/exec/bridge/protocol.js +256 -0
  41. package/dist/exec/bridge/server.d.ts +142 -0
  42. package/dist/exec/bridge/server.js +502 -0
  43. package/dist/exec/bridge/source.d.ts +58 -0
  44. package/dist/exec/bridge/source.js +145 -0
  45. package/dist/exec/bridge/writer.d.ts +26 -0
  46. package/dist/exec/bridge/writer.js +156 -0
  47. package/dist/exec/cli-commands.js +30 -2
  48. package/dist/exec/emitter.d.ts +16 -2
  49. package/dist/exec/emitter.js +26 -6
  50. package/dist/exec/events.d.ts +465 -10
  51. package/dist/exec/events.js +47 -1
  52. package/dist/exec/hooks/dispatch.d.ts +76 -0
  53. package/dist/exec/hooks/dispatch.js +169 -0
  54. package/dist/exec/hooks/index.d.ts +83 -0
  55. package/dist/exec/hooks/index.js +205 -0
  56. package/dist/exec/hooks/limits.d.ts +56 -0
  57. package/dist/exec/hooks/limits.js +49 -0
  58. package/dist/exec/hooks/registry.d.ts +49 -0
  59. package/dist/exec/hooks/registry.js +91 -0
  60. package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
  61. package/dist/exec/hooks/tool-wrapper.js +154 -0
  62. package/dist/exec/hooks/turn.d.ts +61 -0
  63. package/dist/exec/hooks/turn.js +59 -0
  64. package/dist/exec/hooks/validate.d.ts +85 -0
  65. package/dist/exec/hooks/validate.js +310 -0
  66. package/dist/exec/index.d.ts +130 -4
  67. package/dist/exec/index.js +501 -10
  68. package/dist/exec/options.d.ts +124 -2
  69. package/dist/exec/options.js +413 -6
  70. package/dist/exec/permission.d.ts +9 -1
  71. package/dist/exec/permission.js +7 -1
  72. package/dist/exec/request/limits.d.ts +51 -0
  73. package/dist/exec/request/limits.js +49 -0
  74. package/dist/exec/request/scope.d.ts +106 -0
  75. package/dist/exec/request/scope.js +149 -0
  76. package/dist/exec/result/limits.d.ts +74 -0
  77. package/dist/exec/result/limits.js +72 -0
  78. package/dist/exec/result/prompt.d.ts +28 -0
  79. package/dist/exec/result/prompt.js +74 -0
  80. package/dist/exec/result/schema.d.ts +99 -0
  81. package/dist/exec/result/schema.js +464 -0
  82. package/dist/exec/result/sink.d.ts +56 -0
  83. package/dist/exec/result/sink.js +50 -0
  84. package/dist/exec/result/tool.d.ts +37 -0
  85. package/dist/exec/result/tool.js +118 -0
  86. package/dist/exec/runner.d.ts +395 -1
  87. package/dist/exec/runner.js +860 -8
  88. package/dist/exec/threads/aggregate.d.ts +34 -0
  89. package/dist/exec/threads/aggregate.js +30 -0
  90. package/dist/exec/threads/capacity.d.ts +23 -0
  91. package/dist/exec/threads/capacity.js +38 -0
  92. package/dist/exec/threads/dispatch-source.d.ts +60 -0
  93. package/dist/exec/threads/dispatch-source.js +106 -0
  94. package/dist/exec/threads/execute-threads.d.ts +53 -0
  95. package/dist/exec/threads/execute-threads.js +234 -0
  96. package/dist/exec/threads/gate.d.ts +42 -0
  97. package/dist/exec/threads/gate.js +81 -0
  98. package/dist/exec/threads/identity.d.ts +36 -0
  99. package/dist/exec/threads/identity.js +67 -0
  100. package/dist/exec/threads/interrupt.d.ts +29 -0
  101. package/dist/exec/threads/interrupt.js +79 -0
  102. package/dist/exec/threads/limits.d.ts +58 -0
  103. package/dist/exec/threads/limits.js +35 -0
  104. package/dist/exec/threads/outcome.d.ts +47 -0
  105. package/dist/exec/threads/outcome.js +26 -0
  106. package/dist/exec/threads/registry.d.ts +81 -0
  107. package/dist/exec/threads/registry.js +427 -0
  108. package/dist/exec/threads/runtime.d.ts +107 -0
  109. package/dist/exec/threads/runtime.js +523 -0
  110. package/dist/exec/threads/thread-source.d.ts +20 -0
  111. package/dist/exec/threads/thread-source.js +134 -0
  112. package/dist/fast/limits.d.ts +54 -0
  113. package/dist/fast/limits.js +54 -0
  114. package/dist/fast/summary-call.d.ts +72 -0
  115. package/dist/fast/summary-call.js +133 -0
  116. package/dist/fast/summary-prompt.d.ts +106 -0
  117. package/dist/fast/summary-prompt.js +320 -0
  118. package/dist/fast/task-summary-types.d.ts +66 -0
  119. package/dist/fast/task-summary-types.js +17 -0
  120. package/dist/logging/logger.d.ts +1 -1
  121. package/dist/proc/limits.d.ts +7 -0
  122. package/dist/proc/limits.js +7 -0
  123. package/dist/proc/output-streams.d.ts +50 -0
  124. package/dist/proc/output-streams.js +54 -0
  125. package/dist/team/runtime.d.ts +9 -0
  126. package/dist/team/runtime.js +8 -0
  127. package/dist/team/subagent.d.ts +15 -0
  128. package/dist/team/subagent.js +45 -1
  129. package/dist/tools/bash-tool.d.ts +10 -0
  130. package/dist/tools/bash-tool.js +71 -6
  131. package/dist/tools/index.d.ts +93 -1
  132. package/dist/tools/index.js +112 -3
  133. package/dist/tools/tool-gate.d.ts +100 -0
  134. package/dist/tools/tool-gate.js +87 -0
  135. package/dist/ui/App.js +224 -11
  136. package/dist/ui/Header.d.ts +28 -16
  137. package/dist/ui/Header.js +21 -19
  138. package/dist/ui/interaction-hints.js +6 -0
  139. package/dist/ui/layout/OverlayFrame.d.ts +18 -0
  140. package/dist/ui/layout/OverlayFrame.js +19 -4
  141. package/dist/ui/layout/header-hints.d.ts +55 -62
  142. package/dist/ui/layout/header-hints.js +59 -166
  143. package/dist/ui/layout/header-left.d.ts +115 -0
  144. package/dist/ui/layout/header-left.js +206 -0
  145. package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
  146. package/dist/ui/layout/shortcut-panel-layout.js +321 -0
  147. package/dist/ui/layout/status-detail-layout.js +5 -1
  148. package/dist/ui/layout/task-summary-cell.d.ts +61 -0
  149. package/dist/ui/layout/task-summary-cell.js +65 -0
  150. package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
  151. package/dist/ui/overlays/HelpOverlay.js +29 -2
  152. package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
  153. package/dist/ui/overlays/ShortcutPanel.js +106 -0
  154. package/dist/ui/shortcuts.d.ts +81 -0
  155. package/dist/ui/shortcuts.js +221 -0
  156. package/package.json +2 -2
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `submit_result`: the model's only way to hand an answer over as data
3
+ * (headless-request-scope-and-typed-result section 2.3.2 / 2.3.5).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * WHY A TOOL AND NOT A PROVIDER FEATURE. `response_format` / forced `tool_choice`
8
+ * would need three adapter changes inside Core, whose public surface
9
+ * (`public-api.test.ts`) and host-coupling ban (`no-host-coupling.test.ts`) are both
10
+ * frozen. A tool costs one extra round trip and buys three things Core cannot give
11
+ * here: the caller's schema travels to the model as a REAL JSON Schema (every
12
+ * adapter serializes `parameters` verbatim), a violation comes back as a readable
13
+ * field-level message the model can act on inside the same prompt, and the whole
14
+ * mechanism is provable without a network.
15
+ *
16
+ * NOTHING IN HERE KNOWS WHAT A REQUEST OR A PROCESS IS. It validates and it writes
17
+ * one box. Ending the request is the runner's job (`checkResultComplete`), which is
18
+ * what keeps "accepted" and "finished" from becoming two different opinions.
19
+ */
20
+ import { type AgentTool } from '@aragon-agent/core';
21
+ import { type ResultSchemaSpec } from './schema.js';
22
+ import type { ResultSink } from './sink.js';
23
+ /** The one tool name this feature registers. */
24
+ export declare const SUBMIT_RESULT_TOOL = "submit_result";
25
+ export interface SubmitResultToolOptions {
26
+ spec: ResultSchemaSpec;
27
+ sink: ResultSink;
28
+ }
29
+ /**
30
+ * The tool the model sees.
31
+ *
32
+ * THE SCHEMA IS INLINED INTO `parameters.properties.result`, NOT WRITTEN INTO THE
33
+ * DESCRIPTION, and that is not a style choice: `buildSystemPrompt` renders a tool as
34
+ * `description.split('\n')[0]` - one line - so a schema in the description never
35
+ * reaches the model at all, while `parameters` is serialized verbatim.
36
+ */
37
+ export declare function createSubmitResultTool(options: SubmitResultToolOptions): AgentTool;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * `submit_result`: the model's only way to hand an answer over as data
3
+ * (headless-request-scope-and-typed-result section 2.3.2 / 2.3.5).
4
+ *
5
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
6
+ *
7
+ * WHY A TOOL AND NOT A PROVIDER FEATURE. `response_format` / forced `tool_choice`
8
+ * would need three adapter changes inside Core, whose public surface
9
+ * (`public-api.test.ts`) and host-coupling ban (`no-host-coupling.test.ts`) are both
10
+ * frozen. A tool costs one extra round trip and buys three things Core cannot give
11
+ * here: the caller's schema travels to the model as a REAL JSON Schema (every
12
+ * adapter serializes `parameters` verbatim), a violation comes back as a readable
13
+ * field-level message the model can act on inside the same prompt, and the whole
14
+ * mechanism is provable without a network.
15
+ *
16
+ * NOTHING IN HERE KNOWS WHAT A REQUEST OR A PROCESS IS. It validates and it writes
17
+ * one box. Ending the request is the runner's job (`checkResultComplete`), which is
18
+ * what keeps "accepted" and "finished" from becoming two different opinions.
19
+ */
20
+ import { errorResult, textResult, } from '@aragon-agent/core';
21
+ import { RESULT_LIMITS } from './limits.js';
22
+ import { canonicalizeResult, checkResultValue, coerceStringifiedObject, formatResultViolations, resultValueBytes, } from './schema.js';
23
+ /** The one tool name this feature registers. */
24
+ export const SUBMIT_RESULT_TOOL = 'submit_result';
25
+ /**
26
+ * The tool the model sees.
27
+ *
28
+ * THE SCHEMA IS INLINED INTO `parameters.properties.result`, NOT WRITTEN INTO THE
29
+ * DESCRIPTION, and that is not a style choice: `buildSystemPrompt` renders a tool as
30
+ * `description.split('\n')[0]` - one line - so a schema in the description never
31
+ * reaches the model at all, while `parameters` is serialized verbatim.
32
+ */
33
+ export function createSubmitResultTool(options) {
34
+ const { spec, sink } = options;
35
+ return {
36
+ name: SUBMIT_RESULT_TOOL,
37
+ label: 'Submit result',
38
+ description: 'Submit the FINAL answer as an object that conforms to the required schema. ' +
39
+ 'Call this exactly once, when you are done. Do not also print the answer as prose. ' +
40
+ 'If it returns a validation error, fix the fields it names and call it again.',
41
+ // The wrapper is raw JSON from the caller's file; the index signature on
42
+ // `JSONSchema7` is what lets it through without a second schema type of our own.
43
+ parameters: spec.wrapper,
44
+ async execute(id, params) {
45
+ const attempts = sink.noteAttempt();
46
+ const submitted = coerceStringifiedObject(spec.schema, params.result);
47
+ // STEP 1 - THE WRAPPER'S OWN REQUIREMENT, IN THE CALLER'S VOCABULARY.
48
+ // "the key was absent" and "the value was undefined" are the same thing after
49
+ // JSON decoding, and the field name is what the model has to be told.
50
+ if (submitted === undefined)
51
+ return fail(sink, ['result: required']);
52
+ // STEP 1b - THE WRAPPER'S `additionalProperties: false`, ENFORCED HERE AND NOT
53
+ // ONLY BY Core (section 2.3.2, point 3). Core runs that keyword ONLY when `ajv`
54
+ // is importable; its fallback never reads the keyword at all, so on a machine
55
+ // without `ajv` a wrapper rule left to Core is a comment. A stray top-level key
56
+ // is refused rather than ignored: "it was silently dropped" and "it was part of
57
+ // the contract" are indistinguishable from the caller's side of the pipe.
58
+ const extraKeys = wrapperExtraKeys(params);
59
+ if (extraKeys.length > 0)
60
+ return fail(sink, extraKeys.map(describeExtraKey));
61
+ // STEP 2/3 - THE BELT (section 2.3.4). Runs on EVERY machine, including the one
62
+ // with no `ajv`, which is why it implements the keywords Core's fallback skips.
63
+ const violations = checkResultValue(spec.schema, submitted);
64
+ if (violations.length > 0)
65
+ return fail(sink, violations);
66
+ // STEP 4/5 - CANONICAL, THEN SIZED. Canonical first, so the number checked is
67
+ // the size of what a consumer will read back rather than of what was typed.
68
+ const canonical = canonicalizeResult(submitted);
69
+ const bytes = resultValueBytes(canonical);
70
+ if (bytes > RESULT_LIMITS.maxResultValueBytes) {
71
+ return fail(sink, [
72
+ `result: ${bytes} bytes is above the ${RESULT_LIMITS.maxResultValueBytes}-byte limit`,
73
+ ]);
74
+ }
75
+ // STEP 6 - SINGLE ASSIGNMENT. A second accepted call is a no-op by design, and
76
+ // the value a caller is entitled to is the FIRST one.
77
+ if (!sink.accepted())
78
+ sink.accept(canonical, { attempts, callId: id });
79
+ sink.noteViolations([]);
80
+ // STEP 7 - A SUCCESS RESULT EVEN ON THE SECOND CALL (I-S3). An error here would
81
+ // tell the model its answer was not taken, and it would keep trying.
82
+ return textResult('Result accepted. Do not call submit_result again; stop here.');
83
+ },
84
+ };
85
+ }
86
+ /**
87
+ * Record the rejection where the repair loop can quote it, and answer the model.
88
+ *
89
+ * `errorResult` RATHER THAN THROWING: the engine pushes whatever a tool returns into
90
+ * history, and an error RESULT is what keeps the `tool_use` from hanging into the
91
+ * next provider request. A thrown error would be converted by the executor into
92
+ * wording that says nothing about which fields were wrong.
93
+ */
94
+ function fail(sink, violations) {
95
+ sink.noteViolations(violations);
96
+ const shown = formatResultViolations(violations);
97
+ return errorResult(`submit_result was not accepted (attempt ${sink.attempts()}):\n${shown}\n` +
98
+ 'Fix the fields named above and call submit_result again with the whole object.');
99
+ }
100
+ /**
101
+ * The top-level keys the model sent that the wrapper does not accept.
102
+ *
103
+ * `result` IS THE WHOLE CONTRACT (section 2.3.2): the wrapper is
104
+ * `{ required: ['result'], additionalProperties: false }` and nothing else, so every
105
+ * other key is a stray. Returns `[]` for a non-object payload, which the missing-result
106
+ * check above has already answered with a better message.
107
+ */
108
+ function wrapperExtraKeys(params) {
109
+ if (typeof params !== 'object' || params === null)
110
+ return [];
111
+ return Object.keys(params)
112
+ .filter((key) => key !== 'result')
113
+ .slice(0, RESULT_LIMITS.maxViolations);
114
+ }
115
+ /** One stray key, in the same `path: what is wrong` shape as every other violation. */
116
+ function describeExtraKey(key) {
117
+ return `${key}: not allowed (this tool takes only "result")`;
118
+ }
@@ -19,8 +19,11 @@ import type { HeadlessController } from '../agent/headless.js';
19
19
  import { type FollowThroughMode } from '../todo/follow-through.js';
20
20
  import type { ExecEmitter } from './emitter.js';
21
21
  import type { FastSnapshot } from '../fast/types.js';
22
- import type { ExecStopReason } from './events.js';
22
+ import type { ExecCost, ExecStopReason } from './events.js';
23
23
  import type { ProcEvent } from '../proc/types.js';
24
+ import type { BridgeServer } from './bridge/server.js';
25
+ import type { BridgePromptSource } from './bridge/source.js';
26
+ import type { ResultSink } from './result/sink.js';
24
27
  /**
25
28
  * The controller surface the runner needs.
26
29
  *
@@ -54,11 +57,47 @@ export interface ExecRunnerController extends HeadlessController {
54
57
  * literal, and a host with no supervisor has nothing to report.
55
58
  */
56
59
  subscribeProc?(listener: (event: ProcEvent) => void): () => void;
60
+ /**
61
+ * Whether a turn is in flight, and the enqueue path for `steer`.
62
+ *
63
+ * OPTIONAL, FOR THE SAME REASON THE SIX ABOVE ARE: this interface is minimal so
64
+ * a test can satisfy it with an object literal. `steer` over the bridge is their
65
+ * only consumer, and a stub without them answers `not_running`, which is the
66
+ * honest reading of "this controller cannot accept guidance".
67
+ */
68
+ isRunning?(): boolean;
69
+ queueUserMessage?(text: string): string;
70
+ }
71
+ /**
72
+ * One caller message plus the metadata a bridge turn needs.
73
+ *
74
+ * `requestId` is the caller's own frame id, echoed onto `user`, `turn_state`,
75
+ * `assistant`, `tool_call` and `tool_result` so a wrapper can attribute events to
76
+ * the request that caused them. `queued` says whether this message had to wait
77
+ * behind another one - see `bridge/source.ts`, where it is captured at ARRIVAL,
78
+ * the only moment the question has a meaningful answer.
79
+ */
80
+ export interface ExecPrompt {
81
+ text: string;
82
+ requestId?: string;
83
+ queued?: boolean;
57
84
  }
58
85
  /** Where the caller's messages come from. One turn, or an NDJSON stdin stream. */
59
86
  export interface ExecPromptSource {
60
87
  /** The next caller message, or `null` when the caller is done. */
61
88
  next(): Promise<string | null>;
89
+ /**
90
+ * METADATA FOR THE MESSAGE `next()` JUST RETURNED, read once and synchronously.
91
+ *
92
+ * A SIDE CHANNEL RATHER THAN A WIDER RETURN TYPE, and that is a deliberate
93
+ * departure from the design (`§2.4.6` proposed `next(): Promise<ExecPrompt | null>`).
94
+ * Widening `next()` would have rewritten `stdin-stream.ts`'s contract and every
95
+ * caller of it - including `exec-stdin-stream.test.ts`, which AC-3 requires to
96
+ * pass VERBATIM as the proof that the v1 input format did not move. Keeping the
97
+ * return type and adding an optional reader gives the bridge what it needs while
98
+ * leaving that file byte-identical, and it costs exactly one call site: `run()`.
99
+ */
100
+ lastPrompt?(): ExecPrompt | undefined;
62
101
  }
63
102
  export declare function singlePrompt(text: string): ExecPromptSource;
64
103
  /** Replaceable so a unit test can assert the mechanism, not just the outcome. */
@@ -71,6 +110,35 @@ export interface ExecRunnerOptions {
71
110
  sessionId: string;
72
111
  maxTurns?: number;
73
112
  maxDurationMs?: number;
113
+ /**
114
+ * The structured-result contract for this run (headless-request-scope-and-typed-
115
+ * result section 2.4 / 2.3.5). All of these are ABSENT unless `--result-schema`
116
+ * was given, which is what keeps every other run byte-identical (I-S2).
117
+ */
118
+ resultSink?: ResultSink;
119
+ /** The schema's file name, echoed into `request_end.structured` and into errors. */
120
+ resultSchemaName?: string;
121
+ /** `--result-repair-turns`; defaults to `RESULT_LIMITS.defaultRepairTurns`. */
122
+ resultRepairTurns?: number;
123
+ /** `--on-result-unsatisfied`; `error` is the default and the only one that exits. */
124
+ onResultUnsatisfied?: 'error' | 'continue';
125
+ /**
126
+ * The per-request budgets (bridge only). ABSENT MEANS NO CEILING, deliberately:
127
+ * a default ceiling would cut off a legitimate long request with nothing saying
128
+ * why, and section 9.7 keeps "no default" as the decision.
129
+ */
130
+ requestMaxTurns?: number;
131
+ requestMaxDurationMs?: number;
132
+ /**
133
+ * How to price a usage reading.
134
+ *
135
+ * A CALLBACK RATHER THAN AN IMPORT: the cost authority (`buildCost`) lives in
136
+ * `exec/index.ts` because it needs the controller's price table, and importing it
137
+ * here would be a cycle. The runner passes its own cumulative usage and compaction
138
+ * billing, so a request's cost is the DIFFERENCE between two readings of the same
139
+ * function - which is exactly what makes it equal to running that request alone.
140
+ */
141
+ cost?: (usage: TokenUsage, compaction?: ExecCompactionBilling) => ExecCost;
74
142
  partialMessages?: boolean;
75
143
  includeThinking?: boolean;
76
144
  followThrough?: FollowThroughMode;
@@ -80,6 +148,79 @@ export interface ExecRunnerOptions {
80
148
  signals?: ExecSignalPort;
81
149
  /** Turns already spent by earlier invocations of a resumed session. */
82
150
  priorTurns?: number;
151
+ /**
152
+ * WHICH THREAD THIS RUNNER SERVES (headless-threads-and-lifecycle-hooks section
153
+ * 2.3.10, I-T10). Absent in every non-thread run, and its absence is what keeps that
154
+ * stream byte-identical: a field that is not set is not serialized.
155
+ */
156
+ threadId?: string;
157
+ /**
158
+ * Called once per request, in `run()`'s `finally`, AFTER `settleRequest`.
159
+ *
160
+ * THE ADMISSION GATE'S RELEASE POINT, and the reason it is not inside the runner: the
161
+ * gate must free the slot exactly when the request's account has been written, and the
162
+ * runner is the only object that knows that moment. Keeping it a callback means the
163
+ * runner learns nothing about threads.
164
+ */
165
+ onRequestSettled?: (requestSeq: number) => void;
166
+ /**
167
+ * `turn_start` (section 2.4.6). Awaited at the top of every follow-through iteration,
168
+ * AFTER the budget check and BEFORE `prompt()`.
169
+ */
170
+ beforeTurn?: BeforeTurnHook;
171
+ /** `prompt_pre` (section 2.4.7). Runs between `scope.begin()` and the ack. */
172
+ hookPort?: ExecRunnerHookPort;
173
+ /**
174
+ * DEFAULT `true`, AND THREAD MODE PASSES `false` (section 2.3.8a, review P0-1).
175
+ *
176
+ * `installTerminator()` writes into `logging/install.ts`'s SINGLE process-wide slot, so
177
+ * two runners that both install it means the last one wins: Ctrl-C would abort one
178
+ * controller and leave the rest running. Making it an OPTION rather than a convention is
179
+ * what turns "remember not to install twice" into something the type system carries.
180
+ */
181
+ installSignalTerminator?: boolean;
182
+ }
183
+ /** One `turn_start` answer, already validated by the hook runtime. */
184
+ export interface BeforeTurnResult {
185
+ /** Text to queue through steering. */
186
+ injected?: string;
187
+ /** The hook that injected, so a stranded injection can name itself. */
188
+ hookId?: string;
189
+ /** `injection_stranded`, `hook_timeout`, ... */
190
+ code?: string;
191
+ /** `--on-hook-timeout error`: this request must end as an error. */
192
+ isError?: boolean;
193
+ }
194
+ export type BeforeTurnHook = (info: {
195
+ turn: number;
196
+ source: 'caller' | 'todo_continue' | 'result_repair';
197
+ /**
198
+ * The request this turn belongs to. ALWAYS KNOWN HERE - `scope.begin()` has run - and
199
+ * carried on the `hook_request` so a caller can correlate a `turn_start` round trip
200
+ * with the request it belongs to; a synthesized `turn_<n>` would collide across two
201
+ * requests inside one thread.
202
+ */
203
+ requestId: string;
204
+ }) => Promise<BeforeTurnResult | void>;
205
+ /** What `prompt_pre` decided about one caller message. */
206
+ export interface PromptPreDecision {
207
+ action: 'allow' | 'rewrite' | 'deny';
208
+ /** The text the model will actually receive, on `rewrite`. */
209
+ text?: string;
210
+ reason?: string;
211
+ message?: string;
212
+ /** A stable code for the `ack` / `request_end.error` when denied. */
213
+ code?: string;
214
+ /** Whether the denial makes the REQUEST a failure (a hook deny does). */
215
+ isError?: boolean;
216
+ }
217
+ export interface ExecRunnerHookPort {
218
+ /** Never rejects: every failure arrives as a decision. */
219
+ promptPre(input: {
220
+ text: string;
221
+ queued: boolean;
222
+ requestId: string;
223
+ }): Promise<PromptPreDecision>;
83
224
  }
84
225
  export interface ExecCompactionBilling {
85
226
  usage: TokenUsage;
@@ -91,6 +232,20 @@ export interface ExecRunStats {
91
232
  turns: number;
92
233
  usage: TokenUsage;
93
234
  lastAssistantText: string;
235
+ /**
236
+ * The LAST accepted structured result of this process, or absent.
237
+ *
238
+ * PROCESS-LEVEL ON PURPOSE, while the SINK is per request (I-S8): `result` is one
239
+ * event for the whole run and must not report "nothing was accepted" because the
240
+ * final request happened to be a follow-up that submitted nothing. A per-request
241
+ * reader (`request_end.structured`) uses the sink, not this.
242
+ */
243
+ lastStructured?: {
244
+ value: unknown;
245
+ attempts: number;
246
+ };
247
+ /** True when a schema was given and nothing was ever accepted (I-S5). */
248
+ resultUnsatisfied: boolean;
94
249
  errored: boolean;
95
250
  stopReason: ExecStopReason;
96
251
  errorCode: string | null;
@@ -105,6 +260,11 @@ export declare class ExecRunner {
105
260
  private readonly now;
106
261
  private readonly progress;
107
262
  private controller;
263
+ /** Set by `attachBridge`; absent in every non-bridge run (I-1). */
264
+ private bridge;
265
+ private bridgeSource;
266
+ /** The caller's id for the turn currently running, if there is one. */
267
+ private requestId;
108
268
  private unsubscribers;
109
269
  /** Per-job progress throttle state: the last text emitted, and when. */
110
270
  private readonly jobProgressAt;
@@ -121,11 +281,48 @@ export declare class ExecRunner {
121
281
  private errorMessage;
122
282
  private signalNumber;
123
283
  private thinkingBuffer;
284
+ /**
285
+ * An injection that has been queued but whose acceptance has not been observed.
286
+ *
287
+ * ITS ONLY JOB IS TO MAKE I-H12 TRUE: a `turn_start` injection queued for a request that
288
+ * then fails to start must be REPORTED, not silently left in the steering queue where the
289
+ * next request's loop top would push it into history.
290
+ */
291
+ private pendingInjection;
292
+ /**
293
+ * How many `steering_accepted` receipts have arrived.
294
+ *
295
+ * IT EXISTS TO CLOSE A ONE-TURN RACE: a stub (or a very fast engine) can confirm the
296
+ * injection INSIDE `queueUserMessage`, in which case setting the pending marker afterwards
297
+ * would leave a stale one and report a healthy injection as stranded.
298
+ */
299
+ private steeringReceipts;
124
300
  private budgetTimer;
125
301
  private timerStarted;
126
302
  private detached;
127
303
  private lastSignalAt;
304
+ /**
305
+ * The one owner of every request-level fact (section 2.4.3).
306
+ *
307
+ * CREATED UNCONDITIONALLY, even outside bridge mode, and that is a deliberate
308
+ * departure from the design's "only in bridge mode" (implementation note): the
309
+ * acceptance latch and the per-request reset have to exist for a single-request run
310
+ * too, or `checkResultComplete` would need a second latch of its own - and two
311
+ * latches for one fact is how "the second request never stops" happens. It emits
312
+ * nothing and starts no timer unless a request budget was configured, so a
313
+ * non-bridge run is byte-identical.
314
+ */
315
+ private readonly requestScope;
316
+ /** True between the request's `ack` and its `request_end`. */
317
+ private requestInFlight;
318
+ /** Repair continuations used by the request in flight. */
319
+ private repairsUsed;
320
+ private resultUnsatisfied;
321
+ /** The last value any request accepted, for the process-level `result` event. */
322
+ private lastStructured;
128
323
  constructor(options: ExecRunnerOptions);
324
+ /** `buildCost` when the composition root supplied it; zero-and-unknown otherwise. */
325
+ private costOf;
129
326
  /**
130
327
  * Subscribe to all four streams.
131
328
  *
@@ -136,6 +333,70 @@ export declare class ExecRunner {
136
333
  * multiple subscribers.
137
334
  */
138
335
  attach(controller: ExecRunnerController): void;
336
+ /**
337
+ * Wire the protocol channel. Called AFTER `attach(controller)`, never before:
338
+ * the turn ports below reach the controller.
339
+ *
340
+ * THREE LINES OF STATE AND ONE INSTALLATION, so that everything bridge-shaped in
341
+ * this file is reachable from one place. `steer` and `interrupt` belong here
342
+ * rather than in the server because they need the RUNNER's idea of what is in
343
+ * flight - and routing them through the server keeps the source's frame router
344
+ * a pure demultiplexer.
345
+ */
346
+ attachBridge(server: BridgeServer, source?: BridgePromptSource): void;
347
+ /**
348
+ * `steer` over the wire.
349
+ *
350
+ * THE CHECK AND THE CALL ARE SEPARATE, AND BOTH ARE NEEDED: `queueUserMessage()`
351
+ * throws when the agent is not running, and "it was running when I looked" is
352
+ * not the same claim as "it accepted the message". Both failures map to the same
353
+ * receipt, because a caller cannot act on the difference and a lie ("accepted")
354
+ * would make it wait for text that never enters the conversation.
355
+ */
356
+ private onSteerFrame;
357
+ /**
358
+ * `interrupt` over the wire, and the three things it can mean.
359
+ *
360
+ * A NAMED QUEUED TURN IS DELETED, NOT ABORTED: the user removed a message that
361
+ * had not started, and aborting the run in flight would cancel somebody else's
362
+ * work. An unnamed (or in-flight) request is the ordinary signal path, which
363
+ * already settles, persists and reports - the runner's own `requestInterrupt`.
364
+ */
365
+ private onInterruptFrame;
366
+ /**
367
+ * `cancel_request` over the wire: stop ONE request, never the process.
368
+ *
369
+ * THREE OUTCOMES, and the third one is the important one (section 2.4.7):
370
+ *
371
+ * QUEUED the source deletes it and THIS method writes its `request_end`,
372
+ * because `run()`'s loop never saw that prompt and never will.
373
+ * The `ack` echoes the requestId, since a queued prompt has no
374
+ * `ack` of its own before it is served.
375
+ * IN FLIGHT cancel the scope and abort. The request's own `finally` writes the
376
+ * `request_end`, so there is still exactly one writer per fact.
377
+ * UNKNOWN `ack{accepted:false}` and a non-fatal error. A FINISHED REQUEST IS
378
+ * `unknown`, NEVER "in flight" - aborting a healthy run because a
379
+ * caller cancelled something it already got an answer for is the
380
+ * exact defect the source's `cancelQueued` comment records.
381
+ */
382
+ cancelRequest(requestId: string | undefined, frameId: string | undefined): void;
383
+ /**
384
+ * The refusal, written where the caller can act on it.
385
+ *
386
+ * TWO FRAMES, DELIBERATELY: the `ack` says "not accepted" and the non-fatal `error`
387
+ * says why. A caller that only watches `ack` is not left guessing, and one that only
388
+ * watches `error` is not left without a receipt.
389
+ */
390
+ private reportUnknownRequest;
391
+ /**
392
+ * The `request_end` for a request that was cancelled BEFORE it was served.
393
+ *
394
+ * `requestSeq` IS ABSENT, NOT ZERO: the number is minted when a prompt is served,
395
+ * this one never was, and inventing one would collide with a later real request.
396
+ * Everything else is honestly zero, including `cost` - which is `known: true` with
397
+ * `amount: 0` because nothing was spent and that is a fact, not a missing price.
398
+ */
399
+ private writeCancelledRequestEnd;
139
400
  /**
140
401
  * Map a supervisor event onto the `job` channel.
141
402
  *
@@ -174,12 +435,123 @@ export declare class ExecRunner {
174
435
  * the two faces cannot drift about what "unfinished" means.
175
436
  */
176
437
  run(source: ExecPromptSource): Promise<void>;
438
+ /**
439
+ * Run `prompt_pre` for the request that just began, and settle it if a hook refuses.
440
+ *
441
+ * RETURNS THE TEXT TO SEND, or `null` when the request has already been settled and the
442
+ * caller's loop must continue with the next one. A caller hook's denial is a FAILED
443
+ * request - the caller did not get what it asked for - which is why `isError` is true
444
+ * here and false for a caller's own `cancel_request`.
445
+ */
446
+ private applyPromptPre;
447
+ /**
448
+ * The `request_end` for a request a hook refused.
449
+ *
450
+ * ONE DELIBERATE DIFFERENCE FROM `writeCancelledRequestEnd`: `requestSeq` IS PRESENT.
451
+ * That method serves a prompt that was withdrawn while queued, before `begin()` ever ran,
452
+ * so it has no number to give; this one HAS begun, and inventing a denial without the
453
+ * number would make "which request was refused" a guessing game.
454
+ */
455
+ private writeDeniedRequestEnd;
456
+ /**
457
+ * Record a hook-caused request failure (section 2.4.8's `error` policy).
458
+ *
459
+ * ALL THREE FIELDS, because three different readers need them: `errorCode` and
460
+ * `errorMessage` become `request_end.error`, and `requestHadError` is what makes
461
+ * `requestError()` return anything at all - setting the first two alone leaves the frame
462
+ * looking clean, which is exactly the failure `--on-hook-timeout error` exists to prevent.
463
+ */
464
+ private markHookError;
465
+ /**
466
+ * Report an injection that was queued for a request that never accepted it (I-H12).
467
+ *
468
+ * TWO FRAMES, because there are two facts: the hook round trip ended as `aborted` with
469
+ * `injection_stranded` (visible in the hook channel), and this request's `error.code`
470
+ * carries the same string so a caller reading only `request_end` learns it too.
471
+ */
472
+ private reportStrandedInjection;
473
+ /**
474
+ * Write this request's `request_end` - the FIRST of the two writers (I-R9).
475
+ *
476
+ * The second is `cancelRequest`, for a prompt that was cancelled while queued and
477
+ * therefore never reached this method at all. Both go through
478
+ * `RequestScope.remember()`, which is what makes "exactly one per request" true
479
+ * instead of merely intended.
480
+ */
481
+ private settleRequest;
482
+ /**
483
+ * What `RequestScope.end()` needs: the clock, the failure, and - when the PROCESS
484
+ * stopped underneath this request - the process reason (section 2.4.2).
485
+ *
486
+ * SEPARATE FROM `requestError()` BECAUSE THE TWO ANSWER DIFFERENT QUESTIONS: the
487
+ * error says "you did not get what you asked for", the reason says "why this request
488
+ * ended". A process `interrupt` sets the second and not the first, which is exactly
489
+ * the case a caller reading only `stopReason` has to see as `cancelled` rather than
490
+ * as a finished answer.
491
+ */
492
+ private settlementInput;
493
+ /**
494
+ * The process-level stop, translated into the REQUEST vocabulary (section 2.4.2).
495
+ *
496
+ * `undefined` for `end_turn` and for everything a request already speaks for itself.
497
+ * Only the three cases where the RUN ended underneath a live request are mapped, and
498
+ * the mapping is one-way on purpose: `ExecStopReason` and `ExecRequestStopReason` are
499
+ * different unions over different questions and must stay different.
500
+ */
501
+ private processStopForRequest;
502
+ /** The request-level error, or `null`. One place so `result` and `request_end` agree. */
503
+ private requestError;
177
504
  private onCompactionUsage;
178
505
  private requestSeq;
179
506
  private requestHadError;
180
507
  private isRequestCancelled;
181
508
  /** One caller message plus every auto-continuation it produces. */
182
509
  private runFollowThroughChain;
510
+ /**
511
+ * What the structured-result contract says about the request that just ended.
512
+ *
513
+ * FOUR ANSWERS, and `none` is the important one: with no `--result-schema` this
514
+ * method reads nothing and the caller's behaviour is exactly what it was before the
515
+ * feature existed (I-S2).
516
+ */
517
+ private resultContractOutcome;
518
+ /**
519
+ * Record that this request ended without the answer the caller asked for.
520
+ *
521
+ * TWO FACTS, TWO FIELDS, NEVER ONE NAME FOR BOTH (P1-9): `error.code` says the
522
+ * contract failed, and the process-level `stopReason` moves to `error` ONLY when
523
+ * `--on-result-unsatisfied error` (the default) and nothing else already decided why
524
+ * the request stopped.
525
+ */
526
+ private markResultUnsatisfied;
527
+ /**
528
+ * Stop the request for a budget reason, and record the contract failure it implies.
529
+ *
530
+ * THE CANCELLATION GUARD IS THE POINT. A caller that withdrew this request has not
531
+ * failed to deliver a result, so `cancel_request` must not leave a
532
+ * `result_schema_unsatisfied` behind - "the work was called off" and "the answer
533
+ * never arrived" are different facts and only the second is a contract failure
534
+ * (section 2.3.7). One helper rather than three copies, because the copy that
535
+ * forgets this line is a defect nobody notices for a release.
536
+ */
537
+ private haltRequestOnBudget;
538
+ /**
539
+ * The request-level budgets, at a boundary the engine can survive.
540
+ *
541
+ * RETURNS TRUE WHEN IT STOPPED THE REQUEST. The process is never touched: calling
542
+ * `controller.abort()` ends the model's current loop, and the next `prompt()` starts
543
+ * a fresh one - which is the whole of I-R1.
544
+ */
545
+ private stopRequestOnBudget;
546
+ /**
547
+ * "ACCEPTED MEANS FINISHED", enforced at the one event that follows a tool result
548
+ * (section 2.3.6).
549
+ *
550
+ * `turn_end` fires BEFORE `tool_execution_start`, so by the time this runs the turn
551
+ * has already been counted and `agent_end` cannot mistake the abort for a run that
552
+ * produced nothing.
553
+ */
554
+ private checkResultComplete;
183
555
  /**
184
556
  * The wall clock starts at the FIRST `prompt()`, not at process start: the
185
557
  * caller is bounding the run, and config resolution plus a skills scan are not
@@ -258,7 +630,29 @@ export declare class ExecRunner {
258
630
  */
259
631
  requestInterrupt(): void;
260
632
  private onSignal;
633
+ /**
634
+ * THE ONE STAMPING POINT (section 2.3.10, I-T10; review P1-1).
635
+ *
636
+ * NOT `correlation()`, which returns only `{requestId}` and is spread into a handful of
637
+ * events: the thread id has to be on EVERY line this runner produces, including the ones
638
+ * that carry no request (a `todo` snapshot, a `job` update). Stamping here and in
639
+ * `writeToBridge` covers the whole file - and AC-54 turns the claim into a single
640
+ * universal assertion ("every line with a sessionId has a threadId") rather than a
641
+ * per-event checklist that a new event can quietly escape.
642
+ */
261
643
  private emit;
644
+ /**
645
+ * The same stamp for the frames that bypass `emit()`.
646
+ *
647
+ * SIX SITES USE THIS AND THEY ARE ALL ACKS AND CONTROL FRAMES: steer ack, interrupt ack,
648
+ * the cancelled `request_end`, the request ack, the settled `request_end` and the
649
+ * `steering` receipt. A seventh added later that writes to `bridge` directly would lose
650
+ * the stamp - which is why the method exists rather than six copies of a conditional
651
+ * spread.
652
+ */
653
+ private writeToBridge;
654
+ /** The correlation fields every attributable event carries in bridge mode. */
655
+ private correlation;
262
656
  /** Caller-facing prose. STDERR ALWAYS - stdout carries nothing but the schema. */
263
657
  private note;
264
658
  private onAgentEvent;