@ouro.bot/cli 0.1.0-alpha.729 → 0.1.0-alpha.730

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.
@@ -7,6 +7,7 @@ exports.getProvider = getProvider;
7
7
  exports.createSummarize = createSummarize;
8
8
  exports.getProviderDisplayLabel = getProviderDisplayLabel;
9
9
  exports.createHabitCallbackBuffer = createHabitCallbackBuffer;
10
+ exports.resumeApprovalContinuation = resumeApprovalContinuation;
10
11
  exports.isChatStyleChannel = isChatStyleChannel;
11
12
  exports.buildPonderResult = buildPonderResult;
12
13
  exports.isExternalStateQuery = isExternalStateQuery;
@@ -17,6 +18,8 @@ exports.runAgent = runAgent;
17
18
  const config_1 = require("./config");
18
19
  const identity_1 = require("./identity");
19
20
  const tools_1 = require("../repertoire/tools");
21
+ const tool_arguments_1 = require("../repertoire/tool-arguments");
22
+ const session_events_1 = require("./session-events");
20
23
  const friends_1 = require("@ouro.bot/friends");
21
24
  const tools_2 = require("../repertoire/tools");
22
25
  const streaming_1 = require("./streaming");
@@ -244,6 +247,84 @@ function createHabitCallbackBuffer(callbacks) {
244
247
  function createFinalOnlyTextBuffer(callbacks) {
245
248
  return createCallbackBuffer(callbacks, "text-only");
246
249
  }
250
+ function continuationCallbacks(callbacks, text) {
251
+ const noop = () => undefined;
252
+ return {
253
+ onModelStart: callbacks.onModelStart ?? noop,
254
+ onModelStreamStart: callbacks.onModelStreamStart ?? noop,
255
+ onTextChunk: (chunk) => {
256
+ text.value += chunk;
257
+ callbacks.onTextChunk?.(chunk);
258
+ },
259
+ onReasoningChunk: callbacks.onReasoningChunk ?? noop,
260
+ onToolStart: callbacks.onToolStart ?? noop,
261
+ onToolEnd: callbacks.onToolEnd ?? noop,
262
+ onError: callbacks.onError ?? noop,
263
+ ...(callbacks.onClearText ? { onClearText: callbacks.onClearText } : {}),
264
+ ...(callbacks.flushNow ? { flushNow: callbacks.flushNow } : {}),
265
+ ...(callbacks.settleOutputMode ? { settleOutputMode: callbacks.settleOutputMode } : {}),
266
+ };
267
+ }
268
+ async function resumeApprovalContinuation(options) {
269
+ const claim = options.claimContinuation();
270
+ if (!claim.claimed) {
271
+ if (claim.interruptedAfterAttempt) {
272
+ const notice = "the approval continuation was interrupted after provider work began; its outcome is indeterminate and it will not be retried automatically";
273
+ await options.deliver(notice);
274
+ (0, runtime_1.emitNervesEvent)({
275
+ level: "error",
276
+ component: "engine",
277
+ event: "engine.approval_continuation_indeterminate",
278
+ message: "approval continuation interrupted after provider attempt",
279
+ meta: { approvalId: options.record.approvalId, continuationState: claim.record.continuationState },
280
+ });
281
+ return { outcome: "terminal_notice", messages: structuredClone(options.sessionMessages) };
282
+ }
283
+ (0, runtime_1.emitNervesEvent)({
284
+ component: "engine",
285
+ event: "engine.approval_continuation_duplicate",
286
+ message: "approval continuation was already consumed",
287
+ meta: { approvalId: options.record.approvalId },
288
+ });
289
+ return { outcome: "already_continued", messages: structuredClone(options.sessionMessages) };
290
+ }
291
+ const materialized = (0, session_events_1.materializeApprovalTerminal)({
292
+ messages: options.sessionMessages,
293
+ checkpoint: options.checkpoint,
294
+ record: options.record,
295
+ currentSessionRevision: options.currentSessionRevision,
296
+ materializedApprovalIds: options.materializedApprovalIds,
297
+ });
298
+ const recoveredMaterialization = claim.record.continuationState === "materialized";
299
+ if (!recoveredMaterialization) {
300
+ await options.persist(materialized.messages);
301
+ await options.markContinuationMaterialized();
302
+ }
303
+ if (!materialized.resumeProvider) {
304
+ await options.deliver(materialized.directNotice);
305
+ await options.completeContinuation();
306
+ return { outcome: "terminal_notice", messages: materialized.messages };
307
+ }
308
+ const outward = { value: "" };
309
+ const callbacks = continuationCallbacks(options.callbacks, outward);
310
+ await options.markContinuationAttempted();
311
+ const result = await options.runAgent(materialized.messages, callbacks, options.channel, options.signal, options.runAgentOptions);
312
+ if (result.outcome === "suspended") {
313
+ await options.completeContinuation();
314
+ return { outcome: result.outcome, messages: materialized.messages, suspension: result.suspension };
315
+ }
316
+ await options.persist(materialized.messages, result);
317
+ if (outward.value)
318
+ await options.deliver(outward.value);
319
+ await options.completeContinuation();
320
+ (0, runtime_1.emitNervesEvent)({
321
+ component: "engine",
322
+ event: "engine.approval_continuation_completed",
323
+ message: "approval continuation completed through existing provider loop",
324
+ meta: { approvalId: options.record.approvalId, outcome: result.outcome },
325
+ });
326
+ return { outcome: result.outcome, messages: materialized.messages };
327
+ }
247
328
  /**
248
329
  * Strip <think>...</think> blocks for the violation-detection check at the
249
330
  * end of a streaming turn. Used to tell legitimate text-only responses
@@ -932,6 +1013,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
932
1013
  let overflowRetried = false;
933
1014
  let outcome = "settled";
934
1015
  let completion;
1016
+ let suspension;
935
1017
  let terminalError;
936
1018
  let terminalErrorClassification;
937
1019
  let sawSteeringFollowUp = false;
@@ -1623,6 +1705,100 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1623
1705
  done = true;
1624
1706
  continue;
1625
1707
  }
1708
+ const preCallMessages = structuredClone(messages.filter((message) => message.role !== "system"));
1709
+ const validatedCalls = options?.approvalCoordinator
1710
+ ? result.toolCalls.map((call) => {
1711
+ const advertised = activeTools.find((tool) => tool.function.name === call.name);
1712
+ if (!advertised || !advertised.function.parameters || typeof advertised.function.parameters !== "object") {
1713
+ return { call, error: "tool was not advertised with a valid argument schema" };
1714
+ }
1715
+ const validated = (0, tool_arguments_1.validateAdvertisedToolArguments)(call.arguments, advertised.function.parameters);
1716
+ return validated.ok
1717
+ ? { call, advertised, validated: validated.value }
1718
+ : { call, error: validated.reason };
1719
+ })
1720
+ : null;
1721
+ const invalidApprovalCall = validatedCalls?.find((entry) => "error" in entry);
1722
+ if (invalidApprovalCall) {
1723
+ await streamCallbackBuffer?.flush();
1724
+ pushGenerated(msg);
1725
+ for (const entry of validatedCalls) {
1726
+ const detail = "error" in entry ? entry.error : "another call in this batch had invalid arguments";
1727
+ const rejection = `invalid tool arguments: ${detail}`;
1728
+ pushGenerated({ role: "tool", tool_call_id: entry.call.id, content: rejection });
1729
+ providerRuntime.appendToolOutput(entry.call.id, rejection);
1730
+ }
1731
+ (0, runtime_1.emitNervesEvent)({
1732
+ level: "warn",
1733
+ component: "engine",
1734
+ event: "engine.tool_arguments_rejected",
1735
+ message: "tool batch rejected before execution because arguments were invalid",
1736
+ meta: { toolCallId: invalidApprovalCall.call.id, toolName: invalidApprovalCall.call.name },
1737
+ });
1738
+ continue;
1739
+ }
1740
+ const approvalCalls = validatedCalls?.map((entry) => ({
1741
+ ...entry,
1742
+ policy: (0, tools_1.approvalPolicyForToolName)(entry.call.name, entry.validated.arguments),
1743
+ })) ?? [];
1744
+ const protectedCall = approvalCalls.find((entry) => entry.policy.kind === "required");
1745
+ if (protectedCall && result.toolCalls.length !== 1) {
1746
+ streamCallbackBuffer?.discard();
1747
+ pushGenerated(msg);
1748
+ for (const call of result.toolCalls) {
1749
+ const rejection = "rejected: approval-eligible tool must be the sole call; no call in this batch was executed.";
1750
+ pushGenerated({ role: "tool", tool_call_id: call.id, content: rejection });
1751
+ providerRuntime.appendToolOutput(call.id, rejection);
1752
+ }
1753
+ (0, runtime_1.emitNervesEvent)({
1754
+ level: "warn",
1755
+ component: "engine",
1756
+ event: "engine.approval_mixed_batch_rejected",
1757
+ message: "protected tool batch rejected before every handler",
1758
+ meta: { toolCallCount: result.toolCalls.length, protectedToolName: protectedCall.call.name },
1759
+ });
1760
+ continue;
1761
+ }
1762
+ if (protectedCall && protectedCall.policy.kind === "required") {
1763
+ streamCallbackBuffer?.discard();
1764
+ pushGenerated(msg);
1765
+ const toolDigest = (0, tool_arguments_1.digestJson)({
1766
+ name: protectedCall.call.name,
1767
+ schemaDigest: protectedCall.validated.schemaDigest,
1768
+ policyId: protectedCall.policy.policyId,
1769
+ });
1770
+ const policyDigest = (0, tool_arguments_1.digestJson)({
1771
+ policyId: protectedCall.policy.policyId,
1772
+ actionClass: protectedCall.policy.actionClass,
1773
+ classification: "required",
1774
+ });
1775
+ const committed = await options.approvalCoordinator.propose({
1776
+ toolCall: structuredClone(msg.tool_calls[0]),
1777
+ arguments: structuredClone(protectedCall.validated.arguments),
1778
+ preCallMessages,
1779
+ frozenAssistantMessage: structuredClone(msg),
1780
+ schemaDigest: protectedCall.validated.schemaDigest,
1781
+ toolDigest,
1782
+ policyDigest,
1783
+ policyId: protectedCall.policy.policyId,
1784
+ actionClass: protectedCall.policy.actionClass,
1785
+ });
1786
+ suspension = {
1787
+ approvalId: committed.approvalId,
1788
+ toolCallId: protectedCall.call.id,
1789
+ checkpointDigest: committed.checkpointDigest,
1790
+ suspendedSessionRevision: committed.suspendedSessionRevision,
1791
+ };
1792
+ outcome = "suspended";
1793
+ done = true;
1794
+ (0, runtime_1.emitNervesEvent)({
1795
+ component: "engine",
1796
+ event: "engine.approval_turn_suspended",
1797
+ message: "agent turn suspended before protected tool execution",
1798
+ meta: { approvalId: committed.approvalId, toolCallId: protectedCall.call.id },
1799
+ });
1800
+ continue;
1801
+ }
1626
1802
  const containsSoleCallOnlyViolation = result.toolCalls.length > 1
1627
1803
  && result.toolCalls.some((call) => {
1628
1804
  const terminalProjection = (0, tools_1.resolveToolDefinition)(call.name)?.terminalProjection;
@@ -1975,6 +2151,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1975
2151
  usage: lastUsage,
1976
2152
  outcome,
1977
2153
  completion,
2154
+ ...(suspension ? { suspension } : {}),
1978
2155
  ...(terminalError ? { error: terminalError, errorClassification: terminalErrorClassification } : {}),
1979
2156
  };
1980
2157
  }
@@ -34,6 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.EVENT_CONTENT_MAX_CHARS = void 0;
37
+ exports.materializeApprovalTerminal = materializeApprovalTerminal;
38
+ exports.attachApprovalSuspensionCheckpoint = attachApprovalSuspensionCheckpoint;
39
+ exports.listApprovalSuspensionCheckpoints = listApprovalSuspensionCheckpoints;
40
+ exports.removeApprovalSuspensionCheckpoint = removeApprovalSuspensionCheckpoint;
37
41
  exports.truncateLargeEventContent = truncateLargeEventContent;
38
42
  exports.capStructuredRecordString = capStructuredRecordString;
39
43
  exports.capStructuredRecordStringArray = capStructuredRecordStringArray;
@@ -59,8 +63,169 @@ exports.loadSessionEnvelopeFile = loadSessionEnvelopeFile;
59
63
  exports.buildCanonicalSessionEnvelope = buildCanonicalSessionEnvelope;
60
64
  exports.appendSyntheticAssistantEvent = appendSyntheticAssistantEvent;
61
65
  const fs = __importStar(require("fs"));
66
+ const node_crypto_1 = require("node:crypto");
62
67
  const runtime_1 = require("../nerves/runtime");
63
68
  const structured_output_1 = require("./structured-output");
69
+ const APPROVAL_TERMINAL_TEXT = {
70
+ denied: "approval denied by requester; the protected action was not executed",
71
+ expired: "approval expired before execution; the protected action was not executed",
72
+ drifted: "approval became invalid before execution; the protected action was not executed",
73
+ abandoned_before_attempt: "approval was abandoned before execution; the protected action was not executed; request a fresh approval",
74
+ attempted_indeterminate: "execution may have occurred; do not retry automatically",
75
+ };
76
+ function messagesRevision(messages) {
77
+ return (0, node_crypto_1.createHash)("sha256").update(JSON.stringify(messages), "utf8").digest("hex");
78
+ }
79
+ function hasExactMaterializedApprovalPair(messages, checkpoint, toolCallId, content) {
80
+ if (messages.length < 2)
81
+ return false;
82
+ const assistant = messages.at(-2);
83
+ const tool = messages.at(-1);
84
+ const expectedTool = {
85
+ role: "tool",
86
+ tool_call_id: toolCallId,
87
+ content,
88
+ };
89
+ return assistant.role === "assistant"
90
+ && tool.role === "tool"
91
+ && messageFingerprint(assistant) === messageFingerprint(checkpoint.frozenAssistantMessage)
92
+ && messageFingerprint(tool) === messageFingerprint(expectedTool);
93
+ }
94
+ function materializeApprovalTerminal(input) {
95
+ const messages = structuredClone(input.messages);
96
+ if (input.record.state === "session_head_changed") {
97
+ return {
98
+ messages,
99
+ materialized: false,
100
+ resumeProvider: false,
101
+ directNotice: "the session changed before this approval could be applied; the protected action was not executed",
102
+ revision: messagesRevision(messages),
103
+ };
104
+ }
105
+ const content = input.record.state === "succeeded" || input.record.state === "failed"
106
+ ? input.record.result
107
+ : APPROVAL_TERMINAL_TEXT[input.record.state];
108
+ if (!content)
109
+ throw new Error(`approval state ${input.record.state} cannot be materialized`);
110
+ const resumeProvider = input.record.state === "succeeded" || input.record.state === "failed" || input.record.state === "denied";
111
+ const exactPair = hasExactMaterializedApprovalPair(messages, input.checkpoint, input.record.toolCallId, content);
112
+ if (exactPair) {
113
+ return {
114
+ messages,
115
+ materialized: false,
116
+ resumeProvider,
117
+ ...(resumeProvider ? {} : { directNotice: content }),
118
+ ...(input.record.state === "attempted_indeterminate" ? { severity: "high" } : {}),
119
+ revision: messagesRevision(messages),
120
+ };
121
+ }
122
+ const advanced = input.currentSessionRevision !== input.checkpoint.suspendedSessionRevision;
123
+ if (advanced) {
124
+ return {
125
+ messages,
126
+ materialized: false,
127
+ resumeProvider: false,
128
+ directNotice: "the session changed before this approval could be applied; the protected action was not executed",
129
+ revision: messagesRevision(messages),
130
+ };
131
+ }
132
+ if (input.materializedApprovalIds?.includes(input.record.approvalId)) {
133
+ return {
134
+ messages,
135
+ materialized: false,
136
+ resumeProvider,
137
+ ...(resumeProvider ? {} : { directNotice: content }),
138
+ ...(input.record.state === "attempted_indeterminate" ? { severity: "high" } : {}),
139
+ revision: messagesRevision(messages),
140
+ };
141
+ }
142
+ const frozen = structuredClone(input.checkpoint.frozenAssistantMessage);
143
+ messages.push(frozen, { role: "tool", tool_call_id: input.record.toolCallId, content });
144
+ const directNotice = resumeProvider ? undefined : content;
145
+ (0, runtime_1.emitNervesEvent)({
146
+ component: "mind",
147
+ event: "mind.approval_terminal_materialized",
148
+ message: "materialized approval terminal transcript pair",
149
+ meta: { approvalId: input.record.approvalId, state: input.record.state, resumeProvider },
150
+ });
151
+ return {
152
+ messages,
153
+ materialized: true,
154
+ resumeProvider,
155
+ ...(directNotice ? { directNotice } : {}),
156
+ ...(input.record.state === "attempted_indeterminate" ? { severity: "high" } : {}),
157
+ revision: messagesRevision(messages),
158
+ };
159
+ }
160
+ const APPROVAL_HASH = /^[a-f0-9]{64}$/;
161
+ function normalizeApprovalSuspensions(value) {
162
+ if (!Array.isArray(value))
163
+ return [];
164
+ return value.flatMap((item) => {
165
+ if (!item || typeof item !== "object" || Array.isArray(item))
166
+ return [];
167
+ const record = item;
168
+ if (typeof record.approvalId !== "string"
169
+ || typeof record.checkpointDigest !== "string"
170
+ || !APPROVAL_HASH.test(record.checkpointDigest)
171
+ || typeof record.baseSessionRevision !== "string"
172
+ || !APPROVAL_HASH.test(record.baseSessionRevision)
173
+ || typeof record.suspendedSessionRevision !== "string"
174
+ || !APPROVAL_HASH.test(record.suspendedSessionRevision)
175
+ || typeof record.argumentDigest !== "string"
176
+ || !APPROVAL_HASH.test(record.argumentDigest)
177
+ || typeof record.schemaDigest !== "string"
178
+ || !APPROVAL_HASH.test(record.schemaDigest)
179
+ || typeof record.toolDigest !== "string"
180
+ || !APPROVAL_HASH.test(record.toolDigest)
181
+ || typeof record.policyDigest !== "string"
182
+ || !APPROVAL_HASH.test(record.policyDigest)
183
+ || typeof record.preCallDigest !== "string"
184
+ || !APPROVAL_HASH.test(record.preCallDigest)
185
+ || !Array.isArray(record.preCallMessages)
186
+ || !record.frozenAssistantMessage
187
+ || typeof record.frozenAssistantMessage !== "object"
188
+ || Array.isArray(record.frozenAssistantMessage))
189
+ return [];
190
+ return [{
191
+ approvalId: record.approvalId,
192
+ checkpointDigest: record.checkpointDigest,
193
+ baseSessionRevision: record.baseSessionRevision,
194
+ suspendedSessionRevision: record.suspendedSessionRevision,
195
+ argumentDigest: record.argumentDigest,
196
+ schemaDigest: record.schemaDigest,
197
+ toolDigest: record.toolDigest,
198
+ policyDigest: record.policyDigest,
199
+ preCallDigest: record.preCallDigest,
200
+ preCallMessages: structuredClone(record.preCallMessages),
201
+ frozenAssistantMessage: structuredClone(record.frozenAssistantMessage),
202
+ }];
203
+ });
204
+ }
205
+ function attachApprovalSuspensionCheckpoint(envelope, checkpoint) {
206
+ const existing = envelope.approvalSuspensions ?? [];
207
+ const retained = existing.filter((item) => item.approvalId !== checkpoint.approvalId);
208
+ (0, runtime_1.emitNervesEvent)({
209
+ component: "engine",
210
+ event: "engine.approval_checkpoint_attached",
211
+ message: "approval suspension checkpoint attached outside provider projection",
212
+ meta: { approvalId: checkpoint.approvalId },
213
+ });
214
+ return { ...envelope, approvalSuspensions: [...retained, structuredClone(checkpoint)] };
215
+ }
216
+ function listApprovalSuspensionCheckpoints(envelope) {
217
+ return structuredClone(envelope.approvalSuspensions ?? []);
218
+ }
219
+ function removeApprovalSuspensionCheckpoint(envelope, approvalId) {
220
+ const approvalSuspensions = (envelope.approvalSuspensions ?? []).filter((item) => item.approvalId !== approvalId);
221
+ (0, runtime_1.emitNervesEvent)({
222
+ component: "engine",
223
+ event: "engine.approval_checkpoint_removed",
224
+ message: "approval suspension checkpoint removed",
225
+ meta: { approvalId },
226
+ });
227
+ return { ...envelope, approvalSuspensions };
228
+ }
64
229
  exports.EVENT_CONTENT_MAX_CHARS = 256 * 1024;
65
230
  function truncateLargeEventContent(content, maxChars) {
66
231
  if (typeof content !== "string") {
@@ -972,6 +1137,7 @@ function parseSessionEnvelope(raw, options = {}) {
972
1137
  : (0, structured_output_1.normalizeStructuredOutputs)(record.structuredOutputs),
973
1138
  lastUsage: normalizeUsage(record.lastUsage),
974
1139
  state: normalizeContinuityState(record.state),
1140
+ approvalSuspensions: normalizeApprovalSuspensions(record.approvalSuspensions),
975
1141
  };
976
1142
  }
977
1143
  function loadSessionEnvelopeFile(filePath) {
@@ -1109,6 +1275,7 @@ function buildCanonicalSessionEnvelope(options) {
1109
1275
  structuredOutputs: (0, structured_output_1.extractStructuredOutputsFromEvents)(prunedEvents),
1110
1276
  lastUsage: normalizeUsage(options.lastUsage),
1111
1277
  state: normalizeContinuityState(options.state),
1278
+ approvalSuspensions: structuredClone(options.existing?.approvalSuspensions ?? []),
1112
1279
  },
1113
1280
  evictedEvents,
1114
1281
  };