@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
- 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 (!
|
|
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
|
|
77
|
-
if (
|
|
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
|
-
|
|
446
|
-
|
|
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
|
|
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,
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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 & 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
|
-
|
|
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
|
|
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:
|
|
246
|
-
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) {
|