@tangle-network/agent-bench 0.1.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 +15 -0
- package/package.json +35 -0
- package/src/adapters.ts +60 -0
- package/src/aec-gate.mts +217 -0
- package/src/atom-humaneval.mts +197 -0
- package/src/atom-mcp-e2e.mts +223 -0
- package/src/benchmarks/_harness.ts +206 -0
- package/src/benchmarks/aec-bench.test.mts +53 -0
- package/src/benchmarks/aec-bench.ts +319 -0
- package/src/benchmarks/appworld.test.mts +45 -0
- package/src/benchmarks/appworld.ts +426 -0
- package/src/benchmarks/cad-design.ts +429 -0
- package/src/benchmarks/cadbench.ts +135 -0
- package/src/benchmarks/cadgenbench.ts +121 -0
- package/src/benchmarks/commit0.test.mts +71 -0
- package/src/benchmarks/commit0.ts +266 -0
- package/src/benchmarks/enterpriseops-gym.test.mts +77 -0
- package/src/benchmarks/enterpriseops-gym.ts +343 -0
- package/src/benchmarks/finsearchcomp.ts +371 -0
- package/src/benchmarks/frames.ts +520 -0
- package/src/benchmarks/hotpotqa.ts +320 -0
- package/src/benchmarks/humaneval.ts +251 -0
- package/src/benchmarks/mind2web.ts +311 -0
- package/src/benchmarks/programbench.test.mts +60 -0
- package/src/benchmarks/programbench.ts +210 -0
- package/src/benchmarks/simpleqa.ts +390 -0
- package/src/benchmarks/swe-bench.ts +152 -0
- package/src/benchmarks/terminal-bench.ts +182 -0
- package/src/benchmarks/trata-hedge.ts +496 -0
- package/src/benchmarks/types.ts +58 -0
- package/src/browser/adapters/bad-design-audit.ts +153 -0
- package/src/browser/adapters/bad-design-audit.verify.ts +85 -0
- package/src/browser/adapters/bad.ts +165 -0
- package/src/browser/agent-adapter.ts +145 -0
- package/src/browser/process-adapter.ts +146 -0
- package/src/browser/run-to-spans.ts +105 -0
- package/src/browser/run-to-spans.verify.ts +89 -0
- package/src/browser/ui-reviewer.ts +200 -0
- package/src/browser/ui-reviewer.verify.ts +94 -0
- package/src/browser/verify.ts +96 -0
- package/src/clbench-codebase-gate.mts +314 -0
- package/src/clbench-context-gate.mts +305 -0
- package/src/cloud-loop.mts +138 -0
- package/src/coding-skills/minimal-diff.md +9 -0
- package/src/coding-skills/read-before-edit.md +9 -0
- package/src/coding-skills/reproduce-first.md +10 -0
- package/src/coding-skills/run-tests-after-edit.md +9 -0
- package/src/coding-skills/trace-the-failure.md +9 -0
- package/src/commit0-env-run.mts +59 -0
- package/src/commit0-env.ts +173 -0
- package/src/commit0-gate.mts +529 -0
- package/src/commit0-prereqs.sh +48 -0
- package/src/corpus-replay.mts +300 -0
- package/src/corpus-report.mts +504 -0
- package/src/corpus.test.mts +273 -0
- package/src/corpus.ts +325 -0
- package/src/decoder-live.mts +133 -0
- package/src/directives.ts +84 -0
- package/src/diverse-gate.mjs +112 -0
- package/src/egress-probe.mts +26 -0
- package/src/eops-skills/address-every-subtask.md +5 -0
- package/src/eops-skills/exact-tools-and-args.md +5 -0
- package/src/eops-skills/full-sequence-to-goal.md +5 -0
- package/src/eops-skills/ground-every-value.md +5 -0
- package/src/eops-skills/honor-the-policies.md +5 -0
- package/src/examples/README.md +58 -0
- package/src/examples/math-demo.mts +110 -0
- package/src/examples/strategy-demo.mts +119 -0
- package/src/fleet.mts +121 -0
- package/src/gate-cli.mts +101 -0
- package/src/gate.test.mts +129 -0
- package/src/gate.ts +460 -0
- package/src/generate-eval/certify.ts +178 -0
- package/src/generate-eval/schema.ts +78 -0
- package/src/humaneval-gate.mts +204 -0
- package/src/humaneval-repair-gate.mts +143 -0
- package/src/index.ts +19 -0
- package/src/mcp-mount-probe.mts +126 -0
- package/src/profile-coordinates.ts +134 -0
- package/src/profiles.ts +128 -0
- package/src/refine-loop.test.mts +106 -0
- package/src/refine-loop.ts +106 -0
- package/src/research-gate.mts +132 -0
- package/src/research-shot.ts +134 -0
- package/src/resolve-client.ts +58 -0
- package/src/router-executor.ts +51 -0
- package/src/run-pool.ts +48 -0
- package/src/runtime-hook-recorder.ts +137 -0
- package/src/sandbox-run.ts +125 -0
- package/src/search-bench/bridge.ts +124 -0
- package/src/search-bench/export.mts +0 -0
- package/src/search-bench/parametric-check.mts +63 -0
- package/src/search-bench/profiles.ts +98 -0
- package/src/search-bench/run.mts +287 -0
- package/src/search-bench/tasks-fresh.ts +688 -0
- package/src/search-bench/tasks.ts +129 -0
- package/src/search-tool.ts +95 -0
- package/src/selector.test.mts +189 -0
- package/src/selector.ts +366 -0
- package/src/skill-sandbox-smoke.mts +100 -0
- package/src/stats.mts +90 -0
- package/src/terminal-compare.ts +519 -0
- package/src/trajectory-assemble.mjs +130 -0
- package/src/trata-gate.mts +243 -0
- package/src/trata-gepa.mts +434 -0
- package/src/worker-blender.ts +230 -0
- package/src/worker-browser.ts +102 -0
- package/src/worker-build123d.ts +143 -0
- package/src/worker-cad.ts +451 -0
- package/src/worker.ts +136 -0
- package/src/workspace-loop.mts +133 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-Bench blind-vs-refine runner.
|
|
3
|
+
*
|
|
4
|
+
* Reuses Terminal-Bench's OPEN-SOURCE infrastructure end to end: tb's native
|
|
5
|
+
* opencode agent runs IN the task's Docker container and the task's OWN verifier
|
|
6
|
+
* scores it (results.json). There is NO hand-rolled agent and NO hand-rolled
|
|
7
|
+
* judge here.
|
|
8
|
+
*
|
|
9
|
+
* Per task:
|
|
10
|
+
* ROUND 1 (blind) — `tb run --agent opencode` → parse results.json →
|
|
11
|
+
* resolved_1. This IS blind pass@1.
|
|
12
|
+
* ROUND r (refine, only if blind failed AND rounds>1) — extract a compact
|
|
13
|
+
* summary of round 1's commands + terminal state + the
|
|
14
|
+
* failing tests from tb's own run dir, then
|
|
15
|
+
* `tb run --agent-import-path
|
|
16
|
+
* tb_agents.opencode_refine_agent:OpenCodeRefineAgent
|
|
17
|
+
* --agent-kwarg prior_attempt=<summary>`. The refine agent
|
|
18
|
+
* prepends an evidence-gated refine directive; with no
|
|
19
|
+
* prior_attempt it is byte-for-byte the plain opencode agent,
|
|
20
|
+
* so round 1 == blind by construction.
|
|
21
|
+
* Stop early once resolved.
|
|
22
|
+
*
|
|
23
|
+
* Reports blind (resolved_1) vs refine (resolved_final) + rescued/broke counts,
|
|
24
|
+
* matching src/run.ts `batch-compare`. tb is shelled through bench/.venv exactly
|
|
25
|
+
* as src/benchmarks/swe-bench.ts shells its venv.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { execFile } from 'node:child_process'
|
|
29
|
+
import { mkdir, readFile, readdir, stat } from 'node:fs/promises'
|
|
30
|
+
import { join } from 'node:path'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
|
|
33
|
+
import { homedir } from 'node:os'
|
|
34
|
+
|
|
35
|
+
import { appendRunRecord, type AttemptRecord, type RunRecord } from './corpus'
|
|
36
|
+
import { runPool } from './run-pool'
|
|
37
|
+
|
|
38
|
+
const BENCH_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
39
|
+
const TB = join(BENCH_ROOT, '.venv', 'bin', 'tb')
|
|
40
|
+
const RUNS_DIR = join(BENCH_ROOT, 'runs')
|
|
41
|
+
const REFINE_IMPORT = 'tb_agents.opencode_refine_agent:OpenCodeRefineAgent'
|
|
42
|
+
// The durable learning-flywheel corpus (docs/learning-flywheel.md). terminal-bench
|
|
43
|
+
// is bench-orchestrated (tb owns the containers) so it cannot use buildRunRecord,
|
|
44
|
+
// which consumes runLoop Iterations; instead each task's per-round tb artifacts are
|
|
45
|
+
// folded into one RunRecord here so the corpus is genuinely cross-benchmark
|
|
46
|
+
// (finsearch-loop.ts writes the same store from the runLoop path).
|
|
47
|
+
const CORPUS = process.env.CORPUS ?? join(BENCH_ROOT, 'corpus', 'terminal.jsonl')
|
|
48
|
+
|
|
49
|
+
const DATASET = process.env.TB_DATASET ?? 'terminal-bench-core==0.1.1'
|
|
50
|
+
const MODEL = process.env.TB_MODEL ?? 'deepseek/deepseek-v4-pro'
|
|
51
|
+
const ROUNDS = Math.max(1, Number(process.env.ROUNDS ?? 2))
|
|
52
|
+
const CONCURRENCY = Math.max(1, Number(process.env.CONCURRENCY ?? 2))
|
|
53
|
+
// Per-round wall-clock cap (ms). tb itself enforces task timeouts; this is the
|
|
54
|
+
// outer kill-switch so a wedged container can't burn the budget.
|
|
55
|
+
const ROUND_CAP_MS = Math.max(60_000, Number(process.env.ROUND_CAP_MS ?? 1_200_000))
|
|
56
|
+
// tb's own agent-step timeout. Bounds each round's in-container agent work so a
|
|
57
|
+
// slow model can't blow the per-round budget; tb scores the container as-is when
|
|
58
|
+
// it fires (failure_mode=agent_timeout). Unset → tb's task default.
|
|
59
|
+
const TB_AGENT_TIMEOUT_SEC = process.env.TB_AGENT_TIMEOUT_SEC
|
|
60
|
+
? Number(process.env.TB_AGENT_TIMEOUT_SEC)
|
|
61
|
+
: undefined
|
|
62
|
+
const DEFAULT_IDS = ['hello-world', 'fix-permissions']
|
|
63
|
+
const IDS = (process.env.IDS ? process.env.IDS.split(',') : DEFAULT_IDS)
|
|
64
|
+
.map((s) => s.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
|
|
67
|
+
interface BenchmarkResults {
|
|
68
|
+
resolved_ids?: string[]
|
|
69
|
+
unresolved_ids?: string[]
|
|
70
|
+
results?: Array<{
|
|
71
|
+
task_id?: string
|
|
72
|
+
is_resolved?: boolean
|
|
73
|
+
parser_results?: Record<string, string>
|
|
74
|
+
instruction?: string
|
|
75
|
+
failure_mode?: string
|
|
76
|
+
total_input_tokens?: number
|
|
77
|
+
total_output_tokens?: number
|
|
78
|
+
}>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface RoundOutcome {
|
|
82
|
+
round: number
|
|
83
|
+
resolved: boolean
|
|
84
|
+
runId: string
|
|
85
|
+
outputDir: string
|
|
86
|
+
trialDir: string | null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Resolve the router key the in-container opencode agent uses (sk-tan… via the Tangle router). */
|
|
90
|
+
function must(name: string): string {
|
|
91
|
+
const v = process.env[name]
|
|
92
|
+
if (!v || !v.trim()) throw new Error(`missing required env ${name}`)
|
|
93
|
+
return v
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function envForTb(): NodeJS.ProcessEnv {
|
|
97
|
+
// tb's opencode agent (provider=openai) forwards OPENAI_API_KEY / OPENAI_BASE_URL
|
|
98
|
+
// into the container. Point them at the Tangle router.
|
|
99
|
+
return {
|
|
100
|
+
...process.env,
|
|
101
|
+
OPENAI_API_KEY: must('OPENAI_API_KEY'),
|
|
102
|
+
OPENAI_BASE_URL: process.env.OPENAI_BASE_URL ?? 'https://router.tangle.tools/v1',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Shell tb through the bench venv with a hard wall-clock cap; fail loud on nonzero. */
|
|
107
|
+
function runTb(args: string[], runId: string): Promise<void> {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
const child = execFile(
|
|
110
|
+
TB,
|
|
111
|
+
args,
|
|
112
|
+
{ cwd: BENCH_ROOT, env: envForTb(), maxBuffer: 1024 * 1024 * 256, timeout: ROUND_CAP_MS },
|
|
113
|
+
(err) => {
|
|
114
|
+
if (err) {
|
|
115
|
+
const killed = (err as { killed?: boolean }).killed
|
|
116
|
+
reject(
|
|
117
|
+
new Error(
|
|
118
|
+
`tb run failed (runId=${runId}${killed ? ', killed by ROUND_CAP_MS' : ''}): ${err.message}`,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
resolve()
|
|
124
|
+
},
|
|
125
|
+
)
|
|
126
|
+
child.stdout?.on('data', (d) => process.stdout.write(String(d)))
|
|
127
|
+
child.stderr?.on('data', (d) => process.stderr.write(String(d)))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Read tb's top-level results.json for a run and return whether taskId resolved. */
|
|
132
|
+
async function readResolved(outputDir: string, taskId: string): Promise<boolean> {
|
|
133
|
+
const raw = await readFile(join(outputDir, 'results.json'), 'utf8')
|
|
134
|
+
const parsed = JSON.parse(raw) as BenchmarkResults
|
|
135
|
+
if (Array.isArray(parsed.resolved_ids)) return parsed.resolved_ids.includes(taskId)
|
|
136
|
+
// Fall back to the per-trial flag — never silently report a zero.
|
|
137
|
+
const trial = parsed.results?.find((r) => r.task_id === taskId)
|
|
138
|
+
if (!trial || typeof trial.is_resolved !== 'boolean') {
|
|
139
|
+
throw new Error(`results.json for ${taskId} has neither resolved_ids nor a per-trial is_resolved`)
|
|
140
|
+
}
|
|
141
|
+
return trial.is_resolved
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Locate the single trial dir tb wrote under <outputDir>/<taskId>/<trial>/. */
|
|
145
|
+
async function findTrialDir(outputDir: string, taskId: string): Promise<string | null> {
|
|
146
|
+
const taskDir = join(outputDir, taskId)
|
|
147
|
+
let entries: string[]
|
|
148
|
+
try {
|
|
149
|
+
entries = await readdir(taskDir)
|
|
150
|
+
} catch {
|
|
151
|
+
return null
|
|
152
|
+
}
|
|
153
|
+
const trials: Array<{ dir: string; mtime: number }> = []
|
|
154
|
+
for (const e of entries) {
|
|
155
|
+
const p = join(taskDir, e)
|
|
156
|
+
const s = await stat(p)
|
|
157
|
+
if (s.isDirectory()) trials.push({ dir: p, mtime: s.mtimeMs })
|
|
158
|
+
}
|
|
159
|
+
trials.sort((a, b) => b.mtime - a.mtime)
|
|
160
|
+
return trials[0]?.dir ?? null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function readFileSafe(path: string, tailBytes = 0): Promise<string> {
|
|
164
|
+
try {
|
|
165
|
+
const txt = await readFile(path, 'utf8')
|
|
166
|
+
return tailBytes > 0 && txt.length > tailBytes ? txt.slice(-tailBytes) : txt
|
|
167
|
+
} catch {
|
|
168
|
+
return ''
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build the evidence-gated prior-attempt summary from tb's OWN round-1 artifacts:
|
|
174
|
+
* the opencode invocation, the failing tests (parser_results), and the tail of the
|
|
175
|
+
* post-agent terminal pane. Compact + bounded so it fits an --agent-kwarg.
|
|
176
|
+
*/
|
|
177
|
+
async function buildPriorSummary(outcome: RoundOutcome, taskId: string): Promise<string> {
|
|
178
|
+
const parts: string[] = []
|
|
179
|
+
|
|
180
|
+
if (outcome.trialDir) {
|
|
181
|
+
const trialResultsRaw = await readFileSafe(join(outcome.trialDir, 'results.json'))
|
|
182
|
+
if (trialResultsRaw) {
|
|
183
|
+
try {
|
|
184
|
+
const tr = JSON.parse(trialResultsRaw) as { parser_results?: Record<string, string> }
|
|
185
|
+
const parser = tr.parser_results
|
|
186
|
+
if (parser) {
|
|
187
|
+
const failed = Object.entries(parser)
|
|
188
|
+
.filter(([, v]) => v !== 'passed')
|
|
189
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
190
|
+
if (failed.length) parts.push(`FAILING TESTS:\n${failed.join('\n')}`)
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
// non-fatal: fall through to other evidence
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const commands = await readFileSafe(join(outcome.trialDir, 'commands.txt'))
|
|
198
|
+
if (commands) {
|
|
199
|
+
// The opencode invocation line carries the instruction the agent actually ran.
|
|
200
|
+
const opencodeLine = commands
|
|
201
|
+
.split('\n')
|
|
202
|
+
.find((l) => l.includes('opencode --model'))
|
|
203
|
+
if (opencodeLine) parts.push(`AGENT INVOCATION:\n${opencodeLine.slice(0, 600)}`)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const postAgent = await readFileSafe(join(outcome.trialDir, 'panes', 'post-agent.txt'), 1500)
|
|
207
|
+
if (postAgent.trim()) parts.push(`TERMINAL STATE AFTER ATTEMPT (tail):\n${postAgent.trim()}`)
|
|
208
|
+
|
|
209
|
+
const postTest = await readFileSafe(join(outcome.trialDir, 'panes', 'post-test.txt'), 1200)
|
|
210
|
+
if (postTest.trim()) parts.push(`TEST OUTPUT (tail):\n${postTest.trim()}`)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (parts.length === 0) {
|
|
214
|
+
parts.push(
|
|
215
|
+
`The previous attempt ran tb's opencode agent on "${taskId}" and the task's tests did not pass. No transcript artifacts were captured.`,
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
return parts.join('\n\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const TRACE_TAIL_MAX = 600
|
|
222
|
+
const OUTPUT_TAIL_MAX = 2000
|
|
223
|
+
|
|
224
|
+
/** The fields the corpus reads from tb's top-level results.json per round. */
|
|
225
|
+
interface TbRoundFacts {
|
|
226
|
+
instruction: string
|
|
227
|
+
failureMode?: string
|
|
228
|
+
/** tb's own token counts. Absent (not 0) when tb didn't record them — a
|
|
229
|
+
* missing/unparseable results.json or a non-numeric field is "unmeasured",
|
|
230
|
+
* never a fabricated 0 (which would read as a free round downstream). */
|
|
231
|
+
tokensIn?: number
|
|
232
|
+
tokensOut?: number
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Pull the bare task instruction, failure mode, and token counts tb recorded for
|
|
236
|
+
* this round. The instruction tb records is always the unmodified task statement —
|
|
237
|
+
* the refine directive is injected inside the agent, after tb captures it — so the
|
|
238
|
+
* refine steer is taken from `priorSteer`, not from here. */
|
|
239
|
+
async function readRoundFacts(outcome: RoundOutcome, taskId: string): Promise<TbRoundFacts> {
|
|
240
|
+
const raw = await readFileSafe(join(outcome.outputDir, 'results.json'))
|
|
241
|
+
if (!raw) return { instruction: '' }
|
|
242
|
+
let parsed: BenchmarkResults
|
|
243
|
+
try {
|
|
244
|
+
parsed = JSON.parse(raw) as BenchmarkResults
|
|
245
|
+
} catch {
|
|
246
|
+
return { instruction: '' }
|
|
247
|
+
}
|
|
248
|
+
const trial = parsed.results?.find((r) => r.task_id === taskId)
|
|
249
|
+
return {
|
|
250
|
+
instruction: typeof trial?.instruction === 'string' ? trial.instruction : '',
|
|
251
|
+
failureMode: typeof trial?.failure_mode === 'string' ? trial.failure_mode : undefined,
|
|
252
|
+
// Only set when tb actually surfaced a number — absence stays absent.
|
|
253
|
+
...(typeof trial?.total_input_tokens === 'number' ? { tokensIn: trial.total_input_tokens } : {}),
|
|
254
|
+
...(typeof trial?.total_output_tokens === 'number' ? { tokensOut: trial.total_output_tokens } : {}),
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Fold one tb round into a corpus AttemptRecord. `priorSteer` is the evidence-gated
|
|
260
|
+
* refine payload the runner injected for this round (empty for round 1, which is the
|
|
261
|
+
* bare blind attempt). tb does not expose a structured event stream, so eventCount /
|
|
262
|
+
* eventTypes are 0/{}; costUsd is OMITTED (tb reports no cost — we never fabricate
|
|
263
|
+
* a 0). traceTail = the failing-test + terminal-state evidence summary (the same
|
|
264
|
+
* one fed forward as the next steer).
|
|
265
|
+
*/
|
|
266
|
+
async function roundToAttempt(
|
|
267
|
+
outcome: RoundOutcome,
|
|
268
|
+
taskId: string,
|
|
269
|
+
priorSteer: string,
|
|
270
|
+
): Promise<AttemptRecord> {
|
|
271
|
+
const facts = await readRoundFacts(outcome, taskId)
|
|
272
|
+
// Round 1 = the bare task instruction (blind). Refine rounds = the steer actually
|
|
273
|
+
// injected: the evidence-gated prior summary carrying the round's failure info.
|
|
274
|
+
const prompt =
|
|
275
|
+
outcome.round === 1 || !priorSteer
|
|
276
|
+
? facts.instruction
|
|
277
|
+
: `REFINE STEER (prepended to the task instruction):\n${priorSteer}\n\n--- ORIGINAL TASK ---\n${facts.instruction}`
|
|
278
|
+
const transcript = outcome.trialDir
|
|
279
|
+
? await readFileSafe(join(outcome.trialDir, 'panes', 'post-agent.txt'), OUTPUT_TAIL_MAX)
|
|
280
|
+
: ''
|
|
281
|
+
const traceSummary = await buildPriorSummary(outcome, taskId)
|
|
282
|
+
return {
|
|
283
|
+
round: outcome.round,
|
|
284
|
+
prompt,
|
|
285
|
+
output: transcript.trim(),
|
|
286
|
+
valid: outcome.resolved,
|
|
287
|
+
score: outcome.resolved ? 1 : 0,
|
|
288
|
+
tokensIn: facts.tokensIn,
|
|
289
|
+
tokensOut: facts.tokensOut,
|
|
290
|
+
eventCount: 0,
|
|
291
|
+
eventTypes: {},
|
|
292
|
+
traceTail: traceSummary.slice(-TRACE_TAIL_MAX),
|
|
293
|
+
// A failed round is a wrong answer, not an attempt error; surface only a concrete
|
|
294
|
+
// tb-reported failure mode (e.g. agent_timeout). tb writes 'unset'/'none' when it
|
|
295
|
+
// has no specific mode — that carries no signal, so it is not an error.
|
|
296
|
+
error: informativeFailureMode(facts.failureMode),
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** tb's failure_mode, normalized: undefined for its no-signal sentinels. */
|
|
301
|
+
function informativeFailureMode(mode: string | undefined): string | undefined {
|
|
302
|
+
if (!mode) return undefined
|
|
303
|
+
const m = mode.trim().toLowerCase()
|
|
304
|
+
if (m === '' || m === 'unset' || m === 'none') return undefined
|
|
305
|
+
return `failure_mode=${mode}`
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build and persist one flywheel RunRecord per task from terminal-compare's own
|
|
310
|
+
* round data — no buildRunRecord (that consumes runLoop Iterations; tb is not
|
|
311
|
+
* runLoop-shaped). condition = 'refine@k' when a refine budget was available,
|
|
312
|
+
* 'blind' when only the single blind round can run (ROUNDS===1). Appended once
|
|
313
|
+
* per task; never throws into the run (corpus capture must not fail the bench).
|
|
314
|
+
*/
|
|
315
|
+
async function captureRunRecord(
|
|
316
|
+
taskId: string,
|
|
317
|
+
rounds: RoundOutcome[],
|
|
318
|
+
priorSteers: string[],
|
|
319
|
+
): Promise<void> {
|
|
320
|
+
const attempts: AttemptRecord[] = []
|
|
321
|
+
for (const outcome of rounds) {
|
|
322
|
+
// priorSteers[i] is the steer injected for rounds[i] ('' for round 1).
|
|
323
|
+
const steer = priorSteers[outcome.round - 1] ?? ''
|
|
324
|
+
attempts.push(await roundToAttempt(outcome, taskId, steer))
|
|
325
|
+
}
|
|
326
|
+
const blindResolved = rounds[0]?.resolved === true
|
|
327
|
+
const last = rounds[rounds.length - 1]
|
|
328
|
+
const record: RunRecord = {
|
|
329
|
+
ts: new Date().toISOString(),
|
|
330
|
+
benchmark: 'terminal-bench',
|
|
331
|
+
instanceId: taskId,
|
|
332
|
+
condition: ROUNDS > 1 ? `refine@${ROUNDS}` : 'blind',
|
|
333
|
+
model: MODEL,
|
|
334
|
+
blindResolved,
|
|
335
|
+
resolved: last ? last.resolved : blindResolved,
|
|
336
|
+
attempts,
|
|
337
|
+
infraError: false,
|
|
338
|
+
}
|
|
339
|
+
await appendRunRecord(CORPUS, record)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Serially `docker compose build` each task image before the concurrent fan-out.
|
|
344
|
+
* tb builds per-task images on first use; building two cold images concurrently
|
|
345
|
+
* contends on the shared docker build backend and can return nonzero. Warming the
|
|
346
|
+
* cache serially makes the parallel rounds hit a warm cache and never race.
|
|
347
|
+
* Dataset-agnostic: derives the compose path from the tb cache layout
|
|
348
|
+
* (<cache>/<name>/<version>/<task>/docker-compose.yaml).
|
|
349
|
+
*/
|
|
350
|
+
async function prebuildImages(taskIds: string[]): Promise<void> {
|
|
351
|
+
const [name, version] = DATASET.split('==')
|
|
352
|
+
if (!name || !version) {
|
|
353
|
+
console.log(` prebuild: skipped (dataset ${DATASET} is not name==version; relying on tb's lazy build)`)
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
const cacheRoot = join(homedir(), '.cache', 'terminal-bench', name, version)
|
|
357
|
+
for (const taskId of taskIds) {
|
|
358
|
+
const composePath = join(cacheRoot, taskId, 'docker-compose.yaml')
|
|
359
|
+
try {
|
|
360
|
+
await stat(composePath)
|
|
361
|
+
} catch {
|
|
362
|
+
console.log(` prebuild: no compose at ${composePath}; tb will materialize ${taskId} on first run`)
|
|
363
|
+
continue
|
|
364
|
+
}
|
|
365
|
+
process.stdout.write(` prebuild: ${taskId} … `)
|
|
366
|
+
await new Promise<void>((resolve, reject) => {
|
|
367
|
+
execFile(
|
|
368
|
+
'docker',
|
|
369
|
+
['compose', '-p', `tbprebuild-${taskId}`, '-f', composePath, 'build'],
|
|
370
|
+
{ maxBuffer: 1024 * 1024 * 256, timeout: ROUND_CAP_MS },
|
|
371
|
+
(err) => (err ? reject(new Error(`prebuild ${taskId} failed: ${err.message}`)) : resolve()),
|
|
372
|
+
)
|
|
373
|
+
})
|
|
374
|
+
console.log('built')
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function slugRunId(taskId: string, round: number): string {
|
|
379
|
+
// tb derives a docker compose project name from the run-id, which must match
|
|
380
|
+
// [a-z0-9][a-z0-9_-]* — lowercase, no '.', no ISO 'T'/'Z' separators, no ':'.
|
|
381
|
+
const ts = new Date().toISOString().replace(/[^0-9]/g, '')
|
|
382
|
+
return `cmp-${taskId}-r${round}-${ts}`.toLowerCase().replace(/[^a-z0-9_-]/g, '-')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** One tb round. round 1 = plain opencode (blind); round>1 = refine agent + prior_attempt. */
|
|
386
|
+
async function runRound(taskId: string, round: number, prior: string): Promise<RoundOutcome> {
|
|
387
|
+
const runId = slugRunId(taskId, round)
|
|
388
|
+
// tb writes results to <output-path>/<run-id>/, so point output-path at RUNS_DIR
|
|
389
|
+
// and the run materializes at RUNS_DIR/<runId>/.
|
|
390
|
+
const outputDir = join(RUNS_DIR, runId)
|
|
391
|
+
const baseArgs = [
|
|
392
|
+
'run',
|
|
393
|
+
'-d', DATASET,
|
|
394
|
+
'--task-id', taskId,
|
|
395
|
+
'-m', MODEL,
|
|
396
|
+
'--output-path', RUNS_DIR,
|
|
397
|
+
'--run-id', runId,
|
|
398
|
+
'--n-concurrent', '1',
|
|
399
|
+
'--no-livestream',
|
|
400
|
+
...(TB_AGENT_TIMEOUT_SEC ? ['--global-agent-timeout-sec', String(TB_AGENT_TIMEOUT_SEC)] : []),
|
|
401
|
+
]
|
|
402
|
+
// tb injects model_name from -m into the agent kwargs, and its kwarg parser
|
|
403
|
+
// (`key, value = kwarg.split('=')` + ast.literal_eval) rejects '=' and Python
|
|
404
|
+
// literals in values. Hex-encode the prior with an 'h' sentinel so it survives
|
|
405
|
+
// the split and is kept as a string; the refine agent decodes it.
|
|
406
|
+
const priorHex = `h${Buffer.from(prior, 'utf8').toString('hex')}`
|
|
407
|
+
const args =
|
|
408
|
+
round === 1
|
|
409
|
+
? [...baseArgs, '--agent', 'opencode']
|
|
410
|
+
: [
|
|
411
|
+
...baseArgs,
|
|
412
|
+
'--agent-import-path', REFINE_IMPORT,
|
|
413
|
+
'--agent-kwarg', `prior_attempt_hex=${priorHex}`,
|
|
414
|
+
]
|
|
415
|
+
await runTb(args, runId)
|
|
416
|
+
const resolved = await readResolved(outputDir, taskId)
|
|
417
|
+
const trialDir = await findTrialDir(outputDir, taskId)
|
|
418
|
+
return { round, resolved, runId, outputDir, trialDir }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function solveTask(taskId: string): Promise<{
|
|
422
|
+
taskId: string
|
|
423
|
+
blind: boolean
|
|
424
|
+
refine: boolean
|
|
425
|
+
rounds: RoundOutcome[]
|
|
426
|
+
}> {
|
|
427
|
+
const history: RoundOutcome[] = []
|
|
428
|
+
// The evidence-gated steer injected per round, indexed by round number (round 1 has
|
|
429
|
+
// none). Captured here so the corpus records the exact steer each refine round saw.
|
|
430
|
+
const priorSteers: string[] = ['']
|
|
431
|
+
const r1 = await runRound(taskId, 1, '')
|
|
432
|
+
history.push(r1)
|
|
433
|
+
const blind = r1.resolved
|
|
434
|
+
let prev = r1
|
|
435
|
+
|
|
436
|
+
for (let round = 2; round <= ROUNDS && !prev.resolved; round++) {
|
|
437
|
+
const prior = await buildPriorSummary(prev, taskId)
|
|
438
|
+
priorSteers[round - 1] = prior
|
|
439
|
+
const r = await runRound(taskId, round, prior)
|
|
440
|
+
history.push(r)
|
|
441
|
+
prev = r
|
|
442
|
+
}
|
|
443
|
+
// refine = the final round's outcome (blind itself when no refine round fired).
|
|
444
|
+
const last = history[history.length - 1]
|
|
445
|
+
const refine = last ? last.resolved : blind
|
|
446
|
+
// Persist the full per-attempt tuple to the durable cross-benchmark flywheel corpus.
|
|
447
|
+
// Capture is observability over the primary bench result: a write failure is logged
|
|
448
|
+
// loudly (never silently swallowed) but does not abort an expensive container run.
|
|
449
|
+
try {
|
|
450
|
+
await captureRunRecord(taskId, history, priorSteers)
|
|
451
|
+
} catch (err) {
|
|
452
|
+
const msg = err instanceof Error ? (err.stack ?? err.message) : String(err)
|
|
453
|
+
console.error(` [corpus] WARN: failed to persist RunRecord for ${taskId}: ${msg}`)
|
|
454
|
+
}
|
|
455
|
+
return { taskId, blind, refine, rounds: history }
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function main(): Promise<void> {
|
|
459
|
+
await mkdir(RUNS_DIR, { recursive: true })
|
|
460
|
+
// Validate auth up front — fail loud before spending a single container.
|
|
461
|
+
must('OPENAI_API_KEY')
|
|
462
|
+
|
|
463
|
+
console.log('=== TERMINAL-BENCH BLIND vs REFINE — run plan ===')
|
|
464
|
+
console.log(` dataset: ${DATASET}`)
|
|
465
|
+
console.log(` model: ${MODEL}`)
|
|
466
|
+
console.log(` tasks (${IDS.length}): ${IDS.join(', ')}`)
|
|
467
|
+
console.log(` rounds/task: 1 (blind) … up to ${ROUNDS} (refine on blind failure)`)
|
|
468
|
+
console.log(` concurrency: ${CONCURRENCY}`)
|
|
469
|
+
console.log(` per-round cap: ${Math.round(ROUND_CAP_MS / 1000)}s`)
|
|
470
|
+
console.log(
|
|
471
|
+
` worst-case tb container runs: ${IDS.length * ROUNDS} (best case ${IDS.length} if all blind-pass)`,
|
|
472
|
+
)
|
|
473
|
+
console.log('')
|
|
474
|
+
|
|
475
|
+
await prebuildImages(IDS)
|
|
476
|
+
console.log('')
|
|
477
|
+
|
|
478
|
+
const results: Array<Awaited<ReturnType<typeof solveTask>>> = []
|
|
479
|
+
let done = 0
|
|
480
|
+
await runPool(IDS, CONCURRENCY, async (taskId) => {
|
|
481
|
+
const started = Date.now()
|
|
482
|
+
const r = await solveTask(taskId)
|
|
483
|
+
results.push(r)
|
|
484
|
+
done += 1
|
|
485
|
+
const secs = Math.round((Date.now() - started) / 1000)
|
|
486
|
+
const perRound = r.rounds.map((x) => `r${x.round}=${x.resolved ? '✓' : '·'}`).join(' ')
|
|
487
|
+
const tag =
|
|
488
|
+
r.refine && !r.blind ? '↑RESCUED' : r.blind && !r.refine ? '↓BROKE' : r.blind ? '=both✓' : '=both·'
|
|
489
|
+
console.log(
|
|
490
|
+
` [${done}/${IDS.length}] ${taskId}: blind=${r.blind ? '✓' : '·'} refine=${r.refine ? '✓' : '·'} ${tag} (${perRound}, ${secs}s)`,
|
|
491
|
+
)
|
|
492
|
+
for (const rd of r.rounds) console.log(` round ${rd.round} run dir: ${rd.outputDir}`)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
const n = results.length
|
|
496
|
+
const nBlind = results.filter((r) => r.blind).length
|
|
497
|
+
const nRefine = results.filter((r) => r.refine).length
|
|
498
|
+
const rescued = results.filter((r) => r.refine && !r.blind).length
|
|
499
|
+
const broke = results.filter((r) => r.blind && !r.refine).length
|
|
500
|
+
const pct = (x: number) => (n > 0 ? `${((x / n) * 100).toFixed(1)}%` : 'n/a')
|
|
501
|
+
|
|
502
|
+
console.log(`\n=== BLIND vs REFINE (n=${n}, rounds=${ROUNDS}) ===`)
|
|
503
|
+
console.log(` blind (pass@1): ${pct(nBlind)} (${nBlind}/${n})`)
|
|
504
|
+
console.log(` refine (final): ${pct(nRefine)} (${nRefine}/${n})`)
|
|
505
|
+
console.log(
|
|
506
|
+
` ► delta (refine − blind): ${(((nRefine - nBlind) / Math.max(n, 1)) * 100).toFixed(1)} pp [rescued ${rescued}, broke ${broke}]`,
|
|
507
|
+
)
|
|
508
|
+
console.log('\n per-task:')
|
|
509
|
+
for (const r of results) {
|
|
510
|
+
console.log(
|
|
511
|
+
` ${r.taskId}: blind=${r.blind ? '✓' : '·'} refine=${r.refine ? '✓' : '·'} rounds=[${r.rounds.map((x) => `r${x.round}:${x.resolved ? 'pass' : 'fail'}`).join(', ')}]`,
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
main().catch((err) => {
|
|
517
|
+
console.error(err instanceof Error ? err.stack ?? err.message : String(err))
|
|
518
|
+
process.exit(1)
|
|
519
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Step A (increment 1) of the trajectory-capture pipeline: turn a Claude Code session
|
|
2
|
+
// transcript (you driving the top-level agent) into the human-driving signal — the
|
|
3
|
+
// (intent, steer-move, fan-out) half — and emit FeedbackTrajectory-SHAPED records plus a
|
|
4
|
+
// driving-policy summary. This is the imitation corpus: "what Drew steers toward."
|
|
5
|
+
//
|
|
6
|
+
// Increment 1 = the MAIN transcript's human signal (intent turns, short steers, the
|
|
7
|
+
// explicit AskUserQuestion forks, and per-turn subagent/workflow fan-out). Increment 2
|
|
8
|
+
// joins the subagent/workflow subtrees + attaches outcomes, and wires agent-eval's real
|
|
9
|
+
// FeedbackTrajectory type + summarizePreferenceMemory. Plain Node ESM (streams big files).
|
|
10
|
+
//
|
|
11
|
+
// node trajectory-assemble.mjs [transcript.jsonl] [outDir]
|
|
12
|
+
|
|
13
|
+
import { createReadStream, mkdirSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { createInterface } from 'node:readline'
|
|
15
|
+
|
|
16
|
+
const TRANSCRIPT =
|
|
17
|
+
process.argv[2] ??
|
|
18
|
+
'/home/drew/.claude/projects/-home-drew-code-agent-runtime/52d557cf-89a5-45b8-b590-8c598fb378ab.jsonl'
|
|
19
|
+
const OUT_DIR = process.argv[3] ?? '/tmp/trajectory'
|
|
20
|
+
|
|
21
|
+
// A short imperative/approval turn is a STEER (a course-correction on the prior move);
|
|
22
|
+
// a longer turn is a fresh INTENT. Crude but matches how driving reads on the wire.
|
|
23
|
+
const STEER_MAX = 160
|
|
24
|
+
const STEER_HINT = /^(yes|yep|yeah|yalla|ok|okay|do it|go|sure|nah|no|stop|push|merge|continue|keep going|build it|ship it|lgtm|sounds good|great|perfect|exactly|right)/i
|
|
25
|
+
|
|
26
|
+
const intents = [] // { uuid, promptId, ts, text, kind: 'intent'|'steer' }
|
|
27
|
+
const asks = [] // { ts, promptId, toolUseId, questions:[{q,header,options[]}], chosen }
|
|
28
|
+
const spawnsByPrompt = new Map() // promptId -> { agents, workflows }
|
|
29
|
+
const toolCounts = new Map()
|
|
30
|
+
let assistantTurns = 0
|
|
31
|
+
|
|
32
|
+
const bump = (m, k) => m.set(k, (m.get(k) ?? 0) + 1)
|
|
33
|
+
|
|
34
|
+
const rl = createInterface({ input: createReadStream(TRANSCRIPT), crlfDelay: Infinity })
|
|
35
|
+
for await (const line of rl) {
|
|
36
|
+
if (!line.trim()) continue
|
|
37
|
+
let r
|
|
38
|
+
try {
|
|
39
|
+
r = JSON.parse(line)
|
|
40
|
+
} catch {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const c = r?.message?.content
|
|
44
|
+
if (r.type === 'user' && typeof c === 'string' && r.isSidechain !== true) {
|
|
45
|
+
const text = c.trim()
|
|
46
|
+
if (!text) continue
|
|
47
|
+
const kind = text.length <= STEER_MAX && STEER_HINT.test(text) ? 'steer' : 'intent'
|
|
48
|
+
intents.push({ uuid: r.uuid, promptId: r.promptId, ts: r.timestamp, text, kind })
|
|
49
|
+
} else if (r.type === 'assistant' && Array.isArray(c)) {
|
|
50
|
+
assistantTurns += 1
|
|
51
|
+
for (const b of c) {
|
|
52
|
+
if (b?.type !== 'tool_use') continue
|
|
53
|
+
bump(toolCounts, b.name)
|
|
54
|
+
if (b.name === 'Agent' || b.name === 'Workflow') {
|
|
55
|
+
const pid = r.promptId ?? 'unknown'
|
|
56
|
+
const cur = spawnsByPrompt.get(pid) ?? { agents: 0, workflows: 0 }
|
|
57
|
+
if (b.name === 'Agent') cur.agents += 1
|
|
58
|
+
else cur.workflows += 1
|
|
59
|
+
spawnsByPrompt.set(pid, cur)
|
|
60
|
+
}
|
|
61
|
+
if (b.name === 'AskUserQuestion') {
|
|
62
|
+
const questions = (b.input?.questions ?? []).map((q) => ({
|
|
63
|
+
q: q.question,
|
|
64
|
+
header: q.header,
|
|
65
|
+
options: (q.options ?? []).map((o) => o.label),
|
|
66
|
+
}))
|
|
67
|
+
asks.push({ ts: r.timestamp, promptId: r.promptId, toolUseId: b.id, questions, chosen: null })
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} else if (r.type === 'user' && Array.isArray(c)) {
|
|
71
|
+
for (const b of c) {
|
|
72
|
+
if (b?.type !== 'tool_result') continue
|
|
73
|
+
const tur = r.toolUseResult
|
|
74
|
+
const a = asks.find((x) => x.toolUseId === b.tool_use_id && x.chosen === null)
|
|
75
|
+
if (a && tur && (tur.answers || tur.metadata)) a.chosen = tur.answers ?? null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const steers = intents.filter((i) => i.kind === 'steer')
|
|
81
|
+
const realIntents = intents.filter((i) => i.kind === 'intent')
|
|
82
|
+
const fanouts = [...spawnsByPrompt.entries()]
|
|
83
|
+
.map(([pid, v]) => ({ pid, ...v, total: v.agents + v.workflows }))
|
|
84
|
+
.filter((x) => x.total >= 2)
|
|
85
|
+
.sort((a, b) => b.total - a.total)
|
|
86
|
+
|
|
87
|
+
// FeedbackTrajectory-SHAPED records (one per human turn) — the imitation corpus root nodes.
|
|
88
|
+
const trajectory = intents.map((i) => ({
|
|
89
|
+
id: i.uuid,
|
|
90
|
+
createdAt: i.ts,
|
|
91
|
+
task: { intent: i.text },
|
|
92
|
+
labels: [{ source: 'user', kind: i.kind, value: i.text }],
|
|
93
|
+
tags: { promptId: i.promptId, fanout: spawnsByPrompt.get(i.promptId) ?? null },
|
|
94
|
+
attempts: [], // increment 2: join the assistant actions + subagent subtrees here
|
|
95
|
+
outcome: null, // increment 2: label success (verifiable reward / later approval)
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
mkdirSync(OUT_DIR, { recursive: true })
|
|
99
|
+
writeFileSync(`${OUT_DIR}/trajectory.jsonl`, `${trajectory.map((t) => JSON.stringify(t)).join('\n')}\n`)
|
|
100
|
+
|
|
101
|
+
const top = (m, n) =>
|
|
102
|
+
[...m.entries()].sort((a, b) => b[1] - a[1]).slice(0, n).map(([k, v]) => `${k}:${v}`).join(' ')
|
|
103
|
+
|
|
104
|
+
console.log(`\n=== DRIVING-POLICY EXTRACT (${TRANSCRIPT.split('/').pop()}) ===`)
|
|
105
|
+
console.log(`human turns: ${intents.length} (intents ${realIntents.length} · steers ${steers.length})`)
|
|
106
|
+
console.log(`assistant turns: ${assistantTurns} tool calls: ${top(toolCounts, 12)}`)
|
|
107
|
+
console.log(`explicit forks (AskUserQuestion): ${asks.length} parallel fan-out turns (>=2 spawns): ${fanouts.length}`)
|
|
108
|
+
|
|
109
|
+
console.log(`\n--- the explicit decisions you made at forks (your steering policy, labeled) ---`)
|
|
110
|
+
for (const a of asks.slice(0, 30)) {
|
|
111
|
+
for (const q of a.questions) {
|
|
112
|
+
const chosenLabel = a.chosen ? Object.values(a.chosen).map((v) => (typeof v === 'string' ? v : JSON.stringify(v))).join(' / ') : '(unmatched)'
|
|
113
|
+
console.log(` [${q.header ?? '?'}] ${String(q.q).slice(0, 80)}`)
|
|
114
|
+
console.log(` options: ${q.options.join(' | ')}`)
|
|
115
|
+
console.log(` CHOSE: ${chosenLabel}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`\n--- your recurring short steers (the corrections you repeat) ---`)
|
|
120
|
+
const steerCounts = new Map()
|
|
121
|
+
for (const s of steers) bump(steerCounts, s.text.toLowerCase().slice(0, 40))
|
|
122
|
+
for (const [phrase, n] of [...steerCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)) {
|
|
123
|
+
console.log(` ${n}x ${phrase}`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`\n--- biggest parallel fan-outs (one intent → N concurrent agents/workflows) ---`)
|
|
127
|
+
for (const f of fanouts.slice(0, 8)) console.log(` ${f.total} spawns (agents ${f.agents}, workflows ${f.workflows}) promptId ${String(f.pid).slice(0, 12)}`)
|
|
128
|
+
|
|
129
|
+
console.log(`\nwrote ${trajectory.length} FeedbackTrajectory-shaped records → ${OUT_DIR}/trajectory.jsonl`)
|
|
130
|
+
console.log(`(increment 2: join subagent/workflow subtrees + outcome labels + agent-eval FeedbackTrajectory/summarizePreferenceMemory)`)
|