@basein/runner 0.2.11 → 0.3.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/README.md +540 -350
- package/dist/auth/client.d.ts +65 -3
- package/dist/auth/client.js +305 -53
- package/dist/bin/bir-hooks.d.ts +72 -31
- package/dist/bin/bir-hooks.js +176 -284
- package/dist/bin/bir-proxy.d.ts +17 -2
- package/dist/bin/bir-proxy.js +222 -28
- package/dist/bin/bir-scenario.d.ts +61 -10
- package/dist/bin/bir-scenario.js +704 -135
- package/dist/bin/bir.d.ts +14 -8
- package/dist/bin/bir.js +2937 -488
- package/dist/bin/investigate.js +41 -16
- package/dist/bin/scenario-edit.d.ts +43 -10
- package/dist/bin/scenario-edit.js +281 -80
- package/dist/bin/scenario-liveness.d.ts +194 -0
- package/dist/bin/scenario-liveness.js +459 -0
- package/dist/bin/scenario-stderr.d.ts +43 -0
- package/dist/bin/scenario-stderr.js +76 -0
- package/dist/bin/service-read.d.ts +88 -0
- package/dist/bin/service-read.js +128 -0
- package/dist/bin/setup.d.ts +19 -0
- package/dist/bin/setup.js +146 -20
- package/dist/config/adapters/claude-code.d.ts +92 -14
- package/dist/config/adapters/claude-code.js +291 -101
- package/dist/config/adapters/generic.d.ts +4 -1
- package/dist/config/adapters/generic.js +13 -14
- package/dist/config/generate.d.ts +392 -14
- package/dist/config/generate.js +666 -19
- package/dist/config/mcp-approval.d.ts +76 -0
- package/dist/config/mcp-approval.js +210 -0
- package/dist/config/node-path.d.ts +42 -0
- package/dist/config/node-path.js +102 -0
- package/dist/config/overrides.d.ts +62 -0
- package/dist/config/overrides.js +136 -0
- package/dist/config/resolve.d.ts +70 -8
- package/dist/config/resolve.js +186 -15
- package/dist/control/client.d.ts +22 -0
- package/dist/control/client.js +26 -7
- package/dist/control/correlation.d.ts +23 -5
- package/dist/control/correlation.js +0 -0
- package/dist/control/daemon.d.ts +400 -18
- package/dist/control/daemon.js +1105 -102
- package/dist/control/discovery.d.ts +77 -5
- package/dist/control/discovery.js +186 -11
- package/dist/control/ensure-hook.d.ts +61 -0
- package/dist/control/ensure-hook.js +160 -8
- package/dist/control/paths.d.ts +26 -0
- package/dist/control/paths.js +34 -0
- package/dist/control/pin-guard.d.ts +83 -0
- package/dist/control/pin-guard.js +534 -0
- package/dist/control/recorder-keeper.d.ts +229 -0
- package/dist/control/recorder-keeper.js +721 -0
- package/dist/control/recorder-main.d.ts +73 -0
- package/dist/control/recorder-main.js +778 -0
- package/dist/control/server.d.ts +707 -31
- package/dist/control/server.js +2803 -306
- package/dist/control/transcript.d.ts +21 -1
- package/dist/control/transcript.js +378 -112
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/jsonrpc/framing.d.ts +76 -12
- package/dist/jsonrpc/framing.js +216 -59
- package/dist/proxy/intercept.d.ts +49 -5
- package/dist/proxy/intercept.js +188 -59
- package/dist/proxy/relay.d.ts +16 -0
- package/dist/proxy/relay.js +77 -5
- package/dist/proxy/revive.d.ts +122 -0
- package/dist/proxy/revive.js +251 -0
- package/dist/proxy/session.d.ts +222 -14
- package/dist/proxy/session.js +708 -99
- package/dist/record/housekeeping.d.ts +71 -0
- package/dist/record/housekeeping.js +417 -0
- package/dist/record/recorder.d.ts +51 -3
- package/dist/record/recorder.js +3 -0
- package/dist/record/redact.d.ts +103 -9
- package/dist/record/redact.js +659 -30
- package/dist/record/remote-recorder.d.ts +327 -32
- package/dist/record/remote-recorder.js +1517 -187
- package/dist/record/switchable-recorder.d.ts +126 -0
- package/dist/record/switchable-recorder.js +351 -0
- package/dist/record/truncate.d.ts +40 -1
- package/dist/record/truncate.js +111 -4
- package/dist/replay/bash-channel.d.ts +35 -0
- package/dist/replay/bash-channel.js +133 -0
- package/dist/replay/bundle.d.ts +10 -0
- package/dist/replay/bundle.js +10 -0
- package/dist/replay/controller.d.ts +371 -20
- package/dist/replay/controller.js +1185 -111
- package/dist/replay/derive.d.ts +1 -0
- package/dist/replay/derive.js +81 -24
- package/dist/replay/executor.d.ts +51 -16
- package/dist/replay/executor.js +114 -41
- package/dist/replay/handover.d.ts +54 -1
- package/dist/replay/handover.js +55 -5
- package/dist/replay/logic.d.ts +62 -10
- package/dist/replay/logic.js +140 -22
- package/dist/replay/plan.d.ts +201 -27
- package/dist/replay/plan.js +581 -141
- package/dist/replay/safety.d.ts +72 -0
- package/dist/replay/safety.js +303 -0
- package/dist/replay/source-run.d.ts +10 -0
- package/dist/replay/source-run.js +80 -35
- package/dist/replay/targets.d.ts +15 -1
- package/dist/replay/targets.js +62 -4
- package/dist/replay/tool-error.d.ts +15 -1
- package/dist/replay/tool-error.js +33 -1
- package/dist/replay/types.d.ts +7 -4
- package/dist/upstream/client.d.ts +56 -3
- package/dist/upstream/client.js +61 -4
- package/dist/upstream/headers.d.ts +43 -0
- package/dist/upstream/headers.js +90 -0
- package/dist/upstream/http-client.d.ts +213 -22
- package/dist/upstream/http-client.js +1034 -168
- package/dist/upstream/http-transport.d.ts +143 -0
- package/dist/upstream/http-transport.js +391 -0
- package/dist/upstream/lazy-client.d.ts +8 -0
- package/dist/upstream/lazy-client.js +29 -1
- package/dist/upstream/sse.d.ts +78 -0
- package/dist/upstream/sse.js +189 -0
- package/dist/upstream/stdio-client.d.ts +61 -1
- package/dist/upstream/stdio-client.js +356 -33
- package/dist/util/atomic-write.d.ts +29 -0
- package/dist/util/atomic-write.js +96 -0
- package/dist/util/journal.d.ts +15 -0
- package/dist/util/journal.js +48 -3
- package/dist/util/log.d.ts +10 -0
- package/dist/util/log.js +28 -1
- package/dist/util/off-switch.d.ts +46 -0
- package/dist/util/off-switch.js +91 -0
- package/dist/util/service-health.d.ts +133 -0
- package/dist/util/service-health.js +217 -0
- package/docs/BaseInstRunner.md +694 -621
- package/docs/calculatedReplay.md +1625 -1244
- package/docs/calculatedReplayGuide.md +1212 -879
- package/docs/installRun.md +1013 -772
- package/docs/loginWeb.md +627 -607
- package/docs/mcpmark.md +49 -16
- package/docs/my-first-sample.md +569 -545
- package/docs/quickstart.md +384 -271
- package/docs/t-bench.md +406 -394
- package/package.json +58 -57
- package/production.md +1371 -0
- package/scripts/install.ps1 +97 -6
- package/scripts/install.sh +104 -7
|
@@ -17,7 +17,25 @@
|
|
|
17
17
|
* defensive: an unknown shape yields `""` rather than throwing. Nothing here is
|
|
18
18
|
* ever required for a recording to be valid — a missing context is a missing
|
|
19
19
|
* nicety, not a failed step.
|
|
20
|
+
*
|
|
21
|
+
* READ INCREMENTALLY, NEVER WHOLE. Every hook used to `readFileSync` the whole
|
|
22
|
+
* transcript and `JSON.parse` every line of it — on the daemon's one thread,
|
|
23
|
+
* which also answers every other session's hooks. A transcript is append-only
|
|
24
|
+
* and grows for the life of a session; with screenshots and big tool outputs
|
|
25
|
+
* it reaches tens, then hundreds of MB. Measured: ~105 ms per parse at 31 MB,
|
|
26
|
+
* ~550 ms and a 1.3 GB heap at 156 MB, paid on *every* tool call and several
|
|
27
|
+
* times per Stop, with parallel tool calls queueing behind each other.
|
|
28
|
+
*
|
|
29
|
+
* So each path keeps a {@link TranscriptIndex}: the byte offset it has folded in
|
|
30
|
+
* and the few running answers the hooks ask for (usage totals, the last
|
|
31
|
+
* assistant text, the recent messages' text and thinking, this prompt's tool
|
|
32
|
+
* results). A call reads only the bytes appended since the last one. A file
|
|
33
|
+
* that shrank, was replaced, or was rewritten in place is noticed (size, inode,
|
|
34
|
+
* birth time, mtime, and the bytes at its head and at the offset) and read
|
|
35
|
+
* again from the start, which is exactly what the old code did every time.
|
|
20
36
|
*/
|
|
37
|
+
/** Forget every cached index (tests; a caller that knows files were replaced). */
|
|
38
|
+
export declare function forgetTranscripts(): void;
|
|
21
39
|
/**
|
|
22
40
|
* The last `n` tool results of the *current* prompt, redacted and capped.
|
|
23
41
|
*
|
|
@@ -29,7 +47,9 @@
|
|
|
29
47
|
* A transcript compacted mid-turn yields whatever follows that line, which may
|
|
30
48
|
* be nothing; a transcript with no user prompt at all (a run opened by a proxy
|
|
31
49
|
* step) yields nothing. Results with `is_error: true` are kept: a failure is
|
|
32
|
-
* still the agent's evidence about what it is working on.
|
|
50
|
+
* still the agent's evidence about what it is working on. At most the last
|
|
51
|
+
* {@link MAX_RESULTS} are kept, each clipped to {@link MAX_RESULT_CHARS}
|
|
52
|
+
* characters before it is redacted and capped to `maxBytes`.
|
|
33
53
|
*/
|
|
34
54
|
export declare function recentToolResults(transcriptPath: string | undefined, n?: number, maxBytes?: number): string[];
|
|
35
55
|
/** Which rung of R-INTENT-1 a step's intent came from. */
|
|
@@ -17,32 +17,26 @@
|
|
|
17
17
|
* defensive: an unknown shape yields `""` rather than throwing. Nothing here is
|
|
18
18
|
* ever required for a recording to be valid — a missing context is a missing
|
|
19
19
|
* nicety, not a failed step.
|
|
20
|
+
*
|
|
21
|
+
* READ INCREMENTALLY, NEVER WHOLE. Every hook used to `readFileSync` the whole
|
|
22
|
+
* transcript and `JSON.parse` every line of it — on the daemon's one thread,
|
|
23
|
+
* which also answers every other session's hooks. A transcript is append-only
|
|
24
|
+
* and grows for the life of a session; with screenshots and big tool outputs
|
|
25
|
+
* it reaches tens, then hundreds of MB. Measured: ~105 ms per parse at 31 MB,
|
|
26
|
+
* ~550 ms and a 1.3 GB heap at 156 MB, paid on *every* tool call and several
|
|
27
|
+
* times per Stop, with parallel tool calls queueing behind each other.
|
|
28
|
+
*
|
|
29
|
+
* So each path keeps a {@link TranscriptIndex}: the byte offset it has folded in
|
|
30
|
+
* and the few running answers the hooks ask for (usage totals, the last
|
|
31
|
+
* assistant text, the recent messages' text and thinking, this prompt's tool
|
|
32
|
+
* results). A call reads only the bytes appended since the last one. A file
|
|
33
|
+
* that shrank, was replaced, or was rewritten in place is noticed (size, inode,
|
|
34
|
+
* birth time, mtime, and the bytes at its head and at the offset) and read
|
|
35
|
+
* again from the start, which is exactly what the old code did every time.
|
|
20
36
|
*/
|
|
21
|
-
import {
|
|
37
|
+
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
22
38
|
import { redactText } from "../record/redact.js";
|
|
23
39
|
import { truncateString } from "../record/truncate.js";
|
|
24
|
-
function readLines(transcriptPath) {
|
|
25
|
-
let raw;
|
|
26
|
-
try {
|
|
27
|
-
raw = readFileSync(transcriptPath, "utf8");
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return [];
|
|
31
|
-
}
|
|
32
|
-
const out = [];
|
|
33
|
-
for (const line of raw.split("\n")) {
|
|
34
|
-
const trimmed = line.trim();
|
|
35
|
-
if (!trimmed)
|
|
36
|
-
continue;
|
|
37
|
-
try {
|
|
38
|
-
out.push(JSON.parse(trimmed));
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
// Transcripts are appended concurrently; a half-written line is normal.
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return out;
|
|
45
|
-
}
|
|
46
40
|
function blocksOf(line) {
|
|
47
41
|
const content = line.message?.content ?? line.content;
|
|
48
42
|
if (Array.isArray(content))
|
|
@@ -69,6 +63,344 @@ function resultTextOf(block) {
|
|
|
69
63
|
return block.text;
|
|
70
64
|
return "";
|
|
71
65
|
}
|
|
66
|
+
// ── the incremental index ───────────────────────────────────────────────────
|
|
67
|
+
/** Bytes read per `readSync`. A line longer than this spans chunks. */
|
|
68
|
+
const CHUNK_BYTES = 4 * 1024 * 1024;
|
|
69
|
+
/**
|
|
70
|
+
* A line past this is skipped unparsed. Only a tool result carrying an image or
|
|
71
|
+
* a huge log gets here, and nothing read from a transcript needs more of one
|
|
72
|
+
* than its first few KB — while holding and parsing it would put a spike of
|
|
73
|
+
* that size on the daemon's heap.
|
|
74
|
+
*/
|
|
75
|
+
const MAX_LINE_BYTES = 32 * 1024 * 1024;
|
|
76
|
+
/** How many bytes of the head, and of the tail before the offset, prove the file is the same file. */
|
|
77
|
+
const PROBE_BYTES = 64;
|
|
78
|
+
/** Transcripts indexed at once. One per live session; the oldest is forgotten first. */
|
|
79
|
+
const MAX_INDEXES = 16;
|
|
80
|
+
/*
|
|
81
|
+
* RETENTION. A recorder lives for days and holds up to {@link MAX_INDEXES}
|
|
82
|
+
* indexes, so what each one keeps is bounded by what the hooks actually ask
|
|
83
|
+
* for — not by what a transcript happens to contain. Extended thinking runs
|
|
84
|
+
* to tens of KB a message and tool outputs to megabytes; kept whole, sixteen
|
|
85
|
+
* busy sessions came to hundreds of MB that were never released.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* Messages whose text and thinking are kept, newest last. A `PreToolUse` asks
|
|
89
|
+
* about the call being made *now*, whose message is at the tail — every hook
|
|
90
|
+
* folds in what was appended since the last — so a hundred is generous.
|
|
91
|
+
*/
|
|
92
|
+
const MAX_MESSAGES = 128;
|
|
93
|
+
/**
|
|
94
|
+
* Characters of one message's text, and separately of its thinking, kept.
|
|
95
|
+
* Text becomes a step's `context`; thinking is only embedded, and the intent
|
|
96
|
+
* probe sends 4,000 characters of either.
|
|
97
|
+
*/
|
|
98
|
+
const MAX_MESSAGE_CHARS = 16 * 1024;
|
|
99
|
+
/** `tool_use_id`s remembered, newest last. */
|
|
100
|
+
const MAX_TOOL_USES = 4_096;
|
|
101
|
+
/** This prompt's tool results kept for derivation (R-PARAM-2 asks for 5 by default). */
|
|
102
|
+
const MAX_RESULTS = 32;
|
|
103
|
+
/** Characters of one tool result kept. Derivation reads 2 KB of each. */
|
|
104
|
+
const MAX_RESULT_CHARS = 8 * 1024;
|
|
105
|
+
/**
|
|
106
|
+
* Append `piece` to `into` while the total stays within
|
|
107
|
+
* {@link MAX_MESSAGE_CHARS}; the piece that crosses it is clipped, and
|
|
108
|
+
* anything after is dropped. Returns the new total.
|
|
109
|
+
*/
|
|
110
|
+
function keepWithin(into, held, piece) {
|
|
111
|
+
const room = MAX_MESSAGE_CHARS - held;
|
|
112
|
+
if (room <= 0)
|
|
113
|
+
return held;
|
|
114
|
+
const kept = piece.length > room ? piece.slice(0, room) : piece;
|
|
115
|
+
into.push(kept);
|
|
116
|
+
return held + kept.length;
|
|
117
|
+
}
|
|
118
|
+
const indexes = new Map();
|
|
119
|
+
function freshIndex(st) {
|
|
120
|
+
return {
|
|
121
|
+
ino: st.ino,
|
|
122
|
+
birthtimeMs: st.birthtimeMs,
|
|
123
|
+
mtimeMs: st.mtimeMs,
|
|
124
|
+
offset: 0,
|
|
125
|
+
head: Buffer.alloc(0),
|
|
126
|
+
anchor: Buffer.alloc(0),
|
|
127
|
+
skipping: false,
|
|
128
|
+
lineNo: 0,
|
|
129
|
+
usage: new Map(),
|
|
130
|
+
usageSeen: new Set(),
|
|
131
|
+
lastAssistantText: "",
|
|
132
|
+
firstUserPrompt: "",
|
|
133
|
+
sawPrompt: false,
|
|
134
|
+
results: [],
|
|
135
|
+
messages: new Map(),
|
|
136
|
+
toolUses: new Map(),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Whether `idx` can be continued for a file that now looks like `st`. */
|
|
140
|
+
function continues(idx, st) {
|
|
141
|
+
if (st.ino !== idx.ino || st.birthtimeMs !== idx.birthtimeMs)
|
|
142
|
+
return false; // replaced
|
|
143
|
+
if (st.size < idx.offset)
|
|
144
|
+
return false; // truncated, or compacted into a new file
|
|
145
|
+
// Same size, different mtime: rewritten in place. An append changes the size.
|
|
146
|
+
if (st.size === idx.offset && st.mtimeMs !== idx.mtimeMs)
|
|
147
|
+
return false;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
/** Read exactly `length` bytes at `position`, or fewer at end of file. */
|
|
151
|
+
function readAt(fd, position, length) {
|
|
152
|
+
const buf = Buffer.allocUnsafe(length);
|
|
153
|
+
let got = 0;
|
|
154
|
+
while (got < length) {
|
|
155
|
+
const n = readSync(fd, buf, got, length - got, position + got);
|
|
156
|
+
if (n <= 0)
|
|
157
|
+
break;
|
|
158
|
+
got += n;
|
|
159
|
+
}
|
|
160
|
+
return got === length ? buf : buf.subarray(0, got);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The index for `path`, caught up with whatever was appended since the last
|
|
164
|
+
* call. Undefined when the file cannot be read — which every caller treats as
|
|
165
|
+
* an empty transcript, as the whole-file reader always did.
|
|
166
|
+
*/
|
|
167
|
+
function indexFor(path) {
|
|
168
|
+
let st;
|
|
169
|
+
try {
|
|
170
|
+
st = statSync(path);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
indexes.delete(path);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (!st.isFile())
|
|
177
|
+
return undefined;
|
|
178
|
+
let idx = indexes.get(path);
|
|
179
|
+
// Re-inserted on every use, so the Map's order is least-recently-used first.
|
|
180
|
+
if (idx)
|
|
181
|
+
indexes.delete(path);
|
|
182
|
+
if (!idx || !continues(idx, st))
|
|
183
|
+
idx = freshIndex(st);
|
|
184
|
+
indexes.set(path, idx);
|
|
185
|
+
while (indexes.size > MAX_INDEXES) {
|
|
186
|
+
const oldest = indexes.keys().next().value;
|
|
187
|
+
if (oldest === undefined)
|
|
188
|
+
break;
|
|
189
|
+
indexes.delete(oldest);
|
|
190
|
+
}
|
|
191
|
+
if (st.size === idx.offset)
|
|
192
|
+
return idx;
|
|
193
|
+
try {
|
|
194
|
+
return catchUp(path, idx, st);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// Unreadable mid-way (locked, deleted): what was folded in so far stands.
|
|
198
|
+
return indexes.get(path);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Fold in what was appended; the index to use from now on (a fresh one after a rewrite). */
|
|
202
|
+
function catchUp(path, stale, st) {
|
|
203
|
+
const fd = openSync(path, "r");
|
|
204
|
+
try {
|
|
205
|
+
let idx = stale;
|
|
206
|
+
if (idx.offset > 0) {
|
|
207
|
+
// The same size-and-inode can hide a rewrite: prove the bytes we already
|
|
208
|
+
// folded in are still there, at the head and just before the offset.
|
|
209
|
+
const head = readAt(fd, 0, idx.head.length);
|
|
210
|
+
const anchorAt = idx.offset - idx.anchor.length;
|
|
211
|
+
const anchor = readAt(fd, anchorAt, idx.anchor.length);
|
|
212
|
+
if (!head.equals(idx.head) || !anchor.equals(idx.anchor)) {
|
|
213
|
+
idx = freshIndex(st);
|
|
214
|
+
indexes.set(path, idx);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
fold(idx, fd, st.size);
|
|
218
|
+
idx.mtimeMs = st.mtimeMs;
|
|
219
|
+
if (idx.head.length < PROBE_BYTES)
|
|
220
|
+
idx.head = readAt(fd, 0, Math.min(PROBE_BYTES, idx.offset));
|
|
221
|
+
const anchorLength = Math.min(PROBE_BYTES, idx.offset);
|
|
222
|
+
idx.anchor = readAt(fd, idx.offset - anchorLength, anchorLength);
|
|
223
|
+
return idx;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
closeSync(fd);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Fold the bytes from `idx.offset` to `size` into `idx`, line by line.
|
|
231
|
+
*
|
|
232
|
+
* Only whole lines advance the offset. The last, unterminated line is folded in
|
|
233
|
+
* when it already parses — a writer that ends the file without a newline has
|
|
234
|
+
* still written a complete line — and otherwise left for the next call:
|
|
235
|
+
* transcripts are appended concurrently, and a half-written line is normal.
|
|
236
|
+
*/
|
|
237
|
+
function fold(idx, fd, size) {
|
|
238
|
+
let position = idx.offset;
|
|
239
|
+
/** Bytes of the line in progress, from earlier chunks of this pass. */
|
|
240
|
+
let pending = [];
|
|
241
|
+
let pendingBytes = 0;
|
|
242
|
+
while (position < size) {
|
|
243
|
+
const chunk = readAt(fd, position, Math.min(CHUNK_BYTES, size - position));
|
|
244
|
+
if (chunk.length === 0)
|
|
245
|
+
break;
|
|
246
|
+
const chunkStart = position;
|
|
247
|
+
position += chunk.length;
|
|
248
|
+
let start = 0;
|
|
249
|
+
for (;;) {
|
|
250
|
+
const nl = chunk.indexOf(0x0a, start);
|
|
251
|
+
if (nl === -1)
|
|
252
|
+
break;
|
|
253
|
+
if (idx.skipping) {
|
|
254
|
+
idx.skipping = false;
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const line = pendingBytes > 0 ? Buffer.concat([...pending, chunk.subarray(start, nl)]) : chunk.subarray(start, nl);
|
|
258
|
+
ingest(idx, line);
|
|
259
|
+
}
|
|
260
|
+
pending = [];
|
|
261
|
+
pendingBytes = 0;
|
|
262
|
+
start = nl + 1;
|
|
263
|
+
idx.offset = chunkStart + start;
|
|
264
|
+
}
|
|
265
|
+
if (start < chunk.length && !idx.skipping) {
|
|
266
|
+
// Copied: a subarray would pin the whole 4 MB chunk.
|
|
267
|
+
pending.push(Buffer.from(chunk.subarray(start)));
|
|
268
|
+
pendingBytes += chunk.length - start;
|
|
269
|
+
if (pendingBytes > MAX_LINE_BYTES) {
|
|
270
|
+
idx.skipping = true;
|
|
271
|
+
pending = [];
|
|
272
|
+
pendingBytes = 0;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (idx.skipping)
|
|
276
|
+
idx.offset = position; // skipped bytes are consumed bytes
|
|
277
|
+
}
|
|
278
|
+
if (pendingBytes > 0 && !idx.skipping) {
|
|
279
|
+
const tail = Buffer.concat(pending);
|
|
280
|
+
const parsed = parse(tail);
|
|
281
|
+
if (parsed) {
|
|
282
|
+
foldLine(idx, parsed);
|
|
283
|
+
idx.offset = position;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function parse(bytes) {
|
|
288
|
+
const text = bytes.toString("utf8").trim();
|
|
289
|
+
if (!text)
|
|
290
|
+
return undefined;
|
|
291
|
+
try {
|
|
292
|
+
const value = JSON.parse(text);
|
|
293
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function ingest(idx, bytes) {
|
|
300
|
+
const line = parse(bytes);
|
|
301
|
+
// A terminated line that does not parse is corrupt, not half-written: skip it.
|
|
302
|
+
if (line)
|
|
303
|
+
foldLine(idx, line);
|
|
304
|
+
}
|
|
305
|
+
/** What one line contributes to every answer this module gives. */
|
|
306
|
+
function foldLine(idx, line) {
|
|
307
|
+
idx.lineNo += 1;
|
|
308
|
+
const message = line.message;
|
|
309
|
+
const role = message?.role ?? line.type;
|
|
310
|
+
const blocks = blocksOf(line);
|
|
311
|
+
// Usage, deduplicated by message id (see `totalUsage`'s note on why).
|
|
312
|
+
const usage = message?.usage;
|
|
313
|
+
if (usage && typeof usage === "object") {
|
|
314
|
+
const id = message?.id;
|
|
315
|
+
if (!id || !idx.usageSeen.has(id)) {
|
|
316
|
+
if (id)
|
|
317
|
+
idx.usageSeen.add(id);
|
|
318
|
+
const model = message?.model ?? "unknown";
|
|
319
|
+
const acc = idx.usage.get(model) ?? zeroUsage(model);
|
|
320
|
+
acc.inputTokens += usage.input_tokens ?? 0;
|
|
321
|
+
acc.outputTokens += usage.output_tokens ?? 0;
|
|
322
|
+
acc.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
323
|
+
acc.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
324
|
+
idx.usage.set(model, acc);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (role === "assistant") {
|
|
328
|
+
const text = textOf(blocks);
|
|
329
|
+
if (text)
|
|
330
|
+
idx.lastAssistantText = text;
|
|
331
|
+
}
|
|
332
|
+
else if (role === "user") {
|
|
333
|
+
// A user line carrying text is a prompt: "this prompt's results" start
|
|
334
|
+
// again from it, its own tool results included.
|
|
335
|
+
const text = textOf(blocks);
|
|
336
|
+
if (text) {
|
|
337
|
+
if (!idx.sawPrompt)
|
|
338
|
+
idx.firstUserPrompt = text;
|
|
339
|
+
idx.sawPrompt = true;
|
|
340
|
+
idx.results = [];
|
|
341
|
+
}
|
|
342
|
+
if (idx.sawPrompt) {
|
|
343
|
+
for (const block of blocks) {
|
|
344
|
+
if (block.type !== "tool_result")
|
|
345
|
+
continue;
|
|
346
|
+
const result = resultTextOf(block);
|
|
347
|
+
if (!result)
|
|
348
|
+
continue;
|
|
349
|
+
idx.results.push(result.length > MAX_RESULT_CHARS ? result.slice(0, MAX_RESULT_CHARS) : result);
|
|
350
|
+
if (idx.results.length > MAX_RESULTS)
|
|
351
|
+
idx.results.shift();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Intent: every line of a multi-block message repeats its `message.id`, so a
|
|
356
|
+
// tool call's text and thinking are gathered from its siblings (R-INTENT-3).
|
|
357
|
+
const toolUses = blocks.filter((b) => b.type === "tool_use" && typeof b.id === "string");
|
|
358
|
+
const messageId = message?.id;
|
|
359
|
+
// No `message.id`: an older transcript or a fixture, where the line is the
|
|
360
|
+
// whole message. Only such a line holding a tool call is worth keeping.
|
|
361
|
+
if (!messageId && toolUses.length === 0)
|
|
362
|
+
return;
|
|
363
|
+
const key = messageId ?? `#line${idx.lineNo}`;
|
|
364
|
+
let entry = idx.messages.get(key);
|
|
365
|
+
if (!entry) {
|
|
366
|
+
entry = { texts: [], thinkings: [], textChars: 0, thinkingChars: 0 };
|
|
367
|
+
idx.messages.set(key, entry);
|
|
368
|
+
if (idx.messages.size > MAX_MESSAGES) {
|
|
369
|
+
const oldest = idx.messages.keys().next().value;
|
|
370
|
+
if (oldest !== undefined)
|
|
371
|
+
idx.messages.delete(oldest);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
for (const b of blocks) {
|
|
375
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
376
|
+
entry.textChars = keepWithin(entry.texts, entry.textChars, b.text);
|
|
377
|
+
}
|
|
378
|
+
else if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
379
|
+
entry.thinkingChars = keepWithin(entry.thinkings, entry.thinkingChars, b.thinking);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (toolUses.length === 0)
|
|
383
|
+
return;
|
|
384
|
+
const whole = textOf(blocks);
|
|
385
|
+
const ownText = whole.length > MAX_MESSAGE_CHARS ? whole.slice(0, MAX_MESSAGE_CHARS) : whole;
|
|
386
|
+
for (const use of toolUses) {
|
|
387
|
+
const id = use.id;
|
|
388
|
+
// The first line holding the call is the one that counts, as before.
|
|
389
|
+
if (idx.toolUses.has(id))
|
|
390
|
+
continue;
|
|
391
|
+
idx.toolUses.set(id, { message: key, ownText });
|
|
392
|
+
if (idx.toolUses.size > MAX_TOOL_USES) {
|
|
393
|
+
const oldest = idx.toolUses.keys().next().value;
|
|
394
|
+
if (oldest !== undefined)
|
|
395
|
+
idx.toolUses.delete(oldest);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Forget every cached index (tests; a caller that knows files were replaced). */
|
|
400
|
+
export function forgetTranscripts() {
|
|
401
|
+
indexes.clear();
|
|
402
|
+
}
|
|
403
|
+
// ── accessors ───────────────────────────────────────────────────────────────
|
|
72
404
|
/**
|
|
73
405
|
* The last `n` tool results of the *current* prompt, redacted and capped.
|
|
74
406
|
*
|
|
@@ -80,38 +412,17 @@ function resultTextOf(block) {
|
|
|
80
412
|
* A transcript compacted mid-turn yields whatever follows that line, which may
|
|
81
413
|
* be nothing; a transcript with no user prompt at all (a run opened by a proxy
|
|
82
414
|
* step) yields nothing. Results with `is_error: true` are kept: a failure is
|
|
83
|
-
* still the agent's evidence about what it is working on.
|
|
415
|
+
* still the agent's evidence about what it is working on. At most the last
|
|
416
|
+
* {@link MAX_RESULTS} are kept, each clipped to {@link MAX_RESULT_CHARS}
|
|
417
|
+
* characters before it is redacted and capped to `maxBytes`.
|
|
84
418
|
*/
|
|
85
419
|
export function recentToolResults(transcriptPath, n = 5, maxBytes = 2048) {
|
|
86
420
|
if (!transcriptPath || n <= 0)
|
|
87
421
|
return [];
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
91
|
-
const role = lines[i].message?.role ?? lines[i].type;
|
|
92
|
-
if (role !== "user")
|
|
93
|
-
continue;
|
|
94
|
-
if (textOf(blocksOf(lines[i]))) {
|
|
95
|
-
start = i;
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (start === -1)
|
|
422
|
+
const idx = indexFor(transcriptPath);
|
|
423
|
+
if (!idx || !idx.sawPrompt)
|
|
100
424
|
return [];
|
|
101
|
-
|
|
102
|
-
for (let i = start; i < lines.length; i += 1) {
|
|
103
|
-
const role = lines[i].message?.role ?? lines[i].type;
|
|
104
|
-
if (role !== "user")
|
|
105
|
-
continue;
|
|
106
|
-
for (const block of blocksOf(lines[i])) {
|
|
107
|
-
if (block.type !== "tool_result")
|
|
108
|
-
continue;
|
|
109
|
-
const text = resultTextOf(block);
|
|
110
|
-
if (text)
|
|
111
|
-
out.push(text);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return out.slice(-n).map((t) => truncateString(redactText(t), maxBytes));
|
|
425
|
+
return idx.results.slice(-n).map((t) => truncateString(redactText(t), maxBytes));
|
|
115
426
|
}
|
|
116
427
|
/**
|
|
117
428
|
* The intent of one tool call, read the way the transcript really stores it
|
|
@@ -134,28 +445,18 @@ export function recentToolResults(transcriptPath, n = 5, maxBytes = 2048) {
|
|
|
134
445
|
export function intentForToolUse(transcriptPath, toolUseId, sources = new Set(["text", "thinking", "tool"])) {
|
|
135
446
|
if (!transcriptPath || !toolUseId)
|
|
136
447
|
return undefined;
|
|
137
|
-
const
|
|
138
|
-
const holder =
|
|
139
|
-
|
|
448
|
+
const idx = indexFor(transcriptPath);
|
|
449
|
+
const holder = idx?.toolUses.get(toolUseId);
|
|
450
|
+
const message = holder ? idx?.messages.get(holder.message) : undefined;
|
|
451
|
+
if (!message)
|
|
140
452
|
return undefined;
|
|
141
|
-
// No `message.id` means an older transcript, or a hand-written fixture: the
|
|
142
|
-
// line is the whole message and there are no siblings to gather.
|
|
143
|
-
const messageId = holder.message?.id;
|
|
144
|
-
const siblings = messageId
|
|
145
|
-
? lines.filter((line) => line.message?.id === messageId)
|
|
146
|
-
: [holder];
|
|
147
453
|
if (sources.has("text")) {
|
|
148
|
-
const text =
|
|
454
|
+
const text = message.texts.join("\n").trim();
|
|
149
455
|
if (text)
|
|
150
456
|
return { text, source: "text" };
|
|
151
457
|
}
|
|
152
458
|
if (sources.has("thinking")) {
|
|
153
|
-
const thinking =
|
|
154
|
-
.flatMap((line) => blocksOf(line))
|
|
155
|
-
.filter((b) => b.type === "thinking" && typeof b.thinking === "string")
|
|
156
|
-
.map((b) => b.thinking)
|
|
157
|
-
.join("\n")
|
|
158
|
-
.trim();
|
|
459
|
+
const thinking = message.thinkings.join("\n").trim();
|
|
159
460
|
if (thinking)
|
|
160
461
|
return { text: thinking, source: "thinking" };
|
|
161
462
|
}
|
|
@@ -165,29 +466,13 @@ export function intentForToolUse(transcriptPath, toolUseId, sources = new Set(["
|
|
|
165
466
|
export function contextForToolUse(transcriptPath, toolUseId) {
|
|
166
467
|
if (!transcriptPath || !toolUseId)
|
|
167
468
|
return "";
|
|
168
|
-
|
|
169
|
-
const blocks = blocksOf(line);
|
|
170
|
-
if (blocks.some((b) => b.type === "tool_use" && b.id === toolUseId)) {
|
|
171
|
-
return textOf(blocks);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return "";
|
|
469
|
+
return indexFor(transcriptPath)?.toolUses.get(toolUseId)?.ownText ?? "";
|
|
175
470
|
}
|
|
176
471
|
/** The last assistant text in the transcript — the turn's final answer. */
|
|
177
472
|
export function lastAssistantText(transcriptPath) {
|
|
178
473
|
if (!transcriptPath)
|
|
179
474
|
return "";
|
|
180
|
-
|
|
181
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
182
|
-
const line = lines[i];
|
|
183
|
-
const role = line.message?.role ?? line.type;
|
|
184
|
-
if (role !== "assistant")
|
|
185
|
-
continue;
|
|
186
|
-
const text = textOf(blocksOf(line));
|
|
187
|
-
if (text)
|
|
188
|
-
return text;
|
|
189
|
-
}
|
|
190
|
-
return "";
|
|
475
|
+
return indexFor(transcriptPath)?.lastAssistantText ?? "";
|
|
191
476
|
}
|
|
192
477
|
/**
|
|
193
478
|
* The turn's final answer, waiting briefly for the transcript to catch up.
|
|
@@ -251,28 +536,17 @@ const zeroUsage = (model) => ({
|
|
|
251
536
|
*
|
|
252
537
|
* A line with no `message.id` is counted once on its own (older transcripts, and
|
|
253
538
|
* anything hand-written in a test).
|
|
539
|
+
*
|
|
540
|
+
* The running totals live in the index ({@link foldLine}); this returns a copy,
|
|
541
|
+
* because a mark is kept and compared long after the index has moved on.
|
|
254
542
|
*/
|
|
255
543
|
function totalUsage(transcriptPath) {
|
|
256
544
|
const totals = new Map();
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const id = line.message?.id;
|
|
263
|
-
if (id) {
|
|
264
|
-
if (seen.has(id))
|
|
265
|
-
continue;
|
|
266
|
-
seen.add(id);
|
|
267
|
-
}
|
|
268
|
-
const model = line.message?.model ?? "unknown";
|
|
269
|
-
const acc = totals.get(model) ?? zeroUsage(model);
|
|
270
|
-
acc.inputTokens += usage.input_tokens ?? 0;
|
|
271
|
-
acc.outputTokens += usage.output_tokens ?? 0;
|
|
272
|
-
acc.cacheReadTokens += usage.cache_read_input_tokens ?? 0;
|
|
273
|
-
acc.cacheCreationTokens += usage.cache_creation_input_tokens ?? 0;
|
|
274
|
-
totals.set(model, acc);
|
|
275
|
-
}
|
|
545
|
+
const idx = indexFor(transcriptPath);
|
|
546
|
+
if (!idx)
|
|
547
|
+
return totals;
|
|
548
|
+
for (const [model, u] of idx.usage)
|
|
549
|
+
totals.set(model, { ...u });
|
|
276
550
|
return totals;
|
|
277
551
|
}
|
|
278
552
|
/**
|
|
@@ -333,14 +607,6 @@ export function usageSince(transcriptPath, mark) {
|
|
|
333
607
|
export function firstUserPrompt(transcriptPath) {
|
|
334
608
|
if (!transcriptPath)
|
|
335
609
|
return "";
|
|
336
|
-
|
|
337
|
-
const role = line.message?.role ?? line.type;
|
|
338
|
-
if (role !== "user")
|
|
339
|
-
continue;
|
|
340
|
-
const text = textOf(blocksOf(line));
|
|
341
|
-
if (text)
|
|
342
|
-
return text;
|
|
343
|
-
}
|
|
344
|
-
return "";
|
|
610
|
+
return indexFor(transcriptPath)?.firstUserPrompt ?? "";
|
|
345
611
|
}
|
|
346
612
|
//# sourceMappingURL=transcript.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export * from "./control/discovery.js";
|
|
|
26
26
|
export * from "./control/paths.js";
|
|
27
27
|
export * from "./record/recorder.js";
|
|
28
28
|
export { RemoteRecorder } from "./record/remote-recorder.js";
|
|
29
|
-
export { redact, redactText, REDACTED } from "./record/redact.js";
|
|
29
|
+
export { redact, redactText, redactJsonText, REDACTED } from "./record/redact.js";
|
|
30
30
|
export { serializeCapped, truncateString, truncateStrings, DEFAULT_MAX_PAYLOAD_BYTES, } from "./record/truncate.js";
|
|
31
31
|
export { StepQueue } from "./record/queue.js";
|
|
32
32
|
export * from "./config/resolve.js";
|
|
@@ -34,4 +34,8 @@ export * from "./config/generate.js";
|
|
|
34
34
|
export { authenticate, loadCredentials, saveCredentials, clearCredentials } from "./auth/client.js";
|
|
35
35
|
export type { AuthSession, AuthUser } from "./auth/client.js";
|
|
36
36
|
export { logLine, logDetail } from "./util/log.js";
|
|
37
|
+
export { isRunnerOff, offSource, setRunnerOff, offFlagPath, projectOffFlagPath } from "./util/off-switch.js";
|
|
38
|
+
export type { OffSource } from "./util/off-switch.js";
|
|
39
|
+
export { serviceHealth, ServiceHealth, awaitWithin, timeoutSignal, serviceTimeoutMs, isUnhealthyStatus } from "./util/service-health.js";
|
|
40
|
+
export type { HealthSnapshot, ServiceHealthOptions } from "./util/service-health.js";
|
|
37
41
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -22,11 +22,15 @@ export * from "./control/discovery.js";
|
|
|
22
22
|
export * from "./control/paths.js";
|
|
23
23
|
export * from "./record/recorder.js";
|
|
24
24
|
export { RemoteRecorder } from "./record/remote-recorder.js";
|
|
25
|
-
export { redact, redactText, REDACTED } from "./record/redact.js";
|
|
25
|
+
export { redact, redactText, redactJsonText, REDACTED } from "./record/redact.js";
|
|
26
26
|
export { serializeCapped, truncateString, truncateStrings, DEFAULT_MAX_PAYLOAD_BYTES, } from "./record/truncate.js";
|
|
27
27
|
export { StepQueue } from "./record/queue.js";
|
|
28
28
|
export * from "./config/resolve.js";
|
|
29
29
|
export * from "./config/generate.js";
|
|
30
30
|
export { authenticate, loadCredentials, saveCredentials, clearCredentials } from "./auth/client.js";
|
|
31
31
|
export { logLine, logDetail } from "./util/log.js";
|
|
32
|
+
// An embedder hosting its own control server honours the same kill switch
|
|
33
|
+
// (`bir off`) and reads the same verdict on the service as the binaries do.
|
|
34
|
+
export { isRunnerOff, offSource, setRunnerOff, offFlagPath, projectOffFlagPath } from "./util/off-switch.js";
|
|
35
|
+
export { serviceHealth, ServiceHealth, awaitWithin, timeoutSignal, serviceTimeoutMs, isUnhealthyStatus } from "./util/service-health.js";
|
|
32
36
|
//# sourceMappingURL=index.js.map
|