@cat-factory/executor-harness 1.50.14 → 1.50.18
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 +55 -47
- package/dist/agent.js +46 -22
- package/dist/coding-agent.js +15 -2
- package/dist/effort.js +84 -0
- package/dist/job.js +42 -24
- package/dist/pi-workspace.js +14 -2
- package/package.json +3 -3
- package/src/agent-runner.ts +67 -48
- package/src/agent.ts +171 -123
- package/src/coding-agent.ts +38 -21
- package/src/effort.ts +99 -0
- package/src/job.ts +50 -23
- package/src/pi-workspace.ts +15 -2
- package/src/pi.ts +7 -0
package/src/coding-agent.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from './git.js'
|
|
32
32
|
import { FOLLOW_UPS_FILENAME, FollowUpTailer } from './follow-ups.js'
|
|
33
33
|
import type { HarnessCallMetric, PiRunStats } from './pi.js'
|
|
34
|
+
import { EFFORT_REPORT_FILE, type EffortReport } from './effort.js'
|
|
34
35
|
import {
|
|
35
36
|
acquireRepoCheckout,
|
|
36
37
|
agentNeverActed,
|
|
@@ -123,6 +124,8 @@ export interface CodingAgentOutcome {
|
|
|
123
124
|
usage?: { inputTokens: number; outputTokens: number }
|
|
124
125
|
/** Per-model-call telemetry from a subscription harness's CLI stream (absent for Pi). */
|
|
125
126
|
callMetrics?: HarnessCallMetric[]
|
|
127
|
+
/** The agent's effort self-assessment, lifted from its sentinel file (absent when it wrote none). */
|
|
128
|
+
effortReport?: EffortReport
|
|
126
129
|
/**
|
|
127
130
|
* Ralph loop: the verdict of the post-commit validation command (whether it exited 0, the
|
|
128
131
|
* exit code, and a bounded/redacted output tail). Present only when {@link CodingAgentSpec.validation}
|
|
@@ -242,6 +245,14 @@ export async function runCodingAgent(
|
|
|
242
245
|
const workDir = serviceDirectory ? join(dir, serviceDirectory) : dir
|
|
243
246
|
if (serviceDirectory) await mkdir(workDir, { recursive: true })
|
|
244
247
|
|
|
248
|
+
// Every container agent is asked to write its effort self-assessment to `.cat-effort.json`
|
|
249
|
+
// in its cwd (the backend appends EFFORT_REPORT_GUIDANCE to every container prompt). Locally
|
|
250
|
+
// exclude it from git — exactly like the follow-ups sentinel below — so the agent's own
|
|
251
|
+
// `git add` can never stage it into the PR. `readEffortReport` also removes it after the run,
|
|
252
|
+
// but that cannot un-stage a mid-run commit; the per-clone exclude is what prevents it. A bare
|
|
253
|
+
// filename pattern matches the file in any subdirectory, so it covers a monorepo `workDir` too.
|
|
254
|
+
await excludeFromGit(dir, EFFORT_REPORT_FILE, signal)
|
|
255
|
+
|
|
245
256
|
// Follow-up companion: tail the Coder's sentinel file and stream new items out on the
|
|
246
257
|
// job view. Locally exclude it from git first so the agent's own `git add` can never
|
|
247
258
|
// stage it and it never surfaces as an untracked leftover or in the PR. The sentinel
|
|
@@ -462,7 +473,7 @@ async function finalizeCodingRun(args: {
|
|
|
462
473
|
agentRun,
|
|
463
474
|
} = args
|
|
464
475
|
const { signal } = opts
|
|
465
|
-
const { summary, stats, stderrTail, usage, callMetrics } = agentRun
|
|
476
|
+
const { summary, stats, stderrTail, usage, callMetrics, effortReport } = agentRun
|
|
466
477
|
let outcome: CodingAgentOutcome
|
|
467
478
|
|
|
468
479
|
// Stop tailing the follow-up sentinel and flush any items written after the last
|
|
@@ -521,6 +532,7 @@ async function finalizeCodingRun(args: {
|
|
|
521
532
|
...(stderrTail ? { stderrTail } : {}),
|
|
522
533
|
...(usage ? { usage } : {}),
|
|
523
534
|
...(callMetrics ? { callMetrics } : {}),
|
|
535
|
+
...(effortReport ? { effortReport } : {}),
|
|
524
536
|
}
|
|
525
537
|
} else {
|
|
526
538
|
opts.onPhase?.('push')
|
|
@@ -534,6 +546,7 @@ async function finalizeCodingRun(args: {
|
|
|
534
546
|
...(stderrTail ? { stderrTail } : {}),
|
|
535
547
|
...(usage ? { usage } : {}),
|
|
536
548
|
...(callMetrics ? { callMetrics } : {}),
|
|
549
|
+
...(effortReport ? { effortReport } : {}),
|
|
537
550
|
}
|
|
538
551
|
}
|
|
539
552
|
|
|
@@ -765,26 +778,27 @@ export async function runMultiRepoCoding(
|
|
|
765
778
|
// note + the backend system-prompt section explain the layout.
|
|
766
779
|
opts.onPhase?.('agent')
|
|
767
780
|
logger.info('multi-repo: running agent', { repos: legs.map((l) => l.dirName) })
|
|
768
|
-
const { summary, stats, stderrTail, usage, callMetrics } =
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
781
|
+
const { summary, stats, stderrTail, usage, callMetrics, effortReport } =
|
|
782
|
+
await runAgentInWorkspace(
|
|
783
|
+
{
|
|
784
|
+
dir: root,
|
|
785
|
+
systemPrompt: job.systemPrompt,
|
|
786
|
+
userPrompt: job.userPrompt,
|
|
787
|
+
model: job.model,
|
|
788
|
+
harness: job.harness,
|
|
789
|
+
subscriptionToken: job.subscriptionToken,
|
|
790
|
+
subscriptionBaseUrl: job.subscriptionBaseUrl,
|
|
791
|
+
ambientAuth: job.ambientAuth,
|
|
792
|
+
proxyBaseUrl: job.proxyBaseUrl,
|
|
793
|
+
sessionToken: job.sessionToken,
|
|
794
|
+
webToolsGuidance: job.webToolsGuidance,
|
|
795
|
+
webSearchProxy: job.webSearch,
|
|
796
|
+
guardLimits: job.guardLimits,
|
|
797
|
+
...(job.contextFiles ? { contextFiles: job.contextFiles } : {}),
|
|
798
|
+
multiRepo: true,
|
|
799
|
+
},
|
|
800
|
+
opts,
|
|
801
|
+
)
|
|
788
802
|
|
|
789
803
|
// Commit forgotten tracked edits, then push + open a PR for each repo the run actually changed.
|
|
790
804
|
const { primaryPushed, primaryPrUrl, peerPullRequests } = await pushMultiRepoLegs(
|
|
@@ -807,6 +821,7 @@ export async function runMultiRepoCoding(
|
|
|
807
821
|
stats,
|
|
808
822
|
...(usage ? { usage } : {}),
|
|
809
823
|
...(callMetrics ? { callMetrics } : {}),
|
|
824
|
+
...(effortReport ? { effortReport } : {}),
|
|
810
825
|
}
|
|
811
826
|
}
|
|
812
827
|
return {
|
|
@@ -822,6 +837,7 @@ export async function runMultiRepoCoding(
|
|
|
822
837
|
failureCause: 'no-changes',
|
|
823
838
|
...(usage ? { usage } : {}),
|
|
824
839
|
...(callMetrics ? { callMetrics } : {}),
|
|
840
|
+
...(effortReport ? { effortReport } : {}),
|
|
825
841
|
}
|
|
826
842
|
}
|
|
827
843
|
logger.info('multi-repo: complete', {
|
|
@@ -838,6 +854,7 @@ export async function runMultiRepoCoding(
|
|
|
838
854
|
stats,
|
|
839
855
|
...(usage ? { usage } : {}),
|
|
840
856
|
...(callMetrics ? { callMetrics } : {}),
|
|
857
|
+
...(effortReport ? { effortReport } : {}),
|
|
841
858
|
}
|
|
842
859
|
})
|
|
843
860
|
}
|
package/src/effort.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// The agent effort self-assessment side channel. Every container agent is asked
|
|
6
|
+
// (via the backend-composed system prompt) to end its run by writing a short JSON
|
|
7
|
+
// self-assessment — how hard the work was, what reduced its effectiveness, the key
|
|
8
|
+
// obstacles — to a sentinel file in its working directory. The harness reads it after
|
|
9
|
+
// the agent finishes, removes it (so it never lands in a commit), and forwards it on
|
|
10
|
+
// the job result; the backend records it on the step and surfaces it in run details.
|
|
11
|
+
//
|
|
12
|
+
// The filename is kept in sync with `EFFORT_REPORT_FILE` in `@cat-factory/agents`
|
|
13
|
+
// (the executor-harness has no dependency on that package), exactly like CONTEXT_DIR
|
|
14
|
+
// and the follow-ups sentinel. The shape mirrors the contracts `AgentEffortReport`.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** The sentinel file the agent writes its effort self-assessment to (relative to its cwd). */
|
|
18
|
+
export const EFFORT_REPORT_FILE = '.cat-effort.json'
|
|
19
|
+
|
|
20
|
+
/** A container agent's self-assessment of the work it just did. */
|
|
21
|
+
export interface EffortReport {
|
|
22
|
+
/** How hard the work was: 1 (trivial) .. 10 (extremely hard). */
|
|
23
|
+
difficulty: number
|
|
24
|
+
/** One or two sentences on how hard/easy the work was and why. */
|
|
25
|
+
summary?: string
|
|
26
|
+
/** What reduced the agent's effectiveness. */
|
|
27
|
+
reducedEffectiveness?: string
|
|
28
|
+
/** The key obstacles the agent hit. */
|
|
29
|
+
obstacles?: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read + parse + REMOVE the agent's effort sentinel file from `cwd`. Lenient: returns undefined
|
|
34
|
+
* when the file is absent (the agent wrote none), unreadable, not JSON, or carries nothing
|
|
35
|
+
* meaningful. Never throws — a malformed self-report must never fail an otherwise-good run.
|
|
36
|
+
*/
|
|
37
|
+
export async function readEffortReport(cwd: string): Promise<EffortReport | undefined> {
|
|
38
|
+
const path = join(cwd, EFFORT_REPORT_FILE)
|
|
39
|
+
let raw: string
|
|
40
|
+
try {
|
|
41
|
+
raw = await readFile(path, 'utf8')
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined // no report written — the common case
|
|
44
|
+
}
|
|
45
|
+
// Remove it so it never lands in a commit (defence in depth; the backend also excludes it).
|
|
46
|
+
await rm(path, { force: true }).catch(() => {})
|
|
47
|
+
let parsed: unknown
|
|
48
|
+
try {
|
|
49
|
+
parsed = JSON.parse(raw)
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
return coerceEffort(parsed)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Coerce arbitrary parsed JSON into a clean {@link EffortReport}, or undefined when it carries nothing. */
|
|
57
|
+
function coerceEffort(value: unknown): EffortReport | undefined {
|
|
58
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
59
|
+
const o = value as Record<string, unknown>
|
|
60
|
+
const report: EffortReport = { difficulty: clampDifficulty(o.difficulty) }
|
|
61
|
+
if (typeof o.summary === 'string' && o.summary.trim()) {
|
|
62
|
+
report.summary = o.summary.trim().slice(0, 2000)
|
|
63
|
+
}
|
|
64
|
+
if (typeof o.reducedEffectiveness === 'string' && o.reducedEffectiveness.trim()) {
|
|
65
|
+
report.reducedEffectiveness = o.reducedEffectiveness.trim().slice(0, 2000)
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(o.obstacles)) {
|
|
68
|
+
const obstacles = o.obstacles
|
|
69
|
+
.filter((x): x is string => typeof x === 'string' && x.trim().length > 0)
|
|
70
|
+
.map((x) => x.trim().slice(0, 500))
|
|
71
|
+
.slice(0, 20)
|
|
72
|
+
if (obstacles.length) report.obstacles = obstacles
|
|
73
|
+
}
|
|
74
|
+
// Nothing beyond a defaulted difficulty ⇒ the agent didn't really report anything; drop it so
|
|
75
|
+
// run details don't show an empty "5/10, no detail" card for a stray/blank file.
|
|
76
|
+
if (
|
|
77
|
+
report.summary === undefined &&
|
|
78
|
+
report.reducedEffectiveness === undefined &&
|
|
79
|
+
report.obstacles === undefined &&
|
|
80
|
+
!isFiniteNumber(o.difficulty)
|
|
81
|
+
) {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
return report
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clampDifficulty(v: unknown): number {
|
|
88
|
+
const n = isFiniteNumber(v)
|
|
89
|
+
? v
|
|
90
|
+
: typeof v === 'string' && v.trim() !== ''
|
|
91
|
+
? Number(v)
|
|
92
|
+
: Number.NaN
|
|
93
|
+
if (!Number.isFinite(n)) return 5
|
|
94
|
+
return Math.min(10, Math.max(1, Math.round(n)))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isFiniteNumber(v: unknown): v is number {
|
|
98
|
+
return typeof v === 'number' && Number.isFinite(v)
|
|
99
|
+
}
|
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. */
|
|
@@ -1117,6 +1124,24 @@ function isReservedEnvName(key: string): boolean {
|
|
|
1117
1124
|
return RESERVED_ENV_PREFIXES.some((p) => lower.startsWith(p))
|
|
1118
1125
|
}
|
|
1119
1126
|
|
|
1127
|
+
/**
|
|
1128
|
+
* Collect only string→string entries from a raw `env` bag. A non-string value is dropped so a
|
|
1129
|
+
* malformed binding can't inject `[object Object]` (or undefined) as an upstream URL. Reserved
|
|
1130
|
+
* names that would break the toolchain or enable injection (PATH, NODE_OPTIONS, LD_PRELOAD, …) are
|
|
1131
|
+
* dropped too: they are spread over `process.env` at build time, so a binding named `PATH` would
|
|
1132
|
+
* replace it with a URL and the build would no longer find its tools. Extracted from the infra
|
|
1133
|
+
* parsers to keep their cyclomatic complexity down.
|
|
1134
|
+
*/
|
|
1135
|
+
function parseInfraEnv(raw: unknown): Record<string, string> {
|
|
1136
|
+
const env: Record<string, string> = {}
|
|
1137
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
1138
|
+
for (const [key, val] of Object.entries(raw as Record<string, unknown>)) {
|
|
1139
|
+
if (key && !isReservedEnvName(key) && typeof val === 'string') env[key] = val
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return env
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1120
1145
|
/** Parse the frontend UI-test infra spec (`kind: 'frontend'`), tolerating missing knobs. */
|
|
1121
1146
|
function parseFrontendInfraSpec(o: Record<string, unknown>): FrontendInfraSpec {
|
|
1122
1147
|
const packageManager =
|
|
@@ -1126,17 +1151,7 @@ function parseFrontendInfraSpec(o: Record<string, unknown>): FrontendInfraSpec {
|
|
|
1126
1151
|
const serveMode = o.serveMode === 'static' || o.serveMode === 'command' ? o.serveMode : undefined
|
|
1127
1152
|
const envInjection =
|
|
1128
1153
|
o.envInjection === 'build' || o.envInjection === 'runtime' ? o.envInjection : undefined
|
|
1129
|
-
|
|
1130
|
-
// binding can't inject `[object Object]` (or undefined) as an upstream URL. Reserved names
|
|
1131
|
-
// that would break the toolchain or enable injection (PATH, NODE_OPTIONS, LD_PRELOAD, …) are
|
|
1132
|
-
// dropped too: they are spread over `process.env` at build time, so a binding named `PATH`
|
|
1133
|
-
// would replace it with a URL and the build would no longer find its tools.
|
|
1134
|
-
const env: Record<string, string> = {}
|
|
1135
|
-
if (typeof o.env === 'object' && o.env !== null) {
|
|
1136
|
-
for (const [key, val] of Object.entries(o.env as Record<string, unknown>)) {
|
|
1137
|
-
if (key && !isReservedEnvName(key) && typeof val === 'string') env[key] = val
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1154
|
+
const env = parseInfraEnv(o.env)
|
|
1140
1155
|
const servePort = port(o.servePort)
|
|
1141
1156
|
const wiremockPort = port(o.wiremockPort)
|
|
1142
1157
|
// The app's monorepo subdirectory becomes the install/build/serve cwd, so it goes through the
|
|
@@ -1369,11 +1384,7 @@ function assembleAgentJob(
|
|
|
1369
1384
|
ghToken: str(o.ghToken, 'ghToken'),
|
|
1370
1385
|
repo: parseRepoSpec(repo),
|
|
1371
1386
|
branch: str(o.branch, 'branch'),
|
|
1372
|
-
...(
|
|
1373
|
-
...(typeof o.webToolsGuidance === 'string' ? { webToolsGuidance: o.webToolsGuidance } : {}),
|
|
1374
|
-
...(o.webSearch === true ? { webSearch: true } : {}),
|
|
1375
|
-
...(o.full === true ? { full: true } : {}),
|
|
1376
|
-
...(typeof o.mergeBase === 'string' && o.mergeBase ? { mergeBase: o.mergeBase } : {}),
|
|
1387
|
+
...collectOptionalRequestFields(o),
|
|
1377
1388
|
...(bootstrap ? { bootstrap } : {}),
|
|
1378
1389
|
...(output ? { output } : {}),
|
|
1379
1390
|
...(contextFiles.length ? { contextFiles } : {}),
|
|
@@ -1381,20 +1392,36 @@ function assembleAgentJob(
|
|
|
1381
1392
|
...(skill ? { skill } : {}),
|
|
1382
1393
|
...(testSecrets.length ? { testSecrets } : {}),
|
|
1383
1394
|
...(infra ? { infra } : {}),
|
|
1384
|
-
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
1385
|
-
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
|
1386
|
-
...(typeof o.commitMessage === 'string' && o.commitMessage
|
|
1387
|
-
? { commitMessage: o.commitMessage }
|
|
1388
|
-
: {}),
|
|
1389
1395
|
...(pr ? { pr } : {}),
|
|
1390
1396
|
...(peerRepos.length ? { peerRepos } : {}),
|
|
1391
1397
|
...(referenceRepos.length ? { referenceRepos } : {}),
|
|
1392
1398
|
...(referenceBranches.length ? { referenceBranches } : {}),
|
|
1393
1399
|
...(reviewPrNumber !== undefined ? { reviewPrNumber } : {}),
|
|
1400
|
+
...(guardLimits ? { guardLimits } : {}),
|
|
1401
|
+
...(validation ? { validation } : {}),
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* The optional {@link AgentJob} fields read directly off the request `o` (booleans + trimmed
|
|
1407
|
+
* strings). Extracted from {@link assembleAgentJob} to keep its cyclomatic complexity down; every
|
|
1408
|
+
* key is unique so grouping the conditional spreads is behaviour-neutral (spread order is
|
|
1409
|
+
* irrelevant with no colliding keys).
|
|
1410
|
+
*/
|
|
1411
|
+
function collectOptionalRequestFields(o: Record<string, unknown>): Partial<AgentJob> {
|
|
1412
|
+
return {
|
|
1413
|
+
...(typeof o.githubApiBase === 'string' ? { githubApiBase: o.githubApiBase } : {}),
|
|
1414
|
+
...(typeof o.webToolsGuidance === 'string' ? { webToolsGuidance: o.webToolsGuidance } : {}),
|
|
1415
|
+
...(o.webSearch === true ? { webSearch: true } : {}),
|
|
1416
|
+
...(o.full === true ? { full: true } : {}),
|
|
1417
|
+
...(typeof o.mergeBase === 'string' && o.mergeBase ? { mergeBase: o.mergeBase } : {}),
|
|
1418
|
+
...(typeof o.newBranch === 'string' && o.newBranch ? { newBranch: o.newBranch } : {}),
|
|
1419
|
+
...(typeof o.pushBranch === 'string' && o.pushBranch ? { pushBranch: o.pushBranch } : {}),
|
|
1420
|
+
...(typeof o.commitMessage === 'string' && o.commitMessage
|
|
1421
|
+
? { commitMessage: o.commitMessage }
|
|
1422
|
+
: {}),
|
|
1394
1423
|
...(o.noChangesIsError === false ? { noChangesIsError: false } : {}),
|
|
1395
1424
|
...(o.persistentCheckout === true ? { persistentCheckout: true } : {}),
|
|
1396
1425
|
...(o.streamFollowUps === true ? { streamFollowUps: true } : {}),
|
|
1397
|
-
...(guardLimits ? { guardLimits } : {}),
|
|
1398
|
-
...(validation ? { validation } : {}),
|
|
1399
1426
|
}
|
|
1400
1427
|
}
|
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
|
/**
|