@danieltmn/tamnora 0.25.1 → 0.26.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +13 -25
  3. package/SECURITY.md +3 -4
  4. package/harness/packages/cli/src/cli.js +0 -3
  5. package/harness/packages/cli/src/commands/bridge.js +0 -1
  6. package/harness/packages/cli/src/commands/repl.js +3 -3
  7. package/harness/packages/cli/src/commands/run.js +1 -1
  8. package/harness/packages/cli/src/commands/server.js +1 -1
  9. package/harness/packages/cli/src/commands/tui.js +1 -1
  10. package/harness/packages/cli/src/lib/util.js +2 -2
  11. package/harness/packages/cli/src/tui/app.js +2 -2
  12. package/harness/packages/cli/test/tunnel.test.js +5 -32
  13. package/harness/packages/core/src/agents/index.js +13 -41
  14. package/harness/packages/core/src/config/app.js +0 -4
  15. package/harness/packages/core/src/config/home.js +1 -20
  16. package/harness/packages/core/src/config/index.js +5 -8
  17. package/harness/packages/core/src/extensions/load.js +22 -0
  18. package/harness/packages/core/src/frontmatter.js +32 -0
  19. package/harness/packages/core/src/hooks/index.js +2 -16
  20. package/harness/packages/core/src/index.js +2 -14
  21. package/harness/packages/core/src/plugins/index.js +2 -16
  22. package/harness/packages/core/src/procs/project.js +68 -0
  23. package/harness/packages/core/src/procs/static.js +17 -0
  24. package/harness/packages/core/src/rules/index.js +8 -27
  25. package/harness/packages/core/src/skills/index.js +8 -19
  26. package/harness/packages/core/src/storage/sqlite.js +595 -690
  27. package/harness/packages/core/src/tunnel.js +9 -147
  28. package/harness/packages/core/test/home.test.js +2 -35
  29. package/harness/packages/core/test/procs.test.js +3 -64
  30. package/harness/packages/core/test/storage.test.js +1 -12
  31. package/harness/packages/server/public/app.js +2 -19
  32. package/harness/packages/server/public/appearance.js +2 -56
  33. package/harness/packages/server/public/boot.js +2 -2
  34. package/harness/packages/server/public/cards.js +2 -21
  35. package/harness/packages/server/public/chat.js +0 -16
  36. package/harness/packages/server/public/chatFormat.js +0 -11
  37. package/harness/packages/server/public/chrome.js +2 -5
  38. package/harness/packages/server/public/commands.js +1 -2
  39. package/harness/packages/server/public/explorer.js +7 -10
  40. package/harness/packages/server/public/index.html +1228 -1326
  41. package/harness/packages/server/public/landing/boot.js +2 -3
  42. package/harness/packages/server/public/landing/index.html +3 -367
  43. package/harness/packages/server/public/settings.js +1255 -1322
  44. package/harness/packages/server/public/settingsNav.js +0 -1
  45. package/harness/packages/server/public/styles.css +2341 -2867
  46. package/harness/packages/server/public/system.js +356 -495
  47. package/harness/packages/server/public/voice.js +6 -22
  48. package/harness/packages/server/src/auth.js +4 -15
  49. package/harness/packages/server/src/cli.js +1 -1
  50. package/harness/packages/server/src/lib/agents.js +13 -27
  51. package/harness/packages/server/src/lib/appearance.js +3 -22
  52. package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
  53. package/harness/packages/server/src/lib/definitions.js +40 -0
  54. package/harness/packages/server/src/lib/skills.js +14 -21
  55. package/harness/packages/server/src/lib/ttsSettings.js +2 -2
  56. package/harness/packages/server/src/lib/workspace.js +9 -11
  57. package/harness/packages/server/src/routes/chat.js +0 -43
  58. package/harness/packages/server/src/routes/preview.js +5 -22
  59. package/harness/packages/server/src/routes/procs.js +2 -4
  60. package/harness/packages/server/src/routes/tts.js +1 -1
  61. package/harness/packages/server/src/server.js +1 -5
  62. package/harness/packages/server/test/appearance-route.test.js +3 -3
  63. package/harness/packages/server/test/appearance.test.js +12 -38
  64. package/harness/packages/server/test/chatFormat.test.js +0 -8
  65. package/harness/packages/server/test/consoleTabs.test.js +4 -4
  66. package/harness/packages/server/test/ops.test.js +8 -56
  67. package/harness/packages/server/test/preview.test.js +2 -42
  68. package/harness/packages/server/test/security.test.js +194 -370
  69. package/harness/packages/server/test/settingsNav.test.js +2 -2
  70. package/harness/packages/server/test/tts.test.js +4 -4
  71. package/harness/tamnora.example.json +1 -1
  72. package/package.json +2 -2
  73. package/harness/packages/cli/src/commands/eval.js +0 -186
  74. package/harness/packages/core/src/eval/dataset.js +0 -47
  75. package/harness/packages/core/src/eval/judge.js +0 -39
  76. package/harness/packages/core/src/eval/report.js +0 -48
  77. package/harness/packages/core/src/eval/runTask.js +0 -62
  78. package/harness/packages/core/src/eval/runner.js +0 -153
  79. package/harness/packages/core/src/eval/scorers.js +0 -217
  80. package/harness/packages/core/src/eval/task.js +0 -59
  81. package/harness/packages/core/src/procs/detect.js +0 -149
  82. package/harness/packages/core/test/eval.test.js +0 -144
  83. package/harness/packages/server/src/lib/advisor.js +0 -198
  84. package/harness/packages/server/src/routes/advisor.js +0 -79
  85. package/harness/packages/server/src/routes/eval.js +0 -100
  86. package/harness/packages/server/test/advisor.test.js +0 -249
  87. package/harness/scripts/example-dataset.jsonl +0 -4
@@ -1,186 +0,0 @@
1
- import path from 'node:path';
2
- import {
3
- loadConfig,
4
- loadInstructions,
5
- loadDataset,
6
- runDataset,
7
- summarize,
8
- createJudge,
9
- Store,
10
- } from '../lib/core.js';
11
- import { parse, databasePath, num } from '../lib/util.js';
12
- import { buildSystemPrompt, resolveCliProvider } from '../lib/runtime.js';
13
- import { color } from '../ui/color.js';
14
-
15
- const OPTIONS = {
16
- provider: { type: 'string' },
17
- model: { type: 'string', multiple: true },
18
- dir: { type: 'string' },
19
- trials: { type: 'string' },
20
- concurrency: { type: 'string' },
21
- tools: { type: 'string' },
22
- 'no-tools': { type: 'boolean' },
23
- 'judge-model': { type: 'string' },
24
- yes: { type: 'boolean', short: 'y' },
25
- json: { type: 'boolean' },
26
- help: { type: 'boolean', short: 'h' },
27
- };
28
-
29
- function pct(value) {
30
- return `${Math.round(value * 100)}%`;
31
- }
32
-
33
- export async function evalCommand(argv) {
34
- const { values, positionals } = parse(argv, OPTIONS);
35
- if (values.help) {
36
- printHelp();
37
- return 0;
38
- }
39
-
40
- const datasetFile = positionals[0];
41
- if (!datasetFile) {
42
- console.error(color.red('Falta el dataset. Uso: tamnora eval <dataset.jsonl> [--model <id>]'));
43
- return 1;
44
- }
45
-
46
- const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
47
- const config = await loadConfig(cwd);
48
-
49
- const tasks = await loadDataset(path.resolve(cwd, datasetFile));
50
- if (!tasks.length) {
51
- console.error(color.red('El dataset está vacío.'));
52
- return 1;
53
- }
54
-
55
- const instructions = await loadInstructions(cwd, config.instructions);
56
- const systemPrompt = buildSystemPrompt(instructions);
57
- const trials = num(values.trials, 1);
58
- const concurrency = num(values.concurrency, 1);
59
-
60
- const store = new Store(databasePath(cwd));
61
- const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
62
- const providerId = resolved.providerId;
63
- const baseUrl = resolved.baseUrl;
64
- const apiKey = resolved.apiKey;
65
- const customProviders = resolved.customProviders;
66
- const models = (values.model?.length ? values.model : [resolved.model]).filter(Boolean);
67
- if (!models.length) {
68
- console.error(color.red('Falta el modelo (usá --model, Ajustes o "model" en tamnora.json).'));
69
- store.close();
70
- return 1;
71
- }
72
-
73
- const judge = createJudge({
74
- providerId,
75
- model: values['judge-model'] || config.judgeModel,
76
- baseUrl,
77
- apiKey,
78
- customProviders,
79
- });
80
-
81
- const enabledTools = values['no-tools']
82
- ? []
83
- : values.tools
84
- ? String(values.tools).split(',').map((s) => s.trim()).filter(Boolean)
85
- : Array.isArray(config.tools) && config.tools.length
86
- ? config.tools
87
- : undefined;
88
-
89
- const runId = store.createRun({
90
- dataset: datasetFile,
91
- config: { providerId, models, trials, concurrency, tools: enabledTools ?? 'all' },
92
- });
93
- const startedAt = Date.now();
94
-
95
- if (!values.json) {
96
- process.stderr.write(
97
- color.dim(
98
- `run ${runId} · ${tasks.length} tareas × ${models.length} modelos × ${trials} trials · concurrencia ${concurrency}\n`
99
- )
100
- );
101
- }
102
-
103
- const results = await runDataset({
104
- tasks,
105
- models,
106
- providerId,
107
- baseUrl,
108
- apiKey,
109
- customProviders,
110
- systemPrompt,
111
- rootDir: cwd,
112
- trials,
113
- concurrency,
114
- enabledTools,
115
- permissions: config.permissions,
116
- yes: Boolean(values.yes),
117
- judge,
118
- store,
119
- runId,
120
- onProgress(entry) {
121
- if (values.json) return;
122
- const mark = entry.error ? color.red('✗') : entry.passed ? color.green('✓') : color.yellow('·');
123
- const scorers = (entry.trace?.scorers ?? []).map((s) => `${s.scorer}:${s.passed ? 'ok' : 'no'}`).join(' ');
124
- const suffix = entry.error ? ` — ${entry.error}` : scorers ? ` [${scorers}]` : '';
125
- process.stdout.write(`${mark} ${entry.taskId} · ${entry.model}${suffix}\n`);
126
- },
127
- });
128
-
129
- const summary = summarize(results);
130
- summary.durationMs = Date.now() - startedAt;
131
- const status = summary.errors && summary.passed === 0 ? 'failed' : summary.errors ? 'done_with_errors' : 'done';
132
- store.finishRun(runId, { status, summary });
133
- store.close();
134
-
135
- if (values.json) {
136
- process.stdout.write(`${JSON.stringify({ runId, summary }, null, 2)}\n`);
137
- } else {
138
- printReport(summary, runId);
139
- }
140
-
141
- return summary.passed === 0 && summary.errors > 0 ? 1 : summary.passed < summary.total ? 1 : 0;
142
- }
143
-
144
- function printReport(summary, runId) {
145
- const lines = [];
146
- lines.push('');
147
- lines.push(color.bold(`Resultados (run ${runId})`));
148
- lines.push(color.dim('modelo'.padEnd(34) + 'pasó total pass errores'));
149
- for (const m of summary.models) {
150
- lines.push(
151
- `${String(m.model).slice(0, 33).padEnd(34)}${String(m.passed).padStart(4)}${String(m.total).padStart(7)}${pct(m.passRate).padStart(6)}${String(m.errors).padStart(9)}`
152
- );
153
- }
154
- if (summary.scorers.length) {
155
- lines.push('');
156
- lines.push(color.dim('scorer'.padEnd(34) + 'pasó total pass'));
157
- for (const s of summary.scorers) {
158
- lines.push(
159
- `${String(s.scorer).slice(0, 33).padEnd(34)}${String(s.passed).padStart(4)}${String(s.total).padStart(7)}${pct(s.passRate).padStart(6)}`
160
- );
161
- }
162
- }
163
- lines.push('');
164
- lines.push(
165
- color.dim(
166
- `total ${summary.total} · ${summary.passed} ok · ${summary.errors} errores · ${(summary.durationMs / 1000).toFixed(1)}s`
167
- )
168
- );
169
- process.stderr.write(`${lines.join('\n')}\n`);
170
- }
171
-
172
- function printHelp() {
173
- console.log(`Uso: tamnora eval <dataset.jsonl> [opciones]
174
-
175
- Opciones:
176
- --provider <id> Proveedor
177
- --model <id> Modelo (repetible para comparar varios)
178
- --trials <n> Repeticiones por tarea/modelo (default: 1)
179
- --concurrency <n> Tareas en paralelo (default: 1)
180
- --judge-model <id> Modelo para el scorer llmJudge
181
- --dir <path> Directorio del proyecto
182
- --tools <lista> Tools habilitadas (coma)
183
- --no-tools Deshabilitar tools
184
- -y, --yes Aprobar automáticamente permisos "ask"
185
- --json Salida JSON`);
186
- }
@@ -1,47 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
-
3
- /**
4
- * Load an evaluation dataset. Supports:
5
- * - a JSON array of tasks
6
- * - a single JSON object ({ tasks: [...] } or one task)
7
- * - JSONL (one task per line)
8
- *
9
- * @param {string} file
10
- * @returns {Promise<object[]>}
11
- */
12
- export async function loadDataset(file) {
13
- const raw = await readFile(file, 'utf8');
14
- const trimmed = raw.trim();
15
- if (!trimmed) return [];
16
-
17
- // JSONL: each non-empty line is a standalone JSON object
18
- if (!trimmed.startsWith('[') && !trimmed.startsWith('{')) {
19
- return parseJsonl(trimmed, file);
20
- }
21
-
22
- let parsed;
23
- try {
24
- parsed = JSON.parse(trimmed);
25
- } catch {
26
- // Fall back to JSONL (multi-line pretty JSON would have parsed above)
27
- return parseJsonl(trimmed, file);
28
- }
29
-
30
- if (Array.isArray(parsed)) return parsed;
31
- if (Array.isArray(parsed.tasks)) return parsed.tasks;
32
- return [parsed];
33
- }
34
-
35
- function parseJsonl(text, file) {
36
- const tasks = [];
37
- const lines = text.split(/\r?\n/);
38
- lines.forEach((line, index) => {
39
- if (!line.trim()) return;
40
- try {
41
- tasks.push(JSON.parse(line));
42
- } catch (err) {
43
- throw new Error(`Línea ${index + 1} inválida en ${file}: ${err.message}`);
44
- }
45
- });
46
- return tasks;
47
- }
@@ -1,39 +0,0 @@
1
- import { resolveProviderRuntime } from '../providers/index.js';
2
- import { extractJson } from './scorers.js';
3
-
4
- /**
5
- * Build an LLM-as-judge function. Returns null when no judge model is set.
6
- * The judge model receives the criteria and the candidate output and must
7
- * reply with JSON: { "passed": boolean, "reason": string }.
8
- *
9
- * @param {{providerId?:string, model?:string, baseUrl?:string, apiKey?:string, customProviders?:object[]}} opts
10
- */
11
- export function createJudge({ providerId, model, baseUrl, apiKey, customProviders = [] } = {}) {
12
- if (!model) return null;
13
- const { provider, config } = resolveProviderRuntime({ providerId, apiKey, baseUrl, customProviders });
14
-
15
- return async ({ criteria, output, input }) => {
16
- const prompt =
17
- 'Sos un evaluador estricto. Decidí si la RESPUESTA cumple el CRITERIO.\n\n' +
18
- `CRITERIO: ${criteria}\n` +
19
- `ENTRADA: ${input ?? ''}\n` +
20
- `RESPUESTA: ${output ?? ''}\n\n` +
21
- 'Respondé SOLO un JSON: {"passed": true|false, "reason": "breve"}';
22
-
23
- try {
24
- const result = await provider.streamChat(config, {
25
- model,
26
- messages: [{ role: 'user', content: prompt }],
27
- tools: [],
28
- onEvent: () => {},
29
- });
30
- const { value, error } = extractJson(result.content ?? '');
31
- if (error || typeof value?.passed !== 'boolean') {
32
- return { passed: false, detail: `juez: respuesta inválida (${error ?? 'sin "passed"'})` };
33
- }
34
- return { passed: value.passed, detail: value.reason ?? 'juez' };
35
- } catch (err) {
36
- return { passed: false, detail: `juez error: ${err.message}` };
37
- }
38
- };
39
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Aggregate evaluation results into per-model and per-scorer summaries.
3
- * @param {object[]} results
4
- */
5
- export function summarize(results) {
6
- const byModel = new Map();
7
- const byScorer = new Map();
8
- let passed = 0;
9
- let errors = 0;
10
- let durationMs = 0;
11
-
12
- for (const result of results) {
13
- passed += result.passed ? 1 : 0;
14
- errors += result.error ? 1 : 0;
15
- durationMs += result.durationMs ?? 0;
16
-
17
- const model = byModel.get(result.model) ?? {
18
- model: result.model,
19
- total: 0,
20
- passed: 0,
21
- errors: 0,
22
- durationMs: 0,
23
- };
24
- model.total += 1;
25
- model.passed += result.passed ? 1 : 0;
26
- model.errors += result.error ? 1 : 0;
27
- model.durationMs += result.durationMs ?? 0;
28
- byModel.set(result.model, model);
29
-
30
- for (const detail of result.trace?.scorers ?? []) {
31
- const scorer = byScorer.get(detail.scorer) ?? { scorer: detail.scorer, total: 0, passed: 0 };
32
- scorer.total += 1;
33
- scorer.passed += detail.passed ? 1 : 0;
34
- byScorer.set(detail.scorer, scorer);
35
- }
36
- }
37
-
38
- const models = [...byModel.values()].map((m) => ({
39
- ...m,
40
- passRate: m.total ? m.passed / m.total : 0,
41
- }));
42
- const scorers = [...byScorer.values()].map((s) => ({
43
- ...s,
44
- passRate: s.total ? s.passed / s.total : 0,
45
- }));
46
-
47
- return { total: results.length, passed, errors, durationMs, models, scorers };
48
- }
@@ -1,62 +0,0 @@
1
- import { resolveProviderRuntime } from '../providers/index.js';
2
- import { runAgent } from '../agent/runAgent.js';
3
- import { buildMcpRegistry } from '../mcp/index.js';
4
- import { resolveApiKey } from '../config/secrets.js';
5
-
6
- export { resolveApiKey } from '../config/secrets.js';
7
-
8
- /**
9
- * Run a single evaluation task headlessly.
10
- *
11
- * @param {object} task
12
- * @param {string} [task.provider]
13
- * @param {string} [task.model]
14
- * @param {string} [task.input]
15
- * @param {string} [task.systemPrompt]
16
- * @param {object[]} [task.tools] - explicit OpenAI-schema tool definitions
17
- * @param {object[]} [task.mcpServers]
18
- * @param {number} [task.maxRounds]
19
- * @param {object} [opts]
20
- * @param {Record<string,string>} [opts.env]
21
- * @param {Function} [opts.onEvent]
22
- * @param {AbortSignal} [opts.signal]
23
- * @param {Function} [opts.execute] - tool executor
24
- */
25
- export async function runTask(task, { env = {}, onEvent, signal, execute } = {}) {
26
- const { provider, config } = resolveProviderRuntime({
27
- providerId: task.provider,
28
- apiKey: resolveApiKey(task.apiKey, env),
29
- baseUrl: task.baseUrl,
30
- customProviders: task.customProviders ?? [],
31
- });
32
-
33
- let tools = Array.isArray(task.tools) && task.tools[0]?.type === 'function' ? [...task.tools] : [];
34
- let mcp = null;
35
- if (task.mcpServers?.length) {
36
- mcp = await buildMcpRegistry(task.mcpServers, { signal });
37
- tools = [...tools, ...mcp.definitions];
38
- }
39
-
40
- const messages = [];
41
- if (task.systemPrompt) messages.push({ role: 'system', content: task.systemPrompt });
42
- messages.push({ role: 'user', content: task.input });
43
-
44
- try {
45
- const result = await runAgent({
46
- provider,
47
- config,
48
- model: task.model,
49
- messages,
50
- tools,
51
- execute,
52
- toolContext: { ...(task.toolContext ?? {}), mcp },
53
- maxRounds: task.maxRounds,
54
- onEvent,
55
- signal,
56
- });
57
-
58
- return { ...result, provider: provider.id, model: task.model };
59
- } finally {
60
- mcp?.close?.();
61
- }
62
- }
@@ -1,153 +0,0 @@
1
- import { createTaskWorkspace } from './task.js';
2
- import { runTask } from './runTask.js';
3
- import { runScorers } from './scorers.js';
4
- import { createTools } from '../tools/index.js';
5
-
6
- function safeParse(value) {
7
- if (value && typeof value === 'object') return value;
8
- if (typeof value !== 'string') return {};
9
- try {
10
- return JSON.parse(value);
11
- } catch {
12
- return {};
13
- }
14
- }
15
-
16
- export function extractToolCalls(messages = []) {
17
- const calls = [];
18
- for (const message of messages) {
19
- if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
20
- for (const call of message.tool_calls) {
21
- calls.push({
22
- name: call.function?.name ?? call.name,
23
- args: safeParse(call.function?.arguments ?? call.arguments),
24
- });
25
- }
26
- }
27
- }
28
- return calls;
29
- }
30
-
31
- async function pool(items, limit, worker) {
32
- const results = new Array(items.length);
33
- let index = 0;
34
- const size = Math.max(1, Math.min(limit || 1, items.length || 1));
35
- const runners = Array.from({ length: size }, async () => {
36
- while (true) {
37
- const i = index++;
38
- if (i >= items.length) break;
39
- results[i] = await worker(items[i], i);
40
- }
41
- });
42
- await Promise.all(runners);
43
- return results;
44
- }
45
-
46
- /**
47
- * Run an evaluation dataset: tasks × models × trials, each in an isolated
48
- * workspace, scored and (optionally) persisted.
49
- *
50
- * @param {object} opts
51
- * @param {object[]} opts.tasks
52
- * @param {string[]} opts.models
53
- * @param {string} opts.providerId
54
- * @param {string} [opts.baseUrl]
55
- * @param {string} [opts.apiKey]
56
- * @param {object[]} [opts.customProviders]
57
- * @param {string} [opts.systemPrompt]
58
- * @param {string} [opts.rootDir] - base dir for task fixtures
59
- * @param {number} [opts.trials]
60
- * @param {number} [opts.concurrency]
61
- * @param {string[]} [opts.enabledTools]
62
- * @param {object} [opts.permissions]
63
- * @param {boolean} [opts.yes]
64
- * @param {Function} [opts.judge]
65
- * @param {object} [opts.store]
66
- * @param {string} [opts.runId]
67
- * @param {Function} [opts.onProgress]
68
- * @param {AbortSignal} [opts.signal]
69
- */
70
- export async function runDataset({
71
- tasks,
72
- models,
73
- providerId,
74
- baseUrl,
75
- apiKey,
76
- customProviders = [],
77
- systemPrompt,
78
- rootDir = process.cwd(),
79
- trials = 1,
80
- concurrency = 1,
81
- enabledTools,
82
- permissions,
83
- yes = false,
84
- judge,
85
- store,
86
- runId,
87
- onProgress,
88
- signal,
89
- }) {
90
- const jobs = [];
91
- for (const task of tasks) {
92
- for (const model of models) {
93
- for (let trial = 0; trial < trials; trial++) jobs.push({ task, model, trial });
94
- }
95
- }
96
-
97
- return pool(jobs, concurrency, async ({ task, model, trial }) => {
98
- const workspace = await createTaskWorkspace(task, { rootDir });
99
- const tools = createTools({ root: workspace.dir, enabled: enabledTools, permissions, yes });
100
- const startedAt = Date.now();
101
- let result = null;
102
- let error = null;
103
-
104
- try {
105
- result = await runTask(
106
- {
107
- ...task,
108
- provider: providerId,
109
- model,
110
- baseUrl: task.baseUrl ?? baseUrl,
111
- apiKey: task.apiKey ?? apiKey,
112
- customProviders: task.customProviders ?? customProviders,
113
- systemPrompt: task.systemPrompt || systemPrompt,
114
- tools: tools.definitions.length ? tools.definitions : undefined,
115
- },
116
- { env: process.env, execute: tools.execute, signal }
117
- );
118
- } catch (err) {
119
- error = err.message;
120
- }
121
-
122
- const toolCalls = extractToolCalls(result?.messages ?? []);
123
- let scoring = { passed: false, score: 0, details: [] };
124
- if (!error) {
125
- scoring = await runScorers(task, {
126
- output: result.content,
127
- toolCalls,
128
- workspaceDir: workspace.dir,
129
- input: task.input,
130
- judge,
131
- });
132
- }
133
- await workspace.cleanup();
134
-
135
- const entry = {
136
- runId,
137
- taskId: task.id ?? String(task.input ?? '').slice(0, 40),
138
- model,
139
- trial,
140
- output: result?.content ?? null,
141
- passed: Boolean(scoring.passed),
142
- score: scoring.score,
143
- durationMs: Date.now() - startedAt,
144
- error,
145
- toolCalls,
146
- trace: { rounds: result?.rounds, stopReason: result?.stopReason, scorers: scoring.details, error },
147
- };
148
-
149
- if (store) store.addRunResult(entry);
150
- onProgress?.(entry);
151
- return entry;
152
- });
153
- }