spexcode 0.6.8 → 0.7.0-next.1
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/node_modules/@spexcode/session-application/package.json +5 -5
- package/node_modules/@spexcode/session-events/package.json +2 -2
- package/node_modules/@spexcode/session-protocol/package.json +1 -1
- package/node_modules/@spexcode/session-runtime/package.json +2 -2
- package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +12 -1
- package/node_modules/@spexcode/session-selflaunch/dist/locality.js +62 -10
- package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
- package/node_modules/@spexcode/session-topology/package.json +2 -2
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +127 -10
- package/node_modules/@spexcode/spec-cli/dist/cli.js +19 -0
- package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
- package/node_modules/@spexcode/spec-cli/dist/guide.js +9 -11
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +11 -5
- package/node_modules/@spexcode/spec-cli/dist/harness.js +185 -28
- package/node_modules/@spexcode/spec-cli/dist/host.js +5 -1
- package/node_modules/@spexcode/spec-cli/dist/index.js +21 -65
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
- package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +45 -14
- package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
- package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
- package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +4 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +104 -36
- package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +1 -1
- package/node_modules/@spexcode/spec-cli/package.json +8 -6
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +0 -1
- package/node_modules/@spexcode/spec-core/package.json +1 -1
- package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@spexcode/transcript/dist/frames.d.ts +46 -0
- package/node_modules/@spexcode/transcript/dist/frames.js +87 -0
- package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/transcript/dist/index.js +5 -0
- package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
- package/node_modules/@spexcode/transcript/dist/live.js +65 -0
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +50 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +395 -0
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +16 -0
- package/node_modules/@spexcode/transcript/dist/readers.js +375 -0
- package/node_modules/@spexcode/{spec-cli/dist/transcript-reader.d.ts → transcript/dist/turns.d.ts} +11 -5
- package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
- package/node_modules/@spexcode/transcript/package.json +30 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +418 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +127 -0
- package/node_modules/smol-toml/dist/error.d.ts +38 -0
- package/node_modules/smol-toml/dist/error.js +63 -0
- package/node_modules/smol-toml/dist/extract.js +69 -0
- package/node_modules/smol-toml/dist/index.cjs +734 -0
- package/node_modules/smol-toml/dist/index.d.ts +43 -0
- package/node_modules/smol-toml/dist/index.js +33 -0
- package/node_modules/smol-toml/dist/parse.d.ts +36 -0
- package/node_modules/smol-toml/dist/parse.js +149 -0
- package/node_modules/smol-toml/dist/primitive.js +238 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
- package/node_modules/smol-toml/dist/stringify.js +181 -0
- package/node_modules/smol-toml/dist/struct.js +179 -0
- package/node_modules/smol-toml/dist/util.d.ts +38 -0
- package/node_modules/smol-toml/dist/util.js +89 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +3 -3
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -27
- package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
- package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
- package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
- package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +0 -251
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { TranscriptReadError } from './turns.js';
|
|
2
|
+
// ONE PARSER PER HARNESS. Each function turns one native record — a line of Claude's project JSONL (which is
|
|
3
|
+
// also exactly what its `--output-format stream-json` prints), a Codex rollout line, a pi session line, an
|
|
4
|
+
// OpenCode export — into the normalized event the interval collector consumes. The same parser serves a file
|
|
5
|
+
// being tailed ([[transcript-reader]]) and an event stream held in memory ([[live-transcript]]): the source is
|
|
6
|
+
// where bytes come from, the parser is what they mean, and neither knows the other.
|
|
7
|
+
export const MAX_TURNS = 200;
|
|
8
|
+
export const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
9
|
+
const object = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
10
|
+
const items = (value) => Array.isArray(value) ? value : [];
|
|
11
|
+
const string = (value) => typeof value === 'string' && value.trim() ? value : null;
|
|
12
|
+
const idOf = (value) => {
|
|
13
|
+
if (!value)
|
|
14
|
+
return null;
|
|
15
|
+
for (const key of ['id', 'uuid', 'message_id', 'messageId', 'call_id', 'callId', 'client_id', 'clientId']) {
|
|
16
|
+
const found = string(value[key]);
|
|
17
|
+
if (found)
|
|
18
|
+
return found;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
const timestamp = (value) => {
|
|
23
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
24
|
+
return value;
|
|
25
|
+
if (typeof value !== 'string' || !value.trim())
|
|
26
|
+
return null;
|
|
27
|
+
const numeric = Number(value);
|
|
28
|
+
if (Number.isFinite(numeric))
|
|
29
|
+
return numeric;
|
|
30
|
+
const parsed = Date.parse(value);
|
|
31
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
32
|
+
};
|
|
33
|
+
const at = (value) => {
|
|
34
|
+
if (!value)
|
|
35
|
+
return null;
|
|
36
|
+
for (const key of ['timestamp', 'created_at', 'createdAt', 'created', 'time']) {
|
|
37
|
+
const candidate = timestamp(value[key]);
|
|
38
|
+
if (candidate !== null)
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
const compact = (value) => {
|
|
44
|
+
if (typeof value === 'string')
|
|
45
|
+
return value;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.stringify(value) ?? String(value);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return String(value);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
|
|
54
|
+
// --- the seven native shapes -------------------------------------------------------------------------------
|
|
55
|
+
export function claudeEvent(value) {
|
|
56
|
+
const entry = object(value);
|
|
57
|
+
const message = object(entry?.message);
|
|
58
|
+
if (!entry || !message)
|
|
59
|
+
return null;
|
|
60
|
+
const eventAt = at(entry) ?? at(message);
|
|
61
|
+
if (eventAt === null)
|
|
62
|
+
return { at: null, turn: null };
|
|
63
|
+
if (entry.type === 'user' && message.role === 'user') {
|
|
64
|
+
const blocks = items(message.content);
|
|
65
|
+
const text = typeof message.content === 'string'
|
|
66
|
+
? string(message.content)
|
|
67
|
+
: blocks.map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
68
|
+
const outputs = blocks.flatMap((block) => {
|
|
69
|
+
const b = object(block);
|
|
70
|
+
const id = string(b?.tool_use_id);
|
|
71
|
+
return b?.type === 'tool_result' && id ? [{ id, text: compact(b?.content ?? '') }] : [];
|
|
72
|
+
});
|
|
73
|
+
if (outputs.length)
|
|
74
|
+
return { at: eventAt, turn: null, toolOutputs: outputs };
|
|
75
|
+
if (text)
|
|
76
|
+
return { at: eventAt, turn: { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'user', text, tools: [] } };
|
|
77
|
+
}
|
|
78
|
+
if (entry.type === 'assistant' && message.role === 'assistant') {
|
|
79
|
+
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
80
|
+
for (const blockValue of items(message.content)) {
|
|
81
|
+
const block = object(blockValue);
|
|
82
|
+
if (block?.type === 'text')
|
|
83
|
+
turn.text = [turn.text, string(block.text)].filter(Boolean).join('\n') || undefined;
|
|
84
|
+
if (block?.type === 'tool_use') {
|
|
85
|
+
const id = string(block.id) ?? `tool-${turn.tools.length}`;
|
|
86
|
+
turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.input === undefined ? undefined : compact(block.input), outputLines: 0, outputBytes: 0 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { at: eventAt, turn };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
export function codexEvent(value) {
|
|
94
|
+
const entry = object(value);
|
|
95
|
+
const payload = object(entry?.payload);
|
|
96
|
+
if (!entry || !payload)
|
|
97
|
+
return null;
|
|
98
|
+
const eventAt = at(payload) ?? at(entry);
|
|
99
|
+
if (eventAt === null)
|
|
100
|
+
return { at: null, turn: null };
|
|
101
|
+
const type = string(payload.type);
|
|
102
|
+
if ((entry.type === 'event_msg' && type === 'user_message')
|
|
103
|
+
|| (entry.type === 'response_item' && (type === 'message' || type === 'input_message') && payload.role === 'user')) {
|
|
104
|
+
const text = typeof payload.message === 'string' ? payload.message : compact(payload.message ?? payload.content ?? '');
|
|
105
|
+
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
106
|
+
}
|
|
107
|
+
// commentary AND the final answer are both what the agent said; only structured reasoning stays private
|
|
108
|
+
if (entry.type === 'event_msg' && type === 'agent_message') {
|
|
109
|
+
const text = string(payload.message ?? payload.text);
|
|
110
|
+
return text ? { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', text, tools: [] } } : null;
|
|
111
|
+
}
|
|
112
|
+
if (entry.type === 'response_item' && (type === 'custom_tool_call' || type === 'function_call')) {
|
|
113
|
+
const id = string(payload.call_id ?? payload.id) ?? 'tool';
|
|
114
|
+
return { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [{ id, name: string(payload.name ?? payload.tool_name) ?? 'tool', input: payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments), outputLines: 0, outputBytes: 0 }] } };
|
|
115
|
+
}
|
|
116
|
+
if (entry.type === 'response_item' && (type === 'custom_tool_call_output' || type === 'function_call_output')) {
|
|
117
|
+
const id = string(payload.call_id ?? payload.id);
|
|
118
|
+
const output = payload.output ?? payload.result ?? '';
|
|
119
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] } : null;
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const blockText = (content) => typeof content === 'string'
|
|
124
|
+
? string(content)
|
|
125
|
+
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
126
|
+
export function piEvent(value) {
|
|
127
|
+
const entry = object(value);
|
|
128
|
+
const message = object(entry?.message);
|
|
129
|
+
if (!entry || entry.type !== 'message' || !message)
|
|
130
|
+
return null;
|
|
131
|
+
const eventAt = at(entry) ?? at(message);
|
|
132
|
+
if (eventAt === null)
|
|
133
|
+
return { at: null, turn: null };
|
|
134
|
+
if (message.role === 'user') {
|
|
135
|
+
const text = blockText(message.content);
|
|
136
|
+
return text ? { at: eventAt, turn: { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
137
|
+
}
|
|
138
|
+
if (message.role === 'assistant') {
|
|
139
|
+
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
140
|
+
for (const blockValue of items(message.content)) {
|
|
141
|
+
const block = object(blockValue);
|
|
142
|
+
if (block?.type === 'text')
|
|
143
|
+
turn.text = [turn.text, string(block.text)].filter(Boolean).join('\n') || undefined;
|
|
144
|
+
if (block?.type === 'toolCall') {
|
|
145
|
+
const id = string(block.id) ?? `tool-${turn.tools.length}`;
|
|
146
|
+
turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { at: eventAt, turn };
|
|
150
|
+
}
|
|
151
|
+
if (message.role === 'toolResult') {
|
|
152
|
+
const id = string(message.toolCallId);
|
|
153
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
export function geminiEvent(value) {
|
|
158
|
+
const entry = object(value);
|
|
159
|
+
const message = (entry?.type === 'user' || entry?.type === 'gemini')
|
|
160
|
+
? entry
|
|
161
|
+
: object(items(entry?.messages)[0]) ?? object(items(object(entry?.$set)?.messages)[0]);
|
|
162
|
+
if (!entry || !message)
|
|
163
|
+
return null;
|
|
164
|
+
const eventAt = at(message) ?? at(entry);
|
|
165
|
+
if (eventAt === null)
|
|
166
|
+
return { at: null, turn: null };
|
|
167
|
+
const content = message.content;
|
|
168
|
+
const blocks = items(content);
|
|
169
|
+
const outputs = blocks.flatMap((blockValue) => {
|
|
170
|
+
const block = object(blockValue);
|
|
171
|
+
const response = object(block?.functionResponse);
|
|
172
|
+
const id = string(response?.id);
|
|
173
|
+
return id ? [{ id, text: compact(response?.response ?? '') }] : [];
|
|
174
|
+
});
|
|
175
|
+
if (outputs.length)
|
|
176
|
+
return { at: eventAt, turn: null, toolOutputs: outputs };
|
|
177
|
+
if (message.type === 'user') {
|
|
178
|
+
const text = typeof content === 'string' ? string(content) : blocks.map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
179
|
+
return text ? { at: eventAt, turn: { id: idOf(message), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
180
|
+
}
|
|
181
|
+
if (message.type === 'gemini') {
|
|
182
|
+
const turn = { id: idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
183
|
+
if (typeof content === 'string')
|
|
184
|
+
turn.text = string(content) ?? undefined;
|
|
185
|
+
for (const callValue of items(message.toolCalls)) {
|
|
186
|
+
const call = object(callValue);
|
|
187
|
+
const id = string(call?.id) ?? `tool-${turn.tools.length}`;
|
|
188
|
+
turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0 });
|
|
189
|
+
}
|
|
190
|
+
if (!turn.text && !turn.tools.length)
|
|
191
|
+
return null;
|
|
192
|
+
return { at: eventAt, turn };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
export function openclawEvent(value) {
|
|
197
|
+
const entry = object(value);
|
|
198
|
+
const message = object(entry?.message);
|
|
199
|
+
if (!entry || entry.type !== 'message' || !message)
|
|
200
|
+
return null;
|
|
201
|
+
const eventAt = at(message) ?? at(entry);
|
|
202
|
+
if (eventAt === null)
|
|
203
|
+
return { at: null, turn: null };
|
|
204
|
+
if (message.role === 'user') {
|
|
205
|
+
const text = blockText(message.content);
|
|
206
|
+
return text ? { at: eventAt, turn: { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
207
|
+
}
|
|
208
|
+
if (message.role === 'toolResult') {
|
|
209
|
+
const id = string(message.toolCallId);
|
|
210
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
211
|
+
}
|
|
212
|
+
if (message.role === 'assistant') {
|
|
213
|
+
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
214
|
+
for (const blockValue of items(message.content)) {
|
|
215
|
+
const block = object(blockValue);
|
|
216
|
+
if (block?.type === 'text')
|
|
217
|
+
turn.text = [turn.text, string(block.text)].filter(Boolean).join('\n') || undefined;
|
|
218
|
+
if (block?.type === 'toolCall') {
|
|
219
|
+
const id = string(block.id) ?? `tool-${turn.tools.length}`;
|
|
220
|
+
turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return turn.text || turn.tools.length ? { at: eventAt, turn } : null;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
export function hermesEvents(value) {
|
|
228
|
+
const root = object(value);
|
|
229
|
+
const events = [];
|
|
230
|
+
for (const messageValue of items(root?.messages)) {
|
|
231
|
+
const message = object(messageValue);
|
|
232
|
+
if (!message)
|
|
233
|
+
continue;
|
|
234
|
+
const eventAt = at(message);
|
|
235
|
+
if (eventAt === null) {
|
|
236
|
+
events.push({ at: null, turn: null });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const role = message.role;
|
|
240
|
+
if (role === 'user') {
|
|
241
|
+
const text = string(message.content);
|
|
242
|
+
if (text)
|
|
243
|
+
events.push({ at: eventAt, turn: { id: idOf(message), at: eventAt, role: 'user', text, tools: [] } });
|
|
244
|
+
}
|
|
245
|
+
else if (role === 'assistant') {
|
|
246
|
+
const turn = { id: idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
247
|
+
const text = string(message.content);
|
|
248
|
+
if (text)
|
|
249
|
+
turn.text = text;
|
|
250
|
+
for (const callValue of items(message.tool_calls)) {
|
|
251
|
+
const call = object(callValue);
|
|
252
|
+
const fn = object(call?.function);
|
|
253
|
+
const id = string(call?.id) ?? `tool-${turn.tools.length}`;
|
|
254
|
+
turn.tools.push({ id, name: string(fn?.name) ?? 'tool', input: fn?.arguments === undefined ? undefined : compact(fn.arguments), outputLines: 0, outputBytes: 0 });
|
|
255
|
+
}
|
|
256
|
+
if (turn.text || turn.tools.length)
|
|
257
|
+
events.push({ at: eventAt, turn });
|
|
258
|
+
}
|
|
259
|
+
else if (role === 'tool') {
|
|
260
|
+
const id = string(message.tool_call_id);
|
|
261
|
+
if (id)
|
|
262
|
+
events.push({ at: eventAt, turn: null, toolOutputs: [{ id, text: compact(message.content ?? '') }] });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return events;
|
|
266
|
+
}
|
|
267
|
+
// OpenCode's export is one JSON document, not a line stream: every message arrives with its parts, and a tool
|
|
268
|
+
// part already carries its own result — so its turn is complete on arrival, and a part still running simply has
|
|
269
|
+
// no output yet.
|
|
270
|
+
export function opencodeEvents(value) {
|
|
271
|
+
const events = [];
|
|
272
|
+
for (const messageValue of items(object(value)?.messages)) {
|
|
273
|
+
const message = object(messageValue);
|
|
274
|
+
const info = object(message?.info);
|
|
275
|
+
if (!message || !info)
|
|
276
|
+
continue;
|
|
277
|
+
const eventAt = at(object(info.time)) ?? at(info);
|
|
278
|
+
if (eventAt === null) {
|
|
279
|
+
events.push({ at: null, turn: null });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const role = info.role === 'user' ? 'user' : info.role === 'assistant' ? 'assistant' : null;
|
|
283
|
+
if (!role)
|
|
284
|
+
continue;
|
|
285
|
+
const turn = { id: idOf(info) ?? idOf(message), at: eventAt, role, tools: [] };
|
|
286
|
+
for (const partValue of items(message.parts)) {
|
|
287
|
+
const part = object(partValue);
|
|
288
|
+
if (part?.type === 'text')
|
|
289
|
+
turn.text = [turn.text, string(part.text)].filter(Boolean).join('\n') || undefined;
|
|
290
|
+
if (part?.type === 'tool' && role === 'assistant') {
|
|
291
|
+
const state = object(part.state);
|
|
292
|
+
const status = (string(state?.status) ?? '').toLowerCase();
|
|
293
|
+
const tool = { id: string(part.callID ?? part.id) ?? `tool-${turn.tools.length}`, name: string(part.tool) ?? 'tool', input: state?.input === undefined ? undefined : compact(state.input), outputLines: 0, outputBytes: 0 };
|
|
294
|
+
if (/completed|error|cancelled/.test(status)) {
|
|
295
|
+
const output = compact(state?.output ?? state?.error ?? '');
|
|
296
|
+
tool.output = output.slice(0, MAX_OUTPUT_BYTES);
|
|
297
|
+
tool.outputBytes = Buffer.byteLength(output);
|
|
298
|
+
tool.outputLines = lineCount(output);
|
|
299
|
+
}
|
|
300
|
+
turn.tools.push(tool);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (role === 'user' && !turn.text)
|
|
304
|
+
continue;
|
|
305
|
+
events.push({ at: eventAt, turn });
|
|
306
|
+
}
|
|
307
|
+
return events;
|
|
308
|
+
}
|
|
309
|
+
// --- reading an interval ------------------------------------------------------------------------------------
|
|
310
|
+
// Collects the interval's turns from any event source. Caps keep the NEWEST turns, because a live tail and a
|
|
311
|
+
// closed stretch are both read for what happened last; every dropped turn or byte is counted, never hidden.
|
|
312
|
+
export class IntervalCollector {
|
|
313
|
+
turns = [];
|
|
314
|
+
byTool = new Map();
|
|
315
|
+
evicted = new Set();
|
|
316
|
+
synthesized = new Map(); // `<role>@<at>` → how many turns already wore it
|
|
317
|
+
sawTimestamp = false;
|
|
318
|
+
omittedTurns = 0;
|
|
319
|
+
omittedBytes = 0;
|
|
320
|
+
outOfOrderEvents = 0;
|
|
321
|
+
pastRange = false;
|
|
322
|
+
range;
|
|
323
|
+
constructor(range) { this.range = { from: range.from, to: range.to }; }
|
|
324
|
+
// the open interval's end is "now" and moves; extending it never revisits what was already collected
|
|
325
|
+
extend(to) { if (to > this.range.to)
|
|
326
|
+
this.range.to = to; }
|
|
327
|
+
// returns true once the source has moved past `to` (the caller may then bound its lookahead)
|
|
328
|
+
add(event) {
|
|
329
|
+
const eventAt = event.at;
|
|
330
|
+
if (eventAt === null)
|
|
331
|
+
return this.pastRange;
|
|
332
|
+
this.sawTimestamp = true;
|
|
333
|
+
if (!this.pastRange && eventAt > this.range.to)
|
|
334
|
+
this.pastRange = true;
|
|
335
|
+
else if (this.pastRange && eventAt <= this.range.to)
|
|
336
|
+
this.outOfOrderEvents++;
|
|
337
|
+
if (eventAt < this.range.from || eventAt > this.range.to)
|
|
338
|
+
return this.pastRange;
|
|
339
|
+
if (event.toolOutputs) {
|
|
340
|
+
for (const output of event.toolOutputs) {
|
|
341
|
+
const bytes = Buffer.byteLength(output.text);
|
|
342
|
+
const tool = this.byTool.get(output.id);
|
|
343
|
+
if (!tool || this.evicted.has(output.id)) {
|
|
344
|
+
this.omittedBytes += bytes;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
tool.outputBytes += bytes;
|
|
348
|
+
tool.outputLines += lineCount(output.text);
|
|
349
|
+
if (tool.output === undefined)
|
|
350
|
+
tool.output = '';
|
|
351
|
+
const remaining = Math.max(0, MAX_OUTPUT_BYTES - Buffer.byteLength(tool.output));
|
|
352
|
+
tool.output += output.text.slice(0, remaining);
|
|
353
|
+
if (bytes > remaining)
|
|
354
|
+
this.omittedBytes += bytes - remaining;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (event.turn) {
|
|
358
|
+
// the parsed event is never written to: an in-memory source ([[live-transcript]]) collects the same
|
|
359
|
+
// event again on every read, so the collector works on its own copy of the turn and its calls
|
|
360
|
+
const turn = { ...event.turn, tools: event.turn.tools.map((tool) => ({ ...tool })) };
|
|
361
|
+
// a turn without a native id gets one from its place in the thread — deterministic across re-reads of an
|
|
362
|
+
// append-only source, which is what lets a subscriber match it between frames
|
|
363
|
+
if (turn.id === null) {
|
|
364
|
+
const base = `${turn.role}@${turn.at}`;
|
|
365
|
+
const seen = this.synthesized.get(base) ?? 0;
|
|
366
|
+
this.synthesized.set(base, seen + 1);
|
|
367
|
+
turn.id = seen ? `${base}#${seen}` : base;
|
|
368
|
+
}
|
|
369
|
+
this.turns.push(turn);
|
|
370
|
+
for (const tool of turn.tools)
|
|
371
|
+
this.byTool.set(tool.id, tool);
|
|
372
|
+
if (this.turns.length > MAX_TURNS) {
|
|
373
|
+
const dropped = this.turns.shift();
|
|
374
|
+
for (const tool of dropped.tools)
|
|
375
|
+
this.evicted.add(tool.id);
|
|
376
|
+
this.omittedTurns++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return this.pastRange;
|
|
380
|
+
}
|
|
381
|
+
finish(revision, harness) {
|
|
382
|
+
if (!this.sawTimestamp)
|
|
383
|
+
throw new TranscriptReadError('invalid', `${harness} transcript has no reliable timestamps; interval reads are unavailable`);
|
|
384
|
+
return {
|
|
385
|
+
revision,
|
|
386
|
+
from: this.range.from,
|
|
387
|
+
to: this.range.to,
|
|
388
|
+
turns: this.turns.map((turn) => ({ ...turn, id: turn.id, tools: turn.tools.length ? turn.tools.map((tool) => ({ ...tool })) : undefined })),
|
|
389
|
+
truncated: this.omittedTurns > 0 || this.omittedBytes > 0 || this.outOfOrderEvents > 0,
|
|
390
|
+
omittedTurns: this.omittedTurns,
|
|
391
|
+
omittedBytes: this.omittedBytes,
|
|
392
|
+
outOfOrderEvents: this.outOfOrderEvents,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type TranscriptReader } from './turns.js';
|
|
2
|
+
export declare function claudeTranscriptPath(threadId: string, root?: string): string | null;
|
|
3
|
+
export declare function codexRolloutPath(threadId: string, root?: string): string | null;
|
|
4
|
+
export declare function piSessionPath(threadId: string, root?: string): string | null;
|
|
5
|
+
export declare function geminiTranscriptPath(threadId: string, root?: string): string | null;
|
|
6
|
+
export declare function openclawTranscriptPath(threadId: string, root?: string): string | null;
|
|
7
|
+
export declare const claudeTranscript: TranscriptReader;
|
|
8
|
+
export declare const codexTranscript: TranscriptReader;
|
|
9
|
+
export declare const piTranscript: TranscriptReader;
|
|
10
|
+
export declare const geminiTranscript: TranscriptReader;
|
|
11
|
+
export declare const openclawTranscript: TranscriptReader;
|
|
12
|
+
export declare function opencodeTranscriptReader(root?: string, load?: (threadId: string) => string): TranscriptReader;
|
|
13
|
+
export declare const opencodeTranscript: TranscriptReader;
|
|
14
|
+
export declare function hermesTranscriptReader(root?: string, load?: (threadId: string) => string): TranscriptReader;
|
|
15
|
+
export declare const hermesTranscript: TranscriptReader;
|
|
16
|
+
export declare function unsupportedTranscript(harness: string): TranscriptReader;
|