mini-coder 0.5.10 → 0.5.12
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/BENCHMARK.md +408 -0
- package/PROGRESS.md +5 -0
- package/README.md +14 -1
- package/assets/mc-claude-smart.png +0 -0
- package/assets/mc-gpt-smart.png +0 -0
- package/benchmark-loop.sh +19 -0
- package/package.json +1 -1
- package/src/agent.ts +5 -1
- package/src/headless.ts +62 -21
- package/src/index.ts +35 -56
- package/src/prompt.ts +8 -0
- package/src/session-message.ts +393 -0
- package/src/session.ts +102 -396
- package/src/settings.ts +19 -15
- package/src/shared.ts +39 -0
- package/src/submit.ts +3 -25
- package/src/text.ts +71 -0
- package/src/tool-common.ts +91 -0
- package/src/tool-grep.ts +606 -0
- package/src/tool-read.ts +313 -0
- package/src/tool-shell.ts +869 -0
- package/src/tools.ts +186 -995
- package/src/ui/agent.ts +199 -110
- package/src/ui/commands.test.ts +16 -302
- package/src/ui/commands.ts +21 -47
- package/src/ui/conversation.test.ts +263 -1389
- package/src/ui/conversation.ts +496 -151
- package/src/ui/input.test.ts +1 -43
- package/src/ui/runtime.ts +69 -0
- package/src/ui.ts +196 -114
- package/src/ui/agent.test.ts +0 -49
- package/src/ui/help.test.ts +0 -65
- package/src/ui/overlay.test.ts +0 -42
- package/src/ui/render-performance.test.ts +0 -444
- package/src/ui/status.test.ts +0 -489
package/BENCHMARK.md
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Benchmark workflow
|
|
2
|
+
|
|
3
|
+
Use this as the default tuning loop for `mini-coder` on Terminal-Bench.
|
|
4
|
+
|
|
5
|
+
The goal is not to rerun the whole benchmark after every change. The goal is to get fast enough feedback that small prompt / loop / tool-use changes can be judged quickly, then promote only the promising ones to bigger runs.
|
|
6
|
+
|
|
7
|
+
Terminal-Bench is a signal, not the product. The target is a better coding agent, not a higher benchmark score from benchmark-shaped patches.
|
|
8
|
+
|
|
9
|
+
## Principles
|
|
10
|
+
|
|
11
|
+
- Keep changes small.
|
|
12
|
+
- Change one thing at a time.
|
|
13
|
+
- Compare against a fresh baseline, not an old leaderboard run.
|
|
14
|
+
- Use fast suites for iteration, broad suites for promotion.
|
|
15
|
+
- Keep structured `mc --json` logs in trial artifacts so behavior can be analyzed.
|
|
16
|
+
- Optimize for general coding-agent behavior first.
|
|
17
|
+
- Use benchmark failures to extract general behavior gaps, not to encode benchmark lore into the agent.
|
|
18
|
+
- Prefer generic improvements over task-named patches, reminders, or stop-time nudges.
|
|
19
|
+
|
|
20
|
+
## Suites
|
|
21
|
+
|
|
22
|
+
### 1. Guardrail suite
|
|
23
|
+
|
|
24
|
+
Use this to catch regressions on known-good behavior.
|
|
25
|
+
|
|
26
|
+
Tasks:
|
|
27
|
+
|
|
28
|
+
- `cancel-async-tasks`
|
|
29
|
+
- `bn-fit-modify`
|
|
30
|
+
- `sparql-university`
|
|
31
|
+
- `extract-elf`
|
|
32
|
+
|
|
33
|
+
Settings:
|
|
34
|
+
|
|
35
|
+
- `1` attempt
|
|
36
|
+
- `2` concurrent
|
|
37
|
+
- `0` retries
|
|
38
|
+
|
|
39
|
+
Estimated wall time:
|
|
40
|
+
|
|
41
|
+
- about `8–10 minutes`
|
|
42
|
+
|
|
43
|
+
### 2. Fast suite
|
|
44
|
+
|
|
45
|
+
Use this after almost every small change.
|
|
46
|
+
|
|
47
|
+
Tasks:
|
|
48
|
+
|
|
49
|
+
- `polyglot-rust-c`
|
|
50
|
+
- `overfull-hbox`
|
|
51
|
+
- `gcode-to-text`
|
|
52
|
+
- `mteb-retrieve`
|
|
53
|
+
- `dna-insert`
|
|
54
|
+
- `torch-tensor-parallelism`
|
|
55
|
+
|
|
56
|
+
Settings:
|
|
57
|
+
|
|
58
|
+
- `2` attempts
|
|
59
|
+
- `2` concurrent
|
|
60
|
+
- `0` retries
|
|
61
|
+
|
|
62
|
+
Estimated wall time:
|
|
63
|
+
|
|
64
|
+
- about `44 minutes`
|
|
65
|
+
|
|
66
|
+
### 3. Focused suite
|
|
67
|
+
|
|
68
|
+
Use this only if a change looks good on the fast suite.
|
|
69
|
+
|
|
70
|
+
Tasks:
|
|
71
|
+
|
|
72
|
+
- `polyglot-rust-c`
|
|
73
|
+
- `overfull-hbox`
|
|
74
|
+
- `gcode-to-text`
|
|
75
|
+
- `mteb-retrieve`
|
|
76
|
+
- `torch-tensor-parallelism`
|
|
77
|
+
- `filter-js-from-html`
|
|
78
|
+
- `configure-git-webserver`
|
|
79
|
+
- `qemu-alpine-ssh`
|
|
80
|
+
|
|
81
|
+
Settings:
|
|
82
|
+
|
|
83
|
+
- `2` attempts
|
|
84
|
+
- `2` concurrent
|
|
85
|
+
- `0` retries
|
|
86
|
+
|
|
87
|
+
Estimated wall time:
|
|
88
|
+
|
|
89
|
+
- about `1.1 hours`
|
|
90
|
+
|
|
91
|
+
### 4. Broad promotion suite
|
|
92
|
+
|
|
93
|
+
Use this after 1–3 promising changes, or overnight.
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
|
|
97
|
+
- rerun recent failures
|
|
98
|
+
- wider local failure suite
|
|
99
|
+
- full leaderboard-style run
|
|
100
|
+
|
|
101
|
+
Typical settings for rerunning recent failures:
|
|
102
|
+
|
|
103
|
+
- `4` attempts
|
|
104
|
+
- `2` concurrent
|
|
105
|
+
- `0` retries
|
|
106
|
+
|
|
107
|
+
## Baseline procedure
|
|
108
|
+
|
|
109
|
+
Before changing code:
|
|
110
|
+
|
|
111
|
+
1. run the guardrail suite
|
|
112
|
+
2. run the fast suite
|
|
113
|
+
3. run the fast suite again
|
|
114
|
+
|
|
115
|
+
That gives a fresh local baseline for:
|
|
116
|
+
|
|
117
|
+
- pass count
|
|
118
|
+
- runtime
|
|
119
|
+
- variance / noise
|
|
120
|
+
|
|
121
|
+
Do not compare a new change only against an old run from days ago if a fresh same-HEAD baseline is available.
|
|
122
|
+
|
|
123
|
+
## Experiment quality bar
|
|
124
|
+
|
|
125
|
+
Before changing code, write the hypothesis in two layers:
|
|
126
|
+
|
|
127
|
+
1. the benchmark symptom
|
|
128
|
+
2. the general coding-agent behavior gap behind it
|
|
129
|
+
|
|
130
|
+
Only run an experiment if you can answer all of these:
|
|
131
|
+
|
|
132
|
+
- what general behavior is being improved?
|
|
133
|
+
- why should that help outside Terminal-Bench?
|
|
134
|
+
- what would make this change obviously overfit?
|
|
135
|
+
|
|
136
|
+
Good experiment themes:
|
|
137
|
+
|
|
138
|
+
- verification-equivalence before completion
|
|
139
|
+
- artifact-grounded verification of final outputs
|
|
140
|
+
- preferring a task-named local source of truth over approximations
|
|
141
|
+
- reducing shell thrash before first meaningful verification
|
|
142
|
+
|
|
143
|
+
Reject or redesign experiments that:
|
|
144
|
+
|
|
145
|
+
- depend on benchmark-specific task names, file names, package names, or tool names in product logic
|
|
146
|
+
- inject reminders or guards keyed to one benchmark noun unless that rule maps cleanly to a real product behavior
|
|
147
|
+
- only make sense because a particular verifier is known
|
|
148
|
+
- cannot be explained without citing a single task transcript
|
|
149
|
+
|
|
150
|
+
Hard rule:
|
|
151
|
+
|
|
152
|
+
- no task-specific nouns in agent logic unless they map to a real product feature
|
|
153
|
+
|
|
154
|
+
## Iteration loop
|
|
155
|
+
|
|
156
|
+
For each change:
|
|
157
|
+
|
|
158
|
+
1. inspect the last fast / focused failures
|
|
159
|
+
2. translate them into **one** general behavior gap
|
|
160
|
+
3. reject benchmark-shaped ideas; if you cannot phrase the change without task-specific nouns, keep diagnosing
|
|
161
|
+
4. if the change depends on a dynamic trigger, confirm that the trigger actually appears in the target failures
|
|
162
|
+
5. form **one** narrow hypothesis
|
|
163
|
+
6. make **one** small change
|
|
164
|
+
7. run:
|
|
165
|
+
- guardrails
|
|
166
|
+
- fast suite
|
|
167
|
+
8. compare to baseline
|
|
168
|
+
9. decide:
|
|
169
|
+
- keep
|
|
170
|
+
- revert
|
|
171
|
+
- refine
|
|
172
|
+
|
|
173
|
+
Only run the focused suite if:
|
|
174
|
+
|
|
175
|
+
- the fast suite improved, and
|
|
176
|
+
- the guardrails did not regress
|
|
177
|
+
|
|
178
|
+
Only run the broad promotion suite if:
|
|
179
|
+
|
|
180
|
+
- the focused suite also looks good, or
|
|
181
|
+
- enough promising changes have accumulated to justify it
|
|
182
|
+
|
|
183
|
+
## Decision rules
|
|
184
|
+
|
|
185
|
+
Treat the fast suite as a noisy but useful signal.
|
|
186
|
+
|
|
187
|
+
For the 12-trial fast suite:
|
|
188
|
+
|
|
189
|
+
- `+2` or more passes: probably meaningful improvement
|
|
190
|
+
- `-2` or more passes: probably meaningful regression
|
|
191
|
+
- `±1`: likely noise unless it repeats
|
|
192
|
+
|
|
193
|
+
Keep a change if:
|
|
194
|
+
|
|
195
|
+
- the fast suite improves materially
|
|
196
|
+
- guardrails do not regress
|
|
197
|
+
- the result repeats on another fast-suite rerun
|
|
198
|
+
- the mechanism is still a general agent-quality improvement, not just a benchmark-specific patch
|
|
199
|
+
|
|
200
|
+
Revert a change if:
|
|
201
|
+
|
|
202
|
+
- it clearly loses fast-suite passes, or
|
|
203
|
+
- it regresses guardrails
|
|
204
|
+
|
|
205
|
+
Call it inconclusive if:
|
|
206
|
+
|
|
207
|
+
- the delta is tiny and does not repeat
|
|
208
|
+
|
|
209
|
+
Also prefer:
|
|
210
|
+
|
|
211
|
+
- a smaller, more general improvement over a larger but obviously benchmark-specific patch
|
|
212
|
+
- a clearly exercised mechanism over a theory that never fired in the target runs
|
|
213
|
+
|
|
214
|
+
## Failure buckets to optimize against
|
|
215
|
+
|
|
216
|
+
Use these as the main buckets when inspecting logs:
|
|
217
|
+
|
|
218
|
+
### Exact-contract / cleanup misses
|
|
219
|
+
|
|
220
|
+
Examples:
|
|
221
|
+
|
|
222
|
+
- right artifact, wrong path
|
|
223
|
+
- right output plus extra junk
|
|
224
|
+
- correct core work, but violates a strict file / in-place requirement
|
|
225
|
+
|
|
226
|
+
### Verification-equivalence / premature completion
|
|
227
|
+
|
|
228
|
+
Examples:
|
|
229
|
+
|
|
230
|
+
- waited too long to run a meaningful check
|
|
231
|
+
- never checked the exact contract
|
|
232
|
+
- stopped after a weaker non-equivalent check
|
|
233
|
+
- verified the intended design instead of the written artifact
|
|
234
|
+
|
|
235
|
+
### Source-of-truth selection gaps
|
|
236
|
+
|
|
237
|
+
Examples:
|
|
238
|
+
|
|
239
|
+
- task names an exact local tool/package/interface, but the agent uses an approximation instead
|
|
240
|
+
- lower-level library reasoning replaces package-local or task-local semantics
|
|
241
|
+
|
|
242
|
+
### Over-exploration / shell thrash
|
|
243
|
+
|
|
244
|
+
Examples:
|
|
245
|
+
|
|
246
|
+
- too many shell / read steps before first write
|
|
247
|
+
- repeated discovery commands without narrowing the problem
|
|
248
|
+
|
|
249
|
+
### Persistent correctness gaps
|
|
250
|
+
|
|
251
|
+
Examples:
|
|
252
|
+
|
|
253
|
+
- `torch-tensor-parallelism`
|
|
254
|
+
- `filter-js-from-html`
|
|
255
|
+
- `configure-git-webserver`
|
|
256
|
+
|
|
257
|
+
### Long-horizon / timeout-heavy tasks
|
|
258
|
+
|
|
259
|
+
These are useful for promotion runs, not tight inner loops.
|
|
260
|
+
Examples:
|
|
261
|
+
|
|
262
|
+
- `gpt2-codegolf`
|
|
263
|
+
- `query-optimize`
|
|
264
|
+
- `winning-avg-corewars`
|
|
265
|
+
- `train-fasttext`
|
|
266
|
+
|
|
267
|
+
## Behavior analysis requirements
|
|
268
|
+
|
|
269
|
+
Behavior analysis depends on structured agent logs.
|
|
270
|
+
|
|
271
|
+
Keep wrappers on:
|
|
272
|
+
|
|
273
|
+
- `mc --json -p ...`
|
|
274
|
+
|
|
275
|
+
Per trial, keep:
|
|
276
|
+
|
|
277
|
+
- result JSON
|
|
278
|
+
- verifier output
|
|
279
|
+
- exception type
|
|
280
|
+
- agent stderr
|
|
281
|
+
- structured `agent/mini-coder.ndjson`
|
|
282
|
+
- timestamps
|
|
283
|
+
|
|
284
|
+
With those artifacts, analyze things like:
|
|
285
|
+
|
|
286
|
+
- time to first tool call
|
|
287
|
+
- time to first edit
|
|
288
|
+
- number of `shell` / `read` / `grep` / `edit` calls
|
|
289
|
+
- whether the agent ran a verifier-like shell command
|
|
290
|
+
- whether it used the named local source of truth when one was available
|
|
291
|
+
- whether it verified the final artifact it actually wrote
|
|
292
|
+
- whether a dynamic intervention visibly exercised
|
|
293
|
+
- whether it had a local pass before ending
|
|
294
|
+
- whether it kept changing files after a pass-worthy state
|
|
295
|
+
- whether it left extra artifacts
|
|
296
|
+
|
|
297
|
+
## Commands
|
|
298
|
+
|
|
299
|
+
### Guardrail suite
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
GUARDRAIL_TASKS=(
|
|
303
|
+
cancel-async-tasks
|
|
304
|
+
bn-fit-modify
|
|
305
|
+
sparql-university
|
|
306
|
+
extract-elf
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
cmd=(
|
|
310
|
+
harbor run -y
|
|
311
|
+
--job-name local-guardrails-$(date +%F__%H-%M-%S)
|
|
312
|
+
--jobs-dir "$PWD/terminal-bench/jobs"
|
|
313
|
+
--agent-import-path mini_coder_local_agent:MiniCoderLocalAgent
|
|
314
|
+
--model openai-codex/gpt-5.4
|
|
315
|
+
--dataset terminal-bench@2.0
|
|
316
|
+
--n-attempts 1
|
|
317
|
+
--n-concurrent 2
|
|
318
|
+
--max-retries 0
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
for t in "${GUARDRAIL_TASKS[@]}"; do
|
|
322
|
+
cmd+=(--include-task-name "$t")
|
|
323
|
+
done
|
|
324
|
+
|
|
325
|
+
PYTHONPATH="$PWD/terminal-bench" "${cmd[@]}"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Fast suite
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
FAST_TASKS=(
|
|
332
|
+
polyglot-rust-c
|
|
333
|
+
overfull-hbox
|
|
334
|
+
gcode-to-text
|
|
335
|
+
mteb-retrieve
|
|
336
|
+
dna-insert
|
|
337
|
+
torch-tensor-parallelism
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
cmd=(
|
|
341
|
+
harbor run -y
|
|
342
|
+
--job-name local-fast-$(date +%F__%H-%M-%S)
|
|
343
|
+
--jobs-dir "$PWD/terminal-bench/jobs"
|
|
344
|
+
--agent-import-path mini_coder_local_agent:MiniCoderLocalAgent
|
|
345
|
+
--model openai-codex/gpt-5.4
|
|
346
|
+
--dataset terminal-bench@2.0
|
|
347
|
+
--n-attempts 2
|
|
348
|
+
--n-concurrent 2
|
|
349
|
+
--max-retries 0
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
for t in "${FAST_TASKS[@]}"; do
|
|
353
|
+
cmd+=(--include-task-name "$t")
|
|
354
|
+
done
|
|
355
|
+
|
|
356
|
+
PYTHONPATH="$PWD/terminal-bench" "${cmd[@]}"
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Focused suite
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
FOCUS_TASKS=(
|
|
363
|
+
polyglot-rust-c
|
|
364
|
+
overfull-hbox
|
|
365
|
+
gcode-to-text
|
|
366
|
+
mteb-retrieve
|
|
367
|
+
torch-tensor-parallelism
|
|
368
|
+
filter-js-from-html
|
|
369
|
+
configure-git-webserver
|
|
370
|
+
qemu-alpine-ssh
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
cmd=(
|
|
374
|
+
harbor run -y
|
|
375
|
+
--job-name local-focus-$(date +%F__%H-%M-%S)
|
|
376
|
+
--jobs-dir "$PWD/terminal-bench/jobs"
|
|
377
|
+
--agent-import-path mini_coder_local_agent:MiniCoderLocalAgent
|
|
378
|
+
--model openai-codex/gpt-5.4
|
|
379
|
+
--dataset terminal-bench@2.0
|
|
380
|
+
--n-attempts 2
|
|
381
|
+
--n-concurrent 2
|
|
382
|
+
--max-retries 0
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
for t in "${FOCUS_TASKS[@]}"; do
|
|
386
|
+
cmd+=(--include-task-name "$t")
|
|
387
|
+
done
|
|
388
|
+
|
|
389
|
+
PYTHONPATH="$PWD/terminal-bench" "${cmd[@]}"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Minimal experiment log format
|
|
393
|
+
|
|
394
|
+
Keep this in `PROGRESS.md`, a final summary for each completed change:
|
|
395
|
+
|
|
396
|
+
- benchmark symptom
|
|
397
|
+
- general behavior gap
|
|
398
|
+
- why this should help outside Terminal-Bench
|
|
399
|
+
- overfitting risk / why this is still general
|
|
400
|
+
- trigger evidence, if relevant
|
|
401
|
+
- hypothesis
|
|
402
|
+
- files changed
|
|
403
|
+
- local verification
|
|
404
|
+
- guardrail result
|
|
405
|
+
- fast-suite result
|
|
406
|
+
- focused-suite result, if run
|
|
407
|
+
- mechanism exercised?
|
|
408
|
+
- keep / revert / refine (Make the decision very visible in the file).
|
package/PROGRESS.md
ADDED
package/README.md
CHANGED
|
@@ -39,9 +39,11 @@ $ mc
|
|
|
39
39
|
|
|
40
40
|
## Tools
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Six built-in tools, plus a conditional read-only image tool:
|
|
43
43
|
|
|
44
44
|
- **`shell`** — runs commands in the user's shell. Returns stdout, stderr, and exit code. Large output is truncated to protect model context.
|
|
45
|
+
- **`read`** — reads UTF-8 text files from disk, optionally by line window.
|
|
46
|
+
- **`grep`** — searches file contents with ripgrep-style options and returns structured matches.
|
|
45
47
|
- **`edit`** — exact-text replacement in a single file. Fails deterministically if the target is missing or ambiguous. Creates new files when old text is empty.
|
|
46
48
|
- **`todoWrite`** — creates or updates the session todo list incrementally and returns the full current snapshot.
|
|
47
49
|
- **`todoRead`** — returns the full current session todo list snapshot.
|
|
@@ -123,6 +125,17 @@ bun run format
|
|
|
123
125
|
bun run typecheck
|
|
124
126
|
```
|
|
125
127
|
|
|
128
|
+
## Also makes LLMs smarter
|
|
129
|
+
|
|
130
|
+
LLMs famously tell you to walk 50 meters to the car wash — forgetting the car needs to be there too. Not on our watch.
|
|
131
|
+
|
|
132
|
+
<table align="center">
|
|
133
|
+
<tr>
|
|
134
|
+
<td><img src="assets/mc-claude-smart.png" alt="Claude correctly answering the car wash question" width="400" /></td>
|
|
135
|
+
<td><img src="assets/mc-gpt-smart.png" alt="GPT correctly answering the car wash question" width="400" /></td>
|
|
136
|
+
</tr>
|
|
137
|
+
</table>
|
|
138
|
+
|
|
126
139
|
## License
|
|
127
140
|
|
|
128
141
|
MIT
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
stop() {
|
|
5
|
+
echo "stopping loop"
|
|
6
|
+
exit 0
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
trap stop INT TERM
|
|
10
|
+
|
|
11
|
+
while :; do
|
|
12
|
+
echo ""
|
|
13
|
+
echo "> Running step"
|
|
14
|
+
echo ""
|
|
15
|
+
mc -p "See BENCHMARK.md and PROGRESS.md first. Do the next step in the process. Once the step is complete update PROGRESS.md"
|
|
16
|
+
|
|
17
|
+
sleep 60
|
|
18
|
+
done
|
|
19
|
+
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -623,6 +623,10 @@ function getTodoReminderSignature(
|
|
|
623
623
|
);
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
function wrapSystemReminder(content: string): string {
|
|
627
|
+
return `<system_reminder>\n${content}\n</system_reminder>`;
|
|
628
|
+
}
|
|
629
|
+
|
|
626
630
|
function createTodoReminderMessage(
|
|
627
631
|
messages: readonly Message[],
|
|
628
632
|
remindedTodoSignatures: Set<string>,
|
|
@@ -651,7 +655,7 @@ function createTodoReminderMessage(
|
|
|
651
655
|
|
|
652
656
|
return {
|
|
653
657
|
role: "user",
|
|
654
|
-
content: lines.join("\n"),
|
|
658
|
+
content: wrapSystemReminder(lines.join("\n")),
|
|
655
659
|
timestamp: Date.now(),
|
|
656
660
|
};
|
|
657
661
|
}
|
package/src/headless.ts
CHANGED
|
@@ -22,32 +22,64 @@ type HeadlessStopReason = "stop" | "length" | "error" | "aborted";
|
|
|
22
22
|
/** Options for a headless NDJSON run. */
|
|
23
23
|
export interface HeadlessRunOptions {
|
|
24
24
|
/** Optional line writer for completed NDJSON event output. */
|
|
25
|
-
writeLine?: (line: string) => void
|
|
25
|
+
writeLine?: (line: string) => void | Promise<void>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Options for a headless final-text run. */
|
|
29
29
|
export interface HeadlessTextRunOptions {
|
|
30
30
|
/** Optional writer for the final assistant text output. */
|
|
31
|
-
writeText?: (text: string) => void
|
|
31
|
+
writeText?: (text: string) => void | Promise<void>;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
interface HeadlessOutputController {
|
|
35
|
-
/**
|
|
35
|
+
/** Queue text for stdout with broken-pipe handling. */
|
|
36
36
|
write(text: string): void;
|
|
37
37
|
/** Attach SIGINT/stdout error handlers for the active run. */
|
|
38
38
|
attach(): void;
|
|
39
39
|
/** Remove SIGINT/stdout error handlers after the run. */
|
|
40
40
|
detach(): void;
|
|
41
|
-
/**
|
|
42
|
-
finalize(stopReason: HeadlessStopReason): HeadlessStopReason
|
|
41
|
+
/** Wait for queued writes and resolve the final stop reason. */
|
|
42
|
+
finalize(stopReason: HeadlessStopReason): Promise<HeadlessStopReason>;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
// Helpers
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
48
|
|
|
49
|
-
function defaultWrite(text: string): void {
|
|
50
|
-
|
|
49
|
+
function defaultWrite(text: string): Promise<void> {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
|
|
53
|
+
const cleanup = (): void => {
|
|
54
|
+
process.stdout.off("error", handleError);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const settle = (callback: () => void): void => {
|
|
58
|
+
if (settled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
settled = true;
|
|
62
|
+
cleanup();
|
|
63
|
+
callback();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleError = (error: unknown): void => {
|
|
67
|
+
settle(() => {
|
|
68
|
+
reject(error);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
process.stdout.on("error", handleError);
|
|
73
|
+
try {
|
|
74
|
+
process.stdout.write(text, () => {
|
|
75
|
+
settle(resolve);
|
|
76
|
+
});
|
|
77
|
+
} catch (error) {
|
|
78
|
+
settle(() => {
|
|
79
|
+
reject(error);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
51
83
|
}
|
|
52
84
|
|
|
53
85
|
function isBrokenPipeError(error: unknown): boolean {
|
|
@@ -126,10 +158,11 @@ function shouldWriteHeadlessJsonEvent(event: AgentEvent): boolean {
|
|
|
126
158
|
|
|
127
159
|
function createHeadlessOutputController(
|
|
128
160
|
state: AppState,
|
|
129
|
-
writeImpl: (text: string) => void
|
|
161
|
+
writeImpl: (text: string) => void | Promise<void>,
|
|
130
162
|
): HeadlessOutputController {
|
|
131
163
|
let brokenPipe = false;
|
|
132
164
|
let outputError: unknown = null;
|
|
165
|
+
let pendingWrite = Promise.resolve();
|
|
133
166
|
const sigintHandler = createSigintHandler(state);
|
|
134
167
|
|
|
135
168
|
const stopForBrokenPipe = (): void => {
|
|
@@ -140,30 +173,37 @@ function createHeadlessOutputController(
|
|
|
140
173
|
state.abortController?.abort();
|
|
141
174
|
};
|
|
142
175
|
|
|
143
|
-
const
|
|
176
|
+
const failOutput = (error: unknown): void => {
|
|
144
177
|
if (isBrokenPipeError(error)) {
|
|
145
178
|
stopForBrokenPipe();
|
|
146
179
|
return;
|
|
147
180
|
}
|
|
148
181
|
|
|
149
|
-
outputError = error;
|
|
182
|
+
outputError = outputError ?? error;
|
|
150
183
|
state.abortController?.abort();
|
|
151
184
|
};
|
|
152
185
|
|
|
186
|
+
const stdoutErrorHandler = (error: unknown): void => {
|
|
187
|
+
failOutput(error);
|
|
188
|
+
};
|
|
189
|
+
|
|
153
190
|
return {
|
|
154
191
|
write(text) {
|
|
155
|
-
if (brokenPipe) {
|
|
192
|
+
if (brokenPipe || outputError) {
|
|
156
193
|
return;
|
|
157
194
|
}
|
|
158
195
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (!isBrokenPipeError(error)) {
|
|
163
|
-
throw error;
|
|
196
|
+
pendingWrite = pendingWrite.then(async () => {
|
|
197
|
+
if (brokenPipe || outputError) {
|
|
198
|
+
return;
|
|
164
199
|
}
|
|
165
|
-
|
|
166
|
-
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
await writeImpl(text);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
failOutput(error);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
167
207
|
},
|
|
168
208
|
attach() {
|
|
169
209
|
process.stdout.on("error", stdoutErrorHandler);
|
|
@@ -173,7 +213,8 @@ function createHeadlessOutputController(
|
|
|
173
213
|
process.stdout.off("error", stdoutErrorHandler);
|
|
174
214
|
process.off("SIGINT", sigintHandler);
|
|
175
215
|
},
|
|
176
|
-
finalize(stopReason) {
|
|
216
|
+
async finalize(stopReason) {
|
|
217
|
+
await pendingWrite;
|
|
177
218
|
if (outputError) {
|
|
178
219
|
throw outputError;
|
|
179
220
|
}
|
|
@@ -222,7 +263,7 @@ export async function runHeadlessPrompt(
|
|
|
222
263
|
state,
|
|
223
264
|
hooks,
|
|
224
265
|
);
|
|
225
|
-
return output.finalize(stopReason);
|
|
266
|
+
return await output.finalize(stopReason);
|
|
226
267
|
} finally {
|
|
227
268
|
output.detach();
|
|
228
269
|
}
|
|
@@ -271,7 +312,7 @@ export async function runHeadlessPromptText(
|
|
|
271
312
|
if (finalText.length > 0) {
|
|
272
313
|
output.write(finalText);
|
|
273
314
|
}
|
|
274
|
-
return output.finalize(stopReason);
|
|
315
|
+
return await output.finalize(stopReason);
|
|
275
316
|
} finally {
|
|
276
317
|
output.detach();
|
|
277
318
|
}
|