@spexcode/transcript 0.7.0-next.10 → 0.7.0-next.12
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/parsers.d.ts +4 -2
- package/dist/parsers.js +48 -34
- package/dist/turns.d.ts +3 -0
- package/package.json +1 -1
package/dist/parsers.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TranscriptRange, type TranscriptRead } from './turns.js';
|
|
1
|
+
import { type TranscriptRange, type TranscriptRead, type TurnOutcome } from './turns.js';
|
|
2
2
|
export declare const MAX_TURNS = 200;
|
|
3
3
|
export declare const MAX_OUTPUT_BYTES: number;
|
|
4
4
|
export type MutableTool = {
|
|
@@ -16,6 +16,8 @@ export type MutableTurn = {
|
|
|
16
16
|
role: 'user' | 'assistant';
|
|
17
17
|
text?: string;
|
|
18
18
|
tools: MutableTool[];
|
|
19
|
+
outcome?: TurnOutcome;
|
|
20
|
+
error?: string;
|
|
19
21
|
};
|
|
20
22
|
export type ToolOutcome = 'failed' | 'rejected';
|
|
21
23
|
export type ParsedEvent = {
|
|
@@ -34,7 +36,7 @@ export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
|
|
|
34
36
|
export declare function codexAppServerStream(): Parse;
|
|
35
37
|
export declare function piEvent(value: unknown): ParsedEvent | null;
|
|
36
38
|
export declare function geminiEvent(value: unknown): ParsedEvent | null;
|
|
37
|
-
export declare
|
|
39
|
+
export declare const openclawEvent: typeof piEvent;
|
|
38
40
|
export declare function hermesEvents(value: unknown): ParsedEvent[];
|
|
39
41
|
export declare function opencodeEvents(value: unknown): ParsedEvent[];
|
|
40
42
|
export declare class IntervalCollector {
|
package/dist/parsers.js
CHANGED
|
@@ -183,6 +183,18 @@ export function codexEvent(value) {
|
|
|
183
183
|
const rawInput = payload.input === undefined && payload.arguments === undefined ? undefined : compact(payload.input ?? payload.arguments);
|
|
184
184
|
return { at: eventAt, turn: { id: idOf(payload) ?? idOf(entry), at: eventAt, role: 'assistant', tools: [{ id, name: string(payload.name ?? payload.tool_name) ?? 'tool', input: codexExecCommand(rawInput), outputLines: 0, outputBytes: 0 }] } };
|
|
185
185
|
}
|
|
186
|
+
// THE ROLLOUT PUTS THE VERDICT IN A THIRD RECORD. Unlike every other source here, a codex result item
|
|
187
|
+
// carries no failure field at all — `function_call_output` and `custom_tool_call_output` have only
|
|
188
|
+
// `{call_id, output}`. The harness records how the command ended in a separate `event_msg`, joined by the
|
|
189
|
+
// same `call_id`: `exec_command_end.status` (completed | failed, 6,194 failed of 83,990 in the rollouts on
|
|
190
|
+
// this box, matching exactly the nonzero exit codes) and `patch_apply_end.success`. Without this second
|
|
191
|
+
// join every failed command in a codex transcript reads as an ordinary one. The outcome is carried alone,
|
|
192
|
+
// with no text, so it lands on the call the output record already filled.
|
|
193
|
+
if (entry.type === 'event_msg' && (type === 'exec_command_end' || type === 'patch_apply_end')) {
|
|
194
|
+
const id = string(payload.call_id);
|
|
195
|
+
const failed = string(payload.status) === 'failed' || payload.success === false;
|
|
196
|
+
return id && failed ? { at: eventAt, turn: null, toolOutputs: [{ id, text: '', outcome: 'failed' }] } : { at: eventAt, turn: null };
|
|
197
|
+
}
|
|
186
198
|
if (entry.type === 'response_item' && (type === 'custom_tool_call_output' || type === 'function_call_output')) {
|
|
187
199
|
const id = string(payload.call_id ?? payload.id);
|
|
188
200
|
const output = payload.output ?? payload.result ?? '';
|
|
@@ -203,7 +215,12 @@ export function codexAppServerEvent(value) {
|
|
|
203
215
|
const recognized = method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed';
|
|
204
216
|
if (!recognized)
|
|
205
217
|
return null;
|
|
206
|
-
|
|
218
|
+
// `emittedAtMs` is a SIBLING of `method` and `params`, not a field inside them — the app-server's own
|
|
219
|
+
// generated envelope type puts it there, and every line of the capture in `fixtures/codex-app-server` has
|
|
220
|
+
// exactly the keys `method`, `params`, `emittedAtMs`. Reading it from `params` found nothing, which made
|
|
221
|
+
// every `item/agentMessage/delta` clockless and therefore dropped: the streaming path parsed and produced
|
|
222
|
+
// nothing at all. The lifecycle clocks below do sit in `params`.
|
|
223
|
+
const eventAt = timestamp(entry.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
|
|
207
224
|
if (eventAt === null)
|
|
208
225
|
return { at: null, turn: null };
|
|
209
226
|
if (method === 'item/agentMessage/delta') {
|
|
@@ -313,6 +330,16 @@ export function piEvent(value) {
|
|
|
313
330
|
turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
|
|
314
331
|
}
|
|
315
332
|
}
|
|
333
|
+
// the producer's verdict on the TURN (`StopReason` in pi's shipped types): `error` is the provider's own
|
|
334
|
+
// failure, `aborted` the turn a stop ended. These are exactly the turns that carry no text and no calls —
|
|
335
|
+
// 13 of 578 assistant messages in the sessions on this box — so without them the turn is an empty gap.
|
|
336
|
+
const stop = string(message.stopReason);
|
|
337
|
+
if (stop === 'error' || stop === 'aborted') {
|
|
338
|
+
turn.outcome = stop === 'aborted' ? 'cancelled' : 'failed';
|
|
339
|
+
const reason = string(message.errorMessage);
|
|
340
|
+
if (reason)
|
|
341
|
+
turn.error = reason;
|
|
342
|
+
}
|
|
316
343
|
return { at: eventAt, turn };
|
|
317
344
|
}
|
|
318
345
|
if (message.role === 'toolResult') {
|
|
@@ -360,37 +387,23 @@ export function geminiEvent(value) {
|
|
|
360
387
|
}
|
|
361
388
|
return null;
|
|
362
389
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
381
|
-
for (const blockValue of items(message.content)) {
|
|
382
|
-
const block = object(blockValue);
|
|
383
|
-
if (block?.type === 'text')
|
|
384
|
-
turn.text = [turn.text, string(block.text)].filter(Boolean).join('\n') || undefined;
|
|
385
|
-
if (block?.type === 'toolCall') {
|
|
386
|
-
const id = string(block.id) ?? `tool-${turn.tools.length}`;
|
|
387
|
-
turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return turn.text || turn.tools.length ? { at: eventAt, turn } : null;
|
|
391
|
-
}
|
|
392
|
-
return null;
|
|
393
|
-
}
|
|
390
|
+
// OPENCLAW WRITES PI'S FORMAT. Verified field by field against pi's shipped schema: the `{"type":"session",
|
|
391
|
+
// "version":3}` header, the 8-char hex id tree, the `user|assistant|toolResult` roles, the `text`/`thinking`/
|
|
392
|
+
// `toolCall` blocks, `toolResult`'s exact key set. The only OpenClaw-specific things are its own `customType`
|
|
393
|
+
// namespace and where the file lives — neither of which a parser reads. So this is not a second parser: two
|
|
394
|
+
// hand-written copies had already drifted into two defects (a different clock preference on identical bytes,
|
|
395
|
+
// and dropping every turn with no text and no calls, which is precisely the failed and aborted ones). Where a
|
|
396
|
+
// harness differs only in where its file sits, the adapter row is the LOCATOR ([[transcript-reader]]), never a
|
|
397
|
+
// second copy of the parse.
|
|
398
|
+
export const openclawEvent = piEvent;
|
|
399
|
+
// HERMES COUNTS IN SECONDS. Its export writes `timestamp` as a float epoch SECOND (`1787942674.556185`),
|
|
400
|
+
// where every other harness here writes milliseconds and `at()` reads a bare number as one. Unconverted, a
|
|
401
|
+
// Hermes turn lands in January 1970 and every interval read of a real thread — the session API hands `from`
|
|
402
|
+
// and `to` as epoch ms — comes back empty. The unit is this producer's, so the conversion is this adapter's.
|
|
403
|
+
const hermesAt = (message) => {
|
|
404
|
+
const seconds = at(message);
|
|
405
|
+
return seconds === null ? null : Math.round(seconds * 1000);
|
|
406
|
+
};
|
|
394
407
|
export function hermesEvents(value) {
|
|
395
408
|
const root = object(value);
|
|
396
409
|
const events = [];
|
|
@@ -398,7 +411,7 @@ export function hermesEvents(value) {
|
|
|
398
411
|
const message = object(messageValue);
|
|
399
412
|
if (!message)
|
|
400
413
|
continue;
|
|
401
|
-
const eventAt =
|
|
414
|
+
const eventAt = hermesAt(message);
|
|
402
415
|
if (eventAt === null) {
|
|
403
416
|
events.push({ at: null, turn: null });
|
|
404
417
|
continue;
|
|
@@ -458,7 +471,8 @@ export function opencodeEvents(value) {
|
|
|
458
471
|
const state = object(part.state);
|
|
459
472
|
const status = (string(state?.status) ?? '').toLowerCase();
|
|
460
473
|
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 };
|
|
461
|
-
|
|
474
|
+
// the terminal states of OpenCode's ToolState union — a call still pending or running has no result yet
|
|
475
|
+
if (/completed|error/.test(status)) {
|
|
462
476
|
const output = compact(state?.output ?? state?.error ?? '');
|
|
463
477
|
tool.output = output.slice(0, MAX_OUTPUT_BYTES);
|
|
464
478
|
tool.outputBytes = Buffer.byteLength(output);
|
package/dist/turns.d.ts
CHANGED
|
@@ -11,12 +11,15 @@ export type TranscriptTool = Readonly<{
|
|
|
11
11
|
outputBytes: number;
|
|
12
12
|
outcome?: 'failed' | 'rejected';
|
|
13
13
|
}>;
|
|
14
|
+
export type TurnOutcome = 'failed' | 'cancelled';
|
|
14
15
|
export type TranscriptTurn = Readonly<{
|
|
15
16
|
id: string;
|
|
16
17
|
at: number;
|
|
17
18
|
role: 'user' | 'assistant';
|
|
18
19
|
text?: string;
|
|
19
20
|
tools?: readonly TranscriptTool[];
|
|
21
|
+
outcome?: TurnOutcome;
|
|
22
|
+
error?: string;
|
|
20
23
|
}>;
|
|
21
24
|
export type TranscriptRead = Readonly<{
|
|
22
25
|
revision: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
|
|
6
6
|
"files": [
|