@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,346 @@
1
+ /**
2
+ * The bridge wire protocol: frame types, one parser, and the outbound builders.
3
+ *
4
+ * ASCII ONLY - `src/exec/**` is inside the glyph scanner's scope.
5
+ *
6
+ * PURE. No I/O, no timers, no streams: every function here takes text or plain
7
+ * objects and returns text or plain objects. That is what makes the protocol
8
+ * testable at the level where the bugs actually live (a frame with a missing
9
+ * field, a `respondsTo` that points at nothing) without a subprocess in sight.
10
+ *
11
+ * TWO RULES DECIDE EVERYTHING BELOW, and they pull in opposite directions:
12
+ *
13
+ * AN UNKNOWN `type` IS SILENTLY IGNORED (forward compatibility). A caller
14
+ * written against a newer CLI must not be punished for a frame this build never
15
+ * heard of; the same promise `stdin-stream.ts` already makes for `stream-json`.
16
+ *
17
+ * AN UNKNOWN `respondsTo` IS AN ERROR (caller bug). There is no forward
18
+ * compatibility to preserve - the caller answered a request that does not
19
+ * exist - and silence would leave it waiting for an outcome forever.
20
+ */
21
+ import type { NormalizedPlan, NormalizedQuestion, HumanRequest } from '../../tools/human-input.js';
22
+ export declare const BRIDGE_PROTOCOL_VERSION = 2;
23
+ /**
24
+ * What this build announces in `system/init.capabilities` when bridge is on.
25
+ *
26
+ * APPEND ONLY, NEVER REORDER, same discipline as `EXEC_CAPABILITIES`: the caller
27
+ * asks `includes('caller_tools')` and ignores what it does not recognise.
28
+ */
29
+ export declare const BRIDGE_CAPABILITIES: readonly string[];
30
+ /** The four things a caller can be asked to decide. */
31
+ export type DecisionKind = 'tool' | 'questions' | 'plan' | 'skill_install';
32
+ /**
33
+ * The two directions a pending request can travel.
34
+ *
35
+ * `decision` is the CLI asking (a tool approval, a question, a plan, an install);
36
+ * `caller_tool` is the CLI asking the caller to DO something. A `respondsTo`
37
+ * pointing at the wrong one is `bridge_kind_mismatch`, which is why the table has
38
+ * to know which kind each id belongs to.
39
+ */
40
+ export type PendingKindValue = 'decision' | 'caller_tool';
41
+ /**
42
+ * Where a tool call came from. `depth` 0 is the lead; subagents are 1 or more.
43
+ *
44
+ * SENT ON EVERY REQUEST, even the lead's, so a caller never has to infer it from
45
+ * the absence of a field - the same reason `sessionFile ?? null` is stamped
46
+ * unconditionally on `system/init`.
47
+ */
48
+ export interface AgentScope {
49
+ id: string;
50
+ label: string;
51
+ depth: number;
52
+ }
53
+ export declare const LEAD_SCOPE: AgentScope;
54
+ /** One caller-served tool, as `tool_register` declares it. */
55
+ export interface CallerToolSpec {
56
+ name: string;
57
+ label?: string;
58
+ description: string;
59
+ parameters: Record<string, unknown>;
60
+ timeoutMs?: number;
61
+ }
62
+ export interface BridgePromptFrame {
63
+ type: 'prompt';
64
+ id?: string;
65
+ text: string;
66
+ /**
67
+ * Thread mode only (headless-threads-and-lifecycle-hooks section 2.3.2). ABSENT
68
+ * MEANS THE DEFAULT THREAD `main`, which is what keeps a v1 caller working without
69
+ * a single change (AC-11).
70
+ */
71
+ threadId?: string;
72
+ }
73
+ export interface BridgeSteerFrame {
74
+ type: 'steer';
75
+ id?: string;
76
+ text: string;
77
+ threadId?: string;
78
+ }
79
+ export interface BridgeInterruptFrame {
80
+ type: 'interrupt';
81
+ id?: string;
82
+ /** Omitted means "the turn in flight". A queued turn is named explicitly. */
83
+ requestId?: string;
84
+ /**
85
+ * A THREAD ID HERE IS A REFUSAL, NOT A ROUTE (section 2.3.2 / AC-34).
86
+ *
87
+ * It is parsed so the router can answer
88
+ * `ack{accepted:false, code:'interrupt_is_process_level'}` instead of treating the
89
+ * frame as unknown. Degrading it to `cancel_request` would erase the difference
90
+ * between "the user pressed Ctrl-C" and "the backend withdrew one request".
91
+ */
92
+ threadId?: string;
93
+ }
94
+ /**
95
+ * Stop ONE request; keep the process serving (section 2.4.7).
96
+ *
97
+ * A SECOND FRAME RATHER THAN A FLAG ON `interrupt`, because the two answer different
98
+ * questions: `interrupt` means "stop this RUN" (process-level, `128 + signo`), and
99
+ * re-purposing it would make "the user pressed Ctrl-C" indistinguishable from "the
100
+ * backend withdrew one request" - after the fact there would be no way to tell.
101
+ *
102
+ * It is a CONTROL frame, so it continues to be routed after `end` (a caller that has
103
+ * sent its last prompt is still entitled to withdraw one), and it is ignored after
104
+ * `result` like every other frame.
105
+ */
106
+ export interface BridgeCancelRequestFrame {
107
+ type: 'cancel_request';
108
+ /** This frame's own id, echoed on the `ack`. */
109
+ id?: string;
110
+ /** Omitted means "the request in flight"; named, it must match one exactly. */
111
+ requestId?: string;
112
+ threadId?: string;
113
+ }
114
+ /**
115
+ * The four decision shapes share one frame, discriminated by what it carries.
116
+ *
117
+ * A SINGLE FRAME RATHER THAN FOUR is the whole point of `kind` on the outbound
118
+ * side: the caller answers with the same envelope whatever it was asked, and the
119
+ * gate that asked is the one that knows how to read the answer.
120
+ */
121
+ export interface BridgeDecisionFrame {
122
+ type: 'decision';
123
+ id?: string;
124
+ /**
125
+ * OPTIONAL ON PURPOSE (section 2.3.2). `respondsTo` is already globally unique, so a
126
+ * caller that lost its thread bookkeeping is still served correctly by the default
127
+ * thread's server - which is why the field is a convenience rather than a key.
128
+ */
129
+ threadId?: string;
130
+ respondsTo: string;
131
+ decision?: 'allow' | 'deny';
132
+ scope?: 'once' | 'session';
133
+ reason?: string;
134
+ /** Untrusted. `validateHumanResponse` is the only reader. */
135
+ answers?: unknown;
136
+ cancelled?: boolean;
137
+ planDecision?: unknown;
138
+ feedback?: string;
139
+ }
140
+ export interface BridgeToolResultFrame {
141
+ type: 'tool_result';
142
+ id?: string;
143
+ threadId?: string;
144
+ respondsTo: string;
145
+ /** A string is one text part; an array is a content-block list. */
146
+ content: unknown;
147
+ isError?: boolean;
148
+ }
149
+ export interface BridgeToolRegisterFrame {
150
+ type: 'tool_register';
151
+ id?: string;
152
+ threadId?: string;
153
+ /** Untrusted; `validateCallerTools` narrows it. */
154
+ tools: unknown[];
155
+ }
156
+ /**
157
+ * Open one more conversation (section 2.3.2).
158
+ *
159
+ * ACCEPTED ONLY IN THREAD MODE. Without `--threads` the answer is
160
+ * `ack{accepted:false, code:'threads_disabled'}` (AC-2), never silence: a wrapper that
161
+ * believes it opened a thread and gets nothing would send prompts into `main` and
162
+ * receive answers to a conversation it never asked for.
163
+ */
164
+ export interface BridgeThreadOpenFrame {
165
+ type: 'thread_open';
166
+ id?: string;
167
+ /** `[a-z0-9][a-z0-9_-]{0,31}`; `resolveThreadIdentity` is the only validator. */
168
+ threadId: string;
169
+ /** Explicit session id; absent means `${base}-${threadId}`. */
170
+ sessionId?: string;
171
+ }
172
+ export interface BridgeThreadCloseFrame {
173
+ type: 'thread_close';
174
+ id?: string;
175
+ threadId: string;
176
+ /** `settle` (default) waits for the request in flight; `abort` cancels it. */
177
+ mode?: 'settle' | 'abort';
178
+ }
179
+ export interface BridgeThreadListFrame {
180
+ type: 'thread_list';
181
+ id?: string;
182
+ }
183
+ /**
184
+ * Register lifecycle hooks (section 2.4.2).
185
+ *
186
+ * `hooks` IS `unknown[]` ON PURPOSE: the frame comes off a pipe, and `validateHookSpecs`
187
+ * is the only reader. Typing it here would be a lie TypeScript happily propagates into
188
+ * a timeout table.
189
+ */
190
+ export interface BridgeHookRegisterFrame {
191
+ type: 'hook_register';
192
+ id?: string;
193
+ threadId?: string;
194
+ hooks: unknown[];
195
+ }
196
+ /**
197
+ * The caller's answer to one `hook_request`.
198
+ *
199
+ * ONE FRAME FOR FOUR POINTS, discriminated by `action` - the same shape `decision`
200
+ * already uses for its four kinds. The point is not repeated because the CLI already
201
+ * knows which hook id it asked with.
202
+ */
203
+ export interface BridgeHookAnswerFrame {
204
+ type: 'hook_answer';
205
+ id?: string;
206
+ threadId?: string;
207
+ respondsTo: string;
208
+ /** `allow` | `deny` | `rewrite` | `inject` | `keep` | `replace`; validated by point. */
209
+ action: string;
210
+ /** `prompt_pre` rewrite and `turn_start` inject. */
211
+ text?: string;
212
+ /** `tool_pre` rewrite. */
213
+ input?: unknown;
214
+ /** `tool_post` replace. */
215
+ content?: unknown;
216
+ isError?: boolean;
217
+ reason?: string;
218
+ message?: string;
219
+ }
220
+ export interface BridgePingFrame {
221
+ type: 'ping';
222
+ id?: string;
223
+ }
224
+ export interface BridgeEndFrame {
225
+ type: 'end';
226
+ }
227
+ /** A `type` this build does not know. Parsed successfully, then ignored. */
228
+ export interface BridgeIgnoredFrame {
229
+ type: 'ignored';
230
+ rawType: string;
231
+ }
232
+ export type BridgeInputFrame = BridgePromptFrame | BridgeSteerFrame | BridgeInterruptFrame | BridgeCancelRequestFrame | BridgeDecisionFrame | BridgeToolResultFrame | BridgeToolRegisterFrame | BridgeThreadOpenFrame | BridgeThreadCloseFrame | BridgeThreadListFrame | BridgeHookRegisterFrame | BridgeHookAnswerFrame | BridgePingFrame | BridgeEndFrame | BridgeIgnoredFrame;
233
+ export interface BridgeParseOk {
234
+ ok: true;
235
+ frame: BridgeInputFrame;
236
+ }
237
+ export interface BridgeParseFailure {
238
+ ok: false;
239
+ /** A stable `error.code` value; see the event contract in `events.ts`. */
240
+ code: string;
241
+ message: string;
242
+ }
243
+ export type BridgeParseResult = BridgeParseOk | BridgeParseFailure;
244
+ export declare const BRIDGE_FRAME_INVALID = "bridge_frame_invalid";
245
+ export declare const BRIDGE_FRAME_TOO_LARGE = "bridge_frame_too_large";
246
+ /** The `tool` payload of a `decision_request{kind:'tool'}`. */
247
+ export interface DecisionToolPayload {
248
+ name: string;
249
+ callId: string;
250
+ input: Record<string, unknown>;
251
+ /** One readable line, e.g. `Run: npm test`. Reuses `summarize()`. */
252
+ summary: string;
253
+ turn: number;
254
+ }
255
+ export interface DecisionInstallPayload {
256
+ tool: string;
257
+ summary: string;
258
+ }
259
+ /** What one `requestDecision` call is asking about. */
260
+ export type DecisionAsk = {
261
+ kind: 'tool';
262
+ tool: DecisionToolPayload;
263
+ agent?: AgentScope;
264
+ } | {
265
+ kind: 'questions';
266
+ questions: NormalizedQuestion[];
267
+ agent?: AgentScope;
268
+ } | {
269
+ kind: 'plan';
270
+ plan: NormalizedPlan;
271
+ agent?: AgentScope;
272
+ } | {
273
+ kind: 'skill_install';
274
+ install: DecisionInstallPayload;
275
+ agent?: AgentScope;
276
+ };
277
+ /**
278
+ * The caller's answer, unvalidated. `kind` on the request decides its shape.
279
+ *
280
+ * DELIBERATELY LOOSE. These fields come off a pipe, so they are `unknown` until a
281
+ * validator says otherwise; typing them as `Answer[]` here would be a lie that
282
+ * TypeScript happily propagates into model-visible text.
283
+ */
284
+ export interface DecisionAnswer {
285
+ decision?: 'allow' | 'deny';
286
+ scope?: 'once' | 'session';
287
+ reason?: string;
288
+ answers?: unknown;
289
+ cancelled?: boolean;
290
+ planDecision?: unknown;
291
+ feedback?: string;
292
+ }
293
+ /**
294
+ * What `requestDecision` resolves with. IT NEVER REJECTS, AND EVERY NON-ANSWER IS
295
+ * ITS OWN MEMBER (review P1-6).
296
+ *
297
+ * A boolean would have collapsed four different situations - answered, timed out,
298
+ * the turn was aborted, the channel closed - into "not allowed", and the gate
299
+ * that has to write model-visible text cannot tell them apart afterwards. The
300
+ * three non-`answered` members also all map to `null` on the `HumanInputGate`
301
+ * path, because its contract is "resolves null on cancel" rather than "invent an
302
+ * empty answer".
303
+ */
304
+ export type DecisionOutcome = {
305
+ outcome: 'answered';
306
+ answer: DecisionAnswer;
307
+ } | {
308
+ outcome: 'timeout';
309
+ deadlineAt: number;
310
+ } | {
311
+ outcome: 'aborted';
312
+ } | {
313
+ outcome: 'closed';
314
+ reason: string;
315
+ };
316
+ /** A model-driven call into a caller tool. */
317
+ export interface CallerToolCall {
318
+ callId: string;
319
+ turn: number;
320
+ tool: string;
321
+ input: Record<string, unknown>;
322
+ /** The EFFECTIVE ceiling, after `min(spec, --caller-tool-timeout)`. */
323
+ timeoutMs: number;
324
+ agent: AgentScope;
325
+ }
326
+ /** A caller tool's answer, before truncation and content checks. */
327
+ export interface CallerToolAnswer {
328
+ content: unknown;
329
+ isError?: boolean;
330
+ }
331
+ /**
332
+ * Turn one stdin line into a frame, or into a stable failure code.
333
+ *
334
+ * THE OVER-LONG CHECK IS HERE AS WELL AS IN THE SPLITTER, on purpose: the
335
+ * splitter protects memory, this protects the contract, and a caller that raises
336
+ * `maxFrameBytes` for a test still gets the same refusal from the parser.
337
+ */
338
+ export declare function parseBridgeInput(line: string): BridgeParseResult;
339
+ /**
340
+ * Map a `HumanInputGate` request onto the frame that asks the caller.
341
+ *
342
+ * THE CLI IS THE AUTHORITY ON THE QUESTIONS, and this function is where that is
343
+ * visible: it copies the normalized question set the model produced, so the
344
+ * caller renders what was actually asked rather than inventing its own text.
345
+ */
346
+ export declare function humanDecisionFrame(req: HumanRequest): DecisionAsk;
@@ -0,0 +1,256 @@
1
+ import { BRIDGE_LIMITS } from './limits.js';
2
+ export const BRIDGE_PROTOCOL_VERSION = 2;
3
+ /**
4
+ * What this build announces in `system/init.capabilities` when bridge is on.
5
+ *
6
+ * APPEND ONLY, NEVER REORDER, same discipline as `EXEC_CAPABILITIES`: the caller
7
+ * asks `includes('caller_tools')` and ignores what it does not recognise.
8
+ */
9
+ export const BRIDGE_CAPABILITIES = [
10
+ 'bridge',
11
+ 'decision',
12
+ 'caller_tools',
13
+ 'steer',
14
+ 'ping',
15
+ // Per-request budgets, accounting, cancellation and `request_end`
16
+ // (headless-request-scope-and-typed-result section 4.2). APPENDED, and the protocol
17
+ // version stays 2: `protocolVersion` only ever moves for an INCOMPATIBLE shape
18
+ // change, and every frame this capability promises is additive.
19
+ 'request_scope',
20
+ ];
21
+ export const LEAD_SCOPE = { id: 'lead', label: 'lead', depth: 0 };
22
+ export const BRIDGE_FRAME_INVALID = 'bridge_frame_invalid';
23
+ export const BRIDGE_FRAME_TOO_LARGE = 'bridge_frame_too_large';
24
+ // ---------------------------------------------------------------------------
25
+ // Parsing
26
+ // ---------------------------------------------------------------------------
27
+ function invalid(message) {
28
+ return { ok: false, code: BRIDGE_FRAME_INVALID, message };
29
+ }
30
+ function optionalString(value) {
31
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
32
+ }
33
+ /**
34
+ * Turn one stdin line into a frame, or into a stable failure code.
35
+ *
36
+ * THE OVER-LONG CHECK IS HERE AS WELL AS IN THE SPLITTER, on purpose: the
37
+ * splitter protects memory, this protects the contract, and a caller that raises
38
+ * `maxFrameBytes` for a test still gets the same refusal from the parser.
39
+ */
40
+ export function parseBridgeInput(line) {
41
+ const bytes = Buffer.byteLength(line, 'utf-8');
42
+ if (bytes > BRIDGE_LIMITS.maxFrameBytes) {
43
+ return {
44
+ ok: false,
45
+ code: BRIDGE_FRAME_TOO_LARGE,
46
+ message: `Skipped a bridge frame of ${bytes} bytes; the limit is ${BRIDGE_LIMITS.maxFrameBytes}.`,
47
+ };
48
+ }
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(line);
52
+ }
53
+ catch {
54
+ return invalid(`Skipped a bridge line that is not JSON: ${clip(line)}`);
55
+ }
56
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
57
+ return invalid(`Skipped a bridge line that is not a JSON object: ${clip(line)}`);
58
+ }
59
+ const raw = parsed;
60
+ const type = typeof raw.type === 'string' ? raw.type : '';
61
+ if (type.length === 0)
62
+ return invalid('A bridge frame needs a string "type".');
63
+ // `user` is the v1 spelling of a prompt with no id. Kept because a caller
64
+ // migrated from `--input-format stream-json` should not have to rewrite.
65
+ if (type === 'user')
66
+ return parsePrompt({ ...raw, type: 'prompt' }, raw);
67
+ if (type === 'prompt')
68
+ return parsePrompt(raw, raw);
69
+ if (type === 'steer') {
70
+ const text = typeof raw.text === 'string' ? raw.text : '';
71
+ if (text.trim().length === 0)
72
+ return invalid('A "steer" frame needs a non-empty "text".');
73
+ return okFrame({ type: 'steer', text, ...idOf(raw), ...threadOf(raw) });
74
+ }
75
+ if (type === 'interrupt') {
76
+ const requestId = optionalString(raw.requestId);
77
+ return okFrame({
78
+ type: 'interrupt',
79
+ ...idOf(raw),
80
+ ...threadOf(raw),
81
+ ...(requestId ? { requestId } : {}),
82
+ });
83
+ }
84
+ if (type === 'cancel_request') {
85
+ // EVERY FIELD IS OPTIONAL, so there is nothing to reject: no `requestId` means "the
86
+ // request in flight", which is exactly what a caller that lost its own bookkeeping
87
+ // would send. Whether the target exists is a ROUTING question, answered by the
88
+ // runner with an `ack` - not a parse question.
89
+ const requestId = optionalString(raw.requestId);
90
+ return okFrame({
91
+ type: 'cancel_request',
92
+ ...idOf(raw),
93
+ ...threadOf(raw),
94
+ ...(requestId ? { requestId } : {}),
95
+ });
96
+ }
97
+ if (type === 'decision')
98
+ return parseDecision(raw);
99
+ if (type === 'tool_result')
100
+ return parseToolResult(raw);
101
+ if (type === 'tool_register') {
102
+ if (!Array.isArray(raw.tools))
103
+ return invalid('A "tool_register" frame needs a "tools" array.');
104
+ return okFrame({ type: 'tool_register', tools: raw.tools, ...idOf(raw), ...threadOf(raw) });
105
+ }
106
+ if (type === 'thread_open')
107
+ return parseThreadOpen(raw);
108
+ if (type === 'thread_close')
109
+ return parseThreadClose(raw);
110
+ if (type === 'thread_list')
111
+ return okFrame({ type: 'thread_list', ...idOf(raw) });
112
+ if (type === 'hook_register') {
113
+ if (!Array.isArray(raw.hooks))
114
+ return invalid('A "hook_register" frame needs a "hooks" array.');
115
+ return okFrame({ type: 'hook_register', hooks: raw.hooks, ...idOf(raw), ...threadOf(raw) });
116
+ }
117
+ if (type === 'hook_answer')
118
+ return parseHookAnswer(raw);
119
+ if (type === 'ping')
120
+ return okFrame({ type: 'ping', ...idOf(raw) });
121
+ if (type === 'end')
122
+ return okFrame({ type: 'end' });
123
+ // FORWARD COMPATIBILITY, the one case that is silent by design.
124
+ return okFrame({ type: 'ignored', rawType: type });
125
+ }
126
+ function okFrame(frame) {
127
+ return { ok: true, frame };
128
+ }
129
+ function idOf(raw) {
130
+ const id = optionalString(raw.id);
131
+ return id ? { id } : {};
132
+ }
133
+ /**
134
+ * `threadId` is only ever the ROUTE, never validated here.
135
+ *
136
+ * Whether an id names an open thread is a registry question with an `ack` for an
137
+ * answer; whether it is well FORMED is `resolveThreadIdentity`'s question, and it
138
+ * produces the `thread_id_invalid` refusal. A parser that also judged the shape would
139
+ * give the same input two different refusals depending on which layer saw it first.
140
+ */
141
+ function threadOf(raw) {
142
+ const threadId = optionalString(raw.threadId);
143
+ return threadId ? { threadId } : {};
144
+ }
145
+ function parseThreadOpen(raw) {
146
+ const threadId = optionalString(raw.threadId);
147
+ if (!threadId)
148
+ return invalid('A "thread_open" frame needs a "threadId".');
149
+ const sessionId = optionalString(raw.sessionId);
150
+ return okFrame({
151
+ type: 'thread_open',
152
+ threadId,
153
+ ...idOf(raw),
154
+ ...(sessionId ? { sessionId } : {}),
155
+ });
156
+ }
157
+ function parseThreadClose(raw) {
158
+ const threadId = optionalString(raw.threadId);
159
+ if (!threadId)
160
+ return invalid('A "thread_close" frame needs a "threadId".');
161
+ // AN UNKNOWN MODE IS A PARSE ERROR, NOT A DEFAULT (AC-31 / AC-32). Coercing `"ABORT"` or
162
+ // a typo to `settle` would answer "wait for the request in flight" to a caller that asked
163
+ // to cancel it - the two behaviours differ in whether the work happens at all.
164
+ if (raw.mode !== undefined && raw.mode !== 'abort' && raw.mode !== 'settle') {
165
+ return invalid('A "thread_close" mode must be "settle" or "abort".');
166
+ }
167
+ return okFrame({
168
+ type: 'thread_close',
169
+ threadId,
170
+ ...idOf(raw),
171
+ ...(raw.mode === 'abort' || raw.mode === 'settle' ? { mode: raw.mode } : {}),
172
+ });
173
+ }
174
+ function parseHookAnswer(raw) {
175
+ const respondsTo = optionalString(raw.respondsTo);
176
+ if (!respondsTo)
177
+ return invalid('A "hook_answer" frame needs "respondsTo".');
178
+ const action = optionalString(raw.action);
179
+ if (!action)
180
+ return invalid('A "hook_answer" frame needs an "action".');
181
+ return okFrame({
182
+ type: 'hook_answer',
183
+ respondsTo,
184
+ action,
185
+ ...idOf(raw),
186
+ ...threadOf(raw),
187
+ ...(typeof raw.text === 'string' ? { text: raw.text } : {}),
188
+ ...(raw.input !== undefined ? { input: raw.input } : {}),
189
+ ...(raw.content !== undefined ? { content: raw.content } : {}),
190
+ ...(raw.isError === true ? { isError: true } : {}),
191
+ ...(typeof raw.reason === 'string' ? { reason: raw.reason } : {}),
192
+ ...(typeof raw.message === 'string' ? { message: raw.message } : {}),
193
+ });
194
+ }
195
+ function parsePrompt(raw, idSource) {
196
+ const text = typeof raw.text === 'string' ? raw.text : raw.content;
197
+ if (typeof text !== 'string' || text.trim().length === 0) {
198
+ return invalid('A "prompt" frame needs a non-empty "text".');
199
+ }
200
+ return okFrame({ type: 'prompt', text, ...idOf(idSource), ...threadOf(raw) });
201
+ }
202
+ function parseDecision(raw) {
203
+ const respondsTo = optionalString(raw.respondsTo);
204
+ if (!respondsTo)
205
+ return invalid('A "decision" frame needs "respondsTo".');
206
+ const frame = { type: 'decision', respondsTo, ...idOf(raw), ...threadOf(raw) };
207
+ if (raw.decision === 'allow' || raw.decision === 'deny')
208
+ frame.decision = raw.decision;
209
+ if (raw.scope === 'once' || raw.scope === 'session')
210
+ frame.scope = raw.scope;
211
+ if (typeof raw.reason === 'string')
212
+ frame.reason = raw.reason;
213
+ if (typeof raw.feedback === 'string')
214
+ frame.feedback = raw.feedback;
215
+ if (raw.cancelled === true)
216
+ frame.cancelled = true;
217
+ if (raw.answers !== undefined)
218
+ frame.answers = raw.answers;
219
+ if (raw.planDecision !== undefined)
220
+ frame.planDecision = raw.planDecision;
221
+ return okFrame(frame);
222
+ }
223
+ function parseToolResult(raw) {
224
+ const respondsTo = optionalString(raw.respondsTo);
225
+ if (!respondsTo)
226
+ return invalid('A "tool_result" frame needs "respondsTo".');
227
+ if (raw.content === undefined)
228
+ return invalid('A "tool_result" frame needs "content".');
229
+ return okFrame({
230
+ type: 'tool_result',
231
+ respondsTo,
232
+ content: raw.content,
233
+ ...(raw.isError === true ? { isError: true } : {}),
234
+ ...idOf(raw),
235
+ ...threadOf(raw),
236
+ });
237
+ }
238
+ /** Keep an error message readable without dumping a megabyte into the event. */
239
+ function clip(text) {
240
+ return text.length <= 120 ? text : `${text.slice(0, 117)}...`;
241
+ }
242
+ // ---------------------------------------------------------------------------
243
+ // Outbound builders
244
+ // ---------------------------------------------------------------------------
245
+ /**
246
+ * Map a `HumanInputGate` request onto the frame that asks the caller.
247
+ *
248
+ * THE CLI IS THE AUTHORITY ON THE QUESTIONS, and this function is where that is
249
+ * visible: it copies the normalized question set the model produced, so the
250
+ * caller renders what was actually asked rather than inventing its own text.
251
+ */
252
+ export function humanDecisionFrame(req) {
253
+ return req.kind === 'questions'
254
+ ? { kind: 'questions', questions: req.questions }
255
+ : { kind: 'plan', plan: req.plan };
256
+ }