orcareplay 0.2.2 → 0.2.4
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/dist/args.d.ts +33 -0
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +41 -5
- package/dist/args.js.map +1 -1
- package/dist/commands/attach.js +15 -4
- package/dist/commands/attach.js.map +1 -1
- package/dist/commands/inspect.d.ts +11 -0
- package/dist/commands/inspect.d.ts.map +1 -1
- package/dist/commands/inspect.js +43 -0
- package/dist/commands/inspect.js.map +1 -1
- package/dist/commands/quickstart.d.ts +48 -0
- package/dist/commands/quickstart.d.ts.map +1 -0
- package/dist/commands/quickstart.js +373 -0
- package/dist/commands/quickstart.js.map +1 -0
- package/dist/commands/record.d.ts +13 -0
- package/dist/commands/record.d.ts.map +1 -1
- package/dist/commands/record.js +119 -8
- package/dist/commands/record.js.map +1 -1
- package/dist/commands/replay.d.ts.map +1 -1
- package/dist/commands/replay.js +439 -283
- package/dist/commands/replay.js.map +1 -1
- package/dist/commands/scrub.d.ts.map +1 -1
- package/dist/commands/scrub.js +182 -47
- package/dist/commands/scrub.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +67 -4
- package/dist/commands/setup.js.map +1 -1
- package/dist/exchange-events.d.ts.map +1 -1
- package/dist/exchange-events.js +23 -0
- package/dist/exchange-events.js.map +1 -1
- package/dist/flags.d.ts.map +1 -1
- package/dist/flags.js +68 -2
- package/dist/flags.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +32 -0
- package/dist/main.js.map +1 -1
- package/dist/tls-capture.d.ts +10 -2
- package/dist/tls-capture.d.ts.map +1 -1
- package/dist/tls-capture.js +41 -3
- package/dist/tls-capture.js.map +1 -1
- package/dist/upstream.d.ts.map +1 -1
- package/dist/upstream.js +17 -0
- package/dist/upstream.js.map +1 -1
- package/package.json +13 -12
- package/quickstart/project/agent.mjs +128 -0
- package/quickstart/project/package.json +8 -0
- package/quickstart/project/src/schedule.js +41 -0
- package/quickstart/project/test/schedule.test.js +25 -0
- package/quickstart/trace/blobs/f7/f7c1592306093f65f441d2afd5f496b7635b7e18f5c15f55b11f71f0b552a118 +1 -0
- package/quickstart/trace/events.jsonl +19 -0
- package/quickstart/trace/fs/HEAD +1 -0
- package/quickstart/trace/fs/config +11 -0
- package/quickstart/trace/fs/description +1 -0
- package/quickstart/trace/fs/index +0 -0
- package/quickstart/trace/fs/info/exclude +11 -0
- package/quickstart/trace/fs/objects/1b/1f956f306fe2c66611dd9e280e7fd2dce1ecda +0 -0
- package/quickstart/trace/fs/objects/1c/c77eaef0608e088283cb62876b80be9473edce +0 -0
- package/quickstart/trace/fs/objects/20/0c1f7a4be93b037ff27cea57853a738fcbf744 +0 -0
- package/quickstart/trace/fs/objects/20/28a902b70fff80fe02a8aaaf30005fd91bafb7 +0 -0
- package/quickstart/trace/fs/objects/34/522daa46d0e3f8db09ec7d874fa6f05cbcdabf +0 -0
- package/quickstart/trace/fs/objects/48/a7bdafdaae1c53a354c7bd06c740719e1cb255 +0 -0
- package/quickstart/trace/fs/objects/80/13b71377c59b728861dd8eec39a919387a4b81 +0 -0
- package/quickstart/trace/fs/objects/9f/db6ace4a4410e535a2aef50e653e46a4c3694a +0 -0
- package/quickstart/trace/fs/objects/a4/371e26fb48bece62891a54e4374895fba69f0d +0 -0
- package/quickstart/trace/fs/objects/d3/3088476a7db5aef2dfd6c839d16d2b2ccaca40 +0 -0
- package/quickstart/trace/manifest.json +43 -0
- package/quickstart/trace/redactions.json +4 -0
- package/quickstart/trace/shell-frames.jsonl +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { cp, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { parseArgs } from '../args.js';
|
|
7
|
+
import { Output } from '../out.js';
|
|
8
|
+
import { replayCommand } from './replay.js';
|
|
9
|
+
import { showCommand } from './inspect.js';
|
|
10
|
+
/**
|
|
11
|
+
* The first two minutes, with nothing installed and nothing spent.
|
|
12
|
+
*
|
|
13
|
+
* Everything orca does well is visual — a timeline, a causal chain, a comparison — and none of it
|
|
14
|
+
* can be seen until a run exists. Producing one costs an agent, a key, a network and real tokens,
|
|
15
|
+
* so the shortest honest path from `npm i` to "oh, I see what this is" ran through four things a
|
|
16
|
+
* person has to arrange first. Most of them do not.
|
|
17
|
+
*
|
|
18
|
+
* So the package carries a run. A real one: a small agent, a real model, a genuine bug in
|
|
19
|
+
* timezone arithmetic that it genuinely fixes. `quickstart` puts the project and the recording
|
|
20
|
+
* somewhere, then replays the recording against the project with the network off — which is the
|
|
21
|
+
* product's own guarantee doing the work of its own demonstration.
|
|
22
|
+
*
|
|
23
|
+
* The agent it was recorded from is a Node script that ships with it, which is the reason replay
|
|
24
|
+
* needs nothing installed: orca already requires the runtime that agent runs on.
|
|
25
|
+
*
|
|
26
|
+
* The default output fits on one screen, and that is the whole design of it. The first version
|
|
27
|
+
* printed the full timeline and the replayed agent's own chatter — sixty-eight lines, with the
|
|
28
|
+
* failing test count and the passing one fifty lines apart. The comparison is the point, and a
|
|
29
|
+
* comparison you have to scroll between is not one. `--full` still prints everything.
|
|
30
|
+
*/
|
|
31
|
+
/** Where the shipped asset lives, relative to the built `dist/commands/`. */
|
|
32
|
+
const ASSET = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'quickstart');
|
|
33
|
+
const DEFAULT_DIR = 'orca-quickstart';
|
|
34
|
+
const exec = promisify(execFile);
|
|
35
|
+
async function events() {
|
|
36
|
+
const jsonl = await readFile(join(ASSET, 'trace', 'events.jsonl'), 'utf8');
|
|
37
|
+
return jsonl
|
|
38
|
+
.split('\n')
|
|
39
|
+
.filter((l) => l.trim())
|
|
40
|
+
.map((l) => JSON.parse(l));
|
|
41
|
+
}
|
|
42
|
+
/** Read the run id out of the shipped trace, so nothing has to hardcode it. */
|
|
43
|
+
async function shippedRunId() {
|
|
44
|
+
const manifest = JSON.parse(await readFile(join(ASSET, 'trace', 'manifest.json'), 'utf8'));
|
|
45
|
+
const id = manifest.run_id;
|
|
46
|
+
if (id === undefined)
|
|
47
|
+
throw new Error('the shipped quickstart trace has no run_id');
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Whether it is safe to write a project into this path.
|
|
52
|
+
*
|
|
53
|
+
* Only a missing path counts as free. Swallowing every error meant a path that exists and is not a
|
|
54
|
+
* directory read as empty, so `orca quickstart --dir notes.txt` skipped the sentence that explains
|
|
55
|
+
* the problem and surfaced `ENOTDIR: not a directory, mkdir` from three lines further on.
|
|
56
|
+
*/
|
|
57
|
+
async function isEmptyish(dir) {
|
|
58
|
+
try {
|
|
59
|
+
return (await readdir(dir)).length === 0;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return err.code === 'ENOENT';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Tell the trace where it now lives.
|
|
67
|
+
*
|
|
68
|
+
* The shipped manifest names a neutral directory, because a trace records the absolute path it was
|
|
69
|
+
* captured in and publishing one on npm would publish that. But `replay` compares that path against
|
|
70
|
+
* the directory it is running in, and on a mismatch it declines to restore the recorded filesystem
|
|
71
|
+
* — correctly, since restoring a stranger's tree over yours is not a thing to do on a guess.
|
|
72
|
+
*
|
|
73
|
+
* Left alone, that lands on the first person who follows the tour. `orca quickstart` replays with
|
|
74
|
+
* `--in-place` and is unaffected, but the obvious next thing to type is the command from the top of
|
|
75
|
+
* the README, and `orca replay last` in the quickstart directory reported `exact=1` with two
|
|
76
|
+
* divergences and a warning — the flagship guarantee failing on the second look, against a tree the
|
|
77
|
+
* first replay had already changed.
|
|
78
|
+
*
|
|
79
|
+
* So the path is rewritten as the trace is placed. The asset on npm stays neutral, and the copy on
|
|
80
|
+
* disk is honest about where it is. Safe to edit because the integrity digest covers
|
|
81
|
+
* `events.jsonl`, and the replay matcher reads request bodies — neither reads this field.
|
|
82
|
+
*/
|
|
83
|
+
async function adoptTrace(runDir, target) {
|
|
84
|
+
const path = join(runDir, 'manifest.json');
|
|
85
|
+
const manifest = JSON.parse(await readFile(path, 'utf8'));
|
|
86
|
+
manifest['cwd'] = target;
|
|
87
|
+
await writeFile(path, `${JSON.stringify(manifest, null, 2)}
|
|
88
|
+
`, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The project's own test suite, summarised.
|
|
92
|
+
*
|
|
93
|
+
* Run before and after, because the arc is the demo: two tests fail, a recording of an agent
|
|
94
|
+
* fixing them is replayed with the network off, and the same two pass. Telling someone to go and
|
|
95
|
+
* check afterwards does not work — by then the replay has already written the fix, so the
|
|
96
|
+
* instruction to "see two failures" would show them four passes.
|
|
97
|
+
*/
|
|
98
|
+
async function testSummary(dir) {
|
|
99
|
+
let tap = '';
|
|
100
|
+
try {
|
|
101
|
+
// The reporter is named, and the environment that could name a second one is taken away.
|
|
102
|
+
//
|
|
103
|
+
// The counts are read out of TAP, and node picks its reporter from the ambient default and
|
|
104
|
+
// from `NODE_OPTIONS`. A developer with `--test-reporter=spec` set — an ordinary thing to have
|
|
105
|
+
// — got `? passing, ? failing` for both halves of the before-and-after, from a command that
|
|
106
|
+
// still reported success. Naming `tap` on the command line does not fix that on its own:
|
|
107
|
+
// reporters accumulate, and two of them with one destination makes node refuse to start at
|
|
108
|
+
// all. So the variable goes, which is right regardless — this is orca running a fixed test
|
|
109
|
+
// file of its own to read two numbers out of it, not the user running their suite.
|
|
110
|
+
({ stdout: tap } = await exec(process.execPath, ['--test', '--test-reporter=tap', 'test/schedule.test.js'], { cwd: dir, env: withoutNodeOptions(process.env) }));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
tap = err.stdout ?? '';
|
|
114
|
+
}
|
|
115
|
+
const pass = /^# pass (\d+)/m.exec(tap)?.[1] ?? '?';
|
|
116
|
+
const fail = /^# fail (\d+)/m.exec(tap)?.[1] ?? '?';
|
|
117
|
+
const failing = tap
|
|
118
|
+
.split('\n')
|
|
119
|
+
.filter((l) => l.startsWith('not ok'))
|
|
120
|
+
.map((l) => l.replace(/^not ok \d+ - /, ''));
|
|
121
|
+
return { line: `${pass} passing, ${fail} failing`, failing };
|
|
122
|
+
}
|
|
123
|
+
/** A copy of the environment with `NODE_OPTIONS` removed. */
|
|
124
|
+
function withoutNodeOptions(env) {
|
|
125
|
+
const copy = { ...env };
|
|
126
|
+
delete copy['NODE_OPTIONS'];
|
|
127
|
+
return copy;
|
|
128
|
+
}
|
|
129
|
+
/** What the recording did, in three lines rather than nineteen. */
|
|
130
|
+
export function recordingSummary(all) {
|
|
131
|
+
const turns = all.filter((e) => e.type === 'model.response');
|
|
132
|
+
const model = String(turns[0]?.attrs?.['model'] ?? 'a model').replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
133
|
+
const tokensIn = turns.reduce((n, e) => n + Number(e.attrs?.['input_tokens'] ?? 0), 0);
|
|
134
|
+
const tokensOut = turns.reduce((n, e) => n + Number(e.attrs?.['output_tokens'] ?? 0), 0);
|
|
135
|
+
const read = new Set();
|
|
136
|
+
for (const e of all) {
|
|
137
|
+
if (e.type !== 'tool.call')
|
|
138
|
+
continue;
|
|
139
|
+
const input = e.attrs?.['input'];
|
|
140
|
+
if (e.attrs?.['name'] === 'read_file' && input?.path)
|
|
141
|
+
read.add(input.path);
|
|
142
|
+
}
|
|
143
|
+
// Straight off each event. Reading the path out and then looking the event back up gives two
|
|
144
|
+
// changes to one file the *first* one's counts twice, which is a summary that quietly lies.
|
|
145
|
+
const wrote = all
|
|
146
|
+
.filter((e) => e.type === 'fs.change')
|
|
147
|
+
.map((e) => {
|
|
148
|
+
const a = e.attrs ?? {};
|
|
149
|
+
return `${String(a['path'])} (+${a['insertions']} −${a['deletions']})`;
|
|
150
|
+
});
|
|
151
|
+
const lines = [
|
|
152
|
+
`${all.length} events · ${turns.length} model turns · ${model} · ${tokensIn.toLocaleString()} in · ${tokensOut.toLocaleString()} out`,
|
|
153
|
+
];
|
|
154
|
+
if (read.size > 0)
|
|
155
|
+
lines.push(`read ${[...read].join(', ')}`);
|
|
156
|
+
if (wrote.length > 0)
|
|
157
|
+
lines.push(`wrote ${wrote.join(', ')}`);
|
|
158
|
+
return lines;
|
|
159
|
+
}
|
|
160
|
+
/** Files the recording changed on disk. */
|
|
161
|
+
function writtenPaths(all) {
|
|
162
|
+
return all
|
|
163
|
+
.filter((e) => e.type === 'fs.change')
|
|
164
|
+
.map((e) => String(e.attrs?.['path']))
|
|
165
|
+
.filter((p) => p !== 'undefined');
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* What the model said, from the recording rather than from the replayed process.
|
|
169
|
+
*
|
|
170
|
+
* Trimmed to its first sentences and labelled. Unlabelled, eleven lines of an assistant explaining
|
|
171
|
+
* daylight saving read as orca talking — and the recorded reply ends with "If you want, I can also
|
|
172
|
+
* explain the DST edge case in more detail", which in an offline replay invites the obvious
|
|
173
|
+
* question of who it is addressed to.
|
|
174
|
+
*/
|
|
175
|
+
export function modelSaid(all, lines) {
|
|
176
|
+
const alreadySaid = writtenPaths(all);
|
|
177
|
+
for (const e of [...all].reverse()) {
|
|
178
|
+
if (e.type !== 'model.response' || !e.payload)
|
|
179
|
+
continue;
|
|
180
|
+
try {
|
|
181
|
+
const body = JSON.parse(e.payload);
|
|
182
|
+
const text = body.choices?.[0]?.message?.content;
|
|
183
|
+
if (!text)
|
|
184
|
+
continue;
|
|
185
|
+
const kept = text
|
|
186
|
+
.split('\n')
|
|
187
|
+
.map((l) => l.trim())
|
|
188
|
+
.filter(Boolean)
|
|
189
|
+
// Two lines of excerpt is no place to repeat what the summary three lines above already
|
|
190
|
+
// said. A section header on its own carries nothing, and "Fixed src/schedule.js." is the
|
|
191
|
+
// `wrote src/schedule.js` line again — the excerpt is here for the why, not the what.
|
|
192
|
+
.filter((l) => lines > 4 || (!l.endsWith(':') && !restatesTheSummary(l, alreadySaid)))
|
|
193
|
+
.map((l) => (lines > 4 ? l : plain(l)));
|
|
194
|
+
return kept.slice(0, lines);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Whether a line only names a file the summary has already reported as written.
|
|
204
|
+
*
|
|
205
|
+
* Short and path-shaped: "Fixed `src/schedule.js`." carries nothing the `wrote` line above did not,
|
|
206
|
+
* and length is what separates it from a sentence that happens to mention the same file while
|
|
207
|
+
* saying something about it.
|
|
208
|
+
*/
|
|
209
|
+
function restatesTheSummary(line, written) {
|
|
210
|
+
const flat = plain(line);
|
|
211
|
+
return flat.length < 60 && written.some((p) => flat.includes(p));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The excerpt as it is printed: a label and its lines, or nothing at all.
|
|
215
|
+
*
|
|
216
|
+
* Nothing at all is a real outcome, not a defensive branch. The lines are filtered — section
|
|
217
|
+
* headers dropped, restatements of the file the summary already reported dropped — so a reply made
|
|
218
|
+
* entirely of those leaves the label standing over empty space, which reads as output that went
|
|
219
|
+
* missing rather than as output that was never worth printing.
|
|
220
|
+
*/
|
|
221
|
+
export function excerptBlock(all, lines) {
|
|
222
|
+
const said = modelSaid(all, lines);
|
|
223
|
+
if (said.length === 0)
|
|
224
|
+
return [];
|
|
225
|
+
return [
|
|
226
|
+
' the model, out of the recording:',
|
|
227
|
+
...said.map((l) => ` ${l}`),
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
/** Markdown emphasis and bullet syntax removed, clipped to something a terminal can hold. */
|
|
231
|
+
function plain(line) {
|
|
232
|
+
const flat = line
|
|
233
|
+
.replace(/^[-*]\s+/, '')
|
|
234
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
235
|
+
.replace(/`(.+?)`/g, '$1');
|
|
236
|
+
return flat.length > 84 ? `${flat.slice(0, 83)}…` : flat;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Whether the replay demonstrated the thing, rather than merely failing to fail.
|
|
240
|
+
*
|
|
241
|
+
* `turns > 0` is the clause that is easy to leave out and is the whole point. Without it an empty
|
|
242
|
+
* recording makes `0 === 0` true, and the command congratulates itself on a run that never
|
|
243
|
+
* happened — which is what it did, over a project whose tests were still red, until a directory
|
|
244
|
+
* with a space in its name stopped the agent from starting at all.
|
|
245
|
+
*/
|
|
246
|
+
export function replayDemonstrated(turns, served, unmatched) {
|
|
247
|
+
return turns > 0 && served === turns && unmatched === 0;
|
|
248
|
+
}
|
|
249
|
+
export async function quickstartCommand(args, out, cwd = process.cwd()) {
|
|
250
|
+
const target = resolve(cwd, args.str('dir') ?? DEFAULT_DIR);
|
|
251
|
+
const full = args.bool('full');
|
|
252
|
+
const runId = await shippedRunId();
|
|
253
|
+
// Refusing rather than merging: this writes a source tree and a trace, and quietly mixing those
|
|
254
|
+
// into a directory someone already has work in is the kind of help nobody asks for twice.
|
|
255
|
+
if (!(await isEmptyish(target))) {
|
|
256
|
+
throw new Error(`${target} already exists and is not an empty directory\n` +
|
|
257
|
+
' pick somewhere else: orca quickstart --dir <path>');
|
|
258
|
+
}
|
|
259
|
+
await mkdir(join(target, '.orca', 'runs'), { recursive: true });
|
|
260
|
+
await cp(join(ASSET, 'project'), target, { recursive: true });
|
|
261
|
+
const runDir = join(target, '.orca', 'runs', runId);
|
|
262
|
+
await cp(join(ASSET, 'trace'), runDir, { recursive: true });
|
|
263
|
+
await adoptTrace(runDir, target);
|
|
264
|
+
const all = await events();
|
|
265
|
+
const before = await testSummary(target);
|
|
266
|
+
out.info('quickstart.ready', { dir: target, run: runId });
|
|
267
|
+
out.plain('');
|
|
268
|
+
out.plain(' A reminder scheduler with a bug in it, and a recording of an agent fixing it.');
|
|
269
|
+
out.plain(' Nothing here needs a key, a network, or an agent installed.');
|
|
270
|
+
out.plain('');
|
|
271
|
+
out.plain(` Before ${before.line}`);
|
|
272
|
+
for (const f of before.failing)
|
|
273
|
+
out.plain(` ✗ ${f}`);
|
|
274
|
+
out.plain('');
|
|
275
|
+
const summary = recordingSummary(all);
|
|
276
|
+
out.plain(` Recorded ${summary[0]}`);
|
|
277
|
+
for (const l of summary.slice(1))
|
|
278
|
+
out.plain(` ${l}`);
|
|
279
|
+
out.plain('');
|
|
280
|
+
const replayed = await runReplay(target, runId, out, full);
|
|
281
|
+
// The denominator is the recording's, not the replay's. `matchedExact + unmatched` is the number
|
|
282
|
+
// of turns the replay was asked about, which is zero when the agent never started — and `0/0`
|
|
283
|
+
// reads like a pass. Counting the model turns in the trace makes that case print `0/3`.
|
|
284
|
+
const turns = all.filter((e) => e.type === 'model.response').length;
|
|
285
|
+
const ok = replayDemonstrated(turns, replayed.matchedExact, replayed.unmatched);
|
|
286
|
+
if (full)
|
|
287
|
+
out.plain('');
|
|
288
|
+
out.plain(` Replayed ${replayed.matchedExact}/${turns} turns from the trace · ` +
|
|
289
|
+
`${replayed.divergences} divergences · ${replayed.liveCalls} live calls · network blocked`);
|
|
290
|
+
out.plain('');
|
|
291
|
+
const excerpt = excerptBlock(all, full ? 40 : 2);
|
|
292
|
+
if (excerpt.length > 0) {
|
|
293
|
+
for (const line of excerpt)
|
|
294
|
+
out.plain(line);
|
|
295
|
+
out.plain('');
|
|
296
|
+
}
|
|
297
|
+
const after = await testSummary(target);
|
|
298
|
+
out.plain(` After ${after.line}`);
|
|
299
|
+
out.plain('');
|
|
300
|
+
// Said rather than hidden, and reflected in the exit code: a quickstart that quietly reports a
|
|
301
|
+
// success it did not have teaches the opposite of the guarantee it exists to demonstrate.
|
|
302
|
+
if (ok) {
|
|
303
|
+
out.plain(' Nothing above talked to a model. The fix came out of the recording.');
|
|
304
|
+
}
|
|
305
|
+
else if (replayed.matchedExact === 0) {
|
|
306
|
+
out.plain(` The replay served none of the ${turns} recorded turns — the agent did not run.`);
|
|
307
|
+
out.plain(' Re-run with --full to see what it printed.');
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
out.plain(` ${replayed.unmatched} turn(s) could not be served from the recording.`);
|
|
311
|
+
}
|
|
312
|
+
out.plain('');
|
|
313
|
+
out.plain(` cd ${target}`);
|
|
314
|
+
out.plain('');
|
|
315
|
+
// `orca` is on PATH only after a global install. Saying so here rather than letting someone
|
|
316
|
+
// meet `command not found` as the next thing that happens to them.
|
|
317
|
+
out.plain(' See it orca ui # the timeline, in a browser');
|
|
318
|
+
out.plain(' Dig in orca show · orca graph · orca export -o run.html');
|
|
319
|
+
out.plain(' Your own orca record claude -- -p "..."');
|
|
320
|
+
out.plain('');
|
|
321
|
+
out.plain(' Installed locally rather than with -g? Put npx in front: npx orca ui');
|
|
322
|
+
if (!full)
|
|
323
|
+
out.plain(' --full for the whole timeline and the replay as it happened.');
|
|
324
|
+
return {
|
|
325
|
+
dir: target,
|
|
326
|
+
runId,
|
|
327
|
+
unmatched: replayed.unmatched,
|
|
328
|
+
turns,
|
|
329
|
+
served: replayed.matchedExact,
|
|
330
|
+
ok,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Replay the shipped run, printing everything or nothing.
|
|
335
|
+
*
|
|
336
|
+
* `--full` calls the command in process, so the timeline and the replayed agent's own output land
|
|
337
|
+
* on the terminal exactly as they would from `orca replay`.
|
|
338
|
+
*
|
|
339
|
+
* The default passes `--json`, which is what `replay` already keys its quiet mode on: the agent is
|
|
340
|
+
* spawned with inherited stdio, so under anything else its output reaches the terminal without
|
|
341
|
+
* passing through something orca could filter. The result document goes to a discarded `Output`,
|
|
342
|
+
* and this command prints the two numbers worth reading instead.
|
|
343
|
+
*
|
|
344
|
+
* Not a child process. Spawning `../cli.js` resolves from `dist` and not from source, which is a
|
|
345
|
+
* split between how the tests run this and how a user does — and a path that only works in one of
|
|
346
|
+
* those is the shape of bug this project keeps finding in other people's harnesses.
|
|
347
|
+
*/
|
|
348
|
+
async function runReplay(target, runId, out, full) {
|
|
349
|
+
if (full) {
|
|
350
|
+
out.plain('── the recording ──────────────────────────────────');
|
|
351
|
+
out.plain('');
|
|
352
|
+
await showCommand(parseArgs(['show', runId]), out, target);
|
|
353
|
+
out.plain('');
|
|
354
|
+
out.plain('── replaying it, offline ──────────────────────────');
|
|
355
|
+
out.plain('');
|
|
356
|
+
const result = await replayCommand(parseArgs(['replay', runId, '--in-place']), out, target);
|
|
357
|
+
return {
|
|
358
|
+
matchedExact: result.matchedExact,
|
|
359
|
+
unmatched: result.unmatched,
|
|
360
|
+
divergences: result.divergences,
|
|
361
|
+
liveCalls: result.liveCalls,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const quiet = new Output({ write: () => { }, color: false, ci: true });
|
|
365
|
+
const result = await replayCommand(parseArgs(['replay', runId, '--in-place', '--json', '--quiet']), quiet, target);
|
|
366
|
+
return {
|
|
367
|
+
matchedExact: result.matchedExact,
|
|
368
|
+
unmatched: result.unmatched,
|
|
369
|
+
divergences: result.divergences,
|
|
370
|
+
liveCalls: result.liveCalls,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
//# sourceMappingURL=quickstart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quickstart.js","sourceRoot":"","sources":["../../src/commands/quickstart.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,4 +14,17 @@ export interface RecordResult {
|
|
|
14
14
|
exitCode: number;
|
|
15
15
|
}
|
|
16
16
|
export declare function recordCommand(args: ParsedArgs, out: Output, cwd?: string): Promise<RecordResult>;
|
|
17
|
+
/**
|
|
18
|
+
* The origins a configured upstream map actually names, deduplicated and safe to print.
|
|
19
|
+
*
|
|
20
|
+
* `resolveUpstream` writes one entry per dialect and `openai` and `openai-responses` always share
|
|
21
|
+
* a value — so the raw map renders a single gateway three times. What a reader wants is the set of
|
|
22
|
+
* places traffic can go, which is usually one.
|
|
23
|
+
*
|
|
24
|
+
* Through `recordableOrigin` for the same reason the trace goes through it, and against the same
|
|
25
|
+
* sentence: `orca setup --gateway` accepts a URL carrying the key — `https://user:pw@gw.example`,
|
|
26
|
+
* `https://gw.example?key=…` — and `config.ts` says of that file that "nothing ever prints it
|
|
27
|
+
* back". A line on the terminal is printing it back, and it is a line that ends up in CI logs.
|
|
28
|
+
*/
|
|
29
|
+
export declare function distinctOrigins(upstream: Record<string, string> | undefined): string[];
|
|
17
30
|
//# sourceMappingURL=record.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/commands/record.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/commands/record.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAK7C,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AA0BD,wBAAsB,aAAa,CACjC,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,SAAgB,GAClB,OAAO,CAAC,YAAY,CAAC,CAUvB;AA+mBD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,MAAM,EAAE,CActF"}
|
package/dist/commands/record.js
CHANGED
|
@@ -3,14 +3,14 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { delimiter, resolve } from 'node:path';
|
|
4
4
|
import { TraceWriter, ensureRunsDir } from '@orcareplay/core';
|
|
5
5
|
import { FsCapture } from '@orcareplay/fs-capture';
|
|
6
|
-
import { createProxy, RunCa } from '@orcareplay/proxy';
|
|
6
|
+
import { createProxy, unusableOrigin, recordableOrigin, RunCa, } from '@orcareplay/proxy';
|
|
7
7
|
import { captureSession, defaultAdapters, resolveLaunch, snapshotDir } from '@orcareplay/adapters';
|
|
8
8
|
import { ExchangeEventDeriver, appendDerivedEvents } from '../exchange-events.js';
|
|
9
9
|
import { installShellShim, readShellFrames } from '@orcareplay/shell-shim';
|
|
10
10
|
import { drainMcpFrames, pointAtMcpConfig, setupMcpCapture } from '../mcp.js';
|
|
11
11
|
import { SerialQueue } from '../serial.js';
|
|
12
12
|
import { appendSnapshot } from '../fs-events.js';
|
|
13
|
-
import { persistNetExchange, setupTlsCapture, trustRunCa } from '../tls-capture.js';
|
|
13
|
+
import { persistNetExchange, planTlsCapture, setupTlsCapture, trustRunCa } from '../tls-capture.js';
|
|
14
14
|
import { upstreamPlan } from '../upstream.js';
|
|
15
15
|
import { ORCA_VERSION } from '../version.js';
|
|
16
16
|
/**
|
|
@@ -56,6 +56,23 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
56
56
|
if (agentName) {
|
|
57
57
|
adapter = registry.get(agentName);
|
|
58
58
|
}
|
|
59
|
+
else if (args.passthrough.length > 0) {
|
|
60
|
+
// A command after `--` is not a hint about the environment; it is the answer. Detection is
|
|
61
|
+
// machine-wide by construction — every adapter's is `detectAgent(binaries, homePaths)`, which
|
|
62
|
+
// asks whether a binary is on PATH or a directory exists under `$HOME`, and neither is a fact
|
|
63
|
+
// about this directory. So on a machine with Claude Code installed,
|
|
64
|
+
// `orca record -- python my_graph.py` detected `claude-code` and launched *that*, handing it
|
|
65
|
+
// `python my_graph.py` as arguments. The run recorded, so the failure looked like a bad key
|
|
66
|
+
// rather than like orca having started the wrong program.
|
|
67
|
+
//
|
|
68
|
+
// `generic-openai` rather than `exec`: both decline to guess what the command is, but only one
|
|
69
|
+
// of them redirects anything. `exec` points the agent nowhere and warns that interception is
|
|
70
|
+
// required — correct for a Go binary with its origin compiled in, and wrong as a default,
|
|
71
|
+
// because most things run this way read a base-URL variable and would have been captured by
|
|
72
|
+
// setting the three that `generic-openai` sets. Sending everyone to `--tls-intercept` to record
|
|
73
|
+
// a Python script is a worse answer than the bug this replaces.
|
|
74
|
+
adapter = registry.get('generic-openai');
|
|
75
|
+
}
|
|
59
76
|
else {
|
|
60
77
|
adapter = await registry.detect(cwd);
|
|
61
78
|
if (!adapter) {
|
|
@@ -65,6 +82,15 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
65
82
|
}
|
|
66
83
|
out.info('adapter.detected', { id: adapter.id });
|
|
67
84
|
}
|
|
85
|
+
// Before the run directory exists, because both of these refuse: `upstreamPlan` an upstream that
|
|
86
|
+
// is not an origin, `planTlsCapture` a `--tls-hosts` list that names `*` or contradicts itself
|
|
87
|
+
// and an `ORCA_TLS_UPSTREAM_CA` that cannot be read. Resolved after it, a typo in any of them
|
|
88
|
+
// left an empty run behind for `orca list` to show. Neither depends on more than the arguments,
|
|
89
|
+
// the environment and a file the run was going to read anyway, so there is no reason for either
|
|
90
|
+
// to run later — and `planTlsCapture` is the refusing half of `setupTlsCapture` alone, so the
|
|
91
|
+
// certificate authority is still minted below, once the run has a directory to mint it into.
|
|
92
|
+
const plan = await upstreamPlan(args);
|
|
93
|
+
await planTlsCapture(args);
|
|
68
94
|
const dir = await ensureRunsDir(cwd);
|
|
69
95
|
const writer = await TraceWriter.create(dir, {
|
|
70
96
|
adapter: { id: adapter.id, version: ORCA_VERSION, harness_version: adapter.harnessVersions },
|
|
@@ -131,7 +157,6 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
131
157
|
// Serial, not parallel: each persist snapshots the workspace, and two overlapping `git add`
|
|
132
158
|
// calls collide on index.lock. It also keeps seq in the order exchanges actually happened.
|
|
133
159
|
const writes = new SerialQueue();
|
|
134
|
-
const plan = await upstreamPlan(args);
|
|
135
160
|
/**
|
|
136
161
|
* TLS interception, off unless the flag is present.
|
|
137
162
|
*
|
|
@@ -172,6 +197,17 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
172
197
|
let commandToolCalls = 0;
|
|
173
198
|
/** Commands the shim actually saw. Zero against a non-zero `commandToolCalls` is the warning. */
|
|
174
199
|
let shellFrames = 0;
|
|
200
|
+
/**
|
|
201
|
+
* Exchanges the upstream answered with an error status.
|
|
202
|
+
*
|
|
203
|
+
* Counted because a run in which every model call failed is indistinguishable, from the summary
|
|
204
|
+
* line alone, from one that worked: the harness retries, gives up, exits 0, and orca prints
|
|
205
|
+
* `recorded … exit=0` over a trace whose every answer is a 503. That happened against a gateway
|
|
206
|
+
* whose credentials had lapsed, and nothing in the output said so.
|
|
207
|
+
*/
|
|
208
|
+
let erroredExchanges = 0;
|
|
209
|
+
/** The status those errors carried, so the warning can name it rather than describe it. */
|
|
210
|
+
const errorStatuses = new Map();
|
|
175
211
|
const proxy = await createProxy({
|
|
176
212
|
mode: 'record',
|
|
177
213
|
upstream: plan.upstream,
|
|
@@ -179,6 +215,10 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
179
215
|
upstreamHeadersOrigin: plan.headersOrigin,
|
|
180
216
|
onExchange: (exchange) => {
|
|
181
217
|
modelExchanges += 1;
|
|
218
|
+
if (exchange.status >= 400) {
|
|
219
|
+
erroredExchanges += 1;
|
|
220
|
+
errorStatuses.set(exchange.status, (errorStatuses.get(exchange.status) ?? 0) + 1);
|
|
221
|
+
}
|
|
182
222
|
writes.push(() => persist(exchange));
|
|
183
223
|
},
|
|
184
224
|
// A POST on a path no dialect claims is forwarded rather than refused, and lands in the trace
|
|
@@ -290,6 +330,17 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
290
330
|
run: writer.runId,
|
|
291
331
|
adapter: adapter.id,
|
|
292
332
|
proxy: proxy.url,
|
|
333
|
+
// `proxy` is orca's own address, so the line said where the agent would call and never where
|
|
334
|
+
// the call would go on to. A gateway left behind in `~/.orca/config.json` redirects every run
|
|
335
|
+
// on the machine, and the first sign of it used to be an answer from a model nobody asked for.
|
|
336
|
+
//
|
|
337
|
+
// Only when something was configured. Left alone, each dialect has its own vendor default and
|
|
338
|
+
// there is no single value to print — and `upstream=default` on every ordinary run is noise
|
|
339
|
+
// that would teach people to stop reading the line. The trace records the origin either way,
|
|
340
|
+
// per exchange, because that is where it is actually decided.
|
|
341
|
+
...(distinctOrigins(plan.upstream).length > 0
|
|
342
|
+
? { upstream: distinctOrigins(plan.upstream).join(',') }
|
|
343
|
+
: {}),
|
|
293
344
|
fs: fs ? 'on' : 'off',
|
|
294
345
|
shell: shell ? 'on' : 'off',
|
|
295
346
|
});
|
|
@@ -430,16 +481,49 @@ async function runRecording(args, out, cwd, minted) {
|
|
|
430
481
|
* the adapters; this guards the run, which is where a user actually meets it.
|
|
431
482
|
*/
|
|
432
483
|
if (modelExchanges === 0) {
|
|
484
|
+
const baseUrls = Object.keys(launch.env)
|
|
485
|
+
.filter((name) => /(?:_BASE_URL|_API_BASE)$/.test(name))
|
|
486
|
+
.sort();
|
|
487
|
+
// Which cause is worth naming depends on how this adapter captures at all, and the line
|
|
488
|
+
// already carries the evidence: `set=none` means there was never a variable to be ignored.
|
|
489
|
+
// Printing "it may not read a base-URL variable" next to `set=none` contradicts the run's own
|
|
490
|
+
// output and sends the reader after a route that was never the route -- which is how the
|
|
491
|
+
// capture bug fixed earlier in this branch stayed hidden through a dozen rounds of debugging.
|
|
492
|
+
const cause = baseUrls.length > 0
|
|
493
|
+
? 'the agent never called the proxy — it may not read a base-URL variable'
|
|
494
|
+
: tls.ca
|
|
495
|
+
? 'this adapter captures at the transport, and nothing orca decrypted looked like a model call'
|
|
496
|
+
: 'this adapter captures at the transport, and interception was not on';
|
|
433
497
|
out.warn('capture.empty', {
|
|
434
498
|
exchanges: 0,
|
|
435
|
-
cause
|
|
436
|
-
set:
|
|
437
|
-
.filter((name) => /(?:_BASE_URL|_API_BASE)$/.test(name))
|
|
438
|
-
.sort()
|
|
439
|
-
.join(',') || 'none',
|
|
499
|
+
cause,
|
|
500
|
+
set: baseUrls.join(',') || 'none',
|
|
440
501
|
next: 'orca doctor',
|
|
441
502
|
});
|
|
442
503
|
}
|
|
504
|
+
/**
|
|
505
|
+
* Every model call came back an error.
|
|
506
|
+
*
|
|
507
|
+
* `capture.empty` covers the run the proxy never saw. This covers the run it saw all of, where
|
|
508
|
+
* every answer was a refusal: an expired gateway credential, a model the account cannot reach, a
|
|
509
|
+
* provider outage. The harness retries, exhausts its attempts, prints its own error and exits 0,
|
|
510
|
+
* so the summary reads exactly like a successful recording — `recorded events=27 exit=0` — over
|
|
511
|
+
* a trace that contains no model output at all. Replaying it faithfully reproduces the failures.
|
|
512
|
+
*
|
|
513
|
+
* Only when *all* of them failed. A run that retried once and then worked is a run that worked,
|
|
514
|
+
* and warning about it would train people to ignore the line that matters.
|
|
515
|
+
*/
|
|
516
|
+
if (modelExchanges > 0 && erroredExchanges === modelExchanges) {
|
|
517
|
+
const commonest = [...errorStatuses.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
518
|
+
out.warn('capture.errors', {
|
|
519
|
+
exchanges: modelExchanges,
|
|
520
|
+
errors: erroredExchanges,
|
|
521
|
+
status: commonest ? commonest[0] : 'unknown',
|
|
522
|
+
cause: 'every model call the proxy forwarded came back an error',
|
|
523
|
+
effect: 'the trace holds the failures, not any model output — a replay reproduces those',
|
|
524
|
+
next: 'orca show to read the errors, then check the upstream credential or model id',
|
|
525
|
+
});
|
|
526
|
+
}
|
|
443
527
|
/**
|
|
444
528
|
* Shell capture that was on and recorded nothing.
|
|
445
529
|
*
|
|
@@ -535,4 +619,31 @@ async function runChild(command, argv, env, cwd, quietStdout = false) {
|
|
|
535
619
|
});
|
|
536
620
|
});
|
|
537
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* The origins a configured upstream map actually names, deduplicated and safe to print.
|
|
624
|
+
*
|
|
625
|
+
* `resolveUpstream` writes one entry per dialect and `openai` and `openai-responses` always share
|
|
626
|
+
* a value — so the raw map renders a single gateway three times. What a reader wants is the set of
|
|
627
|
+
* places traffic can go, which is usually one.
|
|
628
|
+
*
|
|
629
|
+
* Through `recordableOrigin` for the same reason the trace goes through it, and against the same
|
|
630
|
+
* sentence: `orca setup --gateway` accepts a URL carrying the key — `https://user:pw@gw.example`,
|
|
631
|
+
* `https://gw.example?key=…` — and `config.ts` says of that file that "nothing ever prints it
|
|
632
|
+
* back". A line on the terminal is printing it back, and it is a line that ends up in CI logs.
|
|
633
|
+
*/
|
|
634
|
+
export function distinctOrigins(upstream) {
|
|
635
|
+
const safe = Object.values(upstream ?? {})
|
|
636
|
+
// Dropped only where sanitising cannot work, so a gateway configured with its key in the URL
|
|
637
|
+
// still says *where* the traffic went. `recordableOrigin` removes userinfo and query, which is
|
|
638
|
+
// enough for every `http(s)://` value — but a scheme-less URL parses with its username as the
|
|
639
|
+
// protocol, so it comes back as `myuser://PASSWORD@gw.example/v1`, credential intact, and a
|
|
640
|
+
// filter keeping everything that is not `undefined` kept it. There is no sanitising that: with
|
|
641
|
+
// no scheme there is no telling which half of `a:b` was meant as the host. `upstreamPlan`
|
|
642
|
+
// refuses these before a run starts; this is here because the function is exported and one
|
|
643
|
+
// line from a terminal.
|
|
644
|
+
.filter((origin) => unusableOrigin(origin) === undefined)
|
|
645
|
+
.map(recordableOrigin)
|
|
646
|
+
.filter((origin) => origin !== undefined);
|
|
647
|
+
return [...new Set(safe)];
|
|
648
|
+
}
|
|
538
649
|
//# sourceMappingURL=record.js.map
|