@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.
- package/LICENSE +58 -0
- package/README.md +42 -0
- package/lib/commonjs/blocks/receipts.js +702 -0
- package/lib/commonjs/blocks/receipts.js.map +1 -0
- package/lib/commonjs/blocks/types.js +119 -0
- package/lib/commonjs/blocks/types.js.map +1 -0
- package/lib/commonjs/blocks/uiTool.js +994 -0
- package/lib/commonjs/blocks/uiTool.js.map +1 -0
- package/lib/commonjs/catalog/catalog.g.js +4044 -0
- package/lib/commonjs/catalog/catalog.g.js.map +1 -0
- package/lib/commonjs/catalog/catalog.source.json +4956 -0
- package/lib/commonjs/catalog/snapshotReads.js +256 -0
- package/lib/commonjs/catalog/snapshotReads.js.map +1 -0
- package/lib/commonjs/catalog/toProviderTools.js +107 -0
- package/lib/commonjs/catalog/toProviderTools.js.map +1 -0
- package/lib/commonjs/catalog/validateParams.js +129 -0
- package/lib/commonjs/catalog/validateParams.js.map +1 -0
- package/lib/commonjs/context/buildContextPack.js +141 -0
- package/lib/commonjs/context/buildContextPack.js.map +1 -0
- package/lib/commonjs/effects/ledger.js +229 -0
- package/lib/commonjs/effects/ledger.js.map +1 -0
- package/lib/commonjs/engine/effectFor.js +95 -0
- package/lib/commonjs/engine/effectFor.js.map +1 -0
- package/lib/commonjs/engine/runAgentTurn.js +667 -0
- package/lib/commonjs/engine/runAgentTurn.js.map +1 -0
- package/lib/commonjs/engine/systemPrompt.js +86 -0
- package/lib/commonjs/engine/systemPrompt.js.map +1 -0
- package/lib/commonjs/index.js +288 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/policy/labels.js +303 -0
- package/lib/commonjs/policy/labels.js.map +1 -0
- package/lib/commonjs/policy/policy.js +143 -0
- package/lib/commonjs/policy/policy.js.map +1 -0
- package/lib/commonjs/policy/redact.js +135 -0
- package/lib/commonjs/policy/redact.js.map +1 -0
- package/lib/commonjs/providers/anthropic.js +272 -0
- package/lib/commonjs/providers/anthropic.js.map +1 -0
- package/lib/commonjs/providers/openai.js +193 -0
- package/lib/commonjs/providers/openai.js.map +1 -0
- package/lib/commonjs/providers/sse.js +104 -0
- package/lib/commonjs/providers/sse.js.map +1 -0
- package/lib/commonjs/providers/types.js +6 -0
- package/lib/commonjs/providers/types.js.map +1 -0
- package/lib/commonjs/session.js +173 -0
- package/lib/commonjs/session.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/blocks/receipts.js +696 -0
- package/lib/module/blocks/receipts.js.map +1 -0
- package/lib/module/blocks/types.js +113 -0
- package/lib/module/blocks/types.js.map +1 -0
- package/lib/module/blocks/uiTool.js +987 -0
- package/lib/module/blocks/uiTool.js.map +1 -0
- package/lib/module/catalog/catalog.g.js +4040 -0
- package/lib/module/catalog/catalog.g.js.map +1 -0
- package/lib/module/catalog/catalog.source.json +4956 -0
- package/lib/module/catalog/snapshotReads.js +252 -0
- package/lib/module/catalog/snapshotReads.js.map +1 -0
- package/lib/module/catalog/toProviderTools.js +101 -0
- package/lib/module/catalog/toProviderTools.js.map +1 -0
- package/lib/module/catalog/validateParams.js +124 -0
- package/lib/module/catalog/validateParams.js.map +1 -0
- package/lib/module/context/buildContextPack.js +138 -0
- package/lib/module/context/buildContextPack.js.map +1 -0
- package/lib/module/effects/ledger.js +223 -0
- package/lib/module/effects/ledger.js.map +1 -0
- package/lib/module/engine/effectFor.js +91 -0
- package/lib/module/engine/effectFor.js.map +1 -0
- package/lib/module/engine/runAgentTurn.js +663 -0
- package/lib/module/engine/runAgentTurn.js.map +1 -0
- package/lib/module/engine/systemPrompt.js +82 -0
- package/lib/module/engine/systemPrompt.js.map +1 -0
- package/lib/module/index.js +30 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/policy/labels.js +296 -0
- package/lib/module/policy/labels.js.map +1 -0
- package/lib/module/policy/policy.js +137 -0
- package/lib/module/policy/policy.js.map +1 -0
- package/lib/module/policy/redact.js +128 -0
- package/lib/module/policy/redact.js.map +1 -0
- package/lib/module/providers/anthropic.js +268 -0
- package/lib/module/providers/anthropic.js.map +1 -0
- package/lib/module/providers/openai.js +189 -0
- package/lib/module/providers/openai.js.map +1 -0
- package/lib/module/providers/sse.js +99 -0
- package/lib/module/providers/sse.js.map +1 -0
- package/lib/module/providers/types.js +4 -0
- package/lib/module/providers/types.js.map +1 -0
- package/lib/module/session.js +168 -0
- package/lib/module/session.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/blocks/receipts.d.ts +32 -0
- package/lib/typescript/blocks/receipts.d.ts.map +1 -0
- package/lib/typescript/blocks/types.d.ts +576 -0
- package/lib/typescript/blocks/types.d.ts.map +1 -0
- package/lib/typescript/blocks/uiTool.d.ts +15 -0
- package/lib/typescript/blocks/uiTool.d.ts.map +1 -0
- package/lib/typescript/catalog/catalog.g.d.ts +11 -0
- package/lib/typescript/catalog/catalog.g.d.ts.map +1 -0
- package/lib/typescript/catalog/snapshotReads.d.ts +37 -0
- package/lib/typescript/catalog/snapshotReads.d.ts.map +1 -0
- package/lib/typescript/catalog/toProviderTools.d.ts +52 -0
- package/lib/typescript/catalog/toProviderTools.d.ts.map +1 -0
- package/lib/typescript/catalog/validateParams.d.ts +33 -0
- package/lib/typescript/catalog/validateParams.d.ts.map +1 -0
- package/lib/typescript/context/buildContextPack.d.ts +32 -0
- package/lib/typescript/context/buildContextPack.d.ts.map +1 -0
- package/lib/typescript/effects/ledger.d.ts +119 -0
- package/lib/typescript/effects/ledger.d.ts.map +1 -0
- package/lib/typescript/engine/effectFor.d.ts +21 -0
- package/lib/typescript/engine/effectFor.d.ts.map +1 -0
- package/lib/typescript/engine/runAgentTurn.d.ts +140 -0
- package/lib/typescript/engine/runAgentTurn.d.ts.map +1 -0
- package/lib/typescript/engine/systemPrompt.d.ts +49 -0
- package/lib/typescript/engine/systemPrompt.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +29 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/policy/labels.d.ts +10 -0
- package/lib/typescript/policy/labels.d.ts.map +1 -0
- package/lib/typescript/policy/policy.d.ts +106 -0
- package/lib/typescript/policy/policy.d.ts.map +1 -0
- package/lib/typescript/policy/redact.d.ts +37 -0
- package/lib/typescript/policy/redact.d.ts.map +1 -0
- package/lib/typescript/providers/anthropic.d.ts +22 -0
- package/lib/typescript/providers/anthropic.d.ts.map +1 -0
- package/lib/typescript/providers/openai.d.ts +19 -0
- package/lib/typescript/providers/openai.d.ts.map +1 -0
- package/lib/typescript/providers/sse.d.ts +18 -0
- package/lib/typescript/providers/sse.d.ts.map +1 -0
- package/lib/typescript/providers/types.d.ts +129 -0
- package/lib/typescript/providers/types.d.ts.map +1 -0
- package/lib/typescript/session.d.ts +58 -0
- package/lib/typescript/session.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +119 -0
- package/lib/typescript/types.d.ts.map +1 -0
- 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
|