whatbroke-cli 0.2.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/LICENSE +21 -0
- package/README.md +165 -0
- package/dist/chunk-PXR57ZGT.js +1028 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +186 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +22 -0
- package/examples/support-agent-gpt4o.jsonl +28 -0
- package/examples/support-agent-gpt5mini.jsonl +26 -0
- package/package.json +61 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
diffTraces,
|
|
4
|
+
diffTracesSampled,
|
|
5
|
+
hasSamples,
|
|
6
|
+
loadTrace,
|
|
7
|
+
renderMarkdown,
|
|
8
|
+
renderTerminal,
|
|
9
|
+
startProxy
|
|
10
|
+
} from "./chunk-PXR57ZGT.js";
|
|
11
|
+
|
|
12
|
+
// src/cli.ts
|
|
13
|
+
import pc from "picocolors";
|
|
14
|
+
var HELP = `whatbroke - diff your AI agent's behavior between two runs
|
|
15
|
+
|
|
16
|
+
usage:
|
|
17
|
+
whatbroke diff <before.jsonl> <after.jsonl> [options]
|
|
18
|
+
whatbroke record --out <trace.jsonl> [options]
|
|
19
|
+
|
|
20
|
+
diff options:
|
|
21
|
+
--json machine-readable output
|
|
22
|
+
--md markdown output (drop it in a PR comment)
|
|
23
|
+
--fail-on <level> exit 1 on: breaking (default), warning, never
|
|
24
|
+
--latency <ratio> flag latency regressions above this ratio (default 1.5)
|
|
25
|
+
--cost <ratio> flag cost increases above this ratio (default 1.25)
|
|
26
|
+
--no-outputs skip comparing final outputs
|
|
27
|
+
|
|
28
|
+
record options:
|
|
29
|
+
--out <file> trace file to write (required)
|
|
30
|
+
--port <n> port to listen on (default 4141)
|
|
31
|
+
--run <name> run id when no x-whatbroke-run header is sent
|
|
32
|
+
--target <url> forward everything to this origin instead
|
|
33
|
+
|
|
34
|
+
record starts a local proxy. point your agent at it and run it unchanged:
|
|
35
|
+
OPENAI_BASE_URL=http://127.0.0.1:4141/v1 (openai sdk)
|
|
36
|
+
ANTHROPIC_BASE_URL=http://127.0.0.1:4141 (anthropic sdk)
|
|
37
|
+
stop it with ctrl-c, then diff the trace against a baseline.
|
|
38
|
+
|
|
39
|
+
nondeterministic agent? record each scenario a few times as name#1, name#2, ...
|
|
40
|
+
and diff will report a flap rate per finding instead of noise.
|
|
41
|
+
|
|
42
|
+
trace format: one JSON event per line
|
|
43
|
+
{"type":"run_start","run":"refund-flow","meta":{"model":"gpt-4o"}}
|
|
44
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-4o","latency_ms":900,"tokens":{"input":512,"output":128}}
|
|
45
|
+
{"type":"tool_call","run":"refund-flow","name":"lookup_order","args":{"order_id":"A-1042"}}
|
|
46
|
+
{"type":"output","run":"refund-flow","content":"Refund issued."}
|
|
47
|
+
{"type":"run_end","run":"refund-flow","status":"ok"}
|
|
48
|
+
|
|
49
|
+
you can also record with the SDK (import { Recorder } from "whatbroke") or write
|
|
50
|
+
the JSONL yourself from any language. Full docs: https://github.com/arthi-arumugam-git/whatbroke
|
|
51
|
+
`;
|
|
52
|
+
function fail(message) {
|
|
53
|
+
console.error(pc.red(`whatbroke: ${message}`));
|
|
54
|
+
console.error(pc.dim(`try: whatbroke --help`));
|
|
55
|
+
process.exit(2);
|
|
56
|
+
}
|
|
57
|
+
function main() {
|
|
58
|
+
const argv = process.argv.slice(2);
|
|
59
|
+
if (!argv.length || argv.includes("-h") || argv.includes("--help")) {
|
|
60
|
+
console.log(HELP);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
const command = argv[0];
|
|
64
|
+
if (command === "record") {
|
|
65
|
+
runRecord(argv.slice(1));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (command !== "diff") fail(`unknown command: ${command}`);
|
|
69
|
+
const positional = [];
|
|
70
|
+
let format = "terminal";
|
|
71
|
+
let failOn = "breaking";
|
|
72
|
+
let latencyThreshold = 1.5;
|
|
73
|
+
let costThreshold = 1.25;
|
|
74
|
+
let compareOutputs = true;
|
|
75
|
+
for (let i = 1; i < argv.length; i++) {
|
|
76
|
+
const arg = argv[i];
|
|
77
|
+
switch (arg) {
|
|
78
|
+
case "--json":
|
|
79
|
+
format = "json";
|
|
80
|
+
break;
|
|
81
|
+
case "--md":
|
|
82
|
+
format = "md";
|
|
83
|
+
break;
|
|
84
|
+
case "--no-outputs":
|
|
85
|
+
compareOutputs = false;
|
|
86
|
+
break;
|
|
87
|
+
case "--fail-on": {
|
|
88
|
+
const value = argv[++i];
|
|
89
|
+
if (value !== "breaking" && value !== "warning" && value !== "never") {
|
|
90
|
+
fail(`--fail-on must be breaking, warning, or never`);
|
|
91
|
+
}
|
|
92
|
+
failOn = value;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "--latency": {
|
|
96
|
+
const value = Number(argv[++i]);
|
|
97
|
+
if (!Number.isFinite(value) || value <= 0) fail(`--latency needs a positive number`);
|
|
98
|
+
latencyThreshold = value;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "--cost": {
|
|
102
|
+
const value = Number(argv[++i]);
|
|
103
|
+
if (!Number.isFinite(value) || value <= 0) fail(`--cost needs a positive number`);
|
|
104
|
+
costThreshold = value;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
if (arg.startsWith("-")) fail(`unknown option: ${arg}`);
|
|
109
|
+
positional.push(arg);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (positional.length !== 2) {
|
|
113
|
+
fail("diff needs exactly two trace files: whatbroke diff before.jsonl after.jsonl");
|
|
114
|
+
}
|
|
115
|
+
let result;
|
|
116
|
+
try {
|
|
117
|
+
const before = loadTrace(positional[0]);
|
|
118
|
+
const after = loadTrace(positional[1]);
|
|
119
|
+
const options = { latencyThreshold, costThreshold, compareOutputs };
|
|
120
|
+
result = hasSamples(before) || hasSamples(after) ? diffTracesSampled(before, after, options) : diffTraces(before, after, options);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
123
|
+
}
|
|
124
|
+
if (format === "json") {
|
|
125
|
+
console.log(JSON.stringify(result, null, 2));
|
|
126
|
+
} else if (format === "md") {
|
|
127
|
+
console.log(renderMarkdown(result));
|
|
128
|
+
} else {
|
|
129
|
+
console.log(renderTerminal(result));
|
|
130
|
+
}
|
|
131
|
+
const shouldFail = failOn === "breaking" && result.breaking > 0 || failOn === "warning" && (result.breaking > 0 || result.warnings > 0);
|
|
132
|
+
process.exit(shouldFail ? 1 : 0);
|
|
133
|
+
}
|
|
134
|
+
function runRecord(argv) {
|
|
135
|
+
let out = "";
|
|
136
|
+
let port = 4141;
|
|
137
|
+
let run;
|
|
138
|
+
let target;
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const arg = argv[i];
|
|
141
|
+
switch (arg) {
|
|
142
|
+
case "--out":
|
|
143
|
+
out = argv[++i] ?? "";
|
|
144
|
+
break;
|
|
145
|
+
case "--port": {
|
|
146
|
+
const value = Number(argv[++i]);
|
|
147
|
+
if (!Number.isInteger(value) || value < 0 || value > 65535) {
|
|
148
|
+
fail(`--port needs a number between 0 and 65535`);
|
|
149
|
+
}
|
|
150
|
+
port = value;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "--run":
|
|
154
|
+
run = argv[++i];
|
|
155
|
+
break;
|
|
156
|
+
case "--target":
|
|
157
|
+
target = argv[++i];
|
|
158
|
+
break;
|
|
159
|
+
default:
|
|
160
|
+
fail(`unknown option: ${arg}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (!out) fail("record needs --out <trace.jsonl>");
|
|
164
|
+
startProxy({
|
|
165
|
+
file: out,
|
|
166
|
+
port,
|
|
167
|
+
run,
|
|
168
|
+
target,
|
|
169
|
+
onRecord: (runId, model, toolNames) => {
|
|
170
|
+
const tools = toolNames.length ? ` -> ${toolNames.join(", ")}` : "";
|
|
171
|
+
console.log(pc.dim(` [${runId}] ${model}${tools}`));
|
|
172
|
+
}
|
|
173
|
+
}).then((proxy) => {
|
|
174
|
+
console.log(`recording to ${out}`);
|
|
175
|
+
console.log(pc.dim(` OPENAI_BASE_URL=${proxy.url}/v1`));
|
|
176
|
+
console.log(pc.dim(` ANTHROPIC_BASE_URL=${proxy.url}`));
|
|
177
|
+
console.log(pc.dim(" ctrl-c to stop"));
|
|
178
|
+
console.log("");
|
|
179
|
+
process.on("SIGINT", () => {
|
|
180
|
+
proxy.close().finally(() => process.exit(0));
|
|
181
|
+
});
|
|
182
|
+
}).catch((err) => {
|
|
183
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
interface RunStartEvent {
|
|
2
|
+
type: "run_start";
|
|
3
|
+
run: string;
|
|
4
|
+
ts?: number;
|
|
5
|
+
meta?: Record<string, unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface LlmCallEvent {
|
|
8
|
+
type: "llm_call";
|
|
9
|
+
run: string;
|
|
10
|
+
ts?: number;
|
|
11
|
+
model: string;
|
|
12
|
+
latency_ms?: number;
|
|
13
|
+
tokens?: {
|
|
14
|
+
input?: number;
|
|
15
|
+
output?: number;
|
|
16
|
+
};
|
|
17
|
+
cost_usd?: number;
|
|
18
|
+
stop_reason?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
interface ToolCallEvent {
|
|
22
|
+
type: "tool_call";
|
|
23
|
+
run: string;
|
|
24
|
+
ts?: number;
|
|
25
|
+
name: string;
|
|
26
|
+
args?: Record<string, unknown>;
|
|
27
|
+
latency_ms?: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
interface OutputEvent {
|
|
31
|
+
type: "output";
|
|
32
|
+
run: string;
|
|
33
|
+
ts?: number;
|
|
34
|
+
content: string;
|
|
35
|
+
}
|
|
36
|
+
interface RunEndEvent {
|
|
37
|
+
type: "run_end";
|
|
38
|
+
run: string;
|
|
39
|
+
ts?: number;
|
|
40
|
+
status: "ok" | "error";
|
|
41
|
+
error?: string;
|
|
42
|
+
}
|
|
43
|
+
type TraceEvent = RunStartEvent | LlmCallEvent | ToolCallEvent | OutputEvent | RunEndEvent;
|
|
44
|
+
interface Run {
|
|
45
|
+
id: string;
|
|
46
|
+
meta: Record<string, unknown>;
|
|
47
|
+
llmCalls: LlmCallEvent[];
|
|
48
|
+
toolCalls: ToolCallEvent[];
|
|
49
|
+
outputs: OutputEvent[];
|
|
50
|
+
status: "ok" | "error" | "unknown";
|
|
51
|
+
error?: string;
|
|
52
|
+
}
|
|
53
|
+
type Severity = "breaking" | "warning" | "info";
|
|
54
|
+
interface Finding {
|
|
55
|
+
severity: Severity;
|
|
56
|
+
/** stable identity for the thing that changed (tool name, "output", ...), used to correlate findings across sampled runs */
|
|
57
|
+
subject?: string;
|
|
58
|
+
/** how often this finding appeared across sampled run pairs, e.g. "4/9" */
|
|
59
|
+
rate?: string;
|
|
60
|
+
/** true when the same finding also shows up between baseline samples, i.e. the agent was already flaky here */
|
|
61
|
+
flaky?: boolean;
|
|
62
|
+
kind: "run_missing" | "run_added" | "status_changed" | "tool_removed" | "tool_added" | "tool_args_changed" | "tool_reordered" | "tool_error" | "output_changed" | "output_missing" | "latency_regression" | "cost_increase" | "token_change" | "model_changed";
|
|
63
|
+
run: string;
|
|
64
|
+
message: string;
|
|
65
|
+
detail?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
interface RunStats {
|
|
68
|
+
llmCalls: number;
|
|
69
|
+
toolCalls: number;
|
|
70
|
+
inputTokens: number;
|
|
71
|
+
outputTokens: number;
|
|
72
|
+
costUsd: number;
|
|
73
|
+
latencyMs: number;
|
|
74
|
+
models: string[];
|
|
75
|
+
}
|
|
76
|
+
interface RunDiff {
|
|
77
|
+
run: string;
|
|
78
|
+
findings: Finding[];
|
|
79
|
+
before?: RunStats;
|
|
80
|
+
after?: RunStats;
|
|
81
|
+
}
|
|
82
|
+
interface DiffResult {
|
|
83
|
+
runs: RunDiff[];
|
|
84
|
+
findings: Finding[];
|
|
85
|
+
breaking: number;
|
|
86
|
+
warnings: number;
|
|
87
|
+
info: number;
|
|
88
|
+
}
|
|
89
|
+
interface DiffOptions {
|
|
90
|
+
/** flag latency regressions above this ratio, e.g. 1.5 = 50% slower (default 1.5) */
|
|
91
|
+
latencyThreshold?: number;
|
|
92
|
+
/** flag cost increases above this ratio (default 1.25) */
|
|
93
|
+
costThreshold?: number;
|
|
94
|
+
/** compare final outputs (default true) */
|
|
95
|
+
compareOutputs?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
declare function diffTraces(before: Map<string, Run>, after: Map<string, Run>, options?: DiffOptions): DiffResult;
|
|
99
|
+
|
|
100
|
+
declare function hasSamples(runs: Map<string, Run>): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Like diffTraces, but runs named `name#1`, `name#2`, ... are treated as
|
|
103
|
+
* repeated samples of the same scenario. Every before sample is compared to
|
|
104
|
+
* every after sample, findings carry an occurrence rate, and findings that
|
|
105
|
+
* also appear between baseline samples are demoted to flaky info, because the
|
|
106
|
+
* agent already behaved that way before the change.
|
|
107
|
+
*/
|
|
108
|
+
declare function diffTracesSampled(before: Map<string, Run>, after: Map<string, Run>, options?: DiffOptions): DiffResult;
|
|
109
|
+
|
|
110
|
+
declare function parseTrace(text: string): Map<string, Run>;
|
|
111
|
+
declare function loadTrace(path: string): Map<string, Run>;
|
|
112
|
+
|
|
113
|
+
interface ProxyOptions {
|
|
114
|
+
/** trace file path, e.g. traces/current.jsonl */
|
|
115
|
+
file: string;
|
|
116
|
+
/** port to listen on, 0 picks a free one (default 4141) */
|
|
117
|
+
port?: number;
|
|
118
|
+
/** upstream origin override, e.g. http://localhost:8080 for a mock */
|
|
119
|
+
target?: string;
|
|
120
|
+
/** run id when the client sends no x-whatbroke-run header */
|
|
121
|
+
run?: string;
|
|
122
|
+
/** called once per recorded llm call, used by the CLI for progress output */
|
|
123
|
+
onRecord?: (run: string, model: string, toolNames: string[]) => void;
|
|
124
|
+
}
|
|
125
|
+
interface ProxyHandle {
|
|
126
|
+
port: number;
|
|
127
|
+
url: string;
|
|
128
|
+
close: () => Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Starts a local HTTP proxy that forwards requests to the OpenAI or
|
|
132
|
+
* Anthropic API and records every LLM call, tool call, and final text
|
|
133
|
+
* output to a JSONL trace. Point OPENAI_BASE_URL or ANTHROPIC_BASE_URL at
|
|
134
|
+
* it and run your agent unchanged. Requests to /v1/messages go to
|
|
135
|
+
* api.anthropic.com, everything else to api.openai.com, unless `target`
|
|
136
|
+
* says otherwise. Group runs with an x-whatbroke-run request header.
|
|
137
|
+
*/
|
|
138
|
+
declare function startProxy(options: ProxyOptions): Promise<ProxyHandle>;
|
|
139
|
+
|
|
140
|
+
interface RecorderOptions {
|
|
141
|
+
/** trace file path, e.g. traces/before.jsonl */
|
|
142
|
+
file: string;
|
|
143
|
+
/** run id for this scenario, defaults to "default" */
|
|
144
|
+
run?: string;
|
|
145
|
+
meta?: Record<string, unknown>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Writes trace events to a JSONL file. Use one recorder per scenario run,
|
|
149
|
+
* or pass a run id per call if you drive many scenarios from one place.
|
|
150
|
+
*/
|
|
151
|
+
declare class Recorder {
|
|
152
|
+
private file;
|
|
153
|
+
private run;
|
|
154
|
+
constructor(options: RecorderOptions);
|
|
155
|
+
private write;
|
|
156
|
+
llmCall(data: {
|
|
157
|
+
model: string;
|
|
158
|
+
latencyMs?: number;
|
|
159
|
+
inputTokens?: number;
|
|
160
|
+
outputTokens?: number;
|
|
161
|
+
costUsd?: number;
|
|
162
|
+
stopReason?: string;
|
|
163
|
+
error?: string;
|
|
164
|
+
}): void;
|
|
165
|
+
toolCall(name: string, args?: Record<string, unknown>, data?: {
|
|
166
|
+
latencyMs?: number;
|
|
167
|
+
error?: string;
|
|
168
|
+
}): void;
|
|
169
|
+
output(content: string): void;
|
|
170
|
+
end(status?: "ok" | "error", error?: string): void;
|
|
171
|
+
/**
|
|
172
|
+
* Wraps an OpenAI client (openai npm package) so every
|
|
173
|
+
* chat.completions.create call is recorded, including tool calls the
|
|
174
|
+
* model requested. Returns the same client.
|
|
175
|
+
*/
|
|
176
|
+
wrapOpenAI<T extends object>(client: T): T;
|
|
177
|
+
/**
|
|
178
|
+
* Wraps an Anthropic client (@anthropic-ai/sdk) so every messages.create
|
|
179
|
+
* call is recorded, including tool_use blocks. Returns the same client.
|
|
180
|
+
*/
|
|
181
|
+
wrapAnthropic<T extends object>(client: T): T;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
declare function renderTerminal(result: DiffResult): string;
|
|
185
|
+
declare function renderMarkdown(result: DiffResult): string;
|
|
186
|
+
|
|
187
|
+
export { type DiffOptions, type DiffResult, type Finding, Recorder, type Run, type RunDiff, type RunStats, type Severity, type TraceEvent, diffTraces, diffTracesSampled, hasSamples, loadTrace, parseTrace, renderMarkdown, renderTerminal, startProxy };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Recorder,
|
|
3
|
+
diffTraces,
|
|
4
|
+
diffTracesSampled,
|
|
5
|
+
hasSamples,
|
|
6
|
+
loadTrace,
|
|
7
|
+
parseTrace,
|
|
8
|
+
renderMarkdown,
|
|
9
|
+
renderTerminal,
|
|
10
|
+
startProxy
|
|
11
|
+
} from "./chunk-PXR57ZGT.js";
|
|
12
|
+
export {
|
|
13
|
+
Recorder,
|
|
14
|
+
diffTraces,
|
|
15
|
+
diffTracesSampled,
|
|
16
|
+
hasSamples,
|
|
17
|
+
loadTrace,
|
|
18
|
+
parseTrace,
|
|
19
|
+
renderMarkdown,
|
|
20
|
+
renderTerminal,
|
|
21
|
+
startProxy
|
|
22
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{"type":"run_start","run":"refund-flow","meta":{"model":"gpt-4o","scenario":"customer asks for a refund on order A-1042"}}
|
|
2
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-4o","latency_ms":840,"tokens":{"input":1240,"output":86},"cost_usd":0.0044}
|
|
3
|
+
{"type":"tool_call","run":"refund-flow","name":"lookup_order","args":{"order_id":"A-1042"},"latency_ms":120}
|
|
4
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-4o","latency_ms":920,"tokens":{"input":1410,"output":64},"cost_usd":0.0048}
|
|
5
|
+
{"type":"tool_call","run":"refund-flow","name":"issue_refund","args":{"order_id":"A-1042","amount":42.5,"reason":"damaged item"},"latency_ms":340}
|
|
6
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-4o","latency_ms":610,"tokens":{"input":1520,"output":58},"cost_usd":0.0049}
|
|
7
|
+
{"type":"output","run":"refund-flow","content":"Your refund of $42.50 for order A-1042 has been issued. It should appear in 3-5 business days."}
|
|
8
|
+
{"type":"run_end","run":"refund-flow","status":"ok"}
|
|
9
|
+
{"type":"run_start","run":"cancel-subscription","meta":{"model":"gpt-4o","scenario":"customer wants to cancel their subscription"}}
|
|
10
|
+
{"type":"llm_call","run":"cancel-subscription","model":"gpt-4o","latency_ms":780,"tokens":{"input":1180,"output":92},"cost_usd":0.0042}
|
|
11
|
+
{"type":"tool_call","run":"cancel-subscription","name":"get_subscription","args":{"customer_id":"C-889"},"latency_ms":95}
|
|
12
|
+
{"type":"llm_call","run":"cancel-subscription","model":"gpt-4o","latency_ms":850,"tokens":{"input":1350,"output":71},"cost_usd":0.0046}
|
|
13
|
+
{"type":"tool_call","run":"cancel-subscription","name":"cancel_subscription","args":{"customer_id":"C-889","effective":"end_of_period"},"latency_ms":210}
|
|
14
|
+
{"type":"llm_call","run":"cancel-subscription","model":"gpt-4o","latency_ms":590,"tokens":{"input":1460,"output":55},"cost_usd":0.0047}
|
|
15
|
+
{"type":"output","run":"cancel-subscription","content":"Your subscription is cancelled effective at the end of the current billing period. You keep access until then."}
|
|
16
|
+
{"type":"run_end","run":"cancel-subscription","status":"ok"}
|
|
17
|
+
{"type":"run_start","run":"order-status","meta":{"model":"gpt-4o","scenario":"customer asks where their order is"}}
|
|
18
|
+
{"type":"llm_call","run":"order-status","model":"gpt-4o","latency_ms":720,"tokens":{"input":1100,"output":78},"cost_usd":0.0039}
|
|
19
|
+
{"type":"tool_call","run":"order-status","name":"lookup_order","args":{"order_id":"B-2210"},"latency_ms":110}
|
|
20
|
+
{"type":"llm_call","run":"order-status","model":"gpt-4o","latency_ms":640,"tokens":{"input":1290,"output":66},"cost_usd":0.0043}
|
|
21
|
+
{"type":"output","run":"order-status","content":"Order B-2210 shipped yesterday and is expected to arrive Friday."}
|
|
22
|
+
{"type":"run_end","run":"order-status","status":"ok"}
|
|
23
|
+
{"type":"run_start","run":"angry-escalation","meta":{"model":"gpt-4o","scenario":"furious customer demands a manager"}}
|
|
24
|
+
{"type":"llm_call","run":"angry-escalation","model":"gpt-4o","latency_ms":880,"tokens":{"input":1330,"output":104},"cost_usd":0.0047}
|
|
25
|
+
{"type":"tool_call","run":"angry-escalation","name":"create_ticket","args":{"priority":"high","team":"support-escalations"},"latency_ms":150}
|
|
26
|
+
{"type":"llm_call","run":"angry-escalation","model":"gpt-4o","latency_ms":700,"tokens":{"input":1480,"output":89},"cost_usd":0.0049}
|
|
27
|
+
{"type":"output","run":"angry-escalation","content":"I understand this has been frustrating. I've escalated your case to our support team with high priority. Ticket #4471, someone will reach out within 2 hours."}
|
|
28
|
+
{"type":"run_end","run":"angry-escalation","status":"ok"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{"type":"run_start","run":"refund-flow","meta":{"model":"gpt-5-mini","scenario":"customer asks for a refund on order A-1042"}}
|
|
2
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-5-mini","latency_ms":610,"tokens":{"input":1240,"output":71},"cost_usd":0.0011}
|
|
3
|
+
{"type":"tool_call","run":"refund-flow","name":"lookup_order","args":{"order_id":"A-1042"},"latency_ms":120}
|
|
4
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-5-mini","latency_ms":680,"tokens":{"input":1410,"output":52},"cost_usd":0.0012}
|
|
5
|
+
{"type":"tool_call","run":"refund-flow","name":"issue_refund","args":{"order_id":"A-1042","amount":425,"reason":"damaged item"},"latency_ms":340}
|
|
6
|
+
{"type":"llm_call","run":"refund-flow","model":"gpt-5-mini","latency_ms":450,"tokens":{"input":1520,"output":49},"cost_usd":0.0012}
|
|
7
|
+
{"type":"output","run":"refund-flow","content":"Your refund of $425.00 for order A-1042 has been issued. It should appear in 3-5 business days."}
|
|
8
|
+
{"type":"run_end","run":"refund-flow","status":"ok"}
|
|
9
|
+
{"type":"run_start","run":"cancel-subscription","meta":{"model":"gpt-5-mini","scenario":"customer wants to cancel their subscription"}}
|
|
10
|
+
{"type":"llm_call","run":"cancel-subscription","model":"gpt-5-mini","latency_ms":590,"tokens":{"input":1180,"output":88},"cost_usd":0.001}
|
|
11
|
+
{"type":"tool_call","run":"cancel-subscription","name":"get_subscription","args":{"customer_id":"C-889"},"latency_ms":95}
|
|
12
|
+
{"type":"llm_call","run":"cancel-subscription","model":"gpt-5-mini","latency_ms":620,"tokens":{"input":1350,"output":112},"cost_usd":0.0011}
|
|
13
|
+
{"type":"output","run":"cancel-subscription","content":"I can help with that. To confirm the cancellation, please reply CONFIRM and I will process it right away."}
|
|
14
|
+
{"type":"run_end","run":"cancel-subscription","status":"ok"}
|
|
15
|
+
{"type":"run_start","run":"order-status","meta":{"model":"gpt-5-mini","scenario":"customer asks where their order is"}}
|
|
16
|
+
{"type":"llm_call","run":"order-status","model":"gpt-5-mini","latency_ms":540,"tokens":{"input":1100,"output":74},"cost_usd":0.001}
|
|
17
|
+
{"type":"tool_call","run":"order-status","name":"lookup_order","args":{"order_id":"B-2210"},"latency_ms":110}
|
|
18
|
+
{"type":"llm_call","run":"order-status","model":"gpt-5-mini","latency_ms":480,"tokens":{"input":1290,"output":66},"cost_usd":0.001}
|
|
19
|
+
{"type":"output","run":"order-status","content":"Order B-2210 shipped yesterday and is expected to arrive Friday."}
|
|
20
|
+
{"type":"run_end","run":"order-status","status":"ok"}
|
|
21
|
+
{"type":"run_start","run":"angry-escalation","meta":{"model":"gpt-5-mini","scenario":"furious customer demands a manager"}}
|
|
22
|
+
{"type":"llm_call","run":"angry-escalation","model":"gpt-5-mini","latency_ms":650,"tokens":{"input":1330,"output":96},"cost_usd":0.001}
|
|
23
|
+
{"type":"tool_call","run":"angry-escalation","name":"create_ticket","args":{"priority":"normal","team":"support-general"},"latency_ms":150}
|
|
24
|
+
{"type":"llm_call","run":"angry-escalation","model":"gpt-5-mini","latency_ms":510,"tokens":{"input":1480,"output":81},"cost_usd":0.001}
|
|
25
|
+
{"type":"output","run":"angry-escalation","content":"I'm sorry to hear that. I've created a ticket for your issue and our team will get back to you soon."}
|
|
26
|
+
{"type":"run_end","run":"angry-escalation","status":"ok"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "whatbroke-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Diff your AI agent's behavior between two runs. Swap a model, change a prompt, run whatbroke, see exactly what changed.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai-agents",
|
|
7
|
+
"llm",
|
|
8
|
+
"evals",
|
|
9
|
+
"regression-testing",
|
|
10
|
+
"diff",
|
|
11
|
+
"tracing",
|
|
12
|
+
"openai",
|
|
13
|
+
"anthropic"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Arthi Arumugam",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/arthi-arumugam-git/whatbroke.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/arthi-arumugam-git/whatbroke#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/arthi-arumugam-git/whatbroke/issues"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"whatbroke": "dist/cli.js"
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"examples"
|
|
41
|
+
],
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"dev": "tsx src/cli.ts",
|
|
46
|
+
"prepublishOnly": "npm run build && npm test"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^22.10.0",
|
|
50
|
+
"tsup": "^8.3.0",
|
|
51
|
+
"tsx": "^4.19.0",
|
|
52
|
+
"typescript": "^5.7.0",
|
|
53
|
+
"vitest": "^2.1.0"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"picocolors": "^1.1.0"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=18"
|
|
60
|
+
}
|
|
61
|
+
}
|