@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.
Files changed (38) hide show
  1. package/dist/actions/get-agent-thread-debug.d.ts +9 -0
  2. package/dist/actions/index.d.ts.map +1 -1
  3. package/dist/actions/index.js +2 -0
  4. package/dist/actions/index.js.map +1 -1
  5. package/dist/actions/list-agent-run-failures.d.ts +61 -0
  6. package/dist/actions/list-agent-run-failures.d.ts.map +1 -0
  7. package/dist/actions/list-agent-run-failures.js +38 -0
  8. package/dist/actions/list-agent-run-failures.js.map +1 -0
  9. package/dist/actions/provider-api-register.d.ts +12 -12
  10. package/dist/actions/upsert-destination.d.ts +12 -12
  11. package/dist/actions/view-screen.js +32 -5
  12. package/dist/actions/view-screen.js.map +1 -1
  13. package/dist/hooks/use-navigation-state.d.ts +6 -0
  14. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  15. package/dist/hooks/use-navigation-state.js +30 -0
  16. package/dist/hooks/use-navigation-state.js.map +1 -1
  17. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  18. package/dist/routes/pages/thread-debug.js +291 -77
  19. package/dist/routes/pages/thread-debug.js.map +1 -1
  20. package/dist/server/lib/thread-debug-store.d.ts +72 -0
  21. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  22. package/dist/server/lib/thread-debug-store.js +215 -19
  23. package/dist/server/lib/thread-debug-store.js.map +1 -1
  24. package/dist/server/plugins/agent-chat.js +1 -0
  25. package/dist/server/plugins/agent-chat.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/actions/index.ts +2 -0
  28. package/src/actions/list-agent-run-failures.ts +44 -0
  29. package/src/actions/view-screen.spec.ts +91 -0
  30. package/src/actions/view-screen.ts +34 -4
  31. package/src/hooks/use-navigation-state.spec.ts +45 -0
  32. package/src/hooks/use-navigation-state.ts +28 -0
  33. package/src/routes/pages/thread-debug.spec.tsx +272 -0
  34. package/src/routes/pages/thread-debug.tsx +843 -224
  35. package/src/server/lib/thread-debug-store.spec.ts +292 -16
  36. package/src/server/lib/thread-debug-store.ts +318 -22
  37. package/src/server/plugins/agent-chat.spec.ts +30 -0
  38. package/src/server/plugins/agent-chat.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,kBAAkB,GAAG;IACzB,aAAa;IACb,qBAAqB;IACrB,uBAAuB;IACvB,SAAS;IACT,UAAU;IACV,0BAA0B;IAC1B,2BAA2B;IAC3B,2BAA2B;IAC3B,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,8BAA8B;IAC9B,oCAAoC;IACpC,sBAAsB;IACtB,mBAAmB;IACnB,sBAAsB;IACtB,sBAAsB;IACtB,UAAU;CACX,CAAC;AAEF,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,gBAAgB,EAAE,kBAAkB;IACpC,gBAAgB,EAAE,CAAC,oCAAoC,CAAC;IACxD,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,OAAO,EAAE,eAAe;IACxB,aAAa,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;IAC1C,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;4EAwB4D;CAC3E,CAAC,CAAC","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport { createAgentChatPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\n\nconst INITIAL_TOOL_NAMES = [\n \"view-screen\",\n \"list-workspace-apps\",\n \"list-connected-agents\",\n \"ask_app\",\n \"open_app\",\n \"list-workspace-resources\",\n \"create-workspace-resource\",\n \"update-workspace-resource\",\n \"list-vault-secrets\",\n \"request-vault-secret\",\n \"create-vault-secret\",\n \"list-destinations\",\n \"upsert-destination\",\n \"list-dreams\",\n \"start-workspace-app-creation\",\n \"list-available-workspace-templates\",\n \"provider-api-catalog\",\n \"provider-api-docs\",\n \"provider-api-request\",\n \"query-staged-dataset\",\n \"navigate\",\n];\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\n initialToolNames: INITIAL_TOOL_NAMES,\n connectorCatalog: [\"resolve-integration-source-context\"],\n // Without this, AGENT_ORG_ID is never set on agent action calls and every\n // row written through the frontend (vault secrets, destinations, workspace\n // resources) lands with org_id=NULL — breaking data isolation across orgs.\n resolveOrgId: async (event) => {\n const ctx = await getOrgContext(event);\n return ctx.orgId;\n },\n // Read actions directly from the package's own action map rather than from\n // a build-time-generated `.generated/actions-registry.ts` (the latter is a\n // template-only construct that the Vite plugin emits next to actions/).\n actions: dispatchActions,\n codeExecution: { production: \"sandboxed\" },\n systemPrompt: `You are the central dispatch for this workspace.\n\nDefault posture:\n- Treat Slack and Telegram as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A when another app owns the job.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Prefer replying in the current external thread unless the user explicitly asks you to send to a saved destination.\n\nUse the standard workspace primitives:\n- Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.\n- Use recurring jobs for scheduled behavior.\n- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.\n- You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.\n- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.\n- When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.\n- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.\n- 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.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.\n\nWhen a user asks for something like a digest, reminder, routing rule, or saved behavior:\n- First decide whether it should be a resource, a recurring job, a destination, or a delegated task.\n- Keep responses concise and operational.\n- Avoid inventing integrations or destinations that are not configured yet.`,\n});\n"]}
1
+ {"version":3,"file":"agent-chat.js","sourceRoot":"","sources":["../../../src/server/plugins/agent-chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,kBAAkB,GAAG;IACzB,aAAa;IACb,qBAAqB;IACrB,uBAAuB;IACvB,SAAS;IACT,UAAU;IACV,0BAA0B;IAC1B,2BAA2B;IAC3B,2BAA2B;IAC3B,oBAAoB;IACpB,sBAAsB;IACtB,qBAAqB;IACrB,mBAAmB;IACnB,oBAAoB;IACpB,aAAa;IACb,8BAA8B;IAC9B,oCAAoC;IACpC,sBAAsB;IACtB,mBAAmB;IACnB,sBAAsB;IACtB,sBAAsB;IACtB,UAAU;CACX,CAAC;AAEF,eAAe,qBAAqB,CAAC;IACnC,KAAK,EAAE,UAAU;IACjB,qBAAqB,EAAE,IAAI;IAC3B,gBAAgB,EAAE,kBAAkB;IACpC,gBAAgB,EAAE,CAAC,oCAAoC,CAAC;IACxD,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;QAC5B,MAAM,GAAG,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,GAAG,CAAC,KAAK,CAAC;IACnB,CAAC;IACD,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,OAAO,EAAE,eAAe;IACxB,aAAa,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE;IAC1C,YAAY,EAAE;;;;;;;;;;;;;;;;;;;;;;;;4EAwB4D;CAC3E,CAAC,CAAC","sourcesContent":["import { getOrgContext } from \"@agent-native/core/org\";\nimport { createAgentChatPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\n\nconst INITIAL_TOOL_NAMES = [\n \"view-screen\",\n \"list-workspace-apps\",\n \"list-connected-agents\",\n \"ask_app\",\n \"open_app\",\n \"list-workspace-resources\",\n \"create-workspace-resource\",\n \"update-workspace-resource\",\n \"list-vault-secrets\",\n \"request-vault-secret\",\n \"create-vault-secret\",\n \"list-destinations\",\n \"upsert-destination\",\n \"list-dreams\",\n \"start-workspace-app-creation\",\n \"list-available-workspace-templates\",\n \"provider-api-catalog\",\n \"provider-api-docs\",\n \"provider-api-request\",\n \"query-staged-dataset\",\n \"navigate\",\n];\n\nexport default createAgentChatPlugin({\n appId: \"dispatch\",\n durableBackgroundRuns: true,\n initialToolNames: INITIAL_TOOL_NAMES,\n connectorCatalog: [\"resolve-integration-source-context\"],\n // Without this, AGENT_ORG_ID is never set on agent action calls and every\n // row written through the frontend (vault secrets, destinations, workspace\n // resources) lands with org_id=NULL — breaking data isolation across orgs.\n resolveOrgId: async (event) => {\n const ctx = await getOrgContext(event);\n return ctx.orgId;\n },\n // Read actions directly from the package's own action map rather than from\n // a build-time-generated `.generated/actions-registry.ts` (the latter is a\n // template-only construct that the Vite plugin emits next to actions/).\n actions: dispatchActions,\n codeExecution: { production: \"sandboxed\" },\n systemPrompt: `You are the central dispatch for this workspace.\n\nDefault posture:\n- Treat Slack and Telegram as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A when another app owns the job.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Prefer replying in the current external thread unless the user explicitly asks you to send to a saved destination.\n\nUse the standard workspace primitives:\n- Read and update resources like AGENTS.md, LEARNINGS.md, jobs/*.md, agents/*.md, and remote-agents/*.json when appropriate.\n- Use recurring jobs for scheduled behavior.\n- Use custom agent profiles in agents/*.md for local spawned work and remote-agents/*.json for remote A2A apps.\n- You receive a compact available-apps block with sibling workspace app names and descriptions. Use it to pick the right A2A target, and call list-connected-agents or tool-search only when you need fresh details.\n- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.\n- When answering whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. If you have not requested that probe, absence of agent-card fields means unchecked, not unavailable.\n- When creating a new workspace app, create a separate app under apps/<app-id> with apps/<app-id>/package.json including a concise generated description, mount it at /<app-id>, use relative /<app-id> links, never hardcode localhost or dev ports, use shadcn/ui with @tabler/icons-react rather than lucide-react, and ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath(). There is no separate workspace app registry to edit.\n- 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.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- For broad provider searches, joins, classification, corpus counts, or absence claims, fetch every relevant page or an explicitly bounded cohort, stage/save large responses with stageAs/saveToFile/fetchAllPages, and reduce them with query-staged-dataset or run-code. Report source, filters, row counts, pagination, truncation, failed pages, and uncovered gaps.\n\nWhen a user asks for something like a digest, reminder, routing rule, or saved behavior:\n- First decide whether it should be a resource, a recurring job, a destination, or a delegated task.\n- Keep responses concise and operational.\n- Avoid inventing integrations or destinations that are not configured yet.`,\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.16.1",
3
+ "version": "0.16.3",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -84,7 +84,7 @@
84
84
  "tailwind-merge": "^3.5.0",
85
85
  "vaul": "^1.1.2",
86
86
  "zod": "^4.3.6",
87
- "@agent-native/toolkit": "0.11.1"
87
+ "@agent-native/toolkit": "0.12.0"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@react-router/dev": "^8.1.0",
@@ -98,7 +98,7 @@
98
98
  "typescript-7": "npm:typescript@^7.0.2",
99
99
  "vite": "8.1.0",
100
100
  "vitest": "^4.1.5",
101
- "@agent-native/core": "0.131.2"
101
+ "@agent-native/core": "0.131.6"
102
102
  },
103
103
  "peerDependencies": {
104
104
  "@agent-native/core": ">=0.8.0",
@@ -31,6 +31,7 @@ import getWorkspaceInfo from "./get-workspace-info.js";
31
31
  import getWorkspaceResourceEffectiveContext from "./get-workspace-resource-effective-context.js";
32
32
  import grantVaultSecretsToApp from "./grant-vault-secrets-to-app.js";
33
33
  import grantWorkspaceResourcesToApp from "./grant-workspace-resources-to-app.js";
34
+ import listAgentRunFailures from "./list-agent-run-failures.js";
34
35
  import listAgentThreadSources from "./list-agent-thread-sources.js";
35
36
  import listAvailableWorkspaceTemplates from "./list-available-workspace-templates.js";
36
37
  import listConnectedAgents from "./list-connected-agents.js";
@@ -132,6 +133,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
132
133
  "grant-workspace-resources-to-app": grantWorkspaceResourcesToApp,
133
134
  "grant-vault-secrets-to-app": grantVaultSecretsToApp,
134
135
  "list-agent-thread-sources": listAgentThreadSources,
136
+ "list-agent-run-failures": listAgentRunFailures,
135
137
  "list-available-workspace-templates": listAvailableWorkspaceTemplates,
136
138
  "list-curated-workspace-templates": listCuratedWorkspaceTemplates,
137
139
  "list-connected-agents": listConnectedAgents,
@@ -0,0 +1,44 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+
4
+ import { listAgentRunFailures } from "../server/lib/thread-debug-store.js";
5
+
6
+ export default defineAction({
7
+ description:
8
+ "List recent failed, aborted, or truncated agent runs across the connected thread-debug sources the caller may inspect. Returns source health and run diagnostics; use get-agent-thread-debug with a returned source and run ID for the full transcript and event history.",
9
+ schema: z.object({
10
+ sourceId: z
11
+ .string()
12
+ .default("all")
13
+ .describe(
14
+ "Thread debug source id from list-agent-thread-sources, or 'all' to merge every connected source available to the caller.",
15
+ ),
16
+ ownerEmail: z
17
+ .string()
18
+ .optional()
19
+ .describe(
20
+ "Optional owner email filter. Organization admins may only select members of their current organization.",
21
+ ),
22
+ status: z
23
+ .enum(["all", "errored", "aborted", "truncated"])
24
+ .default("all")
25
+ .describe("Unsuccessful run status to include."),
26
+ lookbackHours: z.coerce
27
+ .number()
28
+ .int()
29
+ .min(1)
30
+ .max(720)
31
+ .default(168)
32
+ .describe("Lookback window in hours."),
33
+ limit: z.coerce
34
+ .number()
35
+ .int()
36
+ .min(1)
37
+ .max(100)
38
+ .default(25)
39
+ .describe("Maximum failures returned after merging and sorting sources."),
40
+ }),
41
+ http: { method: "GET" },
42
+ readOnly: true,
43
+ run: async (input) => listAgentRunFailures(input),
44
+ });
@@ -0,0 +1,91 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ navigation: {} as Record<string, unknown>,
5
+ listAgentRunFailures: vi.fn(),
6
+ listThreadDebugSources: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("@agent-native/core", () => ({
10
+ defineAction: (action: unknown) => action,
11
+ }));
12
+
13
+ vi.mock("@agent-native/core/application-state", () => ({
14
+ readAppState: vi.fn(async () => mocks.navigation),
15
+ }));
16
+
17
+ vi.mock("../server/lib/app-creation-store.js", () => ({
18
+ listWorkspaceApps: vi.fn(),
19
+ }));
20
+
21
+ vi.mock("../server/lib/dispatch-store.js", () => ({
22
+ listOverview: vi.fn(async () => ({
23
+ counts: {},
24
+ settings: {},
25
+ })),
26
+ }));
27
+
28
+ vi.mock("../server/lib/thread-debug-store.js", () => ({
29
+ getAgentThreadDebug: vi.fn(),
30
+ listAgentRunFailures: mocks.listAgentRunFailures,
31
+ listThreadDebugSources: mocks.listThreadDebugSources,
32
+ searchAgentThreads: vi.fn(),
33
+ }));
34
+
35
+ vi.mock("../server/lib/usage-metrics-store.js", () => ({
36
+ listDispatchUsageMetrics: vi.fn(),
37
+ }));
38
+
39
+ vi.mock("../server/lib/vault-store.js", () => ({
40
+ getVaultAccessSettings: vi.fn(),
41
+ listGrants: vi.fn(),
42
+ listRequests: vi.fn(),
43
+ listSecrets: vi.fn(),
44
+ listVaultOverview: vi.fn(async () => ({})),
45
+ }));
46
+
47
+ vi.mock("../server/lib/workspace-resources-store.js", () => ({
48
+ listWorkspaceResourceOptions: vi.fn(),
49
+ listWorkspaceResourcesForApp: vi.fn(),
50
+ }));
51
+
52
+ import viewScreen from "./view-screen.js";
53
+
54
+ describe("view-screen Thread Debug summary", () => {
55
+ beforeEach(() => {
56
+ mocks.navigation = { view: "thread-debug" };
57
+ mocks.listAgentRunFailures.mockReset();
58
+ mocks.listAgentRunFailures.mockResolvedValue({ failures: [] });
59
+ mocks.listThreadDebugSources.mockReset();
60
+ mocks.listThreadDebugSources.mockResolvedValue({ sources: [] });
61
+ });
62
+
63
+ it("matches the UI's default 24-hour failed-run range", async () => {
64
+ await viewScreen.run({});
65
+
66
+ expect(mocks.listAgentRunFailures).toHaveBeenCalledWith({
67
+ sourceId: "all",
68
+ ownerEmail: undefined,
69
+ status: "all",
70
+ lookbackHours: 24,
71
+ limit: 10,
72
+ });
73
+ });
74
+
75
+ it("normalizes invalid status and range query state", async () => {
76
+ mocks.navigation = {
77
+ view: "thread-debug",
78
+ failureStatus: "timed-out",
79
+ range: "forever",
80
+ };
81
+
82
+ await viewScreen.run({});
83
+
84
+ expect(mocks.listAgentRunFailures).toHaveBeenCalledWith(
85
+ expect.objectContaining({
86
+ status: "all",
87
+ lookbackHours: 24,
88
+ }),
89
+ );
90
+ });
91
+ });
@@ -15,6 +15,7 @@ import { listWorkspaceApps } from "../server/lib/app-creation-store.js";
15
15
  import { listOverview } from "../server/lib/dispatch-store.js";
16
16
  import {
17
17
  getAgentThreadDebug,
18
+ listAgentRunFailures,
18
19
  listThreadDebugSources,
19
20
  searchAgentThreads,
20
21
  } from "../server/lib/thread-debug-store.js";
@@ -51,6 +52,20 @@ function stripUndefined(args: Record<string, unknown>) {
51
52
  );
52
53
  }
53
54
 
55
+ function threadDebugLookbackHours(value: unknown): number {
56
+ if (value === "7d") return 168;
57
+ if (value === "30d") return 720;
58
+ return 24;
59
+ }
60
+
61
+ function threadDebugFailureStatus(
62
+ value: unknown,
63
+ ): "all" | "errored" | "aborted" | "truncated" {
64
+ return value === "errored" || value === "aborted" || value === "truncated"
65
+ ? value
66
+ : "all";
67
+ }
68
+
54
69
  export default defineAction({
55
70
  description:
56
71
  "See what the user is currently looking at in the dispatch UI, including navigation state and a compact operational summary.",
@@ -190,7 +205,15 @@ export default defineAction({
190
205
  try {
191
206
  const nav = navigation as Record<string, any>;
192
207
  screen.threadDebugSources = await listThreadDebugSources();
193
- if (nav.query) {
208
+ if (nav.threadDebugMode !== "threads") {
209
+ screen.agentRunFailures = await listAgentRunFailures({
210
+ sourceId: nav.sourceId ?? "all",
211
+ ownerEmail: nav.ownerEmail,
212
+ status: threadDebugFailureStatus(nav.failureStatus),
213
+ lookbackHours: threadDebugLookbackHours(nav.range),
214
+ limit: 10,
215
+ });
216
+ } else if (nav.query) {
194
217
  screen.threadDebugResults = await searchAgentThreads({
195
218
  sourceId: nav.sourceId,
196
219
  query: nav.query,
@@ -198,10 +221,14 @@ export default defineAction({
198
221
  limit: 10,
199
222
  });
200
223
  }
201
- if (nav.threadId) {
224
+ if (nav.threadId || nav.runId) {
202
225
  const detail = await getAgentThreadDebug({
203
- sourceId: nav.sourceId,
204
- threadId: nav.threadId,
226
+ sourceId:
227
+ nav.runId && nav.inspectSourceId
228
+ ? nav.inspectSourceId
229
+ : nav.sourceId,
230
+ threadId: nav.runId ? undefined : nav.threadId,
231
+ runId: nav.runId,
205
232
  ownerEmail: nav.ownerEmail,
206
233
  maxRuns: 5,
207
234
  maxEvents: 80,
@@ -215,6 +242,9 @@ export default defineAction({
215
242
  debug: detail.debug,
216
243
  debugRuns: (detail as any).debugRuns?.slice(-5) ?? [],
217
244
  messages: detail.messages.slice(-6),
245
+ runs: detail.runs
246
+ .slice(0, 5)
247
+ .map(({ events: _events, ...run }) => run),
218
248
  };
219
249
  }
220
250
  } catch (error) {
@@ -49,4 +49,49 @@ describe("buildDispatchNavigationState", () => {
49
49
  path: "/automations",
50
50
  });
51
51
  });
52
+
53
+ it("preserves thread debug filters and selection", () => {
54
+ expect(
55
+ buildDispatchNavigationState(
56
+ "/thread-debug",
57
+ "?mode=failures&source=all&inspectSource=mail&owner=ops%40example.com&status=errored&range=7d&query=timeout&runId=run-1&threadId=thread-1",
58
+ ),
59
+ ).toEqual({
60
+ view: "thread-debug",
61
+ path: "/thread-debug",
62
+ threadDebugMode: "failures",
63
+ sourceId: "all",
64
+ inspectSourceId: "mail",
65
+ ownerEmail: "ops@example.com",
66
+ failureStatus: "errored",
67
+ range: "7d",
68
+ query: "timeout",
69
+ runId: "run-1",
70
+ threadId: "thread-1",
71
+ });
72
+ });
73
+
74
+ it("omits empty thread debug query values", () => {
75
+ expect(
76
+ buildDispatchNavigationState(
77
+ "/thread-debug",
78
+ "?mode=&source=&inspectSource=&owner=&status=&range=&query=&runId=&threadId=",
79
+ ),
80
+ ).toEqual({
81
+ view: "thread-debug",
82
+ path: "/thread-debug",
83
+ });
84
+ });
85
+
86
+ it("does not expose thread debug query state on unrelated routes", () => {
87
+ expect(
88
+ buildDispatchNavigationState(
89
+ "/overview",
90
+ "?mode=failures&source=all&inspectSource=mail&owner=ops%40example.com&status=errored&range=7d&query=timeout&runId=run-1&threadId=thread-1",
91
+ ),
92
+ ).toEqual({
93
+ view: "overview",
94
+ path: "/overview",
95
+ });
96
+ });
52
97
  });
@@ -23,8 +23,14 @@ export interface NavigationState {
23
23
  extensionId?: string;
24
24
  extensionSlug?: string;
25
25
  dreamId?: string;
26
+ threadDebugMode?: string;
26
27
  sourceId?: string;
28
+ inspectSourceId?: string;
29
+ ownerEmail?: string;
30
+ failureStatus?: string;
31
+ range?: string;
27
32
  query?: string;
33
+ runId?: string;
28
34
  threadId?: string;
29
35
  }
30
36
 
@@ -149,6 +155,28 @@ export function buildDispatchNavigationState(
149
155
  if (query) state.query = query;
150
156
  }
151
157
 
158
+ if (state.view === "thread-debug") {
159
+ const params = new URLSearchParams(search);
160
+ const mode = params.get("mode");
161
+ const sourceId = params.get("source");
162
+ const inspectSourceId = params.get("inspectSource");
163
+ const ownerEmail = params.get("owner");
164
+ const status = params.get("status");
165
+ const range = params.get("range");
166
+ const query = params.get("query");
167
+ const runId = params.get("runId");
168
+ const selectedThreadId = params.get("threadId");
169
+ if (mode) state.threadDebugMode = mode;
170
+ if (sourceId) state.sourceId = sourceId;
171
+ if (inspectSourceId) state.inspectSourceId = inspectSourceId;
172
+ if (ownerEmail) state.ownerEmail = ownerEmail;
173
+ if (status) state.failureStatus = status;
174
+ if (range) state.range = range;
175
+ if (query) state.query = query;
176
+ if (runId) state.runId = runId;
177
+ if (selectedThreadId) state.threadId = selectedThreadId;
178
+ }
179
+
152
180
  return state;
153
181
  }
154
182
 
@@ -0,0 +1,272 @@
1
+ // @vitest-environment happy-dom
2
+ import { act, type ReactNode } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { MemoryRouter } from "react-router";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import ThreadDebugRoute from "./thread-debug";
8
+
9
+ const queryState = vi.hoisted(() => ({
10
+ calls: [] as Array<{
11
+ name: string;
12
+ params: Record<string, unknown>;
13
+ enabled: boolean;
14
+ }>,
15
+ }));
16
+
17
+ const failedRun = {
18
+ id: "run-mail-1",
19
+ threadId: "thread-mail-1",
20
+ source: {
21
+ id: "mail",
22
+ label: "Mail",
23
+ kind: "configured",
24
+ databaseUrlEnv: "MAIL_DATABASE_URL",
25
+ },
26
+ ownerEmail: "ops@example.com",
27
+ threadTitle: "Mail agent failed",
28
+ threadPreview: "Provider timed out",
29
+ status: "errored",
30
+ errorCode: "provider_timeout",
31
+ errorDetail: "The provider did not respond.",
32
+ terminalReason: "provider_timeout",
33
+ abortReason: null,
34
+ dispatchMode: "background-processing",
35
+ diagStage: null,
36
+ workerStage: "model",
37
+ startedAt: 1_722_400_000_000,
38
+ completedAt: 1_722_400_030_000,
39
+ durationMs: 30_000,
40
+ };
41
+
42
+ const detail = {
43
+ source: failedRun.source,
44
+ access: {
45
+ viewerEmail: "ops@example.com",
46
+ scope: "current organization",
47
+ canInspectAll: true,
48
+ },
49
+ thread: {
50
+ id: failedRun.threadId,
51
+ ownerEmail: failedRun.ownerEmail,
52
+ title: failedRun.threadTitle,
53
+ preview: failedRun.threadPreview,
54
+ messageCount: 0,
55
+ createdAt: failedRun.startedAt,
56
+ updatedAt: failedRun.completedAt,
57
+ snippet: "",
58
+ },
59
+ lookup: {
60
+ requestedId: failedRun.id,
61
+ threadId: failedRun.threadId,
62
+ runId: failedRun.id,
63
+ },
64
+ messages: [],
65
+ debug: null,
66
+ debugRuns: [],
67
+ queuedMessages: [],
68
+ threadData: {},
69
+ rawThreadData: "{}",
70
+ runs: [],
71
+ traces: { summaries: [], spans: [] },
72
+ feedback: [],
73
+ satisfaction: [],
74
+ evals: [],
75
+ checkpoints: [],
76
+ };
77
+
78
+ vi.mock("@agent-native/core/client/hooks", () => ({
79
+ useActionQuery: (
80
+ name: string,
81
+ params: Record<string, unknown>,
82
+ options?: { enabled?: boolean },
83
+ ) => {
84
+ const enabled = options?.enabled !== false;
85
+ queryState.calls.push({ name, params, enabled });
86
+ const base = {
87
+ isLoading: false,
88
+ isError: false,
89
+ error: null,
90
+ refetch: vi.fn(),
91
+ };
92
+ if (name === "list-agent-thread-sources") {
93
+ return {
94
+ ...base,
95
+ data: {
96
+ access: {
97
+ viewerEmail: "ops@example.com",
98
+ orgId: "org-1",
99
+ role: "admin",
100
+ envAdmin: false,
101
+ canInspectAll: true,
102
+ memberCount: 1,
103
+ },
104
+ sources: [
105
+ {
106
+ id: "current",
107
+ label: "Current Dispatch DB",
108
+ kind: "current",
109
+ current: true,
110
+ connected: true,
111
+ databaseUrlEnv: null,
112
+ databaseAuthTokenEnv: null,
113
+ canInspectAll: true,
114
+ },
115
+ {
116
+ id: "mail",
117
+ label: "Mail",
118
+ kind: "configured",
119
+ current: false,
120
+ connected: true,
121
+ databaseUrlEnv: "MAIL_DATABASE_URL",
122
+ databaseAuthTokenEnv: null,
123
+ canInspectAll: true,
124
+ },
125
+ ],
126
+ },
127
+ };
128
+ }
129
+ if (name === "list-agent-run-failures") {
130
+ return {
131
+ ...base,
132
+ data: enabled
133
+ ? {
134
+ failures: [failedRun],
135
+ count: 1,
136
+ partial: true,
137
+ access: {
138
+ viewerEmail: "ops@example.com",
139
+ scope: "current organization",
140
+ canInspectAll: true,
141
+ },
142
+ sources: [
143
+ {
144
+ source: failedRun.source,
145
+ status: "ok",
146
+ failureCount: 1,
147
+ },
148
+ {
149
+ source: { id: "clips", label: "Clips" },
150
+ status: "unavailable",
151
+ failureCount: 0,
152
+ },
153
+ ],
154
+ }
155
+ : undefined,
156
+ };
157
+ }
158
+ if (name === "get-agent-thread-debug") {
159
+ return { ...base, data: enabled ? detail : undefined };
160
+ }
161
+ return { ...base, data: undefined };
162
+ },
163
+ }));
164
+
165
+ vi.mock("@agent-native/core/client/i18n", () => ({
166
+ useT: () => (key: string, values?: { defaultValue?: string }) =>
167
+ values?.defaultValue ?? key,
168
+ }));
169
+
170
+ vi.mock("../../components/dispatch-shell", () => ({
171
+ DispatchShell: ({
172
+ title,
173
+ description,
174
+ children,
175
+ }: {
176
+ title: ReactNode;
177
+ description: ReactNode;
178
+ children: ReactNode;
179
+ }) => (
180
+ <main>
181
+ <h1>{title}</h1>
182
+ <p>{description}</p>
183
+ {children}
184
+ </main>
185
+ ),
186
+ }));
187
+
188
+ describe("ThreadDebugRoute", () => {
189
+ let container: HTMLDivElement;
190
+ let root: Root;
191
+
192
+ beforeEach(() => {
193
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
194
+ queryState.calls = [];
195
+ container = document.createElement("div");
196
+ document.body.appendChild(container);
197
+ root = createRoot(container);
198
+ });
199
+
200
+ afterEach(() => {
201
+ act(() => root.unmount());
202
+ container.remove();
203
+ vi.unstubAllGlobals();
204
+ });
205
+
206
+ it("uses 24-hour defaults, preserves partial health, and inspects a cross-source failure", async () => {
207
+ await act(async () => {
208
+ root.render(
209
+ <MemoryRouter initialEntries={["/thread-debug"]}>
210
+ <ThreadDebugRoute />
211
+ </MemoryRouter>,
212
+ );
213
+ });
214
+
215
+ expect(
216
+ queryState.calls.find(
217
+ (call) => call.name === "list-agent-run-failures" && call.enabled,
218
+ )?.params,
219
+ ).toMatchObject({
220
+ sourceId: "all",
221
+ status: "all",
222
+ lookbackHours: 24,
223
+ });
224
+ expect(container.textContent).toContain("Partial results");
225
+ expect(container.textContent).toContain("Clips (unavailable)");
226
+
227
+ const failureButton = [...container.querySelectorAll("button")].find(
228
+ (button) => button.textContent?.includes("Inspect failed run"),
229
+ );
230
+ expect(failureButton).toBeTruthy();
231
+ await act(async () => {
232
+ failureButton?.click();
233
+ });
234
+
235
+ expect(
236
+ queryState.calls.find(
237
+ (call) =>
238
+ call.name === "get-agent-thread-debug" &&
239
+ call.enabled &&
240
+ call.params.runId === failedRun.id,
241
+ )?.params,
242
+ ).toMatchObject({
243
+ sourceId: "mail",
244
+ runId: failedRun.id,
245
+ });
246
+ });
247
+
248
+ it("passes valid URL-backed failure filters to the action", async () => {
249
+ await act(async () => {
250
+ root.render(
251
+ <MemoryRouter
252
+ initialEntries={[
253
+ "/thread-debug?source=mail&owner=ops%40example.com&status=errored&range=7d",
254
+ ]}
255
+ >
256
+ <ThreadDebugRoute />
257
+ </MemoryRouter>,
258
+ );
259
+ });
260
+
261
+ expect(
262
+ queryState.calls.find(
263
+ (call) => call.name === "list-agent-run-failures" && call.enabled,
264
+ )?.params,
265
+ ).toMatchObject({
266
+ sourceId: "mail",
267
+ ownerEmail: "ops@example.com",
268
+ status: "errored",
269
+ lookbackHours: 168,
270
+ });
271
+ });
272
+ });