@agentic-kit/run-log 0.3.0 → 0.4.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 (48) hide show
  1. package/README.md +11 -9
  2. package/esm/file-store.d.ts +1 -1
  3. package/esm/file-store.js +3 -2
  4. package/esm/index.d.ts +4 -2
  5. package/esm/index.js +4 -2
  6. package/esm/mirror.d.ts +63 -0
  7. package/esm/mirror.js +87 -0
  8. package/esm/projectors/parts.d.ts +1 -1
  9. package/esm/projectors/parts.js +1 -1
  10. package/esm/projectors/session.d.ts +2 -2
  11. package/esm/projectors/session.js +8 -6
  12. package/esm/projectors/spans.d.ts +1 -1
  13. package/esm/projectors/spans.js +1 -1
  14. package/esm/projectors/tool-state.js +1 -1
  15. package/esm/projectors/usage.d.ts +1 -1
  16. package/esm/projectors/usage.js +2 -2
  17. package/esm/record.d.ts +17 -14
  18. package/esm/record.js +19 -14
  19. package/esm/store.d.ts +6 -3
  20. package/esm/store.js +3 -2
  21. package/esm/transcripts/format.d.ts +25 -0
  22. package/esm/transcripts/format.js +29 -0
  23. package/file-store.d.ts +1 -1
  24. package/file-store.js +2 -1
  25. package/index.d.ts +4 -2
  26. package/index.js +17 -12
  27. package/mirror.d.ts +63 -0
  28. package/mirror.js +91 -0
  29. package/package.json +2 -2
  30. package/projectors/parts.d.ts +1 -1
  31. package/projectors/parts.js +1 -1
  32. package/projectors/session.d.ts +2 -2
  33. package/projectors/session.js +7 -5
  34. package/projectors/spans.d.ts +1 -1
  35. package/projectors/spans.js +1 -1
  36. package/projectors/tool-state.js +1 -1
  37. package/projectors/usage.d.ts +1 -1
  38. package/projectors/usage.js +2 -2
  39. package/record.d.ts +17 -14
  40. package/record.js +20 -15
  41. package/store.d.ts +6 -3
  42. package/store.js +2 -1
  43. package/transcripts/format.d.ts +25 -0
  44. package/transcripts/format.js +33 -0
  45. /package/esm/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
  46. /package/esm/{pi-entry.js → transcripts/pi-entry.js} +0 -0
  47. /package/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
  48. /package/{pi-entry.js → transcripts/pi-entry.js} +0 -0
package/README.md CHANGED
@@ -9,21 +9,22 @@ The append-only **run log**: one ordered record of what an agent run did, wherev
9
9
  A coding agent run has to be observable from a desktop app, a web UI and a CLI; it has to be resumable after a crash, a restart, or a move from the cloud to a laptop; and it has to be meterable. Those are usually four subsystems. Here they are four *projections* of one log:
10
10
 
11
11
  ```
12
- pi session entries ──► run log (append-only) ──┬──► transcript parts (what a UI draws)
13
- ├──► usage totals (what a run cost)
14
- ├──► tool + approval (what is blocked)
15
- └──► pi session file (how it resumes)
12
+ harness entries ──► run log (append-only) ──┬──► transcript parts (what a UI draws)
13
+ ├──► usage totals (what a run cost)
14
+ ├──► tool + approval (what is blocked)
15
+ └──► pi session file (how it resumes)
16
16
  ```
17
17
 
18
- The log stores **pi entries verbatim** under four platform-owned fields. pi already versions and migrates its own session format, so re-encoding it into a second semantic event model would mean maintaining a translation layer that silently loses whatever pi adds next.
18
+ The log stores a harness's **entries verbatim** under five platform-owned fields. A harness already versions and migrates its own session format, so re-encoding it into a second semantic event model would mean maintaining a translation layer that silently loses whatever it adds next. `transcriptFormat` is what makes that safe with more than one harness: it travels with the record, so a reader picks its projector by format and only then migrates by version.
19
19
 
20
20
  ```ts
21
21
  {
22
22
  runId, // which run
23
23
  seq, // 1-based, gapless, strictly increasing
24
24
  recordedAt, // when the platform durably recorded it
25
- piSessionVersion, // pi's session format version at write time
26
- entry // the pi entry, byte-for-byte
25
+ transcriptFormat, // which transcript the entry is encoded in, e.g. `pi`
26
+ transcriptVersion, // that transcript's format version at write time
27
+ entry // the harness's entry, byte-for-byte
27
28
  }
28
29
  ```
29
30
 
@@ -100,8 +101,9 @@ Unreadable input fails loudly. A corrupt record, a mixed-version log, or a sessi
100
101
 
101
102
  ## Related
102
103
 
103
- - [`@agentic-kit/pi`](../pi) — Constructive's typed db tools as a pi extension
104
- - [`@agentic-kit/harness`](../harness) — host-neutral gates and policy
104
+ - [`@agentic-kit/pi`](../pi) — the pi adapter: typed db tools, and the lane that mirrors pi's session entries into this log
105
+ - [`@agentic-kit/harness`](../harness) — host-neutral gates, policy, and the harness adapter contract
106
+ - [`@agentic-kit/metering`](../metering) — the metered gateway and usage reporting
105
107
  - [`agentic-server`](../agentic-server) — the metered inference gateway
106
108
 
107
109
  ## License
@@ -10,10 +10,10 @@
10
10
  * dropped. This is the store a local run uses before (or without) a database,
11
11
  * and the one tests use when they want the log to survive a process restart.
12
12
  */
13
- import type { PiSessionEntry } from './pi-entry';
14
13
  import { type SessionProjectionOptions } from './projectors/session';
15
14
  import { type RunEventRecord } from './record';
16
15
  import { type AppendOptions, type RunLogCursor, type RunLogPage, type RunLogStore } from './store';
16
+ import type { PiSessionEntry } from './transcripts/pi-entry';
17
17
  export interface FileRunLogStoreOptions {
18
18
  /** Absolute path of the log file. Parent directories are created. */
19
19
  path: string;
package/esm/file-store.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
14
  import { dirname } from 'node:path';
15
15
  import { projectSession } from './projectors/session';
16
- import { assertRunEventRecord, idempotencyKey, SUPPORTED_PI_SESSION_VERSION, wrapEntry } from './record';
16
+ import { assertRunEventRecord, idempotencyKey, wrapEntry } from './record';
17
17
  import { cursorAfter, START } from './store';
18
18
  export class FileRunLogStore {
19
19
  path;
@@ -54,7 +54,8 @@ export class FileRunLogStore {
54
54
  seq: existing.length + written.length + 1,
55
55
  entry,
56
56
  ...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
57
- piSessionVersion: options.piSessionVersion ?? SUPPORTED_PI_SESSION_VERSION
57
+ ...(options.transcriptFormat === undefined ? {} : { transcriptFormat: options.transcriptFormat }),
58
+ ...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
58
59
  }));
59
60
  seen.add(key);
60
61
  }
package/esm/index.d.ts CHANGED
@@ -8,11 +8,13 @@
8
8
  */
9
9
  export { constructiveEntryTypes, type EntryTypeDefinition, EntryTypeRegistry, type EntryVisibility } from './entry-types';
10
10
  export { follow, type FollowOptions, readAll } from './follow';
11
- 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 './pi-entry';
11
+ export { type SessionEntrySource, SessionMirror, type SessionMirrorOptions } from './mirror';
12
12
  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
13
  export { parseSessionJsonl, projectSession, type SessionProjection, type SessionProjectionOptions } from './projectors/session';
14
14
  export { type ApprovalSpan, type ModelSpan, projectSpans, type Span, type SpanBase, type SpanKind, type SpanProjection, type SpanStatus, type SpanTiming, type ToolSpan } from './projectors/spans';
15
15
  export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, type ApprovalRequestInput, approvalRequestMessage, type ApprovalResolutionInput, approvalResolutionMessage, type ApprovalState, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, type GateDecisionDetails, gateDecisionMessage, type GateDecisionState, projectToolState, type ToolCallState, type ToolCallStatus, type ToolStateProjection } from './projectors/tool-state';
16
16
  export { modelKey, type ModelUsage, projectUsage, type RunUsage, type UsageTotals } from './projectors/usage';
17
- export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, SUPPORTED_PI_SESSION_VERSION, wrapEntry, type WrapEntryOptions } from './record';
17
+ export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, wrapEntry, type WrapEntryOptions } from './record';
18
18
  export { type AppendOptions, cursorAfter, MemoryRunLogStore, type RunLogAppendStore, type RunLogCursor, type RunLogPage, type RunLogReadStore, type RunLogStore, START } from './store';
19
+ export { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION, type TranscriptFormat } from './transcripts/format';
20
+ 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';
package/esm/index.js CHANGED
@@ -8,11 +8,13 @@
8
8
  */
9
9
  export { constructiveEntryTypes, EntryTypeRegistry } from './entry-types';
10
10
  export { follow, readAll } from './follow';
11
- export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './pi-entry';
11
+ export { SessionMirror } from './mirror';
12
12
  export { projectParts } from './projectors/parts';
13
13
  export { parseSessionJsonl, projectSession } from './projectors/session';
14
14
  export { projectSpans } from './projectors/spans';
15
15
  export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, gateDecisionMessage, projectToolState } from './projectors/tool-state';
16
16
  export { modelKey, projectUsage } from './projectors/usage';
17
- export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, SUPPORTED_PI_SESSION_VERSION, wrapEntry } from './record';
17
+ export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, wrapEntry } from './record';
18
18
  export { cursorAfter, MemoryRunLogStore, START } from './store';
19
+ export { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from './transcripts/format';
20
+ export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './transcripts/pi-entry';
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Mirroring a harness's session into the run log.
3
+ *
4
+ * The harness owns the session: it appends entries to its own append-only
5
+ * transcript. Harnesses generally expose no "entry appended" event to subscribe
6
+ * to, so the mirror drains instead — after anything that could have appended, it
7
+ * takes the entries it has not seen yet and appends them to the run log
8
+ * verbatim. Index-based draining is sound precisely because the transcript is
9
+ * append-only: entries are never rewritten or removed, only branched from.
10
+ *
11
+ * A switch/fork/new-session replaces the entry list under the same source, so the
12
+ * read position is keyed to the session header's id and resets when that id
13
+ * changes; entries carried into the new session are absorbed by the store's
14
+ * idempotency (entry ids).
15
+ *
16
+ * Harness-neutral: the adapter owns only the subscription that calls `drain()`
17
+ * and the format the entries are validated as, so this is testable without a
18
+ * running agent.
19
+ */
20
+ import type { RunEventRecord } from './record';
21
+ import type { RunLogAppendStore } from './store';
22
+ import type { TranscriptFormat } from './transcripts/format';
23
+ import { type PiSessionEntry } from './transcripts/pi-entry';
24
+ /** The slice of a harness session manager the mirror needs. */
25
+ export interface SessionEntrySource {
26
+ getHeader(): unknown;
27
+ getEntries(): readonly unknown[];
28
+ }
29
+ export interface SessionMirrorOptions {
30
+ runId: string;
31
+ store: RunLogAppendStore;
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.
36
+ */
37
+ assertEntry?: (entry: unknown) => PiSessionEntry;
38
+ /** Transcript format the entries are encoded in, e.g. `pi`. */
39
+ transcriptFormat?: TranscriptFormat;
40
+ /** Transcript format version the entries are produced under. */
41
+ transcriptVersion?: number;
42
+ }
43
+ export declare class SessionMirror {
44
+ private readonly runId;
45
+ private readonly store;
46
+ private readonly assertEntry;
47
+ private readonly transcriptFormat;
48
+ private readonly transcriptVersion;
49
+ private source;
50
+ private sessionId;
51
+ private consumed;
52
+ private headerMirrored;
53
+ private tail;
54
+ constructor(options: SessionMirrorOptions);
55
+ /** Point the mirror at a session source. Safe to call on every event. */
56
+ bind(source: SessionEntrySource): void;
57
+ /**
58
+ * Append everything the mirror has not seen yet. Drains are serialized, so
59
+ * concurrent callers cannot interleave batches and break run-log ordering.
60
+ */
61
+ drain(): Promise<RunEventRecord[]>;
62
+ private flushOnce;
63
+ }
package/esm/mirror.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Mirroring a harness's session into the run log.
3
+ *
4
+ * The harness owns the session: it appends entries to its own append-only
5
+ * transcript. Harnesses generally expose no "entry appended" event to subscribe
6
+ * to, so the mirror drains instead — after anything that could have appended, it
7
+ * takes the entries it has not seen yet and appends them to the run log
8
+ * verbatim. Index-based draining is sound precisely because the transcript is
9
+ * append-only: entries are never rewritten or removed, only branched from.
10
+ *
11
+ * A switch/fork/new-session replaces the entry list under the same source, so the
12
+ * read position is keyed to the session header's id and resets when that id
13
+ * changes; entries carried into the new session are absorbed by the store's
14
+ * idempotency (entry ids).
15
+ *
16
+ * Harness-neutral: the adapter owns only the subscription that calls `drain()`
17
+ * and the format the entries are validated as, so this is testable without a
18
+ * running agent.
19
+ */
20
+ import { assertPiSessionEntry } from './transcripts/pi-entry';
21
+ export class SessionMirror {
22
+ runId;
23
+ store;
24
+ assertEntry;
25
+ transcriptFormat;
26
+ transcriptVersion;
27
+ source = null;
28
+ sessionId = null;
29
+ consumed = 0;
30
+ headerMirrored = false;
31
+ tail = Promise.resolve();
32
+ constructor(options) {
33
+ this.runId = options.runId;
34
+ this.store = options.store;
35
+ this.assertEntry = options.assertEntry ?? assertPiSessionEntry;
36
+ this.transcriptFormat = options.transcriptFormat;
37
+ this.transcriptVersion = options.transcriptVersion;
38
+ }
39
+ /** Point the mirror at a session source. Safe to call on every event. */
40
+ bind(source) {
41
+ this.source = source;
42
+ }
43
+ /**
44
+ * Append everything the mirror has not seen yet. Drains are serialized, so
45
+ * concurrent callers cannot interleave batches and break run-log ordering.
46
+ */
47
+ drain() {
48
+ const run = this.tail.then(() => this.flushOnce());
49
+ this.tail = run.then(() => undefined, () => undefined);
50
+ return run;
51
+ }
52
+ async flushOnce() {
53
+ const source = this.source;
54
+ if (!source)
55
+ return [];
56
+ const header = source.getHeader();
57
+ const sessionId = headerSessionId(header);
58
+ if (sessionId !== null && sessionId !== this.sessionId) {
59
+ this.sessionId = sessionId;
60
+ this.consumed = 0;
61
+ this.headerMirrored = false;
62
+ }
63
+ const batch = [];
64
+ if (!this.headerMirrored && header !== null && header !== undefined)
65
+ batch.push(this.assertEntry(header));
66
+ const entries = source.getEntries();
67
+ const upto = entries.length;
68
+ for (let i = this.consumed; i < upto; i += 1)
69
+ batch.push(this.assertEntry(entries[i]));
70
+ if (batch.length === 0)
71
+ return [];
72
+ const written = await this.store.append(this.runId, batch, {
73
+ ...(this.transcriptFormat === undefined ? {} : { transcriptFormat: this.transcriptFormat }),
74
+ ...(this.transcriptVersion === undefined ? {} : { transcriptVersion: this.transcriptVersion })
75
+ });
76
+ // Advance only after a successful append: a failed drain is retried whole.
77
+ this.headerMirrored = true;
78
+ this.consumed = upto;
79
+ return written;
80
+ }
81
+ }
82
+ function headerSessionId(header) {
83
+ if (typeof header !== 'object' || header === null)
84
+ return null;
85
+ const id = header.id;
86
+ return typeof id === 'string' && id.length > 0 ? id : null;
87
+ }
@@ -7,8 +7,8 @@
7
7
  * `unknown` part rather than disappearing — a log written by a newer pi still
8
8
  * renders, minus the detail this version understands.
9
9
  */
10
- import { type PiSessionEntry } from '../pi-entry';
11
10
  import type { RunEventRecord } from '../record';
11
+ import { type PiSessionEntry } from '../transcripts/pi-entry';
12
12
  export type ToolStatus = 'requested' | 'completed' | 'failed';
13
13
  export interface PartBase {
14
14
  /** Sequence of the record that introduced the part — a stable React key. */
@@ -7,7 +7,7 @@
7
7
  * `unknown` part rather than disappearing — a log written by a newer pi still
8
8
  * renders, minus the detail this version understands.
9
9
  */
10
- import { contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from '../pi-entry';
10
+ import { contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
11
11
  /**
12
12
  * Project records into a conversation. Pure and total: the same records always
13
13
  * produce the same parts, whichever host wrote them.
@@ -9,8 +9,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
11
  */
12
- import { type PiSessionEntry } from '../pi-entry';
13
12
  import { type RunEventRecord } from '../record';
13
+ import { type PiSessionEntry } from '../transcripts/pi-entry';
14
14
  export interface SessionProjectionOptions {
15
15
  /** Used when the log carries no session header (a run logged headerless). */
16
16
  sessionId?: string;
@@ -22,7 +22,7 @@ export interface SessionProjection {
22
22
  jsonl: string;
23
23
  /** Entries in file order, header first. */
24
24
  entries: PiSessionEntry[];
25
- piSessionVersion: number;
25
+ transcriptVersion: number;
26
26
  }
27
27
  /**
28
28
  * Project records into a pi session file. Throws when the records cannot form a
@@ -9,8 +9,9 @@
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
11
  */
12
- import { isPiSessionHeader } from '../pi-entry';
13
- import { assertOrdered, SUPPORTED_PI_SESSION_VERSION } from '../record';
12
+ import { assertOrdered } from '../record';
13
+ import { assertTranscriptFormat, PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from '../transcripts/format';
14
+ import { isPiSessionHeader } from '../transcripts/pi-entry';
14
15
  /**
15
16
  * Project records into a pi session file. Throws when the records cannot form a
16
17
  * loadable session — an unresumable session must fail at projection time, not
@@ -18,11 +19,12 @@ import { assertOrdered, SUPPORTED_PI_SESSION_VERSION } from '../record';
18
19
  */
19
20
  export function projectSession(records, options = {}) {
20
21
  assertOrdered(records);
21
- const versions = new Set(records.map((record) => record.piSessionVersion));
22
+ assertTranscriptFormat(records, PI_TRANSCRIPT_FORMAT);
23
+ const versions = new Set(records.map((record) => record.transcriptVersion));
22
24
  if (versions.size > 1) {
23
25
  throw new Error(`run log mixes pi session versions (${Array.from(versions).sort().join(', ')}); migrate the older entries before projecting a session`);
24
26
  }
25
- const piSessionVersion = records[0]?.piSessionVersion ?? SUPPORTED_PI_SESSION_VERSION;
27
+ const transcriptVersion = records[0]?.transcriptVersion ?? SUPPORTED_PI_TRANSCRIPT_VERSION;
26
28
  const entries = records.map((record) => record.entry);
27
29
  const headerIndex = entries.findIndex(isPiSessionHeader);
28
30
  if (headerIndex > 0) {
@@ -33,7 +35,7 @@ export function projectSession(records, options = {}) {
33
35
  ? entries[0]
34
36
  : {
35
37
  type: 'session',
36
- version: piSessionVersion,
38
+ version: transcriptVersion,
37
39
  id: options.sessionId ?? records[0]?.runId ?? 'run-log',
38
40
  timestamp: options.timestamp ?? records[0]?.recordedAt ?? new Date().toISOString(),
39
41
  ...(options.cwd ? { cwd: options.cwd } : {})
@@ -41,7 +43,7 @@ export function projectSession(records, options = {}) {
41
43
  const ordered = [header, ...body];
42
44
  return {
43
45
  entries: ordered,
44
- piSessionVersion,
46
+ transcriptVersion,
45
47
  jsonl: ordered.map((entry) => JSON.stringify(entry)).join('\n') + '\n'
46
48
  };
47
49
  }
@@ -15,8 +15,8 @@
15
15
  * `open` with no duration rather than being closed at the last record: a run
16
16
  * still in flight must not look like one that finished.
17
17
  */
18
- import { type PiUsage } from '../pi-entry';
19
18
  import type { RunEventRecord } from '../record';
19
+ import { type PiUsage } from '../transcripts/pi-entry';
20
20
  export type SpanKind = 'model' | 'tool' | 'approval';
21
21
  export type SpanStatus = 'open' | 'ok' | 'error' | 'denied';
22
22
  /**
@@ -15,7 +15,7 @@
15
15
  * `open` with no duration rather than being closed at the last record: a run
16
16
  * still in flight must not look like one that finished.
17
17
  */
18
- import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../pi-entry';
18
+ import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
19
19
  import { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, assertGateDecisionDetails, GATE_DECISION_TYPE } from './tool-state';
20
20
  const ms = (from, to) => {
21
21
  const start = Date.parse(from);
@@ -8,7 +8,7 @@
8
8
  * having been present when it was raised, and both placements then behave the
9
9
  * same — the cloud already treats the conversation as the approval UI.
10
10
  */
11
- import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../pi-entry';
11
+ import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../transcripts/pi-entry';
12
12
  /** `customType` of an approval request written by the gate extension. */
13
13
  export const APPROVAL_REQUEST_TYPE = 'constructive.approval.request';
14
14
  /** `customType` of the human's answer to a request. */
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * This projection describes what a run *observed*. It is reconciliation input,
11
11
  * never the billing authority — a gateway-observed record is (see
12
- * `@agentic-kit/pi-ext-metered-model`).
12
+ * `@agentic-kit/metering`).
13
13
  */
14
14
  import type { RunEventRecord } from '../record';
15
15
  export interface UsageTotals {
@@ -9,9 +9,9 @@
9
9
  *
10
10
  * This projection describes what a run *observed*. It is reconciliation input,
11
11
  * never the billing authority — a gateway-observed record is (see
12
- * `@agentic-kit/pi-ext-metered-model`).
12
+ * `@agentic-kit/metering`).
13
13
  */
14
- import { isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isToolResultMessage } from '../pi-entry';
14
+ import { isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isToolResultMessage } from '../transcripts/pi-entry';
15
15
  const empty = () => ({
16
16
  input: 0,
17
17
  output: 0,
package/esm/record.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * The run log record: four platform-owned fields around a verbatim pi entry.
2
+ * The run log record: five platform-owned fields around a verbatim harness entry.
3
3
  *
4
- * The wrapper exists to give an entry identity (which run) and order (which
5
- * position) across hosts — nothing else. It deliberately does not re-encode pi's
6
- * semantics, because pi already versions and migrates its own session format;
7
- * `piSessionVersion` records which version an entry was written under so a
8
- * reader can hand old entries to pi's migrations instead of guessing.
4
+ * The wrapper exists to give an entry identity (which run), order (which
5
+ * position) and readability (which transcript) across hosts — nothing else. It
6
+ * deliberately does not re-encode a harness's semantics, because a harness
7
+ * already versions and migrates its own session format; `transcriptFormat` says
8
+ * which reader can parse the entry, and `transcriptVersion` says which version
9
+ * that reader must migrate from.
9
10
  */
10
- import { type PiSessionEntry } from './pi-entry';
11
- /** Version of the wrapper itself — bumped only if these four fields change. */
11
+ import { type TranscriptFormat } from './transcripts/format';
12
+ import { type PiSessionEntry } from './transcripts/pi-entry';
13
+ /** Version of the wrapper itself — bumped only if these fields change. */
12
14
  export declare const RUN_LOG_WRAPPER_VERSION = 1;
13
- /** The pi session format version this package projects without migration. */
14
- export declare const SUPPORTED_PI_SESSION_VERSION = 3;
15
15
  export interface RunEventRecord {
16
16
  /** The run this entry belongs to. */
17
17
  runId: string;
@@ -19,9 +19,11 @@ export interface RunEventRecord {
19
19
  seq: number;
20
20
  /** When the platform durably recorded the entry (ISO 8601). */
21
21
  recordedAt: string;
22
- /** pi's session format version at write time. */
23
- piSessionVersion: number;
24
- /** The pi session entry, byte-for-byte as pi produced it. */
22
+ /** The transcript the entry is encoded in, e.g. `pi`. */
23
+ transcriptFormat: TranscriptFormat;
24
+ /** That transcript's format version at write time. */
25
+ transcriptVersion: number;
26
+ /** The harness's session entry, byte-for-byte as it was produced. */
25
27
  entry: PiSessionEntry;
26
28
  }
27
29
  export interface WrapEntryOptions {
@@ -29,7 +31,8 @@ export interface WrapEntryOptions {
29
31
  seq: number;
30
32
  entry: PiSessionEntry;
31
33
  recordedAt?: string;
32
- piSessionVersion?: number;
34
+ transcriptFormat?: TranscriptFormat;
35
+ transcriptVersion?: number;
33
36
  }
34
37
  export declare function wrapEntry(options: WrapEntryOptions): RunEventRecord;
35
38
  /**
package/esm/record.js CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * The run log record: four platform-owned fields around a verbatim pi entry.
2
+ * The run log record: five platform-owned fields around a verbatim harness entry.
3
3
  *
4
- * The wrapper exists to give an entry identity (which run) and order (which
5
- * position) across hosts — nothing else. It deliberately does not re-encode pi's
6
- * semantics, because pi already versions and migrates its own session format;
7
- * `piSessionVersion` records which version an entry was written under so a
8
- * reader can hand old entries to pi's migrations instead of guessing.
4
+ * The wrapper exists to give an entry identity (which run), order (which
5
+ * position) and readability (which transcript) across hosts — nothing else. It
6
+ * deliberately does not re-encode a harness's semantics, because a harness
7
+ * already versions and migrates its own session format; `transcriptFormat` says
8
+ * which reader can parse the entry, and `transcriptVersion` says which version
9
+ * that reader must migrate from.
9
10
  */
10
- import { assertPiSessionEntry } from './pi-entry';
11
- /** Version of the wrapper itself — bumped only if these four fields change. */
11
+ import { PI_TRANSCRIPT_FORMAT, SUPPORTED_PI_TRANSCRIPT_VERSION } from './transcripts/format';
12
+ import { assertPiSessionEntry } from './transcripts/pi-entry';
13
+ /** Version of the wrapper itself — bumped only if these fields change. */
12
14
  export const RUN_LOG_WRAPPER_VERSION = 1;
13
- /** The pi session format version this package projects without migration. */
14
- export const SUPPORTED_PI_SESSION_VERSION = 3;
15
15
  export function wrapEntry(options) {
16
16
  if (!options.runId)
17
17
  throw new TypeError('a run log record needs a runId');
@@ -22,7 +22,8 @@ export function wrapEntry(options) {
22
22
  runId: options.runId,
23
23
  seq: options.seq,
24
24
  recordedAt: options.recordedAt ?? new Date().toISOString(),
25
- piSessionVersion: options.piSessionVersion ?? SUPPORTED_PI_SESSION_VERSION,
25
+ transcriptFormat: options.transcriptFormat ?? PI_TRANSCRIPT_FORMAT,
26
+ transcriptVersion: options.transcriptVersion ?? SUPPORTED_PI_TRANSCRIPT_VERSION,
26
27
  entry: assertPiSessionEntry(options.entry)
27
28
  };
28
29
  }
@@ -45,14 +46,18 @@ export function assertRunEventRecord(value) {
45
46
  if (typeof record.recordedAt !== 'string') {
46
47
  throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry recordedAt`);
47
48
  }
48
- if (!Number.isInteger(record.piSessionVersion)) {
49
- throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry an integer piSessionVersion`);
49
+ if (typeof record.transcriptFormat !== 'string' || record.transcriptFormat.length === 0) {
50
+ throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry a non-empty transcriptFormat`);
51
+ }
52
+ if (!Number.isInteger(record.transcriptVersion)) {
53
+ throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry an integer transcriptVersion`);
50
54
  }
51
55
  return {
52
56
  runId: record.runId,
53
57
  seq: record.seq,
54
58
  recordedAt: record.recordedAt,
55
- piSessionVersion: record.piSessionVersion,
59
+ transcriptFormat: record.transcriptFormat,
60
+ transcriptVersion: record.transcriptVersion,
56
61
  entry: assertPiSessionEntry(record.entry)
57
62
  };
58
63
  }
package/esm/store.d.ts CHANGED
@@ -5,8 +5,9 @@
5
5
  * with their storage — Postgres in constructive-db, JSONL in `./file-store`,
6
6
  * memory here for tests and for a run that has not been persisted yet.
7
7
  */
8
- import type { PiSessionEntry } from './pi-entry';
9
8
  import { type RunEventRecord } from './record';
9
+ import type { TranscriptFormat } from './transcripts/format';
10
+ import type { PiSessionEntry } from './transcripts/pi-entry';
10
11
  /** Read position: `afterSeq` is exclusive, so `0` means "from the start". */
11
12
  export interface RunLogCursor {
12
13
  afterSeq: number;
@@ -18,8 +19,10 @@ export interface RunLogPage {
18
19
  cursor: RunLogCursor;
19
20
  }
20
21
  export interface AppendOptions {
21
- /** pi session format version the entries were produced under. */
22
- piSessionVersion?: number;
22
+ /** Transcript the entries are encoded in. Defaults to pi's. */
23
+ transcriptFormat?: TranscriptFormat;
24
+ /** That transcript's format version the entries were produced under. */
25
+ transcriptVersion?: number;
23
26
  recordedAt?: string;
24
27
  }
25
28
  export interface RunLogAppendStore {
package/esm/store.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * with their storage — Postgres in constructive-db, JSONL in `./file-store`,
6
6
  * memory here for tests and for a run that has not been persisted yet.
7
7
  */
8
- import { assertOrdered, idempotencyKey, SUPPORTED_PI_SESSION_VERSION, wrapEntry } from './record';
8
+ import { assertOrdered, idempotencyKey, wrapEntry } from './record';
9
9
  export const START = { afterSeq: 0 };
10
10
  export const cursorAfter = (records, from = START) => records.length === 0 ? from : { afterSeq: records[records.length - 1].seq };
11
11
  /** In-memory store: the reference implementation and the test double. */
@@ -25,7 +25,8 @@ export class MemoryRunLogStore {
25
25
  seq: records.length + written.length + 1,
26
26
  entry,
27
27
  ...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
28
- piSessionVersion: options.piSessionVersion ?? SUPPORTED_PI_SESSION_VERSION
28
+ ...(options.transcriptFormat === undefined ? {} : { transcriptFormat: options.transcriptFormat }),
29
+ ...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
29
30
  });
30
31
  written.push(record);
31
32
  seen.add(key);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Which transcript a record's entry is encoded in.
3
+ *
4
+ * The log stores a harness's own entries verbatim, so a reader cannot know how
5
+ * to project a record without being told what it is looking at. The format is
6
+ * therefore carried *per record*, not per run: a run resumed under a different
7
+ * harness, or migrated between them, stays one ordered log.
8
+ *
9
+ * Deliberately a string rather than a closed union — a format is a value that
10
+ * ships with an adapter, so adding one must not require a release of this
11
+ * package. Known formats are declared as constants here.
12
+ */
13
+ export type TranscriptFormat = string;
14
+ /** pi's session-file entries (`@earendil-works/pi-coding-agent`). */
15
+ export declare const PI_TRANSCRIPT_FORMAT = "pi";
16
+ /** The pi session format version this package projects without migration. */
17
+ export declare const SUPPORTED_PI_TRANSCRIPT_VERSION = 3;
18
+ /**
19
+ * Narrow a record's format before projecting it. A projector reads one format;
20
+ * handing it another must fail loudly rather than render an empty conversation
21
+ * out of entries it did not understand.
22
+ */
23
+ export declare function assertTranscriptFormat(records: readonly {
24
+ transcriptFormat: TranscriptFormat;
25
+ }[], expected: TranscriptFormat): void;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which transcript a record's entry is encoded in.
3
+ *
4
+ * The log stores a harness's own entries verbatim, so a reader cannot know how
5
+ * to project a record without being told what it is looking at. The format is
6
+ * therefore carried *per record*, not per run: a run resumed under a different
7
+ * harness, or migrated between them, stays one ordered log.
8
+ *
9
+ * Deliberately a string rather than a closed union — a format is a value that
10
+ * ships with an adapter, so adding one must not require a release of this
11
+ * package. Known formats are declared as constants here.
12
+ */
13
+ /** pi's session-file entries (`@earendil-works/pi-coding-agent`). */
14
+ export const PI_TRANSCRIPT_FORMAT = 'pi';
15
+ /** The pi session format version this package projects without migration. */
16
+ export const SUPPORTED_PI_TRANSCRIPT_VERSION = 3;
17
+ /**
18
+ * Narrow a record's format before projecting it. A projector reads one format;
19
+ * handing it another must fail loudly rather than render an empty conversation
20
+ * out of entries it did not understand.
21
+ */
22
+ export function assertTranscriptFormat(records, expected) {
23
+ const formats = new Set(records.map((record) => record.transcriptFormat));
24
+ formats.delete(expected);
25
+ if (formats.size === 0)
26
+ return;
27
+ throw new Error(`run log carries transcript format(s) ${Array.from(formats).sort().join(', ')}; ` +
28
+ `this projector reads "${expected}" entries only`);
29
+ }
package/file-store.d.ts CHANGED
@@ -10,10 +10,10 @@
10
10
  * dropped. This is the store a local run uses before (or without) a database,
11
11
  * and the one tests use when they want the log to survive a process restart.
12
12
  */
13
- import type { PiSessionEntry } from './pi-entry';
14
13
  import { type SessionProjectionOptions } from './projectors/session';
15
14
  import { type RunEventRecord } from './record';
16
15
  import { type AppendOptions, type RunLogCursor, type RunLogPage, type RunLogStore } from './store';
16
+ import type { PiSessionEntry } from './transcripts/pi-entry';
17
17
  export interface FileRunLogStoreOptions {
18
18
  /** Absolute path of the log file. Parent directories are created. */
19
19
  path: string;