@cat-factory/executor-harness 1.96.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 +5 -1
- package/dist/agent-runner.d.ts +14 -1
- package/dist/agent-runner.js +84 -28
- 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 +6 -1
- package/dist/pi.d.ts +8 -81
- package/dist/pi.js +124 -310
- package/dist/runner.d.ts +31 -0
- package/dist/runner.js +50 -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-runner.ts +100 -30
- 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 +7 -3
- package/src/pi.ts +144 -349
- package/src/runner.ts +91 -4
- package/src/structured-output.ts +2 -1
- package/src/tool-silence.ts +125 -0
package/README.md
CHANGED
|
@@ -210,9 +210,12 @@ Kimi / DeepSeek) and meters spend. The provider key never enters the container.
|
|
|
210
210
|
| File | Responsibility |
|
|
211
211
|
| ------------------ | ------------------------------------------------------------------------------------------------------- |
|
|
212
212
|
| `src/server.ts` | HTTP entry point; routes `/health`, `/run`, `/bootstrap`, `/blueprint`, `/jobs/{id}`. |
|
|
213
|
-
| `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking. |
|
|
213
|
+
| `src/runner.ts` | `JobRegistry`: async job lifecycle, idempotent on `jobId`, progress tracking, and the three per-job watchdogs (max-duration, inactivity, tool-silence). |
|
|
214
|
+
| `src/jsonl-stream.ts` | The BOUNDS on a child CLI's streams, shared by both runners: `JsonlLineReader` frames its JSONL stdout while refusing to buffer a runaway record, `BoundedTail` keeps a capped tail of raw output for failure quoting. Both watchdog timers and the poll endpoints share one event loop with this parsing, so an unbounded buffer here is how a container stops answering polls with no watchdog having fired. |
|
|
214
215
|
| `src/job.ts` | Request types + validators for the job specs. |
|
|
215
216
|
| `src/pi.ts` | Pi provider config, non-interactive run, JSON-line event + todo-progress parsing, global `AGENTS.md` guidance. |
|
|
217
|
+
| `src/pi-reduction.ts` | Reducing a Pi event stream to what the run PRODUCED (summary, stats, diagnostics, terminal failure), FOLDED as records stream rather than over a retained array — memory is O(largest record), not O(records). The array-taking entry points offline tooling uses are defined in terms of the same reducer. |
|
|
218
|
+
| `src/tool-silence.ts` | The tool-silence watchdog (F13) and the `ToolProgressWindow` an agent stream opens, beats and closes. Separate from the phase marker on purpose: a window is only meaningful while something able to reset it is running. |
|
|
216
219
|
| `src/git.ts` | clone / branch / commit / push + GitHub PR creation; bootstrap history reset + force-push. |
|
|
217
220
|
| `src/bootstrap.ts` | The `/bootstrap` handler (clone-or-empty → adapt → reinit + force-push). |
|
|
218
221
|
| `src/blueprint.ts` | The `/blueprint` handler (decompose → render `blueprints/` → commit on branch). |
|
|
@@ -240,6 +243,7 @@ runner):
|
|
|
240
243
|
| `PORT` | `8080` | HTTP port the harness listens on. |
|
|
241
244
|
| `JOB_MAX_DURATION_MS` | `3600000` (60m) | Hard ceiling on a job's wall-clock time; force-fails after. |
|
|
242
245
|
| `JOB_INACTIVITY_MS` | `600000` (10m) | Kills a hung agent that produces no output for this long. |
|
|
246
|
+
| `JOB_TOOL_SILENCE_MS` | half `JOB_MAX_DURATION_MS` (30m at its default) | Kills an agent that keeps producing output but completes no tool call for this long: the "chatty hang" neither watchdog above can see, since streamed output resets the inactivity timer on every chunk while nothing gets done (stuck-run audit F13). Armed ONLY while an agent CLI that reports completed tool calls is running (each runner opens its own window and closes it on exit), so clone / dependency install / push / a validation loop's check commands sit outside it — they are activity-silent by nature and bounded by their own per-command timeouts — and each repair pass opens a fresh window. Derived from the job ceiling rather than fixed. It fires only when output arrived during the window that elapsed, which is what leaves a genuinely quiet run to `JOB_INACTIVITY_MS` and its clearer diagnostic. `0` disables it. |
|
|
243
247
|
| `JOB_MAX_CONSECUTIVE_MCP_CALLS` | `40` | Consecutive tool-server (`mcp__*`) calls with no other tool call between before the run counts as a lookup loop. The counter-bound the no-edit exemption above owes; a per-kind `tuning.guardLimits` entry can only RAISE it. |
|
|
244
248
|
| `JOB_MAX_CONSECUTIVE_NON_ACTION_CALLS` | `200` | Consecutive calls of ANY no-edit-exempt family (reads, searches, web, tool servers, subagent dispatches) with no action call between them. The backstop above the per-family caps, since each of those resets on a call outside its own family; sized as a backstop rather than a research judgement, and reset by any `bash`/edit. |
|
|
245
249
|
| `JOB_COLD_START_MS` | `120000` (2m) | First-output window (ADR 0026 D4). A job that has produced nothing this long records a cold-start diagnostic (a likely onboarding/auth wedge) WITHOUT being killed: logged, exposed on `GET /jobs/{id}`, and folded into the failure `detail` if the job goes on to fail. `0` disables it. |
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Logger } from './logger.js';
|
|
2
|
+
import { type ToolProgressWindow } from './tool-silence.js';
|
|
2
3
|
import { type HarnessCallMetric, type PiRunOutcome, type TodoProgress, type ToolSpan } from './pi.js';
|
|
3
4
|
import { type McpServerSpec, type ObservedMcpServer, type SkillSpec } from './agent-capabilities.js';
|
|
4
5
|
import { type ProgressGuardLimits } from './progress-guard.js';
|
|
@@ -79,6 +80,18 @@ export interface SubscriptionRunOptions {
|
|
|
79
80
|
* DID dies with the container.
|
|
80
81
|
*/
|
|
81
82
|
onSpan?: (span: ToolSpan) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Opens this stream's tool-silence window (see `RunOptions.beginToolWindow`), closed when the
|
|
85
|
+
* CLI exits. Both subscription CLIs report tool activity — claude-code on the `tool_result`
|
|
86
|
+
* turn that answers each call, codex on its tool/command/exec events — so a window either
|
|
87
|
+
* opens is one the run can beat. It is deliberately NOT tied to {@link onSpan}: the trajectory
|
|
88
|
+
* is an observability opt-in, and the codex stream produces none at all while still doing tool
|
|
89
|
+
* work, which a span-keyed window would have read as a run making no progress.
|
|
90
|
+
*
|
|
91
|
+
* A caller with no tool loop (the inline one-shot completion) passes nothing; see the note at
|
|
92
|
+
* `handleInline`.
|
|
93
|
+
*/
|
|
94
|
+
beginToolWindow?: () => ToolProgressWindow;
|
|
82
95
|
/**
|
|
83
96
|
* Called with the FULL set of per-slice reviews each time one lands, so the backend can persist
|
|
84
97
|
* a parallel review's completed work as it happens instead of only from the terminal result.
|
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
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
|
|
@@ -448,6 +454,23 @@ function createClaudeToolTrajectory(opts, secrets) {
|
|
|
448
454
|
onToolResults: (content) => recordClaudeToolResults(tracker, content, (call) => onSpan({ ...call, bodies: 'stored' })),
|
|
449
455
|
};
|
|
450
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
|
+
}
|
|
451
474
|
export async function runClaudeCode(opts) {
|
|
452
475
|
const stats = { toolCalls: 0, assistantChars: 0 };
|
|
453
476
|
let summary = '';
|
|
@@ -524,6 +547,9 @@ export async function runClaudeCode(opts) {
|
|
|
524
547
|
const progressGuard = createClaudeProgressGuard(opts);
|
|
525
548
|
const { rememberTool, feedGuard, guardAbort } = progressGuard;
|
|
526
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;
|
|
527
553
|
const onEvent = (event, meta) => {
|
|
528
554
|
const type = event.type;
|
|
529
555
|
reportToolServerStartup(event, opts.onToolServers);
|
|
@@ -563,6 +589,8 @@ export async function runClaudeCode(opts) {
|
|
|
563
589
|
// tool_result blocks the harness fed back to the model — part of the next prompt.
|
|
564
590
|
const content = event.message.content;
|
|
565
591
|
if (Array.isArray(content)) {
|
|
592
|
+
if (carriesToolResult(content))
|
|
593
|
+
toolWindow.toolCompleted();
|
|
566
594
|
sliceTracker.onUser(content);
|
|
567
595
|
planTracker.onUser(content);
|
|
568
596
|
emitProgress();
|
|
@@ -610,6 +638,9 @@ export async function runClaudeCode(opts) {
|
|
|
610
638
|
const runSignal = opts.signal
|
|
611
639
|
? AbortSignal.any([opts.signal, guardAbort.signal])
|
|
612
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);
|
|
613
644
|
try {
|
|
614
645
|
const { stderrTail } = await streamCli({
|
|
615
646
|
command: 'claude',
|
|
@@ -673,6 +704,7 @@ export async function runClaudeCode(opts) {
|
|
|
673
704
|
throw withAgentReport(err, terminalReport, secrets);
|
|
674
705
|
}
|
|
675
706
|
finally {
|
|
707
|
+
toolWindow.close();
|
|
676
708
|
await subagents?.stop();
|
|
677
709
|
await home.dispose();
|
|
678
710
|
}
|
|
@@ -825,6 +857,27 @@ function claudeUsage(raw) {
|
|
|
825
857
|
// ---------------------------------------------------------------------------
|
|
826
858
|
// Codex
|
|
827
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
|
+
}
|
|
828
881
|
/**
|
|
829
882
|
* Run the Codex CLI headlessly against `opts.cwd`, authenticated with the leased
|
|
830
883
|
* ChatGPT `auth.json` bundle written to an isolated CODEX_HOME, talking direct to
|
|
@@ -881,6 +934,9 @@ export async function runCodex(opts) {
|
|
|
881
934
|
// context into the prompt itself (Claude Code instead rides --append-system-prompt,
|
|
882
935
|
// falling back to this same fold when the prompt overflows argv).
|
|
883
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;
|
|
884
940
|
// Codex's `exec --json` is far thinner than Claude Code's stream: it surfaces only
|
|
885
941
|
// flat assistant text and (on `token_count` events) the per-turn `last_token_usage`
|
|
886
942
|
// plus a cumulative total. It never exposes the request transcript or structured
|
|
@@ -892,17 +948,15 @@ export async function runCodex(opts) {
|
|
|
892
948
|
let pendingText = '';
|
|
893
949
|
const onEvent = (event) => {
|
|
894
950
|
const type = typeof event.type === 'string' ? event.type : '';
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
summary = text;
|
|
901
|
-
pendingText = text;
|
|
902
|
-
}
|
|
951
|
+
const text = codexAssistantText(event, type);
|
|
952
|
+
if (text) {
|
|
953
|
+
stats.assistantChars += text.length;
|
|
954
|
+
summary = text;
|
|
955
|
+
pendingText = text;
|
|
903
956
|
}
|
|
904
|
-
if (
|
|
957
|
+
if (isCodexToolActivity(type)) {
|
|
905
958
|
stats.toolCalls += 1;
|
|
959
|
+
toolWindow.toolCompleted();
|
|
906
960
|
}
|
|
907
961
|
const progress = codexPlanProgress(event);
|
|
908
962
|
if (progress && opts.onProgress)
|
|
@@ -931,6 +985,7 @@ export async function runCodex(opts) {
|
|
|
931
985
|
pendingText = '';
|
|
932
986
|
}
|
|
933
987
|
};
|
|
988
|
+
toolWindow = openToolWindow(opts);
|
|
934
989
|
try {
|
|
935
990
|
const { stderrTail } = await streamCli({
|
|
936
991
|
command: 'codex',
|
|
@@ -985,6 +1040,7 @@ export async function runCodex(opts) {
|
|
|
985
1040
|
throw withAgentReport(err, summary, secrets);
|
|
986
1041
|
}
|
|
987
1042
|
finally {
|
|
1043
|
+
toolWindow.close();
|
|
988
1044
|
if (codexHome) {
|
|
989
1045
|
// Lift the CLI session transcripts (`sessions/`) out for short-lived retention BEFORE the
|
|
990
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
|
+
}
|