@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.
@@ -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
+ exports.assertGateDecisionDetails = assertGateDecisionDetails;
16
+ exports.assertApprovalRequestDetails = assertApprovalRequestDetails;
17
+ exports.assertApprovalResolutionDetails = assertApprovalResolutionDetails;
15
18
  const pi_entry_1 = require("../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',