@cat-factory/executor-harness 1.50.12 → 1.50.16
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/dist/agent-runner.js +30 -29
- package/dist/agent.js +31 -15
- package/dist/coding-agent.js +15 -2
- package/dist/effort.js +84 -0
- package/dist/pi-workspace.js +14 -2
- package/dist/subagents.js +73 -18
- package/package.json +3 -3
- package/src/agent-runner.ts +29 -31
- package/src/agent.ts +121 -82
- package/src/coding-agent.ts +38 -21
- package/src/effort.ts +99 -0
- package/src/job.ts +7 -0
- package/src/pi-workspace.ts +15 -2
- package/src/pi.ts +7 -0
- package/src/subagents.ts +88 -24
package/src/job.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HarnessCallMetric, PiRunStats } from './pi.js'
|
|
2
2
|
import type { HarnessKind } from './pi-workspace.js'
|
|
3
3
|
import type { FailureCause } from './failure.js'
|
|
4
|
+
import type { EffortReport } from './effort.js'
|
|
4
5
|
|
|
5
6
|
// The job the Worker's ContainerAgentExecutor POSTs to /run. Kept as plain
|
|
6
7
|
// types with a hand-rolled validator so the image needs no schema dependency.
|
|
@@ -921,6 +922,12 @@ export interface AgentResult {
|
|
|
921
922
|
* {@link HarnessCallMetric}.
|
|
922
923
|
*/
|
|
923
924
|
callMetrics?: HarnessCallMetric[]
|
|
925
|
+
/**
|
|
926
|
+
* The agent's effort self-assessment (how hard the work was, what reduced its effectiveness,
|
|
927
|
+
* the key obstacles), lifted from its sentinel file after the run. The backend forwards it onto
|
|
928
|
+
* the job result and records it on the step for run details. Absent when the agent wrote none.
|
|
929
|
+
*/
|
|
930
|
+
effortReport?: EffortReport
|
|
924
931
|
}
|
|
925
932
|
|
|
926
933
|
/** Parse the coding-mode bootstrap spec, or undefined when absent. Validates the target. */
|
package/src/pi-workspace.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdir, mkdtemp, rm } from 'node:fs/promises'
|
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import type { RepoSpec, SkillSpec } from './job.js'
|
|
5
|
+
import { readEffortReport } from './effort.js'
|
|
5
6
|
import { log } from './logger.js'
|
|
6
7
|
import {
|
|
7
8
|
type ContextFileInfo,
|
|
@@ -258,7 +259,7 @@ export async function runAgentInWorkspace(
|
|
|
258
259
|
if (!spec.ambientAuth && !spec.subscriptionToken) {
|
|
259
260
|
throw new Error(`The ${spec.harness} harness requires a subscription token`)
|
|
260
261
|
}
|
|
261
|
-
|
|
262
|
+
const subOutcome = await runSubscriptionHarness(spec.harness, {
|
|
262
263
|
cwd: spec.dir,
|
|
263
264
|
model: spec.model,
|
|
264
265
|
systemPrompt: subscriptionSystemPrompt(spec.systemPrompt, contextFiles),
|
|
@@ -272,6 +273,7 @@ export async function runAgentInWorkspace(
|
|
|
272
273
|
onProgress: opts.onProgress,
|
|
273
274
|
...(opts.log ? { log: opts.log } : {}),
|
|
274
275
|
})
|
|
276
|
+
return withEffortReport(spec.dir, subOutcome)
|
|
275
277
|
}
|
|
276
278
|
if (!spec.proxyBaseUrl || !spec.sessionToken) {
|
|
277
279
|
throw new Error('The Pi harness requires proxyBaseUrl and sessionToken')
|
|
@@ -301,7 +303,7 @@ export async function runAgentInWorkspace(
|
|
|
301
303
|
})
|
|
302
304
|
await writePiModelsConfig({ model: spec.model, proxyBaseUrl })
|
|
303
305
|
const { signal, onActivity, onProgress, onSpan } = opts
|
|
304
|
-
|
|
306
|
+
const piOutcome = await runPi({
|
|
305
307
|
cwd: spec.dir,
|
|
306
308
|
model: spec.model,
|
|
307
309
|
userPrompt: spec.userPrompt,
|
|
@@ -316,6 +318,17 @@ export async function runAgentInWorkspace(
|
|
|
316
318
|
guardLimits: mergeGuardLimits(progressGuardLimitsFromEnv(), spec.guardLimits),
|
|
317
319
|
extraEnv,
|
|
318
320
|
})
|
|
321
|
+
return withEffortReport(spec.dir, piOutcome)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Lift the agent's effort self-assessment off its sentinel file in `dir` and fold it onto the
|
|
326
|
+
* run outcome. Shared by both harness paths so EVERY container agent's effort report is captured
|
|
327
|
+
* in one place. Never throws (a bad/absent report just yields no `effortReport`).
|
|
328
|
+
*/
|
|
329
|
+
async function withEffortReport(dir: string, outcome: PiRunOutcome): Promise<PiRunOutcome> {
|
|
330
|
+
const effortReport = await readEffortReport(dir)
|
|
331
|
+
return effortReport ? { ...outcome, effortReport } : outcome
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
/**
|
package/src/pi.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { pathExists } from './fs-utils.js'
|
|
|
7
7
|
import { redactSecrets } from './redact.js'
|
|
8
8
|
import { HarnessFailure } from './failure.js'
|
|
9
9
|
import { log } from './logger.js'
|
|
10
|
+
import type { EffortReport } from './effort.js'
|
|
10
11
|
|
|
11
12
|
// Drives the Pi coding-agent CLI. Pi is pointed at the Worker's OpenAI-compatible
|
|
12
13
|
// proxy via a custom provider in ~/.pi/agent/models.json, authenticated with the
|
|
@@ -534,6 +535,12 @@ export interface PiRunOutcome {
|
|
|
534
535
|
callMetrics?: HarnessCallMetric[]
|
|
535
536
|
/** Output-quality signals (truncation / empty final answer); see {@link RunDiagnostics}. */
|
|
536
537
|
diagnostics?: RunDiagnostics
|
|
538
|
+
/**
|
|
539
|
+
* The agent's effort self-assessment, lifted from its sentinel file after the run (how hard the
|
|
540
|
+
* work was, what reduced its effectiveness, the key obstacles). Absent when the agent wrote none.
|
|
541
|
+
* See {@link EffortReport}.
|
|
542
|
+
*/
|
|
543
|
+
effortReport?: EffortReport
|
|
537
544
|
}
|
|
538
545
|
|
|
539
546
|
/**
|
package/src/subagents.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readdir, stat } from 'node:fs/promises'
|
|
2
|
-
import { createReadStream } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
2
|
+
import { createReadStream, type Dirent } from 'node:fs'
|
|
3
|
+
import { basename, join } from 'node:path'
|
|
4
4
|
import { claudeAssistantContent, claudeCallUsage, isObject, redactBody } from './claude-stream.js'
|
|
5
5
|
import type { Logger } from './logger.js'
|
|
6
6
|
import type { HarnessCallMetric, TodoProgress } from './pi.js'
|
|
7
7
|
|
|
8
|
-
// ADR 0026 D2.1 + D3. When the Claude Code CLI reviews a large PR
|
|
9
|
-
// out across parallel `Task` subagents. Two things then go dark to the
|
|
10
|
-
// only reads the PARENT process's stream-json stdout:
|
|
8
|
+
// ADR 0026 D2.1 + D3, corrected by ADR 0027. When the Claude Code CLI reviews a large PR
|
|
9
|
+
// it fans the work out across parallel `Task` subagents. Two things then go dark to the
|
|
10
|
+
// harness, which only reads the PARENT process's stream-json stdout:
|
|
11
11
|
//
|
|
12
12
|
// - the parent stream falls quiet for the whole (potentially 15+ minute) parallel
|
|
13
13
|
// review, so the inactivity heartbeat freezes and a healthy run looks wedged (P3);
|
|
@@ -21,11 +21,19 @@ import type { HarnessCallMetric, TodoProgress } from './pi.js'
|
|
|
21
21
|
// - {@link createSliceTracker} derives the slice plan + per-slice progress from the
|
|
22
22
|
// PARENT stream alone — the `Task` tool_use dispatch and its terminal tool_result
|
|
23
23
|
// DO appear there (only the subagent's intermediate turns don't), so slices/progress
|
|
24
|
-
// need no file watching (D2.1)
|
|
24
|
+
// need no file watching (D2.1). {@link pickProgress} reconciles it with any parent
|
|
25
|
+
// TodoWrite plan (ADR 0027 Defect B);
|
|
25
26
|
// - {@link startSubagentWatcher} tails the `subagents/*.jsonl` transcripts for the
|
|
26
27
|
// heartbeat (any new bytes ⇒ `onActivity`) and sums each subagent turn's usage into
|
|
27
28
|
// the run's telemetry (D3).
|
|
28
29
|
//
|
|
30
|
+
// The CLI does NOT write those transcripts to `<configHome>/subagents` (the location ADR
|
|
31
|
+
// 0026 assumed, which never exists — ADR 0027 Defect A). It writes them PER SESSION under
|
|
32
|
+
// `<configHome>/projects/<encoded-cwd>/<session-uuid>/subagents/agent-*.jsonl`, and the
|
|
33
|
+
// session-uuid dir isn't known before the CLI mints it — so the watcher is pointed at the
|
|
34
|
+
// `projects` root and DISCOVERS the `subagents/` dir by walking (see
|
|
35
|
+
// {@link findSubagentTranscripts}).
|
|
36
|
+
//
|
|
29
37
|
// Both degrade gracefully: the CLI's subagent transcript layout is not a stable contract,
|
|
30
38
|
// so a missing directory, an unreadable file, or an unparseable line is swallowed and the
|
|
31
39
|
// harness falls back to today's parent-stream-only behaviour.
|
|
@@ -52,8 +60,10 @@ export interface SliceTracker {
|
|
|
52
60
|
hasSlices(): boolean
|
|
53
61
|
/**
|
|
54
62
|
* Progress derived from the dispatched subagents (completed / in-flight / total),
|
|
55
|
-
* or undefined when none have been dispatched.
|
|
56
|
-
*
|
|
63
|
+
* or undefined when none have been dispatched. Reconciled with any parent TodoWrite
|
|
64
|
+
* plan by {@link pickProgress} — it is NOT gated off by the presence of a todo plan
|
|
65
|
+
* (that gate was ADR 0027 Defect B: the pr-reviewer prompt writes the plan ONCE at
|
|
66
|
+
* grouping time and never marks it done, which used to permanently mask this signal).
|
|
57
67
|
*/
|
|
58
68
|
progress(): TodoProgress | undefined
|
|
59
69
|
}
|
|
@@ -106,6 +116,31 @@ export function createSliceTracker(): SliceTracker {
|
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Reconcile the two redundant views of the same slice work into the one to surface
|
|
121
|
+
* (ADR 0027 Defect B). A pr-reviewer run has BOTH a parent `TodoWrite` plan (the slices,
|
|
122
|
+
* written once at grouping time) and the {@link SliceTracker}'s `Task`-dispatch view. The
|
|
123
|
+
* sequential shape advances the todo plan; the parallel-subagent shape advances ONLY the
|
|
124
|
+
* slice tracker (the CLI writes the plan once and never marks it done, while the parallel
|
|
125
|
+
* `Task`s report in-flight/complete). Neither alone covers both shapes, and gating the
|
|
126
|
+
* slice tracker OFF whenever a todo plan exists (the old behaviour) pinned parallel runs
|
|
127
|
+
* at 0%. So prefer whichever view is further along: more `completed`, then more
|
|
128
|
+
* `inProgress` (an all-pending todo plan must not beat live in-flight slices), then more
|
|
129
|
+
* `total` (the richer view — the todo plan can carry an extra "aggregate" entry), else the
|
|
130
|
+
* todo plan. Pure + total; returns whichever single input is present when only one is.
|
|
131
|
+
*/
|
|
132
|
+
export function pickProgress(
|
|
133
|
+
todo: TodoProgress | undefined,
|
|
134
|
+
slice: TodoProgress | undefined,
|
|
135
|
+
): TodoProgress | undefined {
|
|
136
|
+
if (!todo) return slice
|
|
137
|
+
if (!slice) return todo
|
|
138
|
+
if (slice.completed !== todo.completed) return slice.completed > todo.completed ? slice : todo
|
|
139
|
+
if (slice.inProgress !== todo.inProgress) return slice.inProgress > todo.inProgress ? slice : todo
|
|
140
|
+
if (slice.total !== todo.total) return slice.total > todo.total ? slice : todo
|
|
141
|
+
return todo
|
|
142
|
+
}
|
|
143
|
+
|
|
109
144
|
// ---------------------------------------------------------------------------
|
|
110
145
|
// Subagent transcript watcher (heartbeat + usage) (D3)
|
|
111
146
|
// ---------------------------------------------------------------------------
|
|
@@ -135,15 +170,51 @@ export interface SubagentWatcher {
|
|
|
135
170
|
}
|
|
136
171
|
|
|
137
172
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
173
|
+
* Recursively collect every `*.jsonl` file that lives inside a `subagents/` directory
|
|
174
|
+
* anywhere under `root` (the CLI's `<configHome>/projects` tree). The Claude CLI writes
|
|
175
|
+
* each parallel `Task` subagent's transcript to
|
|
176
|
+
* `<configHome>/projects/<encoded-cwd>/<session-uuid>/subagents/agent-*.jsonl`; the
|
|
177
|
+
* session-uuid dir isn't known before the CLI mints it, so we DISCOVER the `subagents/`
|
|
178
|
+
* dir by walking rather than guessing its path (ADR 0027 Defect A). Files NOT under a
|
|
179
|
+
* `subagents/` dir — critically the parent's own `<session-uuid>.jsonl` session transcript,
|
|
180
|
+
* whose per-turn usage the terminal `result` event already totals — are deliberately
|
|
181
|
+
* excluded: reading them would double-count the parent. `root` itself counts as inside a
|
|
182
|
+
* `subagents/` dir when its own basename is `subagents` (so passing the leaf dir works too).
|
|
183
|
+
* Best-effort: an unreadable directory is skipped, never thrown.
|
|
184
|
+
*/
|
|
185
|
+
async function findSubagentTranscripts(root: string): Promise<string[]> {
|
|
186
|
+
const out: string[] = []
|
|
187
|
+
const walk = async (dir: string, inSubagents: boolean): Promise<void> => {
|
|
188
|
+
let entries: Dirent[]
|
|
189
|
+
try {
|
|
190
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
191
|
+
} catch {
|
|
192
|
+
return // dir not created yet (or vanished) — try again next tick
|
|
193
|
+
}
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const full = join(dir, entry.name)
|
|
196
|
+
if (entry.isDirectory()) {
|
|
197
|
+
await walk(full, inSubagents || entry.name === 'subagents')
|
|
198
|
+
} else if (inSubagents && entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
199
|
+
out.push(full)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
await walk(root, basename(root) === 'subagents')
|
|
204
|
+
return out
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Start watching `root` (the CLI's `<configHome>/projects` tree) for subagent `*.jsonl`
|
|
209
|
+
* transcripts — any file under a `subagents/` directory beneath it (see
|
|
210
|
+
* {@link findSubagentTranscripts}) — tailing each file by byte offset. New content feeds
|
|
211
|
+
* `onActivity` (heartbeat) and each assistant turn carrying usage is lifted into a
|
|
212
|
+
* {@link HarnessCallMetric} + summed into the cumulative usage. Best-effort throughout: the
|
|
213
|
+
* tree may not exist yet (created lazily by the CLI), a file may be mid-write, and the
|
|
214
|
+
* line/usage shape may change across CLI versions — every such case is swallowed so the
|
|
215
|
+
* watcher can only ever ADD signal, never break the run.
|
|
145
216
|
*/
|
|
146
|
-
export function startSubagentWatcher(
|
|
217
|
+
export function startSubagentWatcher(root: string, opts: SubagentWatcherOptions): SubagentWatcher {
|
|
147
218
|
const secrets = opts.secrets ?? []
|
|
148
219
|
const offsets = new Map<string, number>()
|
|
149
220
|
const calls: HarnessCallMetric[] = []
|
|
@@ -225,15 +296,8 @@ export function startSubagentWatcher(dir: string, opts: SubagentWatcherOptions):
|
|
|
225
296
|
if (polling) return
|
|
226
297
|
polling = true
|
|
227
298
|
try {
|
|
228
|
-
let entries: string[]
|
|
229
|
-
try {
|
|
230
|
-
entries = (await readdir(dir)).filter((n) => n.endsWith('.jsonl'))
|
|
231
|
-
} catch {
|
|
232
|
-
return // dir not created yet (or vanished) — try again next tick
|
|
233
|
-
}
|
|
234
299
|
let grew = false
|
|
235
|
-
for (const
|
|
236
|
-
const path = join(dir, name)
|
|
300
|
+
for (const path of await findSubagentTranscripts(root)) {
|
|
237
301
|
let size: number
|
|
238
302
|
try {
|
|
239
303
|
size = (await stat(path)).size
|