@spexcode/transcript 0.7.0-next.5 → 0.7.0-next.7

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 CHANGED
@@ -8,6 +8,7 @@ export type MutableTool = {
8
8
  output?: string;
9
9
  outputLines: number;
10
10
  outputBytes: number;
11
+ outcome?: ToolOutcome;
11
12
  };
12
13
  export type MutableTurn = {
13
14
  id: string | null;
@@ -16,12 +17,14 @@ export type MutableTurn = {
16
17
  text?: string;
17
18
  tools: MutableTool[];
18
19
  };
20
+ export type ToolOutcome = 'failed' | 'rejected';
19
21
  export type ParsedEvent = {
20
22
  at: number | null;
21
23
  turn: MutableTurn | null;
22
24
  toolOutputs?: readonly {
23
25
  id: string;
24
26
  text: string;
27
+ outcome?: ToolOutcome;
25
28
  }[];
26
29
  };
27
30
  export type Parse = (value: unknown) => ParsedEvent | null;
package/dist/parsers.js CHANGED
@@ -50,6 +50,30 @@ const compact = (value) => {
50
50
  return String(value);
51
51
  }
52
52
  };
53
+ // A RESULT IS WHAT THE TOOL SAID, NOT ITS WIRE SHAPE. Every harness that records a result as content blocks
54
+ // (Claude's tool_result content, Codex's input_text output blocks, MCP results everywhere) means the text of
55
+ // those blocks, with their line breaks; encoding the block list itself as JSON would show the reader escaped
56
+ // newlines inside a JSON shell. A block that is not text — an image, a reference — is named, not dumped.
57
+ const resultText = (value) => {
58
+ if (typeof value === 'string')
59
+ return value;
60
+ if (!Array.isArray(value))
61
+ return compact(value);
62
+ return value.map((blockValue) => {
63
+ const block = object(blockValue);
64
+ if (!block)
65
+ return compact(blockValue);
66
+ const text = string(block.text);
67
+ if (text !== null)
68
+ return text;
69
+ const type = string(block.type);
70
+ if (type === 'image')
71
+ return '[image]';
72
+ if (type)
73
+ return `[${type}]`;
74
+ return compact(blockValue);
75
+ }).join('\n');
76
+ };
53
77
  const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
54
78
  // --- the seven native shapes -------------------------------------------------------------------------------
55
79
  export function claudeEvent(value) {
@@ -82,7 +106,7 @@ export function claudeEvent(value) {
82
106
  const outputs = blocks.flatMap((block) => {
83
107
  const b = object(block);
84
108
  const id = string(b?.tool_use_id);
85
- return b?.type === 'tool_result' && id ? [{ id, text: compact(b?.content ?? '') }] : [];
109
+ return b?.type === 'tool_result' && id ? [{ id, text: resultText(b?.content ?? ''), ...(b?.is_error === true ? { outcome: 'failed' } : {}) }] : [];
86
110
  });
87
111
  if (outputs.length)
88
112
  return { at: eventAt, turn: null, toolOutputs: outputs };
@@ -142,7 +166,7 @@ export function codexEvent(value) {
142
166
  if (entry.type === 'response_item' && (type === 'custom_tool_call_output' || type === 'function_call_output')) {
143
167
  const id = string(payload.call_id ?? payload.id);
144
168
  const output = payload.output ?? payload.result ?? '';
145
- return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] } : null;
169
+ return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: resultText(output) }] } : null;
146
170
  }
147
171
  return null;
148
172
  }
@@ -204,9 +228,14 @@ export function codexAppServerEvent(value) {
204
228
  output = item.result ?? item.error;
205
229
  else if (type === 'dynamicToolCall')
206
230
  output = item.contentItems ?? item.output ?? item.error;
207
- return output === undefined || output === null
208
- ? { at: eventAt, turn: null }
209
- : { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] };
231
+ // the item status is the app-server's own verdict: `failed`, or `declined` when the person refused the call —
232
+ // a declined call has no output, so the empty result is what ends its "running"
233
+ const status = string(item.status);
234
+ const outcome = status === 'failed' ? 'failed' : status === 'declined' ? 'rejected' : undefined;
235
+ if (output === undefined || output === null) {
236
+ return outcome ? { at: eventAt, turn: null, toolOutputs: [{ id, text: '', outcome }] } : { at: eventAt, turn: null };
237
+ }
238
+ return { at: eventAt, turn: null, toolOutputs: [{ id, text: resultText(output), ...(outcome ? { outcome } : {}) }] };
210
239
  }
211
240
  // Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
212
241
  // re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
@@ -268,7 +297,7 @@ export function piEvent(value) {
268
297
  }
269
298
  if (message.role === 'toolResult') {
270
299
  const id = string(message.toolCallId);
271
- return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
300
+ return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
272
301
  }
273
302
  return null;
274
303
  }
@@ -303,7 +332,7 @@ export function geminiEvent(value) {
303
332
  for (const callValue of items(message.toolCalls)) {
304
333
  const call = object(callValue);
305
334
  const id = string(call?.id) ?? `tool-${turn.tools.length}`;
306
- turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0 });
335
+ turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0, ...(call?.status === 'error' ? { outcome: 'failed' } : {}) });
307
336
  }
308
337
  if (!turn.text && !turn.tools.length)
309
338
  return null;
@@ -325,7 +354,7 @@ export function openclawEvent(value) {
325
354
  }
326
355
  if (message.role === 'toolResult') {
327
356
  const id = string(message.toolCallId);
328
- return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
357
+ return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
329
358
  }
330
359
  if (message.role === 'assistant') {
331
360
  const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
@@ -414,6 +443,8 @@ export function opencodeEvents(value) {
414
443
  tool.output = output.slice(0, MAX_OUTPUT_BYTES);
415
444
  tool.outputBytes = Buffer.byteLength(output);
416
445
  tool.outputLines = lineCount(output);
446
+ if (status === 'error')
447
+ tool.outcome = 'failed';
417
448
  }
418
449
  turn.tools.push(tool);
419
450
  }
@@ -466,6 +497,8 @@ export class IntervalCollector {
466
497
  tool.outputLines += lineCount(output.text);
467
498
  if (tool.output === undefined)
468
499
  tool.output = '';
500
+ if (output.outcome)
501
+ tool.outcome = output.outcome;
469
502
  const remaining = Math.max(0, MAX_OUTPUT_BYTES - Buffer.byteLength(tool.output));
470
503
  tool.output += output.text.slice(0, remaining);
471
504
  if (bytes > remaining)
package/dist/turns.d.ts CHANGED
@@ -9,6 +9,7 @@ export type TranscriptTool = Readonly<{
9
9
  output?: string;
10
10
  outputLines: number;
11
11
  outputBytes: number;
12
+ outcome?: 'failed' | 'rejected';
12
13
  }>;
13
14
  export type TranscriptTurn = Readonly<{
14
15
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spexcode/transcript",
3
- "version": "0.7.0-next.5",
3
+ "version": "0.7.0-next.7",
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": [