@tangle-network/agent-bench 0.7.1 → 0.8.1
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/CHANGELOG.md +12 -0
- package/HARNESS.md +2 -2
- package/dist/adapters.js +6 -2
- package/dist/adapters.js.map +1 -1
- package/dist/benchmarks/appworld.d.ts +61 -2
- package/dist/benchmarks/appworld.js +111 -40
- package/dist/benchmarks/appworld.js.map +1 -1
- package/dist/benchmarks/cadbench.js +1 -1
- package/dist/benchmarks/cadgenbench.js +1 -1
- package/dist/benchmarks/finresearchbench.js +16 -22
- package/dist/benchmarks/finresearchbench.js.map +1 -1
- package/dist/benchmarks/finsearchcomp.js +16 -22
- package/dist/benchmarks/finsearchcomp.js.map +1 -1
- package/dist/benchmarks/frames.js +18 -20
- package/dist/benchmarks/frames.js.map +1 -1
- package/dist/benchmarks/mcad-bench.d.ts +106 -0
- package/dist/benchmarks/mcad-bench.js +569 -0
- package/dist/benchmarks/mcad-bench.js.map +1 -0
- package/dist/benchmarks/mcad-cq-bench.d.ts +82 -0
- package/dist/benchmarks/mcad-cq-bench.js +339 -0
- package/dist/benchmarks/mcad-cq-bench.js.map +1 -0
- package/dist/benchmarks/mcad-cq-golds.d.ts +36 -0
- package/dist/benchmarks/mcad-cq-golds.js +342 -0
- package/dist/benchmarks/mcad-cq-golds.js.map +1 -0
- package/dist/benchmarks/mcad-golds.d.ts +20 -0
- package/dist/benchmarks/mcad-golds.js +318 -0
- package/dist/benchmarks/mcad-golds.js.map +1 -0
- package/dist/benchmarks/mcad-tasks.d.ts +66 -0
- package/dist/benchmarks/mcad-tasks.js +508 -0
- package/dist/benchmarks/mcad-tasks.js.map +1 -0
- package/dist/benchmarks/simpleqa.js +15 -19
- package/dist/benchmarks/simpleqa.js.map +1 -1
- package/dist/benchmarks/trata-hedge.js +20 -23
- package/dist/benchmarks/trata-hedge.js.map +1 -1
- package/dist/{cadbench-BrpwOU6A.js → cadbench-BLSyxR1N.js} +21 -20
- package/dist/cadbench-BLSyxR1N.js.map +1 -0
- package/dist/{cadgenbench-DF7hYHdl.js → cadgenbench-x2OFkf8y.js} +2 -2
- package/dist/cadgenbench-x2OFkf8y.js.map +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +10 -9
- package/dist/index.js.map +1 -1
- package/dist/router-turn-C2wMiDoo.js +31 -0
- package/dist/router-turn-C2wMiDoo.js.map +1 -0
- package/package.json +7 -7
- package/scripts/appworld_driver.py +1 -170
- package/scripts/run-package-tests.mjs +56 -36
- package/scripts/run-package-tests.test.mjs +33 -0
- package/scripts/trata-hedge/README.md +3 -3
- package/scripts/trata-hedge/run.sh +6 -2
- package/src/adapters.ts +11 -0
- package/src/aec-gate.mts +30 -9
- package/src/agent-graphs-gen2.mts +523 -0
- package/src/agent-graphs-gen3.mts +660 -0
- package/src/agent-graphs-improve/offline-seams.mts +128 -0
- package/src/agent-graphs-improve.mts +747 -0
- package/src/atom-humaneval.mts +76 -55
- package/src/atom-mcp-e2e.mts +27 -13
- package/src/benchmarks/appworld.test.mts +119 -1
- package/src/benchmarks/appworld.ts +156 -33
- package/src/benchmarks/cadbench.ts +21 -8
- package/src/benchmarks/finresearchbench.ts +20 -16
- package/src/benchmarks/finsearchcomp.ts +20 -20
- package/src/benchmarks/frames.ts +19 -18
- package/src/benchmarks/mcad-bench.test.mts +455 -0
- package/src/benchmarks/mcad-bench.ts +561 -0
- package/src/benchmarks/mcad-cq-bench.ts +423 -0
- package/src/benchmarks/mcad-cq-golds.ts +374 -0
- package/src/benchmarks/mcad-cq.test.mts +386 -0
- package/src/benchmarks/mcad-golds.ts +359 -0
- package/src/benchmarks/mcad-tasks.ts +490 -0
- package/src/benchmarks/simpleqa.ts +19 -17
- package/src/benchmarks/trata-hedge.ts +26 -18
- package/src/browser/ui-reviewer.ts +1 -1
- package/src/clbench-context-gate.mts +45 -9
- package/src/cloud-loop.mts +24 -6
- package/src/commit0-env-run.mts +6 -3
- package/src/commit0-env.ts +2 -3
- package/src/commit0-gate.mts +53 -24
- package/src/corpus.ts +3 -3
- package/src/david-attribution.mts +28 -9
- package/src/david-goliath.mts +31 -12
- package/src/egress-probe.mts +3 -3
- package/src/examples/lean-proof-gate.mts +10 -3
- package/src/examples/math-demo.mts +10 -3
- package/src/examples/strategy-demo.mts +10 -4
- package/src/fleet.mts +30 -8
- package/src/gate-cli.mts +9 -4
- package/src/gate.test.mts +7 -2
- package/src/gate.ts +23 -75
- package/src/generate-eval/certify.ts +13 -5
- package/src/hev-eval.mts +39 -10
- package/src/hev-improve.mts +28 -16
- package/src/hev-structural.mts +28 -20
- package/src/humaneval-gate.mts +24 -8
- package/src/humaneval-object-ablation.mts +52 -14
- package/src/humaneval-repair-gate.mts +53 -14
- package/src/mbpp-structural.mts +28 -20
- package/src/mcp-mount-probe.mts +23 -11
- package/src/official-optimizer-config.mts +61 -12
- package/src/official-optimizer-config.test.mts +72 -3
- package/src/quant-arena/profiles/quant-researcher.profile.json +6 -1
- package/src/quant-arena/quant-loop.mts +88 -61
- package/src/research-shot.ts +21 -11
- package/src/resolve-client.ts +6 -8
- package/src/rollout-ledger/backfill-swe-arena.test.mts +7 -2
- package/src/router-executor.ts +1 -1
- package/src/router-turn.ts +135 -0
- package/src/run-benchmarks.ts +7 -2
- package/src/sandbox-run.ts +23 -41
- package/src/search-bench/bridge.ts +66 -8
- package/src/search-bench/parametric-check.mts +19 -4
- package/src/search-bench/profiles.ts +24 -9
- package/src/search-bench/run.mts +3 -4
- package/src/smoke-structural-rollout.mts +21 -5
- package/src/supervisor-arena.mts +28 -20
- package/src/swe-arena/arms.ts +73 -15
- package/src/swe-arena/capacity.ts +44 -41
- package/src/swe-arena/diagnosis-ensemble.ts +51 -48
- package/src/swe-arena/execution.test.mts +12 -12
- package/src/swe-arena/gepa-seat.mts +12 -5
- package/src/swe-arena/gepa-seat.test.mts +45 -33
- package/src/swe-arena/outer-loop.mts +94 -70
- package/src/swe-arena/premeasured-from-cells.mts +16 -1
- package/src/swe-arena/premeasured-from-cells.test.mts +21 -1
- package/src/swe-arena/profiles/deepseek-author.profile.json +12 -0
- package/src/swe-arena/profiles/default-author.profile.json +9 -1
- package/src/swe-arena/proposer-fanout.mts +52 -99
- package/src/swe-arena/proposer-fanout.test.mts +92 -57
- package/src/swe-arena/proposer-provenance.mts +1 -1
- package/src/swe-code-improve.mts +30 -52
- package/src/swe-emit-patch.mts +26 -5
- package/src/swe-improve.mts +27 -8
- package/src/swe-jail.test.ts +128 -0
- package/src/swe-jail.ts +106 -22
- package/src/swe-local-proof.mts +27 -5
- package/src/swe-repro-calibrate.mts +5 -0
- package/src/swe-self-improve.mts +45 -10
- package/src/swe-stream.mts +68 -6
- package/src/swe-structural.mts +46 -7
- package/src/tb-container-executor.mts +38 -6
- package/src/tb-container-executor.test.mts +11 -3
- package/src/tb-supervisor-sidecar.mts +11 -27
- package/src/terminal-compare.ts +38 -18
- package/src/trata-gate.mts +26 -26
- package/src/trata-gepa.mts +25 -16
- package/src/trata-hedge-solve.mts +80 -0
- package/src/worker-blender.ts +21 -11
- package/src/worker-browser.ts +18 -5
- package/src/worker-build123d.ts +17 -8
- package/src/worker-cad.ts +31 -12
- package/src/worker.ts +1 -1
- package/tb_agents/opencode_router_agent.py +84 -42
- package/dist/cadbench-BrpwOU6A.js.map +0 -1
- package/dist/cadgenbench-DF7hYHdl.js.map +0 -1
- package/scripts/trata-hedge/solve.py +0 -83
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codemode-skill improvement harness — the BASELINE half of skills/agent-graphs/IMPROVE.md.
|
|
3
|
+
*
|
|
4
|
+
* The improving artifact is the skill TEXT (`skills/agent-graphs/SKILL.md`), a `MutableSurface`
|
|
5
|
+
* string. This file owns exactly the two slots the agent-eval machinery leaves to the caller:
|
|
6
|
+
*
|
|
7
|
+
* • closure A (`dispatchWithSurface`-compatible): author an agent graph from a loose case
|
|
8
|
+
* brief, carrying the full skill text; if the author decides "graph", LOWER it to a real
|
|
9
|
+
* `AgentGraph` and execute it OFFLINE via `runGraph` (scripted brain + stub leaf seam, the
|
|
10
|
+
* `examples/graphs/` pattern) — a validation refusal is captured as data, never papered over.
|
|
11
|
+
* • closure B (`JudgeConfig`-compatible deterministic scorer): map each case's `expect` block
|
|
12
|
+
* to mechanical checks over the authored artifact + the offline edge ledger; partial credit
|
|
13
|
+
* per satisfied expectation, equal weights.
|
|
14
|
+
*
|
|
15
|
+
* Baseline run: pnpm tsx src/agent-graphs-improve.mts (from bench/)
|
|
16
|
+
* Writes skills/agent-graphs/generations/gen1-baseline.json and prints the per-case table.
|
|
17
|
+
*
|
|
18
|
+
* Author model: tangle-router glm-5.2, temperature 0.2, one retry on unparseable JSON.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from 'node:child_process'
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
23
|
+
import { join, dirname, resolve as resolvePath } from 'node:path'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
import {
|
|
26
|
+
defineInlineResource,
|
|
27
|
+
harnessTypeSchema,
|
|
28
|
+
reasoningEffortSchema,
|
|
29
|
+
type AgentProfile,
|
|
30
|
+
} from '@tangle-network/agent-interface'
|
|
31
|
+
import {
|
|
32
|
+
type AgentGraph,
|
|
33
|
+
type AnalystRegistry,
|
|
34
|
+
collectAgentTurn,
|
|
35
|
+
createExecutor,
|
|
36
|
+
defaultEdgeTraversalCap,
|
|
37
|
+
type EdgeTraversal,
|
|
38
|
+
GraphEdgeCapError,
|
|
39
|
+
type GraphResult,
|
|
40
|
+
promptHandle,
|
|
41
|
+
streamAgentTurn,
|
|
42
|
+
} from '../../src/runtime/index.ts'
|
|
43
|
+
import { type RunGraphTestOptions, runGraphWithTestBrain } from '../../src/testing/index.ts'
|
|
44
|
+
import { leafSeam, scriptedBrain, type ScriptedTurn } from './agent-graphs-improve/offline-seams.mts'
|
|
45
|
+
|
|
46
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
47
|
+
const REPO = join(HERE, '..', '..')
|
|
48
|
+
// The skill was re-homed skills/codemode → skills/agent-graphs (925460fe); these paths follow it.
|
|
49
|
+
const SKILL_PATH = join(REPO, 'skills', 'agent-graphs', 'SKILL.md')
|
|
50
|
+
const CASES_DIR = join(REPO, 'skills', 'agent-graphs', 'cases')
|
|
51
|
+
const OUT_PATH = join(REPO, 'skills', 'agent-graphs', 'generations', 'gen1-baseline.json')
|
|
52
|
+
// The v1 surface + cases were removed from the working tree by 5b8d4da5 ("replace codemode plan
|
|
53
|
+
// with current graph guide"); the baseline still measures the v1 text, pinned in git history.
|
|
54
|
+
// Override with SKILL_REF to measure another committed version.
|
|
55
|
+
const SKILL_REF = process.env.SKILL_REF ?? 'afb40bc1'
|
|
56
|
+
|
|
57
|
+
function gitShow(ref: string, path: string): string {
|
|
58
|
+
return execFileSync('git', ['show', `${ref}:${path}`], { cwd: REPO, encoding: 'utf8' })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function gitLs(ref: string, path: string): string[] {
|
|
62
|
+
return execFileSync('git', ['ls-tree', '--name-only', ref, `${path}/`], {
|
|
63
|
+
cwd: REPO,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
})
|
|
66
|
+
.split('\n')
|
|
67
|
+
.filter((l) => l.endsWith('.json'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The surface + cases: from the working tree when present, else pinned from git history. */
|
|
71
|
+
export function loadInputs(): { surface: string; cases: CaseSpec[]; source: string } {
|
|
72
|
+
if (existsSync(SKILL_PATH)) {
|
|
73
|
+
const surface = readFileSync(SKILL_PATH, 'utf8')
|
|
74
|
+
const cases = readdirSync(CASES_DIR)
|
|
75
|
+
.filter((f) => f.endsWith('.json'))
|
|
76
|
+
.sort()
|
|
77
|
+
.map((f) => JSON.parse(readFileSync(join(CASES_DIR, f), 'utf8')) as CaseSpec)
|
|
78
|
+
return { surface, cases, source: 'working-tree' }
|
|
79
|
+
}
|
|
80
|
+
const surface = gitShow(SKILL_REF, 'skills/agent-graphs/SKILL.md')
|
|
81
|
+
const cases = gitLs(SKILL_REF, 'skills/agent-graphs/cases')
|
|
82
|
+
.sort()
|
|
83
|
+
.map((p) => JSON.parse(gitShow(SKILL_REF, p)) as CaseSpec)
|
|
84
|
+
return { surface, cases, source: `git:${SKILL_REF}` }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Case + artifact shapes ─────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export interface CaseExpect {
|
|
90
|
+
correctAnswerIsNoGraph?: boolean
|
|
91
|
+
correctAnswerIsDynamicWorkflow?: boolean
|
|
92
|
+
correctAnswerIsGraph?: boolean
|
|
93
|
+
nodes?: number
|
|
94
|
+
analyzesWarranted?: boolean
|
|
95
|
+
maxTraversalsAtLeast?: number
|
|
96
|
+
deliverableDescribeCarriesMission?: boolean
|
|
97
|
+
checkIsMechanical?: boolean
|
|
98
|
+
trapIsAnalyzesCapAsStop?: boolean
|
|
99
|
+
correctStopIsDelegatesCapOrDeliverable?: boolean
|
|
100
|
+
wrongIfAnalystIsNode?: boolean
|
|
101
|
+
edges?: string[]
|
|
102
|
+
reason?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface CaseSpec {
|
|
106
|
+
id: string
|
|
107
|
+
brief: string
|
|
108
|
+
expect: CaseExpect
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type AuthoredEdge =
|
|
112
|
+
| { kind: 'delegates'; from: string; to: string; maxTraversals?: number }
|
|
113
|
+
| { kind: 'analyzes'; analyst: string; over: string[]; to: string; maxTraversals?: number }
|
|
114
|
+
|
|
115
|
+
export interface AuthoredGraphSpec {
|
|
116
|
+
nodes: Array<{ id: string; systemPrompt: string }>
|
|
117
|
+
edges: AuthoredEdge[]
|
|
118
|
+
budget: { maxIterations: number; maxTokens: number }
|
|
119
|
+
perWorker?: { maxIterations?: number; maxTokens?: number }
|
|
120
|
+
deliverableDescribe: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type Decision = 'graph' | 'single-agent' | 'dynamic-workflow'
|
|
124
|
+
|
|
125
|
+
export interface OfflineRunSummary {
|
|
126
|
+
resultKind: string
|
|
127
|
+
ledger: ReadonlyArray<EdgeTraversal>
|
|
128
|
+
exhaustedEdges: ReadonlyArray<string>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Closure A's output: the authored artifact, plus the offline run evidence when one ran. */
|
|
132
|
+
export interface AuthoredArtifact {
|
|
133
|
+
decision: Decision
|
|
134
|
+
reason: string
|
|
135
|
+
graph?: AuthoredGraphSpec
|
|
136
|
+
run?: OfflineRunSummary
|
|
137
|
+
/** A `runGraph`/`validateGraph` refusal (or offline-run fault) — captured as data. */
|
|
138
|
+
validationError?: string
|
|
139
|
+
/** The author model's raw reply, retained for audit. */
|
|
140
|
+
raw: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── The author model call ──────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function positiveInteger(name: string, raw: string | undefined, fallback: number): number {
|
|
146
|
+
const value = raw === undefined ? fallback : Number(raw)
|
|
147
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
148
|
+
throw new Error(`${name} must be a positive integer`)
|
|
149
|
+
}
|
|
150
|
+
return value
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function authorPrompt(kase: CaseSpec): string {
|
|
154
|
+
return [
|
|
155
|
+
`<case-brief id="${kase.id}">`,
|
|
156
|
+
kase.brief,
|
|
157
|
+
'</case-brief>',
|
|
158
|
+
'',
|
|
159
|
+
'First decide the dialect per the skill. Then reply with JSON ONLY — no markdown fences, no prose outside the JSON:',
|
|
160
|
+
'{"decision":"graph"|"single-agent"|"dynamic-workflow","reason":string,"graph"?:{...}}',
|
|
161
|
+
'',
|
|
162
|
+
'Include "graph" if and only if decision is "graph", with this exact shape:',
|
|
163
|
+
'{"nodes":[{"id":string,"systemPrompt":string}, ...],',
|
|
164
|
+
' "edges":[{"kind":"delegates","from":string,"to":string,"maxTraversals"?:number}',
|
|
165
|
+
' | {"kind":"analyzes","analyst":string,"over":[string,...],"to":string,"maxTraversals"?:number}, ...],',
|
|
166
|
+
' "budget":{"maxIterations":number,"maxTokens":number},',
|
|
167
|
+
' "perWorker"?:{"maxIterations"?:number,"maxTokens"?:number},',
|
|
168
|
+
' "deliverableDescribe":string}',
|
|
169
|
+
'',
|
|
170
|
+
'Rules: the root node must be listed first in "nodes"; every delegates edge originates at the root;',
|
|
171
|
+
'analysts are registry lens ids, never node ids; "deliverableDescribe" is the driver\'s real mission text.',
|
|
172
|
+
].join('\n')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extractJson(text: string): string {
|
|
176
|
+
const stripped = text.replace(/```(?:json)?/g, '').trim()
|
|
177
|
+
const start = stripped.indexOf('{')
|
|
178
|
+
const end = stripped.lastIndexOf('}')
|
|
179
|
+
if (start === -1 || end <= start) throw new Error('no JSON object found in author reply')
|
|
180
|
+
return stripped.slice(start, end + 1)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function buildAgentGraphsAuthorProfile(
|
|
184
|
+
surface: string,
|
|
185
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
186
|
+
): AgentProfile {
|
|
187
|
+
return {
|
|
188
|
+
name: env.AGENT_GRAPHS_AUTHOR_PROFILE_NAME ?? 'agent-graphs-author',
|
|
189
|
+
harness: harnessTypeSchema.parse(env.AGENT_GRAPHS_AUTHOR_HARNESS ?? 'pi'),
|
|
190
|
+
model: {
|
|
191
|
+
provider: env.AGENT_GRAPHS_AUTHOR_PROVIDER ?? 'tangle-router',
|
|
192
|
+
default: env.AGENT_GRAPHS_AUTHOR_MODEL ?? 'deepseek-v4-flash',
|
|
193
|
+
reasoningEffort: reasoningEffortSchema.parse(
|
|
194
|
+
env.AGENT_GRAPHS_AUTHOR_REASONING_EFFORT ?? 'ultracode',
|
|
195
|
+
),
|
|
196
|
+
},
|
|
197
|
+
prompt: {
|
|
198
|
+
systemPrompt:
|
|
199
|
+
env.AGENT_GRAPHS_AUTHOR_SYSTEM_PROMPT ??
|
|
200
|
+
'Apply the attached agent-graphs skill exactly. Return only the requested artifact.',
|
|
201
|
+
},
|
|
202
|
+
resources: {
|
|
203
|
+
failOnError: true,
|
|
204
|
+
skills: [defineInlineResource('agent-graphs', surface)],
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function callAuthor(
|
|
210
|
+
profile: AgentProfile,
|
|
211
|
+
prompt: string,
|
|
212
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
213
|
+
): Promise<string> {
|
|
214
|
+
const bridgeBearer = env.AGENT_GRAPHS_BRIDGE_BEARER ?? env.BRIDGE_BEARER
|
|
215
|
+
if (!bridgeBearer) throw new Error('AGENT_GRAPHS_BRIDGE_BEARER or BRIDGE_BEARER is required')
|
|
216
|
+
const factory = createExecutor({
|
|
217
|
+
backend: 'bridge',
|
|
218
|
+
bridgeUrl: env.AGENT_GRAPHS_BRIDGE_URL ?? env.BRIDGE_URL ?? 'http://127.0.0.1:3355',
|
|
219
|
+
bridgeBearer,
|
|
220
|
+
})
|
|
221
|
+
const timeoutRaw = env.AGENT_GRAPHS_AUTHOR_TIMEOUT_MS
|
|
222
|
+
const timeoutMs =
|
|
223
|
+
timeoutRaw === undefined
|
|
224
|
+
? undefined
|
|
225
|
+
: positiveInteger('AGENT_GRAPHS_AUTHOR_TIMEOUT_MS', timeoutRaw, 1)
|
|
226
|
+
const turn = await collectAgentTurn(
|
|
227
|
+
streamAgentTurn(
|
|
228
|
+
{ kind: 'executor', factory, profile, agentRunName: profile.name ?? 'agent-graphs-author' },
|
|
229
|
+
prompt,
|
|
230
|
+
timeoutMs === undefined ? {} : { timeoutMs },
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
if (turn.status !== 'completed') {
|
|
234
|
+
throw new Error(turn.error?.message ?? `author turn ended with status ${turn.status}`)
|
|
235
|
+
}
|
|
236
|
+
if (!turn.finalText.trim()) throw new Error('author returned empty content')
|
|
237
|
+
return turn.finalText
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
interface AuthoredReply {
|
|
241
|
+
decision: Decision
|
|
242
|
+
reason: string
|
|
243
|
+
graph?: AuthoredGraphSpec
|
|
244
|
+
raw: string
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Prompt the author; one retry on unparseable JSON (or a transport fault). */
|
|
248
|
+
async function authorOnce(surface: string, kase: CaseSpec): Promise<AuthoredReply> {
|
|
249
|
+
const prompt = authorPrompt(kase)
|
|
250
|
+
const profile = buildAgentGraphsAuthorProfile(surface)
|
|
251
|
+
const attempts = positiveInteger(
|
|
252
|
+
'AGENT_GRAPHS_AUTHOR_ATTEMPTS',
|
|
253
|
+
process.env.AGENT_GRAPHS_AUTHOR_ATTEMPTS,
|
|
254
|
+
2,
|
|
255
|
+
)
|
|
256
|
+
let lastErr: unknown
|
|
257
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
258
|
+
try {
|
|
259
|
+
const raw = await callAuthor(profile, prompt)
|
|
260
|
+
const parsed = JSON.parse(extractJson(raw)) as {
|
|
261
|
+
decision?: string
|
|
262
|
+
reason?: string
|
|
263
|
+
graph?: AuthoredGraphSpec
|
|
264
|
+
}
|
|
265
|
+
const decision = parsed.decision
|
|
266
|
+
if (decision !== 'graph' && decision !== 'single-agent' && decision !== 'dynamic-workflow') {
|
|
267
|
+
throw new Error(`author decision '${String(decision)}' is not in the contract`)
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
decision,
|
|
271
|
+
reason: typeof parsed.reason === 'string' ? parsed.reason : '',
|
|
272
|
+
...(parsed.graph !== undefined ? { graph: parsed.graph } : {}),
|
|
273
|
+
raw,
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
lastErr = err
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
throw new Error(
|
|
280
|
+
`author failed after ${attempts} attempt${attempts === 1 ? '' : 's'}: ${String((lastErr as Error)?.message ?? lastErr)}`,
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Lowering an authored spec to a real AgentGraph + offline execution ─────────
|
|
285
|
+
|
|
286
|
+
function findRootId(spec: AuthoredGraphSpec): string {
|
|
287
|
+
const delegatesTo = new Set(
|
|
288
|
+
spec.edges.filter((e) => e.kind === 'delegates').map((e) => (e as { to: string }).to),
|
|
289
|
+
)
|
|
290
|
+
const candidates = spec.nodes.filter((n) => !delegatesTo.has(n.id))
|
|
291
|
+
const fromIds = new Set(
|
|
292
|
+
spec.edges.filter((e) => e.kind === 'delegates').map((e) => (e as { from: string }).from),
|
|
293
|
+
)
|
|
294
|
+
const root = candidates.find((n) => fromIds.has(n.id)) ?? candidates[0] ?? spec.nodes[0]
|
|
295
|
+
if (!root) throw new Error('authored graph has no nodes')
|
|
296
|
+
return root.id
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Execute the authored graph offline: scripted driver (spawn each worker once, await each
|
|
300
|
+
* settle, then finish) + stub leaves + in-memory journal/blobs. */
|
|
301
|
+
async function runAuthoredOffline(spec: AuthoredGraphSpec, runId: string): Promise<OfflineRunSummary> {
|
|
302
|
+
const rootId = findRootId(spec)
|
|
303
|
+
const workerIds = spec.nodes.map((n) => n.id).filter((id) => id !== rootId)
|
|
304
|
+
|
|
305
|
+
const graph: AgentGraph = {
|
|
306
|
+
nodes: spec.nodes.map((n) => ({
|
|
307
|
+
id: n.id,
|
|
308
|
+
profile: { name: n.id, prompt: { systemPrompt: n.systemPrompt } },
|
|
309
|
+
})),
|
|
310
|
+
edges: spec.edges.map((e) =>
|
|
311
|
+
e.kind === 'delegates'
|
|
312
|
+
? {
|
|
313
|
+
kind: 'delegates' as const,
|
|
314
|
+
from: e.from,
|
|
315
|
+
to: e.to,
|
|
316
|
+
directive: promptHandle('delegates/worker-brief/v1'),
|
|
317
|
+
...(e.maxTraversals !== undefined ? { maxTraversals: e.maxTraversals } : {}),
|
|
318
|
+
}
|
|
319
|
+
: {
|
|
320
|
+
kind: 'analyzes' as const,
|
|
321
|
+
analyst: e.analyst,
|
|
322
|
+
over: e.over,
|
|
323
|
+
to: e.to,
|
|
324
|
+
directive: promptHandle('analyzes/findings-report/v1'),
|
|
325
|
+
...(e.maxTraversals !== undefined ? { maxTraversals: e.maxTraversals } : {}),
|
|
326
|
+
},
|
|
327
|
+
),
|
|
328
|
+
// Offline stamp — the ledger is what we score, not deliverable quality.
|
|
329
|
+
deliverable: { describe: spec.deliverableDescribe, check: (out) => out !== undefined },
|
|
330
|
+
budget: spec.budget,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Lenses for whatever analyst ids the author named: ENVIRONMENT, never nodes.
|
|
334
|
+
const analystIds = [
|
|
335
|
+
...new Set(
|
|
336
|
+
spec.edges.filter((e) => e.kind === 'analyzes').map((e) => (e as { analyst: string }).analyst),
|
|
337
|
+
),
|
|
338
|
+
]
|
|
339
|
+
const analysts: AnalystRegistry | undefined =
|
|
340
|
+
analystIds.length > 0
|
|
341
|
+
? {
|
|
342
|
+
kinds: analystIds.map((id) => ({
|
|
343
|
+
id,
|
|
344
|
+
description: `offline stub lens '${id}'`,
|
|
345
|
+
area: 'review',
|
|
346
|
+
})),
|
|
347
|
+
run: async () => [{ claim: 'offline stub finding', severity: 'minor' }],
|
|
348
|
+
}
|
|
349
|
+
: undefined
|
|
350
|
+
|
|
351
|
+
const received: AgentProfile[] = []
|
|
352
|
+
const turns: ScriptedTurn[] = [
|
|
353
|
+
{
|
|
354
|
+
toolCalls: workerIds.map((id) => ({
|
|
355
|
+
name: 'spawn_agent',
|
|
356
|
+
arguments: { profile: { name: id }, task: `work the '${id}' role` },
|
|
357
|
+
})),
|
|
358
|
+
},
|
|
359
|
+
...workerIds.map(() => ({ toolCalls: [{ name: 'await_event', arguments: {} }] })),
|
|
360
|
+
{ content: 'done' },
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
const perWorker =
|
|
364
|
+
spec.perWorker === undefined
|
|
365
|
+
? undefined
|
|
366
|
+
: {
|
|
367
|
+
maxIterations:
|
|
368
|
+
spec.perWorker.maxIterations ?? Math.max(1, Math.floor(spec.budget.maxIterations / 4)),
|
|
369
|
+
maxTokens:
|
|
370
|
+
spec.perWorker.maxTokens ?? Math.max(1, Math.floor(spec.budget.maxTokens / 4)),
|
|
371
|
+
}
|
|
372
|
+
const opts: RunGraphTestOptions = {
|
|
373
|
+
runId,
|
|
374
|
+
maxLiveWorkers: Math.max(workerIds.length, 1),
|
|
375
|
+
...(perWorker !== undefined ? { perWorker } : {}),
|
|
376
|
+
...(analysts !== undefined ? { analysts } : {}),
|
|
377
|
+
makeWorkerAgent: leafSeam(
|
|
378
|
+
received,
|
|
379
|
+
Object.fromEntries(workerIds.map((id) => [id, { withTrace: true }])),
|
|
380
|
+
),
|
|
381
|
+
brain: scriptedBrain(turns),
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const timeoutMs = positiveInteger(
|
|
385
|
+
'AGENT_GRAPHS_OFFLINE_TIMEOUT_MS',
|
|
386
|
+
process.env.AGENT_GRAPHS_OFFLINE_TIMEOUT_MS,
|
|
387
|
+
120_000,
|
|
388
|
+
)
|
|
389
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
390
|
+
const t = setTimeout(
|
|
391
|
+
() => reject(new Error(`offline run timed out (${timeoutMs}ms)`)),
|
|
392
|
+
timeoutMs,
|
|
393
|
+
)
|
|
394
|
+
t.unref?.()
|
|
395
|
+
})
|
|
396
|
+
const res: GraphResult = await Promise.race([runGraphWithTestBrain(graph, opts), timeout])
|
|
397
|
+
return { resultKind: res.result.kind, ledger: res.ledger, exhaustedEdges: res.exhaustedEdges }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** CLOSURE A — `dispatchWithSurface(surface, scenario)`: author from the skill text, lower,
|
|
401
|
+
* execute offline. A refusal is data (`validationError`), never a crash. */
|
|
402
|
+
export async function dispatchWithSurface(surface: string, scenario: CaseSpec): Promise<AuthoredArtifact> {
|
|
403
|
+
const reply = await authorOnce(surface, scenario)
|
|
404
|
+
const artifact: AuthoredArtifact = {
|
|
405
|
+
decision: reply.decision,
|
|
406
|
+
reason: reply.reason,
|
|
407
|
+
...(reply.graph !== undefined ? { graph: reply.graph } : {}),
|
|
408
|
+
raw: reply.raw,
|
|
409
|
+
}
|
|
410
|
+
if (reply.decision !== 'graph' || reply.graph === undefined) return artifact
|
|
411
|
+
try {
|
|
412
|
+
artifact.run = await runAuthoredOffline(reply.graph, `codemode-${scenario.id}`)
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (err instanceof GraphEdgeCapError) {
|
|
415
|
+
// Cap exhaustion still carries the full ledger — keep the evidence AND the refusal.
|
|
416
|
+
artifact.run = {
|
|
417
|
+
resultKind: 'edge-cap-error',
|
|
418
|
+
ledger: err.ledger,
|
|
419
|
+
exhaustedEdges: err.exhaustedEdges,
|
|
420
|
+
}
|
|
421
|
+
artifact.validationError = err.message
|
|
422
|
+
} else {
|
|
423
|
+
artifact.validationError = err instanceof Error ? err.message : String(err)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return artifact
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Closure B: the deterministic scorer ────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
interface Check {
|
|
432
|
+
key: string
|
|
433
|
+
pass: boolean
|
|
434
|
+
note: string
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const STOPWORDS = new Set([
|
|
438
|
+
'should', 'would', 'could', 'about', 'before', 'after', 'their', 'there', 'these', 'those',
|
|
439
|
+
'thing', 'whole', 'based', 'little', 'produce', 'passes',
|
|
440
|
+
])
|
|
441
|
+
|
|
442
|
+
/** Salient domain words of a brief: length ≥ 6, minus function words. */
|
|
443
|
+
function domainWords(brief: string): string[] {
|
|
444
|
+
return [
|
|
445
|
+
...new Set(
|
|
446
|
+
brief
|
|
447
|
+
.toLowerCase()
|
|
448
|
+
.split(/[^a-z]+/)
|
|
449
|
+
.filter((w) => w.length >= 6 && !STOPWORDS.has(w)),
|
|
450
|
+
),
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function delegatesEdges(spec: AuthoredGraphSpec): Array<Extract<AuthoredEdge, { kind: 'delegates' }>> {
|
|
455
|
+
return spec.edges.filter((e): e is Extract<AuthoredEdge, { kind: 'delegates' }> => e.kind === 'delegates')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function analyzesEdges(spec: AuthoredGraphSpec): Array<Extract<AuthoredEdge, { kind: 'analyzes' }>> {
|
|
459
|
+
return spec.edges.filter((e): e is Extract<AuthoredEdge, { kind: 'analyzes' }> => e.kind === 'analyzes')
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function ledgerTraversals(run: OfflineRunSummary | undefined, edgePrefix: string): number {
|
|
463
|
+
if (!run) return 0
|
|
464
|
+
return run.ledger.filter((row) => row.edge.startsWith(edgePrefix)).length
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** CLOSURE B — deterministic `JudgeConfig`-style scorer: each `expect` entry becomes one
|
|
468
|
+
* mechanical check; score = satisfied / total, equal weights. */
|
|
469
|
+
export function judgeArtifact(artifact: AuthoredArtifact, kase: CaseSpec): { score: number; reasons: string[] } {
|
|
470
|
+
const e = kase.expect
|
|
471
|
+
const g = artifact.graph
|
|
472
|
+
const checks: Check[] = []
|
|
473
|
+
const graphOk = artifact.decision === 'graph' && g !== undefined
|
|
474
|
+
|
|
475
|
+
if (e.correctAnswerIsNoGraph !== undefined) {
|
|
476
|
+
checks.push({
|
|
477
|
+
key: 'correctAnswerIsNoGraph',
|
|
478
|
+
pass: artifact.decision === 'single-agent',
|
|
479
|
+
note: `decision=${artifact.decision}`,
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
if (e.correctAnswerIsDynamicWorkflow !== undefined) {
|
|
483
|
+
checks.push({
|
|
484
|
+
key: 'correctAnswerIsDynamicWorkflow',
|
|
485
|
+
pass: artifact.decision === 'dynamic-workflow',
|
|
486
|
+
note: `decision=${artifact.decision}`,
|
|
487
|
+
})
|
|
488
|
+
}
|
|
489
|
+
if (e.correctAnswerIsGraph !== undefined) {
|
|
490
|
+
// Explicit dialect check for cases whose whole point is that a cheap-sounding brief
|
|
491
|
+
// still warrants a graph; requires an authored graph, not just the word "graph".
|
|
492
|
+
checks.push({
|
|
493
|
+
key: 'correctAnswerIsGraph',
|
|
494
|
+
pass: graphOk,
|
|
495
|
+
note: `decision=${artifact.decision}${artifact.decision === 'graph' && g === undefined ? ' (no graph payload)' : ''}`,
|
|
496
|
+
})
|
|
497
|
+
}
|
|
498
|
+
if (e.nodes !== undefined) {
|
|
499
|
+
const total = graphOk ? g.nodes.length : 0
|
|
500
|
+
const workers = graphOk ? Math.max(total - 1, 0) : 0
|
|
501
|
+
checks.push({
|
|
502
|
+
key: 'nodes',
|
|
503
|
+
// The case files don't say whether the count includes the root; accept either reading.
|
|
504
|
+
pass: graphOk && (workers === e.nodes || total === e.nodes),
|
|
505
|
+
note: graphOk ? `workers=${workers} total=${total} expected=${e.nodes}` : 'no graph authored',
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
if (e.maxTraversalsAtLeast !== undefined) {
|
|
509
|
+
const caps = graphOk ? delegatesEdges(g).map((d) => d.maxTraversals ?? defaultEdgeTraversalCap) : []
|
|
510
|
+
const best = caps.length > 0 ? Math.max(...caps) : 0
|
|
511
|
+
checks.push({
|
|
512
|
+
key: 'maxTraversalsAtLeast',
|
|
513
|
+
pass: graphOk && best >= e.maxTraversalsAtLeast,
|
|
514
|
+
note: graphOk
|
|
515
|
+
? `effective delegates cap ${best} (default ${defaultEdgeTraversalCap} when unset) vs ≥${e.maxTraversalsAtLeast}`
|
|
516
|
+
: 'no graph authored',
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
if (e.deliverableDescribeCarriesMission !== undefined) {
|
|
520
|
+
const describe = graphOk ? g.deliverableDescribe ?? '' : ''
|
|
521
|
+
const words = domainWords(kase.brief)
|
|
522
|
+
const hit = words.filter((w) => describe.toLowerCase().includes(w))
|
|
523
|
+
checks.push({
|
|
524
|
+
key: 'deliverableDescribeCarriesMission',
|
|
525
|
+
pass: graphOk && describe.length > 40 && hit.length > 0,
|
|
526
|
+
note: graphOk
|
|
527
|
+
? `describe ${describe.length} chars, domain words hit: [${hit.join(', ')}] of [${words.join(', ')}]`
|
|
528
|
+
: 'no graph authored',
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
if (e.trapIsAnalyzesCapAsStop !== undefined) {
|
|
532
|
+
// The trap is sprung when the ONLY cap in the graph sits on an analyzes edge — i.e. the
|
|
533
|
+
// author treated the observability cap as the stop. A delegates cap or a deliverable-based
|
|
534
|
+
// stop alongside it means the author dodged the trap.
|
|
535
|
+
const anCaps = graphOk ? analyzesEdges(g).some((a) => a.maxTraversals !== undefined) : false
|
|
536
|
+
const delCaps = graphOk ? delegatesEdges(g).some((d) => d.maxTraversals !== undefined) : false
|
|
537
|
+
const sprung = graphOk && anCaps && !delCaps
|
|
538
|
+
checks.push({
|
|
539
|
+
key: 'trapIsAnalyzesCapAsStop',
|
|
540
|
+
pass: graphOk && !sprung,
|
|
541
|
+
note: graphOk
|
|
542
|
+
? `analyzes caps=${anCaps} delegates caps=${delCaps}${sprung ? ' — trap sprung' : ''}`
|
|
543
|
+
: 'no graph authored',
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
if (e.correctStopIsDelegatesCapOrDeliverable !== undefined) {
|
|
547
|
+
const delCaps = graphOk ? delegatesEdges(g).some((d) => d.maxTraversals !== undefined) : false
|
|
548
|
+
const mentionsDeliverable = /deliverable/i.test(artifact.reason)
|
|
549
|
+
checks.push({
|
|
550
|
+
key: 'correctStopIsDelegatesCapOrDeliverable',
|
|
551
|
+
pass: graphOk && (delCaps || mentionsDeliverable),
|
|
552
|
+
note: graphOk
|
|
553
|
+
? `delegates caps=${delCaps}, reason mentions deliverable=${mentionsDeliverable}`
|
|
554
|
+
: 'no graph authored',
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
if (e.analyzesWarranted !== undefined) {
|
|
558
|
+
const has = graphOk ? analyzesEdges(g).length > 0 : false
|
|
559
|
+
checks.push({
|
|
560
|
+
key: 'analyzesWarranted',
|
|
561
|
+
pass: graphOk && has === e.analyzesWarranted,
|
|
562
|
+
note: graphOk ? `analyzes edges=${analyzesEdges(g).length} warranted=${e.analyzesWarranted}` : 'no graph authored',
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
if (e.wrongIfAnalystIsNode !== undefined) {
|
|
566
|
+
const nodeIds = graphOk ? new Set(g.nodes.map((n) => n.id)) : new Set<string>()
|
|
567
|
+
const offenders = graphOk ? analyzesEdges(g).filter((a) => nodeIds.has(a.analyst)) : []
|
|
568
|
+
checks.push({
|
|
569
|
+
key: 'wrongIfAnalystIsNode',
|
|
570
|
+
pass: graphOk && offenders.length === 0,
|
|
571
|
+
note: graphOk
|
|
572
|
+
? offenders.length === 0
|
|
573
|
+
? 'no analyst id collides with a node id'
|
|
574
|
+
: `analyst ids that are nodes: ${offenders.map((o) => o.analyst).join(', ')}`
|
|
575
|
+
: 'no graph authored',
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
if (e.edges !== undefined) {
|
|
579
|
+
for (const want of e.edges) {
|
|
580
|
+
if (/delegates/i.test(want)) {
|
|
581
|
+
const rootId = graphOk ? findRootId(g) : ''
|
|
582
|
+
const workers = graphOk ? g.nodes.map((n) => n.id).filter((id) => id !== rootId) : []
|
|
583
|
+
const covered = graphOk
|
|
584
|
+
? workers.every(
|
|
585
|
+
(id) =>
|
|
586
|
+
delegatesEdges(g).some((d) => d.to === id) &&
|
|
587
|
+
ledgerTraversals(artifact.run, `delegates:${rootId}->${id}`) > 0,
|
|
588
|
+
)
|
|
589
|
+
: false
|
|
590
|
+
checks.push({
|
|
591
|
+
key: `edge:${want}`,
|
|
592
|
+
pass: graphOk && workers.length > 0 && covered,
|
|
593
|
+
note: graphOk
|
|
594
|
+
? `workers [${workers.join(', ')}] each delegated-to with >0 ledger traversals: ${covered}`
|
|
595
|
+
: 'no graph authored',
|
|
596
|
+
})
|
|
597
|
+
} else if (/analyzes/i.test(want)) {
|
|
598
|
+
const rootId = graphOk ? findRootId(g) : ''
|
|
599
|
+
const toRoot = graphOk ? analyzesEdges(g).filter((a) => a.to === rootId) : []
|
|
600
|
+
const fired = toRoot.some((a) => ledgerTraversals(artifact.run, `analyzes:${a.analyst}:`) > 0)
|
|
601
|
+
checks.push({
|
|
602
|
+
key: `edge:${want}`,
|
|
603
|
+
pass: graphOk && toRoot.length > 0 && fired,
|
|
604
|
+
note: graphOk
|
|
605
|
+
? `analyzes→root edges=${toRoot.length}, fired with >0 traversals=${fired}`
|
|
606
|
+
: 'no graph authored',
|
|
607
|
+
})
|
|
608
|
+
} else {
|
|
609
|
+
checks.push({ key: `edge:${want}`, pass: false, note: 'unrecognized edge expectation' })
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const total = checks.length
|
|
615
|
+
const passed = checks.filter((c) => c.pass).length
|
|
616
|
+
const score = total === 0 ? 0 : passed / total
|
|
617
|
+
const reasons = checks.map((c) => `${c.pass ? 'PASS' : 'FAIL'} ${c.key}: ${c.note}`)
|
|
618
|
+
if (artifact.validationError !== undefined) {
|
|
619
|
+
reasons.push(`validationError: ${artifact.validationError}`)
|
|
620
|
+
}
|
|
621
|
+
return { score, reasons }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ── The baseline run ───────────────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
interface CaseResult {
|
|
627
|
+
id: string
|
|
628
|
+
decision: Decision
|
|
629
|
+
score: number
|
|
630
|
+
reasons: string[]
|
|
631
|
+
validationError?: string
|
|
632
|
+
reason: string
|
|
633
|
+
authoredGraph?: AuthoredGraphSpec
|
|
634
|
+
runResultKind?: string
|
|
635
|
+
ledgerRows?: number
|
|
636
|
+
exhaustedEdges?: ReadonlyArray<string>
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function main(): Promise<void> {
|
|
640
|
+
const inputs = loadInputs()
|
|
641
|
+
const surface = inputs.surface
|
|
642
|
+
// CASE=<id> runs a subset — the smoke lever; the baseline artifact is only written on a full run.
|
|
643
|
+
const only = process.env.CASE
|
|
644
|
+
const cases: CaseSpec[] = inputs.cases.filter((c) => only === undefined || c.id === only)
|
|
645
|
+
|
|
646
|
+
const authorProfile = buildAgentGraphsAuthorProfile(surface)
|
|
647
|
+
const authorLabel = [
|
|
648
|
+
authorProfile.harness,
|
|
649
|
+
authorProfile.model?.provider,
|
|
650
|
+
authorProfile.model?.default,
|
|
651
|
+
]
|
|
652
|
+
.filter(Boolean)
|
|
653
|
+
.join('/')
|
|
654
|
+
console.log(
|
|
655
|
+
`codemode baseline: skill v1 (${surface.length} chars, source=${inputs.source}), ${cases.length} cases, author=${authorLabel}`,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
const results: CaseResult[] = []
|
|
659
|
+
for (const kase of cases) {
|
|
660
|
+
const t0 = Date.now()
|
|
661
|
+
process.stdout.write(` ${kase.id} … `)
|
|
662
|
+
try {
|
|
663
|
+
const artifact = await dispatchWithSurface(surface, kase)
|
|
664
|
+
const { score, reasons } = judgeArtifact(artifact, kase)
|
|
665
|
+
results.push({
|
|
666
|
+
id: kase.id,
|
|
667
|
+
decision: artifact.decision,
|
|
668
|
+
score,
|
|
669
|
+
reasons,
|
|
670
|
+
...(artifact.validationError !== undefined ? { validationError: artifact.validationError } : {}),
|
|
671
|
+
reason: artifact.reason,
|
|
672
|
+
...(artifact.graph !== undefined ? { authoredGraph: artifact.graph } : {}),
|
|
673
|
+
...(artifact.run !== undefined
|
|
674
|
+
? {
|
|
675
|
+
runResultKind: artifact.run.resultKind,
|
|
676
|
+
ledgerRows: artifact.run.ledger.length,
|
|
677
|
+
exhaustedEdges: artifact.run.exhaustedEdges,
|
|
678
|
+
}
|
|
679
|
+
: {}),
|
|
680
|
+
})
|
|
681
|
+
console.log(`${artifact.decision} score=${score.toFixed(2)} (${Math.round((Date.now() - t0) / 1000)}s)`)
|
|
682
|
+
for (const line of reasons.filter((x) => !x.startsWith('PASS'))) console.log(` ${line}`)
|
|
683
|
+
} catch (err) {
|
|
684
|
+
// An author-transport failure is a null result for the case, recorded as such.
|
|
685
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
686
|
+
results.push({
|
|
687
|
+
id: kase.id,
|
|
688
|
+
decision: 'single-agent',
|
|
689
|
+
score: 0,
|
|
690
|
+
reasons: [`AUTHOR-FAILED: ${message}`],
|
|
691
|
+
validationError: message,
|
|
692
|
+
reason: '',
|
|
693
|
+
})
|
|
694
|
+
console.log(`AUTHOR-FAILED (${message.slice(0, 80)})`)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const scores = results.map((r) => r.score).sort((a, b) => a - b)
|
|
699
|
+
const mean = scores.reduce((s, x) => s + x, 0) / Math.max(scores.length, 1)
|
|
700
|
+
// Interpolated median (average the middle pair on even n). The first baseline shipped
|
|
701
|
+
// scores[floor(n/2)] — the UPPER middle on even n — and reported "median 1.0" over
|
|
702
|
+
// [0, 0, 0.5, 0.6, 1, 1, 1, 1], overstating the skill. An aggregate that flatters the
|
|
703
|
+
// surface under improvement corrupts every gate downstream of it.
|
|
704
|
+
const mid = Math.floor(scores.length / 2)
|
|
705
|
+
const median =
|
|
706
|
+
scores.length === 0 ? 0
|
|
707
|
+
: scores.length % 2 === 1 ? (scores[mid] ?? 0)
|
|
708
|
+
: ((scores[mid - 1] ?? 0) + (scores[mid] ?? 0)) / 2
|
|
709
|
+
|
|
710
|
+
const out = {
|
|
711
|
+
skillVersion: 'v1',
|
|
712
|
+
surfaceSource: inputs.source,
|
|
713
|
+
authorProfile,
|
|
714
|
+
date: new Date().toISOString(),
|
|
715
|
+
n: results.length,
|
|
716
|
+
aggregate: { mean, median, min: scores[0] ?? 0, max: scores[scores.length - 1] ?? 0 },
|
|
717
|
+
cases: results,
|
|
718
|
+
}
|
|
719
|
+
const wrote = only === undefined
|
|
720
|
+
if (wrote) {
|
|
721
|
+
mkdirSync(dirname(OUT_PATH), { recursive: true })
|
|
722
|
+
writeFileSync(OUT_PATH, `${JSON.stringify(out, null, 2)}\n`)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
console.log('\ncase decision score notes')
|
|
726
|
+
console.log('─'.repeat(96))
|
|
727
|
+
for (const r of results) {
|
|
728
|
+
const fails = r.reasons.filter((x) => x.startsWith('FAIL')).length
|
|
729
|
+
const total = r.reasons.filter((x) => /^(PASS|FAIL)/.test(x)).length
|
|
730
|
+
console.log(
|
|
731
|
+
`${r.id.padEnd(28)}${r.decision.padEnd(19)}${r.score.toFixed(2).padEnd(7)}${total - fails}/${total} checks${r.validationError ? ' [refusal captured]' : ''}`,
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
console.log('─'.repeat(96))
|
|
735
|
+
console.log(`mean=${mean.toFixed(3)} median=${median.toFixed(3)} min=${(scores[0] ?? 0).toFixed(2)} max=${(scores[scores.length - 1] ?? 0).toFixed(2)} n=${results.length}`)
|
|
736
|
+
console.log(wrote ? `written: ${OUT_PATH}` : 'subset run (CASE set) — baseline artifact NOT written')
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Run the baseline only when executed directly; gen2 imports this module for its closures.
|
|
740
|
+
const invokedDirectly =
|
|
741
|
+
process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolvePath(process.argv[1])
|
|
742
|
+
if (invokedDirectly) {
|
|
743
|
+
main().catch((err) => {
|
|
744
|
+
console.error(err instanceof Error ? (err.stack ?? err.message) : String(err))
|
|
745
|
+
process.exit(1)
|
|
746
|
+
})
|
|
747
|
+
}
|