@cat-factory/executor-harness 1.94.0 → 1.98.0
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/README.md +16 -12
- package/dist/agent-capabilities.d.ts +61 -0
- package/dist/agent-capabilities.js +113 -0
- package/dist/agent-runner.d.ts +30 -2
- package/dist/agent-runner.js +146 -47
- package/dist/bootstrap-mode.js +1 -0
- package/dist/coding-agent.d.ts +2 -1
- package/dist/embed.d.ts +2 -1
- package/dist/embed.js +2 -1
- package/dist/failure.d.ts +19 -1
- package/dist/failure.js +40 -0
- package/dist/git.d.ts +6 -0
- package/dist/git.js +16 -9
- package/dist/inline.d.ts +6 -0
- package/dist/inline.js +6 -0
- package/dist/job.d.ts +2 -1
- package/dist/jsonl-stream.d.ts +70 -0
- package/dist/jsonl-stream.js +149 -0
- package/dist/pi-reduction.d.ts +136 -0
- package/dist/pi-reduction.js +303 -0
- package/dist/pi-workspace.d.ts +2 -1
- package/dist/pi-workspace.js +11 -1
- package/dist/pi.d.ts +8 -81
- package/dist/pi.js +124 -310
- package/dist/runner.d.ts +53 -0
- package/dist/runner.js +53 -3
- package/dist/structured-output.js +2 -1
- package/dist/tool-silence.d.ts +74 -0
- package/dist/tool-silence.js +99 -0
- package/package.json +4 -4
- package/src/agent-capabilities.ts +163 -0
- package/src/agent-runner.ts +185 -47
- package/src/agent.ts +1 -1
- package/src/bootstrap-mode.ts +2 -1
- package/src/coding-agent.ts +2 -1
- package/src/embed.ts +8 -5
- package/src/failure.ts +36 -9
- package/src/git.ts +17 -9
- package/src/inline.ts +6 -0
- package/src/job.ts +2 -1
- package/src/jsonl-stream.ts +149 -0
- package/src/pi-reduction.ts +359 -0
- package/src/pi-workspace.ts +12 -3
- package/src/pi.ts +144 -349
- package/src/runner.ts +116 -4
- package/src/structured-output.ts +2 -1
- package/src/tool-silence.ts +125 -0
package/dist/agent-runner.js
CHANGED
|
@@ -5,9 +5,12 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { claudeAssistantContent, isObject, numberOf, redactBody } from './claude-stream.js';
|
|
6
6
|
import { createClaudeRunTelemetry, subagentDispatchId } from './claude-call-aggregator.js';
|
|
7
7
|
import { ToolCallTracker, recordClaudeToolResults, } from './tool-trajectory.js';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
import { NO_TOOL_WINDOW } from './tool-silence.js';
|
|
8
10
|
import { createCallMetricPublisher, publishCallMetric, } from './pi.js';
|
|
9
|
-
import { claudeAllowedToolPatterns, codexMcpConfigToml, mcpServerSecretValues, writeClaudeMcpConfig, } from './agent-capabilities.js';
|
|
11
|
+
import { claudeAllowedToolPatterns, codexMcpConfigToml, mcpServerSecretValues, observeClaudeMcpInit, writeClaudeMcpConfig, } from './agent-capabilities.js';
|
|
10
12
|
import { ProgressGuard } from './progress-guard.js';
|
|
13
|
+
import { BoundedTail, JsonlLineReader } from './jsonl-stream.js';
|
|
11
14
|
import { killChildProcess, spawnDetached } from './process.js';
|
|
12
15
|
import { describeProcessExit } from './process-exit.js';
|
|
13
16
|
import { redact, registerKnownSecrets, secretsToRedact } from './redact.js';
|
|
@@ -59,9 +62,10 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
59
62
|
});
|
|
60
63
|
child.stdin.on('error', () => { });
|
|
61
64
|
child.stdin.end(prompt);
|
|
62
|
-
|
|
65
|
+
// 8 KB is well over the 700 B tail anyone quotes below, and the CLI's stderr is diagnostic
|
|
66
|
+
// noise rather than a product, so a bounded tail is all this ever needed to be.
|
|
67
|
+
const stderr = new BoundedTail(8_000);
|
|
63
68
|
let aborted = false;
|
|
64
|
-
let lineBuffer = '';
|
|
65
69
|
const killChild = () => killChildProcess(child);
|
|
66
70
|
// `final` marks the at-close flush of a trailing unterminated line: the CLI has already
|
|
67
71
|
// exited, so an observer must not act on that record in a way that KILLS the run (mirrors
|
|
@@ -85,16 +89,9 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
85
89
|
// A faulty observer must never break the run.
|
|
86
90
|
}
|
|
87
91
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
while (nl !== -1) {
|
|
92
|
-
const line = lineBuffer.slice(0, nl).trim();
|
|
93
|
-
lineBuffer = lineBuffer.slice(nl + 1);
|
|
94
|
-
nl = lineBuffer.indexOf('\n');
|
|
95
|
-
processLine(line);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
92
|
+
// Bounded framing, shared with `runPi`: an unterminated record must not be able to grow
|
|
93
|
+
// until parsing it stalls the loop the watchdogs and poll handlers run on (audit F6).
|
|
94
|
+
const reader = new JsonlLineReader(processLine);
|
|
98
95
|
const onAbort = () => {
|
|
99
96
|
aborted = true;
|
|
100
97
|
killChild();
|
|
@@ -102,13 +99,11 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
102
99
|
opts.signal?.addEventListener('abort', onAbort, { once: true });
|
|
103
100
|
child.stdout.on('data', (chunk) => {
|
|
104
101
|
opts.onActivity?.();
|
|
105
|
-
|
|
102
|
+
reader.push(chunk.toString());
|
|
106
103
|
});
|
|
107
104
|
child.stderr.on('data', (chunk) => {
|
|
108
105
|
opts.onActivity?.();
|
|
109
|
-
stderr
|
|
110
|
-
if (stderr.length > 8_000)
|
|
111
|
-
stderr = stderr.slice(-8_000);
|
|
106
|
+
stderr.push(chunk.toString());
|
|
112
107
|
});
|
|
113
108
|
child.on('error', (err) => {
|
|
114
109
|
opts.signal?.removeEventListener('abort', onAbort);
|
|
@@ -116,9 +111,20 @@ function streamCli(cli, prompt, opts, env, secrets, onEvent) {
|
|
|
116
111
|
});
|
|
117
112
|
child.on('close', (code, signal) => {
|
|
118
113
|
opts.signal?.removeEventListener('abort', onAbort);
|
|
119
|
-
const stderrTail = redact(stderr, secrets).slice(-700);
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
const stderrTail = redact(stderr.toString(), secrets).slice(-700);
|
|
115
|
+
reader.flush();
|
|
116
|
+
// Surface an oversized record the reader refused to buffer ONCE (a count, not per line),
|
|
117
|
+
// for the same reason `runPi` does: a dropped record costs this run its progress, its
|
|
118
|
+
// trajectory and its per-call telemetry for that turn, and a silent loss reads exactly
|
|
119
|
+
// like a CLI that never emitted it. Falls back to the module logger so the report cannot
|
|
120
|
+
// depend on a caller having wired a per-job one.
|
|
121
|
+
if (reader.droppedLines > 0) {
|
|
122
|
+
;
|
|
123
|
+
(opts.log ?? log).warn('agent CLI: skipped oversized JSONL records', {
|
|
124
|
+
command,
|
|
125
|
+
oversizedLines: reader.droppedLines,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
122
128
|
if (aborted) {
|
|
123
129
|
// Carry the tail on the rejection so a caller that REPLACES this generic message with a
|
|
124
130
|
// more specific cause (the no-progress guard's diagnostic) can still append it — the
|
|
@@ -327,6 +333,60 @@ async function setUpClaudeMcp(servers, configHome) {
|
|
|
327
333
|
cleanup,
|
|
328
334
|
};
|
|
329
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* The LIVE publishers of a claude-code run: everything the stream has revealed so far that the
|
|
338
|
+
* backend should see before the run ends, rather than only in its terminal result.
|
|
339
|
+
*
|
|
340
|
+
* They are grouped because they share one rule and differ on everything else. The rule: each
|
|
341
|
+
* publishes a WHOLE current value (never a delta), so a dropped poll response costs nothing and
|
|
342
|
+
* the caller may fire them as often as it likes. What differs is what is at stake — progress is a
|
|
343
|
+
* disposable count the UI renders, while the slice reviews carry the slices' actual review WORK
|
|
344
|
+
* and are the only thing a resume of a wedged review can be rebuilt from, which is why they are
|
|
345
|
+
* published on the turn a slice lands rather than on the next progress tick.
|
|
346
|
+
*
|
|
347
|
+
* `lastTodo` is a GETTER because the event handler assigns it as the stream goes; taking the value
|
|
348
|
+
* would freeze the plan at construction time.
|
|
349
|
+
*
|
|
350
|
+
* Split out of {@link runClaudeCode} for the per-function line budget.
|
|
351
|
+
*/
|
|
352
|
+
function createClaudeLivePublishers(deps) {
|
|
353
|
+
const { opts, planTracker, sliceTracker } = deps;
|
|
354
|
+
return {
|
|
355
|
+
emitProgress: () => {
|
|
356
|
+
if (!opts.onProgress)
|
|
357
|
+
return;
|
|
358
|
+
const progress = mergeProgress(pickProgress(deps.lastTodo(), planTracker.progress()), sliceTracker.progress());
|
|
359
|
+
if (progress)
|
|
360
|
+
opts.onProgress(progress);
|
|
361
|
+
},
|
|
362
|
+
emitSliceReviews: () => {
|
|
363
|
+
if (!opts.onSliceReviews)
|
|
364
|
+
return;
|
|
365
|
+
const reviews = sliceTracker.sliceReviews();
|
|
366
|
+
if (reviews.length > 0)
|
|
367
|
+
opts.onSliceReviews(reviews);
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Publish the CLI's own startup report about the tool servers it loaded — the OBSERVED half of the
|
|
373
|
+
* run's tool-server record.
|
|
374
|
+
*
|
|
375
|
+
* Handed every event because it is the one thing `runClaudeCode` reads that is neither a turn nor
|
|
376
|
+
* a result: it arrives once, ahead of the first model call, and says whether the servers the
|
|
377
|
+
* backend wired actually came up. {@link observeClaudeMcpInit} answers `undefined` for every other
|
|
378
|
+
* event and for a run that wired none, so a server-less run reports nothing and the caller's
|
|
379
|
+
* record stays honestly absent rather than empty.
|
|
380
|
+
*
|
|
381
|
+
* Split out of {@link runClaudeCode} for the per-function line budget.
|
|
382
|
+
*/
|
|
383
|
+
function reportToolServerStartup(event, onToolServers) {
|
|
384
|
+
if (!onToolServers)
|
|
385
|
+
return;
|
|
386
|
+
const observed = observeClaudeMcpInit(event);
|
|
387
|
+
if (observed)
|
|
388
|
+
onToolServers(observed);
|
|
389
|
+
}
|
|
330
390
|
/**
|
|
331
391
|
* No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
332
392
|
* which cannot see the CLI's internal turns. The caller remembers each `tool_use` id's name off
|
|
@@ -394,6 +454,23 @@ function createClaudeToolTrajectory(opts, secrets) {
|
|
|
394
454
|
onToolResults: (content) => recordClaudeToolResults(tracker, content, (call) => onSpan({ ...call, bodies: 'stored' })),
|
|
395
455
|
};
|
|
396
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Open this run's tool-silence window, or the inert one when the caller wired no watchdog. One
|
|
459
|
+
* definition so both runners resolve "is there a watchdog?" identically, and so neither carries
|
|
460
|
+
* the optional-call noise at the point where it should simply have a window.
|
|
461
|
+
*/
|
|
462
|
+
function openToolWindow(opts) {
|
|
463
|
+
return opts.beginToolWindow ? opts.beginToolWindow() : NO_TOOL_WINDOW;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Whether a claude-code `user` turn carries a `tool_result` block, i.e. whether a tool call just
|
|
467
|
+
* COMPLETED — the progress the tool-silence watchdog measures. Tested explicitly rather than
|
|
468
|
+
* taken from "the model sent a user turn", which a plain follow-up prompt also is: a watchdog
|
|
469
|
+
* reset handed out for work that did nothing is the same as no watchdog.
|
|
470
|
+
*/
|
|
471
|
+
function carriesToolResult(content) {
|
|
472
|
+
return content.some((block) => isObject(block) && block.type === 'tool_result');
|
|
473
|
+
}
|
|
397
474
|
export async function runClaudeCode(opts) {
|
|
398
475
|
const stats = { toolCalls: 0, assistantChars: 0 };
|
|
399
476
|
let summary = '';
|
|
@@ -455,24 +532,12 @@ export async function runClaudeCode(opts) {
|
|
|
455
532
|
const sliceTracker = createSliceTracker(secrets);
|
|
456
533
|
const planTracker = createTaskPlanTracker();
|
|
457
534
|
let lastTodo;
|
|
458
|
-
const emitProgress = (
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
};
|
|
465
|
-
// Publish the per-slice reviews the tracker has captured. Separate from `emitProgress` because
|
|
466
|
-
// the two answer different questions and have different lifetimes: progress is a disposable
|
|
467
|
-
// count the UI renders, while these carry the slices' actual review WORK and are persisted so a
|
|
468
|
-
// run that dies before its aggregation can be resumed from them.
|
|
469
|
-
const emitSliceReviews = () => {
|
|
470
|
-
if (!opts.onSliceReviews)
|
|
471
|
-
return;
|
|
472
|
-
const reviews = sliceTracker.sliceReviews();
|
|
473
|
-
if (reviews.length > 0)
|
|
474
|
-
opts.onSliceReviews(reviews);
|
|
475
|
-
};
|
|
535
|
+
const { emitProgress, emitSliceReviews } = createClaudeLivePublishers({
|
|
536
|
+
opts,
|
|
537
|
+
planTracker,
|
|
538
|
+
sliceTracker,
|
|
539
|
+
lastTodo: () => lastTodo,
|
|
540
|
+
});
|
|
476
541
|
// No-progress guard on the CLI's own tool stream — the claude-code analogue of runPi's guard,
|
|
477
542
|
// absent on this path until now. Claude Code reports a tool CALL (its name) on the `assistant`
|
|
478
543
|
// turn and that call's RESULT (`is_error`) on the following `user` turn, so correlate them by
|
|
@@ -482,8 +547,12 @@ export async function runClaudeCode(opts) {
|
|
|
482
547
|
const progressGuard = createClaudeProgressGuard(opts);
|
|
483
548
|
const { rememberTool, feedGuard, guardAbort } = progressGuard;
|
|
484
549
|
const trajectory = createClaudeToolTrajectory(opts, secrets);
|
|
550
|
+
// This stream's tool-silence window; opened just before the CLI starts and closed in the
|
|
551
|
+
// `finally` below, so it can only ever be armed while the CLI it watches is running.
|
|
552
|
+
let toolWindow = NO_TOOL_WINDOW;
|
|
485
553
|
const onEvent = (event, meta) => {
|
|
486
554
|
const type = event.type;
|
|
555
|
+
reportToolServerStartup(event, opts.onToolServers);
|
|
487
556
|
// A subagent's turns ride the parent's stdout tagged with the dispatch that spawned them;
|
|
488
557
|
// `telemetry` routes them off the parent's chain (and decides who bills them). Progress, slice
|
|
489
558
|
// tracking, the guard and `stats` below deliberately see EVERY event: a subagent grinding on
|
|
@@ -520,6 +589,8 @@ export async function runClaudeCode(opts) {
|
|
|
520
589
|
// tool_result blocks the harness fed back to the model — part of the next prompt.
|
|
521
590
|
const content = event.message.content;
|
|
522
591
|
if (Array.isArray(content)) {
|
|
592
|
+
if (carriesToolResult(content))
|
|
593
|
+
toolWindow.toolCompleted();
|
|
523
594
|
sliceTracker.onUser(content);
|
|
524
595
|
planTracker.onUser(content);
|
|
525
596
|
emitProgress();
|
|
@@ -567,6 +638,9 @@ export async function runClaudeCode(opts) {
|
|
|
567
638
|
const runSignal = opts.signal
|
|
568
639
|
? AbortSignal.any([opts.signal, guardAbort.signal])
|
|
569
640
|
: guardAbort.signal;
|
|
641
|
+
// Opened around the CLI itself, not around this function: everything above is per-run setup
|
|
642
|
+
// (the config home, the skills, the MCP config) which completes no tool calls by nature.
|
|
643
|
+
toolWindow = openToolWindow(opts);
|
|
570
644
|
try {
|
|
571
645
|
const { stderrTail } = await streamCli({
|
|
572
646
|
command: 'claude',
|
|
@@ -630,6 +704,7 @@ export async function runClaudeCode(opts) {
|
|
|
630
704
|
throw withAgentReport(err, terminalReport, secrets);
|
|
631
705
|
}
|
|
632
706
|
finally {
|
|
707
|
+
toolWindow.close();
|
|
633
708
|
await subagents?.stop();
|
|
634
709
|
await home.dispose();
|
|
635
710
|
}
|
|
@@ -782,6 +857,27 @@ function claudeUsage(raw) {
|
|
|
782
857
|
// ---------------------------------------------------------------------------
|
|
783
858
|
// Codex
|
|
784
859
|
// ---------------------------------------------------------------------------
|
|
860
|
+
/**
|
|
861
|
+
* The assistant text a codex event carries, or `''`. Two shapes because the CLI changed its
|
|
862
|
+
* stream between versions and the harness serves both: the flat `agent_message*` events and the
|
|
863
|
+
* newer `item.completed` envelope around a message item.
|
|
864
|
+
*/
|
|
865
|
+
function codexAssistantText(event, type) {
|
|
866
|
+
const isMessage = type.includes('agent_message') || (type === 'item.completed' && isCodexMessageItem(event));
|
|
867
|
+
return (isMessage ? extractText(event) : '') ?? '';
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Whether a codex event reports tool activity — a substring test because the CLI names these
|
|
871
|
+
* events differently across versions (`exec_command_end`, `item.*` around a command execution,
|
|
872
|
+
* `tool_*`) and the harness cares only that SOMETHING ran.
|
|
873
|
+
*
|
|
874
|
+
* This is also the tool-silence watchdog's only signal on this stream. Codex exposes no
|
|
875
|
+
* structured tool bodies, so `runCodex` produces no `ToolSpan` at all, and a window keyed on the
|
|
876
|
+
* trajectory would have force-failed every codex pass that outran it while the run was working.
|
|
877
|
+
*/
|
|
878
|
+
function isCodexToolActivity(type) {
|
|
879
|
+
return type.includes('tool') || type.includes('command') || type.includes('exec');
|
|
880
|
+
}
|
|
785
881
|
/**
|
|
786
882
|
* Run the Codex CLI headlessly against `opts.cwd`, authenticated with the leased
|
|
787
883
|
* ChatGPT `auth.json` bundle written to an isolated CODEX_HOME, talking direct to
|
|
@@ -838,6 +934,9 @@ export async function runCodex(opts) {
|
|
|
838
934
|
// context into the prompt itself (Claude Code instead rides --append-system-prompt,
|
|
839
935
|
// falling back to this same fold when the prompt overflows argv).
|
|
840
936
|
const prompt = foldSystemPrompt(opts.systemPrompt, opts.userPrompt);
|
|
937
|
+
// This stream's tool-silence window (see the claude runner for the shape); opened just before
|
|
938
|
+
// the CLI starts and closed in the `finally` below.
|
|
939
|
+
let toolWindow = NO_TOOL_WINDOW;
|
|
841
940
|
// Codex's `exec --json` is far thinner than Claude Code's stream: it surfaces only
|
|
842
941
|
// flat assistant text and (on `token_count` events) the per-turn `last_token_usage`
|
|
843
942
|
// plus a cumulative total. It never exposes the request transcript or structured
|
|
@@ -849,17 +948,15 @@ export async function runCodex(opts) {
|
|
|
849
948
|
let pendingText = '';
|
|
850
949
|
const onEvent = (event) => {
|
|
851
950
|
const type = typeof event.type === 'string' ? event.type : '';
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
summary = text;
|
|
858
|
-
pendingText = text;
|
|
859
|
-
}
|
|
951
|
+
const text = codexAssistantText(event, type);
|
|
952
|
+
if (text) {
|
|
953
|
+
stats.assistantChars += text.length;
|
|
954
|
+
summary = text;
|
|
955
|
+
pendingText = text;
|
|
860
956
|
}
|
|
861
|
-
if (
|
|
957
|
+
if (isCodexToolActivity(type)) {
|
|
862
958
|
stats.toolCalls += 1;
|
|
959
|
+
toolWindow.toolCompleted();
|
|
863
960
|
}
|
|
864
961
|
const progress = codexPlanProgress(event);
|
|
865
962
|
if (progress && opts.onProgress)
|
|
@@ -888,6 +985,7 @@ export async function runCodex(opts) {
|
|
|
888
985
|
pendingText = '';
|
|
889
986
|
}
|
|
890
987
|
};
|
|
988
|
+
toolWindow = openToolWindow(opts);
|
|
891
989
|
try {
|
|
892
990
|
const { stderrTail } = await streamCli({
|
|
893
991
|
command: 'codex',
|
|
@@ -942,6 +1040,7 @@ export async function runCodex(opts) {
|
|
|
942
1040
|
throw withAgentReport(err, summary, secrets);
|
|
943
1041
|
}
|
|
944
1042
|
finally {
|
|
1043
|
+
toolWindow.close();
|
|
945
1044
|
if (codexHome) {
|
|
946
1045
|
// Lift the CLI session transcripts (`sessions/`) out for short-lived retention BEFORE the
|
|
947
1046
|
// home is deleted — the credential (`auth.json`) lives at the home root, never in
|
package/dist/bootstrap-mode.js
CHANGED
package/dist/coding-agent.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentJob, AgentResult, HarnessAuthFields, RepoSpec, SkillSpec, McpServerSpec } from './job.js';
|
|
2
|
-
import type { HarnessCallMetric
|
|
2
|
+
import type { HarnessCallMetric } from './pi.js';
|
|
3
|
+
import type { PiRunStats } from './pi-reduction.js';
|
|
3
4
|
import { type EffortReport } from './effort.js';
|
|
4
5
|
import { type AgentPrDescription } from './pr-description.js';
|
|
5
6
|
import type { ProgressGuardLimits } from './progress-guard.js';
|
package/dist/embed.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { writePiModelsConfig, writeAgentsContext, runPi, parseTodoProgress, type PiRunOutcome, type TodoItem, type TodoProgress, } from './pi.js';
|
|
2
|
+
export { PI_MAX_OUTPUT_TOKENS, parsePiOutput, summarizePiRun, terminalRunError, type PiRunReduction, type PiRunStats, } from './pi-reduction.js';
|
|
2
3
|
export { DEFAULT_PROGRESS_GUARD_LIMITS, progressGuardLimitsFromEnv, type ProgressGuardLimits, } from './progress-guard.js';
|
|
3
4
|
export { cloneRepo, createBranch, changedPathsFromPorcelain, hasAgentChanges, redactSecrets, } from './git.js';
|
|
4
5
|
export type { RepoSpec } from './job.js';
|
package/dist/embed.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// repo, write the agent context, point Pi at an OpenAI-compatible endpoint, run
|
|
5
5
|
// it, and inspect what changed. The HTTP server / job lifecycle stays internal;
|
|
6
6
|
// only the reusable primitives are exposed here.
|
|
7
|
-
export {
|
|
7
|
+
export { writePiModelsConfig, writeAgentsContext, runPi, parseTodoProgress, } from './pi.js';
|
|
8
|
+
export { PI_MAX_OUTPUT_TOKENS, parsePiOutput, summarizePiRun, terminalRunError, } from './pi-reduction.js';
|
|
8
9
|
export { DEFAULT_PROGRESS_GUARD_LIMITS, progressGuardLimitsFromEnv, } from './progress-guard.js';
|
|
9
10
|
export { cloneRepo, createBranch, changedPathsFromPorcelain, hasAgentChanges, redactSecrets, } from './git.js';
|
package/dist/failure.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* - `inactivity-timeout` — the inactivity watchdog fired (no agent output for the window).
|
|
7
7
|
* - `max-duration` — the overall wall-clock cap fired.
|
|
8
|
+
* - `no-tool-progress` — the tool-silence watchdog fired: the agent kept TALKING but completed
|
|
9
|
+
* no tool call for the window. Distinct from `inactivity-timeout` on
|
|
10
|
+
* purpose, because the two need different fixes: one says the container
|
|
11
|
+
* went quiet, this one says the model rabbit-holed while streaming.
|
|
8
12
|
* - `agent` — the agent ran but produced an unusable/failed result, or threw.
|
|
9
13
|
* - `git` — a git operation failed (clone/push/merge/PR).
|
|
10
14
|
* - `api` — an upstream API call failed (e.g. the GitHub/GitLab PR/MR REST call).
|
|
@@ -13,7 +17,14 @@
|
|
|
13
17
|
* - `no-usable-output` — the agent finished but returned no usable report / structured output.
|
|
14
18
|
* - `no-changes` — a coding agent finished without producing any change to push.
|
|
15
19
|
*/
|
|
16
|
-
export
|
|
20
|
+
export declare const FAILURE_CAUSES: readonly ['inactivity-timeout', 'max-duration', 'no-tool-progress', 'agent', 'git', 'api', 'llm-upstream', 'no-usable-output', 'no-changes'];
|
|
21
|
+
/**
|
|
22
|
+
* See {@link FAILURE_CAUSES}. Derived from the array rather than declared beside it so the two
|
|
23
|
+
* cannot disagree, and so the list is ENUMERABLE at runtime — which is what lets
|
|
24
|
+
* `failure-cause.conformity.test.ts` check this image's vocabulary against the kernel union that
|
|
25
|
+
* has to classify it (the two are kept in step by hand; the image can carry no workspace dep).
|
|
26
|
+
*/
|
|
27
|
+
export type FailureCause = (typeof FAILURE_CAUSES)[number];
|
|
17
28
|
/**
|
|
18
29
|
* A thrown failure that carries a structured {@link FailureCause}, so a `git` / `api`
|
|
19
30
|
* operation that fails deep in a helper surfaces its real cause instead of being flattened
|
|
@@ -40,3 +51,10 @@ export declare function inactivityAbortMessage(inactivityMs: number): string;
|
|
|
40
51
|
* in error-message coverage I5), so it is free to change.
|
|
41
52
|
*/
|
|
42
53
|
export declare function maxDurationAbortMessage(maxDurationMs: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* The tool-silence-watchdog abort message. Human-readable only, like its two siblings — the
|
|
56
|
+
* backend reads the structured `no-tool-progress` {@link FailureCause}. Says what it observed
|
|
57
|
+
* (output, but no completed tool call) rather than "hung": the run was demonstrably alive, which
|
|
58
|
+
* is exactly why the inactivity watchdog never fired.
|
|
59
|
+
*/
|
|
60
|
+
export declare function toolSilenceAbortMessage(toolSilenceMs: number): string;
|
package/dist/failure.js
CHANGED
|
@@ -12,6 +12,36 @@
|
|
|
12
12
|
// the facade-owned eviction sentinel `(container evicted or crashed)`, which
|
|
13
13
|
// `job.logic.isContainerEvictionError` still matches for a DISPATCH-time throw that carries no job
|
|
14
14
|
// view — and which the harness must keep NOT emitting for a non-eviction failure.
|
|
15
|
+
/**
|
|
16
|
+
* The structured reason a harness job failed, surfaced on the job view's `failureCause`.
|
|
17
|
+
* Covers only HARNESS-owned failures — container eviction is detected by the runtime facade
|
|
18
|
+
* (a vanished container → `(container evicted or crashed)`), never set here.
|
|
19
|
+
*
|
|
20
|
+
* - `inactivity-timeout` — the inactivity watchdog fired (no agent output for the window).
|
|
21
|
+
* - `max-duration` — the overall wall-clock cap fired.
|
|
22
|
+
* - `no-tool-progress` — the tool-silence watchdog fired: the agent kept TALKING but completed
|
|
23
|
+
* no tool call for the window. Distinct from `inactivity-timeout` on
|
|
24
|
+
* purpose, because the two need different fixes: one says the container
|
|
25
|
+
* went quiet, this one says the model rabbit-holed while streaming.
|
|
26
|
+
* - `agent` — the agent ran but produced an unusable/failed result, or threw.
|
|
27
|
+
* - `git` — a git operation failed (clone/push/merge/PR).
|
|
28
|
+
* - `api` — an upstream API call failed (e.g. the GitHub/GitLab PR/MR REST call).
|
|
29
|
+
* - `llm-upstream` — the model provider rejected every call (auth/quota/rate-limit) and Pi
|
|
30
|
+
* exhausted its retries, so the run never produced a result.
|
|
31
|
+
* - `no-usable-output` — the agent finished but returned no usable report / structured output.
|
|
32
|
+
* - `no-changes` — a coding agent finished without producing any change to push.
|
|
33
|
+
*/
|
|
34
|
+
export const FAILURE_CAUSES = [
|
|
35
|
+
'inactivity-timeout',
|
|
36
|
+
'max-duration',
|
|
37
|
+
'no-tool-progress',
|
|
38
|
+
'agent',
|
|
39
|
+
'git',
|
|
40
|
+
'api',
|
|
41
|
+
'llm-upstream',
|
|
42
|
+
'no-usable-output',
|
|
43
|
+
'no-changes',
|
|
44
|
+
];
|
|
15
45
|
/**
|
|
16
46
|
* A thrown failure that carries a structured {@link FailureCause}, so a `git` / `api`
|
|
17
47
|
* operation that fails deep in a helper surfaces its real cause instead of being flattened
|
|
@@ -48,3 +78,13 @@ export function inactivityAbortMessage(inactivityMs) {
|
|
|
48
78
|
export function maxDurationAbortMessage(maxDurationMs) {
|
|
49
79
|
return `Aborted: exceeded max duration of ${Math.round(maxDurationMs / 1000)}s`;
|
|
50
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* The tool-silence-watchdog abort message. Human-readable only, like its two siblings — the
|
|
83
|
+
* backend reads the structured `no-tool-progress` {@link FailureCause}. Says what it observed
|
|
84
|
+
* (output, but no completed tool call) rather than "hung": the run was demonstrably alive, which
|
|
85
|
+
* is exactly why the inactivity watchdog never fired.
|
|
86
|
+
*/
|
|
87
|
+
export function toolSilenceAbortMessage(toolSilenceMs) {
|
|
88
|
+
return (`Aborted: the agent produced output but completed no tool call for ` +
|
|
89
|
+
`${Math.round(toolSilenceMs / 1000)}s`);
|
|
90
|
+
}
|
package/dist/git.d.ts
CHANGED
|
@@ -385,10 +385,16 @@ export declare function pushBranch(dir: string, branch: string, ghToken: string,
|
|
|
385
385
|
* .gitignore and/or license picked on the new-repo page), so a fast-forward is
|
|
386
386
|
* impossible. The Worker pre-flights that the target is empty or holds only that
|
|
387
387
|
* boilerplate, so overwriting it is safe and intended.
|
|
388
|
+
*
|
|
389
|
+
* `signal` is the job watchdog's, and threading it is load-bearing rather than tidy: without
|
|
390
|
+
* it the six commands below are bounded only by their own per-command timeouts, so an abort
|
|
391
|
+
* raised during the push phase cannot interrupt them and the job keeps working for up to
|
|
392
|
+
* ~6 × `GIT_TIMEOUT_MS` past its max-duration kill. Every other git helper here threads it.
|
|
388
393
|
*/
|
|
389
394
|
export declare function reinitAndPush(opts: {
|
|
390
395
|
dir: string;
|
|
391
396
|
target: BootstrapTargetSpec;
|
|
392
397
|
ghToken: string;
|
|
393
398
|
message: string;
|
|
399
|
+
signal?: AbortSignal;
|
|
394
400
|
}): Promise<void>;
|
package/dist/git.js
CHANGED
|
@@ -872,20 +872,27 @@ export async function pushBranch(dir, branch, ghToken, signal) {
|
|
|
872
872
|
* .gitignore and/or license picked on the new-repo page), so a fast-forward is
|
|
873
873
|
* impossible. The Worker pre-flights that the target is empty or holds only that
|
|
874
874
|
* boilerplate, so overwriting it is safe and intended.
|
|
875
|
+
*
|
|
876
|
+
* `signal` is the job watchdog's, and threading it is load-bearing rather than tidy: without
|
|
877
|
+
* it the six commands below are bounded only by their own per-command timeouts, so an abort
|
|
878
|
+
* raised during the push phase cannot interrupt them and the job keeps working for up to
|
|
879
|
+
* ~6 × `GIT_TIMEOUT_MS` past its max-duration kill. Every other git helper here threads it.
|
|
875
880
|
*/
|
|
876
881
|
export async function reinitAndPush(opts) {
|
|
877
|
-
|
|
878
|
-
await
|
|
882
|
+
const { dir, signal } = opts;
|
|
883
|
+
await rm(join(dir, '.git'), { recursive: true, force: true });
|
|
884
|
+
await git(['init'], { cwd: dir, signal });
|
|
879
885
|
// Start the history on the target's default branch (init may default to master).
|
|
880
|
-
await git(['checkout', '-b', opts.target.defaultBranch], { cwd:
|
|
881
|
-
await git(['config', 'user.name', GIT_AUTHOR], { cwd:
|
|
882
|
-
await git(['config', 'user.email', GIT_EMAIL], { cwd:
|
|
883
|
-
await git(['add', '-A'], { cwd:
|
|
884
|
-
await git(['commit', '-m', opts.message], { cwd:
|
|
886
|
+
await git(['checkout', '-b', opts.target.defaultBranch], { cwd: dir, signal });
|
|
887
|
+
await git(['config', 'user.name', GIT_AUTHOR], { cwd: dir, signal });
|
|
888
|
+
await git(['config', 'user.email', GIT_EMAIL], { cwd: dir, signal });
|
|
889
|
+
await git(['add', '-A'], { cwd: dir, signal });
|
|
890
|
+
await git(['commit', '-m', opts.message], { cwd: dir, signal });
|
|
885
891
|
const url = authenticatedCloneUrl(opts.target.cloneUrl);
|
|
886
|
-
await git(['remote', 'add', 'origin', url], { cwd:
|
|
892
|
+
await git(['remote', 'add', 'origin', url], { cwd: dir, signal });
|
|
887
893
|
await git(['push', '--force', '-u', 'origin', opts.target.defaultBranch], {
|
|
888
|
-
cwd:
|
|
894
|
+
cwd: dir,
|
|
895
|
+
signal,
|
|
889
896
|
env: await authEnv(opts.ghToken),
|
|
890
897
|
});
|
|
891
898
|
}
|
package/dist/inline.d.ts
CHANGED
|
@@ -6,5 +6,11 @@ import type { RunOptions } from './runner.js';
|
|
|
6
6
|
* directory. The job's watchdog (inactivity + max-duration, see {@link JobRegistry}) bounds
|
|
7
7
|
* it through `opts.signal`; `opts.onActivity` keeps the inactivity timer alive while the CLI
|
|
8
8
|
* streams. The temp cwd is always removed.
|
|
9
|
+
*
|
|
10
|
+
* `opts.beginToolWindow` is deliberately NOT forwarded. An inline completion is a one-shot
|
|
11
|
+
* answer with no checkout and no tool loop, so the tool-silence watchdog would arm a window this
|
|
12
|
+
* run could never beat and could only ever expire — force-failing a healthy completion under a
|
|
13
|
+
* cause ("kept talking, completed no tool call") that misdescribes what it is. The inactivity and
|
|
14
|
+
* max-duration watchdogs still bound it, which is the whole bound this work ever had.
|
|
9
15
|
*/
|
|
10
16
|
export declare function handleInline(job: InlineJob, opts: RunOptions): Promise<InlineResult>;
|
package/dist/inline.js
CHANGED
|
@@ -29,6 +29,12 @@ function deriveFinishReason(calls) {
|
|
|
29
29
|
* directory. The job's watchdog (inactivity + max-duration, see {@link JobRegistry}) bounds
|
|
30
30
|
* it through `opts.signal`; `opts.onActivity` keeps the inactivity timer alive while the CLI
|
|
31
31
|
* streams. The temp cwd is always removed.
|
|
32
|
+
*
|
|
33
|
+
* `opts.beginToolWindow` is deliberately NOT forwarded. An inline completion is a one-shot
|
|
34
|
+
* answer with no checkout and no tool loop, so the tool-silence watchdog would arm a window this
|
|
35
|
+
* run could never beat and could only ever expire — force-failing a healthy completion under a
|
|
36
|
+
* cause ("kept talking, completed no tool call") that misdescribes what it is. The inactivity and
|
|
37
|
+
* max-duration watchdogs still bound it, which is the whole bound this work ever had.
|
|
32
38
|
*/
|
|
33
39
|
export async function handleInline(job, opts) {
|
|
34
40
|
opts.onPhase?.('agent');
|
package/dist/job.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { HarnessCallMetric
|
|
1
|
+
import type { HarnessCallMetric } from './pi.js';
|
|
2
|
+
import type { PiRunStats } from './pi-reduction.js';
|
|
2
3
|
import type { HarnessKind } from './pi-workspace.js';
|
|
3
4
|
import type { FailureCause } from './failure.js';
|
|
4
5
|
import type { EffortReport } from './effort.js';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Longest single JSONL record either CLI may emit before the reader stops buffering it.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately far above the largest LEGITIMATE record — the terminal `agent_end`, which carries
|
|
5
|
+
* the run's whole message transcript including tool results — because dropping that one costs the
|
|
6
|
+
* run its summary and stats. The cap is not a size policy, it is the ceiling that keeps a
|
|
7
|
+
* runaway producer from growing the buffer until parsing it wedges the event loop, so it only
|
|
8
|
+
* has to be low enough that one parse of it stays well inside the poll cadence.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_JSONL_LINE_CHARS: number;
|
|
11
|
+
/**
|
|
12
|
+
* A fixed-size tail of a text stream, for output kept ONLY to quote back on a failure.
|
|
13
|
+
*
|
|
14
|
+
* Retaining a whole run's stdout to slice the last 2 KB off it at close is the memory half of
|
|
15
|
+
* F6: a chatty agent's output is unbounded, and the container OOMing is another way for a job to
|
|
16
|
+
* stop answering polls with no watchdog having fired. The tail is trimmed lazily — only once it
|
|
17
|
+
* has grown past twice the bound — so a run that streams thousands of chunks pays an amortized
|
|
18
|
+
* O(1) copy per chunk rather than an O(maxChars) slice on every one of them.
|
|
19
|
+
*/
|
|
20
|
+
export declare class BoundedTail {
|
|
21
|
+
private readonly maxChars;
|
|
22
|
+
private text;
|
|
23
|
+
private total;
|
|
24
|
+
constructor(maxChars: number);
|
|
25
|
+
push(chunk: string): void;
|
|
26
|
+
/** The last `maxChars` characters seen. */
|
|
27
|
+
toString(): string;
|
|
28
|
+
/** Everything ever pushed, whether or not it is still retained. */
|
|
29
|
+
get totalChars(): number;
|
|
30
|
+
/**
|
|
31
|
+
* Characters dropped off the FRONT because the tail is bounded; 0 while everything still fits.
|
|
32
|
+
*
|
|
33
|
+
* A caller that renders the tail to a human owes them this: a bounded tail is the opposite of a
|
|
34
|
+
* prefix, so a reader who assumes one concludes the producer stopped where the text begins.
|
|
35
|
+
* Diagnostic quotes (a stderr tail) need no such note — being a tail is what they are for.
|
|
36
|
+
*/
|
|
37
|
+
get droppedChars(): number;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Frames a child's LF-delimited JSONL stdout into complete records, bounding what it will buffer
|
|
41
|
+
* for any one of them.
|
|
42
|
+
*
|
|
43
|
+
* `onLine` is invoked per complete record with `final: false`, and once more from {@link flush}
|
|
44
|
+
* with `final: true` for a trailing record that arrived without its newline (a clean exit can
|
|
45
|
+
* leave the last event unterminated). `final` is what lets a caller deliver the record's
|
|
46
|
+
* progress/telemetry signal while suppressing any decision that would KILL the run: the process
|
|
47
|
+
* has already exited, so a guard tripping on that last buffered record would turn a clean exit
|
|
48
|
+
* into a spurious failure.
|
|
49
|
+
*
|
|
50
|
+
* A record that outgrows {@link MAX_JSONL_LINE_CHARS} is DROPPED, not truncated: a partial JSON
|
|
51
|
+
* document is not a record, and handing the parser half of one would report it as corrupt output
|
|
52
|
+
* rather than as the bound firing. The reader then resynchronises on the next newline, so the
|
|
53
|
+
* oversized record costs its own signal and nothing after it. Callers report {@link droppedLines}
|
|
54
|
+
* at close (never per line) so the loss is diagnosable instead of silent.
|
|
55
|
+
*/
|
|
56
|
+
export declare class JsonlLineReader {
|
|
57
|
+
private readonly onLine;
|
|
58
|
+
private readonly maxLineChars;
|
|
59
|
+
private buffer;
|
|
60
|
+
/** True while discarding the tail of a record that already blew the cap. */
|
|
61
|
+
private skipping;
|
|
62
|
+
private dropped;
|
|
63
|
+
constructor(onLine: (line: string, final: boolean) => void, maxLineChars?: number);
|
|
64
|
+
/** Feed one stdout chunk, emitting every complete record it finishes. */
|
|
65
|
+
push(text: string): void;
|
|
66
|
+
/** Emit any trailing unterminated record (see the class doc); call once, after the child exits. */
|
|
67
|
+
flush(): void;
|
|
68
|
+
/** Records dropped for exceeding the line cap; 0 on every ordinary run. */
|
|
69
|
+
get droppedLines(): number;
|
|
70
|
+
}
|