@vibes.diy/api-svc 14.1.21 → 14.1.22
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/create-handler.d.ts +2 -1
- package/create-handler.js +1 -0
- package/create-handler.js.map +1 -1
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/intern/agent-belt.js +15 -4
- package/intern/agent-belt.js.map +1 -1
- package/intern/agent-build-tools.js +5 -2
- package/intern/agent-build-tools.js.map +1 -1
- package/intern/agent-debug-tools.d.ts +33 -0
- package/intern/agent-debug-tools.js +676 -0
- package/intern/agent-debug-tools.js.map +1 -0
- package/intern/agent-file-tools.js +8 -2
- package/intern/agent-file-tools.js.map +1 -1
- package/intern/agent-instruction.d.ts +1 -1
- package/intern/agent-instruction.js +2 -2
- package/intern/agent-instruction.js.map +1 -1
- package/intern/agent-session-instruction.d.ts +2 -2
- package/intern/agent-session-instruction.js +15 -2
- package/intern/agent-session-instruction.js.map +1 -1
- package/intern/agent-session-runtime.d.ts +1 -0
- package/intern/agent-session-runtime.js +8 -3
- package/intern/agent-session-runtime.js.map +1 -1
- package/intern/attempt-backend-fetch.js +7 -1
- package/intern/attempt-backend-fetch.js.map +1 -1
- package/intern/backend-do-addr.d.ts +2 -0
- package/intern/backend-do-addr.js +2 -0
- package/intern/backend-do-addr.js.map +1 -1
- package/intern/backend-do-control-plane.d.ts +18 -0
- package/intern/backend-do-control-plane.js +90 -1
- package/intern/backend-do-control-plane.js.map +1 -1
- package/intern/mint-decision.d.ts +8 -0
- package/intern/mint-decision.js +15 -3
- package/intern/mint-decision.js.map +1 -1
- package/intern/resolve-backend-session-user.d.ts +8 -0
- package/intern/resolve-backend-session-user.js +10 -0
- package/intern/resolve-backend-session-user.js.map +1 -1
- package/package.json +11 -11
- package/public/app-documents-broadcast.d.ts +2 -0
- package/public/app-documents-broadcast.js +7 -0
- package/public/app-documents-broadcast.js.map +1 -1
- package/public/app-documents-read-eventos.d.ts +52 -1
- package/public/app-documents-read-eventos.js +250 -222
- package/public/app-documents-read-eventos.js.map +1 -1
- package/public/app-documents-write-eventos.d.ts +42 -0
- package/public/app-documents-write-eventos.js +334 -334
- package/public/app-documents-write-eventos.js.map +1 -1
- package/public/app-logs.d.ts +14 -2
- package/public/app-logs.js +60 -45
- package/public/app-logs.js.map +1 -1
- package/public/app-setting-items.d.ts +4 -0
- package/public/app-setting-items.js +6 -3
- package/public/app-setting-items.js.map +1 -1
- package/public/app-settings-sql-upsert.d.ts +4 -0
- package/public/app-settings-sql-upsert.js +1 -1
- package/public/app-settings-sql-upsert.js.map +1 -1
- package/public/prompt-chat-section.js +1 -0
- package/public/prompt-chat-section.js.map +1 -1
- package/types.d.ts +2 -1
- package/types.js.map +1 -1
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { Result, exception2Result } from "@adviser/cement";
|
|
2
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { getSkillText } from "@vibes.diy/prompts";
|
|
4
|
+
import { isActiveDeferredIntents, isActivePosture } from "@vibes.diy/api-types";
|
|
5
|
+
import { readComposedSettings, readComposedSettingsForApp, readComposedSettingsOwnerForApp, writeAppSettingItems, } from "../public/app-setting-items.js";
|
|
6
|
+
import { postAppSettingEvent } from "../public/app-settings-sql-upsert.js";
|
|
7
|
+
import { remoteDocChangedFanOut } from "../public/app-documents-broadcast.js";
|
|
8
|
+
import { readAppLogsPage } from "../public/app-logs.js";
|
|
9
|
+
import { authorizeSlugOwner } from "../public/authorize-slug-owner.js";
|
|
10
|
+
import { getBindingFlags } from "../public/unpublished-binding.js";
|
|
11
|
+
import { getDocForCaller, queryDocsForCaller, listDbNamesForCaller } from "../public/app-documents-read-eventos.js";
|
|
12
|
+
import { putDocForCaller, deleteDocForCaller } from "../public/app-documents-write-eventos.js";
|
|
13
|
+
import { loadSelectedBackend } from "./load-selected-backend.js";
|
|
14
|
+
import { loadVersionItems } from "./build-version-ordinal.js";
|
|
15
|
+
import { loadVersionTimeline } from "./version-timeline.js";
|
|
16
|
+
import { skillGuideIndex } from "./agent-file-tools.js";
|
|
17
|
+
import { WORK_TOOL_RESULT_MAX_CHARS } from "./work-tools.js";
|
|
18
|
+
import { MAX_TOOL_RESULT_CHARS } from "./llm-tool-loop.js";
|
|
19
|
+
import { resolveChatApp } from "./agent-actor.js";
|
|
20
|
+
export const READ_APP_LOGS_TOOL_NAME = "read_app_logs";
|
|
21
|
+
export const READ_RELEASE_STATE_TOOL_NAME = "read_release_state";
|
|
22
|
+
export const READ_SCHEDULE_STATUS_TOOL_NAME = "read_schedule_status";
|
|
23
|
+
export const LIST_DATABASES_TOOL_NAME = "list_databases";
|
|
24
|
+
export const QUERY_DOCS_TOOL_NAME = "query_docs";
|
|
25
|
+
export const GET_DOC_TOOL_NAME = "get_doc";
|
|
26
|
+
export const PUT_DOC_TOOL_NAME = "put_doc";
|
|
27
|
+
export const DELETE_DOC_TOOL_NAME = "delete_doc";
|
|
28
|
+
export const RETRACT_DEFERRED_INTENT_TOOL_NAME = "retract_deferred_intent";
|
|
29
|
+
export const CALL_OWN_API_TOOL_NAME = "call_own_api";
|
|
30
|
+
const DEBUGGING_SKILL_NAME = "debugging";
|
|
31
|
+
export const AGENT_DEBUG_TOOL_NAMES = new Set([
|
|
32
|
+
READ_APP_LOGS_TOOL_NAME,
|
|
33
|
+
READ_RELEASE_STATE_TOOL_NAME,
|
|
34
|
+
READ_SCHEDULE_STATUS_TOOL_NAME,
|
|
35
|
+
LIST_DATABASES_TOOL_NAME,
|
|
36
|
+
QUERY_DOCS_TOOL_NAME,
|
|
37
|
+
GET_DOC_TOOL_NAME,
|
|
38
|
+
PUT_DOC_TOOL_NAME,
|
|
39
|
+
DELETE_DOC_TOOL_NAME,
|
|
40
|
+
RETRACT_DEFERRED_INTENT_TOOL_NAME,
|
|
41
|
+
CALL_OWN_API_TOOL_NAME,
|
|
42
|
+
]);
|
|
43
|
+
const MAX_SINCE_MINUTES = 1440;
|
|
44
|
+
const DEFAULT_SINCE_MINUTES = 60;
|
|
45
|
+
const LOG_PAGE_LIMIT = 40;
|
|
46
|
+
const LOG_BODY_CHARS = 400;
|
|
47
|
+
const LOG_BODY_MARKER = "…";
|
|
48
|
+
const LOG_LEVELS = new Set(["debug", "info", "warn", "error"]);
|
|
49
|
+
const DOC_PAGE_DEFAULT_LIMIT = 10;
|
|
50
|
+
const DOC_PAGE_MAX_LIMIT = 20;
|
|
51
|
+
const DOC_BODY_CHARS = 1024;
|
|
52
|
+
const OWN_API_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
53
|
+
export function createAgentDebugTools(vctx, args) {
|
|
54
|
+
const scopeOwner = args.actor.scope?.ownerHandle ?? args.actor.ownerHandle;
|
|
55
|
+
const scopeSlug = args.actor.scope?.appSlug;
|
|
56
|
+
const definitions = [
|
|
57
|
+
{
|
|
58
|
+
type: "function",
|
|
59
|
+
function: {
|
|
60
|
+
name: READ_APP_LOGS_TOOL_NAME,
|
|
61
|
+
description: "Read what the deployed app has been recording about itself — the lines its server code and its page wrote " +
|
|
62
|
+
"with the platform's own log call. Read this when the person says something is not working, or after a " +
|
|
63
|
+
"change that added server work, so what you tell them about the live app comes from the app rather than " +
|
|
64
|
+
"from the code you expect it to be running. Newest window first; page on with the cursor it returns.",
|
|
65
|
+
parameters: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
level: {
|
|
69
|
+
type: "string",
|
|
70
|
+
enum: ["debug", "info", "warn", "error"],
|
|
71
|
+
description: "Keep only lines at this level. Omit for every level.",
|
|
72
|
+
},
|
|
73
|
+
sinceMinutes: {
|
|
74
|
+
type: "number",
|
|
75
|
+
description: "How far back to look, in minutes. Defaults to the last hour; the widest window is a day.",
|
|
76
|
+
},
|
|
77
|
+
cursor: { type: "string", description: "Continue from a previous call's nextCursor." },
|
|
78
|
+
},
|
|
79
|
+
required: [],
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "function",
|
|
86
|
+
function: {
|
|
87
|
+
name: READ_RELEASE_STATE_TOOL_NAME,
|
|
88
|
+
description: "Read which version of this app is actually live: what is published, whether newer work is waiting behind " +
|
|
89
|
+
"it, whether the owner pinned a version or took the app down, who the app lets in, and whether the live " +
|
|
90
|
+
"version carries its record rules and its server file. Read this before telling the person what is live, " +
|
|
91
|
+
"and before saying a change they asked for is already in the app.",
|
|
92
|
+
parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: "function",
|
|
97
|
+
function: {
|
|
98
|
+
name: READ_SCHEDULE_STATUS_TOOL_NAME,
|
|
99
|
+
description: "Read whether this app's scheduled server work is running: when it last ran, when it is next expected, and " +
|
|
100
|
+
"the error it last reported. Read this when the person expects something to happen on its own and says it " +
|
|
101
|
+
"has not.",
|
|
102
|
+
parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "function",
|
|
107
|
+
function: {
|
|
108
|
+
name: LIST_DATABASES_TOOL_NAME,
|
|
109
|
+
description: "List the record collections this app has actually written to. Read this before looking at records, so you " +
|
|
110
|
+
"work from the collections that exist rather than the ones the code implies.",
|
|
111
|
+
parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
type: "function",
|
|
116
|
+
function: {
|
|
117
|
+
name: QUERY_DOCS_TOOL_NAME,
|
|
118
|
+
description: "Read a page of this app's records, exactly as the person whose app this is would see them in the running " +
|
|
119
|
+
"app. Read this when what the person describes and what the code does disagree — the records say which one " +
|
|
120
|
+
"the data matches. Pages are small; continue with the cursor it returns, and keep going while it returns " +
|
|
121
|
+
"one, because a page that filtered everything out can still have more behind it.",
|
|
122
|
+
parameters: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
dbName: { type: "string", description: "The collection to read, from list_databases." },
|
|
126
|
+
limit: { type: "number", description: "Records per page, up to 20. Defaults to 10." },
|
|
127
|
+
after: { type: "string", description: "Continue from a previous call's next." },
|
|
128
|
+
field: { type: "string", description: "Narrow to records indexed on this field." },
|
|
129
|
+
key: { description: "With field, keep only records whose field equals this value." },
|
|
130
|
+
prefix: { description: "With field, keep only records whose field starts with this value." },
|
|
131
|
+
},
|
|
132
|
+
required: ["dbName"],
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
type: "function",
|
|
139
|
+
function: {
|
|
140
|
+
name: GET_DOC_TOOL_NAME,
|
|
141
|
+
description: "Read one record by its id, as the person whose app this is would see it. Read the record before saying " +
|
|
142
|
+
"anything about what it contains.",
|
|
143
|
+
parameters: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
dbName: { type: "string", description: "The collection the record is in." },
|
|
147
|
+
docId: { type: "string", description: "The record's id." },
|
|
148
|
+
},
|
|
149
|
+
required: ["dbName", "docId"],
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
type: "function",
|
|
156
|
+
function: {
|
|
157
|
+
name: PUT_DOC_TOOL_NAME,
|
|
158
|
+
description: "Save one record, as the person whose app this is. Include the record's id as _id to change a record that " +
|
|
159
|
+
"already exists, and leave it out to add a new one. The app's own record rules decide whether the save " +
|
|
160
|
+
"lands, so a refusal here is the app telling you what it allows. For more than a handful of records, add a " +
|
|
161
|
+
"route to the app's server file and run it once instead — read /skills/debugging.md for how.",
|
|
162
|
+
parameters: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
dbName: { type: "string", description: "The collection to save into." },
|
|
166
|
+
doc: { type: "object", description: "The whole record. Its _id, when present, names the record to change." },
|
|
167
|
+
},
|
|
168
|
+
required: ["dbName", "doc"],
|
|
169
|
+
additionalProperties: false,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: "function",
|
|
175
|
+
function: {
|
|
176
|
+
name: DELETE_DOC_TOOL_NAME,
|
|
177
|
+
description: "Remove one record, as the person whose app this is. The app's own record rules decide whether the removal " +
|
|
178
|
+
"lands. Recipients who already hold a copy keep it; this stops it being served from here on.",
|
|
179
|
+
parameters: {
|
|
180
|
+
type: "object",
|
|
181
|
+
properties: {
|
|
182
|
+
dbName: { type: "string", description: "The collection the record is in." },
|
|
183
|
+
docId: { type: "string", description: "The record's id." },
|
|
184
|
+
},
|
|
185
|
+
required: ["dbName", "docId"],
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "function",
|
|
192
|
+
function: {
|
|
193
|
+
name: CALL_OWN_API_TOOL_NAME,
|
|
194
|
+
description: "Call one of this app's own server routes as the person whose app this is. The route runs the live " +
|
|
195
|
+
"release's backend.js fetch handler with the owner's identity, so a route written for bulk record work, a " +
|
|
196
|
+
"health check or a data export answers here the way it answers the owner's curl. Read the release state " +
|
|
197
|
+
"first when unsure which routes the live version carries. Answers with the route's status, its content " +
|
|
198
|
+
"type and its body text, marked when the body was longer than this result holds.",
|
|
199
|
+
parameters: {
|
|
200
|
+
type: "object",
|
|
201
|
+
properties: {
|
|
202
|
+
method: {
|
|
203
|
+
type: "string",
|
|
204
|
+
enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
205
|
+
description: "The HTTP method the route expects.",
|
|
206
|
+
},
|
|
207
|
+
path: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "The part of the route after /_api/, with no leading slash. A query string is fine.",
|
|
210
|
+
},
|
|
211
|
+
body: { type: "string", description: "The request body, for a method that carries one." },
|
|
212
|
+
contentType: { type: "string", description: "The body's content type. Defaults to application/json." },
|
|
213
|
+
},
|
|
214
|
+
required: ["method", "path"],
|
|
215
|
+
additionalProperties: false,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: "function",
|
|
221
|
+
function: {
|
|
222
|
+
name: RETRACT_DEFERRED_INTENT_TOOL_NAME,
|
|
223
|
+
description: "Take back a capability this app said it would add later, once it is either built or no longer wanted. This " +
|
|
224
|
+
"is the same act as the owner dismissing the offer themselves, so use it when the work is done or the " +
|
|
225
|
+
"person says they do not want it.",
|
|
226
|
+
parameters: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
kind: {
|
|
230
|
+
type: "string",
|
|
231
|
+
enum: ["access", "backend"],
|
|
232
|
+
description: "Which offer to take back: record rules, or server work.",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: ["kind"],
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
async function appInScope() {
|
|
242
|
+
if (args.chatId !== undefined) {
|
|
243
|
+
const rApp = await resolveChatApp(vctx, args.actor, args.chatId);
|
|
244
|
+
if (rApp.isErr())
|
|
245
|
+
return { refusal: JSON.stringify(rApp.Err().refusal) };
|
|
246
|
+
return { app: rApp.Ok() };
|
|
247
|
+
}
|
|
248
|
+
if (scopeSlug !== undefined)
|
|
249
|
+
return { app: { ownerHandle: scopeOwner, appSlug: scopeSlug } };
|
|
250
|
+
return {
|
|
251
|
+
refusal: JSON.stringify({
|
|
252
|
+
ok: false,
|
|
253
|
+
reason: "no-app-in-scope",
|
|
254
|
+
message: "This conversation is not about one specific app yet.",
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
let guideIntroduced = false;
|
|
259
|
+
async function withGuide(payload) {
|
|
260
|
+
if (guideIntroduced)
|
|
261
|
+
return toolJson(payload);
|
|
262
|
+
const loaded = await exception2Result(() => (args.loadSkill ?? getSkillText)(DEBUGGING_SKILL_NAME));
|
|
263
|
+
if (loaded.isErr())
|
|
264
|
+
return toolJson(payload);
|
|
265
|
+
guideIntroduced = true;
|
|
266
|
+
return toolJson({ ...payload, guide: skillGuideIndex(DEBUGGING_SKILL_NAME, loaded.Ok()) });
|
|
267
|
+
}
|
|
268
|
+
const failedWithGuide = (message) => withGuide({ ok: false, reason: "failed", message });
|
|
269
|
+
const deniedWithGuide = (message) => withGuide({ ok: false, reason: "not-owner", message });
|
|
270
|
+
const invalidWithGuide = (message) => withGuide({ ok: false, reason: "invalid-input", message });
|
|
271
|
+
const forbiddenWithGuide = (explanation) => withGuide({ ok: false, reason: "forbidden", explanation });
|
|
272
|
+
const executor = async (input) => {
|
|
273
|
+
const scoped = await appInScope();
|
|
274
|
+
if (scoped.refusal !== undefined)
|
|
275
|
+
return Result.Ok(scoped.refusal);
|
|
276
|
+
const app = scoped.app;
|
|
277
|
+
if (input.call.name === READ_APP_LOGS_TOOL_NAME) {
|
|
278
|
+
const auth = await exception2Result(() => authorizeSlugOwner(vctx, { ownerHandle: app.ownerHandle, appSlug: app.appSlug, userId: args.actor.userId }));
|
|
279
|
+
if (auth.isErr())
|
|
280
|
+
return failedWithGuide("Could not read this app's logs just now.");
|
|
281
|
+
if (auth.Ok()?.isOwner !== true) {
|
|
282
|
+
return deniedWithGuide("Only this app's owner can read its logs.");
|
|
283
|
+
}
|
|
284
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
285
|
+
const sinceMinutes = clampSinceMinutes(parsedArgs.sinceMinutes);
|
|
286
|
+
const level = typeof parsedArgs.level === "string" && LOG_LEVELS.has(parsedArgs.level) ? parsedArgs.level : undefined;
|
|
287
|
+
const cursor = typeof parsedArgs.cursor === "string" && parsedArgs.cursor !== "" ? parsedArgs.cursor : undefined;
|
|
288
|
+
const rPage = await readAppLogsPage(vctx, {
|
|
289
|
+
ownerHandle: app.ownerHandle,
|
|
290
|
+
appSlug: app.appSlug,
|
|
291
|
+
sinceTs: new Date(Date.now() - sinceMinutes * 60_000).toISOString(),
|
|
292
|
+
...(cursor === undefined ? {} : { sinceId: cursor }),
|
|
293
|
+
...(level === undefined ? {} : { level }),
|
|
294
|
+
limit: LOG_PAGE_LIMIT,
|
|
295
|
+
});
|
|
296
|
+
if (rPage.isErr())
|
|
297
|
+
return failedWithGuide("Could not read this app's logs just now.");
|
|
298
|
+
const page = rPage.Ok();
|
|
299
|
+
return Result.Ok(boundedLogEventsJson({ ok: true, sinceMinutes }, page.events.map((e) => ({
|
|
300
|
+
id: e.id,
|
|
301
|
+
ts: e.ts,
|
|
302
|
+
lane: e.lane,
|
|
303
|
+
level: e.level,
|
|
304
|
+
trigger: e.trigger,
|
|
305
|
+
fsId: e.fsId,
|
|
306
|
+
body: truncateBody(e.body),
|
|
307
|
+
})), page.nextCursor));
|
|
308
|
+
}
|
|
309
|
+
if (input.call.name === READ_RELEASE_STATE_TOOL_NAME) {
|
|
310
|
+
const r = await exception2Result(() => readReleaseState(vctx, app, args.chatId));
|
|
311
|
+
if (r.isErr() || r.Ok().ok === false)
|
|
312
|
+
return failedWithGuide("Could not read this app's release state just now.");
|
|
313
|
+
return toolJson({ ok: true, ...r.Ok().state });
|
|
314
|
+
}
|
|
315
|
+
if (input.call.name === READ_SCHEDULE_STATUS_TOOL_NAME) {
|
|
316
|
+
const cp = vctx.backendControlPlane;
|
|
317
|
+
if (cp === undefined)
|
|
318
|
+
return failedWithGuide("Could not read this app's schedule just now.");
|
|
319
|
+
const rStatus = await exception2Result(() => cp.tickStatus(app.ownerHandle, app.appSlug));
|
|
320
|
+
if (rStatus.isErr())
|
|
321
|
+
return failedWithGuide("Could not read this app's schedule just now.");
|
|
322
|
+
return toolJson({ ok: true, ...rStatus.Ok() });
|
|
323
|
+
}
|
|
324
|
+
const caller = {
|
|
325
|
+
userId: args.actor.userId,
|
|
326
|
+
ownerHandle: app.ownerHandle,
|
|
327
|
+
appSlug: app.appSlug,
|
|
328
|
+
adminMode: false,
|
|
329
|
+
actingHandle: args.actor.ownerHandle,
|
|
330
|
+
fanOutDocChanged: remoteDocChangedFanOut,
|
|
331
|
+
};
|
|
332
|
+
if (input.call.name === LIST_DATABASES_TOOL_NAME) {
|
|
333
|
+
const r = await exception2Result(() => listDbNamesForCaller(vctx, {
|
|
334
|
+
userId: args.actor.userId,
|
|
335
|
+
ownerHandle: app.ownerHandle,
|
|
336
|
+
appSlug: app.appSlug,
|
|
337
|
+
adminMode: false,
|
|
338
|
+
}));
|
|
339
|
+
if (r.isErr())
|
|
340
|
+
return failedWithGuide("Could not read this app's record collections just now.");
|
|
341
|
+
const answer = r.Ok();
|
|
342
|
+
if (answer.kind === "denied")
|
|
343
|
+
return deniedWithGuide(answer.message);
|
|
344
|
+
return toolJson({ ok: true, dbNames: answer.dbNames });
|
|
345
|
+
}
|
|
346
|
+
if (input.call.name === QUERY_DOCS_TOOL_NAME) {
|
|
347
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
348
|
+
const dbName = stringArg(parsedArgs.dbName);
|
|
349
|
+
if (dbName === undefined)
|
|
350
|
+
return invalidWithGuide("dbName is required.");
|
|
351
|
+
const field = stringArg(parsedArgs.field);
|
|
352
|
+
const r = await exception2Result(() => queryDocsForCaller(vctx, {
|
|
353
|
+
...caller,
|
|
354
|
+
dbName,
|
|
355
|
+
limit: clampDocLimit(parsedArgs.limit),
|
|
356
|
+
...(stringArg(parsedArgs.after) === undefined ? {} : { after: stringArg(parsedArgs.after) }),
|
|
357
|
+
...(field === undefined
|
|
358
|
+
? {}
|
|
359
|
+
: {
|
|
360
|
+
filter: {
|
|
361
|
+
field,
|
|
362
|
+
...(parsedArgs.key === undefined ? {} : { key: parsedArgs.key }),
|
|
363
|
+
...(parsedArgs.prefix === undefined ? {} : { prefix: parsedArgs.prefix }),
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
}));
|
|
367
|
+
if (r.isErr())
|
|
368
|
+
return failedWithGuide("Could not read this app's records just now.");
|
|
369
|
+
const answer = r.Ok();
|
|
370
|
+
if (answer.kind === "denied")
|
|
371
|
+
return deniedWithGuide(answer.message);
|
|
372
|
+
return Result.Ok(boundedDocsJson({ ok: true, dbName, ...(answer.next === undefined ? {} : { next: answer.next }) }, answer.docs.map(truncateDoc)));
|
|
373
|
+
}
|
|
374
|
+
if (input.call.name === GET_DOC_TOOL_NAME) {
|
|
375
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
376
|
+
const dbName = stringArg(parsedArgs.dbName);
|
|
377
|
+
const docId = stringArg(parsedArgs.docId);
|
|
378
|
+
if (dbName === undefined || docId === undefined)
|
|
379
|
+
return invalidWithGuide("dbName and docId are required.");
|
|
380
|
+
const r = await exception2Result(() => getDocForCaller(vctx, { ...caller, dbName, docId }));
|
|
381
|
+
if (r.isErr())
|
|
382
|
+
return failedWithGuide("Could not read that record just now.");
|
|
383
|
+
const answer = r.Ok();
|
|
384
|
+
if (answer.kind === "denied")
|
|
385
|
+
return deniedWithGuide(answer.message);
|
|
386
|
+
if (answer.kind === "not-found")
|
|
387
|
+
return toolJson({ ok: true, dbName, docId, found: false });
|
|
388
|
+
return Result.Ok(boundedDocsJson({ ok: true, dbName, docId, found: true }, [truncateDoc({ _id: answer.docId, ...answer.doc })]));
|
|
389
|
+
}
|
|
390
|
+
if (input.call.name === PUT_DOC_TOOL_NAME) {
|
|
391
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
392
|
+
const dbName = stringArg(parsedArgs.dbName);
|
|
393
|
+
const doc = parsedArgs.doc;
|
|
394
|
+
if (dbName === undefined || typeof doc !== "object" || doc === null || Array.isArray(doc))
|
|
395
|
+
return invalidWithGuide("dbName and a doc object are required.");
|
|
396
|
+
const body = doc;
|
|
397
|
+
const bodyId = stringArg(body._id);
|
|
398
|
+
const r = await exception2Result(() => putDocForCaller(vctx, { ...caller, dbName, doc: body, ...(bodyId === undefined ? {} : { docId: bodyId }) }));
|
|
399
|
+
if (r.isErr())
|
|
400
|
+
return failedWithGuide("Could not save that record just now.");
|
|
401
|
+
const answer = r.Ok();
|
|
402
|
+
if (answer.kind === "denied")
|
|
403
|
+
return forbiddenWithGuide(answer.message);
|
|
404
|
+
return toolJson({ ok: true, dbName, docId: answer.docId });
|
|
405
|
+
}
|
|
406
|
+
if (input.call.name === DELETE_DOC_TOOL_NAME) {
|
|
407
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
408
|
+
const dbName = stringArg(parsedArgs.dbName);
|
|
409
|
+
const docId = stringArg(parsedArgs.docId);
|
|
410
|
+
if (dbName === undefined || docId === undefined)
|
|
411
|
+
return invalidWithGuide("dbName and docId are required.");
|
|
412
|
+
const r = await exception2Result(() => deleteDocForCaller(vctx, { ...caller, dbName, docId }));
|
|
413
|
+
if (r.isErr())
|
|
414
|
+
return failedWithGuide("Could not remove that record just now.");
|
|
415
|
+
const answer = r.Ok();
|
|
416
|
+
if (answer.kind === "denied")
|
|
417
|
+
return forbiddenWithGuide(answer.message);
|
|
418
|
+
return toolJson({ ok: true, dbName, docId: answer.docId });
|
|
419
|
+
}
|
|
420
|
+
if (input.call.name === CALL_OWN_API_TOOL_NAME) {
|
|
421
|
+
const auth = await exception2Result(() => authorizeSlugOwner(vctx, { ownerHandle: app.ownerHandle, appSlug: app.appSlug, userId: args.actor.userId }));
|
|
422
|
+
if (auth.isErr())
|
|
423
|
+
return failedWithGuide("Could not reach this app's server routes just now.");
|
|
424
|
+
if (auth.Ok()?.isOwner !== true) {
|
|
425
|
+
return deniedWithGuide("Only this app's owner can run its server routes from here.");
|
|
426
|
+
}
|
|
427
|
+
const call = vctx.backendOwnerApiFetch;
|
|
428
|
+
if (call === undefined) {
|
|
429
|
+
return failedWithGuide("Running this app's server routes is not available in this environment.");
|
|
430
|
+
}
|
|
431
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
432
|
+
const method = stringArg(parsedArgs.method)?.toUpperCase();
|
|
433
|
+
if (method === undefined || !OWN_API_METHODS.has(method)) {
|
|
434
|
+
return invalidWithGuide("method must be one of GET, POST, PUT, PATCH or DELETE.");
|
|
435
|
+
}
|
|
436
|
+
const path = stringArg(parsedArgs.path);
|
|
437
|
+
if (path === undefined)
|
|
438
|
+
return invalidWithGuide("path is required.");
|
|
439
|
+
if (path.startsWith("/"))
|
|
440
|
+
return invalidWithGuide("path is the part after /_api/, so it starts without a slash.");
|
|
441
|
+
if (path.includes("//") || path.includes("..") || path.includes("#") || path.includes("\\")) {
|
|
442
|
+
return invalidWithGuide("path names one route on this app: no doubled slashes, dot segments or fragments.");
|
|
443
|
+
}
|
|
444
|
+
if (!ownApiPathIsLiteral(path)) {
|
|
445
|
+
return invalidWithGuide("path must name the route literally: no percent-encoded or escaped path segments.");
|
|
446
|
+
}
|
|
447
|
+
const body = stringArg(parsedArgs.body);
|
|
448
|
+
if (body !== undefined && (method === "GET" || method === "HEAD")) {
|
|
449
|
+
return invalidWithGuide(`A ${method} carries no body; put what the route needs in the path's query string.`);
|
|
450
|
+
}
|
|
451
|
+
const r = await exception2Result(() => call({
|
|
452
|
+
ownerHandle: app.ownerHandle,
|
|
453
|
+
appSlug: app.appSlug,
|
|
454
|
+
userId: args.actor.userId,
|
|
455
|
+
method,
|
|
456
|
+
path,
|
|
457
|
+
...(body === undefined ? {} : { body, contentType: stringArg(parsedArgs.contentType) ?? "application/json" }),
|
|
458
|
+
}));
|
|
459
|
+
if (r.isErr())
|
|
460
|
+
return failedWithGuide("Could not reach this app's server routes just now.");
|
|
461
|
+
const answer = r.Ok();
|
|
462
|
+
return toolJson(fitOwnApiToolResult({
|
|
463
|
+
method,
|
|
464
|
+
path,
|
|
465
|
+
status: answer.status,
|
|
466
|
+
contentType: answer.contentType,
|
|
467
|
+
bodyText: answer.bodyText,
|
|
468
|
+
truncated: answer.truncated,
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
if (input.call.name === RETRACT_DEFERRED_INTENT_TOOL_NAME) {
|
|
472
|
+
const parsedArgs = parseArgs(input.call.arguments);
|
|
473
|
+
const kind = parsedArgs.kind;
|
|
474
|
+
if (kind !== "access" && kind !== "backend")
|
|
475
|
+
return invalidWithGuide('kind must be "access" or "backend".');
|
|
476
|
+
const r = await exception2Result(() => retractDeferredIntent(vctx, app, kind));
|
|
477
|
+
if (r.isErr())
|
|
478
|
+
return failedWithGuide("Could not take that offer back just now.");
|
|
479
|
+
return toolJson({ ok: true, kind, retracted: r.Ok() });
|
|
480
|
+
}
|
|
481
|
+
return toolJson({ ok: false, reason: "invalid-input", message: `Unknown tool ${input.call.name}.` });
|
|
482
|
+
};
|
|
483
|
+
return { definitions, executor };
|
|
484
|
+
}
|
|
485
|
+
async function readReleaseState(vctx, app, chatId) {
|
|
486
|
+
const [listing, flags, entries] = await Promise.all([
|
|
487
|
+
loadVersionItems(vctx, app),
|
|
488
|
+
getBindingFlags(vctx, app),
|
|
489
|
+
readComposedSettingsForApp(vctx, app.ownerHandle, app.appSlug),
|
|
490
|
+
]);
|
|
491
|
+
const published = listing.items.find((i) => i.published);
|
|
492
|
+
let headFsId;
|
|
493
|
+
if (chatId !== undefined) {
|
|
494
|
+
const rTimeline = await loadVersionTimeline(vctx, chatId);
|
|
495
|
+
if (rTimeline.isErr())
|
|
496
|
+
return { ok: false };
|
|
497
|
+
const timeline = rTimeline.Ok();
|
|
498
|
+
headFsId = timeline.length > 0 ? timeline[timeline.length - 1].fsId : undefined;
|
|
499
|
+
}
|
|
500
|
+
const tApps = vctx.sql.tables.apps;
|
|
501
|
+
const publishedRow = published === undefined
|
|
502
|
+
? undefined
|
|
503
|
+
: await vctx.sql.db
|
|
504
|
+
.select({ fileSystem: tApps.fileSystem })
|
|
505
|
+
.from(tApps)
|
|
506
|
+
.where(and(eq(tApps.ownerHandle, app.ownerHandle), eq(tApps.appSlug, app.appSlug), eq(tApps.fsId, published.fsId)))
|
|
507
|
+
.orderBy(sql `${tApps.releaseSeq} desc`)
|
|
508
|
+
.limit(1)
|
|
509
|
+
.then((r) => r[0]);
|
|
510
|
+
const files = (publishedRow?.fileSystem ?? []);
|
|
511
|
+
const has = (name) => files.some((f) => f.fileName === name);
|
|
512
|
+
const backendJsInRelease = has("/backend.js");
|
|
513
|
+
const loaded = backendJsInRelease ? await loadSelectedBackend(vctx, app.ownerHandle, app.appSlug) : undefined;
|
|
514
|
+
const backendHandlersUnreadable = backendJsInRelease && loaded?.ok !== true;
|
|
515
|
+
const posture = entries.find(isActivePosture)?.posture;
|
|
516
|
+
const deferred = entries.find(isActiveDeferredIntents);
|
|
517
|
+
return {
|
|
518
|
+
ok: true,
|
|
519
|
+
state: {
|
|
520
|
+
...(published === undefined ? {} : { publishedFsId: published.fsId, publishedAt: published.created }),
|
|
521
|
+
...(flags.lockedFsId === "" ? {} : { lockedFsId: flags.lockedFsId }),
|
|
522
|
+
unpublished: flags.unpublishedAt !== "",
|
|
523
|
+
...(headFsId === undefined ? {} : { headFsId }),
|
|
524
|
+
draftAhead: headFsId !== undefined && headFsId !== published?.fsId,
|
|
525
|
+
...(posture === undefined ? {} : { posture }),
|
|
526
|
+
accessJsInRelease: has("/access.js"),
|
|
527
|
+
backendJsInRelease,
|
|
528
|
+
backendHandlers: backendHandlersUnreadable ? null : loaded !== undefined && loaded.ok ? [...loaded.parsed.handlers] : [],
|
|
529
|
+
...(backendHandlersUnreadable ? { backendHandlersUnreadable: true } : {}),
|
|
530
|
+
...(deferred === undefined
|
|
531
|
+
? {}
|
|
532
|
+
: {
|
|
533
|
+
deferred: {
|
|
534
|
+
...(deferred.access === undefined ? {} : { access: deferred.access }),
|
|
535
|
+
...(deferred.backend === undefined ? {} : { backend: deferred.backend }),
|
|
536
|
+
},
|
|
537
|
+
}),
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
async function retractDeferredIntent(vctx, app, kind) {
|
|
542
|
+
const { userId, entries } = await readComposedSettingsOwnerForApp(vctx, app.ownerHandle, app.appSlug);
|
|
543
|
+
const current = entries.find(isActiveDeferredIntents);
|
|
544
|
+
if (userId === undefined || current === undefined || current[kind] === undefined)
|
|
545
|
+
return false;
|
|
546
|
+
const kept = {
|
|
547
|
+
type: "active.deferred-intents",
|
|
548
|
+
...(kind === "access" || current.access === undefined ? {} : { access: current.access }),
|
|
549
|
+
...(kind === "backend" || current.backend === undefined ? {} : { backend: current.backend }),
|
|
550
|
+
};
|
|
551
|
+
const key = { userId, ownerHandle: app.ownerHandle, appSlug: app.appSlug };
|
|
552
|
+
const empty = kept.access === undefined && kept.backend === undefined;
|
|
553
|
+
await writeAppSettingItems(vctx, key, empty ? { removeKeys: ["active.deferred-intents"] } : { upserts: [kept] });
|
|
554
|
+
await postAppSettingEvent(vctx, app, await readComposedSettings(vctx, key));
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
function boundedDocsJson(envelope, docs) {
|
|
558
|
+
let kept = docs.length;
|
|
559
|
+
for (;;) {
|
|
560
|
+
const omitted = docs.length - kept;
|
|
561
|
+
const json = JSON.stringify({
|
|
562
|
+
...envelope,
|
|
563
|
+
returned: kept,
|
|
564
|
+
...(omitted === 0
|
|
565
|
+
? {}
|
|
566
|
+
: {
|
|
567
|
+
omittedForSize: omitted,
|
|
568
|
+
hint: `${omitted} more record(s) were left out of this result for size. Narrow the read with field/key, or ask for a smaller limit.`,
|
|
569
|
+
}),
|
|
570
|
+
docs: docs.slice(0, kept),
|
|
571
|
+
});
|
|
572
|
+
if (json.length <= WORK_TOOL_RESULT_MAX_CHARS || kept === 0)
|
|
573
|
+
return json;
|
|
574
|
+
kept = kept - 1;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function boundedLogEventsJson(envelope, events, pageCursor) {
|
|
578
|
+
let kept = events.length;
|
|
579
|
+
for (;;) {
|
|
580
|
+
const truncatedCount = events.length - kept;
|
|
581
|
+
const cursor = truncatedCount === 0 || kept === 0 ? pageCursor : events[kept - 1].id;
|
|
582
|
+
const json = JSON.stringify({
|
|
583
|
+
...envelope,
|
|
584
|
+
returned: kept,
|
|
585
|
+
...(truncatedCount === 0
|
|
586
|
+
? {}
|
|
587
|
+
: {
|
|
588
|
+
truncatedCount,
|
|
589
|
+
hint: `${truncatedCount} more line(s) were left out of this result for size. Read on from nextCursor, or narrow with level/sinceMinutes.`,
|
|
590
|
+
}),
|
|
591
|
+
events: events.slice(0, kept),
|
|
592
|
+
...(cursor === null ? {} : { nextCursor: cursor }),
|
|
593
|
+
});
|
|
594
|
+
if (json.length <= WORK_TOOL_RESULT_MAX_CHARS || kept === 0)
|
|
595
|
+
return json;
|
|
596
|
+
kept = kept - 1;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function truncateDoc(doc) {
|
|
600
|
+
const json = JSON.stringify(doc);
|
|
601
|
+
if (json === undefined || json.length <= DOC_BODY_CHARS)
|
|
602
|
+
return doc;
|
|
603
|
+
const id = typeof doc === "object" && doc !== null ? doc._id : undefined;
|
|
604
|
+
return {
|
|
605
|
+
...(typeof id === "string" ? { _id: id } : {}),
|
|
606
|
+
_truncated: true,
|
|
607
|
+
body: `${json.slice(0, DOC_BODY_CHARS)}${LOG_BODY_MARKER}`,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function stringArg(raw) {
|
|
611
|
+
return typeof raw === "string" && raw !== "" ? raw : undefined;
|
|
612
|
+
}
|
|
613
|
+
function clampDocLimit(raw) {
|
|
614
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
|
|
615
|
+
return DOC_PAGE_DEFAULT_LIMIT;
|
|
616
|
+
return Math.min(Math.floor(raw), DOC_PAGE_MAX_LIMIT);
|
|
617
|
+
}
|
|
618
|
+
function clampSinceMinutes(raw) {
|
|
619
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0)
|
|
620
|
+
return DEFAULT_SINCE_MINUTES;
|
|
621
|
+
return Math.min(Math.floor(raw), MAX_SINCE_MINUTES);
|
|
622
|
+
}
|
|
623
|
+
function truncateBody(body) {
|
|
624
|
+
return body.length <= LOG_BODY_CHARS ? body : `${body.slice(0, LOG_BODY_CHARS)}${LOG_BODY_MARKER}`;
|
|
625
|
+
}
|
|
626
|
+
function parseArgs(raw) {
|
|
627
|
+
const r = exception2Result(() => JSON.parse(raw));
|
|
628
|
+
if (r.isErr())
|
|
629
|
+
return {};
|
|
630
|
+
const parsed = r.Ok();
|
|
631
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
632
|
+
}
|
|
633
|
+
function toolJson(payload) {
|
|
634
|
+
return Result.Ok(JSON.stringify(payload));
|
|
635
|
+
}
|
|
636
|
+
export function fitOwnApiToolResult(answer) {
|
|
637
|
+
const envelope = (body) => ({
|
|
638
|
+
ok: true,
|
|
639
|
+
method: answer.method,
|
|
640
|
+
path: answer.path,
|
|
641
|
+
status: answer.status,
|
|
642
|
+
...(answer.contentType === null ? {} : { contentType: answer.contentType }),
|
|
643
|
+
body,
|
|
644
|
+
truncated: true,
|
|
645
|
+
});
|
|
646
|
+
const overhead = JSON.stringify(envelope("")).length;
|
|
647
|
+
let body = answer.bodyText;
|
|
648
|
+
let cut = answer.truncated;
|
|
649
|
+
const budget = Math.max(0, MAX_TOOL_RESULT_CHARS - overhead - 64);
|
|
650
|
+
if (body.length > budget) {
|
|
651
|
+
body = body.slice(0, budget);
|
|
652
|
+
cut = true;
|
|
653
|
+
}
|
|
654
|
+
while (body.length > 0 && JSON.stringify(envelope(body)).length > MAX_TOOL_RESULT_CHARS) {
|
|
655
|
+
const over = JSON.stringify(envelope(body)).length - MAX_TOOL_RESULT_CHARS;
|
|
656
|
+
body = body.slice(0, Math.max(0, body.length - Math.max(over, 16)));
|
|
657
|
+
cut = true;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
ok: true,
|
|
661
|
+
method: answer.method,
|
|
662
|
+
path: answer.path,
|
|
663
|
+
status: answer.status,
|
|
664
|
+
...(answer.contentType === null ? {} : { contentType: answer.contentType }),
|
|
665
|
+
body,
|
|
666
|
+
...(cut ? { truncated: true } : {}),
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function ownApiPathIsLiteral(path) {
|
|
670
|
+
const [beforeQuery] = path.split("?");
|
|
671
|
+
const r = exception2Result(() => new URL(`https://vibe.invalid/${path}`));
|
|
672
|
+
if (r.isErr())
|
|
673
|
+
return false;
|
|
674
|
+
return r.Ok().pathname === `/${beforeQuery}`;
|
|
675
|
+
}
|
|
676
|
+
//# sourceMappingURL=agent-debug-tools.js.map
|