@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,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AgentProfile genome as independently-optimizable coordinates. Every field the harness lets
|
|
3
|
+
* you define — prompt, skills, subagents, hooks, tools, mcp — is a coordinate: a way to inject a
|
|
4
|
+
* set of named candidates into a profile while holding every OTHER field fixed. This is the one
|
|
5
|
+
* abstraction behind "improve any part of the agent, freeze the rest, combine freely":
|
|
6
|
+
*
|
|
7
|
+
* compose(baseProfile, selected) = { ...baseProfile, <thisField>: inject(selected) }
|
|
8
|
+
*
|
|
9
|
+
* Freeze a coordinate ⇒ never select it (it stays as-is in the base profile). Optimize one ⇒
|
|
10
|
+
* vary its selection. Combine ⇒ compose several coordinates' composers in sequence. Because a
|
|
11
|
+
* subagent is ITSELF an AgentProfile, the same coordinates apply recursively to any node in the
|
|
12
|
+
* supervisor flow (root driver, worker, sub-worker) — you point a coordinate at that node's
|
|
13
|
+
* base profile.
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import type { AgentProfile } from '@tangle-network/sandbox'
|
|
18
|
+
import { defineInlineResource } from '@tangle-network/sandbox'
|
|
19
|
+
|
|
20
|
+
export interface ProfileCoordinate {
|
|
21
|
+
/** Coordinate id (the COORDINATE= knob value). */
|
|
22
|
+
readonly name: string
|
|
23
|
+
/** Candidate names the optimizer screens (one per independently-testable unit). */
|
|
24
|
+
candidates(): readonly string[]
|
|
25
|
+
/** Inject the selected candidates into THIS field of the profile, holding all others fixed.
|
|
26
|
+
* Empty selection ⇒ the base profile unchanged (the frozen/baseline arm). */
|
|
27
|
+
compose(base: AgentProfile, selected: readonly string[]): AgentProfile
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const here = (p: string) => join(import.meta.dirname, p)
|
|
31
|
+
|
|
32
|
+
// ── skills: SKILL.md packages materialized to disk (resources.skills) ───────────────
|
|
33
|
+
function skillsCoordinate(dir = here(process.env.SKILLS_DIR ?? 'coding-skills')): ProfileCoordinate {
|
|
34
|
+
const files = () => readdirSync(dir).filter((f) => f.endsWith('.md')).sort()
|
|
35
|
+
return {
|
|
36
|
+
name: 'skills',
|
|
37
|
+
candidates: () => files().map((f) => f.replace(/\.md$/, '')),
|
|
38
|
+
compose: (base, selected) => {
|
|
39
|
+
if (!selected.length) return base
|
|
40
|
+
const refs = selected.map((n) => defineInlineResource(n, readFileSync(join(dir, `${n}.md`), 'utf8')))
|
|
41
|
+
return { ...base, resources: { ...base.resources, skills: [...(base.resources?.skills ?? []), ...refs] } }
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── hooks: shell commands the harness fires on lifecycle events (enforced, not advisory) ──
|
|
47
|
+
const hookDefs: Record<string, { event: string; command: string; matcher?: string }> = {
|
|
48
|
+
'lint-before-edit': { event: 'PreToolUse', matcher: 'Edit|Write', command: 'ruff check . 2>/dev/null || true' },
|
|
49
|
+
'tests-after-edit': { event: 'PostToolUse', matcher: 'Edit|Write', command: 'python -m pytest -q 2>/dev/null | tail -5 || true' },
|
|
50
|
+
'no-print-debugging': { event: 'PreToolUse', matcher: 'Edit|Write', command: 'true' },
|
|
51
|
+
}
|
|
52
|
+
function hooksCoordinate(): ProfileCoordinate {
|
|
53
|
+
return {
|
|
54
|
+
name: 'hooks',
|
|
55
|
+
candidates: () => Object.keys(hookDefs),
|
|
56
|
+
compose: (base, selected) => {
|
|
57
|
+
if (!selected.length) return base
|
|
58
|
+
const hooks: Record<string, { command: string; matcher?: string }[]> = { ...(base.hooks ?? {}) }
|
|
59
|
+
for (const n of selected) {
|
|
60
|
+
const d = hookDefs[n]
|
|
61
|
+
if (!d) throw new Error(`unknown hook ${n}`)
|
|
62
|
+
hooks[d.event] = [...(hooks[d.event] ?? []), { command: d.command, ...(d.matcher ? { matcher: d.matcher } : {}) }]
|
|
63
|
+
}
|
|
64
|
+
return { ...base, hooks: hooks as AgentProfile['hooks'] }
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── tools: enable/disable named harness tools ───────────────────────────────────────
|
|
70
|
+
const toolCandidates = ['webfetch', 'websearch', 'bash', 'edit', 'read', 'grep']
|
|
71
|
+
function toolsCoordinate(): ProfileCoordinate {
|
|
72
|
+
return {
|
|
73
|
+
name: 'tools',
|
|
74
|
+
candidates: () => toolCandidates,
|
|
75
|
+
compose: (base, selected) => {
|
|
76
|
+
if (!selected.length) return base
|
|
77
|
+
const tools: Record<string, boolean> = { ...(base.tools ?? {}) }
|
|
78
|
+
for (const n of selected) tools[n] = true
|
|
79
|
+
return { ...base, tools }
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── prompt: extra instruction lines appended to the active system prompt ─────────────
|
|
85
|
+
const instructionDefs: Record<string, string> = {
|
|
86
|
+
'be-surgical': 'Make the smallest change that satisfies the task; do not touch unrelated code.',
|
|
87
|
+
'check-examples': 'Before finalizing, re-read the examples in the docstring and confirm your output matches them exactly.',
|
|
88
|
+
'edge-cases': 'Enumerate boundary inputs (empty, zero, negative, max) and make sure your solution handles each.',
|
|
89
|
+
}
|
|
90
|
+
function promptCoordinate(): ProfileCoordinate {
|
|
91
|
+
return {
|
|
92
|
+
name: 'prompt',
|
|
93
|
+
candidates: () => Object.keys(instructionDefs),
|
|
94
|
+
compose: (base, selected) => {
|
|
95
|
+
if (!selected.length) return base
|
|
96
|
+
const instructions = [...(base.prompt?.instructions ?? []), ...selected.map((n) => instructionDefs[n]!)]
|
|
97
|
+
return { ...base, prompt: { ...base.prompt, instructions } }
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── subagents: helper agents the root can delegate to (each is itself a mini-profile) ──
|
|
103
|
+
const subagentDefs: Record<string, { description: string; prompt: string; tools?: Record<string, boolean> }> = {
|
|
104
|
+
reviewer: { description: 'Reviews a proposed change for bugs before it is finalized.', prompt: 'You are a strict code reviewer. Find bugs, edge cases, and contract violations in the proposed change. Be concise.' },
|
|
105
|
+
tester: { description: 'Writes and runs a focused test for the change.', prompt: 'You write the minimal test that would catch a regression in this change, run it, and report pass/fail.' },
|
|
106
|
+
}
|
|
107
|
+
function subagentsCoordinate(): ProfileCoordinate {
|
|
108
|
+
return {
|
|
109
|
+
name: 'subagents',
|
|
110
|
+
candidates: () => Object.keys(subagentDefs),
|
|
111
|
+
compose: (base, selected) => {
|
|
112
|
+
if (!selected.length) return base
|
|
113
|
+
const subagents = { ...(base.subagents ?? {}) }
|
|
114
|
+
for (const n of selected) subagents[n] = subagentDefs[n]!
|
|
115
|
+
return { ...base, subagents: subagents as AgentProfile['subagents'] }
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const REGISTRY: Record<string, () => ProfileCoordinate> = {
|
|
121
|
+
skills: () => skillsCoordinate(),
|
|
122
|
+
hooks: hooksCoordinate,
|
|
123
|
+
tools: toolsCoordinate,
|
|
124
|
+
prompt: promptCoordinate,
|
|
125
|
+
subagents: subagentsCoordinate,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getCoordinate(name: string): ProfileCoordinate {
|
|
129
|
+
const make = REGISTRY[name]
|
|
130
|
+
if (!make) throw new Error(`unknown coordinate ${name} (have: ${Object.keys(REGISTRY).join(', ')})`)
|
|
131
|
+
return make()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export const coordinateNames = (): string[] => Object.keys(REGISTRY)
|
package/src/profiles.ts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The profile directory — roles are DATA, not code. One primitive (a profiled agent run as
|
|
3
|
+
* `Agent.act` in an executor, orchestrated by the Supervisor); worker / analyst / driver are the
|
|
4
|
+
* SAME agent, differing only by the profile below. This is the full unification: there is no
|
|
5
|
+
* "operator type" or "analyst type" — there are profiles + the operator toolbox they use to manage
|
|
6
|
+
* each other (in-process via the Scope, in a sandbox via the same verbs exposed as MCP tools).
|
|
7
|
+
*
|
|
8
|
+
* The directory is OPTIMIZED over time by the agent-eval RSI loop (`runImprovementLoop`): `gepaProposer`
|
|
9
|
+
* evolves the prompts, a `skillOptProposer` evolves `skills`, Pareto-tracked + holdout-gated.
|
|
10
|
+
* The trace-analyst's findings are the optimizer's input — the loop improves the profile that the
|
|
11
|
+
* findings say is weak.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The operator toolbox — the verbs a DRIVER profile uses to lead the workers it drives. In-process
|
|
15
|
+
* these are `Scope` methods; in a sandbox they are the SAME verbs exposed as MCP tools (Scope-as-MCP).
|
|
16
|
+
* `analyze`/`define_analyst`/`run_analyst` make the trace-analyst a first-class, definable sub-agent —
|
|
17
|
+
* the driver triggers analysts as a tool (or spawns one), never via raw bash. */
|
|
18
|
+
export const OPERATOR_TOOLS = [
|
|
19
|
+
'list_analysts', // the trace analysts already available
|
|
20
|
+
'define_analyst', // author + register a NEW trace analyst (a profile) on the fly
|
|
21
|
+
'run_analyst', // run an analyst over a worker's trace → findings (selector≠judge: trace, not score)
|
|
22
|
+
'observe_worker', // a worker's in-flight trace, or its last finished episode/shot
|
|
23
|
+
'spawn_worker', // start a worker (or a sub-analyst) — drive many; parallelize when independent
|
|
24
|
+
'steer_worker', // send a live worker a message down: instruction, course-correction, or continuation (interrupt? for forceful)
|
|
25
|
+
'stop', // declare the task complete (verified) or abandon a line
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
export type OperatorTool = (typeof OPERATOR_TOOLS)[number]
|
|
29
|
+
|
|
30
|
+
/** One role in the directory. The unified run-config: who the agent is (`systemPrompt`/`skills`), what
|
|
31
|
+
* it can do (`tools` + `mcp`), and which lane it serves (`role`). The same shape the sandbox executor
|
|
32
|
+
* runs and the optimizer mutates. */
|
|
33
|
+
export interface RoleProfile {
|
|
34
|
+
readonly id: string
|
|
35
|
+
readonly role: 'worker' | 'analyst' | 'driver'
|
|
36
|
+
/** Default model snapshot; the optimizer may pin/sweep it. */
|
|
37
|
+
readonly model: string
|
|
38
|
+
readonly systemPrompt: string
|
|
39
|
+
/** Reusable capabilities the skillopt driver evolves; the driver may PROPOSE new ones from findings. */
|
|
40
|
+
readonly skills: string[]
|
|
41
|
+
/** Tool names available in-box. Workers get the artifact's tools; drivers also get the operator toolbox. */
|
|
42
|
+
readonly tools: string[]
|
|
43
|
+
/** MCP servers wired in-box (the operator toolbox is one, when the driver runs in a sandbox). */
|
|
44
|
+
readonly mcp?: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** WORKER: does the task over the artifact. Its tools are the artifact's (the surface supplies them);
|
|
48
|
+
* its dynamic-workflow / fanout capability comes from the harness it runs (pi/codex/claude-code) +
|
|
49
|
+
* its skills — which is exactly why those live on the profile. */
|
|
50
|
+
export const workerProfile: RoleProfile = {
|
|
51
|
+
id: 'worker/default',
|
|
52
|
+
role: 'worker',
|
|
53
|
+
model: 'deepseek-v4-flash',
|
|
54
|
+
systemPrompt:
|
|
55
|
+
'You are a worker. Use the available tools to bring the artifact to the required final state. ' +
|
|
56
|
+
'Address every distinct change the request implies; after each tool result, check what remains ' +
|
|
57
|
+
'and continue; verify each value you set actually took. Use your harness’s parallelism / dynamic ' +
|
|
58
|
+
'workflow / sub-agents when the sub-tasks are genuinely independent. Reply DONE only once every ' +
|
|
59
|
+
'required change is made and verified.',
|
|
60
|
+
skills: [],
|
|
61
|
+
tools: ['<artifact tools>'],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** ANALYST: reads a trace, emits findings. Firewall — judges from observed tool RESULTS, never a score
|
|
65
|
+
* (selector≠judge). A driver can `define_analyst` to spawn specialized variants of this. */
|
|
66
|
+
export const analystProfile: RoleProfile = {
|
|
67
|
+
id: 'analyst/trace',
|
|
68
|
+
role: 'analyst',
|
|
69
|
+
model: 'deepseek-v4-flash',
|
|
70
|
+
systemPrompt:
|
|
71
|
+
'You audit a worker’s trajectory. From ONLY the task and the worker’s tool-call trace (calls + ' +
|
|
72
|
+
'their RESULTS), list every required change that does NOT yet appear done or verified. Judge from ' +
|
|
73
|
+
'observed results, never from intent, and never from a grader/score. Be specific. If everything ' +
|
|
74
|
+
'required appears done and verified, reply exactly COMPLETE.',
|
|
75
|
+
skills: [],
|
|
76
|
+
tools: ['read_trace'],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** DRIVER: the lead steerer. A sandbox agent that reviews the workers it drives, investigates their
|
|
80
|
+
* traces (using/defining analysts, fanning out when useful), takes the lead or dispatches, and
|
|
81
|
+
* parallelizes intelligently. This is the profile you sketched — formalized. Topology is ITS choice. */
|
|
82
|
+
export const driverProfile: RoleProfile = {
|
|
83
|
+
id: 'driver/operator',
|
|
84
|
+
role: 'driver',
|
|
85
|
+
model: 'deepseek-v4-flash',
|
|
86
|
+
systemPrompt: [
|
|
87
|
+
'You are the OPERATOR. You lead one or more worker agents to fully complete a task over a shared',
|
|
88
|
+
'artifact. You may delegate, investigate, and — when it is faster or more reliable — do the work',
|
|
89
|
+
'yourself. You judge from traces, never from a grader/score (selector ≠ judge).',
|
|
90
|
+
'',
|
|
91
|
+
'TOOLS:',
|
|
92
|
+
'- list_analysts / run_analyst(id, worker): the trace analysts available — run one over a worker’s',
|
|
93
|
+
' trajectory to get findings.',
|
|
94
|
+
'- define_analyst(profile): if no existing analyst fits, AUTHOR a new one and run it. Specialized',
|
|
95
|
+
' analysts are cheap; make them when a worker’s failure mode needs a focused lens.',
|
|
96
|
+
'- observe_worker(worker): the worker’s IN-FLIGHT trace if it is still running, else its last',
|
|
97
|
+
' finished episode/shot.',
|
|
98
|
+
'- spawn_worker(profile, task) / steer_worker(worker, instruction, interrupt?) / stop.',
|
|
99
|
+
'- the artifact’s own tools (read/edit/run) — use them to inspect the workspace and to contribute',
|
|
100
|
+
' decisive work yourself.',
|
|
101
|
+
'',
|
|
102
|
+
'EACH CYCLE:',
|
|
103
|
+
'1. Review the workers you are driving — analyze their in-flight traces, or their last finished',
|
|
104
|
+
' shot. Investigate deeply: run analysts, define a new analyst if needed, and fan out / spawn',
|
|
105
|
+
' sub-analysts in PARALLEL to inspect traces faster when that genuinely helps.',
|
|
106
|
+
'2. Identify everything you can — incomplete work, errors, wrong approaches, policy violations,',
|
|
107
|
+
' stalls, repeated mistakes. Be concrete about what remains.',
|
|
108
|
+
'3. If a finding is a reusable capability, PROPOSE it as a skill (so the optimizer can fold it into',
|
|
109
|
+
' the profile directory) rather than re-deriving it every run.',
|
|
110
|
+
'4. Decide and act. If you are confident on the issue and dispatching would be slower or less',
|
|
111
|
+
' reliable, TAKE THE LEAD: do the decisive turn yourself (directly, via sub-agents, or a dynamic',
|
|
112
|
+
' workflow). Otherwise STEER the worker with a specific, verifiable instruction.',
|
|
113
|
+
'5. Parallelize as much as you intelligently can — NOT frivolously. Fan out only when the sub-tasks',
|
|
114
|
+
' are genuinely independent; go deep on a single line when the work is sequential.',
|
|
115
|
+
'',
|
|
116
|
+
'STOP only when every required change is made AND confirmed in the workers’ tool results.',
|
|
117
|
+
].join('\n'),
|
|
118
|
+
skills: ['trace-review', 'fanout-when-independent', 'take-the-lead-when-confident'],
|
|
119
|
+
tools: [...OPERATOR_TOOLS, '<artifact tools>'],
|
|
120
|
+
mcp: ['coordination'], // Scope-as-MCP: the coordination verbs, in a sandbox
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The directory — the population the RSI loop optimizes (gepa over prompts, skillopt over skills). */
|
|
124
|
+
export const profileDirectory: Record<string, RoleProfile> = {
|
|
125
|
+
[workerProfile.id]: workerProfile,
|
|
126
|
+
[analystProfile.id]: analystProfile,
|
|
127
|
+
[driverProfile.id]: driverProfile,
|
|
128
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-checking test for runRefineLoop (bench is a tsx-script package — no vitest).
|
|
3
|
+
* tsx src/refine-loop.test.mts (non-zero exit on failure)
|
|
4
|
+
*
|
|
5
|
+
* Regressions it defends — the behaviors the 7 forked loops all relied on, now
|
|
6
|
+
* owned by ONE atom:
|
|
7
|
+
* - no judge → run all k rounds; prompt carries prior rounds (history) forward;
|
|
8
|
+
* - judge wired → stop on the first valid round (default decide), no extra shots;
|
|
9
|
+
* - a custom `decide` overrides;
|
|
10
|
+
* - the execution Ctx is created once and threaded to every prompt + shot;
|
|
11
|
+
* - teardown ALWAYS runs (finally), even when a shot throws.
|
|
12
|
+
*/
|
|
13
|
+
import assert from 'node:assert/strict'
|
|
14
|
+
import { runRefineLoop } from './refine-loop.ts'
|
|
15
|
+
|
|
16
|
+
async function run(): Promise<void> {
|
|
17
|
+
// (1) no judge → all k rounds; carry-forward via history
|
|
18
|
+
const prompts: string[] = []
|
|
19
|
+
const r1 = await runRefineLoop<string>({
|
|
20
|
+
rounds: 3,
|
|
21
|
+
prompt: (round, history) => {
|
|
22
|
+
const p = round === 1 ? 'base' : `base+prev:${history[history.length - 1]?.artifact}`
|
|
23
|
+
prompts.push(p)
|
|
24
|
+
return p
|
|
25
|
+
},
|
|
26
|
+
runShot: async (_p, round) => ({ artifact: `r${round}` }),
|
|
27
|
+
})
|
|
28
|
+
assert.equal(r1.rounds.length, 3, 'runs all k with no judge')
|
|
29
|
+
assert.equal(r1.blind.artifact, 'r1')
|
|
30
|
+
assert.equal(r1.final.artifact, 'r3')
|
|
31
|
+
assert.equal(r1.resolved, false, 'no judge → not resolved')
|
|
32
|
+
assert.equal(prompts[1], 'base+prev:r1', 'round 2 carries round 1 artifact')
|
|
33
|
+
assert.equal(prompts[2], 'base+prev:r2', 'round 3 carries round 2 artifact')
|
|
34
|
+
|
|
35
|
+
// (2) judge → stop on the first valid round, no extra shots
|
|
36
|
+
let shots = 0
|
|
37
|
+
const r2 = await runRefineLoop<string>({
|
|
38
|
+
rounds: 5,
|
|
39
|
+
prompt: () => 'p',
|
|
40
|
+
runShot: async () => {
|
|
41
|
+
shots += 1
|
|
42
|
+
return { artifact: shots === 2 ? 'good' : 'bad' }
|
|
43
|
+
},
|
|
44
|
+
judge: async (a) => ({ valid: a === 'good', score: a === 'good' ? 1 : 0 }),
|
|
45
|
+
})
|
|
46
|
+
assert.equal(r2.rounds.length, 2, 'stops at the first valid round')
|
|
47
|
+
assert.equal(r2.resolved, true)
|
|
48
|
+
assert.equal(shots, 2, 'no extra shots after stop')
|
|
49
|
+
|
|
50
|
+
// (3) a custom decide overrides the default
|
|
51
|
+
const r3 = await runRefineLoop<number>({
|
|
52
|
+
rounds: 9,
|
|
53
|
+
prompt: () => 'p',
|
|
54
|
+
runShot: async (_p, round) => ({ artifact: round }),
|
|
55
|
+
decide: (h) => h.length >= 2,
|
|
56
|
+
})
|
|
57
|
+
assert.equal(r3.rounds.length, 2, 'custom decide stops at 2')
|
|
58
|
+
|
|
59
|
+
// (4) Ctx created once + threaded to prompt and shot
|
|
60
|
+
let tornDown = 0
|
|
61
|
+
const seen: string[] = []
|
|
62
|
+
await runRefineLoop<string, { id: string }>({
|
|
63
|
+
rounds: 2,
|
|
64
|
+
setup: async () => ({ id: 'ctx-1' }),
|
|
65
|
+
prompt: (_r, _h, ctx) => {
|
|
66
|
+
seen.push(`prompt:${ctx.id}`)
|
|
67
|
+
return 'p'
|
|
68
|
+
},
|
|
69
|
+
runShot: async (_p, _r, ctx) => {
|
|
70
|
+
seen.push(`shot:${ctx.id}`)
|
|
71
|
+
return { artifact: 'a' }
|
|
72
|
+
},
|
|
73
|
+
teardown: async (ctx) => {
|
|
74
|
+
assert.equal(ctx.id, 'ctx-1')
|
|
75
|
+
tornDown += 1
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
assert.equal(tornDown, 1, 'teardown runs exactly once')
|
|
79
|
+
assert.ok(seen.includes('prompt:ctx-1') && seen.includes('shot:ctx-1'), 'ctx threaded to prompt + shot')
|
|
80
|
+
|
|
81
|
+
// (5) teardown runs even when a shot throws (finally)
|
|
82
|
+
let tornDown2 = 0
|
|
83
|
+
await assert.rejects(
|
|
84
|
+
runRefineLoop<string, { id: string }>({
|
|
85
|
+
rounds: 3,
|
|
86
|
+
setup: async () => ({ id: 'c' }),
|
|
87
|
+
prompt: () => 'p',
|
|
88
|
+
runShot: async (_p, round) => {
|
|
89
|
+
if (round === 2) throw new Error('boom')
|
|
90
|
+
return { artifact: 'a' }
|
|
91
|
+
},
|
|
92
|
+
teardown: async () => {
|
|
93
|
+
tornDown2 += 1
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
/boom/,
|
|
97
|
+
)
|
|
98
|
+
assert.equal(tornDown2, 1, 'teardown runs on throw')
|
|
99
|
+
|
|
100
|
+
console.log('refine-loop.test: all assertions passed')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
run().catch((e) => {
|
|
104
|
+
console.error('refine-loop.test FAILED:', e instanceof Error ? e.message : e)
|
|
105
|
+
process.exit(1)
|
|
106
|
+
})
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runRefineLoop — the ONE shared k-shot loop (see docs/architecture.md §1, §12).
|
|
3
|
+
*
|
|
4
|
+
* It replaces the seven hand-rolled `for (round 1..k) { shot → judge → decide →
|
|
5
|
+
* carry-forward }` copies across the workers (~700 LOC of duplicated skeleton).
|
|
6
|
+
* The worker is an OPAQUE `runShot`; this loop owns iteration, carry-forward
|
|
7
|
+
* steering, optional per-round judging + early stop, and round bookkeeping.
|
|
8
|
+
*
|
|
9
|
+
* Two carry-forward channels, both first-class (the extraction found both in the
|
|
10
|
+
* wild and they must stay pluggable):
|
|
11
|
+
* - EXECUTION CONTEXT (`Ctx`): created once by `setup`, threaded to every shot,
|
|
12
|
+
* torn down by `teardown`. This is how filesystem/session state carries — a
|
|
13
|
+
* cloned repo whose edits persist (SWE refine), a shared sandbox box whose
|
|
14
|
+
* session persists (sandbox research), or a scratch dir.
|
|
15
|
+
* - PROMPT (`prompt(round, history, ctx)`): how textual state carries — round 1
|
|
16
|
+
* is the blind prompt; rounds 2+ fold prior rounds' artifacts + a directive.
|
|
17
|
+
*
|
|
18
|
+
* Judging is OPTIONAL: workers that run all k rounds and let the orchestrator
|
|
19
|
+
* judge omit `judge`/`decide` (the loop runs to budget); a loop that should stop
|
|
20
|
+
* on the first valid answer wires `judge` (default `decide` = stop-on-valid).
|
|
21
|
+
*
|
|
22
|
+
* This is the inference-timescale instance of the spine's atom: `runShot` is the
|
|
23
|
+
* worker Agent's `act→Output`; `prompt` is the driver Agent's `act→steer`. It is
|
|
24
|
+
* deliberately corpus-agnostic — callers map `RefineLoopResult` to a RunRecord.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Minimal per-round verdict — `valid` gates early-stop, `score` is informational. */
|
|
28
|
+
export interface RoundVerdict {
|
|
29
|
+
valid: boolean
|
|
30
|
+
score?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RoundRecord<Artifact> {
|
|
34
|
+
/** 1-based round index. */
|
|
35
|
+
round: number
|
|
36
|
+
prompt: string
|
|
37
|
+
artifact: Artifact
|
|
38
|
+
/** Present only when a `judge` is wired. */
|
|
39
|
+
verdict?: RoundVerdict
|
|
40
|
+
/** Non-fatal note (e.g. a liveness backstop fired this round). */
|
|
41
|
+
note?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RefineLoopSpec<Artifact, Ctx = void> {
|
|
45
|
+
/** Max shots. Always ≥ 1. */
|
|
46
|
+
rounds: number
|
|
47
|
+
/** Build the per-task execution context once (clone repo / create box / mkdtemp). */
|
|
48
|
+
setup?: () => Promise<Ctx>
|
|
49
|
+
/** Round `r`'s prompt. `r === 1` is the blind prompt; `r > 1` carries `history`. */
|
|
50
|
+
prompt: (round: number, history: ReadonlyArray<RoundRecord<Artifact>>, ctx: Ctx) => string
|
|
51
|
+
/** Run ONE shot — the opaque worker (local spawn / sandbox stream / router chat). */
|
|
52
|
+
runShot: (
|
|
53
|
+
prompt: string,
|
|
54
|
+
round: number,
|
|
55
|
+
ctx: Ctx,
|
|
56
|
+
) => Promise<{ artifact: Artifact; note?: string }>
|
|
57
|
+
/** Optional per-round judge. Omit to run all `rounds` (orchestrator judges later). */
|
|
58
|
+
judge?: (artifact: Artifact, round: number) => Promise<RoundVerdict>
|
|
59
|
+
/** Stop after this round? Default: stop once a round's verdict is valid (else run to budget). */
|
|
60
|
+
decide?: (history: ReadonlyArray<RoundRecord<Artifact>>) => boolean
|
|
61
|
+
/** Always runs (even on throw) when `setup` ran. */
|
|
62
|
+
teardown?: (ctx: Ctx) => Promise<void>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RefineLoopResult<Artifact> {
|
|
66
|
+
/** Every executed round in order. */
|
|
67
|
+
rounds: RoundRecord<Artifact>[]
|
|
68
|
+
/** Round 1 — the blind artifact. */
|
|
69
|
+
blind: RoundRecord<Artifact>
|
|
70
|
+
/** The last executed round. */
|
|
71
|
+
final: RoundRecord<Artifact>
|
|
72
|
+
/** True iff any round was judged valid (only meaningful when `judge` is wired). */
|
|
73
|
+
resolved: boolean
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const defaultDecide = <Artifact>(history: ReadonlyArray<RoundRecord<Artifact>>): boolean =>
|
|
77
|
+
history[history.length - 1]?.verdict?.valid === true
|
|
78
|
+
|
|
79
|
+
export async function runRefineLoop<Artifact, Ctx = void>(
|
|
80
|
+
spec: RefineLoopSpec<Artifact, Ctx>,
|
|
81
|
+
): Promise<RefineLoopResult<Artifact>> {
|
|
82
|
+
const rounds = Math.max(1, spec.rounds)
|
|
83
|
+
const decide = spec.decide ?? defaultDecide
|
|
84
|
+
let ctxSet = false
|
|
85
|
+
let ctx = undefined as Ctx
|
|
86
|
+
try {
|
|
87
|
+
if (spec.setup) {
|
|
88
|
+
ctx = await spec.setup()
|
|
89
|
+
ctxSet = true
|
|
90
|
+
}
|
|
91
|
+
const history: RoundRecord<Artifact>[] = []
|
|
92
|
+
for (let r = 1; r <= rounds; r += 1) {
|
|
93
|
+
const prompt = spec.prompt(r, history, ctx)
|
|
94
|
+
const { artifact, note } = await spec.runShot(prompt, r, ctx)
|
|
95
|
+
const verdict = spec.judge ? await spec.judge(artifact, r) : undefined
|
|
96
|
+
history.push({ round: r, prompt, artifact, verdict, note })
|
|
97
|
+
if (decide(history)) break
|
|
98
|
+
}
|
|
99
|
+
// The loop runs ≥ 1 round, so history is non-empty.
|
|
100
|
+
const blind = history[0] as RoundRecord<Artifact>
|
|
101
|
+
const final = history[history.length - 1] as RoundRecord<Artifact>
|
|
102
|
+
return { rounds: history, blind, final, resolved: history.some((h) => h.verdict?.valid === true) }
|
|
103
|
+
} finally {
|
|
104
|
+
if (ctxSet && spec.teardown) await spec.teardown(ctx)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Research leaderboard — model × web-search-provider × multi-shot, on the research
|
|
3
|
+
* benches (finsearchcomp / frames / hotpotqa / simpleqa). This is the OFF-SANDBOX
|
|
4
|
+
* RAG baseline: per shot, (1) provider-pinned web search via the router's proven
|
|
5
|
+
* `/v1/search?provider=<id>` + `web_fetch` of the top-K result pages, (2) answer with
|
|
6
|
+
* the evidence in context (no tools → `content` always present, so every arm differs
|
|
7
|
+
* ONLY by the search provider — a clean controlled A/B). `SEARCH=default` skips search
|
|
8
|
+
* (parametric control). Pure router HTTP (bearer `TANGLE_API_KEY`) — never touches the
|
|
9
|
+
* sandbox, so it never contends with sandbox-bound gates.
|
|
10
|
+
*
|
|
11
|
+
* The retrieve→answer body is the shared `runResearchShot` (research-shot.ts). Reuses
|
|
12
|
+
* `runPool` (bounded concurrency), `appendRunRecord` (the durable corpus), and the bench's own
|
|
13
|
+
* `adapter.judge`; nothing is reinvented. The AGENTIC HARNESS regime (opencode/pi multi-turn in
|
|
14
|
+
* a box) runs through the gate (`runGate`) with a sandbox-backed executor; this
|
|
15
|
+
* file is the flat, non-agentic search-RAG baseline.
|
|
16
|
+
*
|
|
17
|
+
* Each shot's answer is graded by the bench judge; writes one corpus RunRecord/task
|
|
18
|
+
* tagged `search:<provider>` + `model` so the leaderboard slices by arm. Fault-isolated
|
|
19
|
+
* (a flaky call → a NO-ANSWER attempt, never a throw).
|
|
20
|
+
*
|
|
21
|
+
* dotenvx run -f ~/company/devops/secrets/.env.keys -f ~/company/devops/secrets/agent-state.env -- \
|
|
22
|
+
* env BENCH=simpleqa SIMPLEQA_FIXTURES=1 MODEL=gpt-4o-mini SEARCH=you N=10 K=1 \
|
|
23
|
+
* CONCURRENCY=3 JUDGE_MODEL=gpt-4o-mini CORPUS=/tmp/research-you.jsonl tsx src/research-gate.mts
|
|
24
|
+
* tsx src/corpus-report.mts <armA.jsonl> <armB.jsonl> # paired-bootstrap across arms
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { ADAPTERS } from './adapters'
|
|
28
|
+
import { type AttemptRecord, appendRunRecord, type RunRecord } from './corpus'
|
|
29
|
+
import { runResearchShot, type ShotCfg } from './research-shot'
|
|
30
|
+
import { runPool } from './run-pool'
|
|
31
|
+
|
|
32
|
+
function must(name: string): string {
|
|
33
|
+
const v = process.env[name]
|
|
34
|
+
if (!v) throw new Error(`env ${name} is required`)
|
|
35
|
+
return v
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main(): Promise<void> {
|
|
39
|
+
const benchName = process.env.BENCH ?? 'finsearchcomp'
|
|
40
|
+
const makeAdapter = ADAPTERS[benchName]
|
|
41
|
+
if (!makeAdapter) throw new Error(`unknown BENCH=${benchName} (have: ${Object.keys(ADAPTERS).join(', ')})`)
|
|
42
|
+
|
|
43
|
+
const n = Number(process.env.N ?? 6)
|
|
44
|
+
const k = Number(process.env.K ?? 3)
|
|
45
|
+
const model = process.env.MODEL ?? process.env.WORKER_MODEL ?? 'deepseek-v4-flash'
|
|
46
|
+
const search = process.env.SEARCH ?? 'default'
|
|
47
|
+
const cfg: ShotCfg = {
|
|
48
|
+
model,
|
|
49
|
+
search,
|
|
50
|
+
maxResults: Number(process.env.SEARCH_MAX_RESULTS ?? 5),
|
|
51
|
+
fetchTopK: Number(process.env.FETCH_TOP_K ?? 3),
|
|
52
|
+
temperature: Number(process.env.TEMPERATURE ?? 0.7),
|
|
53
|
+
routerBaseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
|
|
54
|
+
routerKey: must('TANGLE_API_KEY'), // worker (router) + bench judge both need it
|
|
55
|
+
timeoutMs: process.env.SHOT_TIMEOUT_MS ? Number(process.env.SHOT_TIMEOUT_MS) : 600_000,
|
|
56
|
+
}
|
|
57
|
+
const concurrency = Number(process.env.CONCURRENCY ?? 3)
|
|
58
|
+
const corpusPath = process.env.CORPUS ?? `/tmp/research-${benchName}-${model.replace(/[^a-z0-9]/gi, '_')}-${search}.jsonl`
|
|
59
|
+
if (!Number.isInteger(n) || n < 1) throw new Error(`N must be a positive integer, got ${process.env.N}`)
|
|
60
|
+
if (!Number.isInteger(k) || k < 1) throw new Error(`K must be a positive integer, got ${process.env.K}`)
|
|
61
|
+
|
|
62
|
+
const adapter = makeAdapter()
|
|
63
|
+
console.log(`=== research gate (router-RAG) · bench=${benchName} · model=${model} · search=${search} · N=${n} K=${k} conc=${concurrency} ===`)
|
|
64
|
+
await adapter.preflight()
|
|
65
|
+
const tasks = await adapter.loadTasks({ limit: n })
|
|
66
|
+
console.log(`loaded ${tasks.length} task(s)`)
|
|
67
|
+
|
|
68
|
+
// Phase 1 — rollouts, concurrent (the shared runPool), off-sandbox. Fault-isolated.
|
|
69
|
+
const units = tasks.flatMap((task) => Array.from({ length: k }, (_, attempt) => ({ task, attempt })))
|
|
70
|
+
console.log(`\n▶ phase 1: ${units.length} rollouts (conc=${concurrency}) · search=${search}`)
|
|
71
|
+
const shots = await runPool(units, concurrency, async (u) => {
|
|
72
|
+
const s = await runResearchShot(u.task.prompt, u.task.id, u.attempt, cfg)
|
|
73
|
+
console.log(` rollout ${u.task.id}#${u.attempt}: ${s.ok ? `answer ${s.answer.length}B · ${s.searches} search(es)` : `NO ANSWER (${s.detail})`} (${(s.wallMs / 1000) | 0}s)`)
|
|
74
|
+
return s
|
|
75
|
+
})
|
|
76
|
+
const shotOf = (id: string, i: number) => shots.find((o) => o.value?.taskId === id && o.value?.attempt === i)?.value
|
|
77
|
+
|
|
78
|
+
// Phase 2 — judge via the bench's OWN judge; write one RunRecord/task (the shared corpus).
|
|
79
|
+
console.log(`\n▶ phase 2: judging via ${adapter.name} judge → ${corpusPath}`)
|
|
80
|
+
let scoredTasks = 0
|
|
81
|
+
for (const task of tasks) {
|
|
82
|
+
const attempts: AttemptRecord[] = []
|
|
83
|
+
for (let i = 0; i < k; i += 1) {
|
|
84
|
+
const s = shotOf(task.id, i)
|
|
85
|
+
let sc: { score: number; resolved: boolean } | undefined
|
|
86
|
+
if (s?.ok) {
|
|
87
|
+
try {
|
|
88
|
+
const v = await adapter.judge(task, s.answer)
|
|
89
|
+
sc = { score: v.score, resolved: v.resolved }
|
|
90
|
+
console.log(` judge ${task.id}#${i}: score=${(v.score * 100).toFixed(1)}% resolved=${v.resolved}`)
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.log(` judge ${task.id}#${i}: ERROR ${(err instanceof Error ? err.message : String(err)).slice(0, 160)}`)
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
console.log(` judge ${task.id}#${i}: skipped (no answer)`)
|
|
96
|
+
}
|
|
97
|
+
attempts.push({
|
|
98
|
+
round: i,
|
|
99
|
+
prompt: `research:${search}`,
|
|
100
|
+
output: s?.answer ?? '',
|
|
101
|
+
...(sc ? { valid: sc.resolved, score: sc.score } : {}),
|
|
102
|
+
wallMs: s?.wallMs ?? 0,
|
|
103
|
+
eventCount: s?.searches ?? 0,
|
|
104
|
+
eventTypes: { 'web_search.requests': s?.searches ?? 0 },
|
|
105
|
+
traceTail: (s?.answer ?? '').slice(-600),
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
if (attempts.some((a) => a.score !== undefined)) scoredTasks += 1
|
|
109
|
+
const record: RunRecord = {
|
|
110
|
+
ts: new Date().toISOString(),
|
|
111
|
+
benchmark: adapter.name,
|
|
112
|
+
instanceId: task.id,
|
|
113
|
+
condition: `search:${search}`,
|
|
114
|
+
model,
|
|
115
|
+
blindResolved: attempts[0]?.valid === true,
|
|
116
|
+
resolved: attempts.some((a) => a.valid === true),
|
|
117
|
+
attempts,
|
|
118
|
+
infraError: false,
|
|
119
|
+
}
|
|
120
|
+
await appendRunRecord(corpusPath, record)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(
|
|
124
|
+
`\n=== wrote ${tasks.length} task(s) (${scoredTasks} with ≥1 scored attempt) → ${corpusPath} ===\n` +
|
|
125
|
+
` arm = model:${model} × search:${search} · compare arms: tsx src/corpus-report.mts <armA.jsonl> <armB.jsonl>`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main().catch((err) => {
|
|
130
|
+
console.error(`research-gate: ${err instanceof Error ? err.message : String(err)}`)
|
|
131
|
+
process.exit(1)
|
|
132
|
+
})
|