@agentic-kit/run-log 0.5.0 → 0.6.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
package/README.md CHANGED
@@ -106,10 +106,6 @@ Unreadable input fails loudly. A corrupt record, a mixed-version log, or a sessi
106
106
  - [`@agentic-kit/metering`](../metering) — the metered gateway and usage reporting
107
107
  - [`agentic-server`](../agentic-server) — the metered inference gateway
108
108
 
109
- ## License
110
-
111
- SEE LICENSE IN LICENSE
112
-
113
109
  ---
114
110
 
115
111
  ## Education and Tutorials
@@ -13,7 +13,7 @@
13
13
  import { type SessionProjectionOptions } from './projectors/session';
14
14
  import { type RunEventRecord } from './record';
15
15
  import { type AppendOptions, type RunLogCursor, type RunLogPage, type RunLogStore } from './store';
16
- import type { PiSessionEntry } from './transcripts/pi-entry';
16
+ import type { TranscriptEntry } from './transcripts/entry';
17
17
  export interface FileRunLogStoreOptions {
18
18
  /** Absolute path of the log file. Parent directories are created. */
19
19
  path: string;
@@ -22,7 +22,7 @@ export declare class FileRunLogStore implements RunLogStore {
22
22
  private readonly path;
23
23
  constructor(options: FileRunLogStoreOptions);
24
24
  private load;
25
- append(runId: string, entries: readonly PiSessionEntry[], options?: AppendOptions): Promise<RunEventRecord[]>;
25
+ append(runId: string, entries: readonly TranscriptEntry[], options?: AppendOptions): Promise<RunEventRecord[]>;
26
26
  read(runId: string, cursor?: RunLogCursor, limit?: number): Promise<RunLogPage>;
27
27
  }
28
28
  /**
package/esm/index.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export { constructiveEntryTypes, type EntryTypeDefinition, EntryTypeRegistry, type EntryVisibility } from './entry-types';
10
10
  export { follow, type FollowOptions, readAll } from './follow';
11
11
  export { type SessionEntrySource, SessionMirror, type SessionMirrorOptions } from './mirror';
12
+ export { type EventGroup, type ProjectionOptions, type SequencedEvent, toEventGroups, toEvents } from './projectors/events';
12
13
  export { type BashPart, type Conversation, type ConversationPart, type CustomPart, projectParts, type SummaryPart, type TextPart, type ThinkingPart, type ToolPart, type ToolStatus, type UnknownPart } from './projectors/parts';
13
14
  export { parseSessionJsonl, projectSession, type SessionProjection, type SessionProjectionOptions } from './projectors/session';
14
15
  export { type ApprovalSpan, type ModelSpan, projectSpans, type Span, type SpanBase, type SpanKind, type SpanProjection, type SpanStatus, type SpanTiming, type ToolSpan } from './projectors/spans';
@@ -16,5 +17,10 @@ export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, type ApprovalRequestIn
16
17
  export { modelKey, type ModelUsage, projectUsage, type RunUsage, type UsageTotals } from './projectors/usage';
17
18
  export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, wrapEntry, type WrapEntryOptions } from './record';
18
19
  export { type AppendOptions, cursorAfter, MemoryRunLogStore, type RunLogAppendStore, type RunLogCursor, type RunLogPage, type RunLogReadStore, type RunLogStore, START } from './store';
20
+ export { assertTranscriptEntry, type TranscriptEntry } from './transcripts/entry';
21
+ export { type BashEvent, type CustomEvent, type ModelResponseEvent, type SessionStartEvent, type SummaryEvent, type TextEvent, type ThinkingEvent, type ToolCallEvent, type ToolResultEvent, type TranscriptEvent, type TranscriptEventBase, type TranscriptUsage, type UnknownEvent } from './transcripts/event';
19
22
  export { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION, type TranscriptFormat } from './transcripts/format';
20
23
  export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, type PiAssistantMessage, type PiBashExecutionMessage, type PiBranchSummaryEntry, type PiCompactionEntry, type PiContent, type PiCustomMessage, type PiEntryBase, type PiImageContent, type PiMessage, type PiMessageEntry, type PiOtherEntry, type PiSessionEntry, type PiSessionHeader, type PiSummaryMessage, type PiTextContent, type PiThinkingContent, type PiToolCallContent, type PiToolResultMessage, type PiUsage, type PiUsageCost, type PiUserMessage, toolCalls } from './transcripts/pi-entry';
24
+ export { piEntryToEvents, piTranscriptReader } from './transcripts/pi-reader';
25
+ export { passthroughReader, type TranscriptReader, TranscriptReaderRegistry } from './transcripts/reader';
26
+ export { defaultTranscriptReaders, transcriptReaders } from './transcripts/registry';
package/esm/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  export { constructiveEntryTypes, EntryTypeRegistry } from './entry-types';
10
10
  export { follow, readAll } from './follow';
11
11
  export { SessionMirror } from './mirror';
12
+ export { toEventGroups, toEvents } from './projectors/events';
12
13
  export { projectParts } from './projectors/parts';
13
14
  export { parseSessionJsonl, projectSession } from './projectors/session';
14
15
  export { projectSpans } from './projectors/spans';
@@ -16,5 +17,9 @@ export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage
16
17
  export { modelKey, projectUsage } from './projectors/usage';
17
18
  export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, wrapEntry } from './record';
18
19
  export { cursorAfter, MemoryRunLogStore, START } from './store';
20
+ export { assertTranscriptEntry } from './transcripts/entry';
19
21
  export { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from './transcripts/format';
20
22
  export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './transcripts/pi-entry';
23
+ export { piEntryToEvents, piTranscriptReader } from './transcripts/pi-reader';
24
+ export { passthroughReader, TranscriptReaderRegistry } from './transcripts/reader';
25
+ export { defaultTranscriptReaders, transcriptReaders } from './transcripts/registry';
package/esm/mirror.d.ts CHANGED
@@ -19,8 +19,8 @@
19
19
  */
20
20
  import type { RunEventRecord } from './record';
21
21
  import type { RunLogAppendStore } from './store';
22
- import type { TranscriptFormat } from './transcripts/format';
23
- import { type PiSessionEntry } from './transcripts/pi-entry';
22
+ import { type TranscriptEntry } from './transcripts/entry';
23
+ import { type TranscriptFormat } from './transcripts/format';
24
24
  /** The slice of a harness session manager the mirror needs. */
25
25
  export interface SessionEntrySource {
26
26
  getHeader(): unknown;
@@ -30,11 +30,11 @@ export interface SessionMirrorOptions {
30
30
  runId: string;
31
31
  store: RunLogAppendStore;
32
32
  /**
33
- * Narrow each entry before it is appended. Defaults to the pi transcript
34
- * reader, which is the only entry shape the record currently types; an adapter
35
- * for another transcript supplies its own.
33
+ * Narrow each entry before it is appended. Defaults to the reader registered
34
+ * for `transcriptFormat`, so an adapter for another transcript gets its own
35
+ * validation by registering its reader.
36
36
  */
37
- assertEntry?: (entry: unknown) => PiSessionEntry;
37
+ assertEntry?: (entry: unknown) => TranscriptEntry;
38
38
  /** Transcript format the entries are encoded in, e.g. `pi`. */
39
39
  transcriptFormat?: TranscriptFormat;
40
40
  /** Transcript format version the entries are produced under. */
package/esm/mirror.js CHANGED
@@ -17,7 +17,8 @@
17
17
  * and the format the entries are validated as, so this is testable without a
18
18
  * running agent.
19
19
  */
20
- import { assertPiSessionEntry } from './transcripts/pi-entry';
20
+ import { PI_TRANSCRIPT_FORMAT } from './transcripts/format';
21
+ import { transcriptReaders } from './transcripts/registry';
21
22
  export class SessionMirror {
22
23
  runId;
23
24
  store;
@@ -32,7 +33,9 @@ export class SessionMirror {
32
33
  constructor(options) {
33
34
  this.runId = options.runId;
34
35
  this.store = options.store;
35
- this.assertEntry = options.assertEntry ?? assertPiSessionEntry;
36
+ this.assertEntry =
37
+ options.assertEntry ??
38
+ transcriptReaders.require(options.transcriptFormat ?? PI_TRANSCRIPT_FORMAT).assertEntry;
36
39
  this.transcriptFormat = options.transcriptFormat;
37
40
  this.transcriptVersion = options.transcriptVersion;
38
41
  }
@@ -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,23 @@
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 { transcriptReaders } from '../transcripts/registry';
9
+ /**
10
+ * Read every record with the reader its format names. Grouped by record because
11
+ * a span asks when the *previous entry* was written, which the flattened stream
12
+ * can no longer tell you.
13
+ */
14
+ export function toEventGroups(records, options = {}) {
15
+ const readers = options.readers ?? transcriptReaders;
16
+ return records.map((record) => ({
17
+ seq: record.seq,
18
+ events: readers.require(record.transcriptFormat).toEvents(record.entry)
19
+ }));
20
+ }
21
+ export function toEvents(records, options = {}) {
22
+ return toEventGroups(records, options).flatMap(({ seq, events }) => events.map((event) => ({ seq, event })));
23
+ }
@@ -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;
@@ -1,131 +1,114 @@
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
- import { contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
12
+ import { toEvents } from './events';
11
13
  /**
12
14
  * Project records into a conversation. Pure and total: the same records always
13
- * produce the same parts, whichever host wrote them.
15
+ * produce the same parts, whichever harness wrote them.
14
16
  */
15
- export function projectParts(records) {
17
+ export function projectParts(records, options = {}) {
16
18
  const parts = [];
17
19
  const toolsByCallId = new Map();
18
20
  let sessionId;
19
21
  let cwd;
20
- for (const record of records) {
21
- const { entry, seq } = record;
22
- if (isPiSessionHeader(entry)) {
23
- sessionId = entry.id;
24
- if (typeof entry.cwd === 'string')
25
- cwd = entry.cwd;
26
- continue;
27
- }
28
- if (isPiCompactionEntry(entry)) {
29
- parts.push({ kind: 'summary', reason: 'compaction', summary: entry.summary, seq, entryId: entry.id });
30
- continue;
31
- }
32
- if (isPiBranchSummaryEntry(entry)) {
33
- parts.push({ kind: 'summary', reason: 'branch', summary: entry.summary, seq, entryId: entry.id });
34
- continue;
35
- }
36
- if (!isPiMessageEntry(entry)) {
37
- parts.push({ kind: 'unknown', entryType: entry.type, entry, seq, entryId: entry.id });
38
- continue;
39
- }
40
- const message = entry.message;
41
- const base = { seq, entryId: entry.id };
42
- if (message.role === 'user') {
43
- parts.push({ kind: 'text', role: 'user', text: contentText(message.content), ...base });
44
- continue;
45
- }
46
- if (isAssistantMessage(message)) {
47
- for (const block of Array.isArray(message.content) ? message.content : []) {
48
- if (block.type === 'text' && block.text.length > 0) {
49
- parts.push({
50
- kind: 'text',
51
- role: 'assistant',
52
- text: block.text,
53
- ...(message.model ? { model: message.model } : {}),
54
- ...(message.provider ? { provider: message.provider } : {}),
55
- ...base
56
- });
57
- }
58
- else if (block.type === 'thinking') {
59
- parts.push({ kind: 'thinking', text: block.thinking, ...base });
60
- }
61
- }
62
- for (const call of toolCalls(message)) {
22
+ for (const { seq, event } of toEvents(records, options)) {
23
+ const base = { seq, ...(event.entryId === undefined ? {} : { entryId: event.entryId }) };
24
+ switch (event.kind) {
25
+ case 'session-start':
26
+ if (event.sessionId !== undefined)
27
+ sessionId = event.sessionId;
28
+ if (event.cwd !== undefined)
29
+ cwd = event.cwd;
30
+ break;
31
+ // A model call is a trace and metering concern; its content arrives as the
32
+ // text and tool-call events that follow it.
33
+ case 'model-response':
34
+ break;
35
+ case 'text':
36
+ parts.push({
37
+ kind: 'text',
38
+ role: event.role,
39
+ text: event.text,
40
+ ...(event.model ? { model: event.model } : {}),
41
+ ...(event.provider ? { provider: event.provider } : {}),
42
+ ...base
43
+ });
44
+ break;
45
+ case 'thinking':
46
+ parts.push({ kind: 'thinking', text: event.text, ...base });
47
+ break;
48
+ case 'tool-call': {
63
49
  const part = {
64
50
  kind: 'tool',
65
- toolCallId: call.id,
66
- name: call.name,
67
- arguments: call.arguments ?? {},
51
+ toolCallId: event.toolCallId,
52
+ name: event.name,
53
+ arguments: event.arguments,
68
54
  status: 'requested',
69
55
  ...base
70
56
  };
71
- toolsByCallId.set(call.id, part);
57
+ toolsByCallId.set(event.toolCallId, part);
72
58
  parts.push(part);
59
+ break;
73
60
  }
74
- continue;
75
- }
76
- if (isToolResultMessage(message)) {
77
- const existing = toolsByCallId.get(message.toolCallId);
78
- const output = contentText(message.content);
79
- const status = message.isError ? 'failed' : 'completed';
80
- if (existing) {
81
- existing.status = status;
82
- existing.output = output;
83
- existing.settledSeq = seq;
84
- if (message.details !== undefined)
85
- existing.details = message.details;
61
+ case 'tool-result': {
62
+ const existing = toolsByCallId.get(event.toolCallId);
63
+ const status = event.failed ? 'failed' : 'completed';
64
+ if (existing) {
65
+ existing.status = status;
66
+ existing.output = event.output;
67
+ existing.settledSeq = seq;
68
+ if (event.details !== undefined)
69
+ existing.details = event.details;
70
+ }
71
+ else {
72
+ // A result whose call is not in this window (paged read, forked branch).
73
+ parts.push({
74
+ kind: 'tool',
75
+ toolCallId: event.toolCallId,
76
+ name: event.name,
77
+ arguments: {},
78
+ status,
79
+ output: event.output,
80
+ settledSeq: seq,
81
+ ...base
82
+ });
83
+ }
84
+ break;
86
85
  }
87
- else {
88
- // A result whose call is not in this window (paged read, forked branch).
86
+ case 'bash':
89
87
  parts.push({
90
- kind: 'tool',
91
- toolCallId: message.toolCallId,
92
- name: message.toolName,
93
- arguments: {},
94
- status,
95
- output,
96
- settledSeq: seq,
88
+ kind: 'bash',
89
+ command: event.command,
90
+ output: event.output,
91
+ ...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
97
92
  ...base
98
93
  });
99
- }
100
- continue;
101
- }
102
- if (message.role === 'bashExecution') {
103
- parts.push({
104
- kind: 'bash',
105
- command: message.command,
106
- output: message.output,
107
- ...(typeof message.exitCode === 'number' ? { exitCode: message.exitCode } : {}),
108
- ...base
109
- });
110
- continue;
111
- }
112
- if (message.role === 'custom') {
113
- parts.push({
114
- kind: 'custom',
115
- customType: message.customType,
116
- text: contentText(message.content),
117
- display: message.display !== false,
118
- ...(message.details !== undefined ? { details: message.details } : {}),
119
- ...base
120
- });
121
- continue;
94
+ break;
95
+ case 'custom':
96
+ parts.push({
97
+ kind: 'custom',
98
+ customType: event.customType,
99
+ text: event.text,
100
+ display: event.display,
101
+ ...(event.details === undefined ? {} : { details: event.details }),
102
+ ...base
103
+ });
104
+ break;
105
+ case 'summary':
106
+ parts.push({ kind: 'summary', reason: event.reason, summary: event.summary, ...base });
107
+ break;
108
+ case 'unknown':
109
+ parts.push({ kind: 'unknown', entryType: event.entryType, entry: event.entry, ...base });
110
+ break;
122
111
  }
123
- parts.push({
124
- kind: 'summary',
125
- reason: message.role === 'branchSummary' ? 'branch' : 'compaction',
126
- summary: message.summary ?? '',
127
- ...base
128
- });
129
112
  }
130
113
  return {
131
114
  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';
@@ -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 { assertOrdered } from '../record';
13
18
  import { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from '../transcripts/format';
@@ -25,6 +30,7 @@ export function projectSession(records, options = {}) {
25
30
  throw new Error(`run log mixes pi session versions (${Array.from(versions).sort().join(', ')}); migrate the older entries before projecting a session`);
26
31
  }
27
32
  const transcriptVersion = records[0]?.transcriptVersion ?? SUPPORTED_PI_TRANSCRIPT_VERSION;
33
+ // Safe: every record was just asserted to carry the pi transcript format.
28
34
  const entries = records.map((record) => record.entry);
29
35
  const headerIndex = entries.findIndex(isPiSessionHeader);
30
36
  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;