@buoy-gg/agent-core 7.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +42 -0
  3. package/lib/commonjs/blocks/receipts.js +702 -0
  4. package/lib/commonjs/blocks/receipts.js.map +1 -0
  5. package/lib/commonjs/blocks/types.js +119 -0
  6. package/lib/commonjs/blocks/types.js.map +1 -0
  7. package/lib/commonjs/blocks/uiTool.js +994 -0
  8. package/lib/commonjs/blocks/uiTool.js.map +1 -0
  9. package/lib/commonjs/catalog/catalog.g.js +4044 -0
  10. package/lib/commonjs/catalog/catalog.g.js.map +1 -0
  11. package/lib/commonjs/catalog/catalog.source.json +4956 -0
  12. package/lib/commonjs/catalog/snapshotReads.js +256 -0
  13. package/lib/commonjs/catalog/snapshotReads.js.map +1 -0
  14. package/lib/commonjs/catalog/toProviderTools.js +107 -0
  15. package/lib/commonjs/catalog/toProviderTools.js.map +1 -0
  16. package/lib/commonjs/catalog/validateParams.js +129 -0
  17. package/lib/commonjs/catalog/validateParams.js.map +1 -0
  18. package/lib/commonjs/context/buildContextPack.js +141 -0
  19. package/lib/commonjs/context/buildContextPack.js.map +1 -0
  20. package/lib/commonjs/effects/ledger.js +229 -0
  21. package/lib/commonjs/effects/ledger.js.map +1 -0
  22. package/lib/commonjs/engine/effectFor.js +95 -0
  23. package/lib/commonjs/engine/effectFor.js.map +1 -0
  24. package/lib/commonjs/engine/runAgentTurn.js +667 -0
  25. package/lib/commonjs/engine/runAgentTurn.js.map +1 -0
  26. package/lib/commonjs/engine/systemPrompt.js +86 -0
  27. package/lib/commonjs/engine/systemPrompt.js.map +1 -0
  28. package/lib/commonjs/index.js +288 -0
  29. package/lib/commonjs/index.js.map +1 -0
  30. package/lib/commonjs/package.json +1 -0
  31. package/lib/commonjs/policy/labels.js +303 -0
  32. package/lib/commonjs/policy/labels.js.map +1 -0
  33. package/lib/commonjs/policy/policy.js +143 -0
  34. package/lib/commonjs/policy/policy.js.map +1 -0
  35. package/lib/commonjs/policy/redact.js +135 -0
  36. package/lib/commonjs/policy/redact.js.map +1 -0
  37. package/lib/commonjs/providers/anthropic.js +272 -0
  38. package/lib/commonjs/providers/anthropic.js.map +1 -0
  39. package/lib/commonjs/providers/openai.js +193 -0
  40. package/lib/commonjs/providers/openai.js.map +1 -0
  41. package/lib/commonjs/providers/sse.js +104 -0
  42. package/lib/commonjs/providers/sse.js.map +1 -0
  43. package/lib/commonjs/providers/types.js +6 -0
  44. package/lib/commonjs/providers/types.js.map +1 -0
  45. package/lib/commonjs/session.js +173 -0
  46. package/lib/commonjs/session.js.map +1 -0
  47. package/lib/commonjs/types.js +2 -0
  48. package/lib/commonjs/types.js.map +1 -0
  49. package/lib/module/blocks/receipts.js +696 -0
  50. package/lib/module/blocks/receipts.js.map +1 -0
  51. package/lib/module/blocks/types.js +113 -0
  52. package/lib/module/blocks/types.js.map +1 -0
  53. package/lib/module/blocks/uiTool.js +987 -0
  54. package/lib/module/blocks/uiTool.js.map +1 -0
  55. package/lib/module/catalog/catalog.g.js +4040 -0
  56. package/lib/module/catalog/catalog.g.js.map +1 -0
  57. package/lib/module/catalog/catalog.source.json +4956 -0
  58. package/lib/module/catalog/snapshotReads.js +252 -0
  59. package/lib/module/catalog/snapshotReads.js.map +1 -0
  60. package/lib/module/catalog/toProviderTools.js +101 -0
  61. package/lib/module/catalog/toProviderTools.js.map +1 -0
  62. package/lib/module/catalog/validateParams.js +124 -0
  63. package/lib/module/catalog/validateParams.js.map +1 -0
  64. package/lib/module/context/buildContextPack.js +138 -0
  65. package/lib/module/context/buildContextPack.js.map +1 -0
  66. package/lib/module/effects/ledger.js +223 -0
  67. package/lib/module/effects/ledger.js.map +1 -0
  68. package/lib/module/engine/effectFor.js +91 -0
  69. package/lib/module/engine/effectFor.js.map +1 -0
  70. package/lib/module/engine/runAgentTurn.js +663 -0
  71. package/lib/module/engine/runAgentTurn.js.map +1 -0
  72. package/lib/module/engine/systemPrompt.js +82 -0
  73. package/lib/module/engine/systemPrompt.js.map +1 -0
  74. package/lib/module/index.js +30 -0
  75. package/lib/module/index.js.map +1 -0
  76. package/lib/module/policy/labels.js +296 -0
  77. package/lib/module/policy/labels.js.map +1 -0
  78. package/lib/module/policy/policy.js +137 -0
  79. package/lib/module/policy/policy.js.map +1 -0
  80. package/lib/module/policy/redact.js +128 -0
  81. package/lib/module/policy/redact.js.map +1 -0
  82. package/lib/module/providers/anthropic.js +268 -0
  83. package/lib/module/providers/anthropic.js.map +1 -0
  84. package/lib/module/providers/openai.js +189 -0
  85. package/lib/module/providers/openai.js.map +1 -0
  86. package/lib/module/providers/sse.js +99 -0
  87. package/lib/module/providers/sse.js.map +1 -0
  88. package/lib/module/providers/types.js +4 -0
  89. package/lib/module/providers/types.js.map +1 -0
  90. package/lib/module/session.js +168 -0
  91. package/lib/module/session.js.map +1 -0
  92. package/lib/module/types.js +2 -0
  93. package/lib/module/types.js.map +1 -0
  94. package/lib/typescript/blocks/receipts.d.ts +32 -0
  95. package/lib/typescript/blocks/receipts.d.ts.map +1 -0
  96. package/lib/typescript/blocks/types.d.ts +576 -0
  97. package/lib/typescript/blocks/types.d.ts.map +1 -0
  98. package/lib/typescript/blocks/uiTool.d.ts +15 -0
  99. package/lib/typescript/blocks/uiTool.d.ts.map +1 -0
  100. package/lib/typescript/catalog/catalog.g.d.ts +11 -0
  101. package/lib/typescript/catalog/catalog.g.d.ts.map +1 -0
  102. package/lib/typescript/catalog/snapshotReads.d.ts +37 -0
  103. package/lib/typescript/catalog/snapshotReads.d.ts.map +1 -0
  104. package/lib/typescript/catalog/toProviderTools.d.ts +52 -0
  105. package/lib/typescript/catalog/toProviderTools.d.ts.map +1 -0
  106. package/lib/typescript/catalog/validateParams.d.ts +33 -0
  107. package/lib/typescript/catalog/validateParams.d.ts.map +1 -0
  108. package/lib/typescript/context/buildContextPack.d.ts +32 -0
  109. package/lib/typescript/context/buildContextPack.d.ts.map +1 -0
  110. package/lib/typescript/effects/ledger.d.ts +119 -0
  111. package/lib/typescript/effects/ledger.d.ts.map +1 -0
  112. package/lib/typescript/engine/effectFor.d.ts +21 -0
  113. package/lib/typescript/engine/effectFor.d.ts.map +1 -0
  114. package/lib/typescript/engine/runAgentTurn.d.ts +140 -0
  115. package/lib/typescript/engine/runAgentTurn.d.ts.map +1 -0
  116. package/lib/typescript/engine/systemPrompt.d.ts +49 -0
  117. package/lib/typescript/engine/systemPrompt.d.ts.map +1 -0
  118. package/lib/typescript/index.d.ts +29 -0
  119. package/lib/typescript/index.d.ts.map +1 -0
  120. package/lib/typescript/policy/labels.d.ts +10 -0
  121. package/lib/typescript/policy/labels.d.ts.map +1 -0
  122. package/lib/typescript/policy/policy.d.ts +106 -0
  123. package/lib/typescript/policy/policy.d.ts.map +1 -0
  124. package/lib/typescript/policy/redact.d.ts +37 -0
  125. package/lib/typescript/policy/redact.d.ts.map +1 -0
  126. package/lib/typescript/providers/anthropic.d.ts +22 -0
  127. package/lib/typescript/providers/anthropic.d.ts.map +1 -0
  128. package/lib/typescript/providers/openai.d.ts +19 -0
  129. package/lib/typescript/providers/openai.d.ts.map +1 -0
  130. package/lib/typescript/providers/sse.d.ts +18 -0
  131. package/lib/typescript/providers/sse.d.ts.map +1 -0
  132. package/lib/typescript/providers/types.d.ts +129 -0
  133. package/lib/typescript/providers/types.d.ts.map +1 -0
  134. package/lib/typescript/session.d.ts +58 -0
  135. package/lib/typescript/session.d.ts.map +1 -0
  136. package/lib/typescript/types.d.ts +119 -0
  137. package/lib/typescript/types.d.ts.map +1 -0
  138. package/package.json +58 -0
@@ -0,0 +1,667 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.runAgentTurn = runAgentTurn;
7
+ exports.runGatedAction = runGatedAction;
8
+ var _catalog = require("../catalog/catalog.g");
9
+ var _validateParams = require("../catalog/validateParams");
10
+ var _toProviderTools = require("../catalog/toProviderTools");
11
+ var _snapshotReads = require("../catalog/snapshotReads");
12
+ var _ledger = require("../effects/ledger");
13
+ var _policy = require("../policy/policy");
14
+ var _redact = require("../policy/redact");
15
+ var _effectFor = require("./effectFor");
16
+ var _uiTool = require("../blocks/uiTool");
17
+ var _receipts = require("../blocks/receipts");
18
+ var _types = require("../blocks/types");
19
+ /**
20
+ * One turn: user sentence in, tool calls and an answer out.
21
+ *
22
+ * The loop is deliberately explicit rather than clever, because every step of
23
+ * it encodes something that bites otherwise:
24
+ *
25
+ * - Validation happens BEFORE dispatch and a failure comes back as a readable
26
+ * tool_result, not an exception. Buoy's adapters validate nothing, so a
27
+ * wrong shape becomes an undefined read deep inside a tool — and an LLM
28
+ * omitting `replace` on `zustand.setState` replaces the entire store.
29
+ * - `app.reloadApp` is TERMINAL. It returns before the reload fires and then
30
+ * kills the JS realm; anything queued behind it dies with the conversation.
31
+ * - Results are truncated before they are sent. `dispatchToolAction` bypasses
32
+ * the 8MB wire cap that protects the broker path, so an in-app agent can
33
+ * receive a 50MB object and then try to feed it to a token-metered model.
34
+ * - Steps and wall-clock are capped, because a model that misreads a result
35
+ * will otherwise retry the same call until the user closes the sheet.
36
+ */
37
+
38
+ /** Beyond this a tool result costs more in tokens than it can possibly be worth. */
39
+ const MAX_RESULT_CHARS = 24_000;
40
+ const DEFAULT_MAX_STEPS = 12;
41
+ const DEFAULT_TURN_MS = 180_000;
42
+ function findDescriptor(catalog, toolId, action) {
43
+ return catalog.find(t => t.toolId === toolId)?.actions.find(a => a.action === action);
44
+ }
45
+ function encodeResult(value) {
46
+ let text;
47
+ try {
48
+ text = JSON.stringify(value ?? null);
49
+ } catch {
50
+ text = String(value);
51
+ }
52
+ if (text.length <= MAX_RESULT_CHARS) return text;
53
+ return `${text.slice(0, MAX_RESULT_CHARS)}\n\n[truncated — ${text.length} characters total. Ask for a narrower slice if you need more.]`;
54
+ }
55
+
56
+ /** A short line for the transcript row, so the UI never renders raw payloads. */
57
+ /**
58
+ * The trailing status word on a transcript row — "12 items", "Not found",
59
+ * "Done". Written for the person watching, not the model (which gets the full
60
+ * result): a list of JSON key names ("found, storeName, …") told a QA tester
61
+ * nothing, so this reads the few result shapes that carry meaning and says
62
+ * "Done" for the rest.
63
+ */
64
+ function summarise(value) {
65
+ if (value === null || value === undefined) return "Done";
66
+ if (Array.isArray(value)) return countOf(value.length);
67
+ if (typeof value === "boolean") return value ? "Done" : "Failed";
68
+ if (typeof value === "object") {
69
+ const rec = value;
70
+ if (rec.ok === false) return typeof rec.error === "string" ? rec.error : "Failed";
71
+ if (rec.found === false) return "Not found";
72
+ if (rec.tapped === false) return "Nothing to tap";
73
+ if (rec.active === false) return "Saved, but inactive in this build";
74
+ for (const key of ["returned", "count", "total"]) {
75
+ const n = rec[key];
76
+ if (typeof n === "number") return countOf(n);
77
+ }
78
+ // `{requests:[…]}` → "12 items", but a snapshot with several lists
79
+ // (`{stack, routes, recentNavigations}`) has no single honest count.
80
+ const lists = Object.values(rec).filter(Array.isArray);
81
+ if (lists.length === 1) return countOf(lists[0].length);
82
+ return "Done";
83
+ }
84
+ return String(value).slice(0, 80);
85
+ }
86
+ function countOf(n) {
87
+ return n === 1 ? "1 item" : `${n} items`;
88
+ }
89
+ async function* runAgentTurn(input) {
90
+ const {
91
+ provider,
92
+ catalog,
93
+ system,
94
+ model,
95
+ maxTokens,
96
+ dispatch,
97
+ readSnapshot,
98
+ storeKeyOwners,
99
+ ledger,
100
+ policy,
101
+ isRelease,
102
+ requestApproval,
103
+ signal
104
+ } = input;
105
+ const messages = [...input.messages];
106
+ const tools = (0, _toProviderTools.toProviderTools)(catalog, {
107
+ availableToolIds: input.availableToolIds,
108
+ availableActions: input.availableActions,
109
+ isRelease
110
+ });
111
+ tools.push((0, _uiTool.uiProviderTool)());
112
+ const maxSteps = policy.maxSteps ?? DEFAULT_MAX_STEPS;
113
+ const deadline = Date.now() + DEFAULT_TURN_MS;
114
+
115
+ // Every http(s) URL that appeared in a tool result THIS turn. The prompt
116
+ // tells the model image URLs must come from here; this Set is the
117
+ // enforcement — a model-remembered or injected URL in an image block is
118
+ // both a fabrication vector and a data-exfil beacon (the GET carries
119
+ // whatever is encoded into the URL).
120
+ const seenUrls = new Set();
121
+ const URL_RE = /https?:\/\/[^\s"'\\)\]}>]+/g;
122
+ for (let step = 0; step < maxSteps; step++) {
123
+ if (signal?.aborted) {
124
+ yield {
125
+ type: "done"
126
+ };
127
+ return messages;
128
+ }
129
+ if (Date.now() > deadline) {
130
+ yield {
131
+ type: "error",
132
+ message: "This is taking too long — stopping here."
133
+ };
134
+ yield {
135
+ type: "done"
136
+ };
137
+ return messages;
138
+ }
139
+ let text = "";
140
+ const calls = [];
141
+ let failed = false;
142
+ for await (const ev of provider.send({
143
+ messages,
144
+ system,
145
+ tools,
146
+ model,
147
+ maxTokens,
148
+ signal
149
+ })) {
150
+ if (ev.type === "text") {
151
+ text += ev.delta;
152
+ yield {
153
+ type: "text",
154
+ delta: ev.delta
155
+ };
156
+ } else if (ev.type === "tool-call") {
157
+ calls.push(ev.call);
158
+ } else if (ev.type === "error") {
159
+ // A mid-stream error can arrive on an already-committed 200.
160
+ yield {
161
+ type: "error",
162
+ message: ev.message
163
+ };
164
+ failed = true;
165
+ } else if (ev.type === "done" && ev.usage) {
166
+ // Surfaced so a host can meter cost per seat. Parsed for a long time;
167
+ // went nowhere anyone could see.
168
+ yield {
169
+ type: "usage",
170
+ input: ev.usage.input,
171
+ output: ev.usage.output
172
+ };
173
+ }
174
+ }
175
+ if (failed) {
176
+ yield {
177
+ type: "done"
178
+ };
179
+ return messages;
180
+ }
181
+ messages.push({
182
+ role: "assistant",
183
+ text,
184
+ toolCalls: calls.length ? calls : undefined
185
+ });
186
+ if (calls.length === 0) {
187
+ yield {
188
+ type: "done"
189
+ };
190
+ return messages;
191
+ }
192
+ const results = [];
193
+ let realmWillDie = false;
194
+ let awaiting = null;
195
+ for (const call of calls) {
196
+ // `buoy_ui`: show or ask. Validated like any action; an Ask block ends
197
+ // the turn after this batch — the answer comes back as a user message.
198
+ if (call.name === _uiTool.UI_TOOL_NAME) {
199
+ const built = (0, _uiTool.buildUiBlock)(call.input);
200
+ if (!built.ok || !built.block) {
201
+ results.push({
202
+ toolCallId: call.id,
203
+ content: `Invalid ${_uiTool.UI_TOOL_NAME} block:\n${built.errors.join("\n")}`,
204
+ isError: true
205
+ });
206
+ continue;
207
+ }
208
+ // Provenance for pictures, ENFORCED: an image URL the model did not
209
+ // read from a tool result this turn does not render. Live or nothing.
210
+ if (built.block.kind === "imageGrid") {
211
+ const kept = built.block.images.filter(img => seenUrls.has(img.url));
212
+ if (kept.length === 0) {
213
+ results.push({
214
+ toolCallId: call.id,
215
+ content: "Rejected: every image URL must be one you read from a tool result THIS turn (images.list, a storage value, a response body). Never invent or remember URLs — read them first.",
216
+ isError: true
217
+ });
218
+ continue;
219
+ }
220
+ built.block = {
221
+ ...built.block,
222
+ images: kept
223
+ };
224
+ }
225
+ if (built.block.kind === "list") {
226
+ built.block = {
227
+ ...built.block,
228
+ items: built.block.items.map(item => item.image && !seenUrls.has(item.image) ? {
229
+ ...item,
230
+ image: undefined
231
+ } : item)
232
+ };
233
+ }
234
+ yield {
235
+ type: "block",
236
+ block: built.block,
237
+ forToolCallId: call.id
238
+ };
239
+ if ((0, _types.isAskBlock)(built.block)) {
240
+ awaiting = built.block.id;
241
+ results.push({
242
+ toolCallId: call.id,
243
+ content: "Shown. Waiting for the user's answer — it will arrive as their next message. Do not guess it."
244
+ });
245
+ } else {
246
+ results.push({
247
+ toolCallId: call.id,
248
+ content: "Shown to the user. Don't repeat its contents in text."
249
+ });
250
+ }
251
+ continue;
252
+ }
253
+ const toolId = (0, _toProviderTools.fromProviderToolName)(call.name, catalog);
254
+ const action = call.input.action;
255
+
256
+ // Two different mistakes, kept apart on purpose. Merging them tells a
257
+ // model that forgot `action` that the TOOL does not exist, and it stops
258
+ // reaching for a tool that was fine — the same distinction
259
+ // `dispatchToolAction` draws between an unknown tool and an unknown
260
+ // action, for the same reason.
261
+ if (!toolId) {
262
+ results.push({
263
+ toolCallId: call.id,
264
+ content: `There is no tool called "${call.name}". Use one of the tools you were given.`,
265
+ isError: true
266
+ });
267
+ continue;
268
+ }
269
+ if (!action) {
270
+ results.push({
271
+ toolCallId: call.id,
272
+ content: `"${call.name}" needs an "action" — it is required, and it names which of this tool's actions to run. See the action list in the tool's description.`,
273
+ isError: true
274
+ });
275
+ continue;
276
+ }
277
+ const descriptor = findDescriptor(catalog, toolId, action);
278
+ if (!descriptor) {
279
+ // Name the alternatives. Without them a model that guessed an action
280
+ // has nowhere to go but guess again, and the next guess is no better
281
+ // informed than the last.
282
+ const known = catalog.find(t => t.toolId === toolId)?.actions.map(a => a.action).join(", ");
283
+ results.push({
284
+ toolCallId: call.id,
285
+ content: `"${action}" is not an action on ${toolId}.${known ? ` Valid actions: ${known}.` : ""}`,
286
+ isError: true
287
+ });
288
+ continue;
289
+ }
290
+ const raw = call.input.params ?? {};
291
+ const params = (0, _validateParams.applyDefaults)(raw, descriptor.params);
292
+ const check = (0, _validateParams.validateParams)(params, descriptor.params);
293
+ if (!check.ok) {
294
+ results.push({
295
+ toolCallId: call.id,
296
+ content: `Invalid parameters for ${toolId}.${action}:\n${check.errors.join("\n")}\n\nExpected schema:\n${JSON.stringify(descriptor.params)}`,
297
+ isError: true
298
+ });
299
+ continue;
300
+ }
301
+ const label = (0, _policy.describeCall)(toolId, descriptor, params);
302
+ const verdict = (0, _policy.decide)({
303
+ descriptor,
304
+ toolId,
305
+ policy,
306
+ isRelease
307
+ });
308
+ if (verdict.verdict === "refuse") {
309
+ yield {
310
+ type: "tool-end",
311
+ id: call.id,
312
+ ok: false,
313
+ summary: "refused"
314
+ };
315
+ results.push({
316
+ toolCallId: call.id,
317
+ content: verdict.reason,
318
+ isError: true
319
+ });
320
+ continue;
321
+ }
322
+ if (verdict.verdict === "needs-approval") {
323
+ yield {
324
+ type: "approval-required",
325
+ id: call.id,
326
+ toolId,
327
+ action,
328
+ label,
329
+ description: descriptor.summary,
330
+ reason: verdict.reason
331
+ };
332
+ const approved = requestApproval ? await requestApproval({
333
+ id: call.id,
334
+ toolId,
335
+ action,
336
+ label,
337
+ description: descriptor.summary,
338
+ reason: verdict.reason
339
+ }) : false;
340
+ if (!approved) {
341
+ results.push({
342
+ toolCallId: call.id,
343
+ content: "The user declined this change. Do not retry it; ask what they would prefer.",
344
+ isError: true
345
+ });
346
+ continue;
347
+ }
348
+ }
349
+ yield {
350
+ type: "tool-start",
351
+ id: call.id,
352
+ toolId,
353
+ action,
354
+ label,
355
+ description: descriptor.summary
356
+ };
357
+
358
+ // Capture the prior value BEFORE a storage write, so undo can restore it
359
+ // exactly. This is the one thing that makes storage reversible here when
360
+ // the Scenarios engine has to treat it as permanent.
361
+ const before = await captureBefore(descriptor, toolId, params, dispatch);
362
+ const stateBefore = await captureStateBefore(descriptor, toolId, params, dispatch);
363
+ try {
364
+ // `getSnapshot` is reserved: it is not an adapter action, so it goes to
365
+ // the snapshot reader and is trimmed to the fields worth sending. See
366
+ // catalog/snapshotReads.ts. `ask-buoy.*` is the session itself — the
367
+ // ledger the model asks about lives HERE, not behind an adapter, so
368
+ // routing it through dispatch would only work on a device and would
369
+ // record the undo as a fresh effect.
370
+ const result = action === _snapshotReads.SNAPSHOT_ACTION ? (0, _snapshotReads.projectSnapshot)(toolId, await requireSnapshot(readSnapshot, toolId), params) : toolId === "ask-buoy" ? await runAskBuoyAction(action, ledger, dispatch) : await dispatch(toolId, action, params);
371
+ const cleaned = (0, _redact.redact)((0, _redact.stripSelfTraffic)(result));
372
+ if (descriptor.effect !== "read" && toolId !== "ask-buoy") {
373
+ // A delete of something we created IS the undo, whoever asked for
374
+ // it — mark the matching entry undone instead of leaving the bar
375
+ // promising an undo against a rule that no longer exists.
376
+ ledger.noteExternalRevert(toolId, action, params);
377
+ const fx = (0, _effectFor.effectFor)(toolId, descriptor, params, result, before);
378
+ if (fx) ledger.record(toolId, action, fx);
379
+ }
380
+ yield {
381
+ type: "tool-end",
382
+ id: call.id,
383
+ ok: true,
384
+ summary: summarise(result)
385
+ };
386
+
387
+ // The receipt: what the user sees of this result, built from the
388
+ // same redacted data the model gets. See blocks/receipts.ts.
389
+ const receipt = (0, _receipts.projectReceipt)({
390
+ toolId,
391
+ action,
392
+ params,
393
+ result: cleaned,
394
+ before: stateBefore ?? before,
395
+ effect: descriptor.effect
396
+ });
397
+ if (receipt) yield {
398
+ type: "block",
399
+ block: receipt,
400
+ forToolCallId: call.id
401
+ };
402
+ const encoded = encodeResult(cleaned);
403
+ for (const url of encoded.match(URL_RE) ?? []) seenUrls.add(url);
404
+ results.push({
405
+ toolCallId: call.id,
406
+ content: encoded + ownedKeyNote(toolId, descriptor, params, storeKeyOwners)
407
+ });
408
+ if (toolId === "app" && (action === "reloadApp" || action === "reload")) {
409
+ realmWillDie = true;
410
+ }
411
+ } catch (err) {
412
+ const message = err instanceof Error ? err.message : String(err);
413
+ yield {
414
+ type: "tool-end",
415
+ id: call.id,
416
+ ok: false,
417
+ summary: message
418
+ };
419
+ results.push({
420
+ toolCallId: call.id,
421
+ content: `Failed: ${message}`,
422
+ isError: true
423
+ });
424
+ }
425
+ }
426
+ messages.push({
427
+ role: "tool-results",
428
+ results
429
+ });
430
+ if (awaiting) {
431
+ yield {
432
+ type: "awaiting-user",
433
+ blockId: awaiting
434
+ };
435
+ yield {
436
+ type: "done"
437
+ };
438
+ return messages;
439
+ }
440
+ if (realmWillDie) {
441
+ // Anything after this dies with the JS realm. Stop cleanly instead of
442
+ // sending a request whose answer can never arrive.
443
+ yield {
444
+ type: "done"
445
+ };
446
+ return messages;
447
+ }
448
+ }
449
+ yield {
450
+ type: "block",
451
+ block: {
452
+ id: `cap${Date.now()}`,
453
+ kind: "notice",
454
+ tone: "warning",
455
+ text: `Stopped after ${maxSteps} steps to avoid looping.`,
456
+ actions: [{
457
+ label: "Continue",
458
+ primary: true,
459
+ send: "Continue where you left off."
460
+ }]
461
+ }
462
+ };
463
+ yield {
464
+ type: "done"
465
+ };
466
+ return messages;
467
+ }
468
+
469
+ /**
470
+ * A factual postscript for a storage write that landed on a store's saved copy.
471
+ *
472
+ * This is a STATEMENT ABOUT WHAT HAPPENED, not a policy reminder — the same
473
+ * kind of thing the release refusals say, and the reason it belongs in the
474
+ * result rather than the system prompt. The write really did succeed and the
475
+ * app really did not change, and only the engine is in a position to know
476
+ * both at once.
477
+ *
478
+ * Deliberately not a refusal. Writing the on-disk value on purpose — "make it
479
+ * this on next launch" — is legitimate, and a hard block would make that
480
+ * impossible. Saying so leaves the model free to continue, correct itself with
481
+ * `zustand.rehydrate`, or explain the distinction to the user.
482
+ */
483
+ function ownedKeyNote(toolId, descriptor, params, storeKeyOwners) {
484
+ if (toolId !== "storage" || descriptor.effect === "read") return "";
485
+ if (!storeKeyOwners || typeof params.key !== "string") return "";
486
+ const storeName = storeKeyOwners[params.key];
487
+ if (!storeName) return "";
488
+ return `\n\n[Buoy] This wrote to disk, but "${params.key}" is the saved copy of the live "${storeName}" store, so nothing on screen has changed — the app read that key once at startup and has held the state in memory since. Call zustand.rehydrate({"storeName":"${storeName}"}) to make the app pick this up, or use zustand.setState next time to change the store directly. If you meant to set the value for the app's next launch, this is already done and no further action is needed.`;
489
+ }
490
+ /**
491
+ * One human-initiated action through EVERY gate the model's calls go through:
492
+ * catalog lookup, param validation, policy, pre-read, effect ledger.
493
+ *
494
+ * This exists for block buttons. The `buoy_ui` schema promises the model that
495
+ * a dispatch button runs "with the same policy and undo as if you called it",
496
+ * and the first implementation called `dispatchToolAction` raw — no deny list,
497
+ * no release refusal, no ledger entry. One tap on a model-authored button was
498
+ * an invisible, ungoverned, unundoable mutation. Route every button through
499
+ * here so the promise is true.
500
+ *
501
+ * `needs-approval` is treated as approved: the human's tap on the button IS
502
+ * the approval. Deny lists, read-only mode and release refusals still stand —
503
+ * a button must never reach further than the model could.
504
+ */
505
+ async function runGatedAction(input) {
506
+ const {
507
+ toolId,
508
+ action,
509
+ dispatch,
510
+ ledger,
511
+ policy,
512
+ isRelease
513
+ } = input;
514
+ const catalog = input.catalog ?? _catalog.CATALOG;
515
+ const descriptor = findDescriptor(catalog, toolId, action);
516
+ if (!descriptor) {
517
+ return {
518
+ ok: false,
519
+ error: `${toolId}.${action} is not in Buoy's catalog.`
520
+ };
521
+ }
522
+ const params = (0, _validateParams.applyDefaults)(input.params ?? {}, descriptor.params);
523
+ const check = (0, _validateParams.validateParams)(params, descriptor.params);
524
+ if (!check.ok) {
525
+ return {
526
+ ok: false,
527
+ error: check.errors.join("\n")
528
+ };
529
+ }
530
+ const verdict = (0, _policy.decide)({
531
+ descriptor,
532
+ toolId,
533
+ policy,
534
+ isRelease
535
+ });
536
+ if (verdict.verdict === "refuse") {
537
+ return {
538
+ ok: false,
539
+ error: verdict.reason
540
+ };
541
+ }
542
+ const before = await captureBefore(descriptor, toolId, params, dispatch);
543
+ try {
544
+ const result = toolId === "ask-buoy" ? await runAskBuoyAction(action, ledger, dispatch) : await dispatch(toolId, action, params);
545
+ if (descriptor.effect !== "read" && toolId !== "ask-buoy") {
546
+ ledger.noteExternalRevert(toolId, action, params);
547
+ const fx = (0, _effectFor.effectFor)(toolId, descriptor, params, result, before);
548
+ if (fx) ledger.record(toolId, action, fx);
549
+ }
550
+ return {
551
+ ok: true,
552
+ result
553
+ };
554
+ } catch (err) {
555
+ return {
556
+ ok: false,
557
+ error: err instanceof Error ? err.message : String(err)
558
+ };
559
+ }
560
+ }
561
+
562
+ /**
563
+ * The `ask-buoy` catalog tool, served by the engine itself. `listChanges` and
564
+ * `undoAll` operate on THIS session's ledger — the same one the changes bar
565
+ * renders — so the model's undo and the human's undo are one mechanism. The
566
+ * adapter's same-named actions remain for desktop/MCP, where the ledger is
567
+ * reached over the broker instead.
568
+ */
569
+ async function runAskBuoyAction(action, ledger, dispatch) {
570
+ if (action === "listChanges") {
571
+ const changes = ledger.list().filter(e => !e.undoneAt).map(e => ({
572
+ toolId: e.toolId,
573
+ action: e.action,
574
+ kind: e.effect.kind,
575
+ label: e.effect.label,
576
+ reversible: (0, _ledger.isReversible)(e.effect)
577
+ }));
578
+ return {
579
+ changes,
580
+ returned: changes.length
581
+ };
582
+ }
583
+ if (action === "undoAll") {
584
+ const outcome = await ledger.revertAll(dispatch);
585
+ return {
586
+ ok: outcome.failed.length === 0,
587
+ ...outcome
588
+ };
589
+ }
590
+ throw new Error(`Unknown ask-buoy action "${action}"`);
591
+ }
592
+
593
+ /** Fail loudly rather than hand the model an empty object it will believe. */
594
+ async function requireSnapshot(readSnapshot, toolId) {
595
+ if (!readSnapshot) {
596
+ throw new Error(`This app cannot read ${toolId}'s snapshot — Buoy's tool registry is not published here.`);
597
+ }
598
+ return await readSnapshot(toolId);
599
+ }
600
+
601
+ /**
602
+ * Read a storage key's current value so the write can be reversed.
603
+ *
604
+ * This pre-read is the ONLY reason storage is reversible here. The adapter's
605
+ * `async.*` / `mmkv.*` proxies all return void, so neither the prior value nor
606
+ * the store event id that `timeTravel.undo` needs ever reaches a caller — and
607
+ * that event only exists while something holds capture open. Reading first is
608
+ * what turns "we don't know what was there" into a real undo.
609
+ *
610
+ * Deliberately NOT gated on `descriptor.undo`: the catalog's undo claims were
611
+ * audited down to almost nothing precisely because they were unsound. The
612
+ * ledger's reversibility comes from this read, not from a catalog promise.
613
+ */
614
+ /**
615
+ * The prior state for a store write, so the receipt can show a real diff.
616
+ * Zustand only for now: `getStoreState` is cheap and the write params carry
617
+ * the after-state, so the diff is exact without the store's cooperation.
618
+ */
619
+ async function captureStateBefore(descriptor, toolId, params, dispatch) {
620
+ if (toolId !== "zustand" || descriptor.action !== "setState") return undefined;
621
+ if (typeof params.storeName !== "string") return undefined;
622
+ try {
623
+ return await dispatch(toolId, "getStoreState", {
624
+ storeName: params.storeName
625
+ });
626
+ } catch {
627
+ return undefined;
628
+ }
629
+ }
630
+ async function captureBefore(descriptor, toolId, params, dispatch) {
631
+ if (descriptor.effect === "read") return undefined;
632
+ if (toolId !== "storage" || typeof params.key !== "string") return undefined;
633
+ const readAction = descriptor.action.startsWith("async.") ? "async.getItem" : descriptor.action.startsWith("mmkv.") ? "mmkv.get" : undefined;
634
+ // secure.* is excluded on purpose: a key registered with
635
+ // `requireAuthentication` cannot be read without a biometric prompt, so a
636
+ // pre-read would either fail or pop a Face ID sheet mid-conversation.
637
+ if (!readAction) return undefined;
638
+
639
+ // MMKV IS PER-INSTANCE, and `mmkv.get` does not throw when the id is missing
640
+ // — it returns `{found:false, reason:"missing instanceId/key"}`. Reading with
641
+ // the key alone therefore produced a `before` that LOOKED captured and was
642
+ // empty, which the ledger then read as "the key did not exist" and undid by
643
+ // deleting it. Without an instance there is nothing to capture, so say so.
644
+ const readParams = {
645
+ key: params.key
646
+ };
647
+ if (readAction === "mmkv.get") {
648
+ if (typeof params.instanceId !== "string") return undefined;
649
+ readParams.instanceId = params.instanceId;
650
+ }
651
+ try {
652
+ const current = await dispatch(toolId, readAction, readParams);
653
+ const rec = current && typeof current === "object" ? current : undefined;
654
+ // An explicit "I could not read that" is not a captured empty value.
655
+ if (rec?.found === false) return undefined;
656
+ const value = rec ? rec.value : current;
657
+ return {
658
+ had: value !== undefined && value !== null,
659
+ value: value ?? undefined
660
+ };
661
+ } catch {
662
+ // A failed pre-read must not block the write the user asked for; it just
663
+ // means this one is not reversible, which the ledger will say.
664
+ return undefined;
665
+ }
666
+ }
667
+ //# sourceMappingURL=runAgentTurn.js.map