@tangle-network/agent-bench 0.7.1 → 0.8.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/CHANGELOG.md +8 -0
- package/HARNESS.md +1 -1
- package/dist/adapters.js +2 -2
- 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/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/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/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
package/src/atom-humaneval.mts
CHANGED
|
@@ -21,20 +21,23 @@ import {
|
|
|
21
21
|
type AgentProfile,
|
|
22
22
|
type AgentSpec,
|
|
23
23
|
contentAddress,
|
|
24
|
-
|
|
25
|
-
driverAgent,
|
|
24
|
+
createExecutor,
|
|
26
25
|
createExecutorRegistry,
|
|
27
26
|
createSupervisor,
|
|
28
|
-
type Executor,
|
|
29
|
-
type ExecutorResult,
|
|
30
27
|
gateOnDeliverable,
|
|
31
28
|
InMemoryResultBlobStore,
|
|
32
29
|
InMemorySpawnJournal,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
routerChatWithUsage,
|
|
30
|
+
mapExecutorResult,
|
|
31
|
+
supervisorAgent,
|
|
36
32
|
} from '../../src/runtime/index'
|
|
37
33
|
import { basePrompt, extractCode, type HumanEvalTask, loadHumanEval, runChecker } from './benchmarks/humaneval'
|
|
34
|
+
import {
|
|
35
|
+
benchProfileModel,
|
|
36
|
+
benchRouterProfile,
|
|
37
|
+
type BenchRouterTarget,
|
|
38
|
+
runBenchRouterTurn,
|
|
39
|
+
withBenchProfile,
|
|
40
|
+
} from './router-turn'
|
|
38
41
|
|
|
39
42
|
function must(k: string): string {
|
|
40
43
|
const v = process.env[k]
|
|
@@ -47,45 +50,48 @@ const K = Number(process.env.K ?? 3)
|
|
|
47
50
|
const OFFSET = Number(process.env.OFFSET ?? 0)
|
|
48
51
|
const WORKER_TEMP = Number(process.env.WORKER_TEMP ?? 0.7)
|
|
49
52
|
|
|
50
|
-
const cfg:
|
|
53
|
+
const cfg: BenchRouterTarget = {
|
|
51
54
|
routerBaseUrl: process.env.ROUTER_BASE ?? 'https://router.tangle.tools/v1',
|
|
52
55
|
routerKey: must('TANGLE_API_KEY'),
|
|
53
|
-
|
|
56
|
+
profile: benchRouterProfile(
|
|
57
|
+
'humaneval-worker',
|
|
58
|
+
process.env.WORKER_MODEL ?? 'deepseek-v4-flash',
|
|
59
|
+
{ temperature: WORKER_TEMP },
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
const driverCfg: BenchRouterTarget = {
|
|
63
|
+
...cfg,
|
|
64
|
+
profile: benchRouterProfile(
|
|
65
|
+
'humaneval-driver',
|
|
66
|
+
process.env.DRIVER_MODEL ?? benchProfileModel(cfg.profile),
|
|
67
|
+
{ maxTurns: K + 4 },
|
|
68
|
+
),
|
|
54
69
|
}
|
|
55
|
-
const driverCfg: RouterConfig = { ...cfg, model: process.env.DRIVER_MODEL ?? cfg.model }
|
|
56
|
-
|
|
57
|
-
// The driver-LLM brain is the SHARED `routerBrain` (the canonical ToolLoopChat seam) — it forwards
|
|
58
|
-
// usage/costUsd, so this bench's driver arms meter their own inference into the conserved pool.
|
|
59
70
|
|
|
60
71
|
// ── A gated router worker: one router call → candidate code, settled valid ⟺ the tests pass ──
|
|
61
72
|
function humanEvalWorker(task: HumanEvalTask, label: string): Agent<unknown, unknown> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
async execute(_t, signal) {
|
|
66
|
-
const res = await routerChatWithUsage(cfg, [{ role: 'user', content: basePrompt(task) }], {
|
|
67
|
-
temperature: WORKER_TEMP,
|
|
68
|
-
...(signal ? { signal } : {}),
|
|
69
|
-
})
|
|
70
|
-
const code = extractCode(res.content)
|
|
71
|
-
artifact = {
|
|
72
|
-
outRef: contentAddress(code),
|
|
73
|
-
out: code,
|
|
74
|
-
spent: { iterations: 1, tokens: res.usage ?? { input: 0, output: 0 }, usd: res.costUsd ?? 0, ms: 0 },
|
|
75
|
-
}
|
|
76
|
-
return artifact
|
|
77
|
-
},
|
|
78
|
-
teardown: () => Promise.resolve({ destroyed: true }),
|
|
79
|
-
resultArtifact: () => {
|
|
80
|
-
if (!artifact) throw new Error('resultArtifact read before execute')
|
|
81
|
-
return artifact
|
|
82
|
-
},
|
|
83
|
-
}
|
|
84
|
-
const gated = gateOnDeliverable(inner, {
|
|
85
|
-
check: async (out) => (await runChecker(task, String(out))).pass === 1,
|
|
86
|
-
describe: `${task.taskId}: the provided test suite passes`,
|
|
73
|
+
const profile: AgentProfile = withBenchProfile(cfg.profile, {
|
|
74
|
+
name: label,
|
|
75
|
+
systemPrompt: basePrompt(task),
|
|
87
76
|
})
|
|
88
|
-
const
|
|
77
|
+
const routerFactory = createExecutor({
|
|
78
|
+
backend: 'router',
|
|
79
|
+
routerBaseUrl: cfg.routerBaseUrl,
|
|
80
|
+
routerKey: cfg.routerKey,
|
|
81
|
+
})
|
|
82
|
+
const executorFactory = (spec: AgentSpec, ctx: Parameters<typeof routerFactory>[1]) => {
|
|
83
|
+
const inner = routerFactory(spec, ctx)
|
|
84
|
+
const mapped = mapExecutorResult(inner, (result) => {
|
|
85
|
+
const raw = result.out as { content?: unknown }
|
|
86
|
+
const code = extractCode(typeof raw?.content === 'string' ? raw.content : '')
|
|
87
|
+
return { outRef: contentAddress(code), out: code }
|
|
88
|
+
})
|
|
89
|
+
return gateOnDeliverable(mapped, {
|
|
90
|
+
check: async (out) => (await runChecker(task, String(out))).pass === 1,
|
|
91
|
+
describe: `${task.taskId}: the provided test suite passes`,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
const spec: AgentSpec = { profile, harness: null, executorFactory }
|
|
89
95
|
return { name: label, act: async () => '', executorSpec: spec } as Agent<unknown, unknown> & {
|
|
90
96
|
executorSpec: AgentSpec
|
|
91
97
|
}
|
|
@@ -113,16 +119,21 @@ async function driveTask(
|
|
|
113
119
|
spawns += 1
|
|
114
120
|
return w
|
|
115
121
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
const root = supervisorAgent(
|
|
123
|
+
withBenchProfile(driverCfg.profile, {
|
|
124
|
+
name: `drv-${task.taskId}`,
|
|
125
|
+
systemPrompt: driverSystem,
|
|
126
|
+
}),
|
|
127
|
+
{
|
|
128
|
+
router: {
|
|
129
|
+
routerBaseUrl: driverCfg.routerBaseUrl,
|
|
130
|
+
routerKey: driverCfg.routerKey,
|
|
131
|
+
},
|
|
132
|
+
blobs,
|
|
133
|
+
makeWorkerAgent: makeWorker,
|
|
134
|
+
perWorker: { maxIterations: 2, maxTokens: 4000 },
|
|
135
|
+
},
|
|
136
|
+
)
|
|
126
137
|
const runId = `he-${task.taskId.replace('/', '-')}`
|
|
127
138
|
const result = await createSupervisor<unknown, unknown>().run(root, basePrompt(task), {
|
|
128
139
|
budget: { maxIterations: 100, maxTokens: 400_000 },
|
|
@@ -145,21 +156,31 @@ async function blindTask(task: HumanEvalTask): Promise<boolean> {
|
|
|
145
156
|
for (let i = 0; i < K; i += 1) {
|
|
146
157
|
// A transient router error is a FAILED attempt, not a crash — the driver arm already types
|
|
147
158
|
// an executor throw into a `down` settlement, so the blind arm must match (fair comparison).
|
|
148
|
-
let
|
|
159
|
+
let content = ''
|
|
149
160
|
try {
|
|
150
|
-
res = await
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
const res = await runBenchRouterTurn(
|
|
162
|
+
{
|
|
163
|
+
routerBaseUrl: cfg.routerBaseUrl,
|
|
164
|
+
routerKey: cfg.routerKey,
|
|
165
|
+
profile: withBenchProfile(cfg.profile, {
|
|
166
|
+
name: 'humaneval-blind-atom-worker',
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
basePrompt(task),
|
|
170
|
+
)
|
|
171
|
+
content = res.finalText
|
|
153
172
|
} catch {
|
|
154
173
|
continue
|
|
155
174
|
}
|
|
156
|
-
if ((await runChecker(task, extractCode(
|
|
175
|
+
if ((await runChecker(task, extractCode(content))).pass === 1) return true
|
|
157
176
|
}
|
|
158
177
|
return false
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
async function main(): Promise<void> {
|
|
162
|
-
console.log(
|
|
181
|
+
console.log(
|
|
182
|
+
`atom-humaneval: N=${N} K=${K} offset=${OFFSET} worker=${benchProfileModel(cfg.profile)} driver=${benchProfileModel(driverCfg.profile)}`,
|
|
183
|
+
)
|
|
163
184
|
const tasks = await loadHumanEval(N, OFFSET)
|
|
164
185
|
const outcomes: TaskOutcome[] = []
|
|
165
186
|
for (const task of tasks) {
|
package/src/atom-mcp-e2e.mts
CHANGED
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
type Agent,
|
|
22
22
|
type AgentProfile,
|
|
23
23
|
type AgentSpec,
|
|
24
|
+
collectAgentTurn,
|
|
24
25
|
contentAddress,
|
|
26
|
+
createExecutor,
|
|
25
27
|
createExecutorRegistry,
|
|
26
28
|
createSupervisor,
|
|
27
29
|
type Executor,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
InMemorySpawnJournal,
|
|
32
34
|
runInWorkspace,
|
|
33
35
|
type Scope,
|
|
36
|
+
streamAgentTurn,
|
|
34
37
|
type Workspace,
|
|
35
38
|
} from '../../src/runtime/index'
|
|
36
39
|
import { asAuthoredProfile } from '../../src/runtime/supervise/authoring'
|
|
@@ -83,19 +86,30 @@ async function bridgeChat(opts: {
|
|
|
83
86
|
cwd?: string
|
|
84
87
|
mcpUrl?: string
|
|
85
88
|
}): Promise<string> {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
if (!BEARER) throw new Error('TANGLE_API_KEY is required')
|
|
90
|
+
const profile: AgentProfile = {
|
|
91
|
+
name: opts.mcpUrl ? 'atom-mcp-supervisor-turn' : 'atom-mcp-worker-turn',
|
|
92
|
+
model: { default: MODEL },
|
|
93
|
+
...(opts.mcpUrl
|
|
94
|
+
? { mcp: { coordination: { transport: 'http', url: opts.mcpUrl } } }
|
|
95
|
+
: {}),
|
|
96
|
+
}
|
|
97
|
+
const factory = createExecutor({
|
|
98
|
+
backend: 'bridge',
|
|
99
|
+
bridgeUrl: BRIDGE.replace(/\/v1$/u, ''),
|
|
100
|
+
bridgeBearer: BEARER,
|
|
101
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
95
102
|
})
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
const turn = await collectAgentTurn(
|
|
104
|
+
streamAgentTurn(
|
|
105
|
+
{ kind: 'executor', factory, profile },
|
|
106
|
+
opts.messages.map((message) => message.content).join('\n\n'),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
if (turn.status !== 'completed') {
|
|
110
|
+
throw new Error(turn.error?.message ?? `bridge turn ended with ${turn.status}`)
|
|
111
|
+
}
|
|
112
|
+
return turn.finalText
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
const transcripts: Array<{ who: string; said: string; delivered?: boolean }> = []
|
|
@@ -109,7 +123,7 @@ function makeWorker(rawProfile: unknown, ws: Workspace, n: number): Agent<unknow
|
|
|
109
123
|
const inner: Executor<unknown> = {
|
|
110
124
|
runtime: 'router',
|
|
111
125
|
async execute() {
|
|
112
|
-
const sys = p?.systemPrompt ?? TASK
|
|
126
|
+
const sys = p?.prompt.systemPrompt ?? TASK
|
|
113
127
|
const run = await runInWorkspace(
|
|
114
128
|
ws,
|
|
115
129
|
async (cwd) => {
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
10
|
import { test } from 'node:test'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
appworldReactResultWithUsage,
|
|
13
|
+
appworldReactUsageEvent,
|
|
14
|
+
appworldSolutionOutput,
|
|
15
|
+
appworldToolLoopClient,
|
|
16
|
+
createAppWorldAdapter,
|
|
17
|
+
} from './appworld'
|
|
12
18
|
|
|
13
19
|
type Events = Parameters<typeof appworldSolutionOutput.parse>[0]
|
|
14
20
|
const stream = (text: string): Events => [{ data: { finalText: text } }] as unknown as Events
|
|
@@ -49,3 +55,115 @@ test('loadTasks either enumerates live engine rows or FAILS LOUD without fabrica
|
|
|
49
55
|
assert.match((err as Error).message, /appworld driver failed|appworld import failed/)
|
|
50
56
|
}
|
|
51
57
|
})
|
|
58
|
+
|
|
59
|
+
test('successful react episode survives unknown catalog dollars without fabricating billed cost', () => {
|
|
60
|
+
const result = appworldReactResultWithUsage(
|
|
61
|
+
{ success: true, passes: 3, fails: 0, num_tests: 3 },
|
|
62
|
+
{
|
|
63
|
+
input: 120,
|
|
64
|
+
output: 30,
|
|
65
|
+
tokensKnown: true,
|
|
66
|
+
costUsd: 0.0042,
|
|
67
|
+
usdKnown: false,
|
|
68
|
+
},
|
|
69
|
+
2,
|
|
70
|
+
'completed task',
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert.equal(result.success, true)
|
|
74
|
+
assert.equal(result.input_tokens, 120)
|
|
75
|
+
assert.equal(result.output_tokens, 30)
|
|
76
|
+
assert.equal(result.cost_usd, undefined)
|
|
77
|
+
const event = appworldReactUsageEvent(result, 'deepseek-v4-flash')
|
|
78
|
+
assert.deepEqual(event?.data, {
|
|
79
|
+
model: 'deepseek-v4-flash',
|
|
80
|
+
tokensIn: 120,
|
|
81
|
+
tokensOut: 30,
|
|
82
|
+
})
|
|
83
|
+
assert.equal(Object.hasOwn(event?.data ?? {}, 'costUsd'), false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
interface TestToolLoopBox {
|
|
87
|
+
streamPrompt(
|
|
88
|
+
prompt: string,
|
|
89
|
+
opts?: { signal?: AbortSignal },
|
|
90
|
+
): AsyncGenerator<Record<string, unknown>>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function testToolLoopBox(client: unknown): Promise<TestToolLoopBox> {
|
|
94
|
+
return (client as { create(): Promise<TestToolLoopBox> }).create()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function drain(stream: AsyncGenerator<Record<string, unknown>>): Promise<void> {
|
|
98
|
+
for await (const _event of stream) {
|
|
99
|
+
// drain
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test('react client refuses an already-aborted round before a Python session or model call', async () => {
|
|
104
|
+
let sessionCalls = 0
|
|
105
|
+
let modelCalls = 0
|
|
106
|
+
const controller = new AbortController()
|
|
107
|
+
controller.abort(new Error('already stopped'))
|
|
108
|
+
const client = appworldToolLoopClient({
|
|
109
|
+
model: 'offline-model',
|
|
110
|
+
routerBaseUrl: 'https://router.invalid',
|
|
111
|
+
routerKey: 'offline',
|
|
112
|
+
runWorldSession: async () => {
|
|
113
|
+
sessionCalls += 1
|
|
114
|
+
throw new Error('unexpected world session')
|
|
115
|
+
},
|
|
116
|
+
complete: async () => {
|
|
117
|
+
modelCalls += 1
|
|
118
|
+
return {}
|
|
119
|
+
},
|
|
120
|
+
})
|
|
121
|
+
const box = await testToolLoopBox(client)
|
|
122
|
+
|
|
123
|
+
await assert.rejects(
|
|
124
|
+
drain(box.streamPrompt('@appworld-react task-1 test_normal\n', { signal: controller.signal })),
|
|
125
|
+
/already stopped/,
|
|
126
|
+
)
|
|
127
|
+
assert.equal(sessionCalls, 0)
|
|
128
|
+
assert.equal(modelCalls, 0)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('react client threads late abort to both the world session and Router call', async () => {
|
|
132
|
+
const controller = new AbortController()
|
|
133
|
+
let sessionSignal: AbortSignal | undefined
|
|
134
|
+
let modelSignal: AbortSignal | undefined
|
|
135
|
+
let sessionStopped = false
|
|
136
|
+
const client = appworldToolLoopClient({
|
|
137
|
+
model: 'offline-model',
|
|
138
|
+
routerBaseUrl: 'https://router.invalid',
|
|
139
|
+
routerKey: 'offline',
|
|
140
|
+
runWorldSession: async (_taskId, _split, signal, fn) => {
|
|
141
|
+
sessionSignal = signal
|
|
142
|
+
try {
|
|
143
|
+
return await fn(async () => ({ success: true, num_tests: 1, passes: 1 }), 'offline task')
|
|
144
|
+
} finally {
|
|
145
|
+
sessionStopped = signal.aborted
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
complete: async (_body, request) => {
|
|
149
|
+
modelSignal = request?.signal
|
|
150
|
+
return new Promise((_resolve, reject) => {
|
|
151
|
+
request?.signal?.addEventListener(
|
|
152
|
+
'abort',
|
|
153
|
+
() => reject(request.signal?.reason ?? new Error('aborted')),
|
|
154
|
+
{ once: true },
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
const box = await testToolLoopBox(client)
|
|
160
|
+
const running = drain(
|
|
161
|
+
box.streamPrompt('@appworld-react task-1 test_normal\n', { signal: controller.signal }),
|
|
162
|
+
)
|
|
163
|
+
setTimeout(() => controller.abort(new Error('late stop')), 0)
|
|
164
|
+
|
|
165
|
+
await assert.rejects(running, /aborted|late stop/i)
|
|
166
|
+
assert.equal(sessionSignal, controller.signal)
|
|
167
|
+
assert.equal(modelSignal?.aborted, true)
|
|
168
|
+
assert.equal(sessionStopped, true)
|
|
169
|
+
})
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
import { spawn } from 'node:child_process'
|
|
23
23
|
import { join } from 'node:path'
|
|
24
24
|
import { createInterface } from 'node:readline'
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
collectAgentTurn,
|
|
27
|
+
createExecutor,
|
|
28
|
+
type OutputAdapter,
|
|
29
|
+
streamAgentTurn,
|
|
30
|
+
type ToolSpec,
|
|
31
|
+
} from '@tangle-network/agent-runtime/kernel'
|
|
26
32
|
import { benchRoot, preflightVenvImports, runVenvScriptStdin, venvPython } from './_harness'
|
|
27
33
|
import type { BenchmarkAdapter, BenchScore, BenchTask, LoadOptions } from './types'
|
|
28
34
|
|
|
@@ -171,7 +177,7 @@ export function createAppWorldAdapter(): BenchmarkAdapter {
|
|
|
171
177
|
|
|
172
178
|
/**
|
|
173
179
|
* AppWorld in its NATIVE protocol, run by OUR runtime: the worker is
|
|
174
|
-
*
|
|
180
|
+
* Runtime's profile-bound `router-tools` executor with one tool —
|
|
175
181
|
* `execute_python` — bound to a persistent AppWorld world session. The driver's
|
|
176
182
|
* `session` subcommand is a dumb world shim (stdin JSONL: execute → output,
|
|
177
183
|
* evaluate → verdict); every inference turn, the metering, and the typed
|
|
@@ -190,7 +196,7 @@ export function createAppWorldAdapter(): BenchmarkAdapter {
|
|
|
190
196
|
* (AppWorld's evaluator ran in-world); judge() parses it, never re-executes.
|
|
191
197
|
*/
|
|
192
198
|
|
|
193
|
-
interface ReactResult {
|
|
199
|
+
export interface ReactResult {
|
|
194
200
|
success?: boolean
|
|
195
201
|
passes?: number
|
|
196
202
|
fails?: number
|
|
@@ -199,9 +205,61 @@ interface ReactResult {
|
|
|
199
205
|
turns?: number
|
|
200
206
|
input_tokens?: number
|
|
201
207
|
output_tokens?: number
|
|
208
|
+
cost_usd?: number
|
|
202
209
|
transcript?: string
|
|
203
210
|
}
|
|
204
211
|
|
|
212
|
+
interface ReactRuntimeUsage {
|
|
213
|
+
input: number
|
|
214
|
+
output: number
|
|
215
|
+
costUsd?: number
|
|
216
|
+
tokensKnown?: boolean
|
|
217
|
+
usdKnown?: boolean
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Preserve a completed scientific/task result even when one accounting dimension is incomplete.
|
|
221
|
+
* Unknown usage fields stay absent; later comparison/reporting can refuse a cost claim without
|
|
222
|
+
* discarding the episode's task evidence. */
|
|
223
|
+
export function appworldReactResultWithUsage(
|
|
224
|
+
verdict: ReactResult,
|
|
225
|
+
usage: ReactRuntimeUsage,
|
|
226
|
+
turns: number | undefined,
|
|
227
|
+
transcript: string,
|
|
228
|
+
): ReactResult {
|
|
229
|
+
return {
|
|
230
|
+
...verdict,
|
|
231
|
+
...(turns !== undefined ? { turns } : {}),
|
|
232
|
+
...(usage.tokensKnown === false
|
|
233
|
+
? {}
|
|
234
|
+
: { input_tokens: usage.input, output_tokens: usage.output }),
|
|
235
|
+
...(usage.usdKnown === false || usage.costUsd === undefined
|
|
236
|
+
? {}
|
|
237
|
+
: { cost_usd: usage.costUsd }),
|
|
238
|
+
transcript,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Emit only usage the Runtime actually knows. Catalog estimates never become observed dollars. */
|
|
243
|
+
export function appworldReactUsageEvent(
|
|
244
|
+
result: ReactResult,
|
|
245
|
+
model: string,
|
|
246
|
+
): { type: 'llm_call'; data: Record<string, unknown> } | undefined {
|
|
247
|
+
const hasTokens =
|
|
248
|
+
typeof result.input_tokens === 'number' && typeof result.output_tokens === 'number'
|
|
249
|
+
const hasCost = typeof result.cost_usd === 'number'
|
|
250
|
+
if (!hasTokens && !hasCost) return undefined
|
|
251
|
+
return {
|
|
252
|
+
type: 'llm_call',
|
|
253
|
+
data: {
|
|
254
|
+
model,
|
|
255
|
+
...(hasTokens
|
|
256
|
+
? { tokensIn: result.input_tokens, tokensOut: result.output_tokens }
|
|
257
|
+
: {}),
|
|
258
|
+
...(hasCost ? { costUsd: result.cost_usd } : {}),
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
205
263
|
const REACT_HEADER = /^@appworld-react (\S+) (\S+)\n?/
|
|
206
264
|
|
|
207
265
|
const SESSION_SYSTEM = [
|
|
@@ -231,11 +289,17 @@ const EXECUTE_TOOL: ToolSpec = {
|
|
|
231
289
|
async function withWorldSession<T>(
|
|
232
290
|
taskId: string,
|
|
233
291
|
split: string,
|
|
292
|
+
signal: AbortSignal,
|
|
234
293
|
fn: (call: (cmd: Record<string, unknown>) => Promise<Record<string, unknown>>, instruction: string) => Promise<T>,
|
|
235
294
|
): Promise<T> {
|
|
295
|
+
signal.throwIfAborted()
|
|
236
296
|
const child = spawn(venvPython, [DRIVER, 'session', '--task-id', taskId, '--split', split], {
|
|
237
297
|
cwd: benchRoot,
|
|
238
298
|
})
|
|
299
|
+
const stopChild = (): void => {
|
|
300
|
+
if (!child.killed) child.kill('SIGTERM')
|
|
301
|
+
}
|
|
302
|
+
signal.addEventListener('abort', stopChild, { once: true })
|
|
239
303
|
const rl = createInterface({ input: child.stdout })
|
|
240
304
|
const pending: Array<(line: string) => void> = []
|
|
241
305
|
const backlog: string[] = []
|
|
@@ -250,6 +314,10 @@ async function withWorldSession<T>(
|
|
|
250
314
|
})
|
|
251
315
|
const nextLine = (timeoutMs: number): Promise<string> =>
|
|
252
316
|
new Promise((resolve, reject) => {
|
|
317
|
+
if (signal.aborted) {
|
|
318
|
+
reject(signal.reason)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
253
321
|
const fromBacklog = backlog.shift()
|
|
254
322
|
if (fromBacklog !== undefined) return resolve(fromBacklog)
|
|
255
323
|
const t = setTimeout(
|
|
@@ -260,14 +328,29 @@ async function withWorldSession<T>(
|
|
|
260
328
|
// cap) — remove it on the resolve path.
|
|
261
329
|
const onExit = (code: number | null): void => {
|
|
262
330
|
clearTimeout(t)
|
|
263
|
-
|
|
331
|
+
signal.removeEventListener('abort', onAbort)
|
|
332
|
+
reject(
|
|
333
|
+
signal.aborted
|
|
334
|
+
? signal.reason
|
|
335
|
+
: new Error(`appworld session exited (${code}); stderr: ${stderr.slice(-400)}`),
|
|
336
|
+
)
|
|
264
337
|
}
|
|
265
|
-
|
|
338
|
+
const onAbort = (): void => {
|
|
266
339
|
clearTimeout(t)
|
|
267
340
|
child.removeListener('exit', onExit)
|
|
268
|
-
|
|
269
|
-
|
|
341
|
+
const index = pending.indexOf(onLine)
|
|
342
|
+
if (index >= 0) pending.splice(index, 1)
|
|
343
|
+
reject(signal.reason)
|
|
344
|
+
}
|
|
345
|
+
const onLine = (line: string): void => {
|
|
346
|
+
clearTimeout(t)
|
|
347
|
+
child.removeListener('exit', onExit)
|
|
348
|
+
signal.removeEventListener('abort', onAbort)
|
|
349
|
+
resolve(line)
|
|
350
|
+
}
|
|
351
|
+
pending.push(onLine)
|
|
270
352
|
child.once('exit', onExit)
|
|
353
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
271
354
|
})
|
|
272
355
|
try {
|
|
273
356
|
const ready = JSON.parse(await nextLine(120_000)) as { ready?: boolean; instruction?: string; error?: string }
|
|
@@ -280,26 +363,43 @@ async function withWorldSession<T>(
|
|
|
280
363
|
}
|
|
281
364
|
return await fn(call, ready.instruction ?? '')
|
|
282
365
|
} finally {
|
|
366
|
+
signal.removeEventListener('abort', stopChild)
|
|
283
367
|
child.stdin.end()
|
|
284
|
-
|
|
368
|
+
stopChild()
|
|
285
369
|
}
|
|
286
370
|
}
|
|
287
371
|
|
|
288
|
-
|
|
372
|
+
type AppWorldWorldSession = typeof withWorldSession
|
|
373
|
+
type AppWorldComplete = (
|
|
374
|
+
body: Record<string, unknown>,
|
|
375
|
+
request?: {
|
|
376
|
+
readonly headers: Readonly<Record<string, string>>
|
|
377
|
+
readonly signal?: AbortSignal
|
|
378
|
+
},
|
|
379
|
+
) => Promise<unknown>
|
|
380
|
+
|
|
381
|
+
/** SandboxClient whose leaf is Runtime's profile-bound Router executor driving a world session. */
|
|
289
382
|
export function appworldToolLoopClient(cfg: {
|
|
290
383
|
model: string
|
|
291
384
|
routerBaseUrl: string
|
|
292
385
|
routerKey: string
|
|
293
386
|
maxTurns?: number
|
|
387
|
+
/** Offline-test seam; production always uses the Python AppWorld session above. */
|
|
388
|
+
runWorldSession?: AppWorldWorldSession
|
|
389
|
+
/** Offline-test seam; production uses Runtime's Router HTTP transport. */
|
|
390
|
+
complete?: AppWorldComplete
|
|
294
391
|
}): unknown {
|
|
295
392
|
const maxTurns = cfg.maxTurns ?? Number(process.env.REACT_MAX_TURNS ?? 40)
|
|
393
|
+
const runWorldSession = cfg.runWorldSession ?? withWorldSession
|
|
296
394
|
let seq = 0
|
|
297
395
|
return {
|
|
298
396
|
async create() {
|
|
299
397
|
const id = `appworld-toolloop-${seq++}`
|
|
300
398
|
return {
|
|
301
399
|
id,
|
|
302
|
-
async *streamPrompt(prompt: string) {
|
|
400
|
+
async *streamPrompt(prompt: string, promptOpts?: { signal?: AbortSignal }) {
|
|
401
|
+
const signal = promptOpts?.signal ?? new AbortController().signal
|
|
402
|
+
signal.throwIfAborted()
|
|
303
403
|
const m = prompt.match(REACT_HEADER)
|
|
304
404
|
if (!m) {
|
|
305
405
|
throw new Error(
|
|
@@ -308,42 +408,65 @@ export function appworldToolLoopClient(cfg: {
|
|
|
308
408
|
}
|
|
309
409
|
const [, taskId, split] = m
|
|
310
410
|
const directive = prompt.replace(REACT_HEADER, '').trim()
|
|
311
|
-
const out = await
|
|
411
|
+
const out = await runWorldSession(taskId as string, split as string, signal, async (call, instruction) => {
|
|
312
412
|
const system = directive ? `${SESSION_SYSTEM}\n\n${directive}` : SESSION_SYSTEM
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
413
|
+
const transcriptSteps: Array<{ args: string; result: string }> = []
|
|
414
|
+
const profile = {
|
|
415
|
+
name: 'appworld-react-worker',
|
|
416
|
+
harness: 'cli-base' as const,
|
|
417
|
+
model: {
|
|
418
|
+
provider: 'tangle-router',
|
|
419
|
+
default: cfg.model,
|
|
420
|
+
metadata: { maxTurns },
|
|
421
|
+
},
|
|
422
|
+
prompt: { systemPrompt: system },
|
|
423
|
+
tools: { execute_python: true },
|
|
424
|
+
}
|
|
425
|
+
const factory = createExecutor({
|
|
426
|
+
backend: 'router-tools',
|
|
427
|
+
routerBaseUrl: cfg.routerBaseUrl,
|
|
428
|
+
routerKey: cfg.routerKey,
|
|
429
|
+
...(cfg.complete ? { complete: cfg.complete } : {}),
|
|
430
|
+
tools: [EXECUTE_TOOL],
|
|
431
|
+
executeToolCall: async (name, args) => {
|
|
319
432
|
if (name !== 'execute_python') return `error: unknown tool ${name}`
|
|
320
433
|
const res = await call({ op: 'execute', code: String(args.code ?? '') })
|
|
321
434
|
const done = res.task_completed === true
|
|
322
|
-
|
|
435
|
+
const result = `${String(res.output ?? '')}${done ? '\n\n[TASK MARKED COMPLETE — reply with a final summary and do not call the tool again]' : ''}`
|
|
436
|
+
transcriptSteps.push({ args: JSON.stringify(args), result })
|
|
437
|
+
return result
|
|
323
438
|
},
|
|
324
|
-
|
|
439
|
+
})
|
|
440
|
+
const loop = await collectAgentTurn(
|
|
441
|
+
streamAgentTurn(
|
|
442
|
+
{ kind: 'executor', factory, profile },
|
|
443
|
+
`Task: ${instruction}`,
|
|
444
|
+
{ signal },
|
|
445
|
+
),
|
|
325
446
|
)
|
|
447
|
+
if (loop.status !== 'completed') {
|
|
448
|
+
throw new Error(loop.error?.message ?? `AppWorld turn ended with ${loop.status}`)
|
|
449
|
+
}
|
|
326
450
|
const verdict = (await call({ op: 'evaluate' })) as unknown as ReactResult
|
|
327
|
-
const transcript =
|
|
451
|
+
const transcript = transcriptSteps
|
|
328
452
|
.slice(-3)
|
|
329
453
|
.map((t) => `CODE:\n${t.args.slice(0, 600)}\nOUTPUT:\n${t.result.slice(0, 600)}`)
|
|
330
454
|
.join('\n---\n')
|
|
331
455
|
.slice(0, 1600)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
456
|
+
const finalEvent = loop.events.at(-1)
|
|
457
|
+
const resultMetadata =
|
|
458
|
+
finalEvent?.type === 'final' && finalEvent.metadata?.result
|
|
459
|
+
? (finalEvent.metadata.result as { spent?: { iterations?: number } })
|
|
460
|
+
: undefined
|
|
461
|
+
return appworldReactResultWithUsage(
|
|
462
|
+
verdict,
|
|
463
|
+
loop.usage,
|
|
464
|
+
resultMetadata?.spent?.iterations,
|
|
337
465
|
transcript,
|
|
338
|
-
|
|
466
|
+
)
|
|
339
467
|
})
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
yield {
|
|
343
|
-
type: 'llm_call',
|
|
344
|
-
data: { tokensIn: out.input_tokens ?? 0, tokensOut: out.output_tokens ?? 0, model: cfg.model },
|
|
345
|
-
}
|
|
346
|
-
}
|
|
468
|
+
const usageEvent = appworldReactUsageEvent(out, cfg.model)
|
|
469
|
+
if (usageEvent) yield usageEvent
|
|
347
470
|
yield { type: 'result', data: { finalText: JSON.stringify(out) } }
|
|
348
471
|
},
|
|
349
472
|
async delete() {},
|