@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
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Span projection: run log records → timed intervals with parents.
4
+ *
5
+ * The log answers *what* happened and the parts projection answers *in what
6
+ * order*, but a trace view asks "how long did this take, and inside what" —
7
+ * questions pi's entries only answer in pairs. Every entry is timestamped, so a
8
+ * tool call and its result, or an approval request and its answer, bracket an
9
+ * interval; nothing new has to be stored to know it.
10
+ *
11
+ * Two honesty rules shape the output. A duration derived from a *pair of writes*
12
+ * is a bound, not a measurement — the gap between a tool call and its result
13
+ * includes the wait for a human — so every span says which it is via `timing`,
14
+ * and a tool span carries its approval wait separately so a renderer can show
15
+ * "blocked on you" apart from "running". And an interval that never closed stays
16
+ * `open` with no duration rather than being closed at the last record: a run
17
+ * still in flight must not look like one that finished.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.projectSpans = projectSpans;
21
+ const pi_entry_1 = require("../transcripts/pi-entry");
22
+ const tool_state_1 = require("./tool-state");
23
+ const ms = (from, to) => {
24
+ const start = Date.parse(from);
25
+ const end = Date.parse(to);
26
+ if (Number.isNaN(start) || Number.isNaN(end))
27
+ return undefined;
28
+ return end - start;
29
+ };
30
+ const close = (span, seq, at) => {
31
+ span.endSeq = seq;
32
+ span.endedAt = at;
33
+ const duration = ms(span.startedAt, at);
34
+ if (duration !== undefined)
35
+ span.durationMs = duration;
36
+ };
37
+ const detailsOf = (value) => typeof value === 'object' && value !== null ? value : {};
38
+ /**
39
+ * Project records into spans. Pure, and like the other projectors it ignores what
40
+ * it does not recognise — a log written by a newer pi still traces. A *registered*
41
+ * type that is malformed is another matter and throws: a gate decision naming no
42
+ * tool call is a writer bug, and a trace that quietly omitted a refusal would be
43
+ * worse than one that fails.
44
+ */
45
+ function projectSpans(records) {
46
+ const spans = [];
47
+ const tools = new Map();
48
+ const approvals = new Map();
49
+ /**
50
+ * The end of the previous entry, which is the earliest the next model call
51
+ * can have begun — pi logs a response, never a request.
52
+ */
53
+ let previousAt;
54
+ for (const { entry, seq } of records) {
55
+ const at = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
56
+ if (at === undefined)
57
+ continue;
58
+ const previous = previousAt;
59
+ previousAt = at;
60
+ if (!(0, pi_entry_1.isPiMessageEntry)(entry))
61
+ continue;
62
+ const message = entry.message;
63
+ if ((0, pi_entry_1.isAssistantMessage)(message)) {
64
+ const span = {
65
+ id: `model:${entry.id}`,
66
+ kind: 'model',
67
+ name: message.model ?? 'model',
68
+ // The turn began no earlier than the previous entry was written; with no
69
+ // previous entry the interval is unknown and the span is a point.
70
+ startedAt: previous ?? at,
71
+ startSeq: seq,
72
+ endSeq: seq,
73
+ endedAt: at,
74
+ timing: 'bounded',
75
+ status: message.stopReason === 'error' ? 'error' : 'ok',
76
+ ...(message.model ? { model: message.model } : {}),
77
+ ...(message.provider ? { provider: message.provider } : {}),
78
+ ...(typeof message.responseId === 'string' ? { responseId: message.responseId } : {}),
79
+ ...(message.stopReason ? { stopReason: message.stopReason } : {}),
80
+ ...(message.usage ? { usage: message.usage } : {}),
81
+ ...(message.errorMessage ? { errorMessage: message.errorMessage } : {})
82
+ };
83
+ const duration = ms(span.startedAt, at);
84
+ if (duration !== undefined)
85
+ span.durationMs = duration;
86
+ spans.push(span);
87
+ for (const call of (0, pi_entry_1.toolCalls)(message)) {
88
+ const tool = {
89
+ id: `tool:${call.id}`,
90
+ kind: 'tool',
91
+ name: call.name,
92
+ parentId: span.id,
93
+ toolCallId: call.id,
94
+ arguments: call.arguments ?? {},
95
+ startedAt: at,
96
+ startSeq: seq,
97
+ // A call and its result are two writes; anything between them —
98
+ // approval, queueing — is inside the interval.
99
+ timing: 'bounded',
100
+ status: 'open'
101
+ };
102
+ tools.set(call.id, tool);
103
+ spans.push(tool);
104
+ }
105
+ continue;
106
+ }
107
+ if ((0, pi_entry_1.isToolResultMessage)(message)) {
108
+ const tool = tools.get(message.toolCallId);
109
+ if (!tool)
110
+ continue;
111
+ close(tool, seq, at);
112
+ tool.status = message.isError ? 'error' : 'ok';
113
+ tool.output = (0, pi_entry_1.contentText)(message.content);
114
+ if (message.details !== undefined)
115
+ tool.details = message.details;
116
+ const approval = approvals.get(message.toolCallId);
117
+ if (approval?.durationMs !== undefined)
118
+ tool.approvalWaitMs = approval.durationMs;
119
+ continue;
120
+ }
121
+ if (message.role !== 'custom')
122
+ continue;
123
+ const info = detailsOf(message.details);
124
+ const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : undefined;
125
+ if (toolCallId === undefined)
126
+ continue;
127
+ if (message.customType === tool_state_1.APPROVAL_REQUEST_TYPE) {
128
+ const tool = tools.get(toolCallId);
129
+ const approval = {
130
+ id: `approval:${toolCallId}`,
131
+ kind: 'approval',
132
+ name: tool?.name ?? 'approval',
133
+ ...(tool ? { parentId: tool.id } : {}),
134
+ toolCallId,
135
+ prompt: (0, pi_entry_1.contentText)(message.content),
136
+ startedAt: at,
137
+ startSeq: seq,
138
+ // Request and answer are exactly the interval a human held the run.
139
+ timing: 'measured',
140
+ status: 'open'
141
+ };
142
+ approvals.set(toolCallId, approval);
143
+ spans.push(approval);
144
+ continue;
145
+ }
146
+ if (message.customType === tool_state_1.APPROVAL_RESOLUTION_TYPE) {
147
+ const approval = approvals.get(toolCallId);
148
+ if (!approval)
149
+ continue;
150
+ close(approval, seq, at);
151
+ const approved = info.decision === 'approved';
152
+ approval.decision = approved ? 'approved' : 'rejected';
153
+ approval.status = approved ? 'ok' : 'denied';
154
+ if (typeof info.actorId === 'string')
155
+ approval.actorId = info.actorId;
156
+ const tool = tools.get(toolCallId);
157
+ if (tool && approval.durationMs !== undefined)
158
+ tool.approvalWaitMs = approval.durationMs;
159
+ continue;
160
+ }
161
+ if (message.customType === tool_state_1.GATE_DECISION_TYPE) {
162
+ // Throws on a malformed decision — see the note on this function.
163
+ const decision = (0, tool_state_1.assertGateDecisionDetails)(message.details, seq);
164
+ const tool = tools.get(decision.toolCallId);
165
+ if (!tool)
166
+ continue;
167
+ tool.gateDecision = decision.decision;
168
+ if (decision.reason !== undefined)
169
+ tool.gateReason = decision.reason;
170
+ // A denied call is never executed, so this write is the only thing that
171
+ // will ever close its span.
172
+ if (decision.decision === 'deny' && tool.status === 'open') {
173
+ close(tool, seq, at);
174
+ tool.status = 'denied';
175
+ }
176
+ }
177
+ }
178
+ return { spans };
179
+ }
@@ -13,6 +13,14 @@ import type { RunEventRecord } from '../record';
13
13
  export declare const APPROVAL_REQUEST_TYPE = "constructive.approval.request";
14
14
  /** `customType` of the human's answer to a request. */
15
15
  export declare const APPROVAL_RESOLUTION_TYPE = "constructive.approval.resolution";
16
+ /**
17
+ * `customType` of a settled gate decision.
18
+ *
19
+ * Unlike an approval, this is written for *every* decision including the ones
20
+ * no human saw: a policy rule that denies a tool call is otherwise invisible in
21
+ * the log, leaving "which tool calls did the gate block" unanswerable.
22
+ */
23
+ export declare const GATE_DECISION_TYPE = "constructive.gate.decision";
16
24
  export type ToolCallStatus = 'requested' | 'awaiting-approval' | 'rejected' | 'running' | 'completed' | 'failed';
17
25
  export interface ToolCallState {
18
26
  toolCallId: string;
@@ -33,12 +41,37 @@ export interface ApprovalState {
33
41
  reason?: string;
34
42
  actorId?: string;
35
43
  }
44
+ /** A settled gate decision as it is carried in the log. */
45
+ export interface GateDecisionDetails {
46
+ toolCallId: string;
47
+ toolName: string;
48
+ /** What the policy said: `allow`, `deny` or `ask`. */
49
+ verdict: string;
50
+ /** How the call was finally settled — `ask` resolves to one of these. */
51
+ decision: 'allow' | 'deny';
52
+ reason?: string;
53
+ actorId?: string;
54
+ decidedAt?: string;
55
+ }
56
+ export interface GateDecisionState extends GateDecisionDetails {
57
+ seq: number;
58
+ }
36
59
  export interface ToolStateProjection {
37
60
  tools: Record<string, ToolCallState>;
61
+ /** Settled gate decisions in log order, keyed by tool call id. */
62
+ gateDecisions: Record<string, GateDecisionState>;
38
63
  /** In log order — the oldest unanswered request first. */
39
64
  pendingApprovals: ApprovalState[];
40
65
  }
41
66
  export declare function projectToolState(records: readonly RunEventRecord[]): ToolStateProjection;
67
+ /** Narrow the `details` of a `constructive.gate.decision` entry. */
68
+ export declare function assertGateDecisionDetails(value: unknown, seq?: number): GateDecisionDetails;
69
+ /** Narrow the `details` of a `constructive.approval.request` entry. */
70
+ export declare function assertApprovalRequestDetails(value: unknown, seq?: number): {
71
+ toolCallId: string;
72
+ };
73
+ /** Narrow the `details` of a `constructive.approval.resolution` entry. */
74
+ export declare function assertApprovalResolutionDetails(value: unknown, seq?: number): ApprovalResolutionInput;
42
75
  /** The parts of an approval request an extension needs to write one. */
43
76
  export interface ApprovalRequestInput {
44
77
  toolCallId: string;
@@ -61,6 +94,23 @@ export declare const approvalRequestMessage: (input: ApprovalRequestInput) => {
61
94
  };
62
95
  timestamp: number;
63
96
  };
97
+ /** Build the pi `custom` message a settled gate decision is carried in. */
98
+ export declare const gateDecisionMessage: (input: GateDecisionDetails) => {
99
+ role: "custom";
100
+ customType: string;
101
+ content: string;
102
+ display: boolean;
103
+ details: {
104
+ decidedAt?: string;
105
+ actorId?: string;
106
+ reason?: string;
107
+ toolCallId: string;
108
+ toolName: string;
109
+ verdict: string;
110
+ decision: "allow" | "deny";
111
+ };
112
+ timestamp: number;
113
+ };
64
114
  /** Build the pi `custom` message a human's answer is carried in. */
65
115
  export declare const approvalResolutionMessage: (input: ApprovalResolutionInput) => {
66
116
  role: "custom";
@@ -10,17 +10,31 @@
10
10
  * same — the cloud already treats the conversation as the approval UI.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
- exports.approvalResolutionMessage = exports.approvalRequestMessage = exports.APPROVAL_RESOLUTION_TYPE = exports.APPROVAL_REQUEST_TYPE = void 0;
13
+ exports.approvalResolutionMessage = exports.gateDecisionMessage = exports.approvalRequestMessage = exports.GATE_DECISION_TYPE = exports.APPROVAL_RESOLUTION_TYPE = exports.APPROVAL_REQUEST_TYPE = void 0;
14
14
  exports.projectToolState = projectToolState;
15
- const pi_entry_1 = require("../pi-entry");
15
+ exports.assertGateDecisionDetails = assertGateDecisionDetails;
16
+ exports.assertApprovalRequestDetails = assertApprovalRequestDetails;
17
+ exports.assertApprovalResolutionDetails = assertApprovalResolutionDetails;
18
+ const pi_entry_1 = require("../transcripts/pi-entry");
16
19
  /** `customType` of an approval request written by the gate extension. */
17
20
  exports.APPROVAL_REQUEST_TYPE = 'constructive.approval.request';
18
21
  /** `customType` of the human's answer to a request. */
19
22
  exports.APPROVAL_RESOLUTION_TYPE = 'constructive.approval.resolution';
23
+ /**
24
+ * `customType` of a settled gate decision.
25
+ *
26
+ * Unlike an approval, this is written for *every* decision including the ones
27
+ * no human saw: a policy rule that denies a tool call is otherwise invisible in
28
+ * the log, leaving "which tool calls did the gate block" unanswerable.
29
+ */
30
+ exports.GATE_DECISION_TYPE = 'constructive.gate.decision';
31
+ /** Where a bad entry is, when the caller is projecting a log rather than validating one value. */
32
+ const at = (seq) => (seq === undefined ? '' : ` at seq ${String(seq)}`);
20
33
  const details = (value) => typeof value === 'object' && value !== null ? value : {};
21
34
  function projectToolState(records) {
22
35
  const tools = {};
23
36
  const approvals = new Map();
37
+ const gateDecisions = {};
24
38
  for (const { entry, seq } of records) {
25
39
  if (!(0, pi_entry_1.isPiMessageEntry)(entry))
26
40
  continue;
@@ -60,11 +74,7 @@ function projectToolState(records) {
60
74
  if (message.role !== 'custom')
61
75
  continue;
62
76
  if (message.customType === exports.APPROVAL_REQUEST_TYPE) {
63
- const info = details(message.details);
64
- const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : null;
65
- if (!toolCallId) {
66
- throw new Error(`approval request at seq ${String(seq)} carries no toolCallId; the run log cannot attribute it`);
67
- }
77
+ const { toolCallId } = assertApprovalRequestDetails(message.details, seq);
68
78
  const approval = {
69
79
  toolCallId,
70
80
  requestedSeq: seq,
@@ -77,19 +87,16 @@ function projectToolState(records) {
77
87
  continue;
78
88
  }
79
89
  if (message.customType === exports.APPROVAL_RESOLUTION_TYPE) {
80
- const info = details(message.details);
81
- const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : null;
82
- if (!toolCallId) {
83
- throw new Error(`approval resolution at seq ${String(seq)} carries no toolCallId; the run log cannot attribute it`);
84
- }
90
+ const info = assertApprovalResolutionDetails(message.details, seq);
91
+ const toolCallId = info.toolCallId;
85
92
  const approved = info.decision === 'approved';
86
93
  const existing = approvals.get(toolCallId);
87
94
  const approval = {
88
95
  ...(existing ?? { toolCallId, requestedSeq: seq, prompt: '' }),
89
96
  resolvedSeq: seq,
90
- decision: approved ? 'approved' : 'rejected',
91
- ...(typeof info.reason === 'string' ? { reason: info.reason } : {}),
92
- ...(typeof info.actorId === 'string' ? { actorId: info.actorId } : {})
97
+ decision: info.decision,
98
+ ...(info.reason === undefined ? {} : { reason: info.reason }),
99
+ ...(info.actorId === undefined ? {} : { actorId: info.actorId })
93
100
  };
94
101
  approvals.set(toolCallId, approval);
95
102
  const state = tools[toolCallId];
@@ -99,12 +106,72 @@ function projectToolState(records) {
99
106
  else if (state) {
100
107
  tools[toolCallId] = { ...state, approval };
101
108
  }
109
+ continue;
110
+ }
111
+ if (message.customType === exports.GATE_DECISION_TYPE) {
112
+ const decision = assertGateDecisionDetails(message.details, seq);
113
+ gateDecisions[decision.toolCallId] = { ...decision, seq };
114
+ const state = tools[decision.toolCallId];
115
+ // A blocked call never produces a tool result, so the gate's `deny` is the
116
+ // only thing that will ever settle it.
117
+ if (state && decision.decision === 'deny' && state.settledSeq === undefined) {
118
+ tools[decision.toolCallId] = {
119
+ ...state,
120
+ status: 'rejected',
121
+ settledSeq: seq,
122
+ ...(decision.reason === undefined ? {} : { output: decision.reason })
123
+ };
124
+ }
102
125
  }
103
126
  }
104
127
  const pendingApprovals = Array.from(approvals.values())
105
128
  .filter((approval) => approval.resolvedSeq === undefined)
106
129
  .sort((a, b) => a.requestedSeq - b.requestedSeq);
107
- return { tools, pendingApprovals };
130
+ return { tools, gateDecisions, pendingApprovals };
131
+ }
132
+ /** Narrow the `details` of a `constructive.gate.decision` entry. */
133
+ function assertGateDecisionDetails(value, seq) {
134
+ const info = details(value);
135
+ const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : null;
136
+ if (!toolCallId) {
137
+ throw new TypeError(`gate decision${at(seq)} carries no toolCallId; the run log cannot attribute it`);
138
+ }
139
+ if (info.decision !== 'allow' && info.decision !== 'deny') {
140
+ throw new TypeError(`gate decision${at(seq)} must settle to "allow" or "deny"`);
141
+ }
142
+ return {
143
+ toolCallId,
144
+ toolName: typeof info.toolName === 'string' ? info.toolName : '',
145
+ verdict: typeof info.verdict === 'string' ? info.verdict : info.decision,
146
+ decision: info.decision,
147
+ ...(typeof info.reason === 'string' ? { reason: info.reason } : {}),
148
+ ...(typeof info.actorId === 'string' ? { actorId: info.actorId } : {}),
149
+ ...(typeof info.decidedAt === 'string' ? { decidedAt: info.decidedAt } : {})
150
+ };
151
+ }
152
+ /** Narrow the `details` of a `constructive.approval.request` entry. */
153
+ function assertApprovalRequestDetails(value, seq) {
154
+ const info = details(value);
155
+ if (typeof info.toolCallId !== 'string' || info.toolCallId.length === 0) {
156
+ throw new TypeError(`approval request${at(seq)} carries no toolCallId; the run log cannot attribute it`);
157
+ }
158
+ return { toolCallId: info.toolCallId };
159
+ }
160
+ /** Narrow the `details` of a `constructive.approval.resolution` entry. */
161
+ function assertApprovalResolutionDetails(value, seq) {
162
+ const info = details(value);
163
+ if (typeof info.toolCallId !== 'string' || info.toolCallId.length === 0) {
164
+ throw new TypeError(`approval resolution${at(seq)} carries no toolCallId; the run log cannot attribute it`);
165
+ }
166
+ if (info.decision !== 'approved' && info.decision !== 'rejected') {
167
+ throw new TypeError(`approval resolution${at(seq)} must decide "approved" or "rejected"`);
168
+ }
169
+ return {
170
+ toolCallId: info.toolCallId,
171
+ decision: info.decision,
172
+ ...(typeof info.reason === 'string' ? { reason: info.reason } : {}),
173
+ ...(typeof info.actorId === 'string' ? { actorId: info.actorId } : {})
174
+ };
108
175
  }
109
176
  /** Build the pi `custom` message an approval request is carried in. */
110
177
  const approvalRequestMessage = (input) => ({
@@ -116,6 +183,26 @@ const approvalRequestMessage = (input) => ({
116
183
  timestamp: Date.now()
117
184
  });
118
185
  exports.approvalRequestMessage = approvalRequestMessage;
186
+ /** Build the pi `custom` message a settled gate decision is carried in. */
187
+ const gateDecisionMessage = (input) => ({
188
+ role: 'custom',
189
+ customType: exports.GATE_DECISION_TYPE,
190
+ content: `gate ${input.decision}: ${input.toolName}${input.reason ? ` — ${input.reason}` : ''}`,
191
+ // The model already learned of a denial through the blocked call's error; a
192
+ // second telling would only spend context.
193
+ display: false,
194
+ details: {
195
+ toolCallId: input.toolCallId,
196
+ toolName: input.toolName,
197
+ verdict: input.verdict,
198
+ decision: input.decision,
199
+ ...(input.reason ? { reason: input.reason } : {}),
200
+ ...(input.actorId ? { actorId: input.actorId } : {}),
201
+ ...(input.decidedAt ? { decidedAt: input.decidedAt } : {})
202
+ },
203
+ timestamp: Date.now()
204
+ });
205
+ exports.gateDecisionMessage = gateDecisionMessage;
119
206
  /** Build the pi `custom` message a human's answer is carried in. */
120
207
  const approvalResolutionMessage = (input) => ({
121
208
  role: 'custom',
@@ -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 {
@@ -10,12 +10,12 @@
10
10
  *
11
11
  * This projection describes what a run *observed*. It is reconciliation input,
12
12
  * never the billing authority — a gateway-observed record is (see
13
- * `@agentic-kit/pi-ext-metered-model`).
13
+ * `@agentic-kit/metering`).
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.modelKey = void 0;
17
17
  exports.projectUsage = projectUsage;
18
- const pi_entry_1 = require("../pi-entry");
18
+ const pi_entry_1 = require("../transcripts/pi-entry");
19
19
  const empty = () => ({
20
20
  input: 0,
21
21
  output: 0,
package/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/record.js CHANGED
@@ -1,24 +1,24 @@
1
1
  "use strict";
2
2
  /**
3
- * The run log record: four platform-owned fields around a verbatim pi entry.
3
+ * The run log record: five platform-owned fields around a verbatim harness entry.
4
4
  *
5
- * The wrapper exists to give an entry identity (which run) and order (which
6
- * position) across hosts — nothing else. It deliberately does not re-encode pi's
7
- * semantics, because pi already versions and migrates its own session format;
8
- * `piSessionVersion` records which version an entry was written under so a
9
- * reader can hand old entries to pi's migrations instead of guessing.
5
+ * The wrapper exists to give an entry identity (which run), order (which
6
+ * position) and readability (which transcript) across hosts — nothing else. It
7
+ * deliberately does not re-encode a harness's semantics, because a harness
8
+ * already versions and migrates its own session format; `transcriptFormat` says
9
+ * which reader can parse the entry, and `transcriptVersion` says which version
10
+ * that reader must migrate from.
10
11
  */
11
12
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.SUPPORTED_PI_SESSION_VERSION = exports.RUN_LOG_WRAPPER_VERSION = void 0;
13
+ exports.RUN_LOG_WRAPPER_VERSION = void 0;
13
14
  exports.wrapEntry = wrapEntry;
14
15
  exports.assertRunEventRecord = assertRunEventRecord;
15
16
  exports.idempotencyKey = idempotencyKey;
16
17
  exports.assertOrdered = assertOrdered;
17
- const pi_entry_1 = require("./pi-entry");
18
- /** Version of the wrapper itself — bumped only if these four fields change. */
18
+ const format_1 = require("./transcripts/format");
19
+ const pi_entry_1 = require("./transcripts/pi-entry");
20
+ /** Version of the wrapper itself — bumped only if these fields change. */
19
21
  exports.RUN_LOG_WRAPPER_VERSION = 1;
20
- /** The pi session format version this package projects without migration. */
21
- exports.SUPPORTED_PI_SESSION_VERSION = 3;
22
22
  function wrapEntry(options) {
23
23
  if (!options.runId)
24
24
  throw new TypeError('a run log record needs a runId');
@@ -29,7 +29,8 @@ function wrapEntry(options) {
29
29
  runId: options.runId,
30
30
  seq: options.seq,
31
31
  recordedAt: options.recordedAt ?? new Date().toISOString(),
32
- piSessionVersion: options.piSessionVersion ?? exports.SUPPORTED_PI_SESSION_VERSION,
32
+ transcriptFormat: options.transcriptFormat ?? format_1.PI_TRANSCRIPT_FORMAT,
33
+ transcriptVersion: options.transcriptVersion ?? format_1.SUPPORTED_PI_TRANSCRIPT_VERSION,
33
34
  entry: (0, pi_entry_1.assertPiSessionEntry)(options.entry)
34
35
  };
35
36
  }
@@ -52,14 +53,18 @@ function assertRunEventRecord(value) {
52
53
  if (typeof record.recordedAt !== 'string') {
53
54
  throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry recordedAt`);
54
55
  }
55
- if (!Number.isInteger(record.piSessionVersion)) {
56
- throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry an integer piSessionVersion`);
56
+ if (typeof record.transcriptFormat !== 'string' || record.transcriptFormat.length === 0) {
57
+ throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry a non-empty transcriptFormat`);
58
+ }
59
+ if (!Number.isInteger(record.transcriptVersion)) {
60
+ throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry an integer transcriptVersion`);
57
61
  }
58
62
  return {
59
63
  runId: record.runId,
60
64
  seq: record.seq,
61
65
  recordedAt: record.recordedAt,
62
- piSessionVersion: record.piSessionVersion,
66
+ transcriptFormat: record.transcriptFormat,
67
+ transcriptVersion: record.transcriptVersion,
63
68
  entry: (0, pi_entry_1.assertPiSessionEntry)(record.entry)
64
69
  };
65
70
  }
package/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/store.js CHANGED
@@ -29,7 +29,8 @@ class MemoryRunLogStore {
29
29
  seq: records.length + written.length + 1,
30
30
  entry,
31
31
  ...(options.recordedAt ? { recordedAt: options.recordedAt } : {}),
32
- piSessionVersion: options.piSessionVersion ?? record_1.SUPPORTED_PI_SESSION_VERSION
32
+ ...(options.transcriptFormat === undefined ? {} : { transcriptFormat: options.transcriptFormat }),
33
+ ...(options.transcriptVersion === undefined ? {} : { transcriptVersion: options.transcriptVersion })
33
34
  });
34
35
  written.push(record);
35
36
  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;