@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.
- package/CHANGELOG.md +2523 -2326
- package/README.md +3614 -3173
- package/dist/agent/controller.d.ts +142 -1
- package/dist/agent/controller.js +171 -3
- package/dist/agent/reducer.d.ts +41 -1
- package/dist/agent/reducer.js +25 -0
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +5 -0
- package/dist/cli.js +85 -27
- package/dist/commands/builtins.js +13 -1
- package/dist/commands/keys-command.d.ts +23 -0
- package/dist/commands/keys-command.js +38 -0
- package/dist/commands/registry.d.ts +39 -0
- package/dist/commands/task-command.d.ts +39 -0
- package/dist/commands/task-command.js +102 -0
- package/dist/config/cli-commands.d.ts +23 -0
- package/dist/config/cli-commands.js +36 -0
- package/dist/config/env.js +17 -4
- package/dist/config/load.js +6 -1
- package/dist/config/schema.d.ts +43 -4
- package/dist/config/schema.js +31 -5
- package/dist/config/store.js +17 -2
- package/dist/diagnostics/doctor.d.ts +9 -0
- package/dist/diagnostics/doctor.js +34 -0
- package/dist/diagnostics/info.d.ts +71 -0
- package/dist/diagnostics/info.js +91 -5
- package/dist/exec/bridge/caller-tools.d.ts +96 -0
- package/dist/exec/bridge/caller-tools.js +213 -0
- package/dist/exec/bridge/decisions.d.ts +78 -0
- package/dist/exec/bridge/decisions.js +116 -0
- package/dist/exec/bridge/human-answers.d.ts +38 -0
- package/dist/exec/bridge/human-answers.js +164 -0
- package/dist/exec/bridge/limits.d.ts +68 -0
- package/dist/exec/bridge/limits.js +68 -0
- package/dist/exec/bridge/ndjson.d.ts +17 -0
- package/dist/exec/bridge/ndjson.js +83 -0
- package/dist/exec/bridge/pending.d.ts +70 -0
- package/dist/exec/bridge/pending.js +132 -0
- package/dist/exec/bridge/protocol.d.ts +346 -0
- package/dist/exec/bridge/protocol.js +256 -0
- package/dist/exec/bridge/server.d.ts +142 -0
- package/dist/exec/bridge/server.js +502 -0
- package/dist/exec/bridge/source.d.ts +58 -0
- package/dist/exec/bridge/source.js +145 -0
- package/dist/exec/bridge/writer.d.ts +26 -0
- package/dist/exec/bridge/writer.js +156 -0
- package/dist/exec/cli-commands.js +30 -2
- package/dist/exec/emitter.d.ts +16 -2
- package/dist/exec/emitter.js +26 -6
- package/dist/exec/events.d.ts +465 -10
- package/dist/exec/events.js +47 -1
- package/dist/exec/hooks/dispatch.d.ts +76 -0
- package/dist/exec/hooks/dispatch.js +169 -0
- package/dist/exec/hooks/index.d.ts +83 -0
- package/dist/exec/hooks/index.js +205 -0
- package/dist/exec/hooks/limits.d.ts +56 -0
- package/dist/exec/hooks/limits.js +49 -0
- package/dist/exec/hooks/registry.d.ts +49 -0
- package/dist/exec/hooks/registry.js +91 -0
- package/dist/exec/hooks/tool-wrapper.d.ts +94 -0
- package/dist/exec/hooks/tool-wrapper.js +154 -0
- package/dist/exec/hooks/turn.d.ts +61 -0
- package/dist/exec/hooks/turn.js +59 -0
- package/dist/exec/hooks/validate.d.ts +85 -0
- package/dist/exec/hooks/validate.js +310 -0
- package/dist/exec/index.d.ts +130 -4
- package/dist/exec/index.js +501 -10
- package/dist/exec/options.d.ts +124 -2
- package/dist/exec/options.js +413 -6
- package/dist/exec/permission.d.ts +9 -1
- package/dist/exec/permission.js +7 -1
- package/dist/exec/request/limits.d.ts +51 -0
- package/dist/exec/request/limits.js +49 -0
- package/dist/exec/request/scope.d.ts +106 -0
- package/dist/exec/request/scope.js +149 -0
- package/dist/exec/result/limits.d.ts +74 -0
- package/dist/exec/result/limits.js +72 -0
- package/dist/exec/result/prompt.d.ts +28 -0
- package/dist/exec/result/prompt.js +74 -0
- package/dist/exec/result/schema.d.ts +99 -0
- package/dist/exec/result/schema.js +464 -0
- package/dist/exec/result/sink.d.ts +56 -0
- package/dist/exec/result/sink.js +50 -0
- package/dist/exec/result/tool.d.ts +37 -0
- package/dist/exec/result/tool.js +118 -0
- package/dist/exec/runner.d.ts +395 -1
- package/dist/exec/runner.js +860 -8
- package/dist/exec/threads/aggregate.d.ts +34 -0
- package/dist/exec/threads/aggregate.js +30 -0
- package/dist/exec/threads/capacity.d.ts +23 -0
- package/dist/exec/threads/capacity.js +38 -0
- package/dist/exec/threads/dispatch-source.d.ts +60 -0
- package/dist/exec/threads/dispatch-source.js +106 -0
- package/dist/exec/threads/execute-threads.d.ts +53 -0
- package/dist/exec/threads/execute-threads.js +234 -0
- package/dist/exec/threads/gate.d.ts +42 -0
- package/dist/exec/threads/gate.js +81 -0
- package/dist/exec/threads/identity.d.ts +36 -0
- package/dist/exec/threads/identity.js +67 -0
- package/dist/exec/threads/interrupt.d.ts +29 -0
- package/dist/exec/threads/interrupt.js +79 -0
- package/dist/exec/threads/limits.d.ts +58 -0
- package/dist/exec/threads/limits.js +35 -0
- package/dist/exec/threads/outcome.d.ts +47 -0
- package/dist/exec/threads/outcome.js +26 -0
- package/dist/exec/threads/registry.d.ts +81 -0
- package/dist/exec/threads/registry.js +427 -0
- package/dist/exec/threads/runtime.d.ts +107 -0
- package/dist/exec/threads/runtime.js +523 -0
- package/dist/exec/threads/thread-source.d.ts +20 -0
- package/dist/exec/threads/thread-source.js +134 -0
- package/dist/fast/limits.d.ts +54 -0
- package/dist/fast/limits.js +54 -0
- package/dist/fast/summary-call.d.ts +72 -0
- package/dist/fast/summary-call.js +133 -0
- package/dist/fast/summary-prompt.d.ts +106 -0
- package/dist/fast/summary-prompt.js +320 -0
- package/dist/fast/task-summary-types.d.ts +66 -0
- package/dist/fast/task-summary-types.js +17 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/limits.d.ts +7 -0
- package/dist/proc/limits.js +7 -0
- package/dist/proc/output-streams.d.ts +50 -0
- package/dist/proc/output-streams.js +54 -0
- package/dist/team/runtime.d.ts +9 -0
- package/dist/team/runtime.js +8 -0
- package/dist/team/subagent.d.ts +15 -0
- package/dist/team/subagent.js +45 -1
- package/dist/tools/bash-tool.d.ts +10 -0
- package/dist/tools/bash-tool.js +71 -6
- package/dist/tools/index.d.ts +93 -1
- package/dist/tools/index.js +112 -3
- package/dist/tools/tool-gate.d.ts +100 -0
- package/dist/tools/tool-gate.js +87 -0
- package/dist/ui/App.js +224 -11
- package/dist/ui/Header.d.ts +28 -16
- package/dist/ui/Header.js +21 -19
- package/dist/ui/interaction-hints.js +6 -0
- package/dist/ui/layout/OverlayFrame.d.ts +18 -0
- package/dist/ui/layout/OverlayFrame.js +19 -4
- package/dist/ui/layout/header-hints.d.ts +55 -62
- package/dist/ui/layout/header-hints.js +59 -166
- package/dist/ui/layout/header-left.d.ts +115 -0
- package/dist/ui/layout/header-left.js +206 -0
- package/dist/ui/layout/shortcut-panel-layout.d.ts +191 -0
- package/dist/ui/layout/shortcut-panel-layout.js +321 -0
- package/dist/ui/layout/status-detail-layout.js +5 -1
- package/dist/ui/layout/task-summary-cell.d.ts +61 -0
- package/dist/ui/layout/task-summary-cell.js +65 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +12 -0
- package/dist/ui/overlays/HelpOverlay.js +29 -2
- package/dist/ui/overlays/ShortcutPanel.d.ts +39 -0
- package/dist/ui/overlays/ShortcutPanel.js +106 -0
- package/dist/ui/shortcuts.d.ts +81 -0
- package/dist/ui/shortcuts.js +221 -0
- package/package.json +2 -2
package/dist/exec/index.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* happen.
|
|
18
18
|
*/
|
|
19
19
|
import { readFileSync } from 'node:fs';
|
|
20
|
+
import { basename } from 'node:path';
|
|
20
21
|
import { randomUUID } from 'node:crypto';
|
|
21
22
|
import process from 'node:process';
|
|
22
23
|
import { runHeadless } from '../agent/headless.js';
|
|
@@ -28,6 +29,20 @@ import { resolveExecOptions } from './options.js';
|
|
|
28
29
|
import { resolvePermission } from './permission.js';
|
|
29
30
|
import { ExecRunner, singlePrompt } from './runner.js';
|
|
30
31
|
import { createStdinPromptSource, prefixSource } from './stdin-stream.js';
|
|
32
|
+
import { getLogger } from '../logging/logger.js';
|
|
33
|
+
import { LEAD_SCOPE } from './bridge/protocol.js';
|
|
34
|
+
import { createBridgeWriter } from './bridge/writer.js';
|
|
35
|
+
import { createBridgeServer } from './bridge/server.js';
|
|
36
|
+
import { createBridgePromptSource } from './bridge/source.js';
|
|
37
|
+
import { createCallerTools } from './bridge/caller-tools.js';
|
|
38
|
+
import { createCallerApprovalGate, createCallerHumanInputGate, createCallerToolGate, } from './bridge/decisions.js';
|
|
39
|
+
import { RESULT_LIMITS } from './result/limits.js';
|
|
40
|
+
import { parseResultSchema } from './result/schema.js';
|
|
41
|
+
import { createResultSink } from './result/sink.js';
|
|
42
|
+
import { createSubmitResultTool, SUBMIT_RESULT_TOOL } from './result/tool.js';
|
|
43
|
+
import { buildStructuredResultBlock } from './result/prompt.js';
|
|
44
|
+
import { createHookRuntime } from './hooks/index.js';
|
|
45
|
+
import { executeThreads } from './threads/execute-threads.js';
|
|
31
46
|
/**
|
|
32
47
|
* Run one `aragon exec` invocation and resolve with the process exit code.
|
|
33
48
|
*
|
|
@@ -43,7 +58,14 @@ export async function runExec(flags, raw, promptArg, ctx) {
|
|
|
43
58
|
const out = ctx.stdout ?? process.stdout;
|
|
44
59
|
const err = ctx.stderr ?? process.stderr;
|
|
45
60
|
const now = ctx.now ?? Date.now;
|
|
46
|
-
|
|
61
|
+
// THE ONE FACT FROM CONFIG THAT CHANGES A FLAG'S LEGALITY, READ ONCE. `--confirm`
|
|
62
|
+
// is `config.confirmTools` (D-6), and without it `ask` plus that setting would
|
|
63
|
+
// arm two approval layers over the same three tools - an invocation whose
|
|
64
|
+
// visible failure is "it never asks". `loadConfig` is read here rather than
|
|
65
|
+
// below because this is the first consumer, and `options.ts` stays a pure
|
|
66
|
+
// flag parser: it is told the fact, it does not go looking for it.
|
|
67
|
+
const config = loadConfig(flags);
|
|
68
|
+
const resolved = resolveExecOptions(raw, { confirmTools: config.confirmTools });
|
|
47
69
|
if (!resolved.ok) {
|
|
48
70
|
err.write(`${resolved.message}\n`);
|
|
49
71
|
return 2;
|
|
@@ -54,10 +76,63 @@ export async function runExec(flags, raw, promptArg, ctx) {
|
|
|
54
76
|
const appended = readOptionalText(options.appendSystemPrompt, options.appendSystemPromptFile, err, '--append-system-prompt-file');
|
|
55
77
|
if (appended === null)
|
|
56
78
|
return 2;
|
|
57
|
-
|
|
79
|
+
// THE CALLER'S SCHEMA, READ AND VALIDATED BEFORE ANYTHING ELSE HAPPENS.
|
|
80
|
+
//
|
|
81
|
+
// EXIT 2 WITH NOTHING ON STDOUT on every failure path, which is the same contract
|
|
82
|
+
// options, prompt and session errors already follow: a run that never started must
|
|
83
|
+
// not emit a `result` line, because `system/init` being first is what lets a wrapper
|
|
84
|
+
// tell "failed to start" from "started and failed".
|
|
85
|
+
//
|
|
86
|
+
// THE SINK IS CREATED HERE, before the controller, for the same reason the interrupt
|
|
87
|
+
// box is: the tool factory needs it, the tool is passed to the controller factory,
|
|
88
|
+
// and the runner only ever reads it.
|
|
89
|
+
const resultSpec = readResultSchema(options, err);
|
|
90
|
+
if (resultSpec === 'failed')
|
|
91
|
+
return 2;
|
|
92
|
+
const resultSink = resultSpec === null ? null : createResultSink({ now });
|
|
93
|
+
const cwd = config.cwd;
|
|
58
94
|
const plan = planSession(options, cwd, err);
|
|
59
95
|
if (!plan)
|
|
60
96
|
return 2;
|
|
97
|
+
// THREAD MODE TAKES ITS OWN PATH, AND IT TAKES IT HERE (section 2.6 row 6).
|
|
98
|
+
//
|
|
99
|
+
// BEFORE ANY PROMPT SOURCE EXISTS, because a source would have started reading stdin. This
|
|
100
|
+
// is the ONE branch the feature adds to this file: everything below - the interrupt box,
|
|
101
|
+
// the bridge runtime, the prompt source, the single-runner assembly - belongs to the
|
|
102
|
+
// single-conversation path and is untouched when `--threads` is on.
|
|
103
|
+
if (options.threads) {
|
|
104
|
+
// A THREAD RUN OWNS STDIN (I-T12). A pre-built source would be a second splitter on the
|
|
105
|
+
// same bytes, and the frames would be split between them - so the combination is refused
|
|
106
|
+
// outright rather than diagnosed later from mysterious lost prompts.
|
|
107
|
+
// A POSITIONAL PROMPT CANNOT BE HONOURED HERE, WHATEVER STDIN IS DOING: thread mode has
|
|
108
|
+
// no single conversation to send it to, and dropping it silently would look like a model
|
|
109
|
+
// that ignored the request. Refused for every caller, not just the ones without a stdin.
|
|
110
|
+
if (promptArg !== undefined) {
|
|
111
|
+
err.write('threads_flag_conflict: --threads takes its prompts from stdin frames; drop the [prompt] argument.\n');
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
return executeThreads({
|
|
115
|
+
flags,
|
|
116
|
+
options,
|
|
117
|
+
plan: { id: plan.id, saving: plan.saving, cwd },
|
|
118
|
+
ctx,
|
|
119
|
+
out,
|
|
120
|
+
err,
|
|
121
|
+
now,
|
|
122
|
+
version: ctx.version,
|
|
123
|
+
resultSpec: resultSpec,
|
|
124
|
+
stdin: ctx.stdin ?? process.stdin,
|
|
125
|
+
persist: (input) => persistThreadSession({
|
|
126
|
+
sessionId: input.sessionId,
|
|
127
|
+
controller: input.controller,
|
|
128
|
+
turns: input.turns,
|
|
129
|
+
usage: input.usage,
|
|
130
|
+
at: input.at,
|
|
131
|
+
cli: ctx.version,
|
|
132
|
+
cwd,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
61
136
|
/**
|
|
62
137
|
* Late-bound interrupt hook (see `resolvePromptSource` and `execute`).
|
|
63
138
|
*
|
|
@@ -69,7 +144,43 @@ export async function runExec(flags, raw, promptArg, ctx) {
|
|
|
69
144
|
* `execute` for why "does nothing" beats "does half of it".
|
|
70
145
|
*/
|
|
71
146
|
const interrupt = { fire: () => { } };
|
|
72
|
-
|
|
147
|
+
// THE BRIDGE EXISTS BEFORE THE PROMPT SOURCE, because the source routes control
|
|
148
|
+
// frames to it and the source is what starts reading stdin. It also exists before
|
|
149
|
+
// the controller, which is why the server reaches the controller through
|
|
150
|
+
// `bindController` rather than a constructor argument.
|
|
151
|
+
//
|
|
152
|
+
// THE HOOK RUNTIME EXISTS BEFORE BOTH (headless-threads-and-lifecycle-hooks section
|
|
153
|
+
// 2.4.9): the tool array is built once, inside `makeController`, so the wrapper has to be
|
|
154
|
+
// available then - even though `hook_register` cannot arrive until later. It is created for
|
|
155
|
+
// EVERY bridge run, and with no hook registered it emits nothing and changes nothing.
|
|
156
|
+
const controllerBox = { controller: null };
|
|
157
|
+
/**
|
|
158
|
+
* The writer, in a box because the hook runtime needs it before the bridge exists.
|
|
159
|
+
*
|
|
160
|
+
* ONE WRITER, ONE `seq` (I-C3). The hook runtime is constructed first only because the
|
|
161
|
+
* controller is; the object it writes through is the same one the server uses, reached
|
|
162
|
+
* through this box instead of being duplicated.
|
|
163
|
+
*/
|
|
164
|
+
const bridgeWriterBox = {};
|
|
165
|
+
const hooks = options.inputFormat === 'bridge'
|
|
166
|
+
? createHookRuntime({
|
|
167
|
+
sessionId: plan.id,
|
|
168
|
+
write: (event) => bridgeWriterBox.writer?.write(event),
|
|
169
|
+
knownTools: () => {
|
|
170
|
+
const names = new Set((controllerBox.controller?.listTools() ?? []).map((tool) => tool.name));
|
|
171
|
+
for (const name of controllerBox.controller?.callerToolNames() ?? [])
|
|
172
|
+
names.add(name);
|
|
173
|
+
return names;
|
|
174
|
+
},
|
|
175
|
+
timeoutMs: options.hookTimeoutMs,
|
|
176
|
+
policy: () => options.onHookTimeout,
|
|
177
|
+
now,
|
|
178
|
+
})
|
|
179
|
+
: undefined;
|
|
180
|
+
const bridge = options.inputFormat === 'bridge'
|
|
181
|
+
? createBridgeRuntime({ out, sessionId: plan.id, options, hooks, writerBox: bridgeWriterBox })
|
|
182
|
+
: null;
|
|
183
|
+
const prompts = resolvePromptSource(options, promptArg, ctx, err, interrupt, bridge);
|
|
73
184
|
if (!prompts)
|
|
74
185
|
return 2;
|
|
75
186
|
const lock = plan.saving ? tryAcquireSessionLock(plan.id) : null;
|
|
@@ -79,12 +190,207 @@ export async function runExec(flags, raw, promptArg, ctx) {
|
|
|
79
190
|
return 2;
|
|
80
191
|
}
|
|
81
192
|
try {
|
|
82
|
-
return await execute({
|
|
193
|
+
return await execute({
|
|
194
|
+
flags,
|
|
195
|
+
options,
|
|
196
|
+
plan,
|
|
197
|
+
prompts,
|
|
198
|
+
ctx,
|
|
199
|
+
out,
|
|
200
|
+
err,
|
|
201
|
+
now,
|
|
202
|
+
appended,
|
|
203
|
+
interrupt,
|
|
204
|
+
bridge,
|
|
205
|
+
...(hooks ? { hooks } : {}),
|
|
206
|
+
controllerBox,
|
|
207
|
+
bridgeWriterBox,
|
|
208
|
+
resultSpec: resultSpec,
|
|
209
|
+
resultSink,
|
|
210
|
+
});
|
|
83
211
|
}
|
|
84
212
|
finally {
|
|
213
|
+
// A SECOND CLOSE, AND THE ONLY ONE ON THE EARLY-EXIT PATHS. The real one runs
|
|
214
|
+
// inside `execute()` before `persist()` and before `result` (see there);
|
|
215
|
+
// `close()` is idempotent, so this call covers a run that never reached that
|
|
216
|
+
// point - a throw, or a usage error after the channel existed. It cannot settle
|
|
217
|
+
// anything into a stream whose `result` has already gone out, because
|
|
218
|
+
// `execute()` sealed the writer.
|
|
219
|
+
await bridge?.server.close('run_finished');
|
|
220
|
+
// AFTER the protocol is closed, never before: releasing stdin first would drop
|
|
221
|
+
// the frames a caller is still entitled to send while the run finishes
|
|
222
|
+
// (`decision`, `tool_result`). Once the server is closed, nothing more can be
|
|
223
|
+
// answered - and the listener that would keep this process alive is exactly
|
|
224
|
+
// what `close()` removes.
|
|
225
|
+
toBridgeSource(prompts)?.close();
|
|
85
226
|
lock?.release();
|
|
86
227
|
}
|
|
87
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Read and preflight `--result-schema`.
|
|
231
|
+
*
|
|
232
|
+
* THREE OUTCOMES, and the third one is not a schema: `null` means "no schema was
|
|
233
|
+
* asked for", `'failed'` means "one was asked for and it is unusable". The caller
|
|
234
|
+
* turns the second into exit 2 with nothing on stdout, which is the contract every
|
|
235
|
+
* other preflight failure in this file already follows.
|
|
236
|
+
*/
|
|
237
|
+
function readResultSchema(options, err) {
|
|
238
|
+
if (options.resultSchemaPath === undefined)
|
|
239
|
+
return null;
|
|
240
|
+
let text;
|
|
241
|
+
try {
|
|
242
|
+
text = readFileSync(options.resultSchemaPath, 'utf-8');
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
reportUsageError(err, 'result_schema_unreadable', `--result-schema: could not read ${options.resultSchemaPath} (${e.message}).`);
|
|
246
|
+
return 'failed';
|
|
247
|
+
}
|
|
248
|
+
const parsed = parseResultSchema(text, basename(options.resultSchemaPath));
|
|
249
|
+
if (!parsed.ok) {
|
|
250
|
+
reportUsageError(err, parsed.code, parsed.message);
|
|
251
|
+
return 'failed';
|
|
252
|
+
}
|
|
253
|
+
return parsed.spec;
|
|
254
|
+
}
|
|
255
|
+
/** One line on stderr, prefixed with the stable code so a caller can grep for it. */
|
|
256
|
+
function reportUsageError(err, code, message) {
|
|
257
|
+
err.write(`${code}: ${message}\n`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Build the writer and the server.
|
|
261
|
+
*
|
|
262
|
+
* ORDER INSIDE HERE IS NOT COSMETIC: the writer is what the `caller_not_reading`
|
|
263
|
+
* escalation needs, the server is what the three gates and the prompt source
|
|
264
|
+
* need, and the controller - which the server's ports call - is bound later by
|
|
265
|
+
* `bindController`. Every port below is therefore written as `bound?.`-style
|
|
266
|
+
* access rather than closing over a controller that does not exist yet.
|
|
267
|
+
*/
|
|
268
|
+
function createBridgeRuntime(args) {
|
|
269
|
+
let bound = null;
|
|
270
|
+
/**
|
|
271
|
+
* A `tool_register` CAN ARRIVE BEFORE THE CONTROLLER EXISTS.
|
|
272
|
+
*
|
|
273
|
+
* The prompt source starts reading stdin in `runExec`, and the controller is
|
|
274
|
+
* built a few statements later in `execute`. A caller whose first write is its
|
|
275
|
+
* tool declaration - which is the natural order, and the one the README shows -
|
|
276
|
+
* would otherwise be acknowledged and then silently dropped, and the model would
|
|
277
|
+
* never see the tool with nothing anywhere reporting why. So the set is parked
|
|
278
|
+
* here and applied by `bindController`.
|
|
279
|
+
*/
|
|
280
|
+
let parkedSpecs = null;
|
|
281
|
+
const hardLimit = { fired: false };
|
|
282
|
+
const writer = createBridgeWriter({
|
|
283
|
+
out: args.out,
|
|
284
|
+
sessionId: args.sessionId,
|
|
285
|
+
onHardLimit: () => {
|
|
286
|
+
// ONE WAY OUT, and it is the existing abort: the run settles through the
|
|
287
|
+
// normal path (persist, then `result`), and `execute` turns the flag into
|
|
288
|
+
// exit 1. Exiting from inside a write would skip both.
|
|
289
|
+
hardLimit.fired = true;
|
|
290
|
+
bound?.abort();
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
const box = { server: null };
|
|
294
|
+
const server = createBridgeServer({
|
|
295
|
+
writer,
|
|
296
|
+
sessionId: args.sessionId,
|
|
297
|
+
decisionTimeoutMs: args.options.decisionTimeoutMs,
|
|
298
|
+
callerToolTimeoutMs: args.options.callerToolTimeoutMs,
|
|
299
|
+
ports: {
|
|
300
|
+
applyCallerTools: (specs) => {
|
|
301
|
+
const target = box.server;
|
|
302
|
+
if (!target || !bound) {
|
|
303
|
+
parkedSpecs = specs;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
bound.applyCallerTools(createCallerTools(specs, target, args.options.callerToolTimeoutMs));
|
|
307
|
+
},
|
|
308
|
+
isTurnActive: () => bound?.isRunning() === true,
|
|
309
|
+
callerToolNames: () => bound?.callerToolNames() ?? [],
|
|
310
|
+
getAgentMode: () => bound?.getAgentMode() ?? 'build',
|
|
311
|
+
},
|
|
312
|
+
// THE AUDIT TRAIL IS THE LOG FILE (section 2.5). Not a second in-memory ledger: the
|
|
313
|
+
// pending table already detects duplicates, and a ring buffer with no reader
|
|
314
|
+
// is state that can only drift.
|
|
315
|
+
onAudit: (record) => getLogger().info('bridge', 'decision', { ...record }),
|
|
316
|
+
// THE HOOK CHANNEL (headless-threads-and-lifecycle-hooks section 2.4.2). Present on
|
|
317
|
+
// every bridge run, because a hook may be registered at any moment; with none
|
|
318
|
+
// registered these two callbacks are never entered.
|
|
319
|
+
...(args.hooks
|
|
320
|
+
? {
|
|
321
|
+
hookPorts: {
|
|
322
|
+
register: (frame) => {
|
|
323
|
+
const ack = args.hooks?.register(frame);
|
|
324
|
+
if (!ack)
|
|
325
|
+
return;
|
|
326
|
+
writer.write({
|
|
327
|
+
type: 'ack',
|
|
328
|
+
...(frame.id ? { id: frame.id } : {}),
|
|
329
|
+
accepted: ack.accepted,
|
|
330
|
+
...(ack.code ? { code: ack.code } : {}),
|
|
331
|
+
...(ack.message ? { message: ack.message } : {}),
|
|
332
|
+
...(ack.hooks ? { hooks: ack.hooks } : {}),
|
|
333
|
+
...(ack.warnings ? { warnings: ack.warnings } : {}),
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
answer: (frame) => args.hooks?.answer(frame) ?? false,
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
: {}),
|
|
340
|
+
});
|
|
341
|
+
box.server = server;
|
|
342
|
+
// THE BOX, filled the moment the writer exists (section 2.4.9): the hook runtime is built
|
|
343
|
+
// before this function runs, and it must write through THIS writer so `seq` stays global.
|
|
344
|
+
if (args.writerBox)
|
|
345
|
+
args.writerBox.writer = writer;
|
|
346
|
+
return {
|
|
347
|
+
writer,
|
|
348
|
+
server,
|
|
349
|
+
sessionId: args.sessionId,
|
|
350
|
+
hardLimit,
|
|
351
|
+
bindController: (controller) => {
|
|
352
|
+
bound = controller;
|
|
353
|
+
if (!parkedSpecs)
|
|
354
|
+
return;
|
|
355
|
+
const specs = parkedSpecs;
|
|
356
|
+
parkedSpecs = null;
|
|
357
|
+
const target = box.server;
|
|
358
|
+
if (!target)
|
|
359
|
+
return;
|
|
360
|
+
controller.applyCallerTools(createCallerTools(specs, target, args.options.callerToolTimeoutMs));
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The three gates, or nothing at all.
|
|
366
|
+
*
|
|
367
|
+
* `{}` IS THE WHOLE COMPATIBILITY GUARANTEE: with no bridge, `makeController`
|
|
368
|
+
* receives exactly the deps object it receives today, so `createBuiltinTools`
|
|
369
|
+
* builds today's array and every `AC-G17`-style identity assertion still holds.
|
|
370
|
+
*/
|
|
371
|
+
function bridgeGates(bridge, options) {
|
|
372
|
+
const allow = new Set(options.allowTools);
|
|
373
|
+
const gateFor = (scope) => createCallerToolGate({
|
|
374
|
+
server: bridge.server,
|
|
375
|
+
mode: options.permissionMode,
|
|
376
|
+
allow,
|
|
377
|
+
scope,
|
|
378
|
+
decisionTimeoutMs: options.decisionTimeoutMs,
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
toolGate: gateFor(LEAD_SCOPE),
|
|
382
|
+
// ONE TABLE, ONE SET OF IDS, ONE CAPACITY - a fan-out of three is still three
|
|
383
|
+
// outstanding decisions, not three independent budgets.
|
|
384
|
+
toolGateForChild: (label) => gateFor({ id: label, label, depth: 1 }),
|
|
385
|
+
approval: createCallerApprovalGate(bridge.server),
|
|
386
|
+
humanInput: createCallerHumanInputGate(bridge.server),
|
|
387
|
+
decisionBudgetMs: options.decisionTimeoutMs,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/** `ExecPromptSource` -> the bridge source's narrower type, or `undefined`. */
|
|
391
|
+
function toBridgeSource(source) {
|
|
392
|
+
return 'cancelQueued' in source ? source : undefined;
|
|
393
|
+
}
|
|
88
394
|
async function execute(input) {
|
|
89
395
|
const { options, plan, ctx, out, err, now, interrupt } = input;
|
|
90
396
|
const startedAt = now();
|
|
@@ -93,22 +399,101 @@ async function execute(input) {
|
|
|
93
399
|
allow: options.allowTools,
|
|
94
400
|
deny: options.denyTools,
|
|
95
401
|
});
|
|
402
|
+
const bridge = input.bridge;
|
|
403
|
+
const gates = bridge ? bridgeGates(bridge, options) : {};
|
|
404
|
+
// THE TWO INJECTION POINTS OF R3 (section 2.6 rows 11 and 17), both SPREAD
|
|
405
|
+
// CONDITIONALLY: with no schema neither key exists, so every other path builds the
|
|
406
|
+
// controller it builds today and I-S2 stays provable by construction.
|
|
407
|
+
//
|
|
408
|
+
// BOTH OR NEITHER IS THE RULE, and the tool is what the prompt block is about: a
|
|
409
|
+
// block without the tool asks the model for something it cannot do, and a tool
|
|
410
|
+
// without the block leaves it undiscovered (the R1 mitigation in section 7 is
|
|
411
|
+
// exactly this pair).
|
|
412
|
+
const resultTool = input.resultSpec && input.resultSink
|
|
413
|
+
? createSubmitResultTool({ spec: input.resultSpec, sink: input.resultSink })
|
|
414
|
+
: undefined;
|
|
415
|
+
const structuredResultBlock = input.resultSpec
|
|
416
|
+
? buildStructuredResultBlock({
|
|
417
|
+
toolName: SUBMIT_RESULT_TOOL,
|
|
418
|
+
hasDefinitions: input.resultSpec.hasDefinitions,
|
|
419
|
+
repairTurns: options.resultRepairTurns ?? RESULT_LIMITS.defaultRepairTurns,
|
|
420
|
+
})
|
|
421
|
+
: '';
|
|
96
422
|
const controller = ctx.makeController(withPermissionMode(input.flags, options.permissionMode), {
|
|
97
423
|
...(permission ? { permission } : {}),
|
|
98
424
|
...(input.appended ? { appendSystemPrompt: input.appended } : {}),
|
|
425
|
+
...(resultTool ? { resultTool } : {}),
|
|
426
|
+
...(structuredResultBlock ? { structuredResultBlock } : {}),
|
|
427
|
+
// THE HOOK RUNNER, SPREAD CONDITIONALLY AND NEXT TO ITS SIBLINGS (D-12): the tool array
|
|
428
|
+
// is built inside this factory, so a dep that is not forwarded here is dropped in
|
|
429
|
+
// silence and the caller's hooks never fire.
|
|
430
|
+
...(input.hooks ? { hookRunner: input.hooks.toolRunner } : {}),
|
|
431
|
+
...gates,
|
|
99
432
|
});
|
|
100
|
-
|
|
433
|
+
// THE BOX, so the hook runtime's tool whitelist can see the union this controller owns -
|
|
434
|
+
// including any `tool_register` that lands later.
|
|
435
|
+
if (input.controllerBox)
|
|
436
|
+
input.controllerBox.controller = controller;
|
|
437
|
+
// THE ONE ASSIGNMENT THAT CLOSES THE ORDERING HOLE: the server was built before
|
|
438
|
+
// the controller (the stdin source needs it) and needs the controller for the
|
|
439
|
+
// three host-side ports. Nothing else in the run may bind it.
|
|
440
|
+
bridge?.bindController(controller);
|
|
441
|
+
const emitter = createEmitter(options.outputFormat, out, bridge?.writer);
|
|
101
442
|
const runner = new ExecRunner({
|
|
102
443
|
emitter,
|
|
103
444
|
sessionId: plan.id,
|
|
104
445
|
...(options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {}),
|
|
105
446
|
...(options.maxDurationMs !== undefined ? { maxDurationMs: options.maxDurationMs } : {}),
|
|
447
|
+
// THE REQUEST-LEVEL BUDGETS AND THE RESULT BOX. Every one of them is conditional, so
|
|
448
|
+
// a run without the flags constructs exactly the runner it constructs today and the
|
|
449
|
+
// request counters inside it are never consulted (I-C1).
|
|
450
|
+
...(input.resultSink
|
|
451
|
+
? {
|
|
452
|
+
resultSink: input.resultSink,
|
|
453
|
+
...(input.resultSpec ? { resultSchemaName: input.resultSpec.name } : {}),
|
|
454
|
+
resultRepairTurns: options.resultRepairTurns ?? RESULT_LIMITS.defaultRepairTurns,
|
|
455
|
+
onResultUnsatisfied: options.onResultUnsatisfied ?? 'error',
|
|
456
|
+
}
|
|
457
|
+
: {}),
|
|
458
|
+
...(options.requestMaxTurns !== undefined
|
|
459
|
+
? { requestMaxTurns: options.requestMaxTurns }
|
|
460
|
+
: {}),
|
|
461
|
+
...(options.requestMaxDurationMs !== undefined
|
|
462
|
+
? { requestMaxDurationMs: options.requestMaxDurationMs }
|
|
463
|
+
: {}),
|
|
464
|
+
// THE ONE COST AUTHORITY, CLOSED OVER HERE. `buildCost` needs the controller's price
|
|
465
|
+
// table, and the runner needs it to answer "what did THIS request cost" - the same
|
|
466
|
+
// function read twice and differenced by the scope, which is what makes a request's
|
|
467
|
+
// `cost` equal to running it alone (AC-38).
|
|
468
|
+
cost: (usage, compaction) => buildCost(controller, usage, compaction),
|
|
106
469
|
partialMessages: options.partialMessages,
|
|
107
470
|
includeThinking: options.includeThinking,
|
|
108
471
|
followThrough: controller.getTodoConfig().followThrough,
|
|
109
472
|
quiet: options.quiet,
|
|
110
473
|
stderr: err,
|
|
111
474
|
now,
|
|
475
|
+
// THE TWO HOOK POINTS THAT LIVE IN THE RUNNER (section 2.4.6 / 2.4.7). Spread
|
|
476
|
+
// conditionally, so a run without a hook channel builds exactly today's runner.
|
|
477
|
+
...(input.hooks
|
|
478
|
+
? {
|
|
479
|
+
hookPort: {
|
|
480
|
+
promptPre: (probe) => input.hooks
|
|
481
|
+
? input.hooks.promptPre({
|
|
482
|
+
text: probe.text,
|
|
483
|
+
queued: probe.queued,
|
|
484
|
+
requestId: probe.requestId,
|
|
485
|
+
})
|
|
486
|
+
: Promise.resolve({ action: 'allow' }),
|
|
487
|
+
},
|
|
488
|
+
beforeTurn: (info) => input.hooks
|
|
489
|
+
? input.hooks.beforeTurn({
|
|
490
|
+
turn: info.turn,
|
|
491
|
+
source: info.source,
|
|
492
|
+
requestId: info.requestId,
|
|
493
|
+
})
|
|
494
|
+
: Promise.resolve(undefined),
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
112
497
|
});
|
|
113
498
|
if (plan.saved) {
|
|
114
499
|
controller.replaceMessages(plan.saved.messages, plan.saved.compactionIdentity);
|
|
@@ -126,8 +511,19 @@ async function execute(input) {
|
|
|
126
511
|
baseUrl: config.baseUrl ?? null,
|
|
127
512
|
},
|
|
128
513
|
permissionMode: options.permissionMode,
|
|
514
|
+
// `listTools()` IS THE UNION, so a caller tool registered before `init` (which
|
|
515
|
+
// the writer queues behind this very line) is already listed here. Reporting
|
|
516
|
+
// only the host's own set would make `init.tools` disagree with the model.
|
|
129
517
|
tools: controller.listTools().map((t) => t.name),
|
|
130
518
|
resumed: plan.saved !== null,
|
|
519
|
+
...(bridge ? { bridge: true } : {}),
|
|
520
|
+
// THE HOOK CHANNEL IS A BRIDGE CAPABILITY, NOT A THREAD ONE (section 9.9): a single
|
|
521
|
+
// conversation with one redactor is the commonest embedding shape, and tying hooks to
|
|
522
|
+
// threads would force it to sign a multi-tenant contract it does not use.
|
|
523
|
+
...(bridge ? { hooks: true } : {}),
|
|
524
|
+
// THE CAPABILITY IS A FACT ABOUT THIS RUN, NOT ABOUT THE BUILD (P0-4). Stamped from
|
|
525
|
+
// the preflighted schema, which is the only thing that can answer it.
|
|
526
|
+
...(input.resultSpec ? { structuredResult: true } : {}),
|
|
131
527
|
// W2 - where this run's conversation lives, so a wrapper can check on disk
|
|
132
528
|
// whether it is still resumable rather than re-deriving the sessions dir.
|
|
133
529
|
//
|
|
@@ -143,6 +539,12 @@ async function execute(input) {
|
|
|
143
539
|
sessionFile: plan.saving ? sessionPathFor(plan.id) : null,
|
|
144
540
|
});
|
|
145
541
|
runner.attach(controller);
|
|
542
|
+
// AFTER `attach`, and before the first prompt: the turn ports reach the
|
|
543
|
+
// controller, and `steer` / `interrupt` must be answerable from the very first
|
|
544
|
+
// frame the caller can send.
|
|
545
|
+
if (bridge) {
|
|
546
|
+
runner.attachBridge(bridge.server, toBridgeSource(input.prompts));
|
|
547
|
+
}
|
|
146
548
|
// AFTER `attach`, never before. The terminator closure ends on
|
|
147
549
|
// `this.controller?.abort()`, so before `attach` the controller is still
|
|
148
550
|
// `undefined` and that last statement is a silent no-op - while the SAME
|
|
@@ -165,8 +567,22 @@ async function execute(input) {
|
|
|
165
567
|
}
|
|
166
568
|
finally {
|
|
167
569
|
runner.detach();
|
|
570
|
+
// NOTHING IS SETTLED HERE: the protocol is closed a few lines below, while
|
|
571
|
+
// `result` is still ahead. Disposing the controller first would tear down the
|
|
572
|
+
// tool that is still waiting for its answer.
|
|
168
573
|
controller.dispose();
|
|
169
574
|
}
|
|
575
|
+
// A CALLER THAT STOPPED READING FAILED THE RUN. `caller_not_reading` already
|
|
576
|
+
// went out as a fatal `error` event; exit 1 is the part a shell script reads.
|
|
577
|
+
if (bridge?.hardLimit.fired)
|
|
578
|
+
exitCode = exitCode === 0 ? 1 : exitCode;
|
|
579
|
+
// CLOSE THE PROTOCOL HERE, WHILE `result` IS STILL AHEAD OF US. Closing it any
|
|
580
|
+
// later (in `runExec`'s `finally`) would be after `result` had already gone out,
|
|
581
|
+
// so an unanswered decision settled by `closeAll` would emit its
|
|
582
|
+
// `decision_resolved` on a stream whose last line is supposed to be `result`
|
|
583
|
+
// (AC-5). Doing it before `persist()` costs nothing: every answer that could
|
|
584
|
+
// still have arrived has been awaited already.
|
|
585
|
+
await bridge?.server.close('run_finished');
|
|
170
586
|
// PERSIST FIRST, ANNOUNCE SECOND (D-2). A caller that reads `result` and
|
|
171
587
|
// immediately respawns with the same `--session-id` must not race the writer
|
|
172
588
|
// of the file it is about to read.
|
|
@@ -174,12 +590,41 @@ async function execute(input) {
|
|
|
174
590
|
if (plan.saving)
|
|
175
591
|
persist(plan, controller, stats.turns, stats.usage, ctx.version, now());
|
|
176
592
|
const todo = controller.getTodoSnapshot?.() ?? null;
|
|
593
|
+
// EVERYTHING QUEUED BEHIND `result` GOES OUT FIRST. `result` is the last line by
|
|
594
|
+
// contract, so a `decision_resolved` still sitting in the writer would either be
|
|
595
|
+
// dropped (the emitter refuses to write after `result`) or arrive out of order.
|
|
596
|
+
await bridge?.writer.flush();
|
|
597
|
+
// THE STRUCTURED ANSWER, IF ANY. `result.result` is REPLACED by the canonical JSON
|
|
598
|
+
// text on success, because under a schema the answer IS that object - the README's
|
|
599
|
+
// `| jq -r .result` idiom has to keep meaning "the answer" - and the prose moves to
|
|
600
|
+
// `result.text` instead of being lost (section 2.3.7, rule 2).
|
|
601
|
+
const structured = stats.lastStructured;
|
|
602
|
+
const structuredValue = structured
|
|
603
|
+
? JSON.stringify(structured.value)
|
|
604
|
+
: stats.lastAssistantText;
|
|
177
605
|
emitter.result({
|
|
178
606
|
sessionId: plan.id,
|
|
179
|
-
isError: exitCode === 1,
|
|
180
|
-
stopReason: stats.stopReason,
|
|
607
|
+
isError: exitCode === 1 || bridge?.hardLimit.fired === true || stats.resultUnsatisfied,
|
|
608
|
+
stopReason: bridge?.hardLimit.fired ? 'error' : stats.stopReason,
|
|
181
609
|
exitCode,
|
|
182
|
-
result:
|
|
610
|
+
result: structuredValue,
|
|
611
|
+
...(input.resultSpec
|
|
612
|
+
? {
|
|
613
|
+
// EXPLICIT `null` ON FAILURE, NOT A MISSING KEY (section 2.3.7, rule 1): a
|
|
614
|
+
// caller that passed `--result-schema` must be able to read one field and
|
|
615
|
+
// know, and "absent" would be indistinguishable from "an old build".
|
|
616
|
+
structured: structured
|
|
617
|
+
? {
|
|
618
|
+
value: structured.value,
|
|
619
|
+
schemaName: input.resultSpec.name,
|
|
620
|
+
schemaBytes: input.resultSpec.bytes,
|
|
621
|
+
attempts: structured.attempts,
|
|
622
|
+
acceptedAt: now(),
|
|
623
|
+
}
|
|
624
|
+
: null,
|
|
625
|
+
text: stats.lastAssistantText,
|
|
626
|
+
}
|
|
627
|
+
: {}),
|
|
183
628
|
turns: stats.turns,
|
|
184
629
|
durationMs: now() - startedAt,
|
|
185
630
|
usage: {
|
|
@@ -202,8 +647,24 @@ async function execute(input) {
|
|
|
202
647
|
? { code: stats.errorCode, message: stats.errorMessage }
|
|
203
648
|
: null,
|
|
204
649
|
});
|
|
650
|
+
// THE SEAL, AFTER `result` AND NEVER BEFORE. It is `result` that must be last, so
|
|
651
|
+
// the writer stays open until that line is out; closing it afterwards makes the
|
|
652
|
+
// contract an enforced one - a frame a late continuation still tries to write is
|
|
653
|
+
// dropped instead of landing behind the final line.
|
|
654
|
+
await bridge?.writer.close();
|
|
205
655
|
return exitCode;
|
|
206
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Persist one thread's session, for thread mode.
|
|
659
|
+
*
|
|
660
|
+
* A THIN WRAPPER OVER `persist()`, and deliberately not a second implementation: the file
|
|
661
|
+
* shape, the cumulative counters and the "a failed write is not a failed run" rule are one
|
|
662
|
+
* set of decisions. Threads never resume (section 1.4), so there is no previous metadata to
|
|
663
|
+
* accumulate onto; `--resume` stays a process-level, default-thread feature.
|
|
664
|
+
*/
|
|
665
|
+
export function persistThreadSession(input) {
|
|
666
|
+
persist({ id: input.sessionId, saved: null, saving: true, cwd: input.cwd }, input.controller, input.turns, input.usage, input.cli, input.at);
|
|
667
|
+
}
|
|
207
668
|
/**
|
|
208
669
|
* `text` DELEGATES TO `runHeadless` VERBATIM (D-1 / AC-1).
|
|
209
670
|
*
|
|
@@ -309,10 +770,22 @@ function failedResult(sessionId, config, message, code, durationMs) {
|
|
|
309
770
|
* Returns `null` after writing the reason to stderr, so the caller can exit 2
|
|
310
771
|
* without a second error vocabulary.
|
|
311
772
|
*/
|
|
312
|
-
function resolvePromptSource(options, promptArg, ctx, err, interrupt) {
|
|
773
|
+
function resolvePromptSource(options, promptArg, ctx, err, interrupt, bridge) {
|
|
313
774
|
const stdin = ctx.stdin ?? process.stdin;
|
|
314
|
-
const opening = [];
|
|
315
775
|
const positional = promptArg?.trim();
|
|
776
|
+
// THE GUARD RUNS BEFORE ANY FILE IS READ, and before `opening` exists.
|
|
777
|
+
//
|
|
778
|
+
// WHY IT HAS TO EXIST AT ALL: `prefixSource` turns a positional argument into a
|
|
779
|
+
// turn that runs BEFORE the first `prompt` frame, so a caller that passed both
|
|
780
|
+
// would see one prompt answered twice - and only the second would carry a
|
|
781
|
+
// `requestId`. A warning would leave that trap armed; a usage error costs one
|
|
782
|
+
// rerun. The message names both offenders so the fix is obvious.
|
|
783
|
+
if (bridge && (positional || options.promptFile)) {
|
|
784
|
+
err.write('--input-format bridge takes its prompts on stdin. ' +
|
|
785
|
+
'Drop the [prompt] argument and --prompt-file.\n');
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
const opening = [];
|
|
316
789
|
if (positional)
|
|
317
790
|
opening.push(positional);
|
|
318
791
|
else if (options.promptFile) {
|
|
@@ -322,6 +795,24 @@ function resolvePromptSource(options, promptArg, ctx, err, interrupt) {
|
|
|
322
795
|
if (text.trim().length > 0)
|
|
323
796
|
opening.push(text);
|
|
324
797
|
}
|
|
798
|
+
if (bridge) {
|
|
799
|
+
return createBridgePromptSource({
|
|
800
|
+
input: stdin,
|
|
801
|
+
server: bridge.server,
|
|
802
|
+
// A BAD FRAME IS AN `error` EVENT, NOT A STDERR LINE (this is the mode that
|
|
803
|
+
// has an event for it). Written through the writer rather than the emitter
|
|
804
|
+
// because the emitter does not exist yet - and the writer queues anything
|
|
805
|
+
// that arrives before `system/init`, which is exactly the guarantee I-11
|
|
806
|
+
// needs for a caller whose first write is already malformed.
|
|
807
|
+
onBadLine: (code, message) => bridge.writer.write({
|
|
808
|
+
type: 'error',
|
|
809
|
+
sessionId: bridge.sessionId,
|
|
810
|
+
fatal: false,
|
|
811
|
+
code,
|
|
812
|
+
message,
|
|
813
|
+
}),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
325
816
|
if (options.inputFormat === 'stream-json') {
|
|
326
817
|
return prefixSource(opening, createStdinPromptSource({
|
|
327
818
|
input: stdin,
|