@growthub/cli 0.13.9 → 0.14.1

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 (39) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
  39. package/package.json +1 -1
@@ -4,9 +4,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import { useRouter, useSearchParams } from "next/navigation";
6
6
  import {
7
+ ArrowDown,
8
+ ArrowUp,
9
+ ArrowUpCircle,
7
10
  Bot,
8
- ChevronDown,
9
- ChevronUp,
10
11
  Code,
11
12
  Filter,
12
13
  FormInput,
@@ -51,7 +52,31 @@ import { AgentSwarmPanel } from "../data-model/components/AgentSwarmPanel.jsx";
51
52
  import { RunSetupPanel } from "./RunSetupPanel.jsx";
52
53
  import { describeRunInputMetadataItems, discoverRunInputSchema } from "@/lib/orchestration-run-inputs";
53
54
  import { selectWorkflowNodeInputSchema } from "@/lib/workspace-metadata-selectors";
54
- import { deriveProvenance, hasConnectionId } from "@/lib/workspace-activation";
55
+ import { deriveProvenance, hasConnectionId, readUiCacheFlag } from "@/lib/workspace-activation";
56
+ import { ApiRegistryCreationCockpit } from "../data-model/components/ApiRegistryCreationCockpit.jsx";
57
+ import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
58
+ import { deriveServerlessUpgradeState, SERVERLESS_UPGRADE_DISMISS_FLAG } from "@/lib/serverless-upgrade";
59
+
60
+ // Set a flag on the governed workspace-ui-cache "activation" row (pure helper,
61
+ // same transform the rail/lens one-time dismisses use).
62
+ function withUiCacheFlag(workspaceConfig, flag, value) {
63
+ const dm = workspaceConfig?.dataModel && typeof workspaceConfig.dataModel === "object" ? workspaceConfig.dataModel : {};
64
+ const objects = Array.isArray(dm.objects) ? dm.objects : [];
65
+ const existing = objects.find((o) => o?.id === "workspace-ui-cache");
66
+ const baseRow = (existing?.rows || []).find((r) => r?.id === "activation") || { id: "activation" };
67
+ const nextRow = { ...baseRow, [flag]: value };
68
+ const nextCache = existing
69
+ ? { ...existing, rows: [nextRow, ...(existing.rows || []).filter((r) => r?.id !== "activation")] }
70
+ : {
71
+ id: "workspace-ui-cache", label: "Workspace UI Cache", source: "Workspace UI Cache",
72
+ objectType: "custom", columns: ["id", flag], rows: [nextRow],
73
+ binding: { mode: "manual", source: "Workspace UI Cache" },
74
+ };
75
+ const nextObjects = existing
76
+ ? objects.map((o) => (o?.id === "workspace-ui-cache" ? nextCache : o))
77
+ : [...objects, nextCache];
78
+ return { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } };
79
+ }
55
80
 
56
81
  // Workspace Metadata Graph V1 — read-only dependency metadata for workflow
57
82
  // sidecars. The runtime path (sandbox-run, publish, draft/live) is
@@ -63,20 +88,33 @@ const WORKFLOW_METADATA_SELECTORS = Object.freeze({
63
88
  });
64
89
 
65
90
  function resolveRegistryRowForSandbox(workspaceConfig, sandboxRow) {
91
+ return resolveRegistryRefForSandbox(workspaceConfig, sandboxRow)?.row || null;
92
+ }
93
+
94
+ function resolveRegistryRefForSandbox(workspaceConfig, sandboxRow) {
66
95
  const graph = parseOrchestrationGraph(sandboxRow?.orchestrationConfig || sandboxRow?.orchestrationGraph);
67
96
  const apiNode = graph?.nodes?.find((n) => n?.type === "api-registry-call");
68
97
  const registryId = String(
69
98
  apiNode?.config?.registryId || apiNode?.config?.integrationId || sandboxRow?.schedulerRegistryId || ""
70
99
  ).trim();
71
- if (!registryId || !workspaceConfig) return null;
100
+ if (!workspaceConfig) return null;
72
101
  const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
102
+ let firstRegistryRow = null;
103
+ let firstRegistryObject = null;
73
104
  for (const objectItem of objects) {
74
105
  if (objectItem?.objectType !== "api-registry") continue;
75
106
  const rows = Array.isArray(objectItem.rows) ? objectItem.rows : [];
76
- const match = rows.find((r) => String(r?.integrationId || "").trim() === registryId);
77
- if (match) return match;
107
+ const firstRow = rows.find((r) => String(r?.integrationId || "").trim());
108
+ if (!firstRegistryRow && firstRow) {
109
+ firstRegistryRow = firstRow;
110
+ firstRegistryObject = objectItem;
111
+ }
112
+ if (registryId) {
113
+ const match = rows.find((r) => String(r?.integrationId || "").trim() === registryId);
114
+ if (match) return { object: objectItem, row: match };
115
+ }
78
116
  }
79
- return null;
117
+ return firstRegistryRow ? { object: firstRegistryObject, row: firstRegistryRow } : null;
80
118
  }
81
119
 
82
120
  function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
@@ -97,6 +135,29 @@ function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
97
135
  };
98
136
  }
99
137
 
138
+ function nodeSandboxRecordRef(objectId, rowName, nodeId) {
139
+ return {
140
+ objectId: String(objectId || "").trim(),
141
+ rowName: String(rowName || "").trim(),
142
+ nodeId: String(nodeId || "").trim()
143
+ };
144
+ }
145
+
146
+ function withGraphSandboxRecordRefs(graph, objectId, rowName) {
147
+ const parsed = parseOrchestrationGraph(graph) || graph;
148
+ if (!parsed || typeof parsed !== "object") return parsed;
149
+ return {
150
+ ...parsed,
151
+ nodes: (Array.isArray(parsed.nodes) ? parsed.nodes : []).map((node) => ({
152
+ ...node,
153
+ config: {
154
+ ...(node?.config || {}),
155
+ sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, node?.id)
156
+ }
157
+ }))
158
+ };
159
+ }
160
+
100
161
  const WORKFLOW_ACTION_GROUPS = [
101
162
  {
102
163
  label: "Data",
@@ -205,6 +266,7 @@ function getNodeDeltaRecords(previousGraph, nextGraph) {
205
266
  nodeId,
206
267
  nodeType: String(node?.type || ""),
207
268
  label: String(node?.label || node?.sandbox || nodeId),
269
+ sandboxRecordRef: config.sandboxRecordRef || null,
208
270
  changeReason,
209
271
  deltaTags,
210
272
  requiresRetest: config.requiresRetest !== false,
@@ -349,6 +411,23 @@ export default function WorkflowSurface() {
349
411
  const [orchestrationGraph, setOrchestrationGraph] = useState(null);
350
412
  const [dirty, setDirty] = useState(false);
351
413
  const [runSetupOpen, setRunSetupOpen] = useState(false);
414
+ const [upgradeOpen, setUpgradeOpen] = useState(false);
415
+ const [serverlessSignals, setServerlessSignals] = useState({ configuredEnvRefs: [], persistenceAdapters: [] });
416
+
417
+ useEffect(() => {
418
+ let cancelled = false;
419
+ fetch("/api/workspace/env-status", { cache: "no-store" })
420
+ .then((res) => (res.ok ? res.json() : {}))
421
+ .then((payload) => {
422
+ if (cancelled) return;
423
+ setServerlessSignals({
424
+ configuredEnvRefs: Array.isArray(payload.configuredEnvRefs) ? payload.configuredEnvRefs : [],
425
+ persistenceAdapters: Array.isArray(payload.persistenceAdapters) ? payload.persistenceAdapters : [],
426
+ });
427
+ })
428
+ .catch(() => {});
429
+ return () => { cancelled = true; };
430
+ }, [objectId, rowId]);
352
431
 
353
432
  const load = useCallback(async () => {
354
433
  setLoading(true);
@@ -442,8 +521,16 @@ export default function WorkflowSurface() {
442
521
 
443
522
  const selectedNode = useMemo(() => {
444
523
  if (!orchestrationGraph?.nodes || !selectedNodeId) return null;
445
- return orchestrationGraph.nodes.find((n) => String(n.id) === selectedNodeId) || null;
446
- }, [orchestrationGraph, selectedNodeId]);
524
+ const node = orchestrationGraph.nodes.find((n) => String(n.id) === selectedNodeId) || null;
525
+ if (!node) return null;
526
+ return {
527
+ ...node,
528
+ config: {
529
+ ...(node.config || {}),
530
+ sandboxRecordRef: nodeSandboxRecordRef(objectId, rowId, node.id)
531
+ }
532
+ };
533
+ }, [orchestrationGraph, selectedNodeId, objectId, rowId]);
447
534
 
448
535
  useEffect(() => {
449
536
  if (graphUnset || graphBlankShell) {
@@ -466,7 +553,7 @@ export default function WorkflowSurface() {
466
553
  }
467
554
 
468
555
  function serializeCurrentGraph() {
469
- return graphUnset ? "" : serializeOrchestrationGraph(orchestrationGraph);
556
+ return graphUnset ? "" : serializeOrchestrationGraph(withGraphSandboxRecordRefs(orchestrationGraph, objectId, rowId));
470
557
  }
471
558
 
472
559
  async function saveDraft(extraFields = {}) {
@@ -533,7 +620,8 @@ export default function WorkflowSurface() {
533
620
  const nextVersion = Number.isFinite(currentVersion) ? String(currentVersion + 1) : "1";
534
621
  const previousDeltas = Array.isArray(sandboxRow?.orchestrationDeltas) ? sandboxRow.orchestrationDeltas : [];
535
622
  const previousPublishedGraph = parseOrchestrationGraph(sandboxRow?.[effectiveFieldName]);
536
- const nodeDeltas = getNodeDeltaRecords(previousPublishedGraph, orchestrationGraph);
623
+ const graphWithRefs = withGraphSandboxRecordRefs(orchestrationGraph, objectId, rowId);
624
+ const nodeDeltas = getNodeDeltaRecords(previousPublishedGraph, graphWithRefs);
537
625
  const deltaTags = normalizeDeltaTags(nodeDeltas.flatMap((delta) => delta.deltaTags));
538
626
  const changeReason = nodeDeltas.map((delta) => delta.changeReason).filter(Boolean).join("\n");
539
627
  const next = patchSandboxRowInConfig(workspaceConfig, objectId, resolved.rowIndex, {
@@ -743,8 +831,12 @@ export default function WorkflowSurface() {
743
831
  function handleNodeConfigChange(configPatch) {
744
832
  if (!selectedNodeId) return;
745
833
  const { __nodePatch, ...configOnly } = configPatch || {};
834
+ const recordRef = nodeSandboxRecordRef(objectId, rowId, selectedNodeId);
746
835
  setOrchestrationGraph((g) => {
747
- const updated = updateGraphNode(g, selectedNodeId, configOnly);
836
+ const updated = updateGraphNode(g, selectedNodeId, {
837
+ ...configOnly,
838
+ sandboxRecordRef: recordRef
839
+ });
748
840
  if (!__nodePatch || typeof __nodePatch !== "object") return updated;
749
841
  const parsed = parseOrchestrationGraph(updated) || updated;
750
842
  return {
@@ -807,6 +899,73 @@ export default function WorkflowSurface() {
807
899
  const showSaveDraft = dirty && !graphUnset;
808
900
  const workflowModeLabel = isDraftMode ? "draft" : lifecycle || "live";
809
901
 
902
+ // Serverless upgrade — same derivation + cockpit as the sandbox/API lanes.
903
+ const upgradeState = deriveServerlessUpgradeState(workspaceConfig || {}, {
904
+ dismissed: readUiCacheFlag(workspaceConfig || {}, SERVERLESS_UPGRADE_DISMISS_FLAG),
905
+ });
906
+ const serverlessState = sandboxRow
907
+ ? deriveSandboxServerlessState({
908
+ sandboxRow,
909
+ workspaceConfig: workspaceConfig || {},
910
+ configuredEnvRefs: serverlessSignals.configuredEnvRefs,
911
+ persistenceAdapters: serverlessSignals.persistenceAdapters,
912
+ })
913
+ : null;
914
+ const isServerlessWorkflow = Boolean(serverlessState?.isServerless);
915
+
916
+ async function patchSandboxAndPersist(fields) {
917
+ if (resolved.rowIndex < 0 || !objectId || !workspaceConfig) return;
918
+ try {
919
+ const next = patchSandboxRowInConfig(workspaceConfig, objectId, resolved.rowIndex, fields);
920
+ await persistWorkspace(next);
921
+ setSaveMessage(fields.runLocality === "serverless"
922
+ ? "Upgraded to serverless. Link a scheduler and configure persistence to close the loop."
923
+ : fields.runLocality === "local"
924
+ ? "Reverted to local execution."
925
+ : "Saved.");
926
+ } catch (err) {
927
+ setSaveMessage(err.message || "Failed to save");
928
+ }
929
+ }
930
+
931
+ async function handleUpgradeAction(action) {
932
+ if (!action) return;
933
+ if (action.id === "toggle-locality") {
934
+ if (isServerlessWorkflow) {
935
+ await patchSandboxAndPersist({ runLocality: "local" });
936
+ return;
937
+ }
938
+ const registryRow = resolveRegistryRowForSandbox(workspaceConfig, sandboxRow);
939
+ const adapterId = String(sandboxRow?.adapter || "").trim();
940
+ await patchSandboxAndPersist({
941
+ runLocality: "serverless",
942
+ schedulerRegistryId: String(registryRow?.integrationId || "").trim(),
943
+ adapter: ["local-agent-host", "local-intelligence"].includes(adapterId) ? "local-process" : adapterId,
944
+ });
945
+ } else if (action.id === "open-settings") {
946
+ router.push(action.href || "/settings");
947
+ } else if (action.id === "link-scheduler") {
948
+ const registryRef = resolveRegistryRefForSandbox(workspaceConfig, sandboxRow);
949
+ if (registryRef?.object?.id && registryRef?.row?.integrationId) {
950
+ router.push(`/data-model?object=${encodeURIComponent(registryRef.object.id)}&row=${encodeURIComponent(registryRef.row.integrationId)}`);
951
+ } else {
952
+ router.push(`/data-model?object=${encodeURIComponent(objectId)}&row=${encodeURIComponent(rowId)}`);
953
+ }
954
+ } else if (action.id === "edit-adapter") {
955
+ // Full scheduler/adapter config lives on the sandbox object's drawer.
956
+ router.push(`/data-model?object=${encodeURIComponent(objectId)}&row=${encodeURIComponent(rowId)}`);
957
+ }
958
+ }
959
+
960
+ async function dismissUpgradeOnboarding() {
961
+ if (!workspaceConfig) return;
962
+ try {
963
+ await persistWorkspace(withUiCacheFlag(workspaceConfig, SERVERLESS_UPGRADE_DISMISS_FLAG, true));
964
+ } catch {
965
+ /* non-fatal */
966
+ }
967
+ }
968
+
810
969
  return (
811
970
  <main className="workspace-builder dm-workflow-page">
812
971
  <WorkspaceRail
@@ -838,7 +997,7 @@ export default function WorkflowSurface() {
838
997
  setAddTarget(null);
839
998
  }}
840
999
  >
841
- <ChevronDown size={14} />
1000
+ <ArrowDown size={13} />
842
1001
  </button>
843
1002
  <button
844
1003
  type="button"
@@ -852,8 +1011,20 @@ export default function WorkflowSurface() {
852
1011
  setAddTarget(null);
853
1012
  }}
854
1013
  >
855
- <ChevronUp size={14} />
1014
+ <ArrowUp size={13} />
856
1015
  </button>
1016
+ {sandboxRow && (
1017
+ <button
1018
+ type="button"
1019
+ className={"dm-workflow-icon-btn dm-workflow-upgrade-btn" + (isServerlessWorkflow ? " is-serverless" : (upgradeState.showOnboarding ? " is-pulse" : ""))}
1020
+ aria-label={isServerlessWorkflow ? "Serverless workflow — review persistence & scheduling" : "Upgrade to serverless environment to ensure persistence"}
1021
+ data-tooltip={isServerlessWorkflow ? "Serverless — review persistence & scheduling" : "Upgrade to serverless environment to ensure persistence"}
1022
+ aria-pressed={upgradeOpen}
1023
+ onClick={() => setUpgradeOpen((open) => !open)}
1024
+ >
1025
+ <ArrowUpCircle size={14} />
1026
+ </button>
1027
+ )}
857
1028
  {showDiscardDraft && (
858
1029
  <button
859
1030
  type="button"
@@ -920,6 +1091,43 @@ export default function WorkflowSurface() {
920
1091
  </div>
921
1092
  ) : null}
922
1093
 
1094
+ {/* One-time serverless upgrade onboarding — shows only when the operator
1095
+ has workflows but none are serverless, and hasn't dismissed it. */}
1096
+ {sandboxRow && !upgradeOpen && upgradeState.showOnboarding ? (
1097
+ <div className="workspace-template-context-banner dm-workflow-upgrade-nudge" role="note">
1098
+ <div>
1099
+ <strong>{upgradeState.headline}</strong>
1100
+ <span style={{ display: "block", marginTop: 2 }}>{upgradeState.subheadline}</span>
1101
+ </div>
1102
+ <div className="dm-workflow-upgrade-nudge-actions">
1103
+ <button type="button" className="dm-btn-primary-sm" onClick={() => setUpgradeOpen(true)}>
1104
+ <ArrowUpCircle size={13} /> Upgrade this workflow
1105
+ </button>
1106
+ <button type="button" className="dm-btn-ghost" onClick={dismissUpgradeOnboarding}>Not now</button>
1107
+ </div>
1108
+ </div>
1109
+ ) : null}
1110
+
1111
+ {/* Serverless cockpit — same derivation + cockpit interface as the API
1112
+ Registry and sandbox lanes. Toggles patch the sandbox row; deep config
1113
+ (scheduler/adapter) routes to the object's Data Model drawer. */}
1114
+ {sandboxRow && upgradeOpen && serverlessState ? (
1115
+ <div className="dm-workflow-upgrade-panel">
1116
+ <div className="dm-workflow-upgrade-panel-head">
1117
+ <span className="dm-api-action-card-eyebrow">Persistence &amp; scheduling</span>
1118
+ <button type="button" className="dm-workflow-icon-btn" aria-label="Close upgrade panel" onClick={() => { setUpgradeOpen(false); dismissUpgradeOnboarding(); }}>
1119
+ <X size={14} />
1120
+ </button>
1121
+ </div>
1122
+ <ApiRegistryCreationCockpit
1123
+ state={serverlessState}
1124
+ onAction={handleUpgradeAction}
1125
+ disabled={saving || publishing || running}
1126
+ eyebrow={isServerlessWorkflow ? "Serverless workflow" : "Upgrade to serverless"}
1127
+ />
1128
+ </div>
1129
+ ) : null}
1130
+
923
1131
  {loading ? (
924
1132
  <p className="dm-workflow-empty">Loading workflow…</p>
925
1133
  ) : error ? (
@@ -1023,6 +1231,8 @@ export default function WorkflowSurface() {
1023
1231
  </div>
1024
1232
  <AgentSwarmPanel
1025
1233
  graph={orchestrationGraph}
1234
+ objectId={objectId}
1235
+ rowName={rowId}
1026
1236
  disabled={false}
1027
1237
  onGraphChange={(updater) => {
1028
1238
  setOrchestrationGraph((g) => (typeof updater === "function" ? updater(g) : updater));
@@ -28,11 +28,9 @@ import path from "node:path";
28
28
  import { pathToFileURL } from "node:url";
29
29
 
30
30
  const staticLoaded = new Set();
31
- let staticLoadDone = false;
31
+ const nativeImport = new Function("specifier", "return import(specifier)");
32
32
 
33
33
  async function loadStaticResolversOnce() {
34
- if (staticLoadDone) return;
35
- staticLoadDone = true;
36
34
  const resolversDir = path.resolve(/*turbopackIgnore: true*/ process.cwd(), "lib/adapters/integrations/resolvers");
37
35
  try {
38
36
  const entries = await fs.readdir(resolversDir);
@@ -42,7 +40,7 @@ async function loadStaticResolversOnce() {
42
40
  if (staticLoaded.has(file)) return;
43
41
  try {
44
42
  const absolutePath = path.join(resolversDir, file);
45
- await import(/*turbopackIgnore: true*/ pathToFileURL(absolutePath).href);
43
+ await nativeImport(pathToFileURL(absolutePath).href);
46
44
  staticLoaded.add(file);
47
45
  } catch {
48
46
  // Malformed resolver — skip silently; operator needs to fix the file
@@ -34,6 +34,7 @@ import path from "node:path";
34
34
  import { registerSandboxAdapter } from "./sandbox-adapter-registry.js";
35
35
 
36
36
  const MAX_OUTPUT_BYTES = 1024 * 256;
37
+ const TELEMETRY_MARKER = "GROWTHUB_AGENT_TELEMETRY:";
37
38
 
38
39
  /**
39
40
  * Canonical Paperclip host catalog — slugs mirror `AGENT_ADAPTER_TYPES`.
@@ -118,6 +119,134 @@ function clampStream(buffer) {
118
119
  return `${head.toString("utf8")}\n…\n[output truncated at ${MAX_OUTPUT_BYTES} bytes]`;
119
120
  }
120
121
 
122
+ function safeNonNegativeInt(value) {
123
+ if (value === null || value === undefined || value === "") return null;
124
+ const n = Number(value);
125
+ if (!Number.isFinite(n) || n < 0) return null;
126
+ return Math.floor(n);
127
+ }
128
+
129
+ function pickFirstNumber(...values) {
130
+ for (const value of values) {
131
+ const n = safeNonNegativeInt(value);
132
+ if (n != null) return n;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ function sumNumbers(...values) {
138
+ let total = 0;
139
+ let seen = false;
140
+ for (const value of values) {
141
+ const n = safeNonNegativeInt(value);
142
+ if (n == null) continue;
143
+ total += n;
144
+ seen = true;
145
+ }
146
+ return seen ? total : null;
147
+ }
148
+
149
+ function extractUsageFromObject(obj) {
150
+ if (!obj || typeof obj !== "object" || Array.isArray(obj)) return { tokens: null, tools: null };
151
+ const usage = (obj.usage && typeof obj.usage === "object" && !Array.isArray(obj.usage))
152
+ ? obj.usage
153
+ : (obj.token_usage && typeof obj.token_usage === "object" && !Array.isArray(obj.token_usage))
154
+ ? obj.token_usage
155
+ : (obj.metadata?.usage && typeof obj.metadata.usage === "object" && !Array.isArray(obj.metadata.usage))
156
+ ? obj.metadata.usage
157
+ : (obj.result?.usage && typeof obj.result.usage === "object" && !Array.isArray(obj.result.usage))
158
+ ? obj.result.usage
159
+ : null;
160
+ const tokens = usage
161
+ ? pickFirstNumber(
162
+ usage.total_tokens,
163
+ usage.totalTokens,
164
+ usage.tokens,
165
+ sumNumbers(usage.input_tokens, usage.output_tokens),
166
+ sumNumbers(usage.prompt_tokens, usage.completion_tokens),
167
+ sumNumbers(usage.inputTokens, usage.outputTokens),
168
+ )
169
+ : pickFirstNumber(obj.total_tokens, obj.totalTokens, obj.tokens);
170
+ const toolArrays = [
171
+ obj.tool_calls,
172
+ obj.toolCalls,
173
+ obj.toolInvocations,
174
+ obj.message?.tool_calls,
175
+ obj.choices?.[0]?.message?.tool_calls,
176
+ obj.result?.tool_calls,
177
+ obj.result?.toolCalls,
178
+ ].filter(Array.isArray);
179
+ const tools = pickFirstNumber(
180
+ obj.tools,
181
+ obj.tool_count,
182
+ obj.toolCount,
183
+ ...toolArrays.map((items) => items.length),
184
+ );
185
+ return { tokens, tools };
186
+ }
187
+
188
+ function mergeTelemetry(base, next) {
189
+ return {
190
+ tokens: base.tokens ?? next.tokens ?? null,
191
+ tools: base.tools ?? next.tools ?? null,
192
+ };
193
+ }
194
+
195
+ function parseJsonMaybe(text) {
196
+ const value = String(text || "").trim();
197
+ if (!value || !/^[\[{]/.test(value)) return null;
198
+ try {
199
+ return JSON.parse(value);
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function extractMarkedTelemetry(text) {
206
+ let out = { tokens: null, tools: null };
207
+ for (const line of String(text || "").split(/\r?\n/)) {
208
+ const idx = line.indexOf(TELEMETRY_MARKER);
209
+ if (idx === -1) continue;
210
+ const json = line.slice(idx + TELEMETRY_MARKER.length).trim();
211
+ const parsed = parseJsonMaybe(json);
212
+ out = mergeTelemetry(out, extractUsageFromObject(parsed));
213
+ }
214
+ return out;
215
+ }
216
+
217
+ function extractJsonLineTelemetry(text) {
218
+ let out = { tokens: null, tools: null };
219
+ for (const line of String(text || "").split(/\r?\n/)) {
220
+ const parsed = parseJsonMaybe(line);
221
+ if (!parsed) continue;
222
+ out = mergeTelemetry(out, extractUsageFromObject(parsed));
223
+ }
224
+ return out;
225
+ }
226
+
227
+ function extractStderrTextTelemetry(stderrText) {
228
+ const text = String(stderrText || "");
229
+ const total = text.match(/\b(?:total\s+tokens|tokens\s+used)\s*(?:[:=]|\r?\n)\s*([0-9][0-9,]*)/i);
230
+ const input = text.match(/\b(?:input|prompt)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
231
+ const output = text.match(/\b(?:output|completion)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
232
+ const tools = text.match(/\b(?:tool\s+calls?|tools\s+used)\s*[:=]\s*([0-9][0-9,]*)/i);
233
+ const tokens = pickFirstNumber(total?.[1]?.replace(/,/g, ""), sumNumbers(input?.[1]?.replace(/,/g, ""), output?.[1]?.replace(/,/g, "")));
234
+ return {
235
+ tokens,
236
+ tools: pickFirstNumber(tools?.[1]?.replace(/,/g, ""), tokens != null ? 0 : null),
237
+ };
238
+ }
239
+
240
+ function extractAgentHostTelemetry({ stdout, stderr }) {
241
+ let out = { tokens: null, tools: null };
242
+ const stdoutJson = parseJsonMaybe(stdout);
243
+ if (stdoutJson && !Array.isArray(stdoutJson)) out = mergeTelemetry(out, extractUsageFromObject(stdoutJson));
244
+ out = mergeTelemetry(out, extractMarkedTelemetry(stderr));
245
+ out = mergeTelemetry(out, extractJsonLineTelemetry(stderr));
246
+ out = mergeTelemetry(out, extractStderrTextTelemetry(stderr));
247
+ return out;
248
+ }
249
+
121
250
  async function run(request) {
122
251
  const hostSlug = typeof request.agentHost === "string" ? request.agentHost.trim() : "";
123
252
  const host = HOST_CATALOG[hostSlug];
@@ -238,12 +367,15 @@ async function run(request) {
238
367
  clearTimeout(timer);
239
368
  const durationMs = Date.now() - startedAt;
240
369
  const ok = !timedOut && exitCode === 0;
370
+ const stdoutText = clampStream(stdout);
371
+ const stderrText = clampStream(stderr);
372
+ const telemetry = extractAgentHostTelemetry({ stdout: stdoutText, stderr: stderrText });
241
373
  resolve({
242
374
  ok,
243
375
  exitCode: typeof exitCode === "number" ? exitCode : null,
244
376
  durationMs,
245
- stdout: clampStream(stdout),
246
- stderr: clampStream(stderr),
377
+ stdout: stdoutText,
378
+ stderr: stderrText,
247
379
  error: timedOut
248
380
  ? `timed out after ${timeoutMs}ms`
249
381
  : (ok ? undefined : `exit ${exitCode ?? signal ?? "unknown"}`),
@@ -254,7 +386,10 @@ async function run(request) {
254
386
  argv,
255
387
  inputMode: host.inputMode,
256
388
  timedOut,
257
- signal: signal || null
389
+ signal: signal || null,
390
+ tokens: telemetry.tokens,
391
+ tools: telemetry.tools,
392
+ telemetrySource: telemetry.tokens != null || telemetry.tools != null ? "agent-host-reported" : "unreported"
258
393
  }
259
394
  });
260
395
  });
@@ -281,4 +416,4 @@ registerSandboxAdapter({
281
416
  run
282
417
  });
283
418
 
284
- export { HOST_CATALOG, SUPPORTED_HOSTS };
419
+ export { HOST_CATALOG, SUPPORTED_HOSTS, extractAgentHostTelemetry };
@@ -190,6 +190,10 @@ async function run(request) {
190
190
  endpoint,
191
191
  model,
192
192
  locality: "local",
193
+ // Truthful telemetry only — taken from the completion's usage block
194
+ // when the endpoint reports one, never estimated. Null means unknown.
195
+ tokens: Number.isFinite(outer?.usage?.total_tokens) ? outer.usage.total_tokens : null,
196
+ tools: Array.isArray(parsed.toolIntents) ? parsed.toolIntents.length : null,
193
197
  },
194
198
  };
195
199
  } catch (err) {