@agent-native/dispatch 0.16.1 → 0.16.3
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/get-agent-thread-debug.d.ts +9 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +2 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-agent-run-failures.d.ts +61 -0
- package/dist/actions/list-agent-run-failures.d.ts.map +1 -0
- package/dist/actions/list-agent-run-failures.js +38 -0
- package/dist/actions/list-agent-run-failures.js.map +1 -0
- package/dist/actions/provider-api-register.d.ts +12 -12
- package/dist/actions/upsert-destination.d.ts +12 -12
- package/dist/actions/view-screen.js +32 -5
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +6 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +30 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +291 -77
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts +72 -0
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
- package/dist/server/lib/thread-debug-store.js +215 -19
- package/dist/server/lib/thread-debug-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +1 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/index.ts +2 -0
- package/src/actions/list-agent-run-failures.ts +44 -0
- package/src/actions/view-screen.spec.ts +91 -0
- package/src/actions/view-screen.ts +34 -4
- package/src/hooks/use-navigation-state.spec.ts +45 -0
- package/src/hooks/use-navigation-state.ts +28 -0
- package/src/routes/pages/thread-debug.spec.tsx +272 -0
- package/src/routes/pages/thread-debug.tsx +843 -224
- package/src/server/lib/thread-debug-store.spec.ts +292 -16
- package/src/server/lib/thread-debug-store.ts +318 -22
- package/src/server/plugins/agent-chat.spec.ts +30 -0
- package/src/server/plugins/agent-chat.ts +1 -0
|
@@ -65,10 +65,45 @@ interface AgentRunRow {
|
|
|
65
65
|
started_at: number | string;
|
|
66
66
|
completed_at?: number | string | null;
|
|
67
67
|
heartbeat_at?: number | string | null;
|
|
68
|
+
turn_id?: string | null;
|
|
69
|
+
last_progress_at?: number | string | null;
|
|
70
|
+
error_code?: string | null;
|
|
71
|
+
error_detail?: string | null;
|
|
72
|
+
terminal_reason?: string | null;
|
|
73
|
+
dispatch_mode?: string | null;
|
|
74
|
+
worker_stage?: string | null;
|
|
75
|
+
diag_stage?: string | null;
|
|
76
|
+
peak_rss_mb?: number | string | null;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
const execCache = new Map<string, Promise<DbExec>>();
|
|
71
80
|
|
|
81
|
+
const UNSUCCESSFUL_RUN_STATUSES = ["errored", "aborted", "truncated"] as const;
|
|
82
|
+
|
|
83
|
+
export type AgentRunFailureStatus = (typeof UNSUCCESSFUL_RUN_STATUSES)[number];
|
|
84
|
+
|
|
85
|
+
type ThreadDebugSourceHealthStatus =
|
|
86
|
+
| "ok"
|
|
87
|
+
| "disconnected"
|
|
88
|
+
| "unsupported"
|
|
89
|
+
| "unavailable";
|
|
90
|
+
|
|
91
|
+
interface AgentRunFailureRow extends AgentRunRow {
|
|
92
|
+
debug_owner_email: string;
|
|
93
|
+
debug_thread_title?: string | null;
|
|
94
|
+
debug_thread_preview?: string | null;
|
|
95
|
+
debug_terminal_event_data?: string | null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class UnsupportedThreadDebugSchemaError extends Error {
|
|
99
|
+
readonly code = "thread_debug_schema_unsupported";
|
|
100
|
+
|
|
101
|
+
constructor(readonly table: string) {
|
|
102
|
+
super(`This database does not have the required "${table}" table.`);
|
|
103
|
+
this.name = "UnsupportedThreadDebugSchemaError";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
72
107
|
function envEmails(name: string): string[] {
|
|
73
108
|
return (process.env[name] ?? "")
|
|
74
109
|
.split(",")
|
|
@@ -89,14 +124,24 @@ function isEnvAdmin(email: string): boolean {
|
|
|
89
124
|
].includes(normalized);
|
|
90
125
|
}
|
|
91
126
|
|
|
127
|
+
function missingTableName(error: unknown): string | null {
|
|
128
|
+
const message = String((error as Error)?.message ?? error);
|
|
129
|
+
const patterns = [
|
|
130
|
+
/no such table:\s*(?:(?:main|public)\.)?["'`]?([a-zA-Z_][\w$]*)/i,
|
|
131
|
+
/relation\s+["'`](?:(?:public)\.)?([a-zA-Z_][\w$]*)["'`]\s+does not exist/i,
|
|
132
|
+
/table\s+["'`](?:[^"'`.]+\.)?([a-zA-Z_][\w$]*)["'`]\s+does(?:n't| not)\s+exist/i,
|
|
133
|
+
/unknown table\s+["'`]?(?:[^"'`.\s]+\.)?([a-zA-Z_][\w$]*)/i,
|
|
134
|
+
/undefined table[^a-zA-Z_]+(?:[^.\s]+\.)?([a-zA-Z_][\w$]*)/i,
|
|
135
|
+
];
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
const match = message.match(pattern);
|
|
138
|
+
if (match?.[1]) return match[1].toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
92
143
|
function isMissingTableError(error: unknown): boolean {
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
message.includes("no such table") ||
|
|
96
|
-
message.includes("does not exist") ||
|
|
97
|
-
message.includes("unknown table") ||
|
|
98
|
-
message.includes("undefined table")
|
|
99
|
-
);
|
|
144
|
+
return missingTableName(error) !== null;
|
|
100
145
|
}
|
|
101
146
|
|
|
102
147
|
async function optionalRows<T = Record<string, unknown>>(
|
|
@@ -120,10 +165,9 @@ async function queryRows<T = Record<string, unknown>>(
|
|
|
120
165
|
try {
|
|
121
166
|
return (await exec.execute({ sql, args })).rows as T[];
|
|
122
167
|
} catch (error) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
168
|
+
const missingTable = missingTableName(error);
|
|
169
|
+
if (missingTable) {
|
|
170
|
+
throw new UnsupportedThreadDebugSchemaError(missingTable);
|
|
127
171
|
}
|
|
128
172
|
throw error;
|
|
129
173
|
}
|
|
@@ -276,7 +320,6 @@ function parseConfiguredSources(): ThreadDebugSourceConfig[] {
|
|
|
276
320
|
: databaseUrlEnv
|
|
277
321
|
? process.env[databaseUrlEnv]
|
|
278
322
|
: undefined;
|
|
279
|
-
if (!databaseUrl) return null;
|
|
280
323
|
return {
|
|
281
324
|
id,
|
|
282
325
|
label:
|
|
@@ -349,7 +392,14 @@ function sourceConfigs(): ThreadDebugSourceConfig[] {
|
|
|
349
392
|
function resolveSourceConfig(sourceId = "current"): ThreadDebugSourceConfig {
|
|
350
393
|
const normalized = sourceId.trim() || "current";
|
|
351
394
|
const direct = sourceConfigs().find((source) => source.id === normalized);
|
|
352
|
-
if (direct)
|
|
395
|
+
if (direct) {
|
|
396
|
+
if (direct.kind !== "current" && !direct.databaseUrl) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Thread debug source "${normalized}" is configured but disconnected.`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return direct;
|
|
402
|
+
}
|
|
353
403
|
|
|
354
404
|
const prefix = envPrefixForSourceId(normalized);
|
|
355
405
|
const databaseUrlEnv = `${prefix}_DATABASE_URL`;
|
|
@@ -371,6 +421,11 @@ function resolveSourceConfig(sourceId = "current"): ThreadDebugSourceConfig {
|
|
|
371
421
|
|
|
372
422
|
async function execForSource(source: ThreadDebugSourceConfig): Promise<DbExec> {
|
|
373
423
|
if (source.kind === "current") return getDbExec();
|
|
424
|
+
if (!source.databaseUrl) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Thread debug source "${source.id}" is configured but disconnected.`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
374
429
|
const cacheKey = `${source.databaseUrl ?? ""}\n${source.databaseAuthToken ?? ""}`;
|
|
375
430
|
if (!execCache.has(cacheKey)) {
|
|
376
431
|
execCache.set(
|
|
@@ -443,19 +498,33 @@ function assertSourceAccess(
|
|
|
443
498
|
}
|
|
444
499
|
}
|
|
445
500
|
|
|
446
|
-
function ownerScope(
|
|
501
|
+
function ownerScope(
|
|
502
|
+
access: DebugAccess,
|
|
503
|
+
ownerEmail?: string,
|
|
504
|
+
column = "owner_email",
|
|
505
|
+
): OwnerScope {
|
|
447
506
|
const requested = ownerEmail?.trim();
|
|
448
507
|
if (!access.canInspectAll) {
|
|
449
508
|
return {
|
|
450
|
-
sql:
|
|
509
|
+
sql: `${column} = ?`,
|
|
451
510
|
args: [access.viewerEmail],
|
|
452
511
|
label: access.viewerEmail,
|
|
453
512
|
};
|
|
454
513
|
}
|
|
455
514
|
|
|
456
515
|
if (requested && requested !== "*") {
|
|
516
|
+
if (
|
|
517
|
+
access.orgId &&
|
|
518
|
+
!access.memberEmails.some(
|
|
519
|
+
(email) => email.toLowerCase() === requested.toLowerCase(),
|
|
520
|
+
)
|
|
521
|
+
) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
"The requested owner is not a member of the current organization.",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
457
526
|
return {
|
|
458
|
-
sql:
|
|
527
|
+
sql: `${column} = ?`,
|
|
459
528
|
args: [requested],
|
|
460
529
|
label: requested,
|
|
461
530
|
};
|
|
@@ -468,14 +537,14 @@ function ownerScope(access: DebugAccess, ownerEmail?: string): OwnerScope {
|
|
|
468
537
|
const emails = access.memberEmails;
|
|
469
538
|
if (emails.length === 0) {
|
|
470
539
|
return {
|
|
471
|
-
sql:
|
|
540
|
+
sql: `${column} = ?`,
|
|
472
541
|
args: [access.viewerEmail],
|
|
473
542
|
label: access.viewerEmail,
|
|
474
543
|
};
|
|
475
544
|
}
|
|
476
545
|
const placeholders = emails.map(() => "?").join(", ");
|
|
477
546
|
return {
|
|
478
|
-
sql:
|
|
547
|
+
sql: `${column} IN (${placeholders})`,
|
|
479
548
|
args: emails,
|
|
480
549
|
label: access.orgId ? "current organization" : "all users",
|
|
481
550
|
};
|
|
@@ -498,11 +567,20 @@ function serializeRun(row: AgentRunRow, events: any[] = []) {
|
|
|
498
567
|
return {
|
|
499
568
|
id: String(row.id),
|
|
500
569
|
threadId: String(row.thread_id),
|
|
570
|
+
turnId: row.turn_id ? String(row.turn_id) : null,
|
|
501
571
|
status: String(row.status),
|
|
502
572
|
abortReason: row.abort_reason ? String(row.abort_reason) : null,
|
|
573
|
+
errorCode: row.error_code ? String(row.error_code) : null,
|
|
574
|
+
errorDetail: row.error_detail ? String(row.error_detail) : null,
|
|
575
|
+
terminalReason: row.terminal_reason ? String(row.terminal_reason) : null,
|
|
503
576
|
startedAt: numberField(row.started_at),
|
|
504
577
|
completedAt: nullableNumberField(row.completed_at),
|
|
505
578
|
heartbeatAt: nullableNumberField(row.heartbeat_at),
|
|
579
|
+
lastProgressAt: nullableNumberField(row.last_progress_at),
|
|
580
|
+
dispatchMode: row.dispatch_mode ? String(row.dispatch_mode) : null,
|
|
581
|
+
workerStage: row.worker_stage ? String(row.worker_stage) : null,
|
|
582
|
+
diagStage: row.diag_stage ? String(row.diag_stage) : null,
|
|
583
|
+
peakRssMb: nullableNumberField(row.peak_rss_mb),
|
|
506
584
|
events,
|
|
507
585
|
};
|
|
508
586
|
}
|
|
@@ -547,6 +625,224 @@ export async function listThreadDebugSources(): Promise<{
|
|
|
547
625
|
};
|
|
548
626
|
}
|
|
549
627
|
|
|
628
|
+
function publicSource(source: ThreadDebugSourceConfig) {
|
|
629
|
+
return {
|
|
630
|
+
id: source.id,
|
|
631
|
+
label: source.label,
|
|
632
|
+
kind: source.kind,
|
|
633
|
+
databaseUrlEnv: source.databaseUrlEnv ?? null,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function parseTerminalEvent(value: unknown): Record<string, unknown> | null {
|
|
638
|
+
if (value == null || value === "") return null;
|
|
639
|
+
try {
|
|
640
|
+
const parsed = JSON.parse(String(value));
|
|
641
|
+
return parsed && typeof parsed === "object"
|
|
642
|
+
? (parsed as Record<string, unknown>)
|
|
643
|
+
: { type: "unparseable" };
|
|
644
|
+
} catch {
|
|
645
|
+
return { type: "unparseable" };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function serializeRunFailure(
|
|
650
|
+
row: AgentRunFailureRow,
|
|
651
|
+
source: ThreadDebugSourceConfig,
|
|
652
|
+
) {
|
|
653
|
+
const startedAt = numberField(row.started_at);
|
|
654
|
+
const completedAt = nullableNumberField(row.completed_at);
|
|
655
|
+
return {
|
|
656
|
+
source: publicSource(source),
|
|
657
|
+
id: String(row.id),
|
|
658
|
+
threadId: String(row.thread_id),
|
|
659
|
+
turnId: row.turn_id ? String(row.turn_id) : null,
|
|
660
|
+
ownerEmail: String(row.debug_owner_email ?? ""),
|
|
661
|
+
threadTitle: String(row.debug_thread_title ?? ""),
|
|
662
|
+
threadPreview: String(row.debug_thread_preview ?? ""),
|
|
663
|
+
status: String(row.status) as AgentRunFailureStatus,
|
|
664
|
+
abortReason: row.abort_reason ? String(row.abort_reason) : null,
|
|
665
|
+
errorCode: row.error_code ? String(row.error_code) : null,
|
|
666
|
+
errorDetail: row.error_detail ? String(row.error_detail) : null,
|
|
667
|
+
terminalReason: row.terminal_reason ? String(row.terminal_reason) : null,
|
|
668
|
+
startedAt,
|
|
669
|
+
completedAt,
|
|
670
|
+
heartbeatAt: nullableNumberField(row.heartbeat_at),
|
|
671
|
+
lastProgressAt: nullableNumberField(row.last_progress_at),
|
|
672
|
+
durationMs: completedAt == null ? null : completedAt - startedAt,
|
|
673
|
+
dispatchMode: row.dispatch_mode ? String(row.dispatch_mode) : null,
|
|
674
|
+
workerStage: row.worker_stage ? String(row.worker_stage) : null,
|
|
675
|
+
diagStage: row.diag_stage ? String(row.diag_stage) : null,
|
|
676
|
+
peakRssMb: nullableNumberField(row.peak_rss_mb),
|
|
677
|
+
terminalEvent: parseTerminalEvent(row.debug_terminal_event_data),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function sourceHealth(
|
|
682
|
+
source: ThreadDebugSourceConfig,
|
|
683
|
+
status: ThreadDebugSourceHealthStatus,
|
|
684
|
+
failureCount: number,
|
|
685
|
+
errorCode:
|
|
686
|
+
| null
|
|
687
|
+
| "thread_debug_source_disconnected"
|
|
688
|
+
| "thread_debug_schema_unsupported"
|
|
689
|
+
| "thread_debug_source_unavailable",
|
|
690
|
+
) {
|
|
691
|
+
return {
|
|
692
|
+
source: publicSource(source),
|
|
693
|
+
status,
|
|
694
|
+
failureCount,
|
|
695
|
+
errorCode,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async function failuresForSource(
|
|
700
|
+
source: ThreadDebugSourceConfig,
|
|
701
|
+
scope: OwnerScope,
|
|
702
|
+
input: {
|
|
703
|
+
status: AgentRunFailureStatus | "all";
|
|
704
|
+
cutoff: number;
|
|
705
|
+
limit: number;
|
|
706
|
+
},
|
|
707
|
+
) {
|
|
708
|
+
const exec = await execForSource(source);
|
|
709
|
+
const statuses =
|
|
710
|
+
input.status === "all" ? [...UNSUCCESSFUL_RUN_STATUSES] : [input.status];
|
|
711
|
+
const statusPlaceholders = statuses.map(() => "?").join(", ");
|
|
712
|
+
const rows = await queryRows<AgentRunFailureRow>(
|
|
713
|
+
exec,
|
|
714
|
+
`SELECT r.*,
|
|
715
|
+
t.owner_email AS debug_owner_email,
|
|
716
|
+
t.title AS debug_thread_title,
|
|
717
|
+
t.preview AS debug_thread_preview,
|
|
718
|
+
(
|
|
719
|
+
SELECT e.event_data
|
|
720
|
+
FROM agent_run_events e
|
|
721
|
+
WHERE e.run_id = r.id
|
|
722
|
+
ORDER BY e.seq DESC
|
|
723
|
+
LIMIT 1
|
|
724
|
+
) AS debug_terminal_event_data
|
|
725
|
+
FROM agent_runs r
|
|
726
|
+
JOIN chat_threads t ON t.id = r.thread_id
|
|
727
|
+
WHERE r.status IN (${statusPlaceholders})
|
|
728
|
+
AND ${scope.sql}
|
|
729
|
+
AND COALESCE(r.completed_at, r.started_at) >= ?
|
|
730
|
+
ORDER BY COALESCE(r.completed_at, r.started_at) DESC, r.id DESC
|
|
731
|
+
LIMIT ?`,
|
|
732
|
+
[...statuses, ...scope.args, input.cutoff, input.limit],
|
|
733
|
+
);
|
|
734
|
+
return rows.map((row) => serializeRunFailure(row, source));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export async function listAgentRunFailures(input: {
|
|
738
|
+
sourceId?: string;
|
|
739
|
+
ownerEmail?: string;
|
|
740
|
+
status?: AgentRunFailureStatus | "all";
|
|
741
|
+
lookbackHours?: number;
|
|
742
|
+
limit?: number;
|
|
743
|
+
}) {
|
|
744
|
+
const access = await resolveDebugAccess();
|
|
745
|
+
const requestedSourceId = input.sourceId?.trim() || "all";
|
|
746
|
+
const status = input.status ?? "all";
|
|
747
|
+
const lookbackHours = Math.max(1, Math.min(720, input.lookbackHours ?? 168));
|
|
748
|
+
const limit = Math.max(1, Math.min(100, input.limit ?? DEFAULT_SEARCH_LIMIT));
|
|
749
|
+
const scope = ownerScope(access, input.ownerEmail, "t.owner_email");
|
|
750
|
+
const cutoff = Date.now() - lookbackHours * 60 * 60 * 1000;
|
|
751
|
+
|
|
752
|
+
let sources: ThreadDebugSourceConfig[];
|
|
753
|
+
if (requestedSourceId === "all") {
|
|
754
|
+
sources = sourceConfigs().filter(
|
|
755
|
+
(source) => source.kind === "current" || access.canInspectAll,
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
const configured = sourceConfigs().find(
|
|
759
|
+
(source) => source.id === requestedSourceId,
|
|
760
|
+
);
|
|
761
|
+
const source = configured ?? resolveSourceConfig(requestedSourceId);
|
|
762
|
+
assertSourceAccess(source, access);
|
|
763
|
+
if (source.kind !== "current" && !source.databaseUrl) {
|
|
764
|
+
throw new Error(
|
|
765
|
+
`Thread debug source "${requestedSourceId}" is configured but disconnected.`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
sources = [source];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const results = await Promise.all(
|
|
772
|
+
sources.map(async (source) => {
|
|
773
|
+
if (source.kind !== "current" && !source.databaseUrl) {
|
|
774
|
+
return {
|
|
775
|
+
failures: [] as ReturnType<typeof serializeRunFailure>[],
|
|
776
|
+
health: sourceHealth(
|
|
777
|
+
source,
|
|
778
|
+
"disconnected",
|
|
779
|
+
0,
|
|
780
|
+
"thread_debug_source_disconnected",
|
|
781
|
+
),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const failures = await failuresForSource(source, scope, {
|
|
786
|
+
status,
|
|
787
|
+
cutoff,
|
|
788
|
+
limit,
|
|
789
|
+
});
|
|
790
|
+
return {
|
|
791
|
+
failures,
|
|
792
|
+
health: sourceHealth(source, "ok", failures.length, null),
|
|
793
|
+
};
|
|
794
|
+
} catch (error) {
|
|
795
|
+
if (error instanceof UnsupportedThreadDebugSchemaError) {
|
|
796
|
+
return {
|
|
797
|
+
failures: [] as ReturnType<typeof serializeRunFailure>[],
|
|
798
|
+
health: sourceHealth(
|
|
799
|
+
source,
|
|
800
|
+
"unsupported",
|
|
801
|
+
0,
|
|
802
|
+
"thread_debug_schema_unsupported",
|
|
803
|
+
),
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
failures: [] as ReturnType<typeof serializeRunFailure>[],
|
|
808
|
+
health: sourceHealth(
|
|
809
|
+
source,
|
|
810
|
+
"unavailable",
|
|
811
|
+
0,
|
|
812
|
+
"thread_debug_source_unavailable",
|
|
813
|
+
),
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}),
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
const failures = results
|
|
820
|
+
.flatMap((result) => result.failures)
|
|
821
|
+
.sort(
|
|
822
|
+
(a, b) =>
|
|
823
|
+
(b.completedAt ?? b.startedAt) - (a.completedAt ?? a.startedAt) ||
|
|
824
|
+
b.id.localeCompare(a.id) ||
|
|
825
|
+
b.source.id.localeCompare(a.source.id),
|
|
826
|
+
)
|
|
827
|
+
.slice(0, limit);
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
sourceId: requestedSourceId,
|
|
831
|
+
status,
|
|
832
|
+
lookbackHours,
|
|
833
|
+
limit,
|
|
834
|
+
count: failures.length,
|
|
835
|
+
partial: results.some((result) => result.health.status !== "ok"),
|
|
836
|
+
access: {
|
|
837
|
+
viewerEmail: access.viewerEmail,
|
|
838
|
+
scope: scope.label,
|
|
839
|
+
canInspectAll: access.canInspectAll,
|
|
840
|
+
},
|
|
841
|
+
sources: results.map((result) => result.health),
|
|
842
|
+
failures,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
550
846
|
export async function searchAgentThreads(input: {
|
|
551
847
|
sourceId?: string;
|
|
552
848
|
query?: string;
|
|
@@ -694,10 +990,10 @@ export async function getAgentThreadDebug(input: {
|
|
|
694
990
|
|
|
695
991
|
const runRows = await optionalRows<AgentRunRow>(
|
|
696
992
|
exec,
|
|
697
|
-
`SELECT
|
|
698
|
-
FROM agent_runs
|
|
699
|
-
WHERE thread_id = ?
|
|
700
|
-
ORDER BY started_at DESC
|
|
993
|
+
`SELECT r.*
|
|
994
|
+
FROM agent_runs r
|
|
995
|
+
WHERE r.thread_id = ?
|
|
996
|
+
ORDER BY r.started_at DESC
|
|
701
997
|
LIMIT ?`,
|
|
702
998
|
[row.id, maxRuns],
|
|
703
999
|
);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
createAgentChatPlugin: vi.fn((options: Record<string, unknown>) => options),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@agent-native/core/server", () => ({
|
|
8
|
+
createAgentChatPlugin: mocks.createAgentChatPlugin,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@agent-native/core/org", () => ({
|
|
12
|
+
getOrgContext: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("../../actions/index.js", () => ({
|
|
16
|
+
dispatchActions: {},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("Dispatch agent chat plugin", () => {
|
|
20
|
+
it("opts delegated work into the durable background run contract", async () => {
|
|
21
|
+
await import("./agent-chat.js");
|
|
22
|
+
|
|
23
|
+
expect(mocks.createAgentChatPlugin).toHaveBeenCalledWith(
|
|
24
|
+
expect.objectContaining({
|
|
25
|
+
appId: "dispatch",
|
|
26
|
+
durableBackgroundRuns: true,
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -29,6 +29,7 @@ const INITIAL_TOOL_NAMES = [
|
|
|
29
29
|
|
|
30
30
|
export default createAgentChatPlugin({
|
|
31
31
|
appId: "dispatch",
|
|
32
|
+
durableBackgroundRuns: true,
|
|
32
33
|
initialToolNames: INITIAL_TOOL_NAMES,
|
|
33
34
|
connectorCatalog: ["resolve-integration-source-context"],
|
|
34
35
|
// Without this, AGENT_ORG_ID is never set on agent action calls and every
|