@qvac/harness 1.0.0-rc.2 → 1.0.0-rc.3
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 +12 -12
- package/bench/README.md +94 -0
- package/bench/bfcl/checker.ts +313 -0
- package/bench/bfcl/data.ts +93 -0
- package/bench/bfcl/observe.ts +93 -0
- package/bench/bfcl/run.ts +213 -0
- package/bench/bfcl/tools.ts +72 -0
- package/bench/bridge/bridge.ts +392 -0
- package/bench/bridge/openai.ts +150 -0
- package/bench/bridge/python/qvac_bench/__init__.py +3 -0
- package/bench/bridge/python/qvac_bench/airline.py +42 -0
- package/bench/bridge/python/qvac_bench/approval.py +26 -0
- package/bench/bridge/python/qvac_bench/bfcl_multi_turn.py +152 -0
- package/bench/bridge/python/qvac_bench/client.py +88 -0
- package/bench/bridge/python/qvac_bench/harbor_agent.py +85 -0
- package/bench/bridge/python/qvac_bench/session.py +107 -0
- package/bench/bridge/python/qvac_bench/speed.py +30 -0
- package/bench/bridge/python/qvac_bench/tau2_agent.py +106 -0
- package/bench/bridge/python/qvac_bench/tools.py +11 -0
- package/bench/bridge/python/run_bfcl_multi_turn.py +95 -0
- package/bench/bridge/python/run_tau2.py +150 -0
- package/bench/bridge/python/smoke.py +47 -0
- package/bench/bridge/python/test_airline.py +28 -0
- package/bench/bridge/python/test_approval.py +57 -0
- package/bench/bridge/python/test_bfcl_multi_turn.py +185 -0
- package/bench/bridge/python/test_harbor_agent.py +107 -0
- package/bench/bridge/serve.ts +52 -0
- package/bench/bridge/transcript.ts +52 -0
- package/bench/bun-tsconfig.json +15 -0
- package/bench/cpu-main.mjs +216 -0
- package/bench/cpu.ts +457 -0
- package/bench/evidence/2026-09-28-review15.md +225 -0
- package/bench/evidence/redact-bun1.3.14.json +92 -0
- package/bench/evidence/redact-node24.11.json +92 -0
- package/bench/graph-metrics.ts +43 -0
- package/bench/harness.ts +73 -0
- package/bench/issue129-matrix.mjs +722 -0
- package/bench/redact-adversarial.ts +256 -0
- package/bench/repeat-cpu.mjs +281 -0
- package/bench/report.ts +1075 -0
- package/bench/skill-eval/harness.ts +30 -0
- package/bench/skill-eval/laws.ts +37 -0
- package/bench/skill-eval/mermaid/client.ts +132 -0
- package/bench/skill-eval/mermaid/validate.mjs +115 -0
- package/bench/skill-eval/pack.ts +61 -0
- package/bench/skill-eval/packs/diagrams/cases.ts +281 -0
- package/bench/skill-eval/packs/diagrams/index.ts +50 -0
- package/bench/skill-eval/packs/diagrams/score.ts +74 -0
- package/bench/skill-eval/packs/index.ts +5 -0
- package/bench/skill-eval/packs/weather/cases.ts +279 -0
- package/bench/skill-eval/packs/weather/fixtures/wttr.json +566 -0
- package/bench/skill-eval/packs/weather/fixtures.ts +84 -0
- package/bench/skill-eval/packs/weather/index.ts +55 -0
- package/bench/skill-eval/packs/weather/score.ts +92 -0
- package/bench/skill-eval/report.ts +194 -0
- package/bench/skill-eval/run.ts +316 -0
- package/bench/speed.ts +80 -0
- package/package.json +4 -13
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""output/venvs/bfcl/bin/python -m unittest test_bfcl_multi_turn, from this folder: no model is loaded.
|
|
2
|
+
|
|
3
|
+
A scripted model plays BFCL's own ground truth through the adapter, so BFCL's checker must pass it;
|
|
4
|
+
the same script with a turn dropped or an argument changed must fail as BFCL fails it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import asyncio
|
|
9
|
+
import itertools
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
from bfcl_eval.constants.default_prompts import DEFAULT_USER_PROMPT_FOR_ADDITIONAL_FUNCTION_FC, MAXIMUM_STEP_LIMIT
|
|
15
|
+
from bfcl_eval.eval_checker.multi_turn_eval.multi_turn_utils import execute_multi_turn_func_call
|
|
16
|
+
|
|
17
|
+
from qvac_bench.bfcl_multi_turn import CATEGORIES, Episode, executed, load
|
|
18
|
+
|
|
19
|
+
names = (f"test_{index}" for index in itertools.count())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse(source: str, tools: list[dict]) -> tuple[str, dict]:
|
|
23
|
+
call = ast.parse(source, mode="eval").body
|
|
24
|
+
name = call.func.id
|
|
25
|
+
params = next(tool for tool in tools if tool["name"] == name)["parameters"]["properties"]
|
|
26
|
+
args = dict(zip(params, (ast.literal_eval(arg) for arg in call.args)))
|
|
27
|
+
args.update({keyword.arg: ast.literal_eval(keyword.value) for keyword in call.keywords})
|
|
28
|
+
return name, args
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def scripted(answer: list[list[str]], change=lambda turn, name, args: args, seen=None, refused=None):
|
|
32
|
+
"""A run that makes one turn's ground-truth calls, one round each, and returns the bridge's done frame."""
|
|
33
|
+
turns = iter(answer)
|
|
34
|
+
|
|
35
|
+
async def run(messages, tools, execute):
|
|
36
|
+
if seen is not None:
|
|
37
|
+
seen.append((list(messages), [tool["name"] for tool in tools]))
|
|
38
|
+
turn = next(turns)
|
|
39
|
+
history = []
|
|
40
|
+
if refused:
|
|
41
|
+
history.append({"role": "assistant", "content": "", "toolCalls": [refused]})
|
|
42
|
+
for index, source in enumerate(turn):
|
|
43
|
+
name, args = parse(source, tools)
|
|
44
|
+
args = change(turn, name, args)
|
|
45
|
+
call_id = f"c{index}"
|
|
46
|
+
result = await execute(name, args)
|
|
47
|
+
history.append({"role": "assistant", "content": "", "toolCalls": [{"id": call_id, "name": name, "arguments": args}]})
|
|
48
|
+
history.append({"role": "tool", "toolCallId": call_id, "name": name, "content": result})
|
|
49
|
+
history.append({"role": "assistant", "content": "Done."})
|
|
50
|
+
return {"type": "done", "reply": "Done.", "calls": len(turn), "error": None, "metrics": None, "history": history}
|
|
51
|
+
|
|
52
|
+
return run
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def play(entry, answer, run):
|
|
56
|
+
return asyncio.run(Episode(entry, answer, next(names)).play(run))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def stepping(steps: int):
|
|
60
|
+
"""A run that makes the same call for `steps` rounds every turn."""
|
|
61
|
+
|
|
62
|
+
async def run(messages, tools, execute):
|
|
63
|
+
history = []
|
|
64
|
+
for _ in range(steps):
|
|
65
|
+
await execute("pwd", {})
|
|
66
|
+
history.append({"role": "assistant", "content": "", "toolCalls": [{"id": "c", "name": "pwd", "arguments": {}}]})
|
|
67
|
+
return {"reply": "", "error": None, "metrics": None, "history": history}
|
|
68
|
+
|
|
69
|
+
return run
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class GroundTruth(unittest.TestCase):
|
|
73
|
+
def test_bfcl_passes_its_own_answers_in_every_category(self):
|
|
74
|
+
for category in CATEGORIES:
|
|
75
|
+
for entry, answer in load(category, 3):
|
|
76
|
+
with self.subTest(entry=entry["id"]):
|
|
77
|
+
self.assertEqual(play(entry, answer, scripted(answer))["valid"], True)
|
|
78
|
+
|
|
79
|
+
def test_a_missed_function_is_offered_only_from_its_turn(self):
|
|
80
|
+
entry, answer = load("multi_turn_miss_func", 1)[0]
|
|
81
|
+
[(turn, [missed])] = entry["missed_function"].items()
|
|
82
|
+
seen = []
|
|
83
|
+
play(entry, answer, scripted(answer, seen=seen))
|
|
84
|
+
|
|
85
|
+
self.assertNotIn(missed["name"], seen[int(turn) - 1][1])
|
|
86
|
+
self.assertIn(missed["name"], seen[int(turn)][1])
|
|
87
|
+
self.assertEqual(seen[int(turn)][0][-1], {"role": "user", "content": DEFAULT_USER_PROMPT_FOR_ADDITIONAL_FUNCTION_FC})
|
|
88
|
+
|
|
89
|
+
def test_each_turn_sees_the_conversation_so_far(self):
|
|
90
|
+
entry, answer = load("multi_turn_base", 1)[0]
|
|
91
|
+
seen = []
|
|
92
|
+
play(entry, answer, scripted(answer, seen=seen))
|
|
93
|
+
|
|
94
|
+
second = seen[1][0]
|
|
95
|
+
self.assertEqual(second[0], entry["question"][0][0])
|
|
96
|
+
self.assertEqual(second[-1], entry["question"][1][0])
|
|
97
|
+
self.assertTrue(any(message.get("toolCalls") for message in second))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Failures(unittest.TestCase):
|
|
101
|
+
def setUp(self):
|
|
102
|
+
self.entry, self.answer = load("multi_turn_base", 1)[0]
|
|
103
|
+
|
|
104
|
+
def test_a_turn_without_its_calls_fails_as_an_empty_turn(self):
|
|
105
|
+
dropped = [self.answer[0], []] + self.answer[2:]
|
|
106
|
+
verdict = play(self.entry, self.answer, scripted(dropped))
|
|
107
|
+
|
|
108
|
+
self.assertEqual(verdict["error_type"], "multi_turn:empty_turn_model_response")
|
|
109
|
+
|
|
110
|
+
def test_a_wrong_argument_fails_on_the_backend_state(self):
|
|
111
|
+
def rename(turn, name, args):
|
|
112
|
+
return {**args, "dir_name": "elsewhere"} if name == "mkdir" else args
|
|
113
|
+
|
|
114
|
+
verdict = play(self.entry, self.answer, scripted(self.answer, rename))
|
|
115
|
+
|
|
116
|
+
self.assertEqual(verdict["error_type"], "multi_turn:instance_state_mismatch")
|
|
117
|
+
|
|
118
|
+
def test_a_harness_error_fails_the_entry_as_bfcl_fails_a_raised_inference(self):
|
|
119
|
+
async def broken(messages, tools, execute):
|
|
120
|
+
return {"reply": "", "error": "context overflow", "metrics": None, "history": []}
|
|
121
|
+
|
|
122
|
+
verdict = play(self.entry, self.answer, broken)
|
|
123
|
+
|
|
124
|
+
self.assertEqual((verdict["error_type"], verdict["error"]), ("multi_turn:inference_error", "context overflow"))
|
|
125
|
+
|
|
126
|
+
def test_a_turn_past_the_step_limit_is_force_terminated(self):
|
|
127
|
+
verdict = play(self.entry, self.answer, stepping(MAXIMUM_STEP_LIMIT + 1))
|
|
128
|
+
|
|
129
|
+
self.assertEqual(verdict["error_type"], "multi_turn:force_terminated")
|
|
130
|
+
|
|
131
|
+
def test_a_turn_at_the_step_limit_is_scored(self):
|
|
132
|
+
verdict = play(self.entry, self.answer, stepping(MAXIMUM_STEP_LIMIT))
|
|
133
|
+
|
|
134
|
+
self.assertNotEqual(verdict["error_type"], "multi_turn:force_terminated")
|
|
135
|
+
|
|
136
|
+
def test_a_call_the_harness_refused_is_not_scored(self):
|
|
137
|
+
cut_off = {"id": "x", "name": "mkdir", "arguments": {"dir_name": "te"}}
|
|
138
|
+
|
|
139
|
+
self.assertTrue(play(self.entry, self.answer, scripted(self.answer, refused=cut_off))["valid"])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_refused_calls_leave_the_rounds_that_ran_as_steps(self):
|
|
143
|
+
def turn(*calls):
|
|
144
|
+
return {"role": "assistant", "content": "", "toolCalls": [{"id": "x", "name": name, "arguments": args} for name, args in calls]}
|
|
145
|
+
|
|
146
|
+
history = [
|
|
147
|
+
turn(("cd", {"folder": "do"}), ("ls", {"a": "tr"})),
|
|
148
|
+
turn(("cd", {"folder": "document"})),
|
|
149
|
+
turn(("ls", {"a": True})),
|
|
150
|
+
]
|
|
151
|
+
ran = [{"cd": {"folder": "document"}}, {"ls": {"a": True}}]
|
|
152
|
+
|
|
153
|
+
self.assertEqual(executed(history, ran), [[ran[0]], [ran[1]]])
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Execution(unittest.TestCase):
|
|
157
|
+
def test_a_call_returns_what_bfcl_sends_the_model(self):
|
|
158
|
+
entry, answer = load("multi_turn_base", 1)[0]
|
|
159
|
+
own, _ = execute_multi_turn_func_call(
|
|
160
|
+
["cd(folder='document')"], entry["initial_config"], entry["involved_classes"], next(names), entry["id"]
|
|
161
|
+
)
|
|
162
|
+
result = asyncio.run(Episode(entry, answer, next(names)).execute("cd", {"folder": "document"}))
|
|
163
|
+
|
|
164
|
+
self.assertEqual(result, json.loads(own[0]), "a JSON result goes out as the object, so it is encoded once")
|
|
165
|
+
|
|
166
|
+
def test_a_text_result_stays_text(self):
|
|
167
|
+
entry, answer = load("multi_turn_base", 1)[0]
|
|
168
|
+
result = asyncio.run(Episode(entry, answer, next(names)).execute("cd", {"path": "document"}))
|
|
169
|
+
|
|
170
|
+
self.assertTrue(result.startswith("Error during execution: "), result)
|
|
171
|
+
|
|
172
|
+
def test_a_long_context_entry_runs_the_long_backend(self):
|
|
173
|
+
def listing(category):
|
|
174
|
+
entry, answer = load(category, 1)[0]
|
|
175
|
+
episode = Episode(entry, answer, next(names))
|
|
176
|
+
asyncio.run(episode.execute("cd", {"folder": "document"}))
|
|
177
|
+
return asyncio.run(episode.execute("ls", {}))
|
|
178
|
+
|
|
179
|
+
base, long = listing("multi_turn_base"), listing("multi_turn_long_context")
|
|
180
|
+
|
|
181
|
+
self.assertGreater(len(long["current_directory_content"]), len(base["current_directory_content"]))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
unittest.main()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""python3 -m unittest test_harbor_agent, from this folder: harbor is not needed."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
import types
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseAgent:
|
|
14
|
+
def __init__(self, logs_dir, model_name=None, **kwargs):
|
|
15
|
+
self.logs_dir = logs_dir
|
|
16
|
+
self.model_name = model_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
for name, attrs in {
|
|
20
|
+
"harbor": {},
|
|
21
|
+
"harbor.agents": {},
|
|
22
|
+
"harbor.agents.base": {"BaseAgent": BaseAgent},
|
|
23
|
+
"harbor.environments": {},
|
|
24
|
+
"harbor.environments.base": {"BaseEnvironment": object},
|
|
25
|
+
"harbor.models": {},
|
|
26
|
+
"harbor.models.agent": {},
|
|
27
|
+
"harbor.models.agent.context": {"AgentContext": object},
|
|
28
|
+
}.items():
|
|
29
|
+
sys.modules.setdefault(name, types.ModuleType(name)).__dict__.update(attrs)
|
|
30
|
+
|
|
31
|
+
from qvac_bench import harbor_agent # noqa: E402
|
|
32
|
+
|
|
33
|
+
DONE = {
|
|
34
|
+
"type": "done",
|
|
35
|
+
"id": "run-0",
|
|
36
|
+
"reply": "Done.",
|
|
37
|
+
"calls": 1,
|
|
38
|
+
"error": None,
|
|
39
|
+
"metrics": {"rounds": [{"promptTokens": 300, "generatedTokens": 20}, {"promptTokens": 50, "generatedTokens": 5}]},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FakeBridge:
|
|
44
|
+
runs = []
|
|
45
|
+
results = []
|
|
46
|
+
|
|
47
|
+
def __init__(self, repo, model):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
async def start(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
async def run(self, messages, tools, execute, system=None, max_tool_rounds=None):
|
|
54
|
+
FakeBridge.runs.append(max_tool_rounds)
|
|
55
|
+
FakeBridge.results.append(await execute("bash", {"command": "ls"}))
|
|
56
|
+
return DONE
|
|
57
|
+
|
|
58
|
+
async def close(self):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Environment:
|
|
63
|
+
async def exec(self, command, timeout_sec=None):
|
|
64
|
+
return types.SimpleNamespace(stdout="a b", stderr="", return_code=0)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TimeoutEnvironment:
|
|
68
|
+
async def exec(self, command, timeout_sec=None):
|
|
69
|
+
raise RuntimeError(f"Command timed out after {timeout_sec} seconds")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def run_agent(environment, max_tool_rounds="40"):
|
|
73
|
+
with tempfile.TemporaryDirectory() as logs, mock.patch.object(harbor_agent, "QvacBridge", FakeBridge), \
|
|
74
|
+
mock.patch.dict(os.environ, {"QVAC_HARNESS_REPO": "/repo"}):
|
|
75
|
+
agent = harbor_agent.QvacHarnessAgent(logs_dir=logs, model_name="m", max_tool_rounds=max_tool_rounds)
|
|
76
|
+
context = types.SimpleNamespace()
|
|
77
|
+
asyncio.run(agent.run("list the files", environment, context))
|
|
78
|
+
with open(os.path.join(logs, "qvac.jsonl")) as log:
|
|
79
|
+
return [json.loads(line) for line in log], context
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Agent(unittest.TestCase):
|
|
83
|
+
def setUp(self):
|
|
84
|
+
FakeBridge.runs = []
|
|
85
|
+
FakeBridge.results = []
|
|
86
|
+
|
|
87
|
+
def test_the_round_limit_reaches_the_run_and_the_log_records_it(self):
|
|
88
|
+
rows, context = run_agent(Environment())
|
|
89
|
+
|
|
90
|
+
self.assertEqual(FakeBridge.runs, [40])
|
|
91
|
+
self.assertEqual([row["type"] for row in rows], ["start", "call", "done", "end"])
|
|
92
|
+
self.assertEqual(rows[0]["max_tool_rounds"], 40)
|
|
93
|
+
self.assertEqual(len(rows[0]["load"]), 3)
|
|
94
|
+
self.assertEqual((rows[1]["command"], rows[1]["return_code"]), ("ls", 0))
|
|
95
|
+
self.assertEqual(rows[2], DONE)
|
|
96
|
+
self.assertEqual((context.n_input_tokens, context.n_output_tokens), (350, 25))
|
|
97
|
+
|
|
98
|
+
def test_a_command_timeout_returns_a_failed_result_and_the_trial_goes_on(self):
|
|
99
|
+
rows, _ = run_agent(TimeoutEnvironment())
|
|
100
|
+
|
|
101
|
+
self.assertEqual(FakeBridge.results, [{"stdout": "", "stderr": "Command timed out after 180 seconds", "return_code": 124}])
|
|
102
|
+
self.assertEqual([row["type"] for row in rows], ["start", "call", "done", "end"])
|
|
103
|
+
self.assertEqual((rows[1]["command"], rows[1]["return_code"]), ("ls", 124))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
unittest.main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// a benchmark in another process drives the harness over stdio: bare bench/bridge/serve.ts
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import process from 'process'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
import { ensureBundledSkillsDir } from '../../lib/tools/skills/bundled-dir.ts'
|
|
7
|
+
import { openBenchHarness } from '../harness.ts'
|
|
8
|
+
import { Bridge, FRAME, type Inbound, type Outbound } from './bridge.ts'
|
|
9
|
+
|
|
10
|
+
const REPO_ROOT = fileURLToPath(new URL('../..', import.meta.url))
|
|
11
|
+
const logFile = process.env.QVAC_BRIDGE_LOG ?? path.join(REPO_ROOT, 'output', 'bridge.log')
|
|
12
|
+
|
|
13
|
+
fs.mkdirSync(path.dirname(logFile), { recursive: true })
|
|
14
|
+
const log = fs.openSync(logFile, 'a')
|
|
15
|
+
// stdout carries the frames; everything the SDK says goes to the log
|
|
16
|
+
console.log =
|
|
17
|
+
console.info =
|
|
18
|
+
console.debug =
|
|
19
|
+
console.warn =
|
|
20
|
+
console.error =
|
|
21
|
+
(...args: unknown[]) => fs.writeSync(log, `${args.map(String).join(' ')}\n`)
|
|
22
|
+
|
|
23
|
+
process.env.QVAC_NO_SKILL_BIN_PROBE = '1'
|
|
24
|
+
const harness = await openBenchHarness({
|
|
25
|
+
root: path.join(REPO_ROOT, 'output', 'bench', 'sdk-root'),
|
|
26
|
+
skillsDir: await ensureBundledSkillsDir()
|
|
27
|
+
})
|
|
28
|
+
const send = (frame: Outbound) => process.stdout.write(`${FRAME}${JSON.stringify(frame)}\n`)
|
|
29
|
+
const bridge = new Bridge((context) => harness.completion(context), send, {
|
|
30
|
+
plan: process.env.QVAC_BRIDGE_PLAN === '1',
|
|
31
|
+
forget: (cacheKey) => harness.deleteKvCache(cacheKey)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
let buffered = ''
|
|
35
|
+
process.stdin.on('data', (chunk: Uint8Array) => {
|
|
36
|
+
buffered += chunk.toString()
|
|
37
|
+
for (let newline = buffered.indexOf('\n'); newline >= 0; newline = buffered.indexOf('\n')) {
|
|
38
|
+
const line = buffered.slice(0, newline).trim()
|
|
39
|
+
buffered = buffered.slice(newline + 1)
|
|
40
|
+
if (line) {
|
|
41
|
+
receive(JSON.parse(line))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
process.stdin.on('end', () => {
|
|
46
|
+
harness.close().then(() => process.exit(0))
|
|
47
|
+
})
|
|
48
|
+
send({ type: 'ready' })
|
|
49
|
+
|
|
50
|
+
function receive(frame: Inbound): void {
|
|
51
|
+
bridge.receive(frame).catch((err: unknown) => console.error('[bridge]', err))
|
|
52
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { CompletionEvent, Message } from '../../lib/contract/index.ts'
|
|
2
|
+
|
|
3
|
+
// A run's turns as a host replays them: one assistant turn per round, then a tool turn per call.
|
|
4
|
+
export class Transcript {
|
|
5
|
+
private readonly _turns: Message[] = []
|
|
6
|
+
private readonly _rounds = new Map<number, Message>()
|
|
7
|
+
|
|
8
|
+
add(event: CompletionEvent): void {
|
|
9
|
+
const round = event.round ?? 0
|
|
10
|
+
if (event.type === 'content') {
|
|
11
|
+
this._answer(round).content += event.text
|
|
12
|
+
} else if (event.type === 'tool-call') {
|
|
13
|
+
const call = { id: event.callId ?? '', name: event.name, arguments: event.args }
|
|
14
|
+
const turn = this._round(round)
|
|
15
|
+
turn.toolCalls = [...(turn.toolCalls ?? []), call]
|
|
16
|
+
} else if (event.type === 'tool-result') {
|
|
17
|
+
const content = JSON.stringify(event.result) ?? ''
|
|
18
|
+
this._turns.push({ role: 'tool', toolCallId: event.callId ?? '', name: event.name, content })
|
|
19
|
+
} else if (event.type === 'round-reset') {
|
|
20
|
+
this._round(round).content = ''
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
turns(): Message[] {
|
|
25
|
+
return this._turns.filter(
|
|
26
|
+
(turn) => turn.role !== 'assistant' || turn.content || turn.toolCalls?.length
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// a wrap-up streams under its last tool round, but it answers that round's results
|
|
31
|
+
private _answer(round: number): Message {
|
|
32
|
+
const turn = this._round(round)
|
|
33
|
+
if (turn === this._turns.at(-1)) {
|
|
34
|
+
return turn
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._rounds.delete(round)
|
|
38
|
+
|
|
39
|
+
return this._round(round)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private _round(round: number): Message {
|
|
43
|
+
let turn = this._rounds.get(round)
|
|
44
|
+
if (!turn) {
|
|
45
|
+
turn = { role: 'assistant', content: '' }
|
|
46
|
+
this._rounds.set(round, turn)
|
|
47
|
+
this._turns.push(turn)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return turn
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"erasableSyntaxOnly": true,
|
|
7
|
+
"verbatimModuleSyntax": true,
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"allowJs": true,
|
|
12
|
+
"checkJs": false,
|
|
13
|
+
"noEmit": true
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Pre-state-machine CPU baseline runner.
|
|
2
|
+
import process from 'process'
|
|
3
|
+
import { CompletionEngine } from '../lib/capabilities/completion-engine/index.ts'
|
|
4
|
+
|
|
5
|
+
const TOKENS = 2000
|
|
6
|
+
const FIRST_TOKEN_MS = 110
|
|
7
|
+
|
|
8
|
+
function options() {
|
|
9
|
+
const args = process.argv.slice(2)
|
|
10
|
+
let runs = 3
|
|
11
|
+
let spacing = 'all'
|
|
12
|
+
let workload = 'all'
|
|
13
|
+
let json = false
|
|
14
|
+
let positionalRuns = false
|
|
15
|
+
for (const arg of args) {
|
|
16
|
+
if (arg === '--json') {
|
|
17
|
+
json = true
|
|
18
|
+
} else if (arg.startsWith('--spacing=')) {
|
|
19
|
+
spacing = arg.slice('--spacing='.length)
|
|
20
|
+
} else if (arg.startsWith('--workload=')) {
|
|
21
|
+
workload = arg.slice('--workload='.length)
|
|
22
|
+
} else if (!arg.startsWith('-') && !positionalRuns) {
|
|
23
|
+
runs = Number(arg)
|
|
24
|
+
positionalRuns = true
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(`Unknown option: ${arg}`)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!Number.isInteger(runs) || runs < 1) {
|
|
31
|
+
throw new Error('runs must be a positive integer')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const spacings = spacing === 'all' ? ['immediate', 'timeout'] : [spacing]
|
|
35
|
+
const workloads = workload === 'all' ? ['tools', 'stream'] : [workload]
|
|
36
|
+
if (spacings.some((value) => !['immediate', 'timeout'].includes(value))) {
|
|
37
|
+
throw new Error(`Unknown spacing: ${spacing}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (workloads.some((value) => !['tools', 'stream'].includes(value))) {
|
|
41
|
+
throw new Error(`Unknown workload: ${workload}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { runs, spacings, workloads, json }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function collect(run) {
|
|
48
|
+
const events = []
|
|
49
|
+
for await (const event of run) {
|
|
50
|
+
events.push(event)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return events
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cpuMs(start) {
|
|
57
|
+
const usage = process.cpuUsage(start)
|
|
58
|
+
return (usage.user + usage.system) / 1000
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function median(values) {
|
|
62
|
+
const sorted = [...values].sort((a, b) => a - b)
|
|
63
|
+
return sorted[Math.floor(sorted.length / 2)]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function history() {
|
|
67
|
+
return Array.from({ length: 101 }, (_, i) => ({
|
|
68
|
+
role: i % 2 ? 'assistant' : 'user',
|
|
69
|
+
content: `${i} ${'x'.repeat(2000)}`
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readTool(name) {
|
|
74
|
+
return {
|
|
75
|
+
name,
|
|
76
|
+
description: 'read something',
|
|
77
|
+
toolset: null,
|
|
78
|
+
parameters: { type: 'object', properties: { path: { type: 'string' } } },
|
|
79
|
+
effect: () => 'read',
|
|
80
|
+
autoApprove: true,
|
|
81
|
+
execute: () => Promise.resolve({ content: `${name} ${'r'.repeat(2048)}` })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toolWorkloadSdk(tools) {
|
|
86
|
+
let round = 0
|
|
87
|
+
return {
|
|
88
|
+
completion: () => {
|
|
89
|
+
const at = round++
|
|
90
|
+
if (at < 3) {
|
|
91
|
+
return {
|
|
92
|
+
tokenStream: (async function* () {})(),
|
|
93
|
+
final: Promise.resolve({
|
|
94
|
+
toolCalls: tools.map((tool, i) => ({
|
|
95
|
+
id: `call-${at}-${i}`,
|
|
96
|
+
name: tool.name,
|
|
97
|
+
arguments: { path: `f${at}${i}` }
|
|
98
|
+
})),
|
|
99
|
+
raw: { fullText: '' }
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
tokenStream: (async function* () {
|
|
106
|
+
yield 'done'
|
|
107
|
+
})(),
|
|
108
|
+
final: Promise.resolve({ toolCalls: [], raw: { fullText: 'done' } })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function streamingSdk(spacing) {
|
|
115
|
+
return {
|
|
116
|
+
completion: () => ({
|
|
117
|
+
tokenStream: (async function* () {
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, FIRST_TOKEN_MS))
|
|
119
|
+
for (let i = 0; i < TOKENS; i++) {
|
|
120
|
+
await new Promise((resolve) =>
|
|
121
|
+
spacing === 'immediate' ? setImmediate(resolve) : setTimeout(resolve, 0)
|
|
122
|
+
)
|
|
123
|
+
yield 't'
|
|
124
|
+
}
|
|
125
|
+
})(),
|
|
126
|
+
final: Promise.resolve({ toolCalls: [], raw: { fullText: 't'.repeat(TOKENS) } })
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function measureTools(runs, tools) {
|
|
132
|
+
// Match the engine benchmark's untimed pre-sample pass for each selected cell.
|
|
133
|
+
await collect(
|
|
134
|
+
CompletionEngine.run(toolWorkloadSdk(tools), {
|
|
135
|
+
modelId: 'm',
|
|
136
|
+
messages: history(),
|
|
137
|
+
tools
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const samples = []
|
|
142
|
+
for (let i = 0; i < runs; i++) {
|
|
143
|
+
const args = { modelId: 'm', messages: history(), tools }
|
|
144
|
+
const start = process.cpuUsage()
|
|
145
|
+
await collect(CompletionEngine.run(toolWorkloadSdk(tools), args))
|
|
146
|
+
samples.push(cpuMs(start))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
workload: 'tools',
|
|
151
|
+
mode: 'main',
|
|
152
|
+
runs,
|
|
153
|
+
cpuMs: median(samples),
|
|
154
|
+
cpuSamplesMs: samples
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function measureStream(runs, spacing) {
|
|
159
|
+
// Run one untimed stream before collecting the timed samples.
|
|
160
|
+
await collect(CompletionEngine.run(streamingSdk(spacing), { modelId: 'm', messages: history() }))
|
|
161
|
+
|
|
162
|
+
const samples = []
|
|
163
|
+
let contentEvents = 0
|
|
164
|
+
for (let i = 0; i < runs; i++) {
|
|
165
|
+
const args = { modelId: 'm', messages: history() }
|
|
166
|
+
const start = process.cpuUsage()
|
|
167
|
+
const events = await collect(CompletionEngine.run(streamingSdk(spacing), args))
|
|
168
|
+
samples.push(cpuMs(start))
|
|
169
|
+
contentEvents = events.filter((event) => event.type === 'content').length
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const cpu = median(samples)
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
workload: 'stream',
|
|
176
|
+
mode: 'main',
|
|
177
|
+
spacing,
|
|
178
|
+
runs,
|
|
179
|
+
cpuMs: cpu,
|
|
180
|
+
cpuSamplesMs: samples,
|
|
181
|
+
usPerToken: (cpu * 1000) / TOKENS,
|
|
182
|
+
contentEvents
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function main() {
|
|
187
|
+
const { runs, spacings, workloads, json } = options()
|
|
188
|
+
const results = []
|
|
189
|
+
const tools = [readTool('read_a'), readTool('read_b')]
|
|
190
|
+
if (workloads.includes('tools')) {
|
|
191
|
+
results.push(await measureTools(runs, tools))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (workloads.includes('stream')) {
|
|
195
|
+
for (const spacing of spacings) {
|
|
196
|
+
results.push(await measureStream(runs, spacing))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const report = {
|
|
201
|
+
runtime: globalThis.Bun ? 'bun' : (process.release?.name ?? 'unknown'),
|
|
202
|
+
version: globalThis.Bun?.version ?? process.version,
|
|
203
|
+
runs,
|
|
204
|
+
results
|
|
205
|
+
}
|
|
206
|
+
if (json) {
|
|
207
|
+
console.log(JSON.stringify(report))
|
|
208
|
+
} else {
|
|
209
|
+
console.log(JSON.stringify(report, null, 2))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch((error) => {
|
|
214
|
+
console.error(error)
|
|
215
|
+
process.exitCode = 1
|
|
216
|
+
})
|