@tangle-network/agent-bench 0.8.12 → 0.8.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/HARNESS.md +109 -335
  3. package/README.md +14 -0
  4. package/dist/adapters.js +2 -2
  5. package/dist/benchmarks/appworld.js +1 -1
  6. package/dist/benchmarks/appworld.js.map +1 -1
  7. package/dist/benchmarks/cadbench.js +1 -1
  8. package/dist/benchmarks/cadgenbench.js +1 -1
  9. package/dist/benchmarks/finresearchbench.js +1 -1
  10. package/dist/benchmarks/finsearchcomp.js +1 -1
  11. package/dist/benchmarks/frames.js +1 -1
  12. package/dist/benchmarks/simpleqa.js +1 -1
  13. package/dist/benchmarks/trata-hedge.js +1 -1
  14. package/dist/{cadbench-BLSyxR1N.js → cadbench-BRF-59Mt.js} +2 -2
  15. package/dist/{cadbench-BLSyxR1N.js.map → cadbench-BRF-59Mt.js.map} +1 -1
  16. package/dist/{cadgenbench-x2OFkf8y.js → cadgenbench-DXtGkuW3.js} +2 -2
  17. package/dist/{cadgenbench-x2OFkf8y.js.map → cadgenbench-DXtGkuW3.js.map} +1 -1
  18. package/dist/index.js +10 -5
  19. package/dist/index.js.map +1 -1
  20. package/dist/{router-turn-C2wMiDoo.js → router-turn-uTYO6KQ1.js} +10 -8
  21. package/dist/router-turn-uTYO6KQ1.js.map +1 -0
  22. package/package.json +8 -7
  23. package/src/atom-mcp-e2e.mts +1 -1
  24. package/src/benchmarks/appworld.ts +1 -1
  25. package/src/commit0-gate.mts +1 -1
  26. package/src/humaneval-repair-gate.mts +1 -1
  27. package/src/mcp-mount-probe.mts +1 -1
  28. package/src/quant-arena/quant-loop.mts +1 -1
  29. package/src/router-turn.ts +10 -1
  30. package/src/run-benchmarks.ts +12 -8
  31. package/src/swe-arena/arms.ts +1 -1
  32. package/dist/router-turn-C2wMiDoo.js.map +0 -1
  33. package/src/agent-graphs-gen2.mts +0 -523
  34. package/src/agent-graphs-gen3.mts +0 -660
  35. package/src/agent-graphs-improve/offline-seams.mts +0 -128
  36. package/src/agent-graphs-improve.mts +0 -747
@@ -1,747 +0,0 @@
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
- }