@agent-native/dispatch 0.31.12 → 0.31.14

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 (53) hide show
  1. package/dist/actions/list-dispatch-usage-metrics.js +9 -1
  2. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
  3. package/dist/actions/provider-api-register.d.ts +12 -12
  4. package/dist/components/layout/Layout.d.ts +2 -1
  5. package/dist/components/layout/Layout.d.ts.map +1 -1
  6. package/dist/components/layout/Layout.js +3 -3
  7. package/dist/components/layout/Layout.js.map +1 -1
  8. package/dist/components/other-apps-section.d.ts +5 -3
  9. package/dist/components/other-apps-section.d.ts.map +1 -1
  10. package/dist/components/other-apps-section.js +15 -3
  11. package/dist/components/other-apps-section.js.map +1 -1
  12. package/dist/components/workspace-app-host.d.ts +8 -2
  13. package/dist/components/workspace-app-host.d.ts.map +1 -1
  14. package/dist/components/workspace-app-host.js +46 -8
  15. package/dist/components/workspace-app-host.js.map +1 -1
  16. package/dist/lib/workspace-apps.d.ts +7 -0
  17. package/dist/lib/workspace-apps.d.ts.map +1 -1
  18. package/dist/lib/workspace-apps.js +65 -0
  19. package/dist/lib/workspace-apps.js.map +1 -1
  20. package/dist/routes/index.js +1 -1
  21. package/dist/routes/index.js.map +1 -1
  22. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  23. package/dist/routes/pages/apps.$appId.js +15 -4
  24. package/dist/routes/pages/apps.$appId.js.map +1 -1
  25. package/dist/routes/pages/apps.d.ts.map +1 -1
  26. package/dist/routes/pages/apps.js +16 -4
  27. package/dist/routes/pages/apps.js.map +1 -1
  28. package/dist/server/lib/usage-metrics-store.d.ts +20 -0
  29. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  30. package/dist/server/lib/usage-metrics-store.js +86 -32
  31. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  32. package/dist/server/plugins/agent-chat.js +3 -0
  33. package/dist/server/plugins/agent-chat.js.map +1 -1
  34. package/package.json +3 -3
  35. package/src/actions/index.spec.ts +17 -0
  36. package/src/actions/list-dispatch-usage-metrics.ts +9 -1
  37. package/src/components/layout/Layout.spec.tsx +75 -0
  38. package/src/components/layout/Layout.tsx +22 -23
  39. package/src/components/other-apps-section.spec.ts +49 -0
  40. package/src/components/other-apps-section.tsx +31 -6
  41. package/src/components/workspace-app-host.spec.tsx +98 -0
  42. package/src/components/workspace-app-host.tsx +70 -4
  43. package/src/lib/workspace-apps.spec.ts +40 -0
  44. package/src/lib/workspace-apps.ts +80 -0
  45. package/src/routes/index.spec.ts +10 -0
  46. package/src/routes/index.ts +1 -1
  47. package/src/routes/pages/apps.$appId.spec.tsx +50 -1
  48. package/src/routes/pages/apps.$appId.tsx +26 -3
  49. package/src/routes/pages/apps.tsx +42 -26
  50. package/src/server/lib/usage-metrics-store.spec.ts +132 -1
  51. package/src/server/lib/usage-metrics-store.ts +173 -65
  52. package/src/server/plugins/agent-chat.spec.ts +2 -0
  53. package/src/server/plugins/agent-chat.ts +3 -0
@@ -10,6 +10,7 @@ import { getDbExec } from "@agent-native/core/db";
10
10
  import { getSetting } from "@agent-native/core/settings";
11
11
  import { ForbiddenError } from "@agent-native/core/sharing";
12
12
  import {
13
+ builderCreditsFromCostCents,
13
14
  getUsageSummary,
14
15
  usageBillingForEngine,
15
16
  type UsageBillingMode,
@@ -73,6 +74,26 @@ export interface DailyUsageMetric {
73
74
  activeUsers: number;
74
75
  }
75
76
 
77
+ export interface MonthlyUserUsageMetric {
78
+ month: string;
79
+ ownerEmail: string;
80
+ costCents: number;
81
+ credits: number;
82
+ calls: number;
83
+ chatCalls: number;
84
+ inputTokens: number;
85
+ outputTokens: number;
86
+ cacheReadTokens: number;
87
+ cacheWriteTokens: number;
88
+ }
89
+
90
+ export interface WorkspaceAppCreationMetric {
91
+ month: string;
92
+ ownerEmail: string;
93
+ count: number;
94
+ appIds: string[];
95
+ }
96
+
76
97
  export interface RecentUsageMetric {
77
98
  id: number;
78
99
  createdAt: number;
@@ -134,6 +155,8 @@ export interface DispatchUsageMetrics {
134
155
  byLabel: UsageMetricBucket[];
135
156
  byModel: UsageMetricBucket[];
136
157
  daily: DailyUsageMetric[];
158
+ monthlyByUser: MonthlyUserUsageMetric[];
159
+ workspaceAppCreationsByUserMonth: WorkspaceAppCreationMetric[];
137
160
  appAccess: AppAccessMetric[];
138
161
  recent: RecentUsageMetric[];
139
162
  }
@@ -412,37 +435,6 @@ async function listOrgMembers(orgId: string | null): Promise<MemberRecord[]> {
412
435
  .filter((member) => member.email);
413
436
  }
414
437
 
415
- async function listSignedInUsers(): Promise<MemberRecord[]> {
416
- const authRows = await queryRows<Record<string, unknown>>(
417
- `SELECT email, created_at AS joined_at FROM "user" ORDER BY created_at ASC`,
418
- );
419
- if (authRows.length > 0) {
420
- return authRows
421
- .map((row) => ({
422
- email: stringField(row, "email").trim(),
423
- role: null,
424
- joinedAt: nullableNumberField(row, "joined_at"),
425
- }))
426
- .filter((member) => member.email);
427
- }
428
-
429
- const usageRows = await queryRows<{ email?: string }>(
430
- `SELECT DISTINCT owner_email AS email FROM token_usage`,
431
- );
432
- const threadRows = await queryRows<{ email?: string }>(
433
- `SELECT DISTINCT owner_email AS email FROM chat_threads`,
434
- );
435
- const emails = new Set<string>();
436
- for (const row of [...usageRows, ...threadRows]) {
437
- if (row.email) emails.add(row.email);
438
- }
439
- return [...emails].sort().map((email) => ({
440
- email,
441
- role: null,
442
- joinedAt: null,
443
- }));
444
- }
445
-
446
438
  function usageScope(
447
439
  sinceMs: number,
448
440
  memberEmails: string[],
@@ -497,6 +489,28 @@ function ownerThreadScope(
497
489
  };
498
490
  }
499
491
 
492
+ function workspaceAppCreationScope(
493
+ sinceMs: number,
494
+ orgId: string | null,
495
+ memberEmails: string[],
496
+ ): { where: string; args: unknown[] } {
497
+ const filters = ["created_at >= ?", "action = ?"];
498
+ const args: unknown[] = [sinceMs, "workspace-app.pending"];
499
+
500
+ if (orgId) {
501
+ filters.push("org_id = ?");
502
+ args.push(orgId);
503
+ }
504
+ if (memberEmails.length > 0) {
505
+ filters.push(
506
+ `LOWER(owner_email) IN (${memberEmails.map(() => "?").join(", ")})`,
507
+ );
508
+ args.push(...memberEmails.map((email) => email.toLowerCase()));
509
+ }
510
+
511
+ return { where: filters.join(" AND "), args };
512
+ }
513
+
500
514
  function bucketFromRow(row: Record<string, unknown>): UsageMetricBucket {
501
515
  const key = stringField(row, "k");
502
516
  return {
@@ -610,7 +624,7 @@ export async function listDispatchUsageMetrics(input: {
610
624
  ? [{ email: viewerEmail, role, joinedAt: null }]
611
625
  : orgId
612
626
  ? await listOrgMembers(orgId)
613
- : await listSignedInUsers();
627
+ : [{ email: viewerEmail, role: null, joinedAt: null }];
614
628
  const members =
615
629
  viewScope === "workspace" && orgId && rawMembers.length === 0
616
630
  ? [{ email: viewerEmail, role, joinedAt: null }]
@@ -632,9 +646,7 @@ export async function listDispatchUsageMetrics(input: {
632
646
  }
633
647
  const memberEmails = selectedUserEmail
634
648
  ? [selectedUserEmail]
635
- : orgId
636
- ? members.map((member) => member.email)
637
- : [];
649
+ : members.map((member) => member.email);
638
650
  const memberByEmail = new Map(
639
651
  members.map((member) => [member.email.toLowerCase(), member]),
640
652
  );
@@ -645,11 +657,25 @@ export async function listDispatchUsageMetrics(input: {
645
657
  ? ownerThreadScope(sinceMs, selectedUserEmail)
646
658
  : threadScope(sinceMs, memberEmails);
647
659
 
648
- const [apps, totalsRows, byApp, byUserBase, byLabel, byModel, chatStats] =
649
- await Promise.all([
650
- listWorkspaceApps({ includeAgentCards: false }),
651
- queryRows<Record<string, unknown>>(
652
- `SELECT
660
+ const workspaceAppCreation = workspaceAppCreationScope(
661
+ sinceMs,
662
+ orgId,
663
+ memberEmails,
664
+ );
665
+
666
+ const [
667
+ apps,
668
+ totalsRows,
669
+ byApp,
670
+ byUserBase,
671
+ byLabel,
672
+ byModel,
673
+ chatStats,
674
+ workspaceAppCreationRows,
675
+ ] = await Promise.all([
676
+ listWorkspaceApps({ includeAgentCards: false }),
677
+ queryRows<Record<string, unknown>>(
678
+ `SELECT
653
679
  COALESCE(SUM(cost_cents_x100), 0) AS cost_x100,
654
680
  COUNT(*) AS calls,
655
681
  SUM(CASE WHEN label = 'chat' THEN 1 ELSE 0 END) AS chat_calls,
@@ -660,29 +686,36 @@ export async function listDispatchUsageMetrics(input: {
660
686
  COUNT(DISTINCT owner_email) AS active_users
661
687
  FROM token_usage
662
688
  WHERE ${usage.where}`,
663
- usage.args,
664
- ),
665
- usageBuckets(
666
- `COALESCE(NULLIF(app, ''), 'unattributed')`,
667
- usage.where,
668
- usage.args,
669
- 20,
670
- ),
671
- usageBuckets("owner_email", usage.where, usage.args, 50),
672
- usageBuckets(
673
- `COALESCE(NULLIF(label, ''), 'chat')`,
674
- usage.where,
675
- usage.args,
676
- 20,
677
- ),
678
- usageBuckets(
679
- `COALESCE(NULLIF(model, ''), 'unknown')`,
680
- usage.where,
681
- usage.args,
682
- 20,
683
- ),
684
- loadChatStats(threads.where, threads.args),
685
- ]);
689
+ usage.args,
690
+ ),
691
+ usageBuckets(
692
+ `COALESCE(NULLIF(app, ''), 'unattributed')`,
693
+ usage.where,
694
+ usage.args,
695
+ 20,
696
+ ),
697
+ usageBuckets("owner_email", usage.where, usage.args, 50),
698
+ usageBuckets(
699
+ `COALESCE(NULLIF(label, ''), 'chat')`,
700
+ usage.where,
701
+ usage.args,
702
+ 20,
703
+ ),
704
+ usageBuckets(
705
+ `COALESCE(NULLIF(model, ''), 'unknown')`,
706
+ usage.where,
707
+ usage.args,
708
+ 20,
709
+ ),
710
+ loadChatStats(threads.where, threads.args),
711
+ queryRows<Record<string, unknown>>(
712
+ `SELECT owner_email, actor, target_id, created_at
713
+ FROM dispatch_audit_events
714
+ WHERE ${workspaceAppCreation.where}
715
+ ORDER BY created_at ASC`,
716
+ workspaceAppCreation.args,
717
+ ),
718
+ ]);
686
719
 
687
720
  const topAppRows = await queryRows<Record<string, unknown>>(
688
721
  `SELECT owner_email AS owner_email,
@@ -746,7 +779,8 @@ export async function listDispatchUsageMetrics(input: {
746
779
  }
747
780
 
748
781
  const dayRows = await queryRows<Record<string, unknown>>(
749
- `SELECT created_at, owner_email, label, cost_cents_x100
782
+ `SELECT created_at, owner_email, label, input_tokens, output_tokens,
783
+ cache_read_tokens, cache_write_tokens, cost_cents_x100
750
784
  FROM token_usage
751
785
  WHERE ${usage.where}
752
786
  ORDER BY created_at ASC`,
@@ -756,10 +790,16 @@ export async function listDispatchUsageMetrics(input: {
756
790
  string,
757
791
  { costX100: number; calls: number; chatCalls: number; users: Set<string> }
758
792
  >();
793
+ const monthlyByUserMap = new Map<
794
+ string,
795
+ Omit<MonthlyUserUsageMetric, "credits">
796
+ >();
759
797
  for (const row of dayRows) {
760
798
  const date = new Date(numberField(row, "created_at"))
761
799
  .toISOString()
762
800
  .slice(0, 10);
801
+ const month = date.slice(0, 7);
802
+ const ownerEmail = stringField(row, "owner_email");
763
803
  const current = dailyMap.get(date) ?? {
764
804
  costX100: 0,
765
805
  calls: 0,
@@ -769,8 +809,29 @@ export async function listDispatchUsageMetrics(input: {
769
809
  current.costX100 += numberField(row, "cost_cents_x100");
770
810
  current.calls += 1;
771
811
  if (stringField(row, "label") === "chat") current.chatCalls += 1;
772
- current.users.add(stringField(row, "owner_email"));
812
+ current.users.add(ownerEmail);
773
813
  dailyMap.set(date, current);
814
+
815
+ const monthlyKey = `${ownerEmail}\u0000${month}`;
816
+ const monthly = monthlyByUserMap.get(monthlyKey) ?? {
817
+ month,
818
+ ownerEmail,
819
+ costCents: 0,
820
+ calls: 0,
821
+ chatCalls: 0,
822
+ inputTokens: 0,
823
+ outputTokens: 0,
824
+ cacheReadTokens: 0,
825
+ cacheWriteTokens: 0,
826
+ };
827
+ monthly.costCents += numberField(row, "cost_cents_x100") / 100;
828
+ monthly.calls += 1;
829
+ if (stringField(row, "label") === "chat") monthly.chatCalls += 1;
830
+ monthly.inputTokens += numberField(row, "input_tokens");
831
+ monthly.outputTokens += numberField(row, "output_tokens");
832
+ monthly.cacheReadTokens += numberField(row, "cache_read_tokens");
833
+ monthly.cacheWriteTokens += numberField(row, "cache_write_tokens");
834
+ monthlyByUserMap.set(monthlyKey, monthly);
774
835
  }
775
836
  const daily = [...dailyMap.entries()]
776
837
  .map(([date, value]) => ({
@@ -782,6 +843,51 @@ export async function listDispatchUsageMetrics(input: {
782
843
  }))
783
844
  .sort((a, b) => a.date.localeCompare(b.date));
784
845
 
846
+ const monthlyByUser = [...monthlyByUserMap.values()]
847
+ .map((row) => ({
848
+ ...row,
849
+ credits: builderCreditsFromCostCents(row.costCents),
850
+ }))
851
+ .sort(
852
+ (a, b) =>
853
+ a.month.localeCompare(b.month) ||
854
+ a.ownerEmail.localeCompare(b.ownerEmail),
855
+ );
856
+
857
+ const workspaceAppCreationMap = new Map<
858
+ string,
859
+ { month: string; ownerEmail: string; count: number; appIds: Set<string> }
860
+ >();
861
+ for (const row of workspaceAppCreationRows) {
862
+ const month = new Date(numberField(row, "created_at"))
863
+ .toISOString()
864
+ .slice(0, 7);
865
+ const ownerEmail =
866
+ stringField(row, "owner_email") || stringField(row, "actor");
867
+ if (!ownerEmail) continue;
868
+ const key = `${ownerEmail}\u0000${month}`;
869
+ const current = workspaceAppCreationMap.get(key) ?? {
870
+ month,
871
+ ownerEmail,
872
+ count: 0,
873
+ appIds: new Set<string>(),
874
+ };
875
+ current.count += 1;
876
+ const appId = stringField(row, "target_id");
877
+ if (appId) current.appIds.add(appId);
878
+ workspaceAppCreationMap.set(key, current);
879
+ }
880
+ const workspaceAppCreationsByUserMonth = [...workspaceAppCreationMap.values()]
881
+ .map(({ appIds, ...row }) => ({
882
+ ...row,
883
+ appIds: [...appIds].sort(),
884
+ }))
885
+ .sort(
886
+ (a, b) =>
887
+ a.month.localeCompare(b.month) ||
888
+ a.ownerEmail.localeCompare(b.ownerEmail),
889
+ );
890
+
785
891
  const recentRows = await queryRows<Record<string, unknown>>(
786
892
  `SELECT id, created_at, owner_email, app, label, model,
787
893
  input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
@@ -873,6 +979,8 @@ export async function listDispatchUsageMetrics(input: {
873
979
  byLabel,
874
980
  byModel,
875
981
  daily,
982
+ monthlyByUser,
983
+ workspaceAppCreationsByUserMonth,
876
984
  appAccess,
877
985
  recent,
878
986
  };
@@ -27,10 +27,12 @@ describe("Dispatch agent chat plugin", () => {
27
27
  initialToolNames: expect.arrayContaining([
28
28
  "list-workspace-apps",
29
29
  "update-workspace-app-metadata",
30
+ "list-dispatch-usage-metrics",
30
31
  ]),
31
32
  mcp: {
32
33
  connectorCatalog: expect.arrayContaining([
33
34
  "start-workspace-app-creation",
35
+ "list-dispatch-usage-metrics",
34
36
  ]),
35
37
  },
36
38
  actionRoutePublicPaths: ["/_agent-native/actions/list-workspace-apps"],
@@ -28,6 +28,7 @@ const INITIAL_TOOL_NAMES = [
28
28
  "upsert-destination",
29
29
  "list-dreams",
30
30
  "start-workspace-app-creation",
31
+ "list-dispatch-usage-metrics",
31
32
  "list-available-workspace-templates",
32
33
  "provider-api-catalog",
33
34
  "provider-api-docs",
@@ -44,6 +45,7 @@ export default createAgentChatPlugin({
44
45
  connectorCatalog: [
45
46
  "resolve-integration-source-context",
46
47
  "start-workspace-app-creation",
48
+ "list-dispatch-usage-metrics",
47
49
  ],
48
50
  },
49
51
  // Without this, AGENT_ORG_ID is never set on agent action calls and every
@@ -91,6 +93,7 @@ Use the standard workspace primitives:
91
93
  - 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.
92
94
  - 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.
93
95
  - 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.
96
+ - For Builder.io or AI credit spend, LLM usage by workspace member or month, or workspace app/Builder branch creation history, call list-dispatch-usage-metrics with scope=workspace and the requested sinceDays. Its monthlyByUser and workspaceAppCreationsByUserMonth fields are the authoritative shared-database result; do not ask for a user export or BigQuery schema.
94
97
 
95
98
  When a user asks for something like a digest, reminder, routing rule, or saved behavior:
96
99
  - First decide whether it should be a resource, a recurring job, a destination, or a delegated task.