@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/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
 
@@ -77,17 +78,32 @@ for await (const batch of follow(store, 'run-1', { waitForChange })) {
77
78
  | --- | --- |
78
79
  | `projectParts` | the renderable transcript — a tool call and its later result collapse into one part, so no renderer correlates messages itself |
79
80
  | `projectUsage` | tokens and cost, per `provider/model` and per run, including nested tool usage and compaction calls |
80
- | `projectToolState` | what is running, and what is waiting on a human (approvals ride in the log as pi `custom` messages, so a surface that reconnects hours later still sees a pending request) |
81
+ | `projectToolState` | what is running, what is waiting on a human, and how the gate settled each call (approvals and gate decisions ride in the log as pi `custom` messages, so a surface that reconnects hours later still sees a pending request) |
82
+ | `projectSpans` | how long each step took, and inside what — tool spans (call → result), model spans, approval spans (request → answer) with parents, for a timeline |
81
83
  | `projectSession` | a pi session file — the resume path, cloud → local included |
82
84
 
83
- All four are pure and total: the same records always produce the same output, whichever host wrote them, which is the property the tests assert as *placement invariance*.
85
+ All of them are pure and total: the same records always produce the same output, whichever host wrote them, which is the property the tests assert as *placement invariance*.
86
+
87
+ A span duration derived from a *pair of writes* is a bound, not a measurement — the gap between a tool call and its result includes the wait for a human — so every span says which it is (`timing: 'bounded' | 'measured'`) and a tool span carries its approval wait separately. An interval that never closed stays `open` with no duration, because a run still in flight must not look like one that finished.
88
+
89
+ ## Entry types
90
+
91
+ New event kinds are namespaced pi `custom` messages, not new tables: the log stores entries verbatim, so a type is only a `customType` string plus a validator. `constructiveEntryTypes()` is the registry of the ones this platform writes — approval requests and answers, and `constructive.gate.decision` — each pairing a details type with a runtime assertion, a label, and a visibility:
92
+
93
+ | Visibility | Meaning |
94
+ | --- | --- |
95
+ | `surface` | part of the conversation the model sees — an approval question and its answer |
96
+ | `log-only` | audit and trace data, never fed back to the model — a gate decision, which the model already learned of through the blocked call's error |
97
+
98
+ An unregistered type is `log-only`: a trace view showing an unknown row is right, a prompt built from one is not.
84
99
 
85
100
  Unreadable input fails loudly. A corrupt record, a mixed-version log, or a session whose header is not first throws rather than degrading into an empty transcript — a run that silently renders as blank is worse than one that reports it cannot be read. An *unknown* entry type is different: it is carried through as an `unknown` part, so a log written by a newer pi still renders.
86
101
 
87
102
  ## Related
88
103
 
89
- - [`@agentic-kit/pi`](../pi) — Constructive's typed db tools as a pi extension
90
- - [`@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
91
107
  - [`agentic-server`](../agentic-server) — the metered inference gateway
92
108
 
93
109
  ## License
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The entry-type registry: the one place that says what a `custom` entry in a
3
+ * run log means.
4
+ *
5
+ * pi's session format is extensible through `custom` messages, and Constructive
6
+ * uses that seam rather than new tables — an approval, a gate decision, a future
7
+ * command are all entries in the same append-only log, so a new kind of event
8
+ * costs a registration here and no schema change at all.
9
+ *
10
+ * Two things are registered per type. A `details` guard, because the payload
11
+ * arrives from a database column or an HTTP body and a projector must not read
12
+ * it untyped. And a *visibility*: whether the entry is part of what the model
13
+ * sees (`surface`) or a record only humans and tooling read (`log-only`). pi
14
+ * decides what to replay from the entries it wrote itself, so the flag does not
15
+ * steer the agent loop; it tells a renderer which rows belong to the
16
+ * conversation and which belong to the trace, and it documents intent for the
17
+ * writer.
18
+ */
19
+ /**
20
+ * Whether an entry contributes to the conversation the model sees.
21
+ *
22
+ * - `surface`: the entry is (or becomes) part of the model's context — an
23
+ * approval prompt the agent must react to, for instance.
24
+ * - `log-only`: durable, replayable, projected into traces and audits, but
25
+ * never part of what the model reads. A gate decision is the archetype: the
26
+ * model already learned of a denial through the blocked tool call's error.
27
+ */
28
+ export type EntryVisibility = 'surface' | 'log-only';
29
+ /** What the registry knows about one `customType`. */
30
+ export interface EntryTypeDefinition<TDetails = unknown> {
31
+ /** The namespaced discriminator, e.g. `constructive.gate.decision`. */
32
+ customType: string;
33
+ visibility: EntryVisibility;
34
+ /** Short human label — a renderer's fallback row title. */
35
+ label: string;
36
+ /**
37
+ * Narrow the entry's `details`. Throws rather than returning null: a payload
38
+ * that cannot be read is a corrupted entry, never an empty one.
39
+ */
40
+ assertDetails: (value: unknown) => TDetails;
41
+ }
42
+ /**
43
+ * A registry of `custom` entry types.
44
+ *
45
+ * Registration is by value rather than by module side effect so a host can hold
46
+ * its own registry (a test, a downstream package adding its own vocabulary)
47
+ * without mutating everyone else's.
48
+ */
49
+ export declare class EntryTypeRegistry {
50
+ private readonly types;
51
+ constructor(definitions?: readonly EntryTypeDefinition<never>[]);
52
+ /** Register a type. Re-registering the same `customType` is a programming error. */
53
+ register<TDetails>(definition: EntryTypeDefinition<TDetails>): this;
54
+ /** The definition for a `customType`, or undefined when nothing claims it. */
55
+ get(customType: string): EntryTypeDefinition<unknown> | undefined;
56
+ has(customType: string): boolean;
57
+ /** Every registered type, in registration order. */
58
+ list(): readonly EntryTypeDefinition<unknown>[];
59
+ /**
60
+ * The visibility of a `customType`. An unregistered type is `log-only`: a
61
+ * renderer showing an unknown row in the trace is right, and one feeding it
62
+ * to a model would not be.
63
+ */
64
+ visibilityOf(customType: string): EntryVisibility;
65
+ }
66
+ /**
67
+ * The types Constructive itself writes.
68
+ *
69
+ * A host that adds its own vocabulary registers onto a copy
70
+ * (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
71
+ * package's registration cannot surprise another's projector.
72
+ */
73
+ export declare const constructiveEntryTypes: () => EntryTypeRegistry;
package/entry-types.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ /**
3
+ * The entry-type registry: the one place that says what a `custom` entry in a
4
+ * run log means.
5
+ *
6
+ * pi's session format is extensible through `custom` messages, and Constructive
7
+ * uses that seam rather than new tables — an approval, a gate decision, a future
8
+ * command are all entries in the same append-only log, so a new kind of event
9
+ * costs a registration here and no schema change at all.
10
+ *
11
+ * Two things are registered per type. A `details` guard, because the payload
12
+ * arrives from a database column or an HTTP body and a projector must not read
13
+ * it untyped. And a *visibility*: whether the entry is part of what the model
14
+ * sees (`surface`) or a record only humans and tooling read (`log-only`). pi
15
+ * decides what to replay from the entries it wrote itself, so the flag does not
16
+ * steer the agent loop; it tells a renderer which rows belong to the
17
+ * conversation and which belong to the trace, and it documents intent for the
18
+ * writer.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.constructiveEntryTypes = exports.EntryTypeRegistry = void 0;
22
+ const tool_state_1 = require("./projectors/tool-state");
23
+ /**
24
+ * A registry of `custom` entry types.
25
+ *
26
+ * Registration is by value rather than by module side effect so a host can hold
27
+ * its own registry (a test, a downstream package adding its own vocabulary)
28
+ * without mutating everyone else's.
29
+ */
30
+ class EntryTypeRegistry {
31
+ types = new Map();
32
+ constructor(definitions = []) {
33
+ for (const definition of definitions)
34
+ this.register(definition);
35
+ }
36
+ /** Register a type. Re-registering the same `customType` is a programming error. */
37
+ register(definition) {
38
+ if (this.types.has(definition.customType)) {
39
+ throw new Error(`entry type ${definition.customType} is already registered`);
40
+ }
41
+ this.types.set(definition.customType, definition);
42
+ return this;
43
+ }
44
+ /** The definition for a `customType`, or undefined when nothing claims it. */
45
+ get(customType) {
46
+ return this.types.get(customType);
47
+ }
48
+ has(customType) {
49
+ return this.types.has(customType);
50
+ }
51
+ /** Every registered type, in registration order. */
52
+ list() {
53
+ return Array.from(this.types.values());
54
+ }
55
+ /**
56
+ * The visibility of a `customType`. An unregistered type is `log-only`: a
57
+ * renderer showing an unknown row in the trace is right, and one feeding it
58
+ * to a model would not be.
59
+ */
60
+ visibilityOf(customType) {
61
+ return this.types.get(customType)?.visibility ?? 'log-only';
62
+ }
63
+ }
64
+ exports.EntryTypeRegistry = EntryTypeRegistry;
65
+ /**
66
+ * The types Constructive itself writes.
67
+ *
68
+ * A host that adds its own vocabulary registers onto a copy
69
+ * (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
70
+ * package's registration cannot surprise another's projector.
71
+ */
72
+ const constructiveEntryTypes = () => new EntryTypeRegistry()
73
+ .register({
74
+ customType: tool_state_1.APPROVAL_REQUEST_TYPE,
75
+ // The agent is blocked on this one, and a reconnecting surface must be
76
+ // able to see it — it belongs to the conversation.
77
+ visibility: 'surface',
78
+ label: 'Approval requested',
79
+ assertDetails: tool_state_1.assertApprovalRequestDetails
80
+ })
81
+ .register({
82
+ customType: tool_state_1.APPROVAL_RESOLUTION_TYPE,
83
+ visibility: 'surface',
84
+ label: 'Approval answered',
85
+ assertDetails: tool_state_1.assertApprovalResolutionDetails
86
+ })
87
+ .register({
88
+ customType: tool_state_1.GATE_DECISION_TYPE,
89
+ visibility: 'log-only',
90
+ label: 'Gate decision',
91
+ assertDetails: (value) => (0, tool_state_1.assertGateDecisionDetails)(value)
92
+ });
93
+ exports.constructiveEntryTypes = constructiveEntryTypes;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The entry-type registry: the one place that says what a `custom` entry in a
3
+ * run log means.
4
+ *
5
+ * pi's session format is extensible through `custom` messages, and Constructive
6
+ * uses that seam rather than new tables — an approval, a gate decision, a future
7
+ * command are all entries in the same append-only log, so a new kind of event
8
+ * costs a registration here and no schema change at all.
9
+ *
10
+ * Two things are registered per type. A `details` guard, because the payload
11
+ * arrives from a database column or an HTTP body and a projector must not read
12
+ * it untyped. And a *visibility*: whether the entry is part of what the model
13
+ * sees (`surface`) or a record only humans and tooling read (`log-only`). pi
14
+ * decides what to replay from the entries it wrote itself, so the flag does not
15
+ * steer the agent loop; it tells a renderer which rows belong to the
16
+ * conversation and which belong to the trace, and it documents intent for the
17
+ * writer.
18
+ */
19
+ /**
20
+ * Whether an entry contributes to the conversation the model sees.
21
+ *
22
+ * - `surface`: the entry is (or becomes) part of the model's context — an
23
+ * approval prompt the agent must react to, for instance.
24
+ * - `log-only`: durable, replayable, projected into traces and audits, but
25
+ * never part of what the model reads. A gate decision is the archetype: the
26
+ * model already learned of a denial through the blocked tool call's error.
27
+ */
28
+ export type EntryVisibility = 'surface' | 'log-only';
29
+ /** What the registry knows about one `customType`. */
30
+ export interface EntryTypeDefinition<TDetails = unknown> {
31
+ /** The namespaced discriminator, e.g. `constructive.gate.decision`. */
32
+ customType: string;
33
+ visibility: EntryVisibility;
34
+ /** Short human label — a renderer's fallback row title. */
35
+ label: string;
36
+ /**
37
+ * Narrow the entry's `details`. Throws rather than returning null: a payload
38
+ * that cannot be read is a corrupted entry, never an empty one.
39
+ */
40
+ assertDetails: (value: unknown) => TDetails;
41
+ }
42
+ /**
43
+ * A registry of `custom` entry types.
44
+ *
45
+ * Registration is by value rather than by module side effect so a host can hold
46
+ * its own registry (a test, a downstream package adding its own vocabulary)
47
+ * without mutating everyone else's.
48
+ */
49
+ export declare class EntryTypeRegistry {
50
+ private readonly types;
51
+ constructor(definitions?: readonly EntryTypeDefinition<never>[]);
52
+ /** Register a type. Re-registering the same `customType` is a programming error. */
53
+ register<TDetails>(definition: EntryTypeDefinition<TDetails>): this;
54
+ /** The definition for a `customType`, or undefined when nothing claims it. */
55
+ get(customType: string): EntryTypeDefinition<unknown> | undefined;
56
+ has(customType: string): boolean;
57
+ /** Every registered type, in registration order. */
58
+ list(): readonly EntryTypeDefinition<unknown>[];
59
+ /**
60
+ * The visibility of a `customType`. An unregistered type is `log-only`: a
61
+ * renderer showing an unknown row in the trace is right, and one feeding it
62
+ * to a model would not be.
63
+ */
64
+ visibilityOf(customType: string): EntryVisibility;
65
+ }
66
+ /**
67
+ * The types Constructive itself writes.
68
+ *
69
+ * A host that adds its own vocabulary registers onto a copy
70
+ * (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
71
+ * package's registration cannot surprise another's projector.
72
+ */
73
+ export declare const constructiveEntryTypes: () => EntryTypeRegistry;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The entry-type registry: the one place that says what a `custom` entry in a
3
+ * run log means.
4
+ *
5
+ * pi's session format is extensible through `custom` messages, and Constructive
6
+ * uses that seam rather than new tables — an approval, a gate decision, a future
7
+ * command are all entries in the same append-only log, so a new kind of event
8
+ * costs a registration here and no schema change at all.
9
+ *
10
+ * Two things are registered per type. A `details` guard, because the payload
11
+ * arrives from a database column or an HTTP body and a projector must not read
12
+ * it untyped. And a *visibility*: whether the entry is part of what the model
13
+ * sees (`surface`) or a record only humans and tooling read (`log-only`). pi
14
+ * decides what to replay from the entries it wrote itself, so the flag does not
15
+ * steer the agent loop; it tells a renderer which rows belong to the
16
+ * conversation and which belong to the trace, and it documents intent for the
17
+ * writer.
18
+ */
19
+ import { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE } from './projectors/tool-state';
20
+ /**
21
+ * A registry of `custom` entry types.
22
+ *
23
+ * Registration is by value rather than by module side effect so a host can hold
24
+ * its own registry (a test, a downstream package adding its own vocabulary)
25
+ * without mutating everyone else's.
26
+ */
27
+ export class EntryTypeRegistry {
28
+ types = new Map();
29
+ constructor(definitions = []) {
30
+ for (const definition of definitions)
31
+ this.register(definition);
32
+ }
33
+ /** Register a type. Re-registering the same `customType` is a programming error. */
34
+ register(definition) {
35
+ if (this.types.has(definition.customType)) {
36
+ throw new Error(`entry type ${definition.customType} is already registered`);
37
+ }
38
+ this.types.set(definition.customType, definition);
39
+ return this;
40
+ }
41
+ /** The definition for a `customType`, or undefined when nothing claims it. */
42
+ get(customType) {
43
+ return this.types.get(customType);
44
+ }
45
+ has(customType) {
46
+ return this.types.has(customType);
47
+ }
48
+ /** Every registered type, in registration order. */
49
+ list() {
50
+ return Array.from(this.types.values());
51
+ }
52
+ /**
53
+ * The visibility of a `customType`. An unregistered type is `log-only`: a
54
+ * renderer showing an unknown row in the trace is right, and one feeding it
55
+ * to a model would not be.
56
+ */
57
+ visibilityOf(customType) {
58
+ return this.types.get(customType)?.visibility ?? 'log-only';
59
+ }
60
+ }
61
+ /**
62
+ * The types Constructive itself writes.
63
+ *
64
+ * A host that adds its own vocabulary registers onto a copy
65
+ * (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
66
+ * package's registration cannot surprise another's projector.
67
+ */
68
+ export const constructiveEntryTypes = () => new EntryTypeRegistry()
69
+ .register({
70
+ customType: APPROVAL_REQUEST_TYPE,
71
+ // The agent is blocked on this one, and a reconnecting surface must be
72
+ // able to see it — it belongs to the conversation.
73
+ visibility: 'surface',
74
+ label: 'Approval requested',
75
+ assertDetails: assertApprovalRequestDetails
76
+ })
77
+ .register({
78
+ customType: APPROVAL_RESOLUTION_TYPE,
79
+ visibility: 'surface',
80
+ label: 'Approval answered',
81
+ assertDetails: assertApprovalResolutionDetails
82
+ })
83
+ .register({
84
+ customType: GATE_DECISION_TYPE,
85
+ visibility: 'log-only',
86
+ label: 'Gate decision',
87
+ assertDetails: (value) => assertGateDecisionDetails(value)
88
+ });
@@ -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
@@ -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/esm/index.js 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, EntryTypeRegistry } from './entry-types';
9
10
  export { follow, readAll } from './follow';
10
- export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './pi-entry';
11
+ export { SessionMirror } from './mirror';
11
12
  export { projectParts } from './projectors/parts';
12
13
  export { parseSessionJsonl, projectSession } from './projectors/session';
13
- export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, projectToolState } from './projectors/tool-state';
14
+ export { projectSpans } from './projectors/spans';
15
+ export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, gateDecisionMessage, projectToolState } from './projectors/tool-state';
14
16
  export { modelKey, projectUsage } from './projectors/usage';
15
- 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';
16
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.