@agentic-kit/run-log 0.2.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 (54) hide show
  1. package/README.md +27 -11
  2. package/entry-types.d.ts +73 -0
  3. package/entry-types.js +93 -0
  4. package/esm/entry-types.d.ts +73 -0
  5. package/esm/entry-types.js +88 -0
  6. package/esm/file-store.d.ts +1 -1
  7. package/esm/file-store.js +3 -2
  8. package/esm/index.d.ts +7 -3
  9. package/esm/index.js +7 -3
  10. package/esm/mirror.d.ts +63 -0
  11. package/esm/mirror.js +87 -0
  12. package/esm/projectors/parts.d.ts +1 -1
  13. package/esm/projectors/parts.js +1 -1
  14. package/esm/projectors/session.d.ts +2 -2
  15. package/esm/projectors/session.js +8 -6
  16. package/esm/projectors/spans.d.ts +84 -0
  17. package/esm/projectors/spans.js +176 -0
  18. package/esm/projectors/tool-state.d.ts +50 -0
  19. package/esm/projectors/tool-state.js +98 -15
  20. package/esm/projectors/usage.d.ts +1 -1
  21. package/esm/projectors/usage.js +2 -2
  22. package/esm/record.d.ts +17 -14
  23. package/esm/record.js +19 -14
  24. package/esm/store.d.ts +6 -3
  25. package/esm/store.js +3 -2
  26. package/esm/transcripts/format.d.ts +25 -0
  27. package/esm/transcripts/format.js +29 -0
  28. package/file-store.d.ts +1 -1
  29. package/file-store.js +2 -1
  30. package/index.d.ts +7 -3
  31. package/index.js +27 -12
  32. package/mirror.d.ts +63 -0
  33. package/mirror.js +91 -0
  34. package/package.json +2 -2
  35. package/projectors/parts.d.ts +1 -1
  36. package/projectors/parts.js +1 -1
  37. package/projectors/session.d.ts +2 -2
  38. package/projectors/session.js +7 -5
  39. package/projectors/spans.d.ts +84 -0
  40. package/projectors/spans.js +179 -0
  41. package/projectors/tool-state.d.ts +50 -0
  42. package/projectors/tool-state.js +103 -16
  43. package/projectors/usage.d.ts +1 -1
  44. package/projectors/usage.js +2 -2
  45. package/record.d.ts +17 -14
  46. package/record.js +20 -15
  47. package/store.d.ts +6 -3
  48. package/store.js +2 -1
  49. package/transcripts/format.d.ts +25 -0
  50. package/transcripts/format.js +33 -0
  51. /package/esm/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
  52. /package/esm/{pi-entry.js → transcripts/pi-entry.js} +0 -0
  53. /package/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
  54. /package/{pi-entry.js → transcripts/pi-entry.js} +0 -0
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;
package/file-store.js CHANGED
@@ -58,7 +58,8 @@ class FileRunLogStore {
58
58
  seq: existing.length + written.length + 1,
59
59
  entry,
60
60
  ...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
61
- piSessionVersion: options.piSessionVersion ?? record_1.SUPPORTED_PI_SESSION_VERSION
61
+ ...(options.transcriptFormat === undefined ? {} : { transcriptFormat: options.transcriptFormat }),
62
+ ...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
62
63
  }));
63
64
  seen.add(key);
64
65
  }
package/index.d.ts CHANGED
@@ -6,11 +6,15 @@
6
6
  * reach for a node builtin. The filesystem store lives behind
7
7
  * `@agentic-kit/run-log/file-store`.
8
8
  */
9
+ export { constructiveEntryTypes, type EntryTypeDefinition, EntryTypeRegistry, type EntryVisibility } from './entry-types';
9
10
  export { follow, type FollowOptions, readAll } from './follow';
10
- 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';
11
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';
12
13
  export { parseSessionJsonl, projectSession, type SessionProjection, type SessionProjectionOptions } from './projectors/session';
13
- export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, type ApprovalRequestInput, approvalRequestMessage, type ApprovalResolutionInput, approvalResolutionMessage, type ApprovalState, projectToolState, type ToolCallState, type ToolCallStatus, type ToolStateProjection } from './projectors/tool-state';
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
+ 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';
14
16
  export { modelKey, type ModelUsage, projectUsage, type RunUsage, type UsageTotals } from './projectors/usage';
15
- 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';
16
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/index.js CHANGED
@@ -8,30 +8,32 @@
8
8
  * `@agentic-kit/run-log/file-store`.
9
9
  */
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.START = exports.MemoryRunLogStore = exports.cursorAfter = exports.wrapEntry = exports.SUPPORTED_PI_SESSION_VERSION = exports.RUN_LOG_WRAPPER_VERSION = exports.idempotencyKey = exports.assertRunEventRecord = exports.assertOrdered = exports.projectUsage = exports.modelKey = exports.projectToolState = exports.approvalResolutionMessage = exports.approvalRequestMessage = exports.APPROVAL_RESOLUTION_TYPE = exports.APPROVAL_REQUEST_TYPE = exports.projectSession = exports.parseSessionJsonl = exports.projectParts = exports.toolCalls = exports.isToolResultMessage = exports.isPiSessionHeader = exports.isPiMessageEntry = exports.isPiCompactionEntry = exports.isPiBranchSummaryEntry = exports.isAssistantMessage = exports.contentText = exports.assertPiSessionEntry = exports.readAll = exports.follow = void 0;
11
+ exports.toolCalls = exports.isToolResultMessage = exports.isPiSessionHeader = exports.isPiMessageEntry = exports.isPiCompactionEntry = exports.isPiBranchSummaryEntry = exports.isAssistantMessage = exports.contentText = exports.assertPiSessionEntry = exports.SUPPORTED_PI_TRANSCRIPT_VERSION = exports.PI_TRANSCRIPT_FORMAT = exports.assertTranscriptFormat = exports.START = exports.MemoryRunLogStore = exports.cursorAfter = exports.wrapEntry = exports.RUN_LOG_WRAPPER_VERSION = exports.idempotencyKey = exports.assertRunEventRecord = exports.assertOrdered = exports.projectUsage = exports.modelKey = exports.projectToolState = exports.gateDecisionMessage = exports.GATE_DECISION_TYPE = exports.assertGateDecisionDetails = exports.assertApprovalResolutionDetails = exports.assertApprovalRequestDetails = exports.approvalResolutionMessage = exports.approvalRequestMessage = exports.APPROVAL_RESOLUTION_TYPE = exports.APPROVAL_REQUEST_TYPE = exports.projectSpans = exports.projectSession = exports.parseSessionJsonl = exports.projectParts = exports.SessionMirror = exports.readAll = exports.follow = exports.EntryTypeRegistry = exports.constructiveEntryTypes = void 0;
12
+ var entry_types_1 = require("./entry-types");
13
+ Object.defineProperty(exports, "constructiveEntryTypes", { enumerable: true, get: function () { return entry_types_1.constructiveEntryTypes; } });
14
+ Object.defineProperty(exports, "EntryTypeRegistry", { enumerable: true, get: function () { return entry_types_1.EntryTypeRegistry; } });
12
15
  var follow_1 = require("./follow");
13
16
  Object.defineProperty(exports, "follow", { enumerable: true, get: function () { return follow_1.follow; } });
14
17
  Object.defineProperty(exports, "readAll", { enumerable: true, get: function () { return follow_1.readAll; } });
15
- var pi_entry_1 = require("./pi-entry");
16
- Object.defineProperty(exports, "assertPiSessionEntry", { enumerable: true, get: function () { return pi_entry_1.assertPiSessionEntry; } });
17
- Object.defineProperty(exports, "contentText", { enumerable: true, get: function () { return pi_entry_1.contentText; } });
18
- Object.defineProperty(exports, "isAssistantMessage", { enumerable: true, get: function () { return pi_entry_1.isAssistantMessage; } });
19
- Object.defineProperty(exports, "isPiBranchSummaryEntry", { enumerable: true, get: function () { return pi_entry_1.isPiBranchSummaryEntry; } });
20
- Object.defineProperty(exports, "isPiCompactionEntry", { enumerable: true, get: function () { return pi_entry_1.isPiCompactionEntry; } });
21
- Object.defineProperty(exports, "isPiMessageEntry", { enumerable: true, get: function () { return pi_entry_1.isPiMessageEntry; } });
22
- Object.defineProperty(exports, "isPiSessionHeader", { enumerable: true, get: function () { return pi_entry_1.isPiSessionHeader; } });
23
- Object.defineProperty(exports, "isToolResultMessage", { enumerable: true, get: function () { return pi_entry_1.isToolResultMessage; } });
24
- Object.defineProperty(exports, "toolCalls", { enumerable: true, get: function () { return pi_entry_1.toolCalls; } });
18
+ var mirror_1 = require("./mirror");
19
+ Object.defineProperty(exports, "SessionMirror", { enumerable: true, get: function () { return mirror_1.SessionMirror; } });
25
20
  var parts_1 = require("./projectors/parts");
26
21
  Object.defineProperty(exports, "projectParts", { enumerable: true, get: function () { return parts_1.projectParts; } });
27
22
  var session_1 = require("./projectors/session");
28
23
  Object.defineProperty(exports, "parseSessionJsonl", { enumerable: true, get: function () { return session_1.parseSessionJsonl; } });
29
24
  Object.defineProperty(exports, "projectSession", { enumerable: true, get: function () { return session_1.projectSession; } });
25
+ var spans_1 = require("./projectors/spans");
26
+ Object.defineProperty(exports, "projectSpans", { enumerable: true, get: function () { return spans_1.projectSpans; } });
30
27
  var tool_state_1 = require("./projectors/tool-state");
31
28
  Object.defineProperty(exports, "APPROVAL_REQUEST_TYPE", { enumerable: true, get: function () { return tool_state_1.APPROVAL_REQUEST_TYPE; } });
32
29
  Object.defineProperty(exports, "APPROVAL_RESOLUTION_TYPE", { enumerable: true, get: function () { return tool_state_1.APPROVAL_RESOLUTION_TYPE; } });
33
30
  Object.defineProperty(exports, "approvalRequestMessage", { enumerable: true, get: function () { return tool_state_1.approvalRequestMessage; } });
34
31
  Object.defineProperty(exports, "approvalResolutionMessage", { enumerable: true, get: function () { return tool_state_1.approvalResolutionMessage; } });
32
+ Object.defineProperty(exports, "assertApprovalRequestDetails", { enumerable: true, get: function () { return tool_state_1.assertApprovalRequestDetails; } });
33
+ Object.defineProperty(exports, "assertApprovalResolutionDetails", { enumerable: true, get: function () { return tool_state_1.assertApprovalResolutionDetails; } });
34
+ Object.defineProperty(exports, "assertGateDecisionDetails", { enumerable: true, get: function () { return tool_state_1.assertGateDecisionDetails; } });
35
+ Object.defineProperty(exports, "GATE_DECISION_TYPE", { enumerable: true, get: function () { return tool_state_1.GATE_DECISION_TYPE; } });
36
+ Object.defineProperty(exports, "gateDecisionMessage", { enumerable: true, get: function () { return tool_state_1.gateDecisionMessage; } });
35
37
  Object.defineProperty(exports, "projectToolState", { enumerable: true, get: function () { return tool_state_1.projectToolState; } });
36
38
  var usage_1 = require("./projectors/usage");
37
39
  Object.defineProperty(exports, "modelKey", { enumerable: true, get: function () { return usage_1.modelKey; } });
@@ -41,9 +43,22 @@ Object.defineProperty(exports, "assertOrdered", { enumerable: true, get: functio
41
43
  Object.defineProperty(exports, "assertRunEventRecord", { enumerable: true, get: function () { return record_1.assertRunEventRecord; } });
42
44
  Object.defineProperty(exports, "idempotencyKey", { enumerable: true, get: function () { return record_1.idempotencyKey; } });
43
45
  Object.defineProperty(exports, "RUN_LOG_WRAPPER_VERSION", { enumerable: true, get: function () { return record_1.RUN_LOG_WRAPPER_VERSION; } });
44
- Object.defineProperty(exports, "SUPPORTED_PI_SESSION_VERSION", { enumerable: true, get: function () { return record_1.SUPPORTED_PI_SESSION_VERSION; } });
45
46
  Object.defineProperty(exports, "wrapEntry", { enumerable: true, get: function () { return record_1.wrapEntry; } });
46
47
  var store_1 = require("./store");
47
48
  Object.defineProperty(exports, "cursorAfter", { enumerable: true, get: function () { return store_1.cursorAfter; } });
48
49
  Object.defineProperty(exports, "MemoryRunLogStore", { enumerable: true, get: function () { return store_1.MemoryRunLogStore; } });
49
50
  Object.defineProperty(exports, "START", { enumerable: true, get: function () { return store_1.START; } });
51
+ var format_1 = require("./transcripts/format");
52
+ Object.defineProperty(exports, "assertTranscriptFormat", { enumerable: true, get: function () { return format_1.assertTranscriptFormat; } });
53
+ Object.defineProperty(exports, "PI_TRANSCRIPT_FORMAT", { enumerable: true, get: function () { return format_1.PI_TRANSCRIPT_FORMAT; } });
54
+ Object.defineProperty(exports, "SUPPORTED_PI_TRANSCRIPT_VERSION", { enumerable: true, get: function () { return format_1.SUPPORTED_PI_TRANSCRIPT_VERSION; } });
55
+ var pi_entry_1 = require("./transcripts/pi-entry");
56
+ Object.defineProperty(exports, "assertPiSessionEntry", { enumerable: true, get: function () { return pi_entry_1.assertPiSessionEntry; } });
57
+ Object.defineProperty(exports, "contentText", { enumerable: true, get: function () { return pi_entry_1.contentText; } });
58
+ Object.defineProperty(exports, "isAssistantMessage", { enumerable: true, get: function () { return pi_entry_1.isAssistantMessage; } });
59
+ Object.defineProperty(exports, "isPiBranchSummaryEntry", { enumerable: true, get: function () { return pi_entry_1.isPiBranchSummaryEntry; } });
60
+ Object.defineProperty(exports, "isPiCompactionEntry", { enumerable: true, get: function () { return pi_entry_1.isPiCompactionEntry; } });
61
+ Object.defineProperty(exports, "isPiMessageEntry", { enumerable: true, get: function () { return pi_entry_1.isPiMessageEntry; } });
62
+ Object.defineProperty(exports, "isPiSessionHeader", { enumerable: true, get: function () { return pi_entry_1.isPiSessionHeader; } });
63
+ Object.defineProperty(exports, "isToolResultMessage", { enumerable: true, get: function () { return pi_entry_1.isToolResultMessage; } });
64
+ Object.defineProperty(exports, "toolCalls", { enumerable: true, get: function () { return pi_entry_1.toolCalls; } });
package/mirror.d.ts ADDED
@@ -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/mirror.js ADDED
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ /**
3
+ * Mirroring a harness's session into the run log.
4
+ *
5
+ * The harness owns the session: it appends entries to its own append-only
6
+ * transcript. Harnesses generally expose no "entry appended" event to subscribe
7
+ * to, so the mirror drains instead — after anything that could have appended, it
8
+ * takes the entries it has not seen yet and appends them to the run log
9
+ * verbatim. Index-based draining is sound precisely because the transcript is
10
+ * append-only: entries are never rewritten or removed, only branched from.
11
+ *
12
+ * A switch/fork/new-session replaces the entry list under the same source, so the
13
+ * read position is keyed to the session header's id and resets when that id
14
+ * changes; entries carried into the new session are absorbed by the store's
15
+ * idempotency (entry ids).
16
+ *
17
+ * Harness-neutral: the adapter owns only the subscription that calls `drain()`
18
+ * and the format the entries are validated as, so this is testable without a
19
+ * running agent.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.SessionMirror = void 0;
23
+ const pi_entry_1 = require("./transcripts/pi-entry");
24
+ class SessionMirror {
25
+ runId;
26
+ store;
27
+ assertEntry;
28
+ transcriptFormat;
29
+ transcriptVersion;
30
+ source = null;
31
+ sessionId = null;
32
+ consumed = 0;
33
+ headerMirrored = false;
34
+ tail = Promise.resolve();
35
+ constructor(options) {
36
+ this.runId = options.runId;
37
+ this.store = options.store;
38
+ this.assertEntry = options.assertEntry ?? pi_entry_1.assertPiSessionEntry;
39
+ this.transcriptFormat = options.transcriptFormat;
40
+ this.transcriptVersion = options.transcriptVersion;
41
+ }
42
+ /** Point the mirror at a session source. Safe to call on every event. */
43
+ bind(source) {
44
+ this.source = source;
45
+ }
46
+ /**
47
+ * Append everything the mirror has not seen yet. Drains are serialized, so
48
+ * concurrent callers cannot interleave batches and break run-log ordering.
49
+ */
50
+ drain() {
51
+ const run = this.tail.then(() => this.flushOnce());
52
+ this.tail = run.then(() => undefined, () => undefined);
53
+ return run;
54
+ }
55
+ async flushOnce() {
56
+ const source = this.source;
57
+ if (!source)
58
+ return [];
59
+ const header = source.getHeader();
60
+ const sessionId = headerSessionId(header);
61
+ if (sessionId !== null && sessionId !== this.sessionId) {
62
+ this.sessionId = sessionId;
63
+ this.consumed = 0;
64
+ this.headerMirrored = false;
65
+ }
66
+ const batch = [];
67
+ if (!this.headerMirrored && header !== null && header !== undefined)
68
+ batch.push(this.assertEntry(header));
69
+ const entries = source.getEntries();
70
+ const upto = entries.length;
71
+ for (let i = this.consumed; i < upto; i += 1)
72
+ batch.push(this.assertEntry(entries[i]));
73
+ if (batch.length === 0)
74
+ return [];
75
+ const written = await this.store.append(this.runId, batch, {
76
+ ...(this.transcriptFormat === undefined ? {} : { transcriptFormat: this.transcriptFormat }),
77
+ ...(this.transcriptVersion === undefined ? {} : { transcriptVersion: this.transcriptVersion })
78
+ });
79
+ // Advance only after a successful append: a failed drain is retried whole.
80
+ this.headerMirrored = true;
81
+ this.consumed = upto;
82
+ return written;
83
+ }
84
+ }
85
+ exports.SessionMirror = SessionMirror;
86
+ function headerSessionId(header) {
87
+ if (typeof header !== 'object' || header === null)
88
+ return null;
89
+ const id = header.id;
90
+ return typeof id === 'string' && id.length > 0 ? id : null;
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentic-kit/run-log",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "The append-only agent run log — pi session entries stored verbatim under a run/seq wrapper, with projections for transcript, usage, tool state and resumable sessions",
6
6
  "main": "index.js",
@@ -35,5 +35,5 @@
35
35
  "run-log",
36
36
  "constructive"
37
37
  ],
38
- "gitHead": "d4bf3a37545daad7246505a96ae8a4b87f44d53c"
38
+ "gitHead": "44a670631c98f7174db0e4c25dc30a3c81fd4437"
39
39
  }
@@ -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. */
@@ -10,7 +10,7 @@
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.projectParts = projectParts;
13
- const pi_entry_1 = require("../pi-entry");
13
+ const pi_entry_1 = require("../transcripts/pi-entry");
14
14
  /**
15
15
  * Project records into a conversation. Pure and total: the same records always
16
16
  * 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
@@ -13,8 +13,9 @@
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.projectSession = projectSession;
15
15
  exports.parseSessionJsonl = parseSessionJsonl;
16
- const pi_entry_1 = require("../pi-entry");
17
16
  const record_1 = require("../record");
17
+ const format_1 = require("../transcripts/format");
18
+ const pi_entry_1 = require("../transcripts/pi-entry");
18
19
  /**
19
20
  * Project records into a pi session file. Throws when the records cannot form a
20
21
  * loadable session — an unresumable session must fail at projection time, not
@@ -22,11 +23,12 @@ const record_1 = require("../record");
22
23
  */
23
24
  function projectSession(records, options = {}) {
24
25
  (0, record_1.assertOrdered)(records);
25
- const versions = new Set(records.map((record) => record.piSessionVersion));
26
+ (0, format_1.assertTranscriptFormat)(records, format_1.PI_TRANSCRIPT_FORMAT);
27
+ const versions = new Set(records.map((record) => record.transcriptVersion));
26
28
  if (versions.size > 1) {
27
29
  throw new Error(`run log mixes pi session versions (${Array.from(versions).sort().join(', ')}); migrate the older entries before projecting a session`);
28
30
  }
29
- const piSessionVersion = records[0]?.piSessionVersion ?? record_1.SUPPORTED_PI_SESSION_VERSION;
31
+ const transcriptVersion = records[0]?.transcriptVersion ?? format_1.SUPPORTED_PI_TRANSCRIPT_VERSION;
30
32
  const entries = records.map((record) => record.entry);
31
33
  const headerIndex = entries.findIndex(pi_entry_1.isPiSessionHeader);
32
34
  if (headerIndex > 0) {
@@ -37,7 +39,7 @@ function projectSession(records, options = {}) {
37
39
  ? entries[0]
38
40
  : {
39
41
  type: 'session',
40
- version: piSessionVersion,
42
+ version: transcriptVersion,
41
43
  id: options.sessionId ?? records[0]?.runId ?? 'run-log',
42
44
  timestamp: options.timestamp ?? records[0]?.recordedAt ?? new Date().toISOString(),
43
45
  ...(options.cwd ? { cwd: options.cwd } : {})
@@ -45,7 +47,7 @@ function projectSession(records, options = {}) {
45
47
  const ordered = [header, ...body];
46
48
  return {
47
49
  entries: ordered,
48
- piSessionVersion,
50
+ transcriptVersion,
49
51
  jsonl: ordered.map((entry) => JSON.stringify(entry)).join('\n') + '\n'
50
52
  };
51
53
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Span projection: run log records → timed intervals with parents.
3
+ *
4
+ * The log answers *what* happened and the parts projection answers *in what
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
7
+ * tool call and its result, or an approval request and its answer, bracket an
8
+ * interval; nothing new has to be stored to know it.
9
+ *
10
+ * Two honesty rules shape the output. A duration derived from a *pair of writes*
11
+ * is a bound, not a measurement — the gap between a tool call and its result
12
+ * includes the wait for a human — so every span says which it is via `timing`,
13
+ * and a tool span carries its approval wait separately so a renderer can show
14
+ * "blocked on you" apart from "running". And an interval that never closed stays
15
+ * `open` with no duration rather than being closed at the last record: a run
16
+ * still in flight must not look like one that finished.
17
+ */
18
+ import type { RunEventRecord } from '../record';
19
+ import { type PiUsage } from '../transcripts/pi-entry';
20
+ export type SpanKind = 'model' | 'tool' | 'approval';
21
+ export type SpanStatus = 'open' | 'ok' | 'error' | 'denied';
22
+ /**
23
+ * Whether the duration is the interval that was measured, or a bound that
24
+ * includes whatever else sat between the two writes.
25
+ */
26
+ export type SpanTiming = 'measured' | 'bounded';
27
+ export interface SpanBase {
28
+ /** Stable within a run — a renderer's key and a parent reference. */
29
+ id: string;
30
+ kind: SpanKind;
31
+ /** Row title: the tool name, the model, or the awaited tool. */
32
+ name: string;
33
+ parentId?: string;
34
+ startSeq: number;
35
+ /** Absent while the span is still open. */
36
+ endSeq?: number;
37
+ startedAt: string;
38
+ endedAt?: string;
39
+ durationMs?: number;
40
+ timing: SpanTiming;
41
+ status: SpanStatus;
42
+ }
43
+ export interface ModelSpan extends SpanBase {
44
+ kind: 'model';
45
+ model?: string;
46
+ provider?: string;
47
+ /** pi's provider response id — the join key to a metered inference row. */
48
+ responseId?: string;
49
+ stopReason?: string;
50
+ usage?: PiUsage;
51
+ errorMessage?: string;
52
+ }
53
+ export interface ToolSpan extends SpanBase {
54
+ kind: 'tool';
55
+ toolCallId: string;
56
+ arguments: Record<string, unknown>;
57
+ output?: string;
58
+ details?: unknown;
59
+ /** Of the total duration, how much was spent waiting on a human. */
60
+ approvalWaitMs?: number;
61
+ /** Set when the gate settled the call instead of an execution. */
62
+ gateDecision?: 'allow' | 'deny';
63
+ gateReason?: string;
64
+ }
65
+ export interface ApprovalSpan extends SpanBase {
66
+ kind: 'approval';
67
+ toolCallId: string;
68
+ prompt: string;
69
+ decision?: 'approved' | 'rejected';
70
+ actorId?: string;
71
+ }
72
+ export type Span = ModelSpan | ToolSpan | ApprovalSpan;
73
+ export interface SpanProjection {
74
+ /** In start order — the order a timeline draws them. */
75
+ spans: Span[];
76
+ }
77
+ /**
78
+ * 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.
83
+ */
84
+ export declare function projectSpans(records: readonly RunEventRecord[]): SpanProjection;