@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.
- package/README.md +27 -11
- package/entry-types.d.ts +73 -0
- package/entry-types.js +93 -0
- package/esm/entry-types.d.ts +73 -0
- package/esm/entry-types.js +88 -0
- package/esm/file-store.d.ts +1 -1
- package/esm/file-store.js +3 -2
- package/esm/index.d.ts +7 -3
- package/esm/index.js +7 -3
- package/esm/mirror.d.ts +63 -0
- package/esm/mirror.js +87 -0
- package/esm/projectors/parts.d.ts +1 -1
- package/esm/projectors/parts.js +1 -1
- package/esm/projectors/session.d.ts +2 -2
- package/esm/projectors/session.js +8 -6
- package/esm/projectors/spans.d.ts +84 -0
- package/esm/projectors/spans.js +176 -0
- package/esm/projectors/tool-state.d.ts +50 -0
- package/esm/projectors/tool-state.js +98 -15
- package/esm/projectors/usage.d.ts +1 -1
- package/esm/projectors/usage.js +2 -2
- package/esm/record.d.ts +17 -14
- package/esm/record.js +19 -14
- package/esm/store.d.ts +6 -3
- package/esm/store.js +3 -2
- package/esm/transcripts/format.d.ts +25 -0
- package/esm/transcripts/format.js +29 -0
- package/file-store.d.ts +1 -1
- package/file-store.js +2 -1
- package/index.d.ts +7 -3
- package/index.js +27 -12
- package/mirror.d.ts +63 -0
- package/mirror.js +91 -0
- package/package.json +2 -2
- package/projectors/parts.d.ts +1 -1
- package/projectors/parts.js +1 -1
- package/projectors/session.d.ts +2 -2
- package/projectors/session.js +7 -5
- package/projectors/spans.d.ts +84 -0
- package/projectors/spans.js +179 -0
- package/projectors/tool-state.d.ts +50 -0
- package/projectors/tool-state.js +103 -16
- package/projectors/usage.d.ts +1 -1
- package/projectors/usage.js +2 -2
- package/record.d.ts +17 -14
- package/record.js +20 -15
- package/store.d.ts +6 -3
- package/store.js +2 -1
- package/transcripts/format.d.ts +25 -0
- package/transcripts/format.js +33 -0
- /package/esm/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
- /package/esm/{pi-entry.js → transcripts/pi-entry.js} +0 -0
- /package/{pi-entry.d.ts → transcripts/pi-entry.d.ts} +0 -0
- /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";
|
package/projectors/tool-state.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 =
|
|
81
|
-
const toolCallId =
|
|
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:
|
|
91
|
-
...(
|
|
92
|
-
...(
|
|
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',
|
package/projectors/usage.d.ts
CHANGED
|
@@ -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/
|
|
12
|
+
* `@agentic-kit/metering`).
|
|
13
13
|
*/
|
|
14
14
|
import type { RunEventRecord } from '../record';
|
|
15
15
|
export interface UsageTotals {
|
package/projectors/usage.js
CHANGED
|
@@ -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/
|
|
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:
|
|
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)
|
|
5
|
-
* position) across hosts — nothing else. It
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* reader can
|
|
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
|
|
11
|
-
|
|
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
|
-
/**
|
|
23
|
-
|
|
24
|
-
/**
|
|
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
|
-
|
|
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:
|
|
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)
|
|
6
|
-
* position) across hosts — nothing else. It
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* reader can
|
|
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.
|
|
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
|
|
18
|
-
|
|
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
|
-
|
|
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 (
|
|
56
|
-
throw new TypeError(`run log record ${record.runId}#${String(record.seq)} must carry
|
|
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
|
-
|
|
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
|
-
/**
|
|
22
|
-
|
|
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
|
-
|
|
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;
|