@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.
- package/CHANGELOG.md +12 -0
- package/README.md +13 -25
- package/SECURITY.md +3 -4
- package/harness/packages/cli/src/cli.js +0 -3
- package/harness/packages/cli/src/commands/bridge.js +0 -1
- package/harness/packages/cli/src/commands/repl.js +3 -3
- package/harness/packages/cli/src/commands/run.js +1 -1
- package/harness/packages/cli/src/commands/server.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +1 -1
- package/harness/packages/cli/src/lib/util.js +2 -2
- package/harness/packages/cli/src/tui/app.js +2 -2
- package/harness/packages/cli/test/tunnel.test.js +5 -32
- package/harness/packages/core/src/agents/index.js +13 -41
- package/harness/packages/core/src/config/app.js +0 -4
- package/harness/packages/core/src/config/home.js +1 -20
- package/harness/packages/core/src/config/index.js +5 -8
- package/harness/packages/core/src/extensions/load.js +22 -0
- package/harness/packages/core/src/frontmatter.js +32 -0
- package/harness/packages/core/src/hooks/index.js +2 -16
- package/harness/packages/core/src/index.js +2 -14
- package/harness/packages/core/src/plugins/index.js +2 -16
- package/harness/packages/core/src/procs/project.js +68 -0
- package/harness/packages/core/src/procs/static.js +17 -0
- package/harness/packages/core/src/rules/index.js +8 -27
- package/harness/packages/core/src/skills/index.js +8 -19
- package/harness/packages/core/src/storage/sqlite.js +595 -690
- package/harness/packages/core/src/tunnel.js +9 -147
- package/harness/packages/core/test/home.test.js +2 -35
- package/harness/packages/core/test/procs.test.js +3 -64
- package/harness/packages/core/test/storage.test.js +1 -12
- package/harness/packages/server/public/app.js +2 -19
- package/harness/packages/server/public/appearance.js +2 -56
- package/harness/packages/server/public/boot.js +2 -2
- package/harness/packages/server/public/cards.js +2 -21
- package/harness/packages/server/public/chat.js +0 -16
- package/harness/packages/server/public/chatFormat.js +0 -11
- package/harness/packages/server/public/chrome.js +2 -5
- package/harness/packages/server/public/commands.js +1 -2
- package/harness/packages/server/public/explorer.js +7 -10
- package/harness/packages/server/public/index.html +1228 -1326
- package/harness/packages/server/public/landing/boot.js +2 -3
- package/harness/packages/server/public/landing/index.html +3 -367
- package/harness/packages/server/public/settings.js +1255 -1322
- package/harness/packages/server/public/settingsNav.js +0 -1
- package/harness/packages/server/public/styles.css +2341 -2867
- package/harness/packages/server/public/system.js +356 -495
- package/harness/packages/server/public/voice.js +6 -22
- package/harness/packages/server/src/auth.js +4 -15
- package/harness/packages/server/src/cli.js +1 -1
- package/harness/packages/server/src/lib/agents.js +13 -27
- package/harness/packages/server/src/lib/appearance.js +3 -22
- package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
- package/harness/packages/server/src/lib/definitions.js +40 -0
- package/harness/packages/server/src/lib/skills.js +14 -21
- package/harness/packages/server/src/lib/ttsSettings.js +2 -2
- package/harness/packages/server/src/lib/workspace.js +9 -11
- package/harness/packages/server/src/routes/chat.js +0 -43
- package/harness/packages/server/src/routes/preview.js +5 -22
- package/harness/packages/server/src/routes/procs.js +2 -4
- package/harness/packages/server/src/routes/tts.js +1 -1
- package/harness/packages/server/src/server.js +1 -5
- package/harness/packages/server/test/appearance-route.test.js +3 -3
- package/harness/packages/server/test/appearance.test.js +12 -38
- package/harness/packages/server/test/chatFormat.test.js +0 -8
- package/harness/packages/server/test/consoleTabs.test.js +4 -4
- package/harness/packages/server/test/ops.test.js +8 -56
- package/harness/packages/server/test/preview.test.js +2 -42
- package/harness/packages/server/test/security.test.js +194 -370
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/packages/server/test/tts.test.js +4 -4
- package/harness/tamnora.example.json +1 -1
- package/package.json +2 -2
- package/harness/packages/cli/src/commands/eval.js +0 -186
- package/harness/packages/core/src/eval/dataset.js +0 -47
- package/harness/packages/core/src/eval/judge.js +0 -39
- package/harness/packages/core/src/eval/report.js +0 -48
- package/harness/packages/core/src/eval/runTask.js +0 -62
- package/harness/packages/core/src/eval/runner.js +0 -153
- package/harness/packages/core/src/eval/scorers.js +0 -217
- package/harness/packages/core/src/eval/task.js +0 -59
- package/harness/packages/core/src/procs/detect.js +0 -149
- package/harness/packages/core/test/eval.test.js +0 -144
- package/harness/packages/server/src/lib/advisor.js +0 -198
- package/harness/packages/server/src/routes/advisor.js +0 -79
- package/harness/packages/server/src/routes/eval.js +0 -100
- package/harness/packages/server/test/advisor.test.js +0 -249
- 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
|
-
}
|