@agent-native/dispatch 0.15.23 → 0.15.25
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 +7 -1
- package/dist/actions/get-agent-thread-debug.js +16 -3
- package/dist/actions/get-agent-thread-debug.js.map +1 -1
- package/dist/actions/list-curated-workspace-templates.js +1 -1
- package/dist/actions/list-curated-workspace-templates.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/actions/remix-workspace-template.js +5 -5
- package/dist/actions/remix-workspace-template.js.map +1 -1
- package/dist/actions/search-agent-threads.js +1 -1
- package/dist/actions/search-agent-threads.js.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +1 -1
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/components/create-app-popover.js +2 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/workspace-template-card.js +4 -4
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +25 -11
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/server/lib/app-creation-store.js +2 -2
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/curated-workspace-templates.d.ts +1 -1
- package/dist/server/lib/curated-workspace-templates.js +2 -2
- package/dist/server/lib/curated-workspace-templates.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts +7 -1
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
- package/dist/server/lib/thread-debug-store.js +43 -5
- package/dist/server/lib/thread-debug-store.js.map +1 -1
- package/dist/server/plugins/integrations.js +1 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/get-agent-thread-debug.ts +30 -15
- package/src/actions/list-curated-workspace-templates.ts +1 -1
- package/src/actions/remix-workspace-template.spec.ts +2 -2
- package/src/actions/remix-workspace-template.ts +5 -5
- package/src/actions/search-agent-threads.ts +1 -1
- package/src/actions/start-workspace-app-creation.ts +1 -1
- package/src/components/create-app-popover.tsx +2 -2
- package/src/components/workspace-template-card.spec.tsx +4 -4
- package/src/components/workspace-template-card.tsx +4 -4
- package/src/routes/pages/thread-debug.tsx +34 -14
- package/src/server/lib/app-creation-store.ts +2 -2
- package/src/server/lib/curated-workspace-templates.ts +2 -2
- package/src/server/lib/thread-debug-store.spec.ts +82 -0
- package/src/server/lib/thread-debug-store.ts +62 -6
- package/src/server/plugins/integrations.ts +1 -1
|
@@ -17,11 +17,11 @@ function buildRemixPrompt(input: {
|
|
|
17
17
|
description?: string | null;
|
|
18
18
|
}): string {
|
|
19
19
|
return [
|
|
20
|
-
"Create a private workspace
|
|
20
|
+
"Create a private workspace app from a curated first-party template.",
|
|
21
21
|
`Source template: ${input.templateName} (${input.templateId}).`,
|
|
22
22
|
"Recreate the source template's product shape and capabilities as an independent workspace app.",
|
|
23
23
|
"Never copy source-app data, records, user content, secrets, credentials, tokens, API keys, or private configuration.",
|
|
24
|
-
"Use empty or synthetic seed data only, keep the
|
|
24
|
+
"Use empty or synthetic seed data only, keep the new app private to the current workspace, and do not create a public demo.",
|
|
25
25
|
`Setup note: ${input.setupNote}`,
|
|
26
26
|
input.description?.trim()
|
|
27
27
|
? `Requested customization: ${input.description.trim()}`
|
|
@@ -31,7 +31,7 @@ function buildRemixPrompt(input: {
|
|
|
31
31
|
|
|
32
32
|
export default defineAction({
|
|
33
33
|
description:
|
|
34
|
-
"Create a private, independent
|
|
34
|
+
"Create a private, independent app from one of Dispatch's curated first-party workspace templates. Valid templates are mail, calendar, analytics, slides, content, clips, brain, assets, forms, and design. The new app must not copy source-app data, secrets, credentials, or private configuration; local workspaces scaffold the template, while hosted workspaces start a Builder app-creation branch.",
|
|
35
35
|
schema: z.object({
|
|
36
36
|
templateId: z
|
|
37
37
|
.string()
|
|
@@ -56,7 +56,7 @@ export default defineAction({
|
|
|
56
56
|
.max(500)
|
|
57
57
|
.optional()
|
|
58
58
|
.nullable()
|
|
59
|
-
.describe("Optional customization or purpose for the private
|
|
59
|
+
.describe("Optional customization or purpose for the private app."),
|
|
60
60
|
}),
|
|
61
61
|
run: async (input) => {
|
|
62
62
|
const sourceTemplate = getCuratedWorkspaceTemplate(input.templateId);
|
|
@@ -83,7 +83,7 @@ export default defineAction({
|
|
|
83
83
|
action: "workspace-app.remix-requested",
|
|
84
84
|
targetType: "workspace-app",
|
|
85
85
|
targetId: appId,
|
|
86
|
-
summary: `Requested private
|
|
86
|
+
summary: `Requested private app from ${sourceTemplate.name}`,
|
|
87
87
|
metadata: {
|
|
88
88
|
sourceTemplate: sourceTemplate.template,
|
|
89
89
|
mode:
|
|
@@ -5,7 +5,7 @@ import { searchAgentThreads } from "../server/lib/thread-debug-store.js";
|
|
|
5
5
|
|
|
6
6
|
export default defineAction({
|
|
7
7
|
description:
|
|
8
|
-
"Search agent chat threads by title, preview,
|
|
8
|
+
"Search agent chat threads by title, preview, full persisted thread content, or an exact request/run ID. Non-admins are limited to their own current Dispatch DB threads.",
|
|
9
9
|
schema: z.object({
|
|
10
10
|
sourceId: z
|
|
11
11
|
.string()
|
|
@@ -6,7 +6,7 @@ import { startWorkspaceAppCreation } from "../server/lib/app-creation-store.js";
|
|
|
6
6
|
|
|
7
7
|
export default defineAction({
|
|
8
8
|
description:
|
|
9
|
-
'Start creating a new workspace app from Dispatch when the request truly needs its own app. Callers should include a concise generated description by default; Dispatch generates one from the prompt when omitted. In local dev this returns a code-agent prompt; in production it creates a Builder branch only when a Builder branch project is configured. If the result mode is "coming-soon", the work still requires a code change but no Builder Cloud Agent can run here; tell the user to edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like, and do not send them to Builder org/beta settings. The result must be a separate workspace app under apps/<app-id>, not a new route or file in apps/chat. If chat is used as the source template, the finished app must be branded as the requested app and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind. If the request needs Mail, Calendar, Analytics, Brain, Assets, or another first-party app, use the existing hosted/connected app via links or A2A; do not
|
|
9
|
+
'Start creating a new workspace app from Dispatch when the request truly needs its own app. Callers should include a concise generated description by default; Dispatch generates one from the prompt when omitted. In local dev this returns a code-agent prompt; in production it creates a Builder branch only when a Builder branch project is configured. If the result mode is "coming-soon", the work still requires a code change but no Builder Cloud Agent can run here; tell the user to edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like, and do not send them to Builder org/beta settings. The result must be a separate workspace app under apps/<app-id>, not a new route or file in apps/chat. If chat is used as the source template, the finished app must be branded as the requested app and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind. If the request needs Mail, Calendar, Analytics, Brain, Assets, or another first-party app, use the existing hosted/connected app via links or A2A; do not wrap or nest those apps inside the new app unless the user explicitly asks for a customized app from that template.',
|
|
10
10
|
schema: z.object({
|
|
11
11
|
prompt: z.string().min(1).describe("The user's app creation request"),
|
|
12
12
|
appId: z
|
|
@@ -117,8 +117,8 @@ function buildAppCreationPrompt(input: {
|
|
|
117
117
|
`Use relative workspace links like /${input.appId}. Do not hardcode localhost, 127.0.0.1, 8080, 8100, or any dev port; the active workspace gateway/browser origin owns the port.`,
|
|
118
118
|
`Use the framework/template UI stack: shadcn/ui components and @tabler/icons-react. Do not add lucide-react or another icon library for standard UI.`,
|
|
119
119
|
`Existing first-party apps are neighbors, not implementation details for this app. If the user's prompt mentions Mail, Calendar, Analytics, Brain, Assets, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, Analytics, Brain, and Assets already exist at https://mail.agent-native.com, https://calendar.agent-native.com, https://analytics.agent-native.com, https://brain.agent-native.com, and https://assets.agent-native.com.`,
|
|
120
|
-
`Do not
|
|
121
|
-
`Only create another first-party app
|
|
120
|
+
`Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, Brain, Assets, etc. inside apps/${input.appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
|
|
121
|
+
`Only create another first-party app when the user explicitly asks for a customized app from that template; otherwise keep using the hosted/shared app so improvements to the base app keep flowing to users.`,
|
|
122
122
|
`Do not satisfy this by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app.`,
|
|
123
123
|
input.vaultAccessMode === "all-apps"
|
|
124
124
|
? `Do not create per-app Dispatch vault grants unless the workspace switches vault access to manual or the user explicitly asks for manual grants.`
|
|
@@ -95,7 +95,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
95
95
|
).toBe(null);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it("
|
|
98
|
+
it("creates an app with the default app id and allows an override", async () => {
|
|
99
99
|
await act(async () => {
|
|
100
100
|
root.render(
|
|
101
101
|
<WorkspaceTemplateCard template={template} defaultAppId="pipeline" />,
|
|
@@ -103,7 +103,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
const trigger = Array.from(container.querySelectorAll("button")).find(
|
|
106
|
-
(button) => button.textContent?.includes("
|
|
106
|
+
(button) => button.textContent?.includes("Create from template"),
|
|
107
107
|
);
|
|
108
108
|
expect(trigger).not.toBeUndefined();
|
|
109
109
|
|
|
@@ -140,7 +140,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
expect(toast.success).toHaveBeenCalledWith(
|
|
143
|
-
"Template
|
|
143
|
+
"Template app creation started.",
|
|
144
144
|
);
|
|
145
145
|
});
|
|
146
146
|
|
|
@@ -167,7 +167,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
167
167
|
expect(container.textContent).toContain("Installed");
|
|
168
168
|
expect(container.querySelector('a[href^="https://"]')).toBeNull();
|
|
169
169
|
const remixButton = Array.from(container.querySelectorAll("button")).find(
|
|
170
|
-
(button) => button.textContent?.includes("
|
|
170
|
+
(button) => button.textContent?.includes("Create from template"),
|
|
171
171
|
);
|
|
172
172
|
expect(remixButton).not.toHaveProperty("disabled", true);
|
|
173
173
|
});
|
|
@@ -78,10 +78,10 @@ const DEFAULT_LABELS: WorkspaceTemplateLabels = {
|
|
|
78
78
|
cancel: "Cancel",
|
|
79
79
|
integrationSetup: "Integration setup",
|
|
80
80
|
installed: "Installed",
|
|
81
|
-
remix: "
|
|
82
|
-
remixing: "
|
|
83
|
-
remixSuccess: "Template
|
|
84
|
-
remixError: "Could not
|
|
81
|
+
remix: "Create from template",
|
|
82
|
+
remixing: "Creating app…",
|
|
83
|
+
remixSuccess: "Template app creation started.",
|
|
84
|
+
remixError: "Could not create an app from this template",
|
|
85
85
|
appIdRequired: "App ID is required.",
|
|
86
86
|
source: "Source",
|
|
87
87
|
viewLiveApp: "View the live app",
|
|
@@ -87,6 +87,7 @@ interface ThreadDebugResponse {
|
|
|
87
87
|
};
|
|
88
88
|
access: { viewerEmail: string; scope: string; canInspectAll: boolean };
|
|
89
89
|
thread: ThreadSearchResult;
|
|
90
|
+
lookup?: { requestedId: string; threadId: string; runId: string | null };
|
|
90
91
|
messages: ThreadMessage[];
|
|
91
92
|
debug: any;
|
|
92
93
|
debugRuns: any[];
|
|
@@ -424,7 +425,7 @@ export default function ThreadDebugRoute() {
|
|
|
424
425
|
const [sourceId, setSourceId] = useState(initialSourceId);
|
|
425
426
|
const [query, setQuery] = useState(initialQuery);
|
|
426
427
|
const [ownerEmail, setOwnerEmail] = useState(initialOwnerEmail);
|
|
427
|
-
const [
|
|
428
|
+
const [lookupId, setLookupId] = useState("");
|
|
428
429
|
const [submittedSearch, setSubmittedSearch] = useState({
|
|
429
430
|
sourceId: initialSourceId,
|
|
430
431
|
query: initialQuery,
|
|
@@ -432,7 +433,8 @@ export default function ThreadDebugRoute() {
|
|
|
432
433
|
});
|
|
433
434
|
const [selected, setSelected] = useState<{
|
|
434
435
|
sourceId: string;
|
|
435
|
-
|
|
436
|
+
lookupId: string;
|
|
437
|
+
lookupKind: "thread" | "run";
|
|
436
438
|
ownerEmail?: string;
|
|
437
439
|
} | null>(null);
|
|
438
440
|
|
|
@@ -475,7 +477,9 @@ export default function ThreadDebugRoute() {
|
|
|
475
477
|
const detailParams = useMemo(
|
|
476
478
|
() => ({
|
|
477
479
|
sourceId: selected?.sourceId ?? "current",
|
|
478
|
-
|
|
480
|
+
...(selected?.lookupKind === "run"
|
|
481
|
+
? { runId: selected.lookupId }
|
|
482
|
+
: { threadId: selected?.lookupId ?? "" }),
|
|
479
483
|
ownerEmail: selected?.ownerEmail,
|
|
480
484
|
maxRuns: 20,
|
|
481
485
|
maxEvents: 800,
|
|
@@ -492,7 +496,7 @@ export default function ThreadDebugRoute() {
|
|
|
492
496
|
"get-agent-thread-debug",
|
|
493
497
|
detailParams,
|
|
494
498
|
{
|
|
495
|
-
enabled: Boolean(selected?.
|
|
499
|
+
enabled: Boolean(selected?.lookupId),
|
|
496
500
|
},
|
|
497
501
|
);
|
|
498
502
|
|
|
@@ -512,10 +516,21 @@ export default function ThreadDebugRoute() {
|
|
|
512
516
|
sourceId,
|
|
513
517
|
query,
|
|
514
518
|
ownerEmail: ownerEmail.trim() || undefined,
|
|
515
|
-
threadId:
|
|
519
|
+
threadId:
|
|
520
|
+
selected?.lookupKind === "thread"
|
|
521
|
+
? selected.lookupId
|
|
522
|
+
: !selected && lookupId.trim() && !lookupId.startsWith("run-")
|
|
523
|
+
? lookupId.trim()
|
|
524
|
+
: undefined,
|
|
525
|
+
runId:
|
|
526
|
+
selected?.lookupKind === "run"
|
|
527
|
+
? selected.lookupId
|
|
528
|
+
: !selected && lookupId.trim() && lookupId.startsWith("run-")
|
|
529
|
+
? lookupId.trim()
|
|
530
|
+
: undefined,
|
|
516
531
|
}),
|
|
517
532
|
}).catch(() => {});
|
|
518
|
-
}, [ownerEmail, query, selected
|
|
533
|
+
}, [ownerEmail, query, selected, sourceId, lookupId]);
|
|
519
534
|
|
|
520
535
|
return (
|
|
521
536
|
<DispatchShell
|
|
@@ -573,20 +588,21 @@ export default function ThreadDebugRoute() {
|
|
|
573
588
|
|
|
574
589
|
<div className="mt-3 grid gap-3 lg:grid-cols-[1fr_auto]">
|
|
575
590
|
<Input
|
|
576
|
-
value={
|
|
577
|
-
onChange={(event) =>
|
|
578
|
-
placeholder="Paste thread ID"
|
|
591
|
+
value={lookupId}
|
|
592
|
+
onChange={(event) => setLookupId(event.target.value)}
|
|
593
|
+
placeholder="Paste thread or request/run ID"
|
|
579
594
|
className="font-mono"
|
|
580
595
|
/>
|
|
581
596
|
<Button
|
|
582
597
|
type="button"
|
|
583
598
|
variant="outline"
|
|
584
599
|
onClick={() => {
|
|
585
|
-
const trimmed =
|
|
600
|
+
const trimmed = lookupId.trim();
|
|
586
601
|
if (!trimmed) return;
|
|
587
602
|
setSelected({
|
|
588
603
|
sourceId,
|
|
589
|
-
|
|
604
|
+
lookupId: trimmed,
|
|
605
|
+
lookupKind: trimmed.startsWith("run-") ? "run" : "thread",
|
|
590
606
|
ownerEmail: ownerEmail.trim() || undefined,
|
|
591
607
|
});
|
|
592
608
|
}}
|
|
@@ -660,11 +676,15 @@ export default function ThreadDebugRoute() {
|
|
|
660
676
|
<ResultCard
|
|
661
677
|
key={result.id}
|
|
662
678
|
result={result}
|
|
663
|
-
selected={
|
|
679
|
+
selected={
|
|
680
|
+
selected?.lookupKind === "thread" &&
|
|
681
|
+
selected.lookupId === result.id
|
|
682
|
+
}
|
|
664
683
|
onSelect={() =>
|
|
665
684
|
setSelected({
|
|
666
685
|
sourceId: submittedSearch.sourceId,
|
|
667
|
-
|
|
686
|
+
lookupId: result.id,
|
|
687
|
+
lookupKind: "thread",
|
|
668
688
|
ownerEmail: submittedSearch.ownerEmail || undefined,
|
|
669
689
|
})
|
|
670
690
|
}
|
|
@@ -691,7 +711,7 @@ export default function ThreadDebugRoute() {
|
|
|
691
711
|
) : (
|
|
692
712
|
<div className="flex min-h-[520px] flex-col items-center justify-center rounded-lg border border-dashed bg-card px-4 text-center text-sm text-muted-foreground">
|
|
693
713
|
<IconFileSearch className="mb-2 h-5 w-5" />
|
|
694
|
-
Select or inspect a thread.
|
|
714
|
+
Select or inspect a thread or request/run ID.
|
|
695
715
|
</div>
|
|
696
716
|
)}
|
|
697
717
|
</section>
|
|
@@ -1765,8 +1765,8 @@ function buildWorkspaceAppPrompt(input: {
|
|
|
1765
1765
|
`Use the workspace app layout: create it under apps/${appId}, mount it at /${appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
|
|
1766
1766
|
`Important routing rule: from outside the app, link to /${appId}; inside apps/${appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${appId}/${appId}/review.`,
|
|
1767
1767
|
"Existing first-party apps are neighbors, not implementation details for this app. If the user prompt mentions Mail, Calendar, Analytics, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, and Analytics already exist at https://mail.agent-native.com, https://calendar.agent-native.com, and https://analytics.agent-native.com.",
|
|
1768
|
-
`Do not
|
|
1769
|
-
"Only create another first-party app
|
|
1768
|
+
`Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, etc. inside apps/${appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
|
|
1769
|
+
"Only create another first-party app when the user explicitly asks for a customized app from that template; otherwise keep using the hosted/shared app so improvements to the base app keep flowing to users.",
|
|
1770
1770
|
selectedKeys.length
|
|
1771
1771
|
? `Dispatch will create pending vault requests for the selected keys for appId "${appId}" after this app creation request is accepted. Do not grant or sync vault keys directly from the app-creation branch.`
|
|
1772
1772
|
: "Do not grant or request any Dispatch vault keys unless the user asks later.",
|
|
@@ -17,7 +17,7 @@ export interface CuratedWorkspaceTemplateStatus extends CuratedWorkspaceTemplate
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Stable first-party template metadata for the initial
|
|
20
|
+
* Stable first-party template metadata for the initial app-creation catalog.
|
|
21
21
|
* `liveUrl` identifies the product URL; it is not a public-demo claim.
|
|
22
22
|
*/
|
|
23
23
|
export const CURATED_WORKSPACE_TEMPLATES: readonly CuratedWorkspaceTemplate[] =
|
|
@@ -85,7 +85,7 @@ export const CURATED_WORKSPACE_TEMPLATES: readonly CuratedWorkspaceTemplate[] =
|
|
|
85
85
|
liveUrl: "https://content.agent-native.com",
|
|
86
86
|
category: "content",
|
|
87
87
|
setupNote:
|
|
88
|
-
"Choose a content folder or create a first document for the private
|
|
88
|
+
"Choose a content folder or create a first document for the private app.",
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
id: "clips",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
execute: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("@agent-native/core/db", () => ({
|
|
8
|
+
createDbExec: vi.fn(),
|
|
9
|
+
getDbExec: () => ({ execute: mocks.execute }),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("./dispatch-store.js", () => ({
|
|
13
|
+
currentOrgId: () => null,
|
|
14
|
+
currentOwnerEmail: () => "owner@example.com",
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
getAgentThreadDebug,
|
|
19
|
+
searchAgentThreads,
|
|
20
|
+
} from "./thread-debug-store.js";
|
|
21
|
+
|
|
22
|
+
const thread = {
|
|
23
|
+
id: "thread-1",
|
|
24
|
+
owner_email: "owner@example.com",
|
|
25
|
+
title: "A production run",
|
|
26
|
+
preview: "Investigate this run",
|
|
27
|
+
thread_data: JSON.stringify({ messages: [] }),
|
|
28
|
+
message_count: 0,
|
|
29
|
+
created_at: 1,
|
|
30
|
+
updated_at: 2,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const run = {
|
|
34
|
+
id: "run-prod-1",
|
|
35
|
+
thread_id: "thread-1",
|
|
36
|
+
status: "completed",
|
|
37
|
+
started_at: 1,
|
|
38
|
+
completed_at: 2,
|
|
39
|
+
heartbeat_at: 2,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function rowsForQuery(sql: string, args: unknown[]) {
|
|
43
|
+
if (sql.includes("FROM org_members")) return [];
|
|
44
|
+
if (sql.includes("FROM agent_runs") && sql.includes("WHERE id = ?")) {
|
|
45
|
+
return [run];
|
|
46
|
+
}
|
|
47
|
+
if (sql.includes("FROM agent_runs") && sql.includes("WHERE thread_id = ?")) {
|
|
48
|
+
return [run];
|
|
49
|
+
}
|
|
50
|
+
if (sql.includes("FROM chat_threads")) {
|
|
51
|
+
return args[0] === "thread-1" || args[0] === "owner@example.com"
|
|
52
|
+
? [thread]
|
|
53
|
+
: [];
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("thread-debug-store request/run lookup", () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
mocks.execute.mockImplementation(async ({ sql, args }) => ({
|
|
61
|
+
rows: rowsForQuery(sql, args),
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("finds a thread when search input is an exact run id", async () => {
|
|
66
|
+
const result = await searchAgentThreads({ query: run.id });
|
|
67
|
+
|
|
68
|
+
expect(result.threads).toHaveLength(1);
|
|
69
|
+
expect(result.threads[0]?.id).toBe(thread.id);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("resolves a run id before loading its debug snapshot", async () => {
|
|
73
|
+
const result = await getAgentThreadDebug({ runId: run.id });
|
|
74
|
+
|
|
75
|
+
expect(result.lookup).toEqual({
|
|
76
|
+
requestedId: run.id,
|
|
77
|
+
threadId: thread.id,
|
|
78
|
+
runId: run.id,
|
|
79
|
+
});
|
|
80
|
+
expect(result.thread.id).toBe(thread.id);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -565,12 +565,30 @@ export async function searchAgentThreads(input: {
|
|
|
565
565
|
const scope = ownerScope(access, input.ownerEmail);
|
|
566
566
|
const where = [scope.sql];
|
|
567
567
|
const args: unknown[] = [...scope.args];
|
|
568
|
+
const runThreadIds = q
|
|
569
|
+
? (
|
|
570
|
+
await optionalRows<{ thread_id: string }>(
|
|
571
|
+
exec,
|
|
572
|
+
"SELECT thread_id FROM agent_runs WHERE id = ? LIMIT 1",
|
|
573
|
+
[q],
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
.map((row) => String(row.thread_id ?? "").trim())
|
|
577
|
+
.filter(Boolean)
|
|
578
|
+
: [];
|
|
568
579
|
if (q) {
|
|
569
|
-
const pattern =
|
|
580
|
+
const pattern = "%" + escapeLike(q.toLowerCase()) + "%";
|
|
581
|
+
const runIdClause =
|
|
582
|
+
runThreadIds.length > 0
|
|
583
|
+
? " OR id IN (" + runThreadIds.map(() => "?").join(", ") + ")"
|
|
584
|
+
: "";
|
|
570
585
|
where.push(
|
|
571
|
-
|
|
586
|
+
"(LOWER(title) LIKE ? ESCAPE '\\' OR LOWER(preview) LIKE ? ESCAPE '\\' OR LOWER(thread_data) LIKE ? ESCAPE '\\'" +
|
|
587
|
+
runIdClause +
|
|
588
|
+
")",
|
|
572
589
|
);
|
|
573
590
|
args.push(pattern, pattern, pattern);
|
|
591
|
+
args.push(...runThreadIds);
|
|
574
592
|
}
|
|
575
593
|
args.push(limit);
|
|
576
594
|
|
|
@@ -603,7 +621,8 @@ export async function searchAgentThreads(input: {
|
|
|
603
621
|
|
|
604
622
|
export async function getAgentThreadDebug(input: {
|
|
605
623
|
sourceId?: string;
|
|
606
|
-
threadId
|
|
624
|
+
threadId?: string;
|
|
625
|
+
runId?: string;
|
|
607
626
|
ownerEmail?: string;
|
|
608
627
|
maxRuns?: number;
|
|
609
628
|
maxEvents?: number;
|
|
@@ -614,17 +633,49 @@ export async function getAgentThreadDebug(input: {
|
|
|
614
633
|
assertSourceAccess(source, access);
|
|
615
634
|
const exec = await execForSource(source);
|
|
616
635
|
const scope = ownerScope(access, input.ownerEmail);
|
|
617
|
-
const
|
|
636
|
+
const requestedId = input.runId?.trim() || input.threadId?.trim() || "";
|
|
637
|
+
if (!requestedId) {
|
|
638
|
+
throw new Error("A thread ID or request/run ID is required.");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
let rows = await queryRows<ChatThreadRow>(
|
|
618
642
|
exec,
|
|
619
643
|
`SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at
|
|
620
644
|
FROM chat_threads
|
|
621
645
|
WHERE id = ? AND ${scope.sql}
|
|
622
646
|
LIMIT 1`,
|
|
623
|
-
[input.threadId, ...scope.args],
|
|
647
|
+
[input.threadId?.trim() || requestedId, ...scope.args],
|
|
624
648
|
);
|
|
649
|
+
let resolvedRunId = input.runId?.trim() || null;
|
|
650
|
+
|
|
651
|
+
if (!rows[0]) {
|
|
652
|
+
const runRows = await optionalRows<{ id: string; thread_id: string }>(
|
|
653
|
+
exec,
|
|
654
|
+
`SELECT id, thread_id
|
|
655
|
+
FROM agent_runs
|
|
656
|
+
WHERE id = ?
|
|
657
|
+
LIMIT 1`,
|
|
658
|
+
[requestedId],
|
|
659
|
+
);
|
|
660
|
+
const threadId = runRows[0]?.thread_id
|
|
661
|
+
? String(runRows[0].thread_id).trim()
|
|
662
|
+
: "";
|
|
663
|
+
if (threadId) {
|
|
664
|
+
rows = await queryRows<ChatThreadRow>(
|
|
665
|
+
exec,
|
|
666
|
+
`SELECT id, owner_email, title, preview, thread_data, message_count, created_at, updated_at
|
|
667
|
+
FROM chat_threads
|
|
668
|
+
WHERE id = ? AND ${scope.sql}
|
|
669
|
+
LIMIT 1`,
|
|
670
|
+
[threadId, ...scope.args],
|
|
671
|
+
);
|
|
672
|
+
if (rows[0]) resolvedRunId = requestedId;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
625
676
|
const row = rows[0];
|
|
626
677
|
if (!row) {
|
|
627
|
-
throw new Error(`Thread "${
|
|
678
|
+
throw new Error(`Thread or request/run ID "${requestedId}" was not found.`);
|
|
628
679
|
}
|
|
629
680
|
|
|
630
681
|
const threadData = safeJsonParse<Record<string, unknown>>(
|
|
@@ -763,6 +814,11 @@ export async function getAgentThreadDebug(input: {
|
|
|
763
814
|
createdAt: numberField(row.created_at),
|
|
764
815
|
updatedAt: numberField(row.updated_at),
|
|
765
816
|
},
|
|
817
|
+
lookup: {
|
|
818
|
+
requestedId,
|
|
819
|
+
threadId: String(row.id),
|
|
820
|
+
runId: resolvedRunId,
|
|
821
|
+
},
|
|
766
822
|
messages: normalizeMessages(threadData),
|
|
767
823
|
debug: (threadData as any)?._debug ?? null,
|
|
768
824
|
debugRuns: Array.isArray((threadData as any)?._debugRuns)
|
|
@@ -40,7 +40,7 @@ When a user asks for something:
|
|
|
40
40
|
- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.
|
|
41
41
|
- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.
|
|
42
42
|
- If the user asks to create, build, make, scaffold, or generate an "agent" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.
|
|
43
|
-
- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized
|
|
43
|
+
- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized app from that template.
|
|
44
44
|
- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind.
|
|
45
45
|
- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. If the request is too vague to classify, ask one concise follow-up. If the action returns mode "builder", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode "local-agent", tell the user it is ready for the local code agent and include the returned app path/prompt summary. If it returns mode "coming-soon", say this requires a code change and they can edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like; do not send them to Builder org/beta settings. If it returns mode "builder-unavailable", the action also returns a \`reason\` code plus a user-facing \`message\` and (for some reasons) an operator-facing \`detail\`. Relay the \`message\` to the user as-is; only surface \`detail\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user.
|
|
46
46
|
- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.
|