@agentic-kit/run-log 0.2.0 → 0.3.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 +16 -2
- 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/index.d.ts +3 -1
- package/esm/index.js +3 -1
- 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 +97 -14
- package/index.d.ts +3 -1
- package/index.js +11 -1
- package/package.json +2 -2
- 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 +102 -15
|
@@ -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";
|
|
@@ -13,10 +13,21 @@ import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage,
|
|
|
13
13
|
export const APPROVAL_REQUEST_TYPE = 'constructive.approval.request';
|
|
14
14
|
/** `customType` of the human's answer to a request. */
|
|
15
15
|
export 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 const GATE_DECISION_TYPE = 'constructive.gate.decision';
|
|
24
|
+
/** Where a bad entry is, when the caller is projecting a log rather than validating one value. */
|
|
25
|
+
const at = (seq) => (seq === undefined ? '' : ` at seq ${String(seq)}`);
|
|
16
26
|
const details = (value) => typeof value === 'object' && value !== null ? value : {};
|
|
17
27
|
export function projectToolState(records) {
|
|
18
28
|
const tools = {};
|
|
19
29
|
const approvals = new Map();
|
|
30
|
+
const gateDecisions = {};
|
|
20
31
|
for (const { entry, seq } of records) {
|
|
21
32
|
if (!isPiMessageEntry(entry))
|
|
22
33
|
continue;
|
|
@@ -56,11 +67,7 @@ export function projectToolState(records) {
|
|
|
56
67
|
if (message.role !== 'custom')
|
|
57
68
|
continue;
|
|
58
69
|
if (message.customType === APPROVAL_REQUEST_TYPE) {
|
|
59
|
-
const
|
|
60
|
-
const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : null;
|
|
61
|
-
if (!toolCallId) {
|
|
62
|
-
throw new Error(`approval request at seq ${String(seq)} carries no toolCallId; the run log cannot attribute it`);
|
|
63
|
-
}
|
|
70
|
+
const { toolCallId } = assertApprovalRequestDetails(message.details, seq);
|
|
64
71
|
const approval = {
|
|
65
72
|
toolCallId,
|
|
66
73
|
requestedSeq: seq,
|
|
@@ -73,19 +80,16 @@ export function projectToolState(records) {
|
|
|
73
80
|
continue;
|
|
74
81
|
}
|
|
75
82
|
if (message.customType === APPROVAL_RESOLUTION_TYPE) {
|
|
76
|
-
const info =
|
|
77
|
-
const toolCallId =
|
|
78
|
-
if (!toolCallId) {
|
|
79
|
-
throw new Error(`approval resolution at seq ${String(seq)} carries no toolCallId; the run log cannot attribute it`);
|
|
80
|
-
}
|
|
83
|
+
const info = assertApprovalResolutionDetails(message.details, seq);
|
|
84
|
+
const toolCallId = info.toolCallId;
|
|
81
85
|
const approved = info.decision === 'approved';
|
|
82
86
|
const existing = approvals.get(toolCallId);
|
|
83
87
|
const approval = {
|
|
84
88
|
...(existing ?? { toolCallId, requestedSeq: seq, prompt: '' }),
|
|
85
89
|
resolvedSeq: seq,
|
|
86
|
-
decision:
|
|
87
|
-
...(
|
|
88
|
-
...(
|
|
90
|
+
decision: info.decision,
|
|
91
|
+
...(info.reason === undefined ? {} : { reason: info.reason }),
|
|
92
|
+
...(info.actorId === undefined ? {} : { actorId: info.actorId })
|
|
89
93
|
};
|
|
90
94
|
approvals.set(toolCallId, approval);
|
|
91
95
|
const state = tools[toolCallId];
|
|
@@ -95,12 +99,72 @@ export function projectToolState(records) {
|
|
|
95
99
|
else if (state) {
|
|
96
100
|
tools[toolCallId] = { ...state, approval };
|
|
97
101
|
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (message.customType === GATE_DECISION_TYPE) {
|
|
105
|
+
const decision = assertGateDecisionDetails(message.details, seq);
|
|
106
|
+
gateDecisions[decision.toolCallId] = { ...decision, seq };
|
|
107
|
+
const state = tools[decision.toolCallId];
|
|
108
|
+
// A blocked call never produces a tool result, so the gate's `deny` is the
|
|
109
|
+
// only thing that will ever settle it.
|
|
110
|
+
if (state && decision.decision === 'deny' && state.settledSeq === undefined) {
|
|
111
|
+
tools[decision.toolCallId] = {
|
|
112
|
+
...state,
|
|
113
|
+
status: 'rejected',
|
|
114
|
+
settledSeq: seq,
|
|
115
|
+
...(decision.reason === undefined ? {} : { output: decision.reason })
|
|
116
|
+
};
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
}
|
|
100
120
|
const pendingApprovals = Array.from(approvals.values())
|
|
101
121
|
.filter((approval) => approval.resolvedSeq === undefined)
|
|
102
122
|
.sort((a, b) => a.requestedSeq - b.requestedSeq);
|
|
103
|
-
return { tools, pendingApprovals };
|
|
123
|
+
return { tools, gateDecisions, pendingApprovals };
|
|
124
|
+
}
|
|
125
|
+
/** Narrow the `details` of a `constructive.gate.decision` entry. */
|
|
126
|
+
export function assertGateDecisionDetails(value, seq) {
|
|
127
|
+
const info = details(value);
|
|
128
|
+
const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : null;
|
|
129
|
+
if (!toolCallId) {
|
|
130
|
+
throw new TypeError(`gate decision${at(seq)} carries no toolCallId; the run log cannot attribute it`);
|
|
131
|
+
}
|
|
132
|
+
if (info.decision !== 'allow' && info.decision !== 'deny') {
|
|
133
|
+
throw new TypeError(`gate decision${at(seq)} must settle to "allow" or "deny"`);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
toolCallId,
|
|
137
|
+
toolName: typeof info.toolName === 'string' ? info.toolName : '',
|
|
138
|
+
verdict: typeof info.verdict === 'string' ? info.verdict : info.decision,
|
|
139
|
+
decision: info.decision,
|
|
140
|
+
...(typeof info.reason === 'string' ? { reason: info.reason } : {}),
|
|
141
|
+
...(typeof info.actorId === 'string' ? { actorId: info.actorId } : {}),
|
|
142
|
+
...(typeof info.decidedAt === 'string' ? { decidedAt: info.decidedAt } : {})
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Narrow the `details` of a `constructive.approval.request` entry. */
|
|
146
|
+
export function assertApprovalRequestDetails(value, seq) {
|
|
147
|
+
const info = details(value);
|
|
148
|
+
if (typeof info.toolCallId !== 'string' || info.toolCallId.length === 0) {
|
|
149
|
+
throw new TypeError(`approval request${at(seq)} carries no toolCallId; the run log cannot attribute it`);
|
|
150
|
+
}
|
|
151
|
+
return { toolCallId: info.toolCallId };
|
|
152
|
+
}
|
|
153
|
+
/** Narrow the `details` of a `constructive.approval.resolution` entry. */
|
|
154
|
+
export function assertApprovalResolutionDetails(value, seq) {
|
|
155
|
+
const info = details(value);
|
|
156
|
+
if (typeof info.toolCallId !== 'string' || info.toolCallId.length === 0) {
|
|
157
|
+
throw new TypeError(`approval resolution${at(seq)} carries no toolCallId; the run log cannot attribute it`);
|
|
158
|
+
}
|
|
159
|
+
if (info.decision !== 'approved' && info.decision !== 'rejected') {
|
|
160
|
+
throw new TypeError(`approval resolution${at(seq)} must decide "approved" or "rejected"`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
toolCallId: info.toolCallId,
|
|
164
|
+
decision: info.decision,
|
|
165
|
+
...(typeof info.reason === 'string' ? { reason: info.reason } : {}),
|
|
166
|
+
...(typeof info.actorId === 'string' ? { actorId: info.actorId } : {})
|
|
167
|
+
};
|
|
104
168
|
}
|
|
105
169
|
/** Build the pi `custom` message an approval request is carried in. */
|
|
106
170
|
export const approvalRequestMessage = (input) => ({
|
|
@@ -111,6 +175,25 @@ export const approvalRequestMessage = (input) => ({
|
|
|
111
175
|
details: { toolCallId: input.toolCallId },
|
|
112
176
|
timestamp: Date.now()
|
|
113
177
|
});
|
|
178
|
+
/** Build the pi `custom` message a settled gate decision is carried in. */
|
|
179
|
+
export const gateDecisionMessage = (input) => ({
|
|
180
|
+
role: 'custom',
|
|
181
|
+
customType: GATE_DECISION_TYPE,
|
|
182
|
+
content: `gate ${input.decision}: ${input.toolName}${input.reason ? ` — ${input.reason}` : ''}`,
|
|
183
|
+
// The model already learned of a denial through the blocked call's error; a
|
|
184
|
+
// second telling would only spend context.
|
|
185
|
+
display: false,
|
|
186
|
+
details: {
|
|
187
|
+
toolCallId: input.toolCallId,
|
|
188
|
+
toolName: input.toolName,
|
|
189
|
+
verdict: input.verdict,
|
|
190
|
+
decision: input.decision,
|
|
191
|
+
...(input.reason ? { reason: input.reason } : {}),
|
|
192
|
+
...(input.actorId ? { actorId: input.actorId } : {}),
|
|
193
|
+
...(input.decidedAt ? { decidedAt: input.decidedAt } : {})
|
|
194
|
+
},
|
|
195
|
+
timestamp: Date.now()
|
|
196
|
+
});
|
|
114
197
|
/** Build the pi `custom` message a human's answer is carried in. */
|
|
115
198
|
export const approvalResolutionMessage = (input) => ({
|
|
116
199
|
role: 'custom',
|
package/index.d.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
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
11
|
export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, type PiAssistantMessage, type PiBashExecutionMessage, type PiBranchSummaryEntry, type PiCompactionEntry, type PiContent, type PiCustomMessage, type PiEntryBase, type PiImageContent, type PiMessage, type PiMessageEntry, type PiOtherEntry, type PiSessionEntry, type PiSessionHeader, type PiSummaryMessage, type PiTextContent, type PiThinkingContent, type PiToolCallContent, type PiToolResultMessage, type PiUsage, type PiUsageCost, type PiUserMessage, toolCalls } from './pi-entry';
|
|
11
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 {
|
|
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
17
|
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, SUPPORTED_PI_SESSION_VERSION, 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';
|
package/index.js
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* `@agentic-kit/run-log/file-store`.
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.START = exports.MemoryRunLogStore = exports.cursorAfter = exports.wrapEntry = exports.SUPPORTED_PI_SESSION_VERSION = exports.RUN_LOG_WRAPPER_VERSION = exports.idempotencyKey = exports.assertRunEventRecord = exports.assertOrdered = exports.projectUsage = exports.modelKey = exports.projectToolState = exports.approvalResolutionMessage = exports.approvalRequestMessage = exports.APPROVAL_RESOLUTION_TYPE = exports.APPROVAL_REQUEST_TYPE = exports.projectSession = exports.parseSessionJsonl = exports.projectParts = exports.toolCalls = exports.isToolResultMessage = exports.isPiSessionHeader = exports.isPiMessageEntry = exports.isPiCompactionEntry = exports.isPiBranchSummaryEntry = exports.isAssistantMessage = exports.contentText = exports.assertPiSessionEntry = exports.readAll = exports.follow = void 0;
|
|
11
|
+
exports.START = exports.MemoryRunLogStore = exports.cursorAfter = exports.wrapEntry = exports.SUPPORTED_PI_SESSION_VERSION = exports.RUN_LOG_WRAPPER_VERSION = exports.idempotencyKey = exports.assertRunEventRecord = exports.assertOrdered = exports.projectUsage = exports.modelKey = exports.projectToolState = exports.gateDecisionMessage = exports.GATE_DECISION_TYPE = exports.assertGateDecisionDetails = exports.assertApprovalResolutionDetails = exports.assertApprovalRequestDetails = exports.approvalResolutionMessage = exports.approvalRequestMessage = exports.APPROVAL_RESOLUTION_TYPE = exports.APPROVAL_REQUEST_TYPE = exports.projectSpans = exports.projectSession = exports.parseSessionJsonl = exports.projectParts = exports.toolCalls = exports.isToolResultMessage = exports.isPiSessionHeader = exports.isPiMessageEntry = exports.isPiCompactionEntry = exports.isPiBranchSummaryEntry = exports.isAssistantMessage = exports.contentText = exports.assertPiSessionEntry = exports.readAll = exports.follow = exports.EntryTypeRegistry = exports.constructiveEntryTypes = void 0;
|
|
12
|
+
var entry_types_1 = require("./entry-types");
|
|
13
|
+
Object.defineProperty(exports, "constructiveEntryTypes", { enumerable: true, get: function () { return entry_types_1.constructiveEntryTypes; } });
|
|
14
|
+
Object.defineProperty(exports, "EntryTypeRegistry", { enumerable: true, get: function () { return entry_types_1.EntryTypeRegistry; } });
|
|
12
15
|
var follow_1 = require("./follow");
|
|
13
16
|
Object.defineProperty(exports, "follow", { enumerable: true, get: function () { return follow_1.follow; } });
|
|
14
17
|
Object.defineProperty(exports, "readAll", { enumerable: true, get: function () { return follow_1.readAll; } });
|
|
@@ -27,11 +30,18 @@ Object.defineProperty(exports, "projectParts", { enumerable: true, get: function
|
|
|
27
30
|
var session_1 = require("./projectors/session");
|
|
28
31
|
Object.defineProperty(exports, "parseSessionJsonl", { enumerable: true, get: function () { return session_1.parseSessionJsonl; } });
|
|
29
32
|
Object.defineProperty(exports, "projectSession", { enumerable: true, get: function () { return session_1.projectSession; } });
|
|
33
|
+
var spans_1 = require("./projectors/spans");
|
|
34
|
+
Object.defineProperty(exports, "projectSpans", { enumerable: true, get: function () { return spans_1.projectSpans; } });
|
|
30
35
|
var tool_state_1 = require("./projectors/tool-state");
|
|
31
36
|
Object.defineProperty(exports, "APPROVAL_REQUEST_TYPE", { enumerable: true, get: function () { return tool_state_1.APPROVAL_REQUEST_TYPE; } });
|
|
32
37
|
Object.defineProperty(exports, "APPROVAL_RESOLUTION_TYPE", { enumerable: true, get: function () { return tool_state_1.APPROVAL_RESOLUTION_TYPE; } });
|
|
33
38
|
Object.defineProperty(exports, "approvalRequestMessage", { enumerable: true, get: function () { return tool_state_1.approvalRequestMessage; } });
|
|
34
39
|
Object.defineProperty(exports, "approvalResolutionMessage", { enumerable: true, get: function () { return tool_state_1.approvalResolutionMessage; } });
|
|
40
|
+
Object.defineProperty(exports, "assertApprovalRequestDetails", { enumerable: true, get: function () { return tool_state_1.assertApprovalRequestDetails; } });
|
|
41
|
+
Object.defineProperty(exports, "assertApprovalResolutionDetails", { enumerable: true, get: function () { return tool_state_1.assertApprovalResolutionDetails; } });
|
|
42
|
+
Object.defineProperty(exports, "assertGateDecisionDetails", { enumerable: true, get: function () { return tool_state_1.assertGateDecisionDetails; } });
|
|
43
|
+
Object.defineProperty(exports, "GATE_DECISION_TYPE", { enumerable: true, get: function () { return tool_state_1.GATE_DECISION_TYPE; } });
|
|
44
|
+
Object.defineProperty(exports, "gateDecisionMessage", { enumerable: true, get: function () { return tool_state_1.gateDecisionMessage; } });
|
|
35
45
|
Object.defineProperty(exports, "projectToolState", { enumerable: true, get: function () { return tool_state_1.projectToolState; } });
|
|
36
46
|
var usage_1 = require("./projectors/usage");
|
|
37
47
|
Object.defineProperty(exports, "modelKey", { enumerable: true, get: function () { return usage_1.modelKey; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentic-kit/run-log",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "The append-only agent run log — pi session entries stored verbatim under a run/seq wrapper, with projections for transcript, usage, tool state and resumable sessions",
|
|
6
6
|
"main": "index.js",
|
|
@@ -35,5 +35,5 @@
|
|
|
35
35
|
"run-log",
|
|
36
36
|
"constructive"
|
|
37
37
|
],
|
|
38
|
-
"gitHead": "
|
|
38
|
+
"gitHead": "6880388c8cd5fb21745decff8bd23c3a430338b3"
|
|
39
39
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span projection: run log records → timed intervals with parents.
|
|
3
|
+
*
|
|
4
|
+
* The log answers *what* happened and the parts projection answers *in what
|
|
5
|
+
* order*, but a trace view asks "how long did this take, and inside what" —
|
|
6
|
+
* questions pi's entries only answer in pairs. Every entry is timestamped, so a
|
|
7
|
+
* tool call and its result, or an approval request and its answer, bracket an
|
|
8
|
+
* interval; nothing new has to be stored to know it.
|
|
9
|
+
*
|
|
10
|
+
* Two honesty rules shape the output. A duration derived from a *pair of writes*
|
|
11
|
+
* is a bound, not a measurement — the gap between a tool call and its result
|
|
12
|
+
* includes the wait for a human — so every span says which it is via `timing`,
|
|
13
|
+
* and a tool span carries its approval wait separately so a renderer can show
|
|
14
|
+
* "blocked on you" apart from "running". And an interval that never closed stays
|
|
15
|
+
* `open` with no duration rather than being closed at the last record: a run
|
|
16
|
+
* still in flight must not look like one that finished.
|
|
17
|
+
*/
|
|
18
|
+
import { type PiUsage } from '../pi-entry';
|
|
19
|
+
import type { RunEventRecord } from '../record';
|
|
20
|
+
export type SpanKind = 'model' | 'tool' | 'approval';
|
|
21
|
+
export type SpanStatus = 'open' | 'ok' | 'error' | 'denied';
|
|
22
|
+
/**
|
|
23
|
+
* Whether the duration is the interval that was measured, or a bound that
|
|
24
|
+
* includes whatever else sat between the two writes.
|
|
25
|
+
*/
|
|
26
|
+
export type SpanTiming = 'measured' | 'bounded';
|
|
27
|
+
export interface SpanBase {
|
|
28
|
+
/** Stable within a run — a renderer's key and a parent reference. */
|
|
29
|
+
id: string;
|
|
30
|
+
kind: SpanKind;
|
|
31
|
+
/** Row title: the tool name, the model, or the awaited tool. */
|
|
32
|
+
name: string;
|
|
33
|
+
parentId?: string;
|
|
34
|
+
startSeq: number;
|
|
35
|
+
/** Absent while the span is still open. */
|
|
36
|
+
endSeq?: number;
|
|
37
|
+
startedAt: string;
|
|
38
|
+
endedAt?: string;
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
timing: SpanTiming;
|
|
41
|
+
status: SpanStatus;
|
|
42
|
+
}
|
|
43
|
+
export interface ModelSpan extends SpanBase {
|
|
44
|
+
kind: 'model';
|
|
45
|
+
model?: string;
|
|
46
|
+
provider?: string;
|
|
47
|
+
/** pi's provider response id — the join key to a metered inference row. */
|
|
48
|
+
responseId?: string;
|
|
49
|
+
stopReason?: string;
|
|
50
|
+
usage?: PiUsage;
|
|
51
|
+
errorMessage?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ToolSpan extends SpanBase {
|
|
54
|
+
kind: 'tool';
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
arguments: Record<string, unknown>;
|
|
57
|
+
output?: string;
|
|
58
|
+
details?: unknown;
|
|
59
|
+
/** Of the total duration, how much was spent waiting on a human. */
|
|
60
|
+
approvalWaitMs?: number;
|
|
61
|
+
/** Set when the gate settled the call instead of an execution. */
|
|
62
|
+
gateDecision?: 'allow' | 'deny';
|
|
63
|
+
gateReason?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ApprovalSpan extends SpanBase {
|
|
66
|
+
kind: 'approval';
|
|
67
|
+
toolCallId: string;
|
|
68
|
+
prompt: string;
|
|
69
|
+
decision?: 'approved' | 'rejected';
|
|
70
|
+
actorId?: string;
|
|
71
|
+
}
|
|
72
|
+
export type Span = ModelSpan | ToolSpan | ApprovalSpan;
|
|
73
|
+
export interface SpanProjection {
|
|
74
|
+
/** In start order — the order a timeline draws them. */
|
|
75
|
+
spans: Span[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Project records into spans. Pure, and like the other projectors it ignores what
|
|
79
|
+
* it does not recognise — a log written by a newer pi still traces. A *registered*
|
|
80
|
+
* type that is malformed is another matter and throws: a gate decision naming no
|
|
81
|
+
* tool call is a writer bug, and a trace that quietly omitted a refusal would be
|
|
82
|
+
* worse than one that fails.
|
|
83
|
+
*/
|
|
84
|
+
export declare function projectSpans(records: readonly RunEventRecord[]): SpanProjection;
|
|
@@ -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("../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";
|