@otto-code/brain 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/bin/otto-brain +2 -0
  2. package/dist/bench/context-corpus.d.ts +58 -0
  3. package/dist/bench/context-corpus.js +210 -0
  4. package/dist/bench/corpus.d.ts +31 -0
  5. package/dist/bench/corpus.js +379 -0
  6. package/dist/bench/curated-repos.d.ts +35 -0
  7. package/dist/bench/curated-repos.js +41 -0
  8. package/dist/bench/health.d.ts +50 -0
  9. package/dist/bench/health.js +112 -0
  10. package/dist/bench/index.d.ts +103 -0
  11. package/dist/bench/index.js +209 -0
  12. package/dist/bench/mine.d.ts +32 -0
  13. package/dist/bench/mine.js +101 -0
  14. package/dist/bench/repo-task.d.ts +93 -0
  15. package/dist/bench/repo-task.js +380 -0
  16. package/dist/bench/repo.d.ts +125 -0
  17. package/dist/bench/repo.js +195 -0
  18. package/dist/bench/rescore.d.ts +41 -0
  19. package/dist/bench/rescore.js +127 -0
  20. package/dist/bench/tasks.d.ts +124 -0
  21. package/dist/bench/tasks.js +1249 -0
  22. package/dist/bench/verify.d.ts +142 -0
  23. package/dist/bench/verify.js +350 -0
  24. package/dist/cli.d.ts +13 -0
  25. package/dist/cli.js +72 -0
  26. package/dist/commands/bench.d.ts +20 -0
  27. package/dist/commands/bench.js +222 -0
  28. package/dist/commands/calibrate.d.ts +19 -0
  29. package/dist/commands/calibrate.js +75 -0
  30. package/dist/commands/catalog.d.ts +32 -0
  31. package/dist/commands/catalog.js +48 -0
  32. package/dist/commands/config.d.ts +13 -0
  33. package/dist/commands/config.js +90 -0
  34. package/dist/commands/lifecycle.d.ts +29 -0
  35. package/dist/commands/lifecycle.js +194 -0
  36. package/dist/commands/pull.d.ts +22 -0
  37. package/dist/commands/pull.js +135 -0
  38. package/dist/commands/report.d.ts +10 -0
  39. package/dist/commands/report.js +34 -0
  40. package/dist/commands/rescore.d.ts +11 -0
  41. package/dist/commands/rescore.js +48 -0
  42. package/dist/commands/runtime.d.ts +21 -0
  43. package/dist/commands/runtime.js +62 -0
  44. package/dist/commands/scan.d.ts +24 -0
  45. package/dist/commands/scan.js +55 -0
  46. package/dist/commands/search.d.ts +31 -0
  47. package/dist/commands/search.js +135 -0
  48. package/dist/commands/share.d.ts +26 -0
  49. package/dist/commands/share.js +148 -0
  50. package/dist/commands/sweep.d.ts +21 -0
  51. package/dist/commands/sweep.js +69 -0
  52. package/dist/commands/ui.d.ts +9 -0
  53. package/dist/commands/ui.js +28 -0
  54. package/dist/config/env.d.ts +10 -0
  55. package/dist/config/env.js +48 -0
  56. package/dist/config/index.d.ts +9 -0
  57. package/dist/config/index.js +9 -0
  58. package/dist/config/otto-home.d.ts +2 -0
  59. package/dist/config/otto-home.js +24 -0
  60. package/dist/config/paths.d.ts +20 -0
  61. package/dist/config/paths.js +33 -0
  62. package/dist/config/private-files.d.ts +3 -0
  63. package/dist/config/private-files.js +32 -0
  64. package/dist/config/profiles.d.ts +34 -0
  65. package/dist/config/profiles.js +116 -0
  66. package/dist/config/schema.d.ts +1035 -0
  67. package/dist/config/schema.js +191 -0
  68. package/dist/config/store.d.ts +11 -0
  69. package/dist/config/store.js +73 -0
  70. package/dist/gguf.d.ts +61 -0
  71. package/dist/gguf.js +231 -0
  72. package/dist/gpu.d.ts +12 -0
  73. package/dist/gpu.js +49 -0
  74. package/dist/index.d.ts +14 -0
  75. package/dist/index.js +13 -0
  76. package/dist/main.d.ts +2 -0
  77. package/dist/main.js +5 -0
  78. package/dist/models/dirs.d.ts +9 -0
  79. package/dist/models/dirs.js +21 -0
  80. package/dist/models/download.d.ts +31 -0
  81. package/dist/models/download.js +105 -0
  82. package/dist/models/enrich.d.ts +36 -0
  83. package/dist/models/enrich.js +68 -0
  84. package/dist/models/hf.d.ts +62 -0
  85. package/dist/models/hf.js +114 -0
  86. package/dist/models/index.d.ts +15 -0
  87. package/dist/models/index.js +49 -0
  88. package/dist/models/manage.d.ts +27 -0
  89. package/dist/models/manage.js +112 -0
  90. package/dist/models/pick.d.ts +3 -0
  91. package/dist/models/pick.js +36 -0
  92. package/dist/models/scan.d.ts +13 -0
  93. package/dist/models/scan.js +185 -0
  94. package/dist/ops/archive.d.ts +34 -0
  95. package/dist/ops/archive.js +105 -0
  96. package/dist/ops/calibrate.d.ts +49 -0
  97. package/dist/ops/calibrate.js +74 -0
  98. package/dist/ops/report.d.ts +26 -0
  99. package/dist/ops/report.js +532 -0
  100. package/dist/ops/results.d.ts +207 -0
  101. package/dist/ops/results.js +250 -0
  102. package/dist/ops/sweep.d.ts +77 -0
  103. package/dist/ops/sweep.js +126 -0
  104. package/dist/output/index.d.ts +5 -0
  105. package/dist/output/index.js +5 -0
  106. package/dist/output/render.d.ts +4 -0
  107. package/dist/output/render.js +96 -0
  108. package/dist/output/types.d.ts +52 -0
  109. package/dist/output/types.js +9 -0
  110. package/dist/output/with-output.d.ts +3 -0
  111. package/dist/output/with-output.js +42 -0
  112. package/dist/run.d.ts +5 -0
  113. package/dist/run.js +19 -0
  114. package/dist/runtime/args.d.ts +21 -0
  115. package/dist/runtime/args.js +71 -0
  116. package/dist/runtime/index.d.ts +13 -0
  117. package/dist/runtime/index.js +41 -0
  118. package/dist/runtime/lmstudio.d.ts +10 -0
  119. package/dist/runtime/lmstudio.js +88 -0
  120. package/dist/runtime/managed.d.ts +26 -0
  121. package/dist/runtime/managed.js +148 -0
  122. package/dist/service/model-selector.d.ts +36 -0
  123. package/dist/service/model-selector.js +123 -0
  124. package/dist/service/pid-lock.d.ts +18 -0
  125. package/dist/service/pid-lock.js +50 -0
  126. package/dist/service/router.d.ts +144 -0
  127. package/dist/service/router.js +746 -0
  128. package/dist/service/scheduler.d.ts +74 -0
  129. package/dist/service/scheduler.js +110 -0
  130. package/dist/service/serve.d.ts +41 -0
  131. package/dist/service/serve.js +278 -0
  132. package/dist/service/supervisor.d.ts +67 -0
  133. package/dist/service/supervisor.js +222 -0
  134. package/dist/service/tailscale.d.ts +19 -0
  135. package/dist/service/tailscale.js +62 -0
  136. package/dist/service/tls.d.ts +77 -0
  137. package/dist/service/tls.js +192 -0
  138. package/dist/sysmon.d.ts +50 -0
  139. package/dist/sysmon.js +103 -0
  140. package/dist/tui/app.d.ts +208 -0
  141. package/dist/tui/app.js +1801 -0
  142. package/dist/tui/screen.d.ts +72 -0
  143. package/dist/tui/screen.js +176 -0
  144. package/dist/types.d.ts +76 -0
  145. package/dist/types.js +7 -0
  146. package/dist/version.d.ts +2 -0
  147. package/dist/version.js +8 -0
  148. package/dist/vram.d.ts +67 -0
  149. package/dist/vram.js +132 -0
  150. package/package.json +58 -0
@@ -0,0 +1,1249 @@
1
+ import { runUnittestFiles, verifyPython, verifyToolCall } from "./verify.js";
2
+ import { EXTRA_CORPUS, EXTRA_HIDDEN_TEST, EXTRA_PY_FILES, EXTRA_TARGET_FILE } from "./corpus.js";
3
+ import { generateContextCorpus } from "./context-corpus.js";
4
+ export const TOOLS = [
5
+ {
6
+ type: "function",
7
+ function: {
8
+ name: "read_file",
9
+ description: "Read the full contents of a file from disk.",
10
+ parameters: {
11
+ type: "object",
12
+ properties: { path: { type: "string", description: "Path to the file" } },
13
+ required: ["path"],
14
+ },
15
+ },
16
+ },
17
+ {
18
+ type: "function",
19
+ function: {
20
+ name: "write_file",
21
+ description: "Write contents to a file, replacing it entirely.",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {
25
+ path: { type: "string" },
26
+ content: { type: "string", description: "Complete new file contents" },
27
+ },
28
+ required: ["path", "content"],
29
+ },
30
+ },
31
+ },
32
+ {
33
+ type: "function",
34
+ function: {
35
+ name: "run_command",
36
+ description: "Run a shell command in the project directory and return its output.",
37
+ parameters: {
38
+ type: "object",
39
+ properties: { command: { type: "string" } },
40
+ required: ["command"],
41
+ },
42
+ },
43
+ },
44
+ {
45
+ type: "function",
46
+ function: {
47
+ name: "list_directory",
48
+ description: "List the files in a directory.",
49
+ parameters: {
50
+ type: "object",
51
+ properties: { path: { type: "string" } },
52
+ required: ["path"],
53
+ },
54
+ },
55
+ },
56
+ {
57
+ type: "function",
58
+ function: {
59
+ name: "edit_file",
60
+ description: "Apply a targeted edit to a file by replacing an exact string with a new one. " +
61
+ "Use this instead of write_file when changing part of an existing file.",
62
+ parameters: {
63
+ type: "object",
64
+ properties: {
65
+ path: { type: "string" },
66
+ old_text: { type: "string", description: "Exact text to find and replace" },
67
+ new_text: { type: "string", description: "Replacement text" },
68
+ },
69
+ required: ["path", "old_text", "new_text"],
70
+ },
71
+ },
72
+ },
73
+ {
74
+ type: "function",
75
+ function: {
76
+ name: "search_files",
77
+ description: "Search the contents of files across the project for a string or pattern (like grep). " +
78
+ "Returns the matching lines and their file paths.",
79
+ parameters: {
80
+ type: "object",
81
+ properties: {
82
+ query: { type: "string", description: "The text or pattern to search for" },
83
+ },
84
+ required: ["query"],
85
+ },
86
+ },
87
+ },
88
+ {
89
+ type: "function",
90
+ function: {
91
+ name: "find_files",
92
+ description: "Find files by name or glob pattern across the project (like a filename search). " +
93
+ "Use this to locate files by their name, not their contents.",
94
+ parameters: {
95
+ type: "object",
96
+ properties: {
97
+ pattern: { type: "string", description: "A filename or glob, e.g. **/*_test.py" },
98
+ },
99
+ required: ["pattern"],
100
+ },
101
+ },
102
+ },
103
+ {
104
+ type: "function",
105
+ function: {
106
+ name: "delete_file",
107
+ description: "Delete a file from disk.",
108
+ parameters: {
109
+ type: "object",
110
+ properties: { path: { type: "string" } },
111
+ required: ["path"],
112
+ },
113
+ },
114
+ },
115
+ {
116
+ type: "function",
117
+ function: {
118
+ name: "make_directory",
119
+ description: "Create a new directory.",
120
+ parameters: {
121
+ type: "object",
122
+ properties: { path: { type: "string" } },
123
+ required: ["path"],
124
+ },
125
+ },
126
+ },
127
+ ];
128
+ // --------------------------------------------------------------- tool calling
129
+ export const TOOL_CASES = [
130
+ {
131
+ id: "tools/read",
132
+ prompt: "I need to see what is inside src/config.py. Use your tools.",
133
+ expect: {
134
+ name: "read_file",
135
+ requiredArgs: ["path"],
136
+ check: (args) => String(args.path).includes("config.py") ? true : `path was "${String(args.path)}"`,
137
+ },
138
+ },
139
+ {
140
+ id: "tools/command",
141
+ prompt: "Run the project test suite with pytest and tell me what happens. Use your tools.",
142
+ expect: {
143
+ name: "run_command",
144
+ requiredArgs: ["command"],
145
+ check: (args) => /pytest|python -m pytest/i.test(String(args.command))
146
+ ? true
147
+ : `command was "${String(args.command)}"`,
148
+ },
149
+ },
150
+ {
151
+ id: "tools/write-content",
152
+ prompt: "Create a file called greet.py containing a single function greet(name) that " +
153
+ 'returns the string "Hello, " followed by the name. Use your tools to write it.',
154
+ expect: {
155
+ name: "write_file",
156
+ requiredArgs: ["path", "content"],
157
+ check: (args) => {
158
+ if (!String(args.path).includes("greet.py"))
159
+ return `path was "${String(args.path)}"`;
160
+ const content = String(args.content);
161
+ if (!/def\s+greet\s*\(/.test(content))
162
+ return "content has no greet() definition";
163
+ if (!/return/.test(content))
164
+ return "content has no return statement";
165
+ return true;
166
+ },
167
+ },
168
+ },
169
+ {
170
+ id: "tools/list",
171
+ prompt: "What files are in the tests directory? Use your tools.",
172
+ expect: {
173
+ name: "list_directory",
174
+ requiredArgs: ["path"],
175
+ check: (args) => /tests?/.test(String(args.path)) ? true : `path was "${String(args.path)}"`,
176
+ },
177
+ },
178
+ {
179
+ id: "tools/edit",
180
+ prompt: "In src/config.py there is a line `TIMEOUT = 30`. Change it to `TIMEOUT = 60`. " +
181
+ "Edit the file in place, do not rewrite the whole thing. Use your tools.",
182
+ expect: {
183
+ name: "edit_file",
184
+ requiredArgs: ["path", "old_text", "new_text"],
185
+ check: (args) => {
186
+ if (!String(args.path).includes("config.py"))
187
+ return `path was "${String(args.path)}"`;
188
+ if (!/\b30\b/.test(String(args.old_text)))
189
+ return "old_text did not target the 30 value";
190
+ if (!/\b60\b/.test(String(args.new_text)))
191
+ return "new_text did not contain 60";
192
+ return true;
193
+ },
194
+ },
195
+ },
196
+ {
197
+ id: "tools/search",
198
+ prompt: "Where in the codebase is the string DATABASE_URL used? Search the file contents. Use your tools.",
199
+ expect: {
200
+ name: "search_files",
201
+ requiredArgs: ["query"],
202
+ check: (args) => /DATABASE_URL/.test(String(args.query)) ? true : `query was "${String(args.query)}"`,
203
+ },
204
+ },
205
+ {
206
+ id: "tools/find",
207
+ prompt: "Locate every file in the project whose name ends in _test.py. Match by filename, not contents. Use your tools.",
208
+ expect: {
209
+ name: "find_files",
210
+ requiredArgs: ["pattern"],
211
+ check: (args) => /_test\.py|test/i.test(String(args.pattern))
212
+ ? true
213
+ : `pattern was "${String(args.pattern)}"`,
214
+ },
215
+ },
216
+ {
217
+ id: "tools/delete",
218
+ prompt: "Delete the stale file build/output.log from the project. Use your tools.",
219
+ expect: {
220
+ name: "delete_file",
221
+ requiredArgs: ["path"],
222
+ check: (args) => /output\.log/.test(String(args.path)) ? true : `path was "${String(args.path)}"`,
223
+ },
224
+ },
225
+ {
226
+ id: "tools/mkdir",
227
+ prompt: "Create a new directory named migrations at the project root. Use your tools.",
228
+ expect: {
229
+ name: "make_directory",
230
+ requiredArgs: ["path"],
231
+ check: (args) => /migrations/.test(String(args.path)) ? true : `path was "${String(args.path)}"`,
232
+ },
233
+ },
234
+ ];
235
+ /** Single-shot tool selection and argument correctness. */
236
+ const toolCallingTask = {
237
+ id: "tool-calling",
238
+ category: "Tool calling",
239
+ weight: 2,
240
+ description: "Picks the right tool and fills its arguments correctly",
241
+ async run({ chat }) {
242
+ const outcomes = [];
243
+ for (const testCase of TOOL_CASES) {
244
+ const response = await chat({
245
+ messages: [{ role: "user", content: testCase.prompt }],
246
+ tools: TOOLS,
247
+ max_tokens: 2048,
248
+ temperature: 0.3,
249
+ });
250
+ const message = response.choices?.[0]?.message || {};
251
+ const verdict = verifyToolCall(message.tool_calls, testCase.expect);
252
+ outcomes.push({
253
+ id: testCase.id,
254
+ ok: verdict.ok,
255
+ reason: verdict.reason || null,
256
+ tokens: response.usage?.completion_tokens ?? null,
257
+ });
258
+ }
259
+ const passed = outcomes.filter((o) => o.ok).length;
260
+ // Name the cases that failed (and why) so the scorecard says which tool
261
+ // call the model missed, not just how many. Case ids drop the "tools/"
262
+ // prefix to stay legible in the fixed-width summary column.
263
+ const failed = outcomes
264
+ .filter((o) => !o.ok)
265
+ .map((o) => `${o.id.replace(/^tools\//, "")} (${o.reason ?? "failed"})`);
266
+ return {
267
+ score: passed / outcomes.length,
268
+ summary: `${passed}/${outcomes.length} tool calls correct` +
269
+ (failed.length ? ` - missed: ${failed.join("; ")}` : ""),
270
+ detail: outcomes,
271
+ };
272
+ },
273
+ };
274
+ // ------------------------------------------------------------ agentic loop
275
+ /**
276
+ * A read-fix-verify loop graded by real execution. The model reads a buggy
277
+ * file, writes (or edits) a fix, then runs the tests - and the harness actually
278
+ * runs its fix against a HIDDEN unittest suite, feeding the real pass/fail back
279
+ * so the model can iterate. Scoring is the fraction of hidden tests that pass,
280
+ * which discriminates: the empty-list crash is obvious, but the equal-values
281
+ * (zero-span) crash is easy to miss, so weaker models land partway.
282
+ *
283
+ * The `read_file` result is simulated (deterministic input), but the verdict is
284
+ * the interpreter's - never a model grading a model.
285
+ */
286
+ const BUGGY_SCALE = `def normalize(values, low=0.0, high=1.0):
287
+ lo = min(values)
288
+ hi = max(values)
289
+ span = hi - lo
290
+ return [low + (high - low) * (v - lo) / span for v in values]
291
+ `;
292
+ const SCALE_TESTS = `import unittest
293
+ from scale import normalize
294
+
295
+
296
+ class TestNormalize(unittest.TestCase):
297
+ def test_basic(self):
298
+ self.assertEqual(normalize([0, 5, 10]), [0.0, 0.5, 1.0])
299
+
300
+ def test_custom_range(self):
301
+ self.assertEqual(normalize([0, 10], 1.0, 3.0), [1.0, 3.0])
302
+
303
+ def test_empty(self):
304
+ self.assertEqual(normalize([]), [])
305
+
306
+ def test_all_equal(self):
307
+ self.assertEqual(normalize([7, 7, 7]), [0.0, 0.0, 0.0])
308
+
309
+ def test_single(self):
310
+ self.assertEqual(normalize([4]), [0.0])
311
+
312
+
313
+ if __name__ == "__main__":
314
+ unittest.main()
315
+ `;
316
+ const agenticLoopTask = {
317
+ id: "agentic-loop",
318
+ category: "Agentic loop",
319
+ weight: 3,
320
+ description: "Reads a bug, fixes it, and iterates against a hidden test suite",
321
+ async run({ chat, execute = true, contextWindow }) {
322
+ const messages = [
323
+ {
324
+ role: "user",
325
+ content: "scale.py has a function normalize(values, low=0.0, high=1.0) that linearly rescales " +
326
+ "the numbers in `values` into the range [low, high]. It crashes on some inputs. Make " +
327
+ "it robust: an empty list must return [], and when every value is equal each result " +
328
+ "should be the low bound. Read the file first, then write the corrected scale.py, then " +
329
+ 'run the tests with "python -m unittest". Use your tools for every step.',
330
+ },
331
+ ];
332
+ // The model's current version of scale.py, updated by write_file/edit_file.
333
+ let currentFile = BUGGY_SCALE;
334
+ const steps = { read: false, wrote: false, ranTests: false };
335
+ let bestPassed = 0;
336
+ let total = null;
337
+ let lastOutput = "";
338
+ let testsExecuted = false;
339
+ let turns = 0;
340
+ const maxTurns = 8;
341
+ let stalled = null;
342
+ // Peak prompt size across turns: how much context the model actually held
343
+ // while working. Reported as a fraction of the loaded window.
344
+ let peakPromptTokens = 0;
345
+ while (turns < maxTurns) {
346
+ turns += 1;
347
+ const response = await chat({ messages, tools: TOOLS, max_tokens: 4096, temperature: 0.3 });
348
+ peakPromptTokens = Math.max(peakPromptTokens, response.usage?.prompt_tokens ?? 0);
349
+ const message = response.choices?.[0]?.message || {};
350
+ const calls = message.tool_calls || [];
351
+ if (!calls.length) {
352
+ if (!message.content)
353
+ stalled = "produced neither content nor a tool call";
354
+ break;
355
+ }
356
+ messages.push({ role: "assistant", content: message.content || null, tool_calls: calls });
357
+ for (const call of calls) {
358
+ const name = call.function?.name || call.name;
359
+ let args = call.function?.arguments ?? call.input ?? {};
360
+ if (typeof args === "string") {
361
+ try {
362
+ args = JSON.parse(args);
363
+ }
364
+ catch {
365
+ args = {};
366
+ }
367
+ }
368
+ const argObj = args && typeof args === "object" ? args : {};
369
+ let toolResult = "ok";
370
+ if (name === "read_file") {
371
+ steps.read = true;
372
+ toolResult = currentFile;
373
+ }
374
+ else if (name === "write_file") {
375
+ steps.wrote = true;
376
+ currentFile = String(argObj.content || "");
377
+ toolResult = "wrote scale.py";
378
+ }
379
+ else if (name === "edit_file") {
380
+ // Apply the targeted edit to the file we are tracking.
381
+ steps.wrote = true;
382
+ const oldText = String(argObj.old_text ?? "");
383
+ const newText = String(argObj.new_text ?? "");
384
+ if (oldText && currentFile.includes(oldText)) {
385
+ currentFile = currentFile.replace(oldText, newText);
386
+ toolResult = "edited scale.py";
387
+ }
388
+ else {
389
+ toolResult = "edit failed: old_text not found in scale.py";
390
+ }
391
+ }
392
+ else if (name === "run_command") {
393
+ steps.ranTests = true;
394
+ const run = await runUnittestFiles({ "scale.py": currentFile, "test_scale.py": SCALE_TESTS }, "test_scale", { execute });
395
+ if (run.ran && run.total) {
396
+ testsExecuted = true;
397
+ total = run.total;
398
+ bestPassed = Math.max(bestPassed, run.passed ?? 0);
399
+ lastOutput = run.output;
400
+ toolResult = run.output || `ran ${run.total} tests`;
401
+ }
402
+ else if (!run.compiled) {
403
+ toolResult = `scale.py failed to compile:\n${run.output}`;
404
+ }
405
+ else {
406
+ toolResult = "tests could not be executed in this environment";
407
+ }
408
+ }
409
+ else if (name === "list_directory") {
410
+ toolResult = "scale.py\ntest_scale.py";
411
+ }
412
+ else if (name === "find_files") {
413
+ toolResult = "scale.py\ntest_scale.py";
414
+ }
415
+ messages.push({
416
+ role: "tool",
417
+ tool_call_id: call.id || "call_0",
418
+ content: toolResult,
419
+ });
420
+ }
421
+ if (testsExecuted && total !== null && bestPassed >= total)
422
+ break;
423
+ }
424
+ const ratio = total ? bestPassed / total : 0;
425
+ let score;
426
+ if (testsExecuted) {
427
+ // Correctness dominates; a little credit for driving the loop at all.
428
+ score =
429
+ (steps.read ? 0.1 : 0) + (steps.wrote ? 0.1 : 0) + (steps.ranTests ? 0.1 : 0) + ratio * 0.7;
430
+ }
431
+ else {
432
+ // No interpreter (or execute disabled): fall back to process credit only.
433
+ score = (steps.read ? 0.34 : 0) + (steps.wrote ? 0.33 : 0) + (steps.ranTests ? 0.33 : 0);
434
+ }
435
+ const contextUtilization = contextWindow && contextWindow > 0 ? peakPromptTokens / contextWindow : null;
436
+ const ctxNote = contextUtilization !== null ? `, held ${Math.round(contextUtilization * 100)}% ctx` : "";
437
+ return {
438
+ score: Math.min(1, score),
439
+ summary: stalled
440
+ ? `stalled: ${stalled}`
441
+ : testsExecuted
442
+ ? `${bestPassed}/${total} hidden tests pass in ${turns} turns${ctxNote}`
443
+ : `no tests executed (${[steps.read && "read", steps.wrote && "wrote", steps.ranTests && "ran"].filter(Boolean).join(", ") || "no progress"})`,
444
+ detail: {
445
+ steps,
446
+ turns,
447
+ bestPassed,
448
+ total,
449
+ testsExecuted,
450
+ stalled,
451
+ lastOutput,
452
+ peakPromptTokens,
453
+ contextWindow: contextWindow ?? null,
454
+ contextUtilization,
455
+ },
456
+ };
457
+ },
458
+ };
459
+ // --------------------------------------------------------- long-horizon code
460
+ export const LONG_TASK_PROMPT = "Write a complete, working Python implementation of a thread-safe LRU cache with " +
461
+ "per-entry TTL expiry and an eviction callback. Produce these THREE files in full, " +
462
+ "each in its own fenced code block labelled with its filename:\n\n" +
463
+ "1. lru.py - class LRUCache(maxsize, ttl=None, on_evict=None) built on OrderedDict " +
464
+ "and threading.RLock, with:\n" +
465
+ " - get(key): returns the value and marks it most-recently-used; returns None if " +
466
+ "missing or expired.\n" +
467
+ " - peek(key): returns the value WITHOUT changing recency; None if missing/expired.\n" +
468
+ " - put(key, value): inserts/updates; evicts the least-recently-used entry when over " +
469
+ "maxsize.\n" +
470
+ " - delete(key), clear(), __len__ and __contains__ - all treating expired entries as " +
471
+ "absent.\n" +
472
+ " - resize(new_maxsize): change the cap, evicting least-recently-used entries if now " +
473
+ "over it.\n" +
474
+ " - on_evict, when provided, is called with (key, value) for every eviction AND every " +
475
+ "TTL expiry.\n" +
476
+ "2. metrics.py - class Metrics tracking hits, misses, evictions and expirations, with " +
477
+ "snapshot() returning a dict of those four counts and reset() zeroing them.\n" +
478
+ "3. test_lru.py - a unittest suite with at least 12 test methods covering recency/eviction " +
479
+ "order, TTL expiry, peek not changing recency, resize eviction, the on_evict callback, " +
480
+ "delete, clear, __contains__, and concurrent access from threads. It must import from lru " +
481
+ 'and metrics and pass when run with "python -m unittest test_lru".\n\n' +
482
+ "Write every file completely. Do not abbreviate or leave placeholders.";
483
+ const longHorizonTask = {
484
+ id: "long-horizon",
485
+ category: "Long-horizon code",
486
+ weight: 4,
487
+ description: "Generates a multi-file program that compiles and whose tests pass",
488
+ async run({ chat, execute, contextWindow }) {
489
+ const started = Date.now();
490
+ const response = await chat({
491
+ messages: [{ role: "user", content: LONG_TASK_PROMPT }],
492
+ // Richer spec than before (peek, resize, eviction callback, 12+ tests);
493
+ // give thinking models room to reason and still emit every file.
494
+ max_tokens: 16384,
495
+ temperature: 0.4,
496
+ });
497
+ const elapsedSeconds = (Date.now() - started) / 1000;
498
+ const choice = response.choices?.[0] || {};
499
+ const message = choice.message || {};
500
+ const content = message.content || "";
501
+ const reasoning = message.reasoning_content || "";
502
+ if (!content) {
503
+ return {
504
+ score: 0,
505
+ summary: reasoning
506
+ ? `no content - all ${response.usage?.completion_tokens ?? "?"} tokens went to reasoning`
507
+ : "empty response",
508
+ detail: {
509
+ finishReason: choice.finish_reason,
510
+ reasoningChars: reasoning.length,
511
+ outputTokens: response.usage?.completion_tokens ?? null,
512
+ },
513
+ };
514
+ }
515
+ const verification = await verifyPython(content, {
516
+ expectedFiles: ["lru.py", "metrics.py", "test_lru.py"],
517
+ execute,
518
+ });
519
+ // Credit progressive achievement: files present, then compiling, then passing.
520
+ const fileScore = (3 - verification.filesMissing.length) / 3;
521
+ const compileScore = verification.compiled ? 1 : 0;
522
+ let testScore = 0;
523
+ if (verification.testsRun && verification.testsTotal) {
524
+ testScore = (verification.testsPassed ?? 0) / verification.testsTotal;
525
+ }
526
+ else if (!execute && verification.compiled) {
527
+ testScore = null; // not measured
528
+ }
529
+ const parts = [fileScore * 0.3, compileScore * 0.3];
530
+ let weightUsed = 0.6;
531
+ if (testScore !== null) {
532
+ parts.push(testScore * 0.4);
533
+ weightUsed = 1.0;
534
+ }
535
+ // Penalise elisions - they defeat the purpose of a long-horizon task.
536
+ const placeholderPenalty = Math.min(0.2, verification.placeholders * 0.1);
537
+ const score = Math.max(0, parts.reduce((a, b) => a + b, 0) / weightUsed - placeholderPenalty);
538
+ const bits = [
539
+ `${3 - verification.filesMissing.length}/3 files`,
540
+ verification.compiled ? "compiles" : "syntax errors",
541
+ ];
542
+ if (verification.testsRun) {
543
+ bits.push(`${verification.testsPassed}/${verification.testsTotal} tests pass`);
544
+ }
545
+ else if (!execute) {
546
+ bits.push("tests not executed");
547
+ }
548
+ else if (!verification.compiled) {
549
+ bits.push("tests not run (compile failed)");
550
+ }
551
+ else if (!verification.testFile) {
552
+ bits.push("no test file produced");
553
+ }
554
+ else {
555
+ bits.push("tests did not run");
556
+ }
557
+ if (verification.placeholders)
558
+ bits.push(`${verification.placeholders} placeholders`);
559
+ // How much of the loaded window the single generation held, reported like the
560
+ // other agentic tasks so every long-horizon row carries a context figure.
561
+ const peakPromptTokens = response.usage?.prompt_tokens ?? 0;
562
+ const contextUtilization = contextWindow && contextWindow > 0 ? peakPromptTokens / contextWindow : null;
563
+ const ctxNote = contextUtilization !== null ? `, held ${Math.round(contextUtilization * 100)}% ctx` : "";
564
+ return {
565
+ score,
566
+ summary: bits.join(", ") + ctxNote,
567
+ detail: {
568
+ finishReason: choice.finish_reason,
569
+ outputTokens: response.usage?.completion_tokens ?? null,
570
+ reasoningChars: reasoning.length,
571
+ contentChars: content.length,
572
+ elapsedSeconds,
573
+ peakPromptTokens,
574
+ contextWindow: contextWindow ?? null,
575
+ contextUtilization,
576
+ tokensPerSecond: response.timings?.predicted_per_second ?? null,
577
+ verification: {
578
+ ...verification,
579
+ testOutput: verification.testOutput ? verification.testOutput.slice(-800) : null,
580
+ },
581
+ },
582
+ };
583
+ },
584
+ };
585
+ // --------------------------------------------------------- extra-long horizon
586
+ /**
587
+ * The hardest task: fix a spec-driven bug in a multi-file codebase whose correct
588
+ * behaviour lives only in the docs. The model must EXPLORE the tree, read the
589
+ * markdown spec, and only then can it produce a fix the hidden oracle accepts -
590
+ * a naive code-only fix passes some tests but misses the two rules documented in
591
+ * docs/SPEC.md (discounts apply to the original subtotal; combined discount is
592
+ * capped at 50%). Reading widely fills context, which the task reports as a
593
+ * fraction of the loaded window. Scored on the real interpreter, never on prose.
594
+ */
595
+ const extraLongHorizonTask = {
596
+ id: "extra-long-horizon",
597
+ category: "Extra-long horizon",
598
+ weight: 4,
599
+ description: "Researches a codebase and its docs to fix a spec-driven bug",
600
+ async run({ chat, execute = true, contextWindow }) {
601
+ // The model's editable copy of the Python modules; docs stay read-only.
602
+ const workingCopy = new Map();
603
+ for (const file of EXTRA_PY_FILES)
604
+ workingCopy.set(file, EXTRA_CORPUS[file]);
605
+ const readFile = (path) => {
606
+ if (workingCopy.has(path))
607
+ return workingCopy.get(path) ?? null;
608
+ return path in EXTRA_CORPUS ? EXTRA_CORPUS[path] : null;
609
+ };
610
+ const listDir = (dir) => {
611
+ const prefix = dir === "." || dir === "" || dir === "/" ? "" : `${dir.replace(/\/+$/, "")}/`;
612
+ const names = new Set();
613
+ for (const path of Object.keys(EXTRA_CORPUS)) {
614
+ if (!path.startsWith(prefix))
615
+ continue;
616
+ const rest = path.slice(prefix.length);
617
+ const seg = rest.split("/")[0];
618
+ if (seg)
619
+ names.add(rest.includes("/") ? `${seg}/` : seg);
620
+ }
621
+ return [...names].sort().join("\n") || "(empty)";
622
+ };
623
+ const messages = [
624
+ {
625
+ role: "user",
626
+ content: "The orderkit library prices an order by applying discount rules to a cart subtotal. A " +
627
+ "bug in pricing.py makes large multi-rule discounts wrong - some order totals come out " +
628
+ "too low. The intended behaviour is defined in the project's own documentation, and is " +
629
+ "NOT obvious from the code. Explore the repository (list_directory, read_file, " +
630
+ "search_files, find_files), find and read the specification, then fix pricing.py so it " +
631
+ 'matches the spec, and run the tests with "python -m unittest test_pricing". Use your ' +
632
+ "tools for every step; the test files are read-only.",
633
+ },
634
+ ];
635
+ const filesRead = new Set();
636
+ const steps = { explored: false, researched: false, wrote: false, ranTests: false };
637
+ let bestPassed = 0;
638
+ let total = null;
639
+ let lastOutput = "";
640
+ let testsExecuted = false;
641
+ let peakPromptTokens = 0;
642
+ let turns = 0;
643
+ const maxTurns = 16;
644
+ let stalled = null;
645
+ while (turns < maxTurns) {
646
+ turns += 1;
647
+ const response = await chat({ messages, tools: TOOLS, max_tokens: 4096, temperature: 0.3 });
648
+ peakPromptTokens = Math.max(peakPromptTokens, response.usage?.prompt_tokens ?? 0);
649
+ const message = response.choices?.[0]?.message || {};
650
+ const calls = message.tool_calls || [];
651
+ if (!calls.length) {
652
+ if (!message.content)
653
+ stalled = "produced neither content nor a tool call";
654
+ break;
655
+ }
656
+ messages.push({ role: "assistant", content: message.content || null, tool_calls: calls });
657
+ for (const call of calls) {
658
+ const name = call.function?.name || call.name;
659
+ let args = call.function?.arguments ?? call.input ?? {};
660
+ if (typeof args === "string") {
661
+ try {
662
+ args = JSON.parse(args);
663
+ }
664
+ catch {
665
+ args = {};
666
+ }
667
+ }
668
+ const a = args && typeof args === "object" ? args : {};
669
+ const cleanPath = (p) => String(p ?? "").replace(/^\.?\//, "");
670
+ let toolResult = "ok";
671
+ if (name === "read_file") {
672
+ const path = cleanPath(a.path);
673
+ const content = readFile(path);
674
+ if (content === null) {
675
+ toolResult = `file not found: ${path}`;
676
+ }
677
+ else {
678
+ filesRead.add(path);
679
+ if (/spec/i.test(path))
680
+ steps.researched = true;
681
+ toolResult = content;
682
+ }
683
+ }
684
+ else if (name === "list_directory") {
685
+ toolResult = listDir(String(a.path ?? "."));
686
+ }
687
+ else if (name === "find_files") {
688
+ const pattern = String(a.pattern ?? "");
689
+ toolResult =
690
+ Object.keys(EXTRA_CORPUS)
691
+ .filter((p) => globMatchesPath(pattern, p))
692
+ .join("\n") || "(no matches)";
693
+ }
694
+ else if (name === "search_files") {
695
+ const query = String(a.query ?? "");
696
+ const hits = [];
697
+ if (query) {
698
+ for (const [path, content] of Object.entries(EXTRA_CORPUS)) {
699
+ content.split("\n").forEach((line, i) => {
700
+ if (line.includes(query))
701
+ hits.push(`${path}:${i + 1}: ${line.trim()}`);
702
+ });
703
+ }
704
+ }
705
+ toolResult = hits.slice(0, 40).join("\n") || "(no matches)";
706
+ }
707
+ else if (name === "write_file") {
708
+ const path = cleanPath(a.path);
709
+ if (/test_/.test(path)) {
710
+ toolResult = "refused: test files are read-only";
711
+ }
712
+ else {
713
+ workingCopy.set(path, String(a.content ?? ""));
714
+ if (path === EXTRA_TARGET_FILE)
715
+ steps.wrote = true;
716
+ toolResult = `wrote ${path}`;
717
+ }
718
+ }
719
+ else if (name === "edit_file") {
720
+ const path = cleanPath(a.path);
721
+ const oldText = String(a.old_text ?? "");
722
+ const newText = String(a.new_text ?? "");
723
+ const current = workingCopy.get(path) ?? EXTRA_CORPUS[path];
724
+ if (/test_/.test(path)) {
725
+ toolResult = "refused: test files are read-only";
726
+ }
727
+ else if (current && oldText && current.includes(oldText)) {
728
+ workingCopy.set(path, current.replace(oldText, newText));
729
+ if (path === EXTRA_TARGET_FILE)
730
+ steps.wrote = true;
731
+ toolResult = `edited ${path}`;
732
+ }
733
+ else {
734
+ toolResult = "edit failed: old_text not found";
735
+ }
736
+ }
737
+ else if (name === "run_command") {
738
+ steps.ranTests = true;
739
+ const files = { "test_pricing.py": EXTRA_HIDDEN_TEST };
740
+ for (const file of EXTRA_PY_FILES)
741
+ files[file] = workingCopy.get(file) ?? EXTRA_CORPUS[file];
742
+ const run = await runUnittestFiles(files, "test_pricing", { execute });
743
+ if (run.ran && run.total) {
744
+ testsExecuted = true;
745
+ total = run.total;
746
+ bestPassed = Math.max(bestPassed, run.passed ?? 0);
747
+ lastOutput = run.output;
748
+ toolResult = run.output || `ran ${run.total} tests`;
749
+ }
750
+ else if (!run.compiled) {
751
+ toolResult = `code failed to compile:\n${run.output}`;
752
+ }
753
+ else {
754
+ toolResult = "tests could not be executed in this environment";
755
+ }
756
+ }
757
+ messages.push({ role: "tool", tool_call_id: call.id || "call_0", content: toolResult });
758
+ }
759
+ steps.explored = filesRead.size >= 3;
760
+ if (testsExecuted && total !== null && bestPassed >= total)
761
+ break;
762
+ }
763
+ const ratio = total ? bestPassed / total : 0;
764
+ let score;
765
+ if (testsExecuted) {
766
+ score =
767
+ (steps.explored ? 0.1 : 0) +
768
+ (steps.researched ? 0.1 : 0) +
769
+ (steps.wrote ? 0.05 : 0) +
770
+ (steps.ranTests ? 0.05 : 0) +
771
+ ratio * 0.7;
772
+ }
773
+ else {
774
+ score =
775
+ (steps.explored ? 0.3 : 0) +
776
+ (steps.researched ? 0.3 : 0) +
777
+ (steps.wrote ? 0.2 : 0) +
778
+ (steps.ranTests ? 0.2 : 0);
779
+ }
780
+ const contextUtilization = contextWindow && contextWindow > 0 ? peakPromptTokens / contextWindow : null;
781
+ const ctxNote = contextUtilization !== null ? `, held ${Math.round(contextUtilization * 100)}% ctx` : "";
782
+ return {
783
+ score: Math.min(1, score),
784
+ summary: stalled
785
+ ? `stalled: ${stalled}`
786
+ : testsExecuted
787
+ ? `${bestPassed}/${total} tests pass, ${filesRead.size} files read${steps.researched ? "" : " (missed the spec)"}${ctxNote}`
788
+ : `no tests executed (${filesRead.size} files read)`,
789
+ detail: {
790
+ steps,
791
+ turns,
792
+ bestPassed,
793
+ total,
794
+ testsExecuted,
795
+ filesRead: [...filesRead],
796
+ peakPromptTokens,
797
+ contextWindow: contextWindow ?? null,
798
+ contextUtilization,
799
+ stalled,
800
+ lastOutput,
801
+ },
802
+ };
803
+ },
804
+ };
805
+ // -------------------------------------------------------------- context stress
806
+ /**
807
+ * A volume test, not a puzzle: a staged pipeline whose N modules are all
808
+ * passthrough placeholders, with each stage's real rule living ONLY in a
809
+ * specification generated large enough to fill ~55% of the served context
810
+ * window (see context-corpus.ts). The model must read the spec to fix any stage
811
+ * and hold most of it to fix them all, so a high score is proof it held real
812
+ * context - the signal a 1-2%-held long-horizon task cannot give. Sized from the
813
+ * model's own loaded window; scored on a hidden per-stage oracle.
814
+ */
815
+ const contextStressTask = {
816
+ id: "context-stress",
817
+ category: "Context stress",
818
+ weight: 4,
819
+ description: "Reconstructs N spec-defined pipeline stages from a window-filling spec",
820
+ async run({ chat, execute = true, contextWindow }) {
821
+ // Fill ~55% of the loaded window (est. 4 chars/token), clamped so a tiny
822
+ // window still gets a real pipeline and a huge one stays bounded. Fall back
823
+ // to a solid fixed size when the endpoint does not report a window.
824
+ const targetTokens = contextWindow && contextWindow > 0
825
+ ? Math.min(160000, Math.round(contextWindow * 0.55))
826
+ : 16000;
827
+ const corpus = generateContextCorpus({ targetTokens });
828
+ const workingCopy = new Map();
829
+ for (const file of corpus.pyFiles)
830
+ workingCopy.set(file, corpus.files[file]);
831
+ const readFile = (path) => {
832
+ if (workingCopy.has(path))
833
+ return workingCopy.get(path) ?? null;
834
+ return path in corpus.files ? corpus.files[path] : null;
835
+ };
836
+ const listDir = (dir) => {
837
+ const prefix = dir === "." || dir === "" || dir === "/" ? "" : `${dir.replace(/\/+$/, "")}/`;
838
+ const names = new Set();
839
+ for (const path of Object.keys(corpus.files)) {
840
+ if (!path.startsWith(prefix))
841
+ continue;
842
+ const rest = path.slice(prefix.length);
843
+ const seg = rest.split("/")[0];
844
+ if (seg)
845
+ names.add(rest.includes("/") ? `${seg}/` : seg);
846
+ }
847
+ return [...names].sort().join("\n") || "(empty)";
848
+ };
849
+ const messages = [
850
+ {
851
+ role: "user",
852
+ content: `The staged-pipeline project has ${corpus.stages.length} stage modules ` +
853
+ "(stage_00.py, stage_01.py, ...), each a placeholder that returns its input unchanged. " +
854
+ "The rule every stage must implement - an operation and an operand - is defined ONLY in " +
855
+ "the specification under docs/spec/. Explore the repository (list_directory, read_file, " +
856
+ "search_files, find_files), read the spec parts, implement every stage's apply(x) exactly " +
857
+ 'as specified, and run the tests with "python -m unittest test_pipeline". The code carries ' +
858
+ "no hint of the operands; you must read the spec. Test files are read-only.",
859
+ },
860
+ ];
861
+ const filesRead = new Set();
862
+ const steps = { explored: false, researched: false, wrote: false, ranTests: false };
863
+ let bestPassed = 0;
864
+ let total = null;
865
+ let lastOutput = "";
866
+ let testsExecuted = false;
867
+ let peakPromptTokens = 0;
868
+ let turns = 0;
869
+ const maxTurns = 24;
870
+ let stalled = null;
871
+ while (turns < maxTurns) {
872
+ turns += 1;
873
+ const response = await chat({ messages, tools: TOOLS, max_tokens: 4096, temperature: 0.3 });
874
+ peakPromptTokens = Math.max(peakPromptTokens, response.usage?.prompt_tokens ?? 0);
875
+ const message = response.choices?.[0]?.message || {};
876
+ const calls = message.tool_calls || [];
877
+ if (!calls.length) {
878
+ if (!message.content)
879
+ stalled = "produced neither content nor a tool call";
880
+ break;
881
+ }
882
+ messages.push({ role: "assistant", content: message.content || null, tool_calls: calls });
883
+ for (const call of calls) {
884
+ const name = call.function?.name || call.name;
885
+ let args = call.function?.arguments ?? call.input ?? {};
886
+ if (typeof args === "string") {
887
+ try {
888
+ args = JSON.parse(args);
889
+ }
890
+ catch {
891
+ args = {};
892
+ }
893
+ }
894
+ const a = args && typeof args === "object" ? args : {};
895
+ const cleanPath = (p) => String(p ?? "").replace(/^\.?\//, "");
896
+ const isStagePath = (p) => /^stage_\d+\.py$/.test(p) || p === "pipeline.py";
897
+ let toolResult = "ok";
898
+ if (name === "read_file") {
899
+ const path = cleanPath(a.path);
900
+ const content = readFile(path);
901
+ if (content === null) {
902
+ toolResult = `file not found: ${path}`;
903
+ }
904
+ else {
905
+ filesRead.add(path);
906
+ if (/spec/i.test(path))
907
+ steps.researched = true;
908
+ toolResult = content;
909
+ }
910
+ }
911
+ else if (name === "list_directory") {
912
+ toolResult = listDir(String(a.path ?? "."));
913
+ }
914
+ else if (name === "find_files") {
915
+ const pattern = String(a.pattern ?? "");
916
+ toolResult =
917
+ Object.keys(corpus.files)
918
+ .filter((p) => globMatchesPath(pattern, p))
919
+ .join("\n") || "(no matches)";
920
+ }
921
+ else if (name === "search_files") {
922
+ const query = String(a.query ?? "");
923
+ const hits = [];
924
+ if (query) {
925
+ for (const [path, content] of Object.entries(corpus.files)) {
926
+ content.split("\n").forEach((line, i) => {
927
+ if (line.includes(query))
928
+ hits.push(`${path}:${i + 1}: ${line.trim()}`);
929
+ });
930
+ }
931
+ }
932
+ toolResult = hits.slice(0, 40).join("\n") || "(no matches)";
933
+ }
934
+ else if (name === "write_file") {
935
+ const path = cleanPath(a.path);
936
+ if (/test_/.test(path)) {
937
+ toolResult = "refused: test files are read-only";
938
+ }
939
+ else {
940
+ workingCopy.set(path, String(a.content ?? ""));
941
+ if (isStagePath(path))
942
+ steps.wrote = true;
943
+ toolResult = `wrote ${path}`;
944
+ }
945
+ }
946
+ else if (name === "edit_file") {
947
+ const path = cleanPath(a.path);
948
+ const oldText = String(a.old_text ?? "");
949
+ const newText = String(a.new_text ?? "");
950
+ const current = workingCopy.get(path) ?? corpus.files[path];
951
+ if (/test_/.test(path)) {
952
+ toolResult = "refused: test files are read-only";
953
+ }
954
+ else if (current && oldText && current.includes(oldText)) {
955
+ workingCopy.set(path, current.replace(oldText, newText));
956
+ if (isStagePath(path))
957
+ steps.wrote = true;
958
+ toolResult = `edited ${path}`;
959
+ }
960
+ else {
961
+ toolResult = "edit failed: old_text not found";
962
+ }
963
+ }
964
+ else if (name === "run_command") {
965
+ steps.ranTests = true;
966
+ const files = { "test_pipeline.py": corpus.hiddenTest };
967
+ for (const file of corpus.pyFiles)
968
+ files[file] = workingCopy.get(file) ?? corpus.files[file];
969
+ const run = await runUnittestFiles(files, "test_pipeline", { execute });
970
+ if (run.ran && run.total) {
971
+ testsExecuted = true;
972
+ total = run.total;
973
+ bestPassed = Math.max(bestPassed, run.passed ?? 0);
974
+ lastOutput = run.output;
975
+ toolResult = run.output || `ran ${run.total} tests`;
976
+ }
977
+ else if (!run.compiled) {
978
+ toolResult = `code failed to compile:\n${run.output}`;
979
+ }
980
+ else {
981
+ toolResult = "tests could not be executed in this environment";
982
+ }
983
+ }
984
+ messages.push({ role: "tool", tool_call_id: call.id || "call_0", content: toolResult });
985
+ }
986
+ steps.explored = filesRead.size >= 3;
987
+ if (testsExecuted && total !== null && bestPassed >= total)
988
+ break;
989
+ }
990
+ const ratio = total ? bestPassed / total : 0;
991
+ let score;
992
+ if (testsExecuted) {
993
+ score =
994
+ (steps.explored ? 0.1 : 0) +
995
+ (steps.researched ? 0.1 : 0) +
996
+ (steps.wrote ? 0.05 : 0) +
997
+ (steps.ranTests ? 0.05 : 0) +
998
+ ratio * 0.7;
999
+ }
1000
+ else {
1001
+ score =
1002
+ (steps.explored ? 0.3 : 0) +
1003
+ (steps.researched ? 0.3 : 0) +
1004
+ (steps.wrote ? 0.2 : 0) +
1005
+ (steps.ranTests ? 0.2 : 0);
1006
+ }
1007
+ const contextUtilization = contextWindow && contextWindow > 0 ? peakPromptTokens / contextWindow : null;
1008
+ const ctxNote = contextUtilization !== null ? `, held ${Math.round(contextUtilization * 100)}% ctx` : "";
1009
+ return {
1010
+ score: Math.min(1, score),
1011
+ summary: stalled
1012
+ ? `stalled: ${stalled}`
1013
+ : testsExecuted
1014
+ ? `${bestPassed}/${total} stages pass, ${filesRead.size} files read${steps.researched ? "" : " (missed the spec)"}${ctxNote}`
1015
+ : `no tests executed (${filesRead.size} files read)`,
1016
+ detail: {
1017
+ steps,
1018
+ turns,
1019
+ stageCount: corpus.stages.length,
1020
+ targetTokens,
1021
+ bestPassed,
1022
+ total,
1023
+ testsExecuted,
1024
+ filesRead: [...filesRead],
1025
+ peakPromptTokens,
1026
+ contextWindow: contextWindow ?? null,
1027
+ contextUtilization,
1028
+ stalled,
1029
+ lastOutput,
1030
+ },
1031
+ };
1032
+ },
1033
+ };
1034
+ /**
1035
+ * A 400 from llama-server when the prompt is longer than the model's loaded
1036
+ * context. That is a limit of how the model was loaded, not a quality failure,
1037
+ * so the depth task stops probing rather than zeroing the whole category.
1038
+ */
1039
+ function isContextLimitError(message) {
1040
+ // Specific to a context-length rejection — a bare "context" would misclassify
1041
+ // any unrelated 500 whose body happens to mention the word.
1042
+ return /exceed|n_ctx|context (?:length|window|size)|too (?:many|long|large)|larger than|prompt is too long/i.test(message);
1043
+ }
1044
+ // Match a corpus path against a simple glob (single-star and recursive
1045
+ // double-star wildcards) on a path suffix, so a conventional recursive
1046
+ // "star-star-slash-star-dot-py" pattern finds `pricing.py`. The old behavior
1047
+ // just stripped every star, turning that pattern into a needle that matched
1048
+ // nothing in a flat corpus and starved the model's file-discovery signal.
1049
+ function globMatchesPath(pattern, filePath) {
1050
+ const raw = pattern.toLowerCase().trim();
1051
+ if (!raw)
1052
+ return false;
1053
+ const body = raw
1054
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
1055
+ .replace(/\*\*\//g, "")
1056
+ .replace(/\*/g, "[^/]*");
1057
+ try {
1058
+ return new RegExp(`(?:^|/)${body}$`).test(filePath.toLowerCase());
1059
+ }
1060
+ catch {
1061
+ return filePath.toLowerCase().includes(raw.replace(/\*/g, ""));
1062
+ }
1063
+ }
1064
+ /**
1065
+ * Throughput and latency at realistic prompt depth. Agent loops resend large
1066
+ * prefixes, so behaviour on an empty context says little about real use.
1067
+ */
1068
+ const contextDepthTask = {
1069
+ id: "context-depth",
1070
+ category: "Depth scaling",
1071
+ weight: 2,
1072
+ description: "Holds latency and throughput as the prompt grows",
1073
+ async run({ chat, depths = [] }) {
1074
+ const filler = ("The following is an excerpt from an internal engineering handbook about " +
1075
+ "service reliability, retry budgets, idempotency keys and backoff strategy. ").repeat(40);
1076
+ // Ascending, so once a depth exceeds the loaded context every deeper probe
1077
+ // would too - we stop there and score the depths that fit.
1078
+ const ordered = [...depths].sort((a, b) => a - b);
1079
+ const points = [];
1080
+ let contextLimited = false;
1081
+ for (const targetTokens of ordered) {
1082
+ // ~4 characters per token is close enough to hit a depth band.
1083
+ const repeats = Math.max(1, Math.round((targetTokens * 4) / filler.length));
1084
+ const padding = filler.repeat(repeats);
1085
+ let response;
1086
+ try {
1087
+ response = await chat({
1088
+ messages: [
1089
+ {
1090
+ role: "user",
1091
+ content: `Reference material:\n\n${padding}\n\nIn one sentence, what is an idempotency key for?`,
1092
+ },
1093
+ ],
1094
+ // Generous enough that a thinking model still reaches content; this
1095
+ // task measures latency and throughput, not answer quality.
1096
+ max_tokens: 2048,
1097
+ temperature: 0.3,
1098
+ });
1099
+ }
1100
+ catch (error) {
1101
+ const message = error instanceof Error ? error.message : String(error);
1102
+ // A prompt past the loaded context ends the probe cleanly; score what
1103
+ // fit. Any other error with no measurement yet is a genuine failure.
1104
+ if (isContextLimitError(message)) {
1105
+ contextLimited = true;
1106
+ break;
1107
+ }
1108
+ if (points.length === 0)
1109
+ throw error;
1110
+ break;
1111
+ }
1112
+ const usage = response.usage || {};
1113
+ const timings = response.timings || {};
1114
+ const message = response.choices?.[0]?.message || {};
1115
+ const answered = (message.content || "").trim().length > 0;
1116
+ const reasonedOnly = !answered && (message.reasoning_content || "").length > 0;
1117
+ points.push({
1118
+ targetTokens,
1119
+ promptTokens: usage.prompt_tokens ?? null,
1120
+ ttftSeconds: timings.prompt_ms ? timings.prompt_ms / 1000 : null,
1121
+ promptPerSecond: timings.prompt_per_second ?? null,
1122
+ generatePerSecond: timings.predicted_per_second ?? null,
1123
+ answered,
1124
+ reasonedOnly,
1125
+ });
1126
+ }
1127
+ // Even the shallowest probe exceeded the context - nothing measurable.
1128
+ if (points.length === 0) {
1129
+ return {
1130
+ score: 0,
1131
+ summary: contextLimited
1132
+ ? "every probe depth exceeds the model's loaded context"
1133
+ : "no depth measurements",
1134
+ detail: points,
1135
+ };
1136
+ }
1137
+ const answeredAll = points.every((p) => p.answered);
1138
+ const first = points[0];
1139
+ const last = points[points.length - 1];
1140
+ // Generation speed should not collapse as context grows; a large drop
1141
+ // usually means the KV cache spilled out of VRAM. Retention needs at least
1142
+ // two depths to compare - a single fitting depth says nothing about scaling.
1143
+ const firstGen = first?.generatePerSecond;
1144
+ const lastGen = last?.generatePerSecond;
1145
+ const retention = points.length >= 2 && firstGen && lastGen ? lastGen / firstGen : null;
1146
+ let score = answeredAll ? 0.5 : 0.2;
1147
+ if (retention !== null)
1148
+ score += Math.max(0, Math.min(0.5, retention * 0.5));
1149
+ const depthLabel = last.promptTokens?.toLocaleString() ?? "?";
1150
+ let summary = retention !== null
1151
+ ? `${(retention * 100).toFixed(0)}% of throughput retained at ${depthLabel} tokens`
1152
+ : `held ${depthLabel} tokens`;
1153
+ if (!answeredAll) {
1154
+ summary += points.some((p) => p.reasonedOnly)
1155
+ ? ", some depths returned only reasoning"
1156
+ : ", some depths returned nothing";
1157
+ }
1158
+ if (contextLimited)
1159
+ summary += `, context-limited (${points.length}/${ordered.length} depths fit)`;
1160
+ return {
1161
+ score: Math.min(1, score),
1162
+ summary,
1163
+ detail: points,
1164
+ };
1165
+ },
1166
+ };
1167
+ // ---------------------------------------------------------------- concurrency
1168
+ /**
1169
+ * Aggregate throughput with several requests in flight at once — the load a
1170
+ * shared host actually sees when multiple chats or a model's own sub-agents hit
1171
+ * it together. Reports tokens/sec in (prompt) and out (generation) summed across
1172
+ * the concurrent requests; the score is how many returned real content, which
1173
+ * also catches reasoning-runaway showing up only under load.
1174
+ */
1175
+ const THROUGHPUT_PROMPT = "Explain, in about 200 words, how a hash map resolves collisions, covering " +
1176
+ "separate chaining and open addressing and when each is preferable.";
1177
+ const concurrencyTask = {
1178
+ id: "concurrency",
1179
+ category: "Concurrency",
1180
+ weight: 2,
1181
+ description: "Aggregate tokens/sec with several requests in flight at once",
1182
+ async run({ chat, concurrency = 3, reasoningBudget }) {
1183
+ const n = Math.max(1, concurrency);
1184
+ // A thinking model needs room to finish reasoning AND still emit content.
1185
+ // Cap the response at 512 and the whole allowance goes to reasoning, so every
1186
+ // request comes back empty and this task scores 0% no matter the model. Size
1187
+ // the cap above the reasoning budget: the budget (enforced by llama-server's
1188
+ // --reasoning-budget) is then what ends thinking, leaving CONTENT_MARGIN
1189
+ // tokens for the actual answer. Fall back to a safe cap when the budget is
1190
+ // unknown (an arbitrary --endpoint) or thinking is disabled.
1191
+ const CONTENT_MARGIN = 512;
1192
+ // reasoningBudget === -1 means UNRESTRICTED thinking (sweep couldn't cap it):
1193
+ // the 2048 fallback is then the exact "all tokens go to reasoning, content is
1194
+ // empty, task scores 0%" trap the note above describes, so give a generous cap
1195
+ // instead. Positive budgets size the cap above the budget; 2048 is only for
1196
+ // thinking disabled/unknown.
1197
+ const UNRESTRICTED_THINKING_MAX_TOKENS = 8192;
1198
+ const maxTokens = typeof reasoningBudget === "number" && reasoningBudget > 0
1199
+ ? reasoningBudget + CONTENT_MARGIN
1200
+ : reasoningBudget === -1
1201
+ ? UNRESTRICTED_THINKING_MAX_TOKENS
1202
+ : 2048;
1203
+ const started = Date.now();
1204
+ const responses = await Promise.all(Array.from({ length: n }, () => chat({
1205
+ messages: [{ role: "user", content: THROUGHPUT_PROMPT }],
1206
+ max_tokens: maxTokens,
1207
+ temperature: 0.4,
1208
+ }).catch((error) => ({ error: error.message }))));
1209
+ const wallSeconds = (Date.now() - started) / 1000;
1210
+ let promptTokens = 0;
1211
+ let genTokens = 0;
1212
+ let completed = 0;
1213
+ for (const response of responses) {
1214
+ if (response.error)
1215
+ continue;
1216
+ const usage = response.usage || {};
1217
+ promptTokens += usage.prompt_tokens || 0;
1218
+ genTokens += usage.completion_tokens || 0;
1219
+ if ((response.choices?.[0]?.message?.content || "").trim())
1220
+ completed += 1;
1221
+ }
1222
+ const genPerSecond = wallSeconds ? genTokens / wallSeconds : 0;
1223
+ const promptPerSecond = wallSeconds ? promptTokens / wallSeconds : 0;
1224
+ return {
1225
+ score: completed / n,
1226
+ summary: `${n} at once: ${genPerSecond.toFixed(0)} gen tok/s, ${promptPerSecond.toFixed(0)} prompt tok/s, ${completed}/${n} answered`,
1227
+ detail: {
1228
+ concurrency: n,
1229
+ maxTokens,
1230
+ wallSeconds,
1231
+ promptTokens,
1232
+ genTokens,
1233
+ genPerSecond,
1234
+ promptPerSecond,
1235
+ completed,
1236
+ },
1237
+ };
1238
+ },
1239
+ };
1240
+ export const TASKS = [
1241
+ toolCallingTask,
1242
+ agenticLoopTask,
1243
+ longHorizonTask,
1244
+ extraLongHorizonTask,
1245
+ contextStressTask,
1246
+ contextDepthTask,
1247
+ concurrencyTask,
1248
+ ];
1249
+ //# sourceMappingURL=tasks.js.map