@moda-ai/cli 1.55.44 → 1.55.46
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 +7 -8
- package/dist/{agent-5ax7fqhe.js → agent-aewgndk0.js} +16 -16
- package/dist/{cli-tztzrh6b.js → cli-4kf462jq.js} +3 -3
- package/dist/{cli-xcpwp0gb.js → cli-d6gfxx8t.js} +1 -1
- package/dist/{cli-t4xfvrry.js → cli-dxbd1xxs.js} +5 -5
- package/dist/{cli-wwbr5caw.js → cli-fqc02t6h.js} +12 -91
- package/dist/{cli-sejqvvhv.js → cli-h1z9yyqk.js} +1 -1
- package/dist/{cli-dmm9jm9m.js → cli-j08a1mam.js} +1 -1
- package/dist/{cli-8pcq2et6.js → cli-m4474bnj.js} +1 -1
- package/dist/{cli-xx0pjzkh.js → cli-s7d069g0.js} +5 -19
- package/dist/{cli-k2ka2kjb.js → cli-sqnw96gj.js} +7 -2
- package/dist/{cli-d4cdczhe.js → cli-x7rc3twr.js} +42 -42
- package/dist/{cli-5aa48ry5.js → cli-xctqdn42.js} +3 -3
- package/dist/cli.js +227 -630
- package/dist/{doctor-k4tdgvgv.js → doctor-fz5kq44d.js} +8 -8
- package/dist/{harness-avynbq4f.js → harness-cy0gpyhj.js} +10 -10
- package/dist/{harness-github-actions-gd6vyxb0.js → harness-github-actions-aac3tn96.js} +9 -9
- package/dist/{index-63k2a6gb.js → index-5eyq3ykt.js} +26 -26
- package/dist/{production-intelligence-sv3qx7f4.js → production-intelligence-8j8008d2.js} +6 -4
- package/dist/{profile-commands-mh3kfqkn.js → profile-commands-zbyn9xg2.js} +5 -5
- package/dist/{prompt-attribution-wvvnzw4m.js → prompt-attribution-g9gtv191.js} +9 -9
- package/dist/prompt-source-mcp.js +9 -9
- package/dist/{prompts-snnbyxmz.js → prompts-b6awryvv.js} +9 -13
- package/dist/{provision-t1dpzcjp.js → provision-xkhwz86g.js} +6 -6
- package/dist/{registry-8zkxefjs.js → registry-d7xdm4dw.js} +4 -4
- package/dist/{skills-d4215fa9.js → skills-zfenne8w.js} +6 -6
- package/dist/{sync-17f9yxb1.js → sync-5vd8gwte.js} +12 -12
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +7 -10
- package/skills/moda-cli/references/commands.md +1 -2
- package/skills/moda-cli/references/dashboard-parity.md +2 -3
- package/skills/moda-cli/references/output-protocol.md +1 -1
- package/skills/moda-cli/references/production-intelligence.md +0 -4
- package/skills/moda-cli/references/prompts.md +1 -27
- package/skills/moda-cli/references/{replays-and-registry.md → registry.md} +15 -59
- package/dist/cli-4pc0a18x.js +0 -12
- package/dist/cli-8pksf2x7.js +0 -754
- package/dist/fix-nk5xffz8.js +0 -1440
- package/dist/prompts-ab-y7egdzg4.js +0 -64
- package/dist/prompts-propose-5nw25v9a.js +0 -256
- package/dist/{cli-e24fjyrb.js → cli-da3h2p6w.js} +28 -28
package/dist/cli-8pksf2x7.js
DELETED
|
@@ -1,754 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
assertReplayablePrompt,
|
|
3
|
-
discoverPrompts,
|
|
4
|
-
parsePromptFileForAb,
|
|
5
|
-
readPromptLock,
|
|
6
|
-
runPromptSync
|
|
7
|
-
} from "./cli-wwbr5caw.js";
|
|
8
|
-
import {
|
|
9
|
-
DEFAULT_POLL_INTERVAL_MS,
|
|
10
|
-
DEFAULT_WAIT_TIMEOUT_MS,
|
|
11
|
-
fetchLatestRun
|
|
12
|
-
} from "./cli-4pc0a18x.js";
|
|
13
|
-
import {
|
|
14
|
-
UUID_RE
|
|
15
|
-
} from "./cli-tztzrh6b.js";
|
|
16
|
-
import {
|
|
17
|
-
callControlAPI,
|
|
18
|
-
callDataAPI
|
|
19
|
-
} from "./cli-dmm9jm9m.js";
|
|
20
|
-
import {
|
|
21
|
-
EXIT_ERROR,
|
|
22
|
-
EXIT_OK,
|
|
23
|
-
resolveApiTenantId,
|
|
24
|
-
setPlatformTenantOverride,
|
|
25
|
-
shellArg,
|
|
26
|
-
validateConfig
|
|
27
|
-
} from "./cli-e24fjyrb.js";
|
|
28
|
-
import {
|
|
29
|
-
CliInputError
|
|
30
|
-
} from "./cli-k4tqwah4.js";
|
|
31
|
-
|
|
32
|
-
// src/prompts-ab.ts
|
|
33
|
-
import { accessSync, constants as fsConstants, existsSync, statSync } from "node:fs";
|
|
34
|
-
import { isAbsolute, resolve } from "node:path";
|
|
35
|
-
var DEFAULT_SEEDS_PER_CASE = 3;
|
|
36
|
-
var DEFAULT_AUTO_GENERATE_CASES = 5;
|
|
37
|
-
var MAX_AUTO_GENERATE_CASES = 100;
|
|
38
|
-
var MAX_REPLAY_TRACES = 100;
|
|
39
|
-
var DEFAULT_PROBLEM_MAX_CASES = 10;
|
|
40
|
-
var PROBLEM_PAGE_LIMIT = 50;
|
|
41
|
-
var PROBLEM_MAX_PAGES = 4;
|
|
42
|
-
var PLAYOUT_CONFIRM_THRESHOLD = 200;
|
|
43
|
-
var REPLAY_MODEL_ALLOWLIST = [
|
|
44
|
-
"openai/gpt-5.6-luna",
|
|
45
|
-
"openai/gpt-4o-mini",
|
|
46
|
-
"openai/gpt-4o",
|
|
47
|
-
"anthropic/claude-sonnet-4-5",
|
|
48
|
-
"anthropic/claude-haiku-4-5",
|
|
49
|
-
"google/gemini-2.5-flash",
|
|
50
|
-
"google/gemini-2.5-pro"
|
|
51
|
-
];
|
|
52
|
-
var TERMINAL_RUN_STATUSES = new Set(["completed", "skipped", "error"]);
|
|
53
|
-
var FALLBACK_SUCCESS_CRITERIA = [
|
|
54
|
-
"The agent understands the user request",
|
|
55
|
-
"The agent uses tools appropriately when needed",
|
|
56
|
-
"The agent provides a helpful final answer"
|
|
57
|
-
];
|
|
58
|
-
async function runPromptAb(flags, profileOptions, context) {
|
|
59
|
-
validateConfig();
|
|
60
|
-
const tenantId = flags["tenant-id"] || resolveApiTenantId(profileOptions);
|
|
61
|
-
if (!tenantId) {
|
|
62
|
-
throw new Error("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
63
|
-
}
|
|
64
|
-
setPlatformTenantOverride(tenantId);
|
|
65
|
-
const baselineSource = flags.baseline || flags["baseline-file"] || flags["baseline-key"];
|
|
66
|
-
const candidateSource = flags.candidate || flags["candidate-file"] || flags["candidate-key"];
|
|
67
|
-
if (!baselineSource || !candidateSource) {
|
|
68
|
-
throw new Error("Usage: moda prompts ab --baseline=<file|key> --candidate=<file|key> [--auto-generate [--cases=1-100]|--set-id=ID|--traces=id1,id2[@msg_index] (max 100)|--problem=<problem_id> [--max-cases=1-100]] [--seeds=1-10] [--model=<slug>] [--yes] (legacy alias: --conversations=)");
|
|
69
|
-
}
|
|
70
|
-
await assertArmSourceShape(baselineSource, "baseline");
|
|
71
|
-
await assertArmSourceShape(candidateSource, "candidate");
|
|
72
|
-
const seedsPerCase = parsePositiveInt(flags.seeds ?? flags["seeds-per-case"], DEFAULT_SEEDS_PER_CASE, 10, flags.seeds !== undefined ? "--seeds" : "--seeds-per-case");
|
|
73
|
-
parsePositiveInt(flags["lookback-days"], 30, 365, "--lookback-days");
|
|
74
|
-
const timeoutMs = parsePositiveInt(flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, 24 * 60 * 60 * 1000, "--timeout");
|
|
75
|
-
const pollIntervalMs = parsePositiveInt(flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, 120000, "--poll-interval");
|
|
76
|
-
const assistantModel = assertAllowedModel(flags.model ?? flags["assistant-model"], flags, "--model");
|
|
77
|
-
const plan = await planReplaySet(flags, tenantId, profileOptions);
|
|
78
|
-
const problemCases = plan.kind === "traces" ? plan.problem : undefined;
|
|
79
|
-
if (problemCases && context.outputMode === "human") {
|
|
80
|
-
process.stderr.write(describeProblemCases(problemCases));
|
|
81
|
-
}
|
|
82
|
-
const plannedPlayouts = plan.cases === null ? null : plan.cases * seedsPerCase * 2;
|
|
83
|
-
assertPlayoutBudget(plannedPlayouts, { seedsPerCase, cases: plan.cases }, context.yes, rerunWithYes("prompts ab", [], flags));
|
|
84
|
-
if (context.outputMode === "human") {
|
|
85
|
-
process.stderr.write(`${describePlayouts(plannedPlayouts, plan.cases, seedsPerCase)}
|
|
86
|
-
`);
|
|
87
|
-
}
|
|
88
|
-
if (flags.sync === "true") {
|
|
89
|
-
for (const [source, label] of [[baselineSource, "baseline"], [candidateSource, "candidate"]]) {
|
|
90
|
-
try {
|
|
91
|
-
await resolvePromptArmSpec(source, label);
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (!(error instanceof UnresolvedPromptError))
|
|
94
|
-
throw error;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
await runPromptSync({ ...flags, watch: "false" }, profileOptions);
|
|
98
|
-
}
|
|
99
|
-
const promptArms = {
|
|
100
|
-
prod: await resolvePromptArmSpec(baselineSource, "baseline"),
|
|
101
|
-
proposed: await resolvePromptArmSpec(candidateSource, "candidate")
|
|
102
|
-
};
|
|
103
|
-
const { setId: replaySetId, caseSources, warnings } = await ensureReplaySet(plan, flags, tenantId, profileOptions);
|
|
104
|
-
if (context.outputMode === "human") {
|
|
105
|
-
for (const warning of warnings)
|
|
106
|
-
process.stderr.write(`Warning: ${warning}
|
|
107
|
-
`);
|
|
108
|
-
}
|
|
109
|
-
const writeOptions = warnings.length ? { warnings } : undefined;
|
|
110
|
-
try {
|
|
111
|
-
const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/run`, {
|
|
112
|
-
method: "POST",
|
|
113
|
-
body: JSON.stringify({
|
|
114
|
-
promptArms,
|
|
115
|
-
seedsPerCase,
|
|
116
|
-
assistantModel: assistantModel || undefined,
|
|
117
|
-
promotePrimary: flags.promote === "true" && flags["no-promote"] !== "true",
|
|
118
|
-
...plan.kind === "existing" ? replayCasePin(plan.caseIds) : {}
|
|
119
|
-
})
|
|
120
|
-
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
121
|
-
if (context.outputMode === "human")
|
|
122
|
-
warnIfQueued(enqueue);
|
|
123
|
-
const noWait = flags.wait === "false" || flags["no-wait"] === "true";
|
|
124
|
-
if (noWait) {
|
|
125
|
-
context.output.writeData({
|
|
126
|
-
replaySetId,
|
|
127
|
-
...problemCases ? { problemCases } : {},
|
|
128
|
-
...caseSources ? { caseSources } : {},
|
|
129
|
-
...warnings.length ? { warnings } : {},
|
|
130
|
-
runId: enqueue.runId,
|
|
131
|
-
status: enqueue.status,
|
|
132
|
-
message: enqueue.message ?? "Replay comparison queued",
|
|
133
|
-
plannedPlayouts
|
|
134
|
-
}, writeOptions);
|
|
135
|
-
return EXIT_OK;
|
|
136
|
-
}
|
|
137
|
-
const deadline = Date.now() + timeoutMs;
|
|
138
|
-
let latest = null;
|
|
139
|
-
while (Date.now() < deadline) {
|
|
140
|
-
latest = await fetchLatestRun(tenantId, replaySetId, enqueue.runId, profileOptions);
|
|
141
|
-
const status = latest.run?.status ?? "";
|
|
142
|
-
const runId = latest.run?.runId ?? "";
|
|
143
|
-
if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
if (context.outputMode === "human") {
|
|
147
|
-
process.stderr.write(`Waiting for replay run (${waitLabel(latest.run)})...
|
|
148
|
-
`);
|
|
149
|
-
}
|
|
150
|
-
await sleep(pollIntervalMs);
|
|
151
|
-
}
|
|
152
|
-
if (!latest?.run || latest.run.runId !== enqueue.runId || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
|
|
153
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
|
|
154
|
-
}
|
|
155
|
-
const result = {
|
|
156
|
-
replaySetId,
|
|
157
|
-
...problemCases ? { problemCases } : {},
|
|
158
|
-
...caseSources ? { caseSources } : {},
|
|
159
|
-
...warnings.length ? { warnings } : {},
|
|
160
|
-
plannedPlayouts,
|
|
161
|
-
runId: latest.run.runId,
|
|
162
|
-
status: latest.run.status,
|
|
163
|
-
verdict: formatVerdict(latest),
|
|
164
|
-
run: { ...latest.run },
|
|
165
|
-
cases: latest.cases.map((row) => ({ ...row }))
|
|
166
|
-
};
|
|
167
|
-
if (context.outputMode === "human") {
|
|
168
|
-
printHumanVerdict(result);
|
|
169
|
-
}
|
|
170
|
-
const failed = result.status === "error";
|
|
171
|
-
context.output.writeData(result, { ...writeOptions, ...failed ? { status: "error" } : {} });
|
|
172
|
-
return failed ? EXIT_ERROR : EXIT_OK;
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (warnings.length && error && typeof error === "object") {
|
|
175
|
-
Object.assign(error, { warnings });
|
|
176
|
-
}
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
function warnIfQueued(enqueue) {
|
|
181
|
-
const { runsAhead, maxActiveRuns } = enqueue;
|
|
182
|
-
if (typeof runsAhead !== "number" || typeof maxActiveRuns !== "number")
|
|
183
|
-
return;
|
|
184
|
-
if (runsAhead < maxActiveRuns)
|
|
185
|
-
return;
|
|
186
|
-
process.stderr.write(`Note: ${runsAhead} replay runs for this tenant are already running or queued ` + `(${maxActiveRuns} at a time); run ${enqueue.runId} waits in the queue until a slot frees.
|
|
187
|
-
`);
|
|
188
|
-
}
|
|
189
|
-
function waitLabel(run) {
|
|
190
|
-
if (!run?.status)
|
|
191
|
-
return "pending";
|
|
192
|
-
if (run.status === "accepted") {
|
|
193
|
-
return typeof run.runsAhead === "number" ? `queued, ${run.runsAhead} run${run.runsAhead === 1 ? "" : "s"} ahead` : "queued";
|
|
194
|
-
}
|
|
195
|
-
return run.status;
|
|
196
|
-
}
|
|
197
|
-
var FILE_ARM_RE = /[\\/]|\.(?:md|txt|json|ya?ml|jinja2?|j2|hbs|tmpl|prompt|[cm]?[jt]sx?|py)$/i;
|
|
198
|
-
var PROMPT_KEY_RE = /^[^\s"'`\x00-\x1f\x7f]+$/;
|
|
199
|
-
async function assertArmSourceShape(source, label) {
|
|
200
|
-
const trimmed = source.trim();
|
|
201
|
-
if (!trimmed) {
|
|
202
|
-
throw new CliInputError(`Empty prompt source for ${label}, so nothing was sent.`);
|
|
203
|
-
}
|
|
204
|
-
const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
|
|
205
|
-
const exists = existsSync(absPath);
|
|
206
|
-
if (!exists && FILE_ARM_RE.test(trimmed)) {
|
|
207
|
-
if (await isDiscoveredKey(trimmed))
|
|
208
|
-
return;
|
|
209
|
-
throw new CliInputError(`--${label} file not found: ${trimmed}, so nothing was sent.`);
|
|
210
|
-
}
|
|
211
|
-
if (exists) {
|
|
212
|
-
if (!statSync(absPath).isFile()) {
|
|
213
|
-
throw new CliInputError(`--${label} is not a file: ${trimmed}, so nothing was sent.`);
|
|
214
|
-
}
|
|
215
|
-
try {
|
|
216
|
-
accessSync(absPath, fsConstants.R_OK);
|
|
217
|
-
} catch {
|
|
218
|
-
throw new CliInputError(`--${label} file is not readable: ${trimmed}, so nothing was sent.`);
|
|
219
|
-
}
|
|
220
|
-
if (/\.(?:[cm]?[jt]sx?|py)$/i.test(absPath)) {
|
|
221
|
-
throw new CliInputError(`--${label}=${trimmed} is a source module; source modules cannot be replayed as instructions, so nothing was sent.`, "Use the registered prompt key for this code source.");
|
|
222
|
-
}
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (!PROMPT_KEY_RE.test(trimmed)) {
|
|
226
|
-
throw new CliInputError(`--${label}=${JSON.stringify(trimmed)} is neither an existing prompt file nor a valid prompt key, so nothing was sent.`, "Pass a .prompt.md path or a prompt key (no spaces or quotes).");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
async function isDiscoveredKey(key) {
|
|
230
|
-
try {
|
|
231
|
-
return (await discoverPrompts()).some((prompt) => prompt.key === key);
|
|
232
|
-
} catch {
|
|
233
|
-
return false;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
class UnresolvedPromptError extends Error {
|
|
238
|
-
}
|
|
239
|
-
async function resolvePromptArmSpec(source, label, opts = {}) {
|
|
240
|
-
const trimmed = source.trim();
|
|
241
|
-
if (!trimmed) {
|
|
242
|
-
throw new Error(`Empty prompt source for ${label}`);
|
|
243
|
-
}
|
|
244
|
-
const fromDiscovery = async () => {
|
|
245
|
-
const discovered = await discoverPrompts();
|
|
246
|
-
const matches = discovered.filter((prompt) => prompt.key === trimmed || prompt.sourcePath === trimmed);
|
|
247
|
-
if (matches.length > 1)
|
|
248
|
-
throw new Error(`Multiple prompts use '${trimmed}'; select a stable prompt key.`);
|
|
249
|
-
const match = matches[0];
|
|
250
|
-
if (match)
|
|
251
|
-
assertReplayablePrompt(match);
|
|
252
|
-
return match ? { key: match.key, content: match.content } : null;
|
|
253
|
-
};
|
|
254
|
-
const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
|
|
255
|
-
let key;
|
|
256
|
-
let content;
|
|
257
|
-
const preferred = opts.preferKey ? await fromDiscovery() : null;
|
|
258
|
-
if (preferred) {
|
|
259
|
-
key = preferred.key;
|
|
260
|
-
content = preferred.content;
|
|
261
|
-
} else if (opts.preferKey) {
|
|
262
|
-
throw new Error(`Could not resolve prompt key '${trimmed}' — no discovered prompt matched this key`);
|
|
263
|
-
} else if (existsSync(absPath)) {
|
|
264
|
-
if (/\.(?:[cm]?[jt]sx?|py)$/.test(absPath))
|
|
265
|
-
throw new Error("Use a registered prompt key for code sources; source modules cannot be replayed as instructions.");
|
|
266
|
-
const parsed = parsePromptFileForAb(absPath);
|
|
267
|
-
key = parsed.key;
|
|
268
|
-
content = parsed.content;
|
|
269
|
-
} else {
|
|
270
|
-
const match = await fromDiscovery();
|
|
271
|
-
if (!match) {
|
|
272
|
-
throw new UnresolvedPromptError(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
|
|
273
|
-
}
|
|
274
|
-
key = match.key;
|
|
275
|
-
content = match.content;
|
|
276
|
-
}
|
|
277
|
-
const locked = readPromptLock()?.prompts[key];
|
|
278
|
-
return {
|
|
279
|
-
content,
|
|
280
|
-
prompt_version_id: locked?.versionId ?? "",
|
|
281
|
-
prompt_id: locked?.promptId ?? "",
|
|
282
|
-
prompt_label: label
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
async function planReplaySet(flags, tenantId, profileOptions) {
|
|
286
|
-
const problemPlan = await planProblemCases(flags);
|
|
287
|
-
if (problemPlan)
|
|
288
|
-
return problemPlan;
|
|
289
|
-
const existingSetId = (flags["set-id"] || flags["replay-set-id"] || "").trim();
|
|
290
|
-
if (existingSetId) {
|
|
291
|
-
const snapshot = await fetchReplaySetCases(tenantId, existingSetId, profileOptions);
|
|
292
|
-
return { kind: "existing", setId: existingSetId, cases: snapshot?.ids.length ?? null, caseIds: snapshot?.ids };
|
|
293
|
-
}
|
|
294
|
-
const conversations = [...new Set(parseCsv(flags.traces ?? flags.conversations ?? flags["conversation-ids"]))];
|
|
295
|
-
if (conversations.length > MAX_REPLAY_TRACES) {
|
|
296
|
-
throw new CliInputError(`--traces has ${conversations.length} ids; the limit is ${MAX_REPLAY_TRACES} per replay set, so nothing was sent.`, "Split the traces across several runs, or use --auto-generate.");
|
|
297
|
-
}
|
|
298
|
-
if (conversations.length) {
|
|
299
|
-
return { kind: "traces", traces: conversations, cases: conversations.length };
|
|
300
|
-
}
|
|
301
|
-
if (flags["auto-generate"] === "false") {
|
|
302
|
-
throw new Error("Provide --set-id=, --traces= (legacy alias: --conversations=), or allow --auto-generate (default)");
|
|
303
|
-
}
|
|
304
|
-
return {
|
|
305
|
-
kind: "auto-generate",
|
|
306
|
-
cases: parseCappedInt(flags.cases ?? flags["case-count"], DEFAULT_AUTO_GENERATE_CASES, MAX_AUTO_GENERATE_CASES, "--cases")
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
var PROBLEM_CASE_CONFLICTS = [
|
|
310
|
-
["traces", "--traces"],
|
|
311
|
-
["conversations", "--conversations"],
|
|
312
|
-
["conversation-ids", "--conversation-ids"],
|
|
313
|
-
["set-id", "--set-id"],
|
|
314
|
-
["replay-set-id", "--replay-set-id"],
|
|
315
|
-
["cases", "--cases"],
|
|
316
|
-
["case-count", "--case-count"]
|
|
317
|
-
];
|
|
318
|
-
async function planProblemCases(flags) {
|
|
319
|
-
const raw = flags.problem;
|
|
320
|
-
if (raw === undefined) {
|
|
321
|
-
if (flags["max-cases"] !== undefined) {
|
|
322
|
-
throw new CliInputError("--max-cases applies only with --problem, so nothing was sent.", "Use --cases=N with --auto-generate, or pass --problem=<problem_id>.");
|
|
323
|
-
}
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
const problemId = raw.trim();
|
|
327
|
-
const conflicts = PROBLEM_CASE_CONFLICTS.filter(([key]) => flags[key] !== undefined).map(([, name]) => name);
|
|
328
|
-
if (flags["auto-generate"] === "true")
|
|
329
|
-
conflicts.push("--auto-generate");
|
|
330
|
-
if (conflicts.length) {
|
|
331
|
-
throw new CliInputError(`--problem cannot be combined with ${conflicts.join(", ")}: each picks the replay cases, so nothing was sent.`, "Pass one of --problem=<problem_id> [--max-cases=N], --traces=<ids>, --set-id=<id>, or --auto-generate [--cases=N].");
|
|
332
|
-
}
|
|
333
|
-
if (!UUID_RE.test(problemId)) {
|
|
334
|
-
throw new CliInputError(`--problem needs a canonical problem UUID; got "${problemId}", so nothing was sent.`, "Run `moda problems` to find the problem_id.");
|
|
335
|
-
}
|
|
336
|
-
const maxCases = parsePositiveInt(flags["max-cases"], DEFAULT_PROBLEM_MAX_CASES, MAX_REPLAY_TRACES, "--max-cases");
|
|
337
|
-
const selection = await fetchProblemCases(problemId, maxCases);
|
|
338
|
-
if (!selection.traces.length) {
|
|
339
|
-
throw new CliInputError(`Problem ${problemId} has no affected traces to replay, so nothing was sent.`, `Check the id with \`moda problems get ${problemId}\`, or pass --traces=<ids>.`);
|
|
340
|
-
}
|
|
341
|
-
return {
|
|
342
|
-
kind: "traces",
|
|
343
|
-
traces: selection.traces.map((c) => c.msg_index === null ? c.conversation_id : `${c.conversation_id}@${c.msg_index}`),
|
|
344
|
-
cases: selection.traces.length,
|
|
345
|
-
problem: selection
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
async function readProblemPages(problemId, view, enough, drain = false) {
|
|
349
|
-
const rows = [];
|
|
350
|
-
let enoughAt = null;
|
|
351
|
-
let cursor = null;
|
|
352
|
-
let resolvedId = null;
|
|
353
|
-
let hasMore = false;
|
|
354
|
-
for (let page = 0;page < PROBLEM_MAX_PAGES; page++) {
|
|
355
|
-
const query = new URLSearchParams({ limit: String(PROBLEM_PAGE_LIMIT) });
|
|
356
|
-
if (cursor)
|
|
357
|
-
query.set("cursor", cursor);
|
|
358
|
-
const data = await callDataAPI(`/problems/${encodeURIComponent(problemId)}/${view}?${query.toString()}`);
|
|
359
|
-
if (typeof data?.problem_id === "string" && data.problem_id)
|
|
360
|
-
resolvedId = data.problem_id;
|
|
361
|
-
const pageRows = data?.[view];
|
|
362
|
-
if (Array.isArray(pageRows))
|
|
363
|
-
rows.push(...pageRows);
|
|
364
|
-
const next = data?.pagination?.next_cursor;
|
|
365
|
-
hasMore = data?.pagination?.has_more === true && typeof next === "string" && next !== "";
|
|
366
|
-
if (enoughAt === null && enough(rows))
|
|
367
|
-
enoughAt = rows.length;
|
|
368
|
-
if (!hasMore || enoughAt !== null && !drain)
|
|
369
|
-
break;
|
|
370
|
-
cursor = next;
|
|
371
|
-
}
|
|
372
|
-
return { rows, enoughAt, resolvedId, hasMore };
|
|
373
|
-
}
|
|
374
|
-
async function fetchProblemCases(problemId, maxCases) {
|
|
375
|
-
const anchored = (evidenceRows, traceRows2) => selectProblemCases(evidenceRows, traceRows2, MAX_REPLAY_TRACES).traces.filter((c) => c.msg_index !== null).length;
|
|
376
|
-
const evidence = await readProblemPages(problemId, "evidence", (rows) => anchored(rows, []) >= maxCases, true);
|
|
377
|
-
const choiceRows = evidence.enoughAt === null ? evidence.rows : evidence.rows.slice(0, evidence.enoughAt);
|
|
378
|
-
let traceRows = [];
|
|
379
|
-
let tracesHaveMore = false;
|
|
380
|
-
let resolvedId = evidence.resolvedId;
|
|
381
|
-
if (anchored(choiceRows, []) < maxCases) {
|
|
382
|
-
const traces2 = await readProblemPages(problemId, "conversations", (rows) => anchored(evidence.rows, rows) >= maxCases);
|
|
383
|
-
traceRows = traces2.rows;
|
|
384
|
-
tracesHaveMore = traces2.hasMore;
|
|
385
|
-
resolvedId ??= traces2.resolvedId;
|
|
386
|
-
}
|
|
387
|
-
const selected = selectProblemCases(choiceRows, traceRows, maxCases);
|
|
388
|
-
const all = selectProblemCases(evidence.rows, traceRows, Number.MAX_SAFE_INTEGER);
|
|
389
|
-
const earliestByTrace = new Map(all.traces.map((c) => [c.conversation_id, c]));
|
|
390
|
-
const traces = selected.traces.map((c) => {
|
|
391
|
-
const first = earliestByTrace.get(c.conversation_id);
|
|
392
|
-
return first && first.msg_index !== null && (c.msg_index === null || first.msg_index < c.msg_index) ? first : c;
|
|
393
|
-
});
|
|
394
|
-
const found = all.found;
|
|
395
|
-
const resolved = resolvedId ?? problemId;
|
|
396
|
-
return {
|
|
397
|
-
problemId: resolved,
|
|
398
|
-
...resolved !== problemId ? { requestedProblemId: problemId } : {},
|
|
399
|
-
maxCases,
|
|
400
|
-
traces,
|
|
401
|
-
found,
|
|
402
|
-
moreAvailable: found > traces.length || evidence.hasMore || tracesHaveMore
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
function anchorIndex(value) {
|
|
406
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
407
|
-
}
|
|
408
|
-
function selectProblemCases(evidenceRows, traceRows, maxCases) {
|
|
409
|
-
const byTrace = new Map;
|
|
410
|
-
const consider = (conversationId, msgIndex, source, family) => {
|
|
411
|
-
if (typeof conversationId !== "string" || !conversationId.trim())
|
|
412
|
-
return;
|
|
413
|
-
const id = conversationId.trim();
|
|
414
|
-
const existing = byTrace.get(id);
|
|
415
|
-
const signalFamily = typeof family === "string" && family ? family : undefined;
|
|
416
|
-
if (existing && (msgIndex === null || existing.msg_index !== null && existing.msg_index <= msgIndex))
|
|
417
|
-
return;
|
|
418
|
-
byTrace.set(id, {
|
|
419
|
-
conversation_id: id,
|
|
420
|
-
msg_index: msgIndex,
|
|
421
|
-
source,
|
|
422
|
-
...signalFamily ? { signal_family: signalFamily } : {}
|
|
423
|
-
});
|
|
424
|
-
};
|
|
425
|
-
for (const row of evidenceRows) {
|
|
426
|
-
const r = row ?? {};
|
|
427
|
-
consider(r.conversation_id, anchorIndex(r.anchor_msg_index), "evidence", r.signal_family);
|
|
428
|
-
}
|
|
429
|
-
for (const row of traceRows) {
|
|
430
|
-
const r = row ?? {};
|
|
431
|
-
const anchor = r.latest_anchor ?? {};
|
|
432
|
-
consider(r.conversation_id, anchorIndex(anchor.anchor_msg_index), "trace", undefined);
|
|
433
|
-
}
|
|
434
|
-
const all = [...byTrace.values()];
|
|
435
|
-
const ordered = [...all.filter((c) => c.msg_index !== null), ...all.filter((c) => c.msg_index === null)];
|
|
436
|
-
return { traces: ordered.slice(0, maxCases), found: all.length };
|
|
437
|
-
}
|
|
438
|
-
function describeProblemCases(selection) {
|
|
439
|
-
const requested = selection.requestedProblemId ? ` (requested ${selection.requestedProblemId})` : "";
|
|
440
|
-
const lines = [
|
|
441
|
-
`Replaying ${selection.traces.length} trace(s) from Problem ${selection.problemId}${requested}, each from its evidence anchor:`
|
|
442
|
-
];
|
|
443
|
-
for (const c of selection.traces) {
|
|
444
|
-
const where = c.msg_index === null ? " (no anchor: replays from the first message)" : ` @ msg ${c.msg_index}`;
|
|
445
|
-
lines.push(` ${c.conversation_id}${where}${c.signal_family ? ` [${c.signal_family}]` : ""}`);
|
|
446
|
-
}
|
|
447
|
-
if (selection.moreAvailable) {
|
|
448
|
-
lines.push(`Capped at --max-cases=${selection.maxCases}; the Problem has more affected traces.`);
|
|
449
|
-
}
|
|
450
|
-
return `${lines.join(`
|
|
451
|
-
`)}
|
|
452
|
-
`;
|
|
453
|
-
}
|
|
454
|
-
async function fetchReplaySetCases(tenantId, setId, profileOptions) {
|
|
455
|
-
try {
|
|
456
|
-
const set = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(setId)}`, { method: "GET" }, profileOptions);
|
|
457
|
-
if (!Array.isArray(set?.cases))
|
|
458
|
-
return null;
|
|
459
|
-
return {
|
|
460
|
-
ids: set.cases.filter((c) => String(c?.scenario ?? "").trim()).map((c, index) => String(c?.id ?? `case_${index}`))
|
|
461
|
-
};
|
|
462
|
-
} catch (error) {
|
|
463
|
-
if (error?.statusCode === 404) {
|
|
464
|
-
throw new CliInputError(`Replay set '${setId}' not found for tenant ${tenantId}, so nothing was sent.`, "Check --set-id (list sets in the dashboard, or omit it to auto-generate).");
|
|
465
|
-
}
|
|
466
|
-
return null;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
function replayCasePin(caseIds) {
|
|
470
|
-
if (!caseIds)
|
|
471
|
-
return {};
|
|
472
|
-
return caseIds.length <= REPLAY_CASE_IDS_PIN_MAX ? { caseIds, expectedCases: caseIds.length } : { expectedCases: caseIds.length };
|
|
473
|
-
}
|
|
474
|
-
var REPLAY_CASE_IDS_PIN_MAX = 500;
|
|
475
|
-
function describePlayouts(planned, cases, seeds) {
|
|
476
|
-
return planned === null ? `Planned replay playouts: unknown (case count unavailable) × ${seeds} seeds × 2 arms` : `Planned replay playouts: ${planned} (${cases} cases × ${seeds} seeds × 2 arms)`;
|
|
477
|
-
}
|
|
478
|
-
function assertPlayoutBudget(planned, shape, yes, rerun) {
|
|
479
|
-
if (yes)
|
|
480
|
-
return;
|
|
481
|
-
if (planned !== null && planned <= PLAYOUT_CONFIRM_THRESHOLD)
|
|
482
|
-
return;
|
|
483
|
-
const what = planned === null ? "an unknown number of replay playouts (the replay set's case count could not be read)" : `${planned} replay playouts (${shape.cases} cases × ${shape.seedsPerCase} seeds × 2 arms)`;
|
|
484
|
-
throw new CliInputError(`This would run ${what}, above the ${PLAYOUT_CONFIRM_THRESHOLD}-playout limit that needs confirmation. Nothing was sent.`, `Re-run with --yes to confirm: ${rerun}`);
|
|
485
|
-
}
|
|
486
|
-
function assertAllowedModel(raw, flags, flagName) {
|
|
487
|
-
const model = (raw ?? "").trim();
|
|
488
|
-
if (!model || flags["allow-any-model"] === "true")
|
|
489
|
-
return model;
|
|
490
|
-
if (REPLAY_MODEL_ALLOWLIST.includes(model))
|
|
491
|
-
return model;
|
|
492
|
-
throw new CliInputError(`${flagName}=${model} is not in the replay model allowlist, so nothing was sent.`, `Allowed: ${REPLAY_MODEL_ALLOWLIST.join(", ")}. Pass --allow-any-model to use another OpenRouter slug.`);
|
|
493
|
-
}
|
|
494
|
-
function rerunWithYes(command, positionals, flags) {
|
|
495
|
-
const parts = ["moda", ...command.split(" "), ...positionals.map(shellArg)];
|
|
496
|
-
for (const [key, value] of Object.entries(flags)) {
|
|
497
|
-
if (key === "yes")
|
|
498
|
-
continue;
|
|
499
|
-
parts.push(value === "true" ? `--${key}` : `--${key}=${shellArg(value)}`);
|
|
500
|
-
}
|
|
501
|
-
parts.push("--yes");
|
|
502
|
-
return parts.join(" ");
|
|
503
|
-
}
|
|
504
|
-
async function ensureReplaySet(plan, flags, tenantId, profileOptions) {
|
|
505
|
-
if (plan.kind === "existing")
|
|
506
|
-
return { setId: plan.setId, warnings: [] };
|
|
507
|
-
if (plan.kind === "traces") {
|
|
508
|
-
const defaultName = plan.problem ? `Prompt A/B problem ${plan.problem.problemId.slice(0, 8)} (${plan.traces.length} trace(s))` : undefined;
|
|
509
|
-
return createSetFromTraces(plan.traces, flags, tenantId, profileOptions, defaultName);
|
|
510
|
-
}
|
|
511
|
-
const caseCount = plan.cases;
|
|
512
|
-
const lookbackDays = parsePositiveInt(flags["lookback-days"], 30, 365, "--lookback-days");
|
|
513
|
-
const name = (flags.name ?? flags["set-name"] ?? `Prompt A/B ${new Date().toISOString().slice(0, 10)}`).trim();
|
|
514
|
-
const generated = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/auto-generate`, {
|
|
515
|
-
method: "POST",
|
|
516
|
-
body: JSON.stringify({ name, caseCount, lookbackDays })
|
|
517
|
-
}, profileOptions, { timeoutMs: 300000, retries: 0 });
|
|
518
|
-
if (!generated?.id) {
|
|
519
|
-
throw new Error("Auto-generate did not return a replay set id");
|
|
520
|
-
}
|
|
521
|
-
return { setId: generated.id, warnings: [] };
|
|
522
|
-
}
|
|
523
|
-
function parseTraceRef(ref) {
|
|
524
|
-
const match = /^(.+)@(\d+)$/.exec(ref.trim());
|
|
525
|
-
if (!match)
|
|
526
|
-
return { conversationId: ref.trim() };
|
|
527
|
-
return { conversationId: match[1], startMsgIndex: Number(match[2]) };
|
|
528
|
-
}
|
|
529
|
-
async function createSetFromTraces(conversationIds, flags, tenantId, profileOptions, defaultName) {
|
|
530
|
-
const refs = [...new Set(conversationIds)].map(parseTraceRef);
|
|
531
|
-
const traces = [...new Set(refs.map((ref) => ref.conversationId))];
|
|
532
|
-
const startByConversation = new Map;
|
|
533
|
-
for (const ref of refs) {
|
|
534
|
-
if (ref.startMsgIndex !== undefined)
|
|
535
|
-
startByConversation.set(ref.conversationId, ref.startMsgIndex);
|
|
536
|
-
}
|
|
537
|
-
const name = (flags.name ?? flags["set-name"] ?? defaultName ?? `Prompt A/B ${traces.length} trace(s)`).trim();
|
|
538
|
-
const base = `/tenants/${encodeURIComponent(tenantId)}/replay-sets`;
|
|
539
|
-
const traceAnchors = traces.filter((id) => startByConversation.has(id)).map((id) => ({ conversationId: id, msgIndex: startByConversation.get(id) }));
|
|
540
|
-
let set = null;
|
|
541
|
-
let unavailable = null;
|
|
542
|
-
try {
|
|
543
|
-
set = await callControlAPI(`${base}/auto-generate`, {
|
|
544
|
-
method: "POST",
|
|
545
|
-
body: JSON.stringify({ name, conversationIds: traces, ...traceAnchors.length ? { traceAnchors } : {} })
|
|
546
|
-
}, profileOptions, { timeoutMs: 300000, retries: 0 });
|
|
547
|
-
if (!set?.id)
|
|
548
|
-
throw new Error("Auto-generate did not return a replay set id");
|
|
549
|
-
} catch (error) {
|
|
550
|
-
const status = error?.statusCode;
|
|
551
|
-
if (status === 401 || status === 403)
|
|
552
|
-
throw error;
|
|
553
|
-
unavailable = error instanceof Error ? error.message : String(error);
|
|
554
|
-
set = null;
|
|
555
|
-
}
|
|
556
|
-
const distilledIds = new Set((set?.cases ?? []).map((c) => String(c?.sourceConversationId ?? "")).filter(Boolean));
|
|
557
|
-
const distilled = traces.filter((id) => distilledIds.has(id));
|
|
558
|
-
const fallback = traces.filter((id) => !distilledIds.has(id));
|
|
559
|
-
if (!set) {
|
|
560
|
-
set = await callControlAPI(base, { method: "POST", body: JSON.stringify({ name, description: "Created by moda prompts ab" }) }, profileOptions);
|
|
561
|
-
}
|
|
562
|
-
const requestedIndex = new Map(traces.map((id, index) => [id, index]));
|
|
563
|
-
for (const existing of set.cases ?? []) {
|
|
564
|
-
const conversationId = String(existing?.sourceConversationId ?? "");
|
|
565
|
-
const index = requestedIndex.get(conversationId);
|
|
566
|
-
const startMsgIndex = startByConversation.get(conversationId);
|
|
567
|
-
const update = {};
|
|
568
|
-
if (fallback.length && index !== undefined && existing.position !== index)
|
|
569
|
-
update.position = index;
|
|
570
|
-
if (startMsgIndex !== undefined && existing.sourceStartMsgIndex !== startMsgIndex) {
|
|
571
|
-
update.sourceStartMsgIndex = startMsgIndex;
|
|
572
|
-
}
|
|
573
|
-
if (!Object.keys(update).length)
|
|
574
|
-
continue;
|
|
575
|
-
await callControlAPI(`${base}/${encodeURIComponent(set.id)}/cases/${encodeURIComponent(existing.id)}`, { method: "PUT", body: JSON.stringify(update) }, profileOptions);
|
|
576
|
-
}
|
|
577
|
-
for (const conversationId of fallback) {
|
|
578
|
-
const scenario = await loadScenarioFromConversation(conversationId, startByConversation.get(conversationId));
|
|
579
|
-
await callControlAPI(`${base}/${encodeURIComponent(set.id)}/cases`, {
|
|
580
|
-
method: "POST",
|
|
581
|
-
body: JSON.stringify({
|
|
582
|
-
title: conversationId,
|
|
583
|
-
scenario,
|
|
584
|
-
sourceConversationId: conversationId,
|
|
585
|
-
...startByConversation.has(conversationId) ? { sourceStartMsgIndex: startByConversation.get(conversationId) } : {},
|
|
586
|
-
successCriteria: FALLBACK_SUCCESS_CRITERIA,
|
|
587
|
-
position: requestedIndex.get(conversationId)
|
|
588
|
-
})
|
|
589
|
-
}, profileOptions);
|
|
590
|
-
}
|
|
591
|
-
const warnings = [];
|
|
592
|
-
if (fallback.length) {
|
|
593
|
-
const why = unavailable ? `Moda could not distill these traces (${unavailable.slice(0, 600)})` : "Moda could not distill a scenario for these traces";
|
|
594
|
-
warnings.push(`${why}, so ${fallback.length} of ${traces.length} case(s) use only the opening user message and three generic ` + `success criteria, which long traces pass easily: ${fallback.slice(0, 10).join(", ")}${fallback.length > 10 ? ", …" : ""}. ` + "Edit their scenario/criteria in the dashboard, or pass --set-id with a curated set.");
|
|
595
|
-
}
|
|
596
|
-
return { setId: set.id, caseSources: { distilled, fallback }, warnings };
|
|
597
|
-
}
|
|
598
|
-
var ANCHOR_REQUEST_MAX_READS = 20;
|
|
599
|
-
var CONTEXT_WINDOW = 10;
|
|
600
|
-
async function loadScenarioFromConversation(conversationId, startMsgIndex) {
|
|
601
|
-
const anchored = startMsgIndex !== undefined && startMsgIndex > 0;
|
|
602
|
-
const userRequest = (messages, atOrBefore) => messages.filter((message) => message.role === "user" && String(message.content ?? "").trim() && (atOrBefore === undefined || typeof message.index === "number" && message.index <= atOrBefore));
|
|
603
|
-
const readContext = async (msgIndex, window) => {
|
|
604
|
-
const data = await callDataAPI(`/conversations/${encodeURIComponent(conversationId)}/context?msg_index=${msgIndex}&window=${window}`);
|
|
605
|
-
return data.context?.messages ?? data.messages ?? [];
|
|
606
|
-
};
|
|
607
|
-
try {
|
|
608
|
-
if (anchored) {
|
|
609
|
-
let center = startMsgIndex;
|
|
610
|
-
for (let read = 0;read < ANCHOR_REQUEST_MAX_READS; read++) {
|
|
611
|
-
const lead = userRequest(await readContext(center, CONTEXT_WINDOW), startMsgIndex).pop();
|
|
612
|
-
const request = String(lead?.content ?? "").trim();
|
|
613
|
-
if (request) {
|
|
614
|
-
return `Earlier in the conversation the user and the agent already talked; the replay keeps those turns as history. ` + `Now the user says: "${request}"
|
|
615
|
-
|
|
616
|
-
Help them with this request using the available tools.`;
|
|
617
|
-
}
|
|
618
|
-
if (center - CONTEXT_WINDOW <= 0)
|
|
619
|
-
break;
|
|
620
|
-
center = Math.max(0, center - (2 * CONTEXT_WINDOW + 1));
|
|
621
|
-
}
|
|
622
|
-
} else {
|
|
623
|
-
const request = String(userRequest(await readContext(0, 5))[0]?.content ?? "").trim();
|
|
624
|
-
if (request) {
|
|
625
|
-
return `The user says: "${request}"
|
|
626
|
-
|
|
627
|
-
Replay the conversation faithfully and help them accomplish their goal.`;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} catch {}
|
|
631
|
-
return anchored ? `Replay conversation ${conversationId} from message ${startMsgIndex}: help the user with their request at that point using the available tools.` : `Replay conversation ${conversationId}: help the user accomplish their original goal using the available tools.`;
|
|
632
|
-
}
|
|
633
|
-
function candidateOutcome(payload) {
|
|
634
|
-
const run = payload.run;
|
|
635
|
-
if (!run || run.status !== "completed")
|
|
636
|
-
return "no-run";
|
|
637
|
-
const delta = run.proposedPassCount - run.prodPassCount;
|
|
638
|
-
if (delta > 0)
|
|
639
|
-
return "candidate";
|
|
640
|
-
if (delta < 0)
|
|
641
|
-
return "baseline";
|
|
642
|
-
return "tie";
|
|
643
|
-
}
|
|
644
|
-
function formatVerdict(payload) {
|
|
645
|
-
const run = payload.run;
|
|
646
|
-
if (!run)
|
|
647
|
-
return "no run";
|
|
648
|
-
if (run.status === "skipped")
|
|
649
|
-
return `skipped: ${run.notes || "no reason"}`;
|
|
650
|
-
if (run.status === "error")
|
|
651
|
-
return `error: ${run.notes || "unknown error"}`;
|
|
652
|
-
if (run.status !== "completed")
|
|
653
|
-
return `status=${run.status}`;
|
|
654
|
-
const delta = run.proposedPassCount - run.prodPassCount;
|
|
655
|
-
const winner = delta > 0 ? "candidate wins" : delta < 0 ? "baseline wins" : "tie on case passes";
|
|
656
|
-
const rates = `baseline ${run.prodPassCount}/${run.caseCount} (${pct(run.prodPassRate)}), candidate ${run.proposedPassCount}/${run.caseCount} (${pct(run.proposedPassRate)})`;
|
|
657
|
-
const notes = run.notes?.trim();
|
|
658
|
-
return `${winner}; ${rates}${notes ? `; ${notes}` : ""}`;
|
|
659
|
-
}
|
|
660
|
-
function printHumanVerdict(result) {
|
|
661
|
-
process.stderr.write(`
|
|
662
|
-
Prompt A/B replay verdict
|
|
663
|
-
`);
|
|
664
|
-
process.stderr.write(`-------------------------
|
|
665
|
-
`);
|
|
666
|
-
process.stderr.write(`Replay set: ${result.replaySetId}
|
|
667
|
-
`);
|
|
668
|
-
process.stderr.write(`Run id: ${result.runId}
|
|
669
|
-
`);
|
|
670
|
-
process.stderr.write(`Status: ${result.status}
|
|
671
|
-
`);
|
|
672
|
-
process.stderr.write(`Verdict: ${result.verdict}
|
|
673
|
-
`);
|
|
674
|
-
if (result.cases.length) {
|
|
675
|
-
process.stderr.write(`
|
|
676
|
-
Per-case:
|
|
677
|
-
`);
|
|
678
|
-
for (const row of result.cases) {
|
|
679
|
-
const caseId = String(row.caseId ?? "");
|
|
680
|
-
const prod = formatPass(row.prodPass);
|
|
681
|
-
const proposed = formatPass(row.proposedPass);
|
|
682
|
-
process.stderr.write(` ${caseId}: baseline=${prod} candidate=${proposed}
|
|
683
|
-
`);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
process.stderr.write(`
|
|
687
|
-
`);
|
|
688
|
-
}
|
|
689
|
-
function formatPass(value) {
|
|
690
|
-
if (value === null || value === undefined)
|
|
691
|
-
return "abstained";
|
|
692
|
-
return value ? "pass" : "fail";
|
|
693
|
-
}
|
|
694
|
-
function pct(rate) {
|
|
695
|
-
return `${Math.round(Math.max(0, Math.min(1, rate)) * 100)}%`;
|
|
696
|
-
}
|
|
697
|
-
async function enqueueAndPollComparison(args, profileOptions) {
|
|
698
|
-
const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(args.tenantId)}/replay-sets/${encodeURIComponent(args.replaySetId)}/run`, {
|
|
699
|
-
method: "POST",
|
|
700
|
-
body: JSON.stringify({
|
|
701
|
-
promptArms: args.promptArms,
|
|
702
|
-
seedsPerCase: args.seedsPerCase ?? DEFAULT_SEEDS_PER_CASE,
|
|
703
|
-
assistantModel: args.assistantModel || undefined,
|
|
704
|
-
promotePrimary: args.promotePrimary ?? false,
|
|
705
|
-
...replayCasePin(args.caseIds)
|
|
706
|
-
})
|
|
707
|
-
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
708
|
-
if (args.onWaiting)
|
|
709
|
-
warnIfQueued(enqueue);
|
|
710
|
-
const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
711
|
-
const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
712
|
-
const deadline = Date.now() + timeoutMs;
|
|
713
|
-
let latest = null;
|
|
714
|
-
while (Date.now() < deadline) {
|
|
715
|
-
latest = await fetchLatestRun(args.tenantId, args.replaySetId, enqueue.runId, profileOptions);
|
|
716
|
-
const status = latest.run?.status ?? "";
|
|
717
|
-
const runId = latest.run?.runId ?? "";
|
|
718
|
-
if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
|
|
719
|
-
return latest;
|
|
720
|
-
}
|
|
721
|
-
args.onWaiting?.(waitLabel(latest.run));
|
|
722
|
-
await sleep(pollIntervalMs);
|
|
723
|
-
}
|
|
724
|
-
if (!latest?.run || latest.run.runId !== enqueue.runId || !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
|
|
725
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
|
|
726
|
-
}
|
|
727
|
-
return latest;
|
|
728
|
-
}
|
|
729
|
-
function parsePositiveInt(raw, fallback, max, flagName) {
|
|
730
|
-
if (raw === undefined)
|
|
731
|
-
return fallback;
|
|
732
|
-
const text = String(raw).trim();
|
|
733
|
-
const parsed = Number(text);
|
|
734
|
-
if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < 1) {
|
|
735
|
-
throw new CliInputError(`${flagName}=${raw} must be a whole number of at least 1, so nothing was sent.`, `Use ${flagName}=N with 1 <= N <= ${max}.`);
|
|
736
|
-
}
|
|
737
|
-
if (parsed > max) {
|
|
738
|
-
throw new CliInputError(`${flagName}=${parsed} is above the maximum of ${max}, so nothing was sent.`, `Use ${flagName}=${max} or less.`);
|
|
739
|
-
}
|
|
740
|
-
return parsed;
|
|
741
|
-
}
|
|
742
|
-
function parseCappedInt(raw, fallback, max, flagName) {
|
|
743
|
-
return parsePositiveInt(raw, fallback, max, flagName);
|
|
744
|
-
}
|
|
745
|
-
function parseCsv(raw) {
|
|
746
|
-
if (!raw?.trim())
|
|
747
|
-
return [];
|
|
748
|
-
return raw.split(",").map((part) => part.trim()).filter(Boolean);
|
|
749
|
-
}
|
|
750
|
-
function sleep(ms) {
|
|
751
|
-
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
export { MAX_AUTO_GENERATE_CASES, MAX_REPLAY_TRACES, DEFAULT_PROBLEM_MAX_CASES, PLAYOUT_CONFIRM_THRESHOLD, REPLAY_MODEL_ALLOWLIST, TERMINAL_RUN_STATUSES, runPromptAb, waitLabel, assertArmSourceShape, resolvePromptArmSpec, planReplaySet, fetchProblemCases, selectProblemCases, describeProblemCases, fetchReplaySetCases, replayCasePin, describePlayouts, assertPlayoutBudget, assertAllowedModel, rerunWithYes, parseTraceRef, candidateOutcome, formatVerdict, enqueueAndPollComparison };
|