@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.
Files changed (79) hide show
  1. package/dist/actions/archive-workspace-app.d.ts +3 -0
  2. package/dist/actions/archive-workspace-app.d.ts.map +1 -0
  3. package/dist/actions/archive-workspace-app.js +15 -0
  4. package/dist/actions/archive-workspace-app.js.map +1 -0
  5. package/dist/actions/get-agent-thread-debug.d.ts +3 -0
  6. package/dist/actions/get-agent-thread-debug.d.ts.map +1 -0
  7. package/dist/actions/get-agent-thread-debug.js +24 -0
  8. package/dist/actions/get-agent-thread-debug.js.map +1 -0
  9. package/dist/actions/index.d.ts.map +1 -1
  10. package/dist/actions/index.js +8 -0
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/actions/list-agent-thread-sources.d.ts +3 -0
  13. package/dist/actions/list-agent-thread-sources.d.ts.map +1 -0
  14. package/dist/actions/list-agent-thread-sources.js +11 -0
  15. package/dist/actions/list-agent-thread-sources.js.map +1 -0
  16. package/dist/actions/list-available-workspace-templates.d.ts +3 -0
  17. package/dist/actions/list-available-workspace-templates.d.ts.map +1 -0
  18. package/dist/actions/list-available-workspace-templates.js +10 -0
  19. package/dist/actions/list-available-workspace-templates.js.map +1 -0
  20. package/dist/actions/navigate.js +1 -1
  21. package/dist/actions/navigate.js.map +1 -1
  22. package/dist/actions/remove-pending-workspace-app.d.ts +3 -0
  23. package/dist/actions/remove-pending-workspace-app.d.ts.map +1 -0
  24. package/dist/actions/remove-pending-workspace-app.js +15 -0
  25. package/dist/actions/remove-pending-workspace-app.js.map +1 -0
  26. package/dist/actions/scaffold-workspace-app.d.ts +3 -0
  27. package/dist/actions/scaffold-workspace-app.d.ts.map +1 -0
  28. package/dist/actions/scaffold-workspace-app.js +27 -0
  29. package/dist/actions/scaffold-workspace-app.js.map +1 -0
  30. package/dist/actions/search-agent-threads.d.ts +3 -0
  31. package/dist/actions/search-agent-threads.d.ts.map +1 -0
  32. package/dist/actions/search-agent-threads.js +25 -0
  33. package/dist/actions/search-agent-threads.js.map +1 -0
  34. package/dist/actions/unarchive-workspace-app.d.ts +3 -0
  35. package/dist/actions/unarchive-workspace-app.d.ts.map +1 -0
  36. package/dist/actions/unarchive-workspace-app.js +15 -0
  37. package/dist/actions/unarchive-workspace-app.js.map +1 -0
  38. package/dist/actions/view-screen.d.ts.map +1 -1
  39. package/dist/actions/view-screen.js +38 -0
  40. package/dist/actions/view-screen.js.map +1 -1
  41. package/dist/components/layout/Layout.d.ts.map +1 -1
  42. package/dist/components/layout/Layout.js +8 -1
  43. package/dist/components/layout/Layout.js.map +1 -1
  44. package/dist/components/ui/command.d.ts +7 -7
  45. package/dist/hooks/use-navigation-state.js +5 -0
  46. package/dist/hooks/use-navigation-state.js.map +1 -1
  47. package/dist/routes/index.d.ts.map +1 -1
  48. package/dist/routes/index.js +1 -0
  49. package/dist/routes/index.js.map +1 -1
  50. package/dist/routes/pages/thread-debug.d.ts +5 -0
  51. package/dist/routes/pages/thread-debug.d.ts.map +1 -0
  52. package/dist/routes/pages/thread-debug.js +160 -0
  53. package/dist/routes/pages/thread-debug.js.map +1 -0
  54. package/dist/server/lib/app-creation-store.d.ts +40 -0
  55. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  56. package/dist/server/lib/app-creation-store.js +245 -6
  57. package/dist/server/lib/app-creation-store.js.map +1 -1
  58. package/dist/server/lib/thread-debug-store.d.ts +101 -0
  59. package/dist/server/lib/thread-debug-store.d.ts.map +1 -0
  60. package/dist/server/lib/thread-debug-store.js +587 -0
  61. package/dist/server/lib/thread-debug-store.js.map +1 -0
  62. package/package.json +2 -2
  63. package/src/actions/archive-workspace-app.ts +16 -0
  64. package/src/actions/get-agent-thread-debug.ts +25 -0
  65. package/src/actions/index.ts +12 -0
  66. package/src/actions/list-agent-thread-sources.ts +12 -0
  67. package/src/actions/list-available-workspace-templates.ts +11 -0
  68. package/src/actions/navigate.ts +1 -1
  69. package/src/actions/remove-pending-workspace-app.ts +16 -0
  70. package/src/actions/scaffold-workspace-app.ts +34 -0
  71. package/src/actions/search-agent-threads.ts +30 -0
  72. package/src/actions/unarchive-workspace-app.ts +16 -0
  73. package/src/actions/view-screen.ts +41 -0
  74. package/src/components/layout/Layout.tsx +8 -0
  75. package/src/hooks/use-navigation-state.ts +4 -0
  76. package/src/routes/index.ts +1 -0
  77. package/src/routes/pages/thread-debug.tsx +683 -0
  78. package/src/server/lib/app-creation-store.ts +312 -15
  79. 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