@agentic-kit/run-log 0.5.0 → 0.7.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.
Files changed (64) hide show
  1. package/README.md +0 -4
  2. package/esm/file-store.d.ts +2 -2
  3. package/esm/index.d.ts +6 -0
  4. package/esm/index.js +5 -0
  5. package/esm/mirror.d.ts +6 -6
  6. package/esm/mirror.js +5 -2
  7. package/esm/projectors/events.d.ts +31 -0
  8. package/esm/projectors/events.js +23 -0
  9. package/esm/projectors/parts.d.ts +13 -10
  10. package/esm/projectors/parts.js +88 -105
  11. package/esm/projectors/session.d.ts +5 -0
  12. package/esm/projectors/session.js +6 -0
  13. package/esm/projectors/spans.d.ts +10 -9
  14. package/esm/projectors/spans.js +115 -111
  15. package/esm/projectors/tool-state.d.ts +4 -3
  16. package/esm/projectors/tool-state.js +28 -33
  17. package/esm/projectors/usage.d.ts +3 -2
  18. package/esm/projectors/usage.js +12 -18
  19. package/esm/record.d.ts +9 -6
  20. package/esm/record.js +15 -13
  21. package/esm/store.d.ts +5 -5
  22. package/esm/transcripts/entry.d.ts +20 -0
  23. package/esm/transcripts/entry.js +23 -0
  24. package/esm/transcripts/event.d.ts +118 -0
  25. package/esm/transcripts/event.js +15 -0
  26. package/esm/transcripts/pi-entry.d.ts +1 -0
  27. package/esm/transcripts/pi-reader.d.ts +16 -0
  28. package/esm/transcripts/pi-reader.js +137 -0
  29. package/esm/transcripts/reader.d.ts +45 -0
  30. package/esm/transcripts/reader.js +66 -0
  31. package/esm/transcripts/registry.d.ts +16 -0
  32. package/esm/transcripts/registry.js +17 -0
  33. package/file-store.d.ts +2 -2
  34. package/index.d.ts +6 -0
  35. package/index.js +15 -1
  36. package/mirror.d.ts +6 -6
  37. package/mirror.js +5 -2
  38. package/package.json +2 -2
  39. package/projectors/events.d.ts +31 -0
  40. package/projectors/events.js +27 -0
  41. package/projectors/parts.d.ts +13 -10
  42. package/projectors/parts.js +88 -105
  43. package/projectors/session.d.ts +5 -0
  44. package/projectors/session.js +6 -0
  45. package/projectors/spans.d.ts +10 -9
  46. package/projectors/spans.js +115 -111
  47. package/projectors/tool-state.d.ts +4 -3
  48. package/projectors/tool-state.js +28 -33
  49. package/projectors/usage.d.ts +3 -2
  50. package/projectors/usage.js +12 -18
  51. package/record.d.ts +9 -6
  52. package/record.js +14 -12
  53. package/store.d.ts +5 -5
  54. package/transcripts/entry.d.ts +20 -0
  55. package/transcripts/entry.js +28 -0
  56. package/transcripts/event.d.ts +118 -0
  57. package/transcripts/event.js +16 -0
  58. package/transcripts/pi-entry.d.ts +1 -0
  59. package/transcripts/pi-reader.d.ts +16 -0
  60. package/transcripts/pi-reader.js +141 -0
  61. package/transcripts/reader.d.ts +45 -0
  62. package/transcripts/reader.js +71 -0
  63. package/transcripts/registry.d.ts +16 -0
  64. package/transcripts/registry.js +21 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The step every projector shares: records → neutral events, in log order.
3
+ *
4
+ * Each record names its own transcript format, so this is also where a mixed
5
+ * log stops being a special case: the reader is chosen per record, and a run
6
+ * that changed harness projects as one conversation.
7
+ */
8
+ import type { RunEventRecord } from '../record';
9
+ import type { TranscriptEvent } from '../transcripts/event';
10
+ import type { TranscriptReaderRegistry } from '../transcripts/reader';
11
+ export interface ProjectionOptions {
12
+ /** Readers to interpret the records with. Defaults to the process registry. */
13
+ readers?: TranscriptReaderRegistry;
14
+ }
15
+ /** What one record meant, tagged with its position in the run. */
16
+ export interface EventGroup {
17
+ seq: number;
18
+ events: TranscriptEvent[];
19
+ }
20
+ /** One event, tagged with the position of the record it came from. */
21
+ export interface SequencedEvent {
22
+ seq: number;
23
+ event: TranscriptEvent;
24
+ }
25
+ /**
26
+ * Read every record with the reader its format names. Grouped by record because
27
+ * a span asks when the *previous entry* was written, which the flattened stream
28
+ * can no longer tell you.
29
+ */
30
+ export declare function toEventGroups(records: readonly RunEventRecord[], options?: ProjectionOptions): EventGroup[];
31
+ export declare function toEvents(records: readonly RunEventRecord[], options?: ProjectionOptions): SequencedEvent[];
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * The step every projector shares: records → neutral events, in log order.
4
+ *
5
+ * Each record names its own transcript format, so this is also where a mixed
6
+ * log stops being a special case: the reader is chosen per record, and a run
7
+ * that changed harness projects as one conversation.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.toEventGroups = toEventGroups;
11
+ exports.toEvents = toEvents;
12
+ const registry_1 = require("../transcripts/registry");
13
+ /**
14
+ * Read every record with the reader its format names. Grouped by record because
15
+ * a span asks when the *previous entry* was written, which the flattened stream
16
+ * can no longer tell you.
17
+ */
18
+ function toEventGroups(records, options = {}) {
19
+ const readers = options.readers ?? registry_1.transcriptReaders;
20
+ return records.map((record) => ({
21
+ seq: record.seq,
22
+ events: readers.require(record.transcriptFormat).toEvents(record.entry)
23
+ }));
24
+ }
25
+ function toEvents(records, options = {}) {
26
+ return toEventGroups(records, options).flatMap(({ seq, events }) => events.map((event) => ({ seq, event })));
27
+ }
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * Renderable projection: run log records → an ordered list of parts a UI draws.
3
3
  *
4
- * This is the projection that replaces per-host transcript encodings. A tool
5
- * call and its later result collapse into one part, so a renderer never has to
6
- * correlate two messages itself, and an unknown entry type becomes an
7
- * `unknown` part rather than disappearing — a log written by a newer pi still
8
- * renders, minus the detail this version understands.
4
+ * This is the projection that replaces per-host transcript encodings. It reads
5
+ * neutral transcript events rather than any harness's entries, so a renderer
6
+ * draws a run without knowing which harness produced it. A tool call and its
7
+ * later result collapse into one part, so a renderer never has to correlate two
8
+ * messages itself, and an entry the format's reader does not understand becomes
9
+ * an `unknown` part rather than disappearing — a log written by a newer harness
10
+ * still renders, minus the detail this version understands.
9
11
  */
10
12
  import type { RunEventRecord } from '../record';
11
- import { type PiSessionEntry } from '../transcripts/pi-entry';
13
+ import type { TranscriptEntry } from '../transcripts/entry';
14
+ import { type ProjectionOptions } from './events';
12
15
  export type ToolStatus = 'requested' | 'completed' | 'failed';
13
16
  export interface PartBase {
14
17
  /** Sequence of the record that introduced the part — a stable React key. */
@@ -59,17 +62,17 @@ export interface SummaryPart extends PartBase {
59
62
  export interface UnknownPart extends PartBase {
60
63
  kind: 'unknown';
61
64
  entryType: string;
62
- entry: PiSessionEntry;
65
+ entry: TranscriptEntry;
63
66
  }
64
67
  export type ConversationPart = TextPart | ThinkingPart | ToolPart | BashPart | CustomPart | SummaryPart | UnknownPart;
65
68
  export interface Conversation {
66
69
  parts: ConversationPart[];
67
- /** From the session header, when the log carries one. */
70
+ /** From the transcript's opening entry, when the log carries one. */
68
71
  sessionId?: string;
69
72
  cwd?: string;
70
73
  }
71
74
  /**
72
75
  * Project records into a conversation. Pure and total: the same records always
73
- * produce the same parts, whichever host wrote them.
76
+ * produce the same parts, whichever harness wrote them.
74
77
  */
75
- export declare function projectParts(records: readonly RunEventRecord[]): Conversation;
78
+ export declare function projectParts(records: readonly RunEventRecord[], options?: ProjectionOptions): Conversation;
@@ -2,133 +2,116 @@
2
2
  /**
3
3
  * Renderable projection: run log records → an ordered list of parts a UI draws.
4
4
  *
5
- * This is the projection that replaces per-host transcript encodings. A tool
6
- * call and its later result collapse into one part, so a renderer never has to
7
- * correlate two messages itself, and an unknown entry type becomes an
8
- * `unknown` part rather than disappearing — a log written by a newer pi still
9
- * renders, minus the detail this version understands.
5
+ * This is the projection that replaces per-host transcript encodings. It reads
6
+ * neutral transcript events rather than any harness's entries, so a renderer
7
+ * draws a run without knowing which harness produced it. A tool call and its
8
+ * later result collapse into one part, so a renderer never has to correlate two
9
+ * messages itself, and an entry the format's reader does not understand becomes
10
+ * an `unknown` part rather than disappearing — a log written by a newer harness
11
+ * still renders, minus the detail this version understands.
10
12
  */
11
13
  Object.defineProperty(exports, "__esModule", { value: true });
12
14
  exports.projectParts = projectParts;
13
- const pi_entry_1 = require("../transcripts/pi-entry");
15
+ const events_1 = require("./events");
14
16
  /**
15
17
  * Project records into a conversation. Pure and total: the same records always
16
- * produce the same parts, whichever host wrote them.
18
+ * produce the same parts, whichever harness wrote them.
17
19
  */
18
- function projectParts(records) {
20
+ function projectParts(records, options = {}) {
19
21
  const parts = [];
20
22
  const toolsByCallId = new Map();
21
23
  let sessionId;
22
24
  let cwd;
23
- for (const record of records) {
24
- const { entry, seq } = record;
25
- if ((0, pi_entry_1.isPiSessionHeader)(entry)) {
26
- sessionId = entry.id;
27
- if (typeof entry.cwd === 'string')
28
- cwd = entry.cwd;
29
- continue;
30
- }
31
- if ((0, pi_entry_1.isPiCompactionEntry)(entry)) {
32
- parts.push({ kind: 'summary', reason: 'compaction', summary: entry.summary, seq, entryId: entry.id });
33
- continue;
34
- }
35
- if ((0, pi_entry_1.isPiBranchSummaryEntry)(entry)) {
36
- parts.push({ kind: 'summary', reason: 'branch', summary: entry.summary, seq, entryId: entry.id });
37
- continue;
38
- }
39
- if (!(0, pi_entry_1.isPiMessageEntry)(entry)) {
40
- parts.push({ kind: 'unknown', entryType: entry.type, entry, seq, entryId: entry.id });
41
- continue;
42
- }
43
- const message = entry.message;
44
- const base = { seq, entryId: entry.id };
45
- if (message.role === 'user') {
46
- parts.push({ kind: 'text', role: 'user', text: (0, pi_entry_1.contentText)(message.content), ...base });
47
- continue;
48
- }
49
- if ((0, pi_entry_1.isAssistantMessage)(message)) {
50
- for (const block of Array.isArray(message.content) ? message.content : []) {
51
- if (block.type === 'text' && block.text.length > 0) {
52
- parts.push({
53
- kind: 'text',
54
- role: 'assistant',
55
- text: block.text,
56
- ...(message.model ? { model: message.model } : {}),
57
- ...(message.provider ? { provider: message.provider } : {}),
58
- ...base
59
- });
60
- }
61
- else if (block.type === 'thinking') {
62
- parts.push({ kind: 'thinking', text: block.thinking, ...base });
63
- }
64
- }
65
- for (const call of (0, pi_entry_1.toolCalls)(message)) {
25
+ for (const { seq, event } of (0, events_1.toEvents)(records, options)) {
26
+ const base = { seq, ...(event.entryId === undefined ? {} : { entryId: event.entryId }) };
27
+ switch (event.kind) {
28
+ case 'session-start':
29
+ if (event.sessionId !== undefined)
30
+ sessionId = event.sessionId;
31
+ if (event.cwd !== undefined)
32
+ cwd = event.cwd;
33
+ break;
34
+ // A model call is a trace and metering concern; its content arrives as the
35
+ // text and tool-call events that follow it.
36
+ case 'model-response':
37
+ break;
38
+ case 'text':
39
+ parts.push({
40
+ kind: 'text',
41
+ role: event.role,
42
+ text: event.text,
43
+ ...(event.model ? { model: event.model } : {}),
44
+ ...(event.provider ? { provider: event.provider } : {}),
45
+ ...base
46
+ });
47
+ break;
48
+ case 'thinking':
49
+ parts.push({ kind: 'thinking', text: event.text, ...base });
50
+ break;
51
+ case 'tool-call': {
66
52
  const part = {
67
53
  kind: 'tool',
68
- toolCallId: call.id,
69
- name: call.name,
70
- arguments: call.arguments ?? {},
54
+ toolCallId: event.toolCallId,
55
+ name: event.name,
56
+ arguments: event.arguments,
71
57
  status: 'requested',
72
58
  ...base
73
59
  };
74
- toolsByCallId.set(call.id, part);
60
+ toolsByCallId.set(event.toolCallId, part);
75
61
  parts.push(part);
62
+ break;
76
63
  }
77
- continue;
78
- }
79
- if ((0, pi_entry_1.isToolResultMessage)(message)) {
80
- const existing = toolsByCallId.get(message.toolCallId);
81
- const output = (0, pi_entry_1.contentText)(message.content);
82
- const status = message.isError ? 'failed' : 'completed';
83
- if (existing) {
84
- existing.status = status;
85
- existing.output = output;
86
- existing.settledSeq = seq;
87
- if (message.details !== undefined)
88
- existing.details = message.details;
64
+ case 'tool-result': {
65
+ const existing = toolsByCallId.get(event.toolCallId);
66
+ const status = event.failed ? 'failed' : 'completed';
67
+ if (existing) {
68
+ existing.status = status;
69
+ existing.output = event.output;
70
+ existing.settledSeq = seq;
71
+ if (event.details !== undefined)
72
+ existing.details = event.details;
73
+ }
74
+ else {
75
+ // A result whose call is not in this window (paged read, forked branch).
76
+ parts.push({
77
+ kind: 'tool',
78
+ toolCallId: event.toolCallId,
79
+ name: event.name,
80
+ arguments: {},
81
+ status,
82
+ output: event.output,
83
+ settledSeq: seq,
84
+ ...base
85
+ });
86
+ }
87
+ break;
89
88
  }
90
- else {
91
- // A result whose call is not in this window (paged read, forked branch).
89
+ case 'bash':
92
90
  parts.push({
93
- kind: 'tool',
94
- toolCallId: message.toolCallId,
95
- name: message.toolName,
96
- arguments: {},
97
- status,
98
- output,
99
- settledSeq: seq,
91
+ kind: 'bash',
92
+ command: event.command,
93
+ output: event.output,
94
+ ...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
100
95
  ...base
101
96
  });
102
- }
103
- continue;
104
- }
105
- if (message.role === 'bashExecution') {
106
- parts.push({
107
- kind: 'bash',
108
- command: message.command,
109
- output: message.output,
110
- ...(typeof message.exitCode === 'number' ? { exitCode: message.exitCode } : {}),
111
- ...base
112
- });
113
- continue;
114
- }
115
- if (message.role === 'custom') {
116
- parts.push({
117
- kind: 'custom',
118
- customType: message.customType,
119
- text: (0, pi_entry_1.contentText)(message.content),
120
- display: message.display !== false,
121
- ...(message.details !== undefined ? { details: message.details } : {}),
122
- ...base
123
- });
124
- continue;
97
+ break;
98
+ case 'custom':
99
+ parts.push({
100
+ kind: 'custom',
101
+ customType: event.customType,
102
+ text: event.text,
103
+ display: event.display,
104
+ ...(event.details === undefined ? {} : { details: event.details }),
105
+ ...base
106
+ });
107
+ break;
108
+ case 'summary':
109
+ parts.push({ kind: 'summary', reason: event.reason, summary: event.summary, ...base });
110
+ break;
111
+ case 'unknown':
112
+ parts.push({ kind: 'unknown', entryType: event.entryType, entry: event.entry, ...base });
113
+ break;
125
114
  }
126
- parts.push({
127
- kind: 'summary',
128
- reason: message.role === 'branchSummary' ? 'branch' : 'compaction',
129
- summary: message.summary ?? '',
130
- ...base
131
- });
132
115
  }
133
116
  return {
134
117
  parts,
@@ -8,6 +8,11 @@
8
8
  *
9
9
  * Returns a string rather than writing a file: the projection is pure, and the
10
10
  * node-side write lives in `@agentic-kit/run-log/file-store`.
11
+ *
12
+ * Unlike the other projectors this one is deliberately *not* neutral:
13
+ * re-emitting a transcript in its native encoding is per-format work, so a
14
+ * second harness ships its own session projection beside its reader rather than
15
+ * reusing this. The neutral half of "read a log" is `../transcripts/event`.
11
16
  */
12
17
  import { type RunEventRecord } from '../record';
13
18
  import { type PiSessionEntry } from '../transcripts/pi-entry';
@@ -9,6 +9,11 @@
9
9
  *
10
10
  * Returns a string rather than writing a file: the projection is pure, and the
11
11
  * node-side write lives in `@agentic-kit/run-log/file-store`.
12
+ *
13
+ * Unlike the other projectors this one is deliberately *not* neutral:
14
+ * re-emitting a transcript in its native encoding is per-format work, so a
15
+ * second harness ships its own session projection beside its reader rather than
16
+ * reusing this. The neutral half of "read a log" is `../transcripts/event`.
12
17
  */
13
18
  Object.defineProperty(exports, "__esModule", { value: true });
14
19
  exports.projectSession = projectSession;
@@ -29,6 +34,7 @@ function projectSession(records, options = {}) {
29
34
  throw new Error(`run log mixes pi session versions (${Array.from(versions).sort().join(', ')}); migrate the older entries before projecting a session`);
30
35
  }
31
36
  const transcriptVersion = records[0]?.transcriptVersion ?? format_1.SUPPORTED_PI_TRANSCRIPT_VERSION;
37
+ // Safe: every record was just asserted to carry the pi transcript format.
32
38
  const entries = records.map((record) => record.entry);
33
39
  const headerIndex = entries.findIndex(pi_entry_1.isPiSessionHeader);
34
40
  if (headerIndex > 0) {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The log answers *what* happened and the parts projection answers *in what
5
5
  * order*, but a trace view asks "how long did this take, and inside what" —
6
- * questions pi's entries only answer in pairs. Every entry is timestamped, so a
6
+ * questions a transcript only answers in pairs. Every entry is timestamped, so a
7
7
  * tool call and its result, or an approval request and its answer, bracket an
8
8
  * interval; nothing new has to be stored to know it.
9
9
  *
@@ -16,7 +16,8 @@
16
16
  * still in flight must not look like one that finished.
17
17
  */
18
18
  import type { RunEventRecord } from '../record';
19
- import { type PiUsage } from '../transcripts/pi-entry';
19
+ import type { TranscriptUsage } from '../transcripts/event';
20
+ import { type ProjectionOptions } from './events';
20
21
  export type SpanKind = 'model' | 'tool' | 'approval';
21
22
  export type SpanStatus = 'open' | 'ok' | 'error' | 'denied';
22
23
  /**
@@ -44,10 +45,10 @@ export interface ModelSpan extends SpanBase {
44
45
  kind: 'model';
45
46
  model?: string;
46
47
  provider?: string;
47
- /** pi's provider response id — the join key to a metered inference row. */
48
+ /** The provider's response id — the join key to a metered inference row. */
48
49
  responseId?: string;
49
50
  stopReason?: string;
50
- usage?: PiUsage;
51
+ usage?: TranscriptUsage;
51
52
  errorMessage?: string;
52
53
  }
53
54
  export interface ToolSpan extends SpanBase {
@@ -76,9 +77,9 @@ export interface SpanProjection {
76
77
  }
77
78
  /**
78
79
  * Project records into spans. Pure, and like the other projectors it ignores what
79
- * it does not recognise — a log written by a newer pi still traces. A *registered*
80
- * type that is malformed is another matter and throws: a gate decision naming no
81
- * tool call is a writer bug, and a trace that quietly omitted a refusal would be
82
- * worse than one that fails.
80
+ * it does not recognise — a log written by a newer harness still traces. A
81
+ * *registered* type that is malformed is another matter and throws: a gate
82
+ * decision naming no tool call is a writer bug, and a trace that quietly omitted
83
+ * a refusal would be worse than one that fails.
83
84
  */
84
- export declare function projectSpans(records: readonly RunEventRecord[]): SpanProjection;
85
+ export declare function projectSpans(records: readonly RunEventRecord[], options?: ProjectionOptions): SpanProjection;