@agent-native/dispatch 0.2.20 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/archive-workspace-app.d.ts +3 -0
- package/dist/actions/archive-workspace-app.d.ts.map +1 -0
- package/dist/actions/archive-workspace-app.js +15 -0
- package/dist/actions/archive-workspace-app.js.map +1 -0
- package/dist/actions/get-agent-thread-debug.d.ts +3 -0
- package/dist/actions/get-agent-thread-debug.d.ts.map +1 -0
- package/dist/actions/get-agent-thread-debug.js +24 -0
- package/dist/actions/get-agent-thread-debug.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-thread-sources.d.ts +3 -0
- package/dist/actions/list-agent-thread-sources.d.ts.map +1 -0
- package/dist/actions/list-agent-thread-sources.js +11 -0
- package/dist/actions/list-agent-thread-sources.js.map +1 -0
- package/dist/actions/list-available-workspace-templates.d.ts +3 -0
- package/dist/actions/list-available-workspace-templates.d.ts.map +1 -0
- package/dist/actions/list-available-workspace-templates.js +10 -0
- package/dist/actions/list-available-workspace-templates.js.map +1 -0
- package/dist/actions/navigate.js +1 -1
- package/dist/actions/navigate.js.map +1 -1
- package/dist/actions/remove-pending-workspace-app.d.ts +3 -0
- package/dist/actions/remove-pending-workspace-app.d.ts.map +1 -0
- package/dist/actions/remove-pending-workspace-app.js +15 -0
- package/dist/actions/remove-pending-workspace-app.js.map +1 -0
- package/dist/actions/scaffold-workspace-app.d.ts +3 -0
- package/dist/actions/scaffold-workspace-app.d.ts.map +1 -0
- package/dist/actions/scaffold-workspace-app.js +27 -0
- package/dist/actions/scaffold-workspace-app.js.map +1 -0
- package/dist/actions/search-agent-threads.d.ts +3 -0
- package/dist/actions/search-agent-threads.d.ts.map +1 -0
- package/dist/actions/search-agent-threads.js +25 -0
- package/dist/actions/search-agent-threads.js.map +1 -0
- package/dist/actions/unarchive-workspace-app.d.ts +3 -0
- package/dist/actions/unarchive-workspace-app.d.ts.map +1 -0
- package/dist/actions/unarchive-workspace-app.js +15 -0
- package/dist/actions/unarchive-workspace-app.js.map +1 -0
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +38 -0
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +8 -1
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/command.d.ts +7 -7
- package/dist/hooks/use-navigation-state.js +5 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts +5 -0
- package/dist/routes/pages/thread-debug.d.ts.map +1 -0
- package/dist/routes/pages/thread-debug.js +160 -0
- package/dist/routes/pages/thread-debug.js.map +1 -0
- package/dist/server/lib/app-creation-store.d.ts +40 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +245 -6
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts +101 -0
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -0
- package/dist/server/lib/thread-debug-store.js +587 -0
- package/dist/server/lib/thread-debug-store.js.map +1 -0
- package/package.json +2 -2
- package/src/actions/archive-workspace-app.ts +16 -0
- package/src/actions/get-agent-thread-debug.ts +25 -0
- package/src/actions/index.ts +12 -0
- package/src/actions/list-agent-thread-sources.ts +12 -0
- package/src/actions/list-available-workspace-templates.ts +11 -0
- package/src/actions/navigate.ts +1 -1
- package/src/actions/remove-pending-workspace-app.ts +16 -0
- package/src/actions/scaffold-workspace-app.ts +34 -0
- package/src/actions/search-agent-threads.ts +30 -0
- package/src/actions/unarchive-workspace-app.ts +16 -0
- package/src/actions/view-screen.ts +41 -0
- package/src/components/layout/Layout.tsx +8 -0
- package/src/hooks/use-navigation-state.ts +4 -0
- package/src/routes/index.ts +1 -0
- package/src/routes/pages/thread-debug.tsx +683 -0
- package/src/server/lib/app-creation-store.ts +312 -15
- package/src/server/lib/thread-debug-store.ts +779 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import { createDbExec, getDbExec } from "@agent-native/core/db";
|
|
2
|
+
import { currentOrgId, currentOwnerEmail } from "./dispatch-store.js";
|
|
3
|
+
const CONFIG_ENV_KEY = "AGENT_NATIVE_THREAD_DEBUG_DATABASES";
|
|
4
|
+
const MAX_RAW_PREVIEW_CHARS = 1_500;
|
|
5
|
+
const DEFAULT_SEARCH_LIMIT = 25;
|
|
6
|
+
const MAX_SEARCH_LIMIT = 100;
|
|
7
|
+
const DEFAULT_RUN_LIMIT = 20;
|
|
8
|
+
const DEFAULT_EVENT_LIMIT = 600;
|
|
9
|
+
const DEFAULT_TRACE_SPAN_LIMIT = 500;
|
|
10
|
+
const execCache = new Map();
|
|
11
|
+
function envEmails(name) {
|
|
12
|
+
return (process.env[name] ?? "")
|
|
13
|
+
.split(",")
|
|
14
|
+
.map((value) => value.trim().toLowerCase())
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
function isEnvAdmin(email) {
|
|
18
|
+
const normalized = email.trim().toLowerCase();
|
|
19
|
+
return [
|
|
20
|
+
...envEmails("DISPATCH_ADMIN_EMAILS"),
|
|
21
|
+
...envEmails("WORKSPACE_OWNER_EMAIL"),
|
|
22
|
+
...envEmails("DISPATCH_DEFAULT_OWNER_EMAIL"),
|
|
23
|
+
].includes(normalized);
|
|
24
|
+
}
|
|
25
|
+
function isMissingTableError(error) {
|
|
26
|
+
const message = String(error?.message ?? error).toLowerCase();
|
|
27
|
+
return (message.includes("no such table") ||
|
|
28
|
+
message.includes("does not exist") ||
|
|
29
|
+
message.includes("unknown table") ||
|
|
30
|
+
message.includes("undefined table"));
|
|
31
|
+
}
|
|
32
|
+
async function optionalRows(exec, sql, args = []) {
|
|
33
|
+
try {
|
|
34
|
+
return (await exec.execute({ sql, args })).rows;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (isMissingTableError(error))
|
|
38
|
+
return [];
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function queryRows(exec, sql, args = []) {
|
|
43
|
+
try {
|
|
44
|
+
return (await exec.execute({ sql, args })).rows;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isMissingTableError(error)) {
|
|
48
|
+
throw new Error("This database does not have agent chat thread tables yet.");
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function numberField(value) {
|
|
54
|
+
const parsed = Number(value ?? 0);
|
|
55
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
56
|
+
}
|
|
57
|
+
function nullableNumberField(value) {
|
|
58
|
+
if (value == null)
|
|
59
|
+
return null;
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
62
|
+
}
|
|
63
|
+
function safeJsonParse(value, fallback) {
|
|
64
|
+
if (value == null || value === "")
|
|
65
|
+
return fallback;
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(String(value));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function safeJsonStringify(value) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.stringify(value, null, 2);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return String(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function textFromContent(content) {
|
|
82
|
+
if (typeof content === "string")
|
|
83
|
+
return content;
|
|
84
|
+
if (!Array.isArray(content))
|
|
85
|
+
return "";
|
|
86
|
+
return content
|
|
87
|
+
.map((part) => {
|
|
88
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
89
|
+
return part.text;
|
|
90
|
+
}
|
|
91
|
+
if (part?.type === "tool-call") {
|
|
92
|
+
const name = part.toolName ?? part.name ?? "tool";
|
|
93
|
+
return `[tool:${name}] ${safeJsonStringify(part.args ?? part.input ?? {})}`;
|
|
94
|
+
}
|
|
95
|
+
if (part?.type === "tool-result") {
|
|
96
|
+
return `[tool-result:${part.toolName ?? part.toolCallId ?? "tool"}] ${typeof part.content === "string"
|
|
97
|
+
? part.content
|
|
98
|
+
: safeJsonStringify(part.content)}`;
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
})
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function normalizeMessages(threadData) {
|
|
106
|
+
const messages = Array.isArray(threadData?.messages)
|
|
107
|
+
? threadData.messages
|
|
108
|
+
: [];
|
|
109
|
+
return messages.map((entry, index) => {
|
|
110
|
+
const message = entry?.message ?? entry;
|
|
111
|
+
const content = message?.content;
|
|
112
|
+
const contentParts = Array.isArray(content) ? content : [];
|
|
113
|
+
return {
|
|
114
|
+
index,
|
|
115
|
+
id: typeof message?.id === "string" ? message.id : null,
|
|
116
|
+
role: typeof message?.role === "string" ? message.role : "unknown",
|
|
117
|
+
createdAt: message?.createdAt ?? null,
|
|
118
|
+
status: message?.status ?? null,
|
|
119
|
+
text: textFromContent(content),
|
|
120
|
+
contentParts,
|
|
121
|
+
attachments: Array.isArray(message?.attachments)
|
|
122
|
+
? message.attachments
|
|
123
|
+
: [],
|
|
124
|
+
metadata: message?.metadata ?? null,
|
|
125
|
+
parentId: entry?.parentId ?? null,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function snippetFor(row, query) {
|
|
130
|
+
const raw = `${row.title ?? ""}\n${row.preview ?? ""}\n${row.thread_data ?? ""}`;
|
|
131
|
+
const compact = raw.replace(/\s+/g, " ").trim();
|
|
132
|
+
if (!query.trim()) {
|
|
133
|
+
return compact.slice(0, MAX_RAW_PREVIEW_CHARS);
|
|
134
|
+
}
|
|
135
|
+
const lower = compact.toLowerCase();
|
|
136
|
+
const needle = query.trim().toLowerCase();
|
|
137
|
+
const match = lower.indexOf(needle);
|
|
138
|
+
if (match === -1)
|
|
139
|
+
return compact.slice(0, MAX_RAW_PREVIEW_CHARS);
|
|
140
|
+
const start = Math.max(0, match - 160);
|
|
141
|
+
const end = Math.min(compact.length, match + needle.length + 320);
|
|
142
|
+
const prefix = start > 0 ? "..." : "";
|
|
143
|
+
const suffix = end < compact.length ? "..." : "";
|
|
144
|
+
return `${prefix}${compact.slice(start, end)}${suffix}`;
|
|
145
|
+
}
|
|
146
|
+
function envPrefixForSourceId(sourceId) {
|
|
147
|
+
return sourceId.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
148
|
+
}
|
|
149
|
+
function sourceIdFromEnvPrefix(prefix) {
|
|
150
|
+
return prefix.toLowerCase().replace(/_/g, "-");
|
|
151
|
+
}
|
|
152
|
+
function labelFromSourceId(sourceId) {
|
|
153
|
+
return sourceId
|
|
154
|
+
.split(/[-_\s]+/)
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
157
|
+
.join(" ");
|
|
158
|
+
}
|
|
159
|
+
function currentDatabaseUrlEnv() {
|
|
160
|
+
const appName = process.env.APP_NAME?.toUpperCase().replace(/-/g, "_");
|
|
161
|
+
if (appName && process.env[`${appName}_DATABASE_URL`]) {
|
|
162
|
+
return `${appName}_DATABASE_URL`;
|
|
163
|
+
}
|
|
164
|
+
return process.env.DATABASE_URL ? "DATABASE_URL" : null;
|
|
165
|
+
}
|
|
166
|
+
function parseConfiguredSources() {
|
|
167
|
+
const raw = process.env[CONFIG_ENV_KEY];
|
|
168
|
+
if (!raw)
|
|
169
|
+
return [];
|
|
170
|
+
const parsed = safeJsonParse(raw, null);
|
|
171
|
+
const entries = Array.isArray(parsed)
|
|
172
|
+
? parsed
|
|
173
|
+
: parsed && typeof parsed === "object"
|
|
174
|
+
? Object.entries(parsed).map(([id, value]) => ({ id, ...value }))
|
|
175
|
+
: [];
|
|
176
|
+
return entries
|
|
177
|
+
.map((entry) => {
|
|
178
|
+
const id = typeof entry?.id === "string" ? entry.id.trim() : "";
|
|
179
|
+
if (!id)
|
|
180
|
+
return null;
|
|
181
|
+
const databaseUrlEnv = typeof entry.databaseUrlEnv === "string"
|
|
182
|
+
? entry.databaseUrlEnv.trim()
|
|
183
|
+
: null;
|
|
184
|
+
const databaseAuthTokenEnv = typeof entry.databaseAuthTokenEnv === "string"
|
|
185
|
+
? entry.databaseAuthTokenEnv.trim()
|
|
186
|
+
: null;
|
|
187
|
+
const databaseUrl = typeof entry.databaseUrl === "string" && entry.databaseUrl.trim()
|
|
188
|
+
? entry.databaseUrl.trim()
|
|
189
|
+
: databaseUrlEnv
|
|
190
|
+
? process.env[databaseUrlEnv]
|
|
191
|
+
: undefined;
|
|
192
|
+
if (!databaseUrl)
|
|
193
|
+
return null;
|
|
194
|
+
return {
|
|
195
|
+
id,
|
|
196
|
+
label: typeof entry.label === "string" && entry.label.trim()
|
|
197
|
+
? entry.label.trim()
|
|
198
|
+
: labelFromSourceId(id),
|
|
199
|
+
kind: "configured",
|
|
200
|
+
databaseUrl,
|
|
201
|
+
databaseAuthToken: typeof entry.databaseAuthToken === "string"
|
|
202
|
+
? entry.databaseAuthToken
|
|
203
|
+
: databaseAuthTokenEnv
|
|
204
|
+
? process.env[databaseAuthTokenEnv]
|
|
205
|
+
: undefined,
|
|
206
|
+
databaseUrlEnv,
|
|
207
|
+
databaseAuthTokenEnv,
|
|
208
|
+
};
|
|
209
|
+
})
|
|
210
|
+
.filter((source) => Boolean(source));
|
|
211
|
+
}
|
|
212
|
+
function discoverEnvSources() {
|
|
213
|
+
const ignored = new Set([
|
|
214
|
+
"DATABASE_URL",
|
|
215
|
+
"NETLIFY_DATABASE_URL",
|
|
216
|
+
"NETLIFY_DATABASE_URL_UNPOOLED",
|
|
217
|
+
]);
|
|
218
|
+
const currentAppPrefix = process.env.APP_NAME?.toUpperCase().replace(/-/g, "_");
|
|
219
|
+
const sources = [];
|
|
220
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
221
|
+
if (!value || !key.endsWith("_DATABASE_URL"))
|
|
222
|
+
continue;
|
|
223
|
+
if (ignored.has(key) || key.endsWith("_UNPOOLED_DATABASE_URL"))
|
|
224
|
+
continue;
|
|
225
|
+
const prefix = key.slice(0, -"_DATABASE_URL".length);
|
|
226
|
+
if (!prefix || prefix === "NETLIFY")
|
|
227
|
+
continue;
|
|
228
|
+
if (currentAppPrefix && prefix === currentAppPrefix)
|
|
229
|
+
continue;
|
|
230
|
+
const id = sourceIdFromEnvPrefix(prefix);
|
|
231
|
+
const tokenEnv = `${prefix}_DATABASE_AUTH_TOKEN`;
|
|
232
|
+
sources.push({
|
|
233
|
+
id,
|
|
234
|
+
label: labelFromSourceId(id),
|
|
235
|
+
kind: "env",
|
|
236
|
+
databaseUrl: value,
|
|
237
|
+
databaseAuthToken: process.env[tokenEnv],
|
|
238
|
+
databaseUrlEnv: key,
|
|
239
|
+
databaseAuthTokenEnv: process.env[tokenEnv] ? tokenEnv : null,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return sources;
|
|
243
|
+
}
|
|
244
|
+
function sourceConfigs() {
|
|
245
|
+
const byId = new Map();
|
|
246
|
+
byId.set("current", {
|
|
247
|
+
id: "current",
|
|
248
|
+
label: "Current Dispatch DB",
|
|
249
|
+
kind: "current",
|
|
250
|
+
databaseUrlEnv: currentDatabaseUrlEnv(),
|
|
251
|
+
databaseAuthTokenEnv: process.env.DATABASE_AUTH_TOKEN
|
|
252
|
+
? "DATABASE_AUTH_TOKEN"
|
|
253
|
+
: null,
|
|
254
|
+
});
|
|
255
|
+
for (const source of discoverEnvSources())
|
|
256
|
+
byId.set(source.id, source);
|
|
257
|
+
for (const source of parseConfiguredSources())
|
|
258
|
+
byId.set(source.id, source);
|
|
259
|
+
return [...byId.values()];
|
|
260
|
+
}
|
|
261
|
+
function resolveSourceConfig(sourceId = "current") {
|
|
262
|
+
const normalized = sourceId.trim() || "current";
|
|
263
|
+
const direct = sourceConfigs().find((source) => source.id === normalized);
|
|
264
|
+
if (direct)
|
|
265
|
+
return direct;
|
|
266
|
+
const prefix = envPrefixForSourceId(normalized);
|
|
267
|
+
const databaseUrlEnv = `${prefix}_DATABASE_URL`;
|
|
268
|
+
const databaseUrl = process.env[databaseUrlEnv];
|
|
269
|
+
if (!databaseUrl) {
|
|
270
|
+
throw new Error(`Thread debug source "${normalized}" is not configured.`);
|
|
271
|
+
}
|
|
272
|
+
const tokenEnv = `${prefix}_DATABASE_AUTH_TOKEN`;
|
|
273
|
+
return {
|
|
274
|
+
id: normalized,
|
|
275
|
+
label: labelFromSourceId(normalized),
|
|
276
|
+
kind: "env",
|
|
277
|
+
databaseUrl,
|
|
278
|
+
databaseAuthToken: process.env[tokenEnv],
|
|
279
|
+
databaseUrlEnv,
|
|
280
|
+
databaseAuthTokenEnv: process.env[tokenEnv] ? tokenEnv : null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
async function execForSource(source) {
|
|
284
|
+
if (source.kind === "current")
|
|
285
|
+
return getDbExec();
|
|
286
|
+
const cacheKey = `${source.databaseUrl ?? ""}\n${source.databaseAuthToken ?? ""}`;
|
|
287
|
+
if (!execCache.has(cacheKey)) {
|
|
288
|
+
execCache.set(cacheKey, createDbExec({
|
|
289
|
+
url: source.databaseUrl,
|
|
290
|
+
authToken: source.databaseAuthToken,
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
return execCache.get(cacheKey);
|
|
294
|
+
}
|
|
295
|
+
async function currentDbRows(sql, args = []) {
|
|
296
|
+
return optionalRows(getDbExec(), sql, args);
|
|
297
|
+
}
|
|
298
|
+
async function viewerOrgRole(orgId, viewerEmail) {
|
|
299
|
+
if (!orgId)
|
|
300
|
+
return null;
|
|
301
|
+
const rows = await currentDbRows(`SELECT role FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`, [orgId, viewerEmail.toLowerCase()]);
|
|
302
|
+
return typeof rows[0]?.role === "string" ? rows[0].role : null;
|
|
303
|
+
}
|
|
304
|
+
async function currentOrgMembers(orgId) {
|
|
305
|
+
if (!orgId)
|
|
306
|
+
return [];
|
|
307
|
+
const rows = await currentDbRows(`SELECT email FROM org_members WHERE org_id = ?`, [orgId]);
|
|
308
|
+
return rows.map((row) => String(row.email ?? "").trim()).filter(Boolean);
|
|
309
|
+
}
|
|
310
|
+
async function resolveDebugAccess() {
|
|
311
|
+
const viewerEmail = currentOwnerEmail();
|
|
312
|
+
const orgId = currentOrgId();
|
|
313
|
+
const role = await viewerOrgRole(orgId, viewerEmail);
|
|
314
|
+
const envAdmin = isEnvAdmin(viewerEmail);
|
|
315
|
+
const canInspectAll = envAdmin || role === "owner" || role === "admin";
|
|
316
|
+
const memberEmails = canInspectAll
|
|
317
|
+
? await currentOrgMembers(orgId)
|
|
318
|
+
: [viewerEmail];
|
|
319
|
+
return {
|
|
320
|
+
viewerEmail,
|
|
321
|
+
orgId,
|
|
322
|
+
role,
|
|
323
|
+
envAdmin,
|
|
324
|
+
canInspectAll,
|
|
325
|
+
memberEmails: memberEmails.length > 0 ? memberEmails : [viewerEmail],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function assertSourceAccess(source, access) {
|
|
329
|
+
if (source.kind === "current")
|
|
330
|
+
return;
|
|
331
|
+
if (!access.canInspectAll) {
|
|
332
|
+
throw new Error("Only Dispatch admins can inspect thread databases from other apps.");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function ownerScope(access, ownerEmail) {
|
|
336
|
+
const requested = ownerEmail?.trim();
|
|
337
|
+
if (!access.canInspectAll) {
|
|
338
|
+
return {
|
|
339
|
+
sql: "owner_email = ?",
|
|
340
|
+
args: [access.viewerEmail],
|
|
341
|
+
label: access.viewerEmail,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (requested && requested !== "*") {
|
|
345
|
+
return {
|
|
346
|
+
sql: "owner_email = ?",
|
|
347
|
+
args: [requested],
|
|
348
|
+
label: requested,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (access.envAdmin && !access.orgId) {
|
|
352
|
+
return { sql: "1 = 1", args: [], label: "all users" };
|
|
353
|
+
}
|
|
354
|
+
const emails = access.memberEmails;
|
|
355
|
+
if (emails.length === 0) {
|
|
356
|
+
return {
|
|
357
|
+
sql: "owner_email = ?",
|
|
358
|
+
args: [access.viewerEmail],
|
|
359
|
+
label: access.viewerEmail,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const placeholders = emails.map(() => "?").join(", ");
|
|
363
|
+
return {
|
|
364
|
+
sql: `owner_email IN (${placeholders})`,
|
|
365
|
+
args: emails,
|
|
366
|
+
label: access.orgId ? "current organization" : "all users",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function serializeThreadSummary(row, query = "") {
|
|
370
|
+
return {
|
|
371
|
+
id: String(row.id),
|
|
372
|
+
ownerEmail: String(row.owner_email ?? ""),
|
|
373
|
+
title: String(row.title ?? ""),
|
|
374
|
+
preview: String(row.preview ?? ""),
|
|
375
|
+
messageCount: numberField(row.message_count),
|
|
376
|
+
createdAt: numberField(row.created_at),
|
|
377
|
+
updatedAt: numberField(row.updated_at),
|
|
378
|
+
snippet: snippetFor(row, query),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function serializeRun(row, events = []) {
|
|
382
|
+
return {
|
|
383
|
+
id: String(row.id),
|
|
384
|
+
threadId: String(row.thread_id),
|
|
385
|
+
status: String(row.status),
|
|
386
|
+
abortReason: row.abort_reason ? String(row.abort_reason) : null,
|
|
387
|
+
startedAt: numberField(row.started_at),
|
|
388
|
+
completedAt: nullableNumberField(row.completed_at),
|
|
389
|
+
heartbeatAt: nullableNumberField(row.heartbeat_at),
|
|
390
|
+
events,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function parseRunEvent(row) {
|
|
394
|
+
const raw = String(row.event_data ?? "");
|
|
395
|
+
return {
|
|
396
|
+
runId: String(row.run_id ?? ""),
|
|
397
|
+
seq: numberField(row.seq),
|
|
398
|
+
event: safeJsonParse(raw, { type: "unparseable", raw }),
|
|
399
|
+
rawEventData: raw,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
export async function listThreadDebugSources() {
|
|
403
|
+
const access = await resolveDebugAccess();
|
|
404
|
+
return {
|
|
405
|
+
access: {
|
|
406
|
+
viewerEmail: access.viewerEmail,
|
|
407
|
+
orgId: access.orgId,
|
|
408
|
+
role: access.role,
|
|
409
|
+
envAdmin: access.envAdmin,
|
|
410
|
+
canInspectAll: access.canInspectAll,
|
|
411
|
+
memberCount: access.memberEmails.length,
|
|
412
|
+
},
|
|
413
|
+
sources: sourceConfigs().map((source) => ({
|
|
414
|
+
id: source.id,
|
|
415
|
+
label: source.label,
|
|
416
|
+
kind: source.kind,
|
|
417
|
+
current: source.kind === "current",
|
|
418
|
+
connected: source.kind === "current" || Boolean(source.databaseUrl),
|
|
419
|
+
databaseUrlEnv: source.databaseUrlEnv ?? null,
|
|
420
|
+
databaseAuthTokenEnv: source.databaseAuthTokenEnv ?? null,
|
|
421
|
+
canInspectAll: access.canInspectAll,
|
|
422
|
+
})),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export async function searchAgentThreads(input) {
|
|
426
|
+
const source = resolveSourceConfig(input.sourceId ?? "current");
|
|
427
|
+
const access = await resolveDebugAccess();
|
|
428
|
+
assertSourceAccess(source, access);
|
|
429
|
+
const exec = await execForSource(source);
|
|
430
|
+
const limit = Math.max(1, Math.min(MAX_SEARCH_LIMIT, input.limit ?? DEFAULT_SEARCH_LIMIT));
|
|
431
|
+
const q = input.query?.trim() ?? "";
|
|
432
|
+
const scope = ownerScope(access, input.ownerEmail);
|
|
433
|
+
const where = [scope.sql];
|
|
434
|
+
const args = [...scope.args];
|
|
435
|
+
if (q) {
|
|
436
|
+
const pattern = `%${q.toLowerCase()}%`;
|
|
437
|
+
where.push(`(LOWER(title) LIKE ? OR LOWER(preview) LIKE ? OR LOWER(thread_data) LIKE ?)`);
|
|
438
|
+
args.push(pattern, pattern, pattern);
|
|
439
|
+
}
|
|
440
|
+
args.push(limit);
|
|
441
|
+
const rows = await optionalRows(exec, `SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at
|
|
442
|
+
FROM chat_threads
|
|
443
|
+
WHERE ${where.join(" AND ")}
|
|
444
|
+
ORDER BY updated_at DESC
|
|
445
|
+
LIMIT ?`, args);
|
|
446
|
+
return {
|
|
447
|
+
source: {
|
|
448
|
+
id: source.id,
|
|
449
|
+
label: source.label,
|
|
450
|
+
kind: source.kind,
|
|
451
|
+
databaseUrlEnv: source.databaseUrlEnv ?? null,
|
|
452
|
+
},
|
|
453
|
+
access: {
|
|
454
|
+
viewerEmail: access.viewerEmail,
|
|
455
|
+
scope: scope.label,
|
|
456
|
+
canInspectAll: access.canInspectAll,
|
|
457
|
+
},
|
|
458
|
+
query: q,
|
|
459
|
+
count: rows.length,
|
|
460
|
+
threads: rows.map((row) => serializeThreadSummary(row, q)),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
export async function getAgentThreadDebug(input) {
|
|
464
|
+
const source = resolveSourceConfig(input.sourceId ?? "current");
|
|
465
|
+
const access = await resolveDebugAccess();
|
|
466
|
+
assertSourceAccess(source, access);
|
|
467
|
+
const exec = await execForSource(source);
|
|
468
|
+
const scope = ownerScope(access, input.ownerEmail);
|
|
469
|
+
const rows = await queryRows(exec, `SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at
|
|
470
|
+
FROM chat_threads
|
|
471
|
+
WHERE id = ? AND ${scope.sql}
|
|
472
|
+
LIMIT 1`, [input.threadId, ...scope.args]);
|
|
473
|
+
const row = rows[0];
|
|
474
|
+
if (!row) {
|
|
475
|
+
throw new Error(`Thread "${input.threadId}" was not found.`);
|
|
476
|
+
}
|
|
477
|
+
const threadData = safeJsonParse(row.thread_data, {});
|
|
478
|
+
const maxRuns = Math.max(1, Math.min(50, input.maxRuns ?? DEFAULT_RUN_LIMIT));
|
|
479
|
+
const maxEvents = Math.max(1, Math.min(2_000, input.maxEvents ?? DEFAULT_EVENT_LIMIT));
|
|
480
|
+
const maxTraceSpans = Math.max(1, Math.min(2_000, input.maxTraceSpans ?? DEFAULT_TRACE_SPAN_LIMIT));
|
|
481
|
+
const runRows = await optionalRows(exec, `SELECT id, thread_id, status, abort_reason, started_at, heartbeat_at, completed_at
|
|
482
|
+
FROM agent_runs
|
|
483
|
+
WHERE thread_id = ?
|
|
484
|
+
ORDER BY started_at DESC
|
|
485
|
+
LIMIT ?`, [row.id, maxRuns]);
|
|
486
|
+
const runs = [];
|
|
487
|
+
for (const run of runRows) {
|
|
488
|
+
const eventRows = await optionalRows(exec, `SELECT run_id, seq, event_data
|
|
489
|
+
FROM agent_run_events
|
|
490
|
+
WHERE run_id = ?
|
|
491
|
+
ORDER BY seq ASC
|
|
492
|
+
LIMIT ?`, [run.id, maxEvents]);
|
|
493
|
+
runs.push(serializeRun(run, eventRows.map(parseRunEvent)));
|
|
494
|
+
}
|
|
495
|
+
const runIds = runRows.map((run) => run.id);
|
|
496
|
+
const runPlaceholders = runIds.map(() => "?").join(", ");
|
|
497
|
+
const traceSummaryRows = await optionalRows(exec, runIds.length > 0
|
|
498
|
+
? `SELECT *
|
|
499
|
+
FROM agent_trace_summaries
|
|
500
|
+
WHERE thread_id = ? OR run_id IN (${runPlaceholders})
|
|
501
|
+
ORDER BY created_at DESC
|
|
502
|
+
LIMIT 50`
|
|
503
|
+
: `SELECT *
|
|
504
|
+
FROM agent_trace_summaries
|
|
505
|
+
WHERE thread_id = ?
|
|
506
|
+
ORDER BY created_at DESC
|
|
507
|
+
LIMIT 50`, runIds.length > 0 ? [row.id, ...runIds] : [row.id]);
|
|
508
|
+
const traceSpanRows = await optionalRows(exec, runIds.length > 0
|
|
509
|
+
? `SELECT *
|
|
510
|
+
FROM agent_trace_spans
|
|
511
|
+
WHERE thread_id = ? OR run_id IN (${runPlaceholders})
|
|
512
|
+
ORDER BY created_at ASC
|
|
513
|
+
LIMIT ?`
|
|
514
|
+
: `SELECT *
|
|
515
|
+
FROM agent_trace_spans
|
|
516
|
+
WHERE thread_id = ?
|
|
517
|
+
ORDER BY created_at ASC
|
|
518
|
+
LIMIT ?`, runIds.length > 0
|
|
519
|
+
? [row.id, ...runIds, maxTraceSpans]
|
|
520
|
+
: [row.id, maxTraceSpans]);
|
|
521
|
+
const feedbackRows = await optionalRows(exec, `SELECT *
|
|
522
|
+
FROM agent_feedback
|
|
523
|
+
WHERE thread_id = ?
|
|
524
|
+
ORDER BY created_at DESC
|
|
525
|
+
LIMIT 50`, [row.id]);
|
|
526
|
+
const satisfactionRows = await optionalRows(exec, `SELECT *
|
|
527
|
+
FROM agent_satisfaction_scores
|
|
528
|
+
WHERE thread_id = ?
|
|
529
|
+
ORDER BY computed_at DESC
|
|
530
|
+
LIMIT 10`, [row.id]);
|
|
531
|
+
const evalRows = await optionalRows(exec, runIds.length > 0
|
|
532
|
+
? `SELECT *
|
|
533
|
+
FROM agent_evals
|
|
534
|
+
WHERE thread_id = ? OR run_id IN (${runPlaceholders})
|
|
535
|
+
ORDER BY created_at DESC
|
|
536
|
+
LIMIT 50`
|
|
537
|
+
: `SELECT *
|
|
538
|
+
FROM agent_evals
|
|
539
|
+
WHERE thread_id = ?
|
|
540
|
+
ORDER BY created_at DESC
|
|
541
|
+
LIMIT 50`, runIds.length > 0 ? [row.id, ...runIds] : [row.id]);
|
|
542
|
+
const checkpointRows = await optionalRows(exec, `SELECT id, thread_id, run_id, commit_sha, message, created_at
|
|
543
|
+
FROM agent_checkpoints
|
|
544
|
+
WHERE thread_id = ?
|
|
545
|
+
ORDER BY created_at DESC
|
|
546
|
+
LIMIT 50`, [row.id]);
|
|
547
|
+
return {
|
|
548
|
+
source: {
|
|
549
|
+
id: source.id,
|
|
550
|
+
label: source.label,
|
|
551
|
+
kind: source.kind,
|
|
552
|
+
databaseUrlEnv: source.databaseUrlEnv ?? null,
|
|
553
|
+
},
|
|
554
|
+
access: {
|
|
555
|
+
viewerEmail: access.viewerEmail,
|
|
556
|
+
scope: scope.label,
|
|
557
|
+
canInspectAll: access.canInspectAll,
|
|
558
|
+
},
|
|
559
|
+
thread: {
|
|
560
|
+
id: String(row.id),
|
|
561
|
+
ownerEmail: String(row.owner_email ?? ""),
|
|
562
|
+
title: String(row.title ?? ""),
|
|
563
|
+
preview: String(row.preview ?? ""),
|
|
564
|
+
messageCount: numberField(row.message_count),
|
|
565
|
+
createdAt: numberField(row.created_at),
|
|
566
|
+
updatedAt: numberField(row.updated_at),
|
|
567
|
+
},
|
|
568
|
+
messages: normalizeMessages(threadData),
|
|
569
|
+
debug: threadData?._debug ?? null,
|
|
570
|
+
debugRuns: Array.isArray(threadData?._debugRuns)
|
|
571
|
+
? threadData._debugRuns
|
|
572
|
+
: [],
|
|
573
|
+
queuedMessages: threadData?.queuedMessages ?? [],
|
|
574
|
+
threadData,
|
|
575
|
+
rawThreadData: row.thread_data ?? "",
|
|
576
|
+
runs,
|
|
577
|
+
traces: {
|
|
578
|
+
summaries: traceSummaryRows,
|
|
579
|
+
spans: traceSpanRows,
|
|
580
|
+
},
|
|
581
|
+
feedback: feedbackRows,
|
|
582
|
+
satisfaction: satisfactionRows,
|
|
583
|
+
evals: evalRows,
|
|
584
|
+
checkpoints: checkpointRows,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
//# sourceMappingURL=thread-debug-store.js.map
|