openmerit 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +239 -58
- package/data/catalogue.json +6778 -0
- package/dist/catalogue/catalogue.d.ts +41 -0
- package/dist/catalogue/catalogue.js +33 -0
- package/dist/cli/prompt.d.ts +26 -0
- package/dist/cli/prompt.js +114 -0
- package/dist/cli/render.d.ts +12 -0
- package/dist/cli/render.js +104 -0
- package/dist/cli.js +245 -158
- package/dist/connect.d.ts +34 -0
- package/dist/connect.js +158 -0
- package/dist/connectors/langfuse/index.d.ts +37 -110
- package/dist/connectors/langfuse/index.js +95 -246
- package/dist/daemon/daemon.d.ts +81 -0
- package/dist/daemon/daemon.js +408 -0
- package/dist/daemon/registry.d.ts +111 -0
- package/dist/daemon/registry.js +109 -0
- package/dist/eval/proxy.d.ts +54 -0
- package/dist/eval/proxy.js +174 -0
- package/dist/eval/runner.d.ts +29 -0
- package/dist/eval/runner.js +80 -0
- package/dist/harness/claude-bridge.d.ts +21 -0
- package/dist/harness/claude-bridge.js +65 -0
- package/dist/harness/harness.d.ts +60 -0
- package/dist/harness/harness.js +221 -0
- package/dist/index.d.ts +9 -10
- package/dist/index.js +9 -10
- package/dist/local.d.ts +12 -0
- package/dist/local.js +40 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +155 -0
- package/dist/scout/pool.d.ts +17 -0
- package/dist/scout/pool.js +56 -0
- package/dist/scout/scan.d.ts +42 -0
- package/dist/scout/scan.js +169 -0
- package/dist/scout/search.d.ts +25 -98
- package/dist/scout/search.js +56 -156
- package/dist/scout/workflow.d.ts +221 -0
- package/dist/scout/workflow.js +941 -0
- package/dist/setup.d.ts +44 -0
- package/dist/setup.js +211 -0
- package/dist/state/config.d.ts +116 -0
- package/dist/state/config.js +60 -0
- package/dist/state/db.d.ts +159 -0
- package/dist/state/db.js +260 -0
- package/dist/state/paths.d.ts +12 -0
- package/dist/state/paths.js +23 -0
- package/dist/update.d.ts +22 -0
- package/dist/update.js +58 -0
- package/package.json +12 -17
- package/dist/connectors/langfuse/otlp.d.ts +0 -60
- package/dist/connectors/langfuse/otlp.js +0 -40
- package/dist/pi/extension.d.ts +0 -13
- package/dist/pi/extension.js +0 -148
- package/dist/pi/harness.d.ts +0 -20
- package/dist/pi/harness.js +0 -49
- package/dist/pi/instructions.d.ts +0 -2
- package/dist/pi/instructions.js +0 -12
- package/dist/scout/evaluators/module.d.ts +0 -24
- package/dist/scout/evaluators/module.js +0 -19
- package/dist/scout/evaluators/pi.d.ts +0 -53
- package/dist/scout/evaluators/pi.js +0 -117
- package/dist/scout/pipeline.d.ts +0 -111
- package/dist/scout/pipeline.js +0 -157
- package/dist/scout/report.d.ts +0 -3
- package/dist/scout/report.js +0 -42
- package/dist/scout/service.d.ts +0 -148
- package/dist/scout/service.js +0 -143
- package/examples/health-declaration-codes/dataset.jsonl +0 -14
- package/examples/sahra-enrollment/evaluator.mts +0 -152
package/dist/pi/extension.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import { Type } from "typebox";
|
|
3
|
-
import { candidates, connections, plan, previewDataset, run, saveDataset } from "../scout/service.js";
|
|
4
|
-
import { SCOUT_INSTRUCTIONS } from "./instructions.js";
|
|
5
|
-
const json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }], details: undefined });
|
|
6
|
-
const filterSchema = Type.Object({
|
|
7
|
-
providers: Type.Optional(Type.Array(Type.String(), { description: "Connected Pi providers to draw candidates from, e.g. [\"openrouter\"] or [\"anthropic\"]" })),
|
|
8
|
-
tools: Type.Optional(Type.Boolean({ description: "The production call uses tool calls" })),
|
|
9
|
-
maxPrice: Type.Optional(Type.Number({ description: "Max list price, USD per million input+output tokens" })),
|
|
10
|
-
include: Type.Optional(Type.Array(Type.String(), { description: "Keep only models whose provider/id contains one of these" })),
|
|
11
|
-
exclude: Type.Optional(Type.Array(Type.String(), { description: "Drop models whose provider/id contains one of these" })),
|
|
12
|
-
});
|
|
13
|
-
const runSchema = Type.Object({
|
|
14
|
-
task: Type.String({ description: "One paragraph: what the model does in production, its inputs and outputs, and what counts as correct" }),
|
|
15
|
-
langfuseDataset: Type.Optional(Type.String({ description: "Existing Langfuse dataset of labelled items" })),
|
|
16
|
-
datasetPath: Type.Optional(Type.String({ description: "Local JSONL of {id?, input, expectedOutput}, pushed to Langfuse before the run" })),
|
|
17
|
-
evaluator: Type.Object({
|
|
18
|
-
type: StringEnum(["pi", "module"], { description: "pi: one model call per item through Pi; module: the project's own harness evaluator" }),
|
|
19
|
-
systemPrompt: Type.Optional(Type.String({ description: "pi: the production system prompt the candidate receives" })),
|
|
20
|
-
inputTemplate: Type.Optional(Type.String({ description: "pi: user message template; {{input}} is the item input" })),
|
|
21
|
-
grader: Type.Optional(Type.Object({
|
|
22
|
-
type: StringEnum(["exact", "contains", "json", "judge"], { description: "json: every expected field must match, strings case-insensitively, arrays as sets (order ignored, same length); exact/contains: normalized text; judge: Scout's model applies the rubric" }),
|
|
23
|
-
field: Type.Optional(Type.String()),
|
|
24
|
-
rubric: Type.Optional(Type.String({ description: "judge: what a correct output must do" })),
|
|
25
|
-
})),
|
|
26
|
-
path: Type.Optional(Type.String({ description: "module: path to an evaluator module, relative to the project" })),
|
|
27
|
-
}),
|
|
28
|
-
filter: Type.Optional(filterSchema),
|
|
29
|
-
sampleSize: Type.Optional(Type.Integer({ minimum: 2, maximum: 20 })),
|
|
30
|
-
finalists: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
|
|
31
|
-
repeats: Type.Optional(Type.Integer({ minimum: 1, maximum: 10, description: "Runs of every item per finalist (default 3)" })),
|
|
32
|
-
});
|
|
33
|
-
function host(ctx, options) {
|
|
34
|
-
const registry = ctx.modelRegistry;
|
|
35
|
-
const caller = { getModel: (provider, id) => registry.find(provider, id), complete: (model, context, extra) => registry.complete(model, context, extra) };
|
|
36
|
-
const scoutModel = options.scoutModel ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
37
|
-
return {
|
|
38
|
-
caller, cwd: ctx.cwd, env: process.env, log: options.log ?? (() => { }),
|
|
39
|
-
models: async () => registry.getAvailable().map(model => ({ provider: model.provider, id: model.id, name: model.name, cost: model.cost })),
|
|
40
|
-
...(scoutModel ? { scoutModel } : {}),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
function toRunParams(params) {
|
|
44
|
-
const { evaluator } = params;
|
|
45
|
-
let spec;
|
|
46
|
-
if (evaluator.type === "module") {
|
|
47
|
-
if (!evaluator.path)
|
|
48
|
-
throw new Error("evaluator.path is required for a module evaluator");
|
|
49
|
-
spec = { type: "module", path: evaluator.path };
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
if (!evaluator.systemPrompt || !evaluator.grader)
|
|
53
|
-
throw new Error("A pi evaluator needs systemPrompt and grader");
|
|
54
|
-
const { type, field, rubric } = evaluator.grader;
|
|
55
|
-
const grader = type === "judge" ? { type: "judge", rubric: rubric ?? "The output must agree with the expected output." }
|
|
56
|
-
: type === "exact" ? { type: "exact", ...(field ? { field } : {}) }
|
|
57
|
-
: type === "contains" ? { type: "contains", ...(field ? { field } : {}) } : { type: "json" };
|
|
58
|
-
spec = { type: "pi", systemPrompt: evaluator.systemPrompt, grader, ...(evaluator.inputTemplate ? { inputTemplate: evaluator.inputTemplate } : {}) };
|
|
59
|
-
}
|
|
60
|
-
if (!params.langfuseDataset && !params.datasetPath)
|
|
61
|
-
throw new Error("Give langfuseDataset or datasetPath");
|
|
62
|
-
return {
|
|
63
|
-
task: params.task, evaluator: spec,
|
|
64
|
-
...(params.langfuseDataset ? { langfuseDataset: params.langfuseDataset } : {}), ...(params.datasetPath ? { datasetPath: params.datasetPath } : {}),
|
|
65
|
-
...(params.filter ? { filter: Object.fromEntries(Object.entries(params.filter).filter(([, value]) => value !== undefined)) } : {}),
|
|
66
|
-
...(params.sampleSize ? { sampleSize: params.sampleSize } : {}), ...(params.finalists ? { finalists: params.finalists } : {}), ...(params.repeats ? { repeats: params.repeats } : {}),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
/** OpenMerit Scout inside Pi: model-recommendation tools, a `/scout` command and Scout's instructions. */
|
|
70
|
-
export function createScoutExtension(options = {}) {
|
|
71
|
-
const allowSpend = options.allowSpend ?? "ask";
|
|
72
|
-
return (pi) => {
|
|
73
|
-
pi.registerTool({
|
|
74
|
-
name: "scout_connections",
|
|
75
|
-
label: "Scout connections",
|
|
76
|
-
description: "Lists the model providers OpenMerit can run (API keys and subscriptions connected in Pi) and the Langfuse project and datasets.",
|
|
77
|
-
promptSnippet: "See which model providers and Langfuse datasets Scout can use",
|
|
78
|
-
parameters: Type.Object({}),
|
|
79
|
-
async execute(_id, _params, _signal, _update, ctx) { return json(await connections(host(ctx, options))); },
|
|
80
|
-
});
|
|
81
|
-
pi.registerTool({
|
|
82
|
-
name: "scout_candidates",
|
|
83
|
-
label: "Scout candidates",
|
|
84
|
-
description: "Ranks the runnable models by the Epoch Capabilities Index (ECI), weakest first, after the filter: Epoch's published score, or ScoutBM's estimated/calibrated ECI bundled with the package. Shows how many candidates the binary search will cover.",
|
|
85
|
-
promptSnippet: "Rank runnable candidate models by public capability (ECI)",
|
|
86
|
-
parameters: filterSchema,
|
|
87
|
-
async execute(_id, params, _signal, _update, ctx) {
|
|
88
|
-
const filter = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined));
|
|
89
|
-
const ranked = await candidates(host(ctx, options), filter);
|
|
90
|
-
const short = (list) => list.map(item => `${item.model} ECI ${item.eci.toFixed(1)}${item.eciSource === "published" ? "" : ` (${item.eciSource} ±${item.eciError?.toFixed(1)})`} $${item.price?.toFixed(2) ?? "?"}/M`);
|
|
91
|
-
return json({ count: ranked.candidates.length, counts: ranked.counts, weakest: short(ranked.candidates.slice(0, 8)), strongest: short(ranked.candidates.slice(-8)), unranked: ranked.unranked.length, dropped: ranked.dropped.length, eci: ranked.eci });
|
|
92
|
-
},
|
|
93
|
-
});
|
|
94
|
-
pi.registerTool({
|
|
95
|
-
name: "scout_dataset",
|
|
96
|
-
label: "Scout dataset",
|
|
97
|
-
description: "Previews a labelled eval dataset: a Langfuse dataset by name or a local JSONL of {id?, input, expectedOutput}. Scout needs at least 5 labelled items.",
|
|
98
|
-
promptSnippet: "Preview a labelled eval dataset (Langfuse or local JSONL)",
|
|
99
|
-
parameters: Type.Object({ langfuseDataset: Type.Optional(Type.String()), path: Type.Optional(Type.String()) }),
|
|
100
|
-
async execute(_id, params, _signal, _update, ctx) {
|
|
101
|
-
return json(await previewDataset(host(ctx, options), { ...(params.langfuseDataset ? { langfuseDataset: params.langfuseDataset } : {}), ...(params.path ? { path: params.path } : {}) }));
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
pi.registerTool({
|
|
105
|
-
name: "scout_save_dataset",
|
|
106
|
-
label: "Scout save dataset",
|
|
107
|
-
description: "Saves labelled items Scout drafted from the codebase (fixtures, tests, examples) to .openmerit/datasets/<name>.jsonl. Each item is marked as a Scout draft; label only what the code or fixtures state.",
|
|
108
|
-
promptSnippet: "Save drafted labelled items as a local eval dataset",
|
|
109
|
-
parameters: Type.Object({
|
|
110
|
-
name: Type.String(),
|
|
111
|
-
items: Type.Array(Type.Object({ id: Type.Optional(Type.String()), input: Type.Unknown(), expectedOutput: Type.Unknown() }), { minItems: 1 }),
|
|
112
|
-
}),
|
|
113
|
-
async execute(_id, params, _signal, _update, ctx) {
|
|
114
|
-
return json(await saveDataset(host(ctx, options), params.name, params.items.map(item => ({ ...(item.id ? { id: item.id } : {}), input: item.input, expectedOutput: item.expectedOutput }))));
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
pi.registerTool({
|
|
118
|
-
name: "scout_run",
|
|
119
|
-
label: "Scout run",
|
|
120
|
-
description: "Runs Scout Model Binary Search: ECI-ranked candidates, 5 sampled items per probe (pass = 5/5), 5 finalists on every item 3 times, each recorded as a Langfuse experiment; the winner is read back from Langfuse. Spends provider credit; without approval it returns the plan only.",
|
|
121
|
-
promptSnippet: "Binary-search the candidates on labelled items and pick the winner from Langfuse",
|
|
122
|
-
executionMode: "sequential",
|
|
123
|
-
parameters: runSchema,
|
|
124
|
-
async execute(_id, params, signal, onUpdate, ctx) {
|
|
125
|
-
const scoutHost = host(ctx, options);
|
|
126
|
-
const runParams = toRunParams(params);
|
|
127
|
-
const planned = await plan(scoutHost, runParams);
|
|
128
|
-
const summary = { candidates: planned.ranked.candidates.length, datasetSize: planned.datasetSize, estimatedEvaluations: planned.estimate.evaluations, filter: planned.filter, evaluator: planned.plugin?.name ?? "pi" };
|
|
129
|
-
let approved = allowSpend === true;
|
|
130
|
-
if (allowSpend === "ask" && ctx.hasUI)
|
|
131
|
-
approved = await ctx.ui.confirm("Run Scout?", `About ${summary.estimatedEvaluations} evaluations over ${summary.candidates} candidates on ${summary.datasetSize} items. This spends provider credit.`);
|
|
132
|
-
if (!approved)
|
|
133
|
-
return json({ status: "plan_only", ...summary, note: "Not run: spending was not approved. Rerun headless with --execute, or approve in the dialog." });
|
|
134
|
-
const progress = (line) => { scoutHost.log(line); onUpdate?.({ content: [{ type: "text", text: line }], details: undefined }); };
|
|
135
|
-
const result = await run({ ...scoutHost, log: progress }, runParams, signal);
|
|
136
|
-
return { content: [{ type: "text", text: `${result.markdown}\nReport files: ${result.files.markdown}, ${result.files.json}` }], details: undefined };
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
pi.registerCommand("scout", {
|
|
140
|
-
description: "Ask OpenMerit Scout which model to use for a task",
|
|
141
|
-
handler: async (args) => { pi.sendUserMessage(`Use OpenMerit Scout to recommend a model for this task: ${args || "the model calls in this codebase"}`); },
|
|
142
|
-
});
|
|
143
|
-
pi.on("before_agent_start", (event) => {
|
|
144
|
-
event.systemPromptOptions.sections.openmerit_scout = SCOUT_INSTRUCTIONS;
|
|
145
|
-
});
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
export default createScoutExtension();
|
package/dist/pi/harness.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { RunnableModel } from "../scout/candidates.js";
|
|
3
|
-
import type { ModelCaller } from "../scout/evaluators/pi.js";
|
|
4
|
-
/**
|
|
5
|
-
* OpenMerit's changes to a stock Pi process, applied before Pi reads its environment:
|
|
6
|
-
* - its own agent directory (~/.openmerit/agent, or $OPENMERIT_HOME/agent), so OpenMerit's
|
|
7
|
-
* logins and settings never touch a separate Pi installation; PI_CODING_AGENT_DIR still wins;
|
|
8
|
-
* - a Claude Code subscription token (CLAUDE_CODE_OAUTH_TOKEN) works as the Anthropic
|
|
9
|
-
* subscription credential when no Anthropic key or token is set;
|
|
10
|
-
* - Pi's own update check is off, because this package pins Pi.
|
|
11
|
-
*/
|
|
12
|
-
export declare function configureHarness(env?: Record<string, string | undefined>): {
|
|
13
|
-
agentDir: string;
|
|
14
|
-
};
|
|
15
|
-
export declare function openRuntime(): Promise<{
|
|
16
|
-
runtime: ModelRuntime;
|
|
17
|
-
caller: ModelCaller;
|
|
18
|
-
models: () => Promise<RunnableModel[]>;
|
|
19
|
-
}>;
|
|
20
|
-
export declare function pickScoutModel(runtime: ModelRuntime, requested?: string): Promise<import("@earendil-works/pi-ai").Model<import("@earendil-works/pi-ai").Api>>;
|
package/dist/pi/harness.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
/**
|
|
5
|
-
* OpenMerit's changes to a stock Pi process, applied before Pi reads its environment:
|
|
6
|
-
* - its own agent directory (~/.openmerit/agent, or $OPENMERIT_HOME/agent), so OpenMerit's
|
|
7
|
-
* logins and settings never touch a separate Pi installation; PI_CODING_AGENT_DIR still wins;
|
|
8
|
-
* - a Claude Code subscription token (CLAUDE_CODE_OAUTH_TOKEN) works as the Anthropic
|
|
9
|
-
* subscription credential when no Anthropic key or token is set;
|
|
10
|
-
* - Pi's own update check is off, because this package pins Pi.
|
|
11
|
-
*/
|
|
12
|
-
export function configureHarness(env = process.env) {
|
|
13
|
-
env.PI_CODING_AGENT_DIR ??= join(env.OPENMERIT_HOME ?? join(homedir(), ".openmerit"), "agent");
|
|
14
|
-
if (env.CLAUDE_CODE_OAUTH_TOKEN && !env.ANTHROPIC_API_KEY && !env.ANTHROPIC_OAUTH_TOKEN && !env.ANTHROPIC_AUTH_TOKEN) {
|
|
15
|
-
env.ANTHROPIC_OAUTH_TOKEN = env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
16
|
-
}
|
|
17
|
-
env.PI_SKIP_VERSION_CHECK ??= "1";
|
|
18
|
-
return { agentDir: env.PI_CODING_AGENT_DIR };
|
|
19
|
-
}
|
|
20
|
-
/** Scout runs on the strongest connected general model unless the user names one. */
|
|
21
|
-
const SCOUT_MODEL_PREFERENCE = [
|
|
22
|
-
"anthropic/claude-opus-5-5", "anthropic/claude-sonnet-5-5", "openai/gpt-6.1-sol", "openai/gpt-6-sol",
|
|
23
|
-
"openrouter/anthropic/claude-opus-5.5", "openrouter/anthropic/claude-sonnet-5.5", "openrouter/openai/gpt-6.1-sol",
|
|
24
|
-
];
|
|
25
|
-
export async function openRuntime() {
|
|
26
|
-
const runtime = await ModelRuntime.create({ allowModelNetwork: true, modelRefreshTimeoutMs: 20_000 });
|
|
27
|
-
const caller = { getModel: (provider, id) => runtime.getModel(provider, id), complete: (model, context, extra) => runtime.complete(model, context, extra) };
|
|
28
|
-
const models = async () => (await runtime.getAvailable()).map(model => ({ provider: model.provider, id: model.id, name: model.name, cost: model.cost }));
|
|
29
|
-
return { runtime, caller, models };
|
|
30
|
-
}
|
|
31
|
-
export async function pickScoutModel(runtime, requested) {
|
|
32
|
-
const available = await runtime.getAvailable();
|
|
33
|
-
const ref = (model) => `${model.provider}/${model.id}`;
|
|
34
|
-
if (requested) {
|
|
35
|
-
const model = available.find(item => ref(item) === requested);
|
|
36
|
-
if (!model)
|
|
37
|
-
throw new Error(`Scout model ${requested} is not available with the connected providers. Run \`openmerit status\`.`);
|
|
38
|
-
return model;
|
|
39
|
-
}
|
|
40
|
-
for (const wanted of SCOUT_MODEL_PREFERENCE) {
|
|
41
|
-
const model = available.find(item => ref(item) === wanted);
|
|
42
|
-
if (model)
|
|
43
|
-
return model;
|
|
44
|
-
}
|
|
45
|
-
const model = available[0];
|
|
46
|
-
if (!model)
|
|
47
|
-
throw new Error("No model provider is connected. Set an API key (for example OPENROUTER_API_KEY or ANTHROPIC_API_KEY) or run `openmerit login` for a subscription.");
|
|
48
|
-
return model;
|
|
49
|
-
}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
/** Scout's workflow, added to Pi's system prompt as the `openmerit_scout` section. */
|
|
2
|
-
export declare const SCOUT_INSTRUCTIONS = "You are OpenMerit Scout. You recommend the model a user should run for a task, from measured results on the user's own labelled examples, not from memory.\n\nWorkflow when asked which model to use:\n1. Understand the task. If a codebase is present, find where the model is called (grep for model IDs, SDK calls, \"systemPrompt\", \"model:\"), the production prompt, whether it calls tools, its output format, and existing evals: fixtures, tests with expected outputs, Langfuse datasets (scout_connections lists them), or .openmerit/datasets/*.jsonl.\n2. Find labelled data. Prefer an existing Langfuse dataset or fixture set with expected outputs. Preview it with scout_dataset; Scout needs at least 5 items. If none exists, draft at least 6 items only from what the code, fixtures or the user state, save them with scout_save_dataset, and say they are drafts the user should check. Never invent labels you cannot justify.\n3. Choose the evaluator. If the project ships an evaluator module that runs its real harness (for example .openmerit/evaluator.mts, .openmerit/evaluator.ts or examples/*/evaluator.mts), use evaluator type \"module\". Otherwise use type \"pi\" with the production system prompt and the strictest grader that fits: \"json\" for structured outputs, \"exact\"/\"contains\" for short answers, \"judge\" with a precise rubric only for free text.\n4. Check candidates with scout_candidates. Filter only for real constraints: providers the user can run, tools when the call uses tools, a price ceiling the user gave, data-residency or vendor exclusions the user named. Do not narrow the pool from your own preferences.\n5. Run scout_run with the task, dataset and evaluator. It binary-searches the ECI-ranked candidates (pass = every sampled item), screens neighbours, runs 5 finalists (plus cheaper, stronger challengers) on every item 3 times, records each as a Langfuse experiment, and picks the winner from Langfuse: most passes, then lower measured cost per item, then lower ECI.\n6. Answer with the recommended model ID, its passes and cost per item, the runner-up, the Langfuse experiment links, the report path, and the limitations the report lists (sample size, untested skipped models, drafted labels, residency or policy gaps). If scout_run returns plan_only, report the plan and the command to run it.\n\nRules: measured results on the user's data outrank public benchmarks. Provider, auth or billing errors are not quality failures; report them. Never print API keys or tokens.";
|
package/dist/pi/instructions.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/** Scout's workflow, added to Pi's system prompt as the `openmerit_scout` section. */
|
|
2
|
-
export const SCOUT_INSTRUCTIONS = `You are OpenMerit Scout. You recommend the model a user should run for a task, from measured results on the user's own labelled examples, not from memory.
|
|
3
|
-
|
|
4
|
-
Workflow when asked which model to use:
|
|
5
|
-
1. Understand the task. If a codebase is present, find where the model is called (grep for model IDs, SDK calls, "systemPrompt", "model:"), the production prompt, whether it calls tools, its output format, and existing evals: fixtures, tests with expected outputs, Langfuse datasets (scout_connections lists them), or .openmerit/datasets/*.jsonl.
|
|
6
|
-
2. Find labelled data. Prefer an existing Langfuse dataset or fixture set with expected outputs. Preview it with scout_dataset; Scout needs at least 5 items. If none exists, draft at least 6 items only from what the code, fixtures or the user state, save them with scout_save_dataset, and say they are drafts the user should check. Never invent labels you cannot justify.
|
|
7
|
-
3. Choose the evaluator. If the project ships an evaluator module that runs its real harness (for example .openmerit/evaluator.mts, .openmerit/evaluator.ts or examples/*/evaluator.mts), use evaluator type "module". Otherwise use type "pi" with the production system prompt and the strictest grader that fits: "json" for structured outputs, "exact"/"contains" for short answers, "judge" with a precise rubric only for free text.
|
|
8
|
-
4. Check candidates with scout_candidates. Filter only for real constraints: providers the user can run, tools when the call uses tools, a price ceiling the user gave, data-residency or vendor exclusions the user named. Do not narrow the pool from your own preferences.
|
|
9
|
-
5. Run scout_run with the task, dataset and evaluator. It binary-searches the ECI-ranked candidates (pass = every sampled item), screens neighbours, runs 5 finalists (plus cheaper, stronger challengers) on every item 3 times, records each as a Langfuse experiment, and picks the winner from Langfuse: most passes, then lower measured cost per item, then lower ECI.
|
|
10
|
-
6. Answer with the recommended model ID, its passes and cost per item, the runner-up, the Langfuse experiment links, the report path, and the limitations the report lists (sample size, untested skipped models, drafted labels, residency or policy gaps). If scout_run returns plan_only, report the plan and the command to run it.
|
|
11
|
-
|
|
12
|
-
Rules: measured results on the user's data outrank public benchmarks. Provider, auth or billing errors are not quality failures; report them. Never print API keys or tokens.`;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { Evaluate } from "../search.js";
|
|
2
|
-
/**
|
|
3
|
-
* An evaluator that runs a candidate through the user's own harness, such as a deployed agent's
|
|
4
|
-
* HTTP API, and checks the result against the labelled item. A module's default export is one of these.
|
|
5
|
-
*/
|
|
6
|
-
export interface EvaluatorPlugin {
|
|
7
|
-
name: string;
|
|
8
|
-
/** Pi providers the harness can run, such as ["openrouter"]. Candidates are drawn only from these. */
|
|
9
|
-
providers?: string[];
|
|
10
|
-
/** The harness calls tools, so models without tool calls are not candidates. */
|
|
11
|
-
tools?: boolean;
|
|
12
|
-
/** Score name recorded per item in Langfuse, ideally the customer's own metric (e.g. `enrollment-correct`). */
|
|
13
|
-
passScore?: string;
|
|
14
|
-
/** Experiment metadata key `model_<role>`. */
|
|
15
|
-
role?: string;
|
|
16
|
-
/** Items evaluated at once within one probe. */
|
|
17
|
-
concurrency?: number;
|
|
18
|
-
create(context: {
|
|
19
|
-
env: Record<string, string | undefined>;
|
|
20
|
-
log: (line: string) => void;
|
|
21
|
-
}): Evaluate | Promise<Evaluate>;
|
|
22
|
-
}
|
|
23
|
-
/** Imports an evaluator module (`.mts`, `.ts`, `.mjs` or `.js`) from the user's project. Node.js 22.19 strips its types. */
|
|
24
|
-
export declare function loadEvaluatorPlugin(path: string, cwd?: string): Promise<EvaluatorPlugin>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { resolve } from "node:path";
|
|
10
|
-
import { pathToFileURL } from "node:url";
|
|
11
|
-
/** Imports an evaluator module (`.mts`, `.ts`, `.mjs` or `.js`) from the user's project. Node.js 22.19 strips its types. */
|
|
12
|
-
export async function loadEvaluatorPlugin(path, cwd = process.cwd()) {
|
|
13
|
-
const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(cwd, path)).href));
|
|
14
|
-
const plugin = loaded.default;
|
|
15
|
-
if (!plugin || typeof plugin !== "object" || typeof plugin.create !== "function" || typeof plugin.name !== "string") {
|
|
16
|
-
throw new Error(`${path} must default-export an evaluator: { name, create(context) => evaluate }`);
|
|
17
|
-
}
|
|
18
|
-
return plugin;
|
|
19
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { Api, AssistantMessage, Context, Model } from "@earendil-works/pi-ai";
|
|
2
|
-
import { type Evaluate } from "../search.js";
|
|
3
|
-
/** What the Pi evaluator needs from Pi: a `ModelRuntime`, or an extension's `ctx.modelRegistry` through `fromRegistry`. */
|
|
4
|
-
export interface ModelCaller {
|
|
5
|
-
getModel(provider: string, id: string): Model<Api> | undefined;
|
|
6
|
-
complete(model: Model<Api>, context: Context, options?: {
|
|
7
|
-
signal?: AbortSignal;
|
|
8
|
-
maxTokens?: number;
|
|
9
|
-
apiKey?: string;
|
|
10
|
-
}): Promise<AssistantMessage>;
|
|
11
|
-
}
|
|
12
|
-
export type Grader =
|
|
13
|
-
/** Normalized text equality with `expectedOutput` (or `expectedOutput[field]`). */
|
|
14
|
-
{
|
|
15
|
-
type: "exact";
|
|
16
|
-
field?: string;
|
|
17
|
-
}
|
|
18
|
-
/** The output contains the expected text, case-insensitively. */
|
|
19
|
-
| {
|
|
20
|
-
type: "contains";
|
|
21
|
-
field?: string;
|
|
22
|
-
}
|
|
23
|
-
/** The output's JSON contains every expected field with an equal value (strings compared case-insensitively, arrays as sets). */
|
|
24
|
-
| {
|
|
25
|
-
type: "json";
|
|
26
|
-
}
|
|
27
|
-
/** A judge model answers PASS or FAIL for the output against the expected output and rubric. */
|
|
28
|
-
| {
|
|
29
|
-
type: "judge";
|
|
30
|
-
rubric: string;
|
|
31
|
-
model?: string;
|
|
32
|
-
};
|
|
33
|
-
export interface PiTaskSpec {
|
|
34
|
-
/** The candidate's system prompt: the instructions the production model receives. */
|
|
35
|
-
systemPrompt: string;
|
|
36
|
-
/** Optional template for the user message; `{{input}}` is the item's input as text or JSON. */
|
|
37
|
-
inputTemplate?: string;
|
|
38
|
-
grader: Grader;
|
|
39
|
-
maxTokens?: number;
|
|
40
|
-
}
|
|
41
|
-
/** The first JSON object or array in a reply, with code fences removed. */
|
|
42
|
-
export declare function extractJson(reply: string): unknown;
|
|
43
|
-
/** Every expected field matches: objects by key, arrays as sets, strings case-insensitively. */
|
|
44
|
-
export declare function jsonMatches(expected: unknown, actual: unknown): boolean;
|
|
45
|
-
/**
|
|
46
|
-
* Runs each item as one model call through Pi, so any provider Pi connects (API key or
|
|
47
|
-
* subscription) can be a candidate, then grades the reply. Tool-using agents need an
|
|
48
|
-
* evaluator module that runs the real harness instead.
|
|
49
|
-
*/
|
|
50
|
-
export declare function piEvaluator(caller: ModelCaller, spec: PiTaskSpec, options?: {
|
|
51
|
-
judgeModel?: string;
|
|
52
|
-
retryDelaysMs?: number[];
|
|
53
|
-
}): Evaluate;
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import { CandidateUnavailable } from "../search.js";
|
|
3
|
-
const text = (message) => message.content.filter(part => part.type === "text").map(part => part.text).join("").trim();
|
|
4
|
-
const asText = (value) => (typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
5
|
-
const normalize = (value) => value.toLowerCase().replace(/\s+/g, " ").replace(/[.!\s]+$/, "").trim();
|
|
6
|
-
/** The first JSON object or array in a reply, with code fences removed. */
|
|
7
|
-
export function extractJson(reply) {
|
|
8
|
-
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(reply)?.[1] ?? reply;
|
|
9
|
-
const start = fenced.search(/[[{]/);
|
|
10
|
-
if (start < 0)
|
|
11
|
-
return undefined;
|
|
12
|
-
for (let end = fenced.length; end > start; end--) {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(fenced.slice(start, end));
|
|
15
|
-
}
|
|
16
|
-
catch { /* shorten */ }
|
|
17
|
-
}
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
/** Every expected field matches: objects by key, arrays as sets, strings case-insensitively. */
|
|
21
|
-
export function jsonMatches(expected, actual) {
|
|
22
|
-
if (expected === null || typeof expected !== "object") {
|
|
23
|
-
if (typeof expected === "string" && typeof actual === "string")
|
|
24
|
-
return normalize(expected) === normalize(actual);
|
|
25
|
-
return expected === actual;
|
|
26
|
-
}
|
|
27
|
-
if (Array.isArray(expected)) {
|
|
28
|
-
return Array.isArray(actual) && expected.length === actual.length && expected.every(item => actual.some(other => jsonMatches(item, other)));
|
|
29
|
-
}
|
|
30
|
-
if (!actual || typeof actual !== "object" || Array.isArray(actual))
|
|
31
|
-
return false;
|
|
32
|
-
return Object.entries(expected).every(([key, value]) => jsonMatches(value, actual[key]));
|
|
33
|
-
}
|
|
34
|
-
const field = (expected, name) => asText(name && expected && typeof expected === "object" ? expected[name] : expected);
|
|
35
|
-
/** Messages with a provider error are infrastructure failures, not quality verdicts. */
|
|
36
|
-
function ensureOk(message, what) {
|
|
37
|
-
if (message.stopReason === "error" || message.stopReason === "aborted")
|
|
38
|
-
throw new Error(`${what}: ${message.errorMessage ?? message.stopReason}`);
|
|
39
|
-
}
|
|
40
|
-
const status = (message) => Number(/^(\d{3})\b/.exec(message.errorMessage ?? "")?.[1] ?? 0);
|
|
41
|
-
/**
|
|
42
|
-
* One call with retries for transient provider errors (429, 5xx, network), waiting 5, 20 and 60 s.
|
|
43
|
-
* A model the provider rejects (400, 404) or the balance cannot cover (402) is unavailable, not failed.
|
|
44
|
-
*/
|
|
45
|
-
async function call(caller, model, context, options, what, unavailable, waits) {
|
|
46
|
-
for (const wait of [...waits, -1]) {
|
|
47
|
-
const reply = await caller.complete(model, context, options);
|
|
48
|
-
if (reply.stopReason !== "error")
|
|
49
|
-
return reply;
|
|
50
|
-
const code = status(reply);
|
|
51
|
-
if (unavailable && [400, 402, 404, 413, 422].includes(code))
|
|
52
|
-
throw new CandidateUnavailable(`${what}: ${(reply.errorMessage ?? "").slice(0, 240)}`);
|
|
53
|
-
if (wait < 0 || (code !== 0 && code !== 429 && code < 500))
|
|
54
|
-
return reply;
|
|
55
|
-
await delay(wait, undefined, options.signal ? { signal: options.signal } : undefined);
|
|
56
|
-
}
|
|
57
|
-
throw new Error("unreachable");
|
|
58
|
-
}
|
|
59
|
-
function resolve(caller, ref) {
|
|
60
|
-
const slash = ref.indexOf("/");
|
|
61
|
-
const model = slash > 0 ? caller.getModel(ref.slice(0, slash), ref.slice(slash + 1)) : undefined;
|
|
62
|
-
if (!model)
|
|
63
|
-
throw new Error(`Model ${ref} is not available with the connected providers`);
|
|
64
|
-
return model;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Runs each item as one model call through Pi, so any provider Pi connects (API key or
|
|
68
|
-
* subscription) can be a candidate, then grades the reply. Tool-using agents need an
|
|
69
|
-
* evaluator module that runs the real harness instead.
|
|
70
|
-
*/
|
|
71
|
-
export function piEvaluator(caller, spec, options = {}) {
|
|
72
|
-
const waits = options.retryDelaysMs ?? [5_000, 20_000, 60_000];
|
|
73
|
-
return async (candidate, item, context) => {
|
|
74
|
-
const model = resolve(caller, candidate.model);
|
|
75
|
-
const input = asText(item.input);
|
|
76
|
-
const user = spec.inputTemplate ? spec.inputTemplate.replaceAll("{{input}}", input) : input;
|
|
77
|
-
const reply = await call(caller, model, { systemPrompt: spec.systemPrompt, messages: [{ role: "user", content: user, timestamp: Date.now() }] }, { ...(context.signal ? { signal: context.signal } : {}), maxTokens: spec.maxTokens ?? 4096 }, candidate.model, true, waits);
|
|
78
|
-
ensureOk(reply, candidate.model);
|
|
79
|
-
const output = text(reply);
|
|
80
|
-
const usage = { input: reply.usage.input, output: reply.usage.output, total: reply.usage.totalTokens };
|
|
81
|
-
const graded = await grade(spec.grader, item, output, caller, options.judgeModel, waits, context.signal);
|
|
82
|
-
return { pass: graded.pass, comment: graded.comment, output, costUsd: reply.usage.cost.total, usage, ...(reply.durationMs ? { latencyMs: reply.durationMs } : {}) };
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
async function grade(grader, item, output, caller, judgeModel, waits, signal) {
|
|
86
|
-
switch (grader.type) {
|
|
87
|
-
case "exact": {
|
|
88
|
-
const expected = field(item.expectedOutput, grader.field);
|
|
89
|
-
return { pass: normalize(output) === normalize(expected), comment: `expected "${expected.slice(0, 80)}"` };
|
|
90
|
-
}
|
|
91
|
-
case "contains": {
|
|
92
|
-
const expected = field(item.expectedOutput, grader.field);
|
|
93
|
-
return { pass: normalize(output).includes(normalize(expected)), comment: `expected to contain "${expected.slice(0, 80)}"` };
|
|
94
|
-
}
|
|
95
|
-
case "json": {
|
|
96
|
-
const parsed = extractJson(output);
|
|
97
|
-
if (parsed === undefined)
|
|
98
|
-
return { pass: false, comment: "reply has no JSON" };
|
|
99
|
-
const pass = jsonMatches(item.expectedOutput, parsed);
|
|
100
|
-
return { pass, comment: pass ? "every expected field matches" : `expected ${JSON.stringify(item.expectedOutput).slice(0, 160)}` };
|
|
101
|
-
}
|
|
102
|
-
case "judge": {
|
|
103
|
-
const ref = grader.model ?? judgeModel;
|
|
104
|
-
if (!ref)
|
|
105
|
-
throw new Error("The judge grader needs a judge model (--judge-model or the Scout model)");
|
|
106
|
-
const verdict = await call(caller, resolve(caller, ref), {
|
|
107
|
-
systemPrompt: "You grade one model output against a labelled expected output. Reply with JSON only: {\"pass\": true|false, \"reason\": \"<one sentence>\"}. Pass only if the output satisfies the rubric and agrees with the expected output on every point the rubric names.",
|
|
108
|
-
messages: [{ role: "user", content: `Rubric:\n${grader.rubric}\n\nInput:\n${asText(item.input)}\n\nExpected output:\n${asText(item.expectedOutput)}\n\nModel output:\n${output}`, timestamp: Date.now() }],
|
|
109
|
-
}, { ...(signal ? { signal } : {}), maxTokens: 1024 }, `judge ${ref}`, false, waits);
|
|
110
|
-
ensureOk(verdict, `judge ${ref}`);
|
|
111
|
-
const parsed = extractJson(text(verdict));
|
|
112
|
-
if (typeof parsed?.pass !== "boolean")
|
|
113
|
-
throw new Error(`Judge ${ref} returned no verdict`);
|
|
114
|
-
return { pass: parsed.pass, comment: `judge: ${typeof parsed.reason === "string" ? parsed.reason : ""}` };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
package/dist/scout/pipeline.d.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import type { LangfuseConnector } from "../connectors/langfuse/index.js";
|
|
2
|
-
import type { Candidate } from "./candidates.js";
|
|
3
|
-
import { type EvalItem, type Evaluate, type SearchResult } from "./search.js";
|
|
4
|
-
export interface DatasetSource {
|
|
5
|
-
/** An existing Langfuse dataset, such as the customer's `sahra-enrollments`. */
|
|
6
|
-
langfuseDataset?: string;
|
|
7
|
-
/** Local labelled items; pushed to Langfuse as `name` before evaluation so every run is an experiment on it. */
|
|
8
|
-
local?: {
|
|
9
|
-
name: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
items: EvalItem[];
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
export interface ScoutRunOptions {
|
|
15
|
-
task: string;
|
|
16
|
-
dataset: DatasetSource;
|
|
17
|
-
candidates: Candidate[];
|
|
18
|
-
evaluate: Evaluate;
|
|
19
|
-
/** Score name each recorded item carries; the pass rule reads it back from Langfuse. */
|
|
20
|
-
passScore?: string;
|
|
21
|
-
/** Experiment metadata key for the candidate: `model_<role>`. */
|
|
22
|
-
role?: string;
|
|
23
|
-
langfuse: LangfuseConnector | null;
|
|
24
|
-
sampleSize?: number;
|
|
25
|
-
finalists?: number;
|
|
26
|
-
/** See SearchOptions.challengers. */
|
|
27
|
-
challengers?: number;
|
|
28
|
-
/** See SearchOptions.repeats. */
|
|
29
|
-
repeats?: number;
|
|
30
|
-
concurrency?: number;
|
|
31
|
-
log?: (line: string) => void;
|
|
32
|
-
signal?: AbortSignal;
|
|
33
|
-
/** Injected for tests. */
|
|
34
|
-
pickIndex?: (remaining: number) => number;
|
|
35
|
-
langfuseTimeoutMs?: number;
|
|
36
|
-
}
|
|
37
|
-
export interface FinalVerdict {
|
|
38
|
-
model: string;
|
|
39
|
-
eci: number;
|
|
40
|
-
eciSource: string;
|
|
41
|
-
listPrice: number | null;
|
|
42
|
-
/** Passes as Langfuse reports them; local passes when Langfuse is not connected. */
|
|
43
|
-
passes: number;
|
|
44
|
-
/** Item runs: dataset items × repeats. */
|
|
45
|
-
total: number;
|
|
46
|
-
/** Runs of each item. */
|
|
47
|
-
runs: number;
|
|
48
|
-
/** Passes per item over the repeats, e.g. { "APP-2005": 2 } when it passed 2 of 3 runs. */
|
|
49
|
-
passesByItem: Record<string, number>;
|
|
50
|
-
/** Mean measured cost per item run in USD: Langfuse's recorded generation cost when available. */
|
|
51
|
-
costPerItemUsd: number | null;
|
|
52
|
-
localPasses: number;
|
|
53
|
-
verifiedInLangfuse: boolean;
|
|
54
|
-
missingScores: string[];
|
|
55
|
-
/** One Langfuse experiment per repeat (Langfuse allows one result per dataset item per experiment). */
|
|
56
|
-
experimentIds: string[];
|
|
57
|
-
experimentId: string | null;
|
|
58
|
-
experimentUrl: string | null;
|
|
59
|
-
/** Items that failed at least one run. */
|
|
60
|
-
failedItems: string[];
|
|
61
|
-
}
|
|
62
|
-
export interface ScoutReport {
|
|
63
|
-
scout: "OpenMerit Scout";
|
|
64
|
-
runId: string;
|
|
65
|
-
task: string;
|
|
66
|
-
createdAt: string;
|
|
67
|
-
dataset: {
|
|
68
|
-
name: string;
|
|
69
|
-
source: "langfuse" | "local";
|
|
70
|
-
size: number;
|
|
71
|
-
datasetId: string | null;
|
|
72
|
-
url: string | null;
|
|
73
|
-
};
|
|
74
|
-
candidates: number;
|
|
75
|
-
search: Omit<SearchResult, "ordered" | "trials" | "finals" | "unavailable"> & {
|
|
76
|
-
unavailable: SearchResult["unavailable"];
|
|
77
|
-
trials: {
|
|
78
|
-
stage: string;
|
|
79
|
-
model: string;
|
|
80
|
-
eci: number;
|
|
81
|
-
eciSource: string;
|
|
82
|
-
passes: number;
|
|
83
|
-
total: number;
|
|
84
|
-
accepted: boolean;
|
|
85
|
-
costUsd: number | null;
|
|
86
|
-
experimentUrl: string | null;
|
|
87
|
-
}[];
|
|
88
|
-
};
|
|
89
|
-
finals: FinalVerdict[];
|
|
90
|
-
recommendation: FinalVerdict | null;
|
|
91
|
-
runnerUp: FinalVerdict | null;
|
|
92
|
-
why: string;
|
|
93
|
-
spend: {
|
|
94
|
-
measuredUsd: number;
|
|
95
|
-
unknownCostItems: number;
|
|
96
|
-
evaluations: number;
|
|
97
|
-
/** OpenRouter account credit used between the start and the end of the run (account-wide: concurrent use counts too). */
|
|
98
|
-
openRouterCreditsUsd?: number;
|
|
99
|
-
};
|
|
100
|
-
langfuse: {
|
|
101
|
-
baseUrl: string;
|
|
102
|
-
project: string | null;
|
|
103
|
-
experiments: number;
|
|
104
|
-
} | null;
|
|
105
|
-
limitations: string[];
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Scout end to end: dataset → binary search over ECI-ranked candidates → finals on every item,
|
|
109
|
-
* each probe and final recorded as a Langfuse experiment → the winner from Langfuse's results.
|
|
110
|
-
*/
|
|
111
|
-
export declare function runScout(options: ScoutRunOptions): Promise<ScoutReport>;
|