@growthub/cli 0.13.4 → 0.13.5
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/metadata-graph/route.js +184 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +16 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +49 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/workspace/metadata-graph
|
|
3
|
+
*
|
|
4
|
+
* Growthub Workspace Metadata Graph V1 — read-only projection.
|
|
5
|
+
*
|
|
6
|
+
* Combines the governed workspace config and the live source-record sidecar
|
|
7
|
+
* into a typed metadata store and a node/edge graph. Consumers (the UI
|
|
8
|
+
* inspector, the workspace helper agent, and external operators) use this
|
|
9
|
+
* envelope to ask dependency questions without re-deriving widget/workflow
|
|
10
|
+
* contracts inside every component.
|
|
11
|
+
*
|
|
12
|
+
* Optional query parameters:
|
|
13
|
+
* - staleKind: "object" | "field" | "sourceRecord" | "workflow" | "agentHost" | "widget"
|
|
14
|
+
* - staleId: the corresponding metadata id (for `field`, use
|
|
15
|
+
* "<objectId>::<fieldId>")
|
|
16
|
+
*
|
|
17
|
+
* When both are provided the response `stale.groups` and `stale.reasons`
|
|
18
|
+
* reflect `selectStaleMetadataGroups({ kind, id })`. When omitted the
|
|
19
|
+
* stale section returns the empty baseline.
|
|
20
|
+
*
|
|
21
|
+
* Authority invariants:
|
|
22
|
+
* - GET only. PATCH / POST / PUT / DELETE are not exposed. Writes still
|
|
23
|
+
* flow through the existing governed routes
|
|
24
|
+
* (`PATCH /api/workspace`, `POST /api/workspace/refresh-sources`,
|
|
25
|
+
* `POST /api/workspace/sandbox-run`).
|
|
26
|
+
* - growthub.config.json remains the authoritative artifact.
|
|
27
|
+
* - No secrets are returned. Field metadata derived from secret-shaped
|
|
28
|
+
* column names is marked `isSecret: true` but no value is echoed.
|
|
29
|
+
* - Failures during read OR projection fall back to an empty store with
|
|
30
|
+
* warnings — this route never throws.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { NextResponse } from "next/server";
|
|
34
|
+
import { readWorkspaceConfig, readWorkspaceSourceRecords } from "@/lib/workspace-config";
|
|
35
|
+
import { buildWorkspaceMetadataStore } from "@/lib/workspace-metadata-store";
|
|
36
|
+
import { buildWorkspaceMetadataGraph } from "@/lib/workspace-metadata-graph";
|
|
37
|
+
import { selectStaleMetadataGroups } from "@/lib/workspace-metadata-selectors";
|
|
38
|
+
|
|
39
|
+
const ENVELOPE_KIND = "growthub-workspace-metadata-graph-v1";
|
|
40
|
+
const ENVELOPE_VERSION = 1;
|
|
41
|
+
|
|
42
|
+
function emptyMetadataStore() {
|
|
43
|
+
return {
|
|
44
|
+
kind: "growthub-workspace-metadata-store-v1",
|
|
45
|
+
version: 1,
|
|
46
|
+
objects: [],
|
|
47
|
+
fields: [],
|
|
48
|
+
views: [],
|
|
49
|
+
filters: [],
|
|
50
|
+
sorts: [],
|
|
51
|
+
widgets: [],
|
|
52
|
+
dashboards: [],
|
|
53
|
+
workflows: [],
|
|
54
|
+
workflowNodes: [],
|
|
55
|
+
workflowActions: [],
|
|
56
|
+
runInputs: [],
|
|
57
|
+
agentHosts: [],
|
|
58
|
+
sandboxes: [],
|
|
59
|
+
integrations: [],
|
|
60
|
+
integrationEntities: [],
|
|
61
|
+
sourceRecords: [],
|
|
62
|
+
runs: [],
|
|
63
|
+
outputArtifacts: [],
|
|
64
|
+
workerKits: [],
|
|
65
|
+
pipelineHealth: [],
|
|
66
|
+
warnings: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function GET(request) {
|
|
71
|
+
const warnings = [];
|
|
72
|
+
|
|
73
|
+
let workspaceConfig = null;
|
|
74
|
+
try {
|
|
75
|
+
workspaceConfig = await readWorkspaceConfig();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
warnings.push(`Failed to read workspace config: ${error?.message || "unknown error"}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let workspaceSourceRecords = {};
|
|
81
|
+
try {
|
|
82
|
+
workspaceSourceRecords = (await readWorkspaceSourceRecords()) || {};
|
|
83
|
+
} catch (error) {
|
|
84
|
+
warnings.push(`Failed to read source records sidecar: ${error?.message || "unknown error"}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Defensive: helpers are designed to never throw on partial/unknown input,
|
|
88
|
+
// but the route must remain HTTP-200 even if an unexpected exception bubbles
|
|
89
|
+
// up (so the UI inspector and agents always get a typed envelope).
|
|
90
|
+
let metadataStore;
|
|
91
|
+
try {
|
|
92
|
+
metadataStore = buildWorkspaceMetadataStore({
|
|
93
|
+
workspaceConfig: workspaceConfig || {},
|
|
94
|
+
workspaceSourceRecords
|
|
95
|
+
});
|
|
96
|
+
warnings.push(...metadataStore.warnings);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
warnings.push(`Failed to build metadata store: ${error?.message || "unknown error"}`);
|
|
99
|
+
metadataStore = emptyMetadataStore();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let graph;
|
|
103
|
+
try {
|
|
104
|
+
graph = buildWorkspaceMetadataGraph(metadataStore);
|
|
105
|
+
warnings.push(...graph.warnings);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
warnings.push(`Failed to build metadata graph: ${error?.message || "unknown error"}`);
|
|
108
|
+
graph = { kind: "growthub-workspace-metadata-graph-v1", version: 1, nodes: [], edges: [], warnings: [] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Optional stale-group selector via query params.
|
|
112
|
+
let staleGroups = [];
|
|
113
|
+
let staleReasons = [];
|
|
114
|
+
try {
|
|
115
|
+
const url = request && request.url ? new URL(request.url) : null;
|
|
116
|
+
const staleKind = url ? (url.searchParams.get("staleKind") || "").trim() : "";
|
|
117
|
+
const staleId = url ? (url.searchParams.get("staleId") || "").trim() : "";
|
|
118
|
+
if (staleKind && staleId) {
|
|
119
|
+
const result = selectStaleMetadataGroups(metadataStore, { kind: staleKind, id: staleId });
|
|
120
|
+
staleGroups = Array.isArray(result?.groups) ? result.groups : [];
|
|
121
|
+
staleReasons = Array.isArray(result?.reasons) ? result.reasons : [];
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
warnings.push(`Failed to compute stale groups: ${error?.message || "unknown error"}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return NextResponse.json({
|
|
128
|
+
kind: ENVELOPE_KIND,
|
|
129
|
+
version: ENVELOPE_VERSION,
|
|
130
|
+
authority: {
|
|
131
|
+
config: "growthub.config.json",
|
|
132
|
+
sourceRecords: "growthub.source-records.json",
|
|
133
|
+
readOnlyProjection: true
|
|
134
|
+
},
|
|
135
|
+
metadata: {
|
|
136
|
+
objects: metadataStore.objects,
|
|
137
|
+
fields: metadataStore.fields,
|
|
138
|
+
views: metadataStore.views,
|
|
139
|
+
filters: metadataStore.filters,
|
|
140
|
+
sorts: metadataStore.sorts,
|
|
141
|
+
widgets: metadataStore.widgets,
|
|
142
|
+
dashboards: metadataStore.dashboards,
|
|
143
|
+
workflows: metadataStore.workflows,
|
|
144
|
+
workflowNodes: metadataStore.workflowNodes,
|
|
145
|
+
workflowActions: metadataStore.workflowActions,
|
|
146
|
+
runInputs: metadataStore.runInputs,
|
|
147
|
+
agentHosts: metadataStore.agentHosts,
|
|
148
|
+
sandboxes: metadataStore.sandboxes,
|
|
149
|
+
integrations: metadataStore.integrations,
|
|
150
|
+
integrationEntities: metadataStore.integrationEntities,
|
|
151
|
+
sourceRecords: metadataStore.sourceRecords,
|
|
152
|
+
runs: metadataStore.runs,
|
|
153
|
+
outputArtifacts: metadataStore.outputArtifacts,
|
|
154
|
+
workerKits: metadataStore.workerKits,
|
|
155
|
+
pipelineHealth: metadataStore.pipelineHealth
|
|
156
|
+
},
|
|
157
|
+
graph: {
|
|
158
|
+
nodes: graph.nodes,
|
|
159
|
+
edges: graph.edges
|
|
160
|
+
},
|
|
161
|
+
stale: {
|
|
162
|
+
groups: staleGroups,
|
|
163
|
+
reasons: staleReasons
|
|
164
|
+
},
|
|
165
|
+
warnings,
|
|
166
|
+
selectors: {
|
|
167
|
+
// Manifest of selectors the route honours. Only `selectStaleMetadataGroups`
|
|
168
|
+
// is wired through HTTP (via `?staleKind=&staleId=`). The remaining
|
|
169
|
+
// selectors are exposed as importable helpers for server-side consumers
|
|
170
|
+
// and the read-only inspector; they are NOT toggled through query
|
|
171
|
+
// params in V1.
|
|
172
|
+
httpEnabled: ["selectStaleMetadataGroups"],
|
|
173
|
+
helperOnly: [
|
|
174
|
+
"selectWidgetRequiredFields",
|
|
175
|
+
"selectWorkflowNodeInputSchema",
|
|
176
|
+
"selectObjectFilterableFields",
|
|
177
|
+
"selectObjectSortableFields",
|
|
178
|
+
"selectRunLineage"
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { GET };
|
|
@@ -392,6 +392,12 @@ function buildRunResponse({
|
|
|
392
392
|
base.runInputs = runInputs;
|
|
393
393
|
base.inputSummary = summarizeRunInputs(runInputs);
|
|
394
394
|
}
|
|
395
|
+
if (result && typeof result === "object" && result.swarm && typeof result.swarm === "object") {
|
|
396
|
+
base.swarm = result.swarm;
|
|
397
|
+
}
|
|
398
|
+
if (result && typeof result === "object" && Array.isArray(result.logTree)) {
|
|
399
|
+
base.logTree = result.logTree;
|
|
400
|
+
}
|
|
395
401
|
base.exports = {
|
|
396
402
|
available: ["download-json", "copy-output", "download-stdout", "download-stderr", "download-log-node"],
|
|
397
403
|
external: []
|
|
@@ -558,11 +564,28 @@ async function POST(request) {
|
|
|
558
564
|
workspaceConfig,
|
|
559
565
|
row: rowForRun,
|
|
560
566
|
timeoutMs,
|
|
561
|
-
runInputs: normalizedRunInputs
|
|
567
|
+
runInputs: normalizedRunInputs,
|
|
568
|
+
executionContext: {
|
|
569
|
+
runId,
|
|
570
|
+
ranAt,
|
|
571
|
+
runtime,
|
|
572
|
+
agentHost,
|
|
573
|
+
adapterId,
|
|
574
|
+
env,
|
|
575
|
+
envRefSlugs,
|
|
576
|
+
envRefsMissing,
|
|
577
|
+
envRefsResolved,
|
|
578
|
+
networkAllow,
|
|
579
|
+
allowList,
|
|
580
|
+
instructions,
|
|
581
|
+
command,
|
|
582
|
+
timeoutMs,
|
|
583
|
+
sandboxName: rowForRun.Name || name
|
|
584
|
+
}
|
|
562
585
|
});
|
|
563
586
|
if (graphResult !== null) {
|
|
564
587
|
result = graphResult;
|
|
565
|
-
effectiveAdapterId = "orchestration-graph";
|
|
588
|
+
effectiveAdapterId = String(graphResult?.adapterMeta?.adapter || "").trim() || "orchestration-graph";
|
|
566
589
|
}
|
|
567
590
|
}
|
|
568
591
|
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
5
|
+
import { HOST_AUTH_CATALOG } from "@/lib/sandbox-agent-host-catalog";
|
|
6
|
+
|
|
7
|
+
function getHostOptions() {
|
|
8
|
+
return Object.entries(HOST_AUTH_CATALOG || {}).map(([slug, host]) => ({
|
|
9
|
+
value: slug,
|
|
10
|
+
label: host?.label || slug
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function patchOrchestrator(graph, patch) {
|
|
15
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
16
|
+
return {
|
|
17
|
+
...graph,
|
|
18
|
+
nodes: nodes.map((node) =>
|
|
19
|
+
node?.type === "thinAdapter"
|
|
20
|
+
? { ...node, config: { ...(node.config || {}), ...patch } }
|
|
21
|
+
: node
|
|
22
|
+
)
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function patchSynthesis(graph, patch) {
|
|
27
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
28
|
+
return {
|
|
29
|
+
...graph,
|
|
30
|
+
nodes: nodes.map((node) =>
|
|
31
|
+
node?.type === "tool-result"
|
|
32
|
+
? { ...node, config: { ...(node.config || {}), ...patch } }
|
|
33
|
+
: node
|
|
34
|
+
)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function patchSubagent(graph, nodeId, patch) {
|
|
39
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
40
|
+
return {
|
|
41
|
+
...graph,
|
|
42
|
+
nodes: nodes.map((node) =>
|
|
43
|
+
node?.type === "ai-agent" && String(node.id) === String(nodeId)
|
|
44
|
+
? {
|
|
45
|
+
...node,
|
|
46
|
+
label: patch.role != null ? String(patch.role) : node.label,
|
|
47
|
+
config: { ...(node.config || {}), ...patch }
|
|
48
|
+
}
|
|
49
|
+
: node
|
|
50
|
+
)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addSubagent(graph, defaults = {}) {
|
|
55
|
+
const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
56
|
+
const edges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
57
|
+
const existing = new Set(nodes.map((n) => String(n?.id || "")));
|
|
58
|
+
let id = `subagent-${nodes.filter((n) => n?.type === "ai-agent").length + 1}`;
|
|
59
|
+
let index = 2;
|
|
60
|
+
while (existing.has(id)) {
|
|
61
|
+
id = `subagent-${index}`;
|
|
62
|
+
index += 1;
|
|
63
|
+
}
|
|
64
|
+
const node = {
|
|
65
|
+
id,
|
|
66
|
+
type: "ai-agent",
|
|
67
|
+
label: defaults.role || "Subagent",
|
|
68
|
+
subtitle: "Swarm subagent",
|
|
69
|
+
config: {
|
|
70
|
+
role: defaults.role || "Subagent",
|
|
71
|
+
description: defaults.description || "",
|
|
72
|
+
taskPrompt: defaults.taskPrompt || "",
|
|
73
|
+
tools: Array.isArray(defaults.tools) ? defaults.tools : [],
|
|
74
|
+
agentHost: defaults.agentHost || "",
|
|
75
|
+
required: true,
|
|
76
|
+
canReadWorkspace: true,
|
|
77
|
+
canWriteDraft: false,
|
|
78
|
+
networkAccess: false,
|
|
79
|
+
maxTokens: 0,
|
|
80
|
+
timeoutMs: 0
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const nextEdges = [...edges];
|
|
84
|
+
if (nodes.some((n) => n?.type === "thinAdapter")) {
|
|
85
|
+
nextEdges.push({ from: "orchestrator", to: id, passes: "subtask-assignment" });
|
|
86
|
+
}
|
|
87
|
+
if (nodes.some((n) => n?.type === "tool-result")) {
|
|
88
|
+
const synth = nodes.find((n) => n?.type === "tool-result");
|
|
89
|
+
nextEdges.push({ from: id, to: synth.id, passes: "subtask-result" });
|
|
90
|
+
}
|
|
91
|
+
return { ...graph, nodes: [...nodes, node], edges: nextEdges };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function removeSubagent(graph, nodeId) {
|
|
95
|
+
const id = String(nodeId);
|
|
96
|
+
return {
|
|
97
|
+
...graph,
|
|
98
|
+
nodes: (Array.isArray(graph?.nodes) ? graph.nodes : []).filter((n) => String(n.id) !== id),
|
|
99
|
+
edges: (Array.isArray(graph?.edges) ? graph.edges : []).filter(
|
|
100
|
+
(edge) => String(edge.from) !== id && String(edge.to) !== id
|
|
101
|
+
)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function patchSwarmConfig(graph, patch) {
|
|
106
|
+
const base = graph?.swarm && typeof graph.swarm === "object" ? graph.swarm : {};
|
|
107
|
+
return { ...graph, swarm: { ...base, ...patch } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function AgentSwarmPanel({ graph, onGraphChange, disabled }) {
|
|
111
|
+
const hostOptions = useMemo(getHostOptions, []);
|
|
112
|
+
if (!graph || typeof graph !== "object") return null;
|
|
113
|
+
|
|
114
|
+
const orchestrator = (Array.isArray(graph.nodes) ? graph.nodes : []).find((n) => n?.type === "thinAdapter") || null;
|
|
115
|
+
const subagents = (Array.isArray(graph.nodes) ? graph.nodes : []).filter((n) => n?.type === "ai-agent");
|
|
116
|
+
const synthesis = (Array.isArray(graph.nodes) ? graph.nodes : []).find((n) => n?.type === "tool-result") || null;
|
|
117
|
+
const swarmCfg = graph.swarm && typeof graph.swarm === "object" ? graph.swarm : {};
|
|
118
|
+
const weights = swarmCfg.rewardWeights || { parallel: 0.25, finish: 0.35, outcome: 0.4 };
|
|
119
|
+
|
|
120
|
+
function patchGraph(updater) {
|
|
121
|
+
if (typeof updater !== "function") return;
|
|
122
|
+
onGraphChange?.(updater);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="dm-orchestration-config dm-agent-swarm-panel">
|
|
127
|
+
<div className="dm-orchestration-config__pane">
|
|
128
|
+
<div className="dm-orchestration-config__section">
|
|
129
|
+
<span>Orchestrator</span>
|
|
130
|
+
<label className="dm-orchestration-config__field">
|
|
131
|
+
<span>Prompt</span>
|
|
132
|
+
<textarea
|
|
133
|
+
rows={3}
|
|
134
|
+
value={orchestrator?.config?.prompt || ""}
|
|
135
|
+
disabled={disabled || !orchestrator}
|
|
136
|
+
onChange={(e) => patchGraph((g) => patchOrchestrator(g, { prompt: e.target.value }))}
|
|
137
|
+
/>
|
|
138
|
+
</label>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="dm-orchestration-config__section">
|
|
142
|
+
<div className="dm-agent-swarm-panel__row">
|
|
143
|
+
<span>Subagents · {subagents.length}</span>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className="dm-btn-outline"
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
onClick={() => patchGraph((g) => addSubagent(g))}
|
|
149
|
+
>
|
|
150
|
+
<Plus size={12} aria-hidden="true" /> Add
|
|
151
|
+
</button>
|
|
152
|
+
</div>
|
|
153
|
+
{subagents.length === 0 && (
|
|
154
|
+
<p className="dm-orchestration-config__hint">Add at least one subagent.</p>
|
|
155
|
+
)}
|
|
156
|
+
{subagents.map((node) => {
|
|
157
|
+
const cfg = node.config || {};
|
|
158
|
+
return (
|
|
159
|
+
<div key={node.id} className="dm-agent-swarm-panel__subagent">
|
|
160
|
+
<div className="dm-agent-swarm-panel__row">
|
|
161
|
+
<input
|
|
162
|
+
className="dm-agent-swarm-panel__role"
|
|
163
|
+
placeholder="Role"
|
|
164
|
+
value={cfg.role || node.label || ""}
|
|
165
|
+
disabled={disabled}
|
|
166
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { role: e.target.value }))}
|
|
167
|
+
/>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
className="dm-btn-ghost"
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
onClick={() => patchGraph((g) => removeSubagent(g, node.id))}
|
|
173
|
+
aria-label={`Remove ${cfg.role || node.id}`}
|
|
174
|
+
>
|
|
175
|
+
<Trash2 size={12} aria-hidden="true" />
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
<label className="dm-orchestration-config__field">
|
|
179
|
+
<span>Description</span>
|
|
180
|
+
<input
|
|
181
|
+
placeholder="One-sentence charter"
|
|
182
|
+
value={cfg.description || ""}
|
|
183
|
+
disabled={disabled}
|
|
184
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { description: e.target.value }))}
|
|
185
|
+
/>
|
|
186
|
+
</label>
|
|
187
|
+
<label className="dm-orchestration-config__field">
|
|
188
|
+
<span>Task</span>
|
|
189
|
+
<textarea
|
|
190
|
+
rows={2}
|
|
191
|
+
value={cfg.taskPrompt || ""}
|
|
192
|
+
disabled={disabled}
|
|
193
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { taskPrompt: e.target.value }))}
|
|
194
|
+
/>
|
|
195
|
+
</label>
|
|
196
|
+
<label className="dm-orchestration-config__field">
|
|
197
|
+
<span>Tools</span>
|
|
198
|
+
<input
|
|
199
|
+
placeholder="read, summarize"
|
|
200
|
+
value={Array.isArray(cfg.tools) ? cfg.tools.join(", ") : ""}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, {
|
|
203
|
+
tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean)
|
|
204
|
+
}))}
|
|
205
|
+
/>
|
|
206
|
+
</label>
|
|
207
|
+
<label className="dm-orchestration-config__field">
|
|
208
|
+
<span>Max tokens</span>
|
|
209
|
+
<input
|
|
210
|
+
type="number"
|
|
211
|
+
min="0"
|
|
212
|
+
placeholder="0 = inherit"
|
|
213
|
+
value={cfg.maxTokens || 0}
|
|
214
|
+
disabled={disabled}
|
|
215
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { maxTokens: Math.max(0, Number(e.target.value) || 0) }))}
|
|
216
|
+
/>
|
|
217
|
+
</label>
|
|
218
|
+
<label className="dm-orchestration-config__field">
|
|
219
|
+
<span>Agent host</span>
|
|
220
|
+
<select
|
|
221
|
+
value={cfg.agentHost || ""}
|
|
222
|
+
disabled={disabled}
|
|
223
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { agentHost: e.target.value }))}
|
|
224
|
+
>
|
|
225
|
+
<option value="">Inherit</option>
|
|
226
|
+
{hostOptions.map((opt) => (
|
|
227
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
228
|
+
))}
|
|
229
|
+
</select>
|
|
230
|
+
</label>
|
|
231
|
+
<label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
|
|
232
|
+
<input
|
|
233
|
+
type="checkbox"
|
|
234
|
+
checked={cfg.required !== false}
|
|
235
|
+
disabled={disabled}
|
|
236
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { required: e.target.checked }))}
|
|
237
|
+
/>
|
|
238
|
+
<span>Required</span>
|
|
239
|
+
</label>
|
|
240
|
+
<label
|
|
241
|
+
className="dm-orchestration-config__field dm-orchestration-config__field-inline"
|
|
242
|
+
title="Network is granted only when both this and the row's networkAllow are on."
|
|
243
|
+
>
|
|
244
|
+
<input
|
|
245
|
+
type="checkbox"
|
|
246
|
+
checked={cfg.networkAccess === true}
|
|
247
|
+
disabled={disabled}
|
|
248
|
+
onChange={(e) => patchGraph((g) => patchSubagent(g, node.id, { networkAccess: e.target.checked }))}
|
|
249
|
+
/>
|
|
250
|
+
<span>Network</span>
|
|
251
|
+
</label>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
})}
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
<div className="dm-orchestration-config__section">
|
|
258
|
+
<span>Concurrency & reward</span>
|
|
259
|
+
<label className="dm-orchestration-config__field">
|
|
260
|
+
<span>Max concurrency</span>
|
|
261
|
+
<input
|
|
262
|
+
type="number"
|
|
263
|
+
min="1"
|
|
264
|
+
value={swarmCfg.maxConcurrency ?? subagents.length}
|
|
265
|
+
disabled={disabled}
|
|
266
|
+
onChange={(e) => patchGraph((g) => patchSwarmConfig(g, { maxConcurrency: Math.max(1, Number(e.target.value) || 1) }))}
|
|
267
|
+
/>
|
|
268
|
+
</label>
|
|
269
|
+
<div className="dm-agent-swarm-panel__weights">
|
|
270
|
+
{[
|
|
271
|
+
["parallel", "Parallel"],
|
|
272
|
+
["finish", "Finish"],
|
|
273
|
+
["outcome", "Outcome"]
|
|
274
|
+
].map(([key, label]) => (
|
|
275
|
+
<label key={key} className="dm-orchestration-config__field">
|
|
276
|
+
<span>{label}</span>
|
|
277
|
+
<input
|
|
278
|
+
type="number"
|
|
279
|
+
step="0.05"
|
|
280
|
+
min="0"
|
|
281
|
+
max="1"
|
|
282
|
+
value={weights[key] ?? 0}
|
|
283
|
+
disabled={disabled}
|
|
284
|
+
onChange={(e) =>
|
|
285
|
+
patchGraph((g) =>
|
|
286
|
+
patchSwarmConfig(g, {
|
|
287
|
+
rewardWeights: { ...weights, [key]: Number(e.target.value) || 0 }
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
/>
|
|
292
|
+
</label>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
<label className="dm-orchestration-config__field">
|
|
296
|
+
<span>Outcome criteria</span>
|
|
297
|
+
<textarea
|
|
298
|
+
rows={2}
|
|
299
|
+
value={swarmCfg.outcomeCriteria || ""}
|
|
300
|
+
disabled={disabled}
|
|
301
|
+
onChange={(e) => patchGraph((g) => patchSwarmConfig(g, { outcomeCriteria: e.target.value }))}
|
|
302
|
+
/>
|
|
303
|
+
</label>
|
|
304
|
+
<p className="dm-orchestration-config__hint">
|
|
305
|
+
Outcome is parsed from the synthesizer's last <code>OUTCOME_SCORE: 0–1</code>; otherwise falls back to required-completion.
|
|
306
|
+
</p>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{synthesis && (
|
|
310
|
+
<div className="dm-orchestration-config__section">
|
|
311
|
+
<span>Synthesizer</span>
|
|
312
|
+
<label className="dm-orchestration-config__field">
|
|
313
|
+
<span>Prompt</span>
|
|
314
|
+
<textarea
|
|
315
|
+
rows={2}
|
|
316
|
+
value={synthesis?.config?.outcomePrompt || ""}
|
|
317
|
+
disabled={disabled}
|
|
318
|
+
onChange={(e) => patchGraph((g) => patchSynthesis(g, { outcomePrompt: e.target.value }))}
|
|
319
|
+
/>
|
|
320
|
+
</label>
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
@@ -5,6 +5,7 @@ import { useState } from "react";
|
|
|
5
5
|
export function OrchestrationGraphEmptyCanvas({
|
|
6
6
|
onStartFromRegistry,
|
|
7
7
|
onStartBlank,
|
|
8
|
+
onStartAgentSwarm,
|
|
8
9
|
onPasteGraph,
|
|
9
10
|
disabled
|
|
10
11
|
}) {
|
|
@@ -23,6 +24,11 @@ export function OrchestrationGraphEmptyCanvas({
|
|
|
23
24
|
<button type="button" className="dm-btn-outline" disabled={disabled} onClick={onStartBlank}>
|
|
24
25
|
Start blank
|
|
25
26
|
</button>
|
|
27
|
+
{onStartAgentSwarm && (
|
|
28
|
+
<button type="button" className="dm-btn-outline" disabled={disabled} onClick={onStartAgentSwarm}>
|
|
29
|
+
Add Agent Swarm
|
|
30
|
+
</button>
|
|
31
|
+
)}
|
|
26
32
|
</div>
|
|
27
33
|
<details
|
|
28
34
|
className="dm-orchestration-canvas__paste"
|
|
@@ -897,7 +897,94 @@ export function OrchestrationNodeConfigPanel({
|
|
|
897
897
|
</div>
|
|
898
898
|
)}
|
|
899
899
|
|
|
900
|
-
{activeTab === "configuration" && type === "ai-agent" && (
|
|
900
|
+
{activeTab === "configuration" && type === "ai-agent" && (config.role || config.taskPrompt || config.required != null) && (
|
|
901
|
+
<div className="dm-orchestration-config__pane">
|
|
902
|
+
<label className="dm-orchestration-config__field">
|
|
903
|
+
<span>Role</span>
|
|
904
|
+
<input
|
|
905
|
+
value={config.role || node.label || ""}
|
|
906
|
+
disabled={disabled}
|
|
907
|
+
onChange={(e) => patchConfig({ role: e.target.value, __nodePatch: { label: e.target.value } })}
|
|
908
|
+
/>
|
|
909
|
+
</label>
|
|
910
|
+
<label className="dm-orchestration-config__field">
|
|
911
|
+
<span>Description</span>
|
|
912
|
+
<input
|
|
913
|
+
placeholder="One-sentence charter"
|
|
914
|
+
value={config.description || ""}
|
|
915
|
+
disabled={disabled}
|
|
916
|
+
onChange={(e) => patchConfig({ description: e.target.value })}
|
|
917
|
+
/>
|
|
918
|
+
</label>
|
|
919
|
+
<label className="dm-orchestration-config__field">
|
|
920
|
+
<span>Task</span>
|
|
921
|
+
<textarea
|
|
922
|
+
rows={4}
|
|
923
|
+
value={config.taskPrompt || ""}
|
|
924
|
+
disabled={disabled}
|
|
925
|
+
onChange={(e) => patchConfig({ taskPrompt: e.target.value })}
|
|
926
|
+
/>
|
|
927
|
+
</label>
|
|
928
|
+
<label className="dm-orchestration-config__field">
|
|
929
|
+
<span>Tools</span>
|
|
930
|
+
<input
|
|
931
|
+
placeholder="read, summarize"
|
|
932
|
+
value={Array.isArray(config.tools) ? config.tools.join(", ") : ""}
|
|
933
|
+
disabled={disabled}
|
|
934
|
+
onChange={(e) => patchConfig({
|
|
935
|
+
tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean)
|
|
936
|
+
})}
|
|
937
|
+
/>
|
|
938
|
+
</label>
|
|
939
|
+
<label className="dm-orchestration-config__field">
|
|
940
|
+
<span>Max tokens</span>
|
|
941
|
+
<input
|
|
942
|
+
type="number"
|
|
943
|
+
min="0"
|
|
944
|
+
placeholder="0 = inherit"
|
|
945
|
+
value={config.maxTokens || 0}
|
|
946
|
+
disabled={disabled}
|
|
947
|
+
onChange={(e) => patchConfig({ maxTokens: Math.max(0, Number(e.target.value) || 0) })}
|
|
948
|
+
/>
|
|
949
|
+
</label>
|
|
950
|
+
<label className="dm-orchestration-config__field">
|
|
951
|
+
<span>Agent host</span>
|
|
952
|
+
<select
|
|
953
|
+
value={config.agentHost || ""}
|
|
954
|
+
disabled={disabled}
|
|
955
|
+
onChange={(e) => patchConfig({ agentHost: e.target.value })}
|
|
956
|
+
>
|
|
957
|
+
<option value="">Inherit</option>
|
|
958
|
+
{Object.entries(HOST_AUTH_CATALOG || {}).map(([slug, host]) => (
|
|
959
|
+
<option key={slug} value={slug}>{host?.label || slug}</option>
|
|
960
|
+
))}
|
|
961
|
+
</select>
|
|
962
|
+
</label>
|
|
963
|
+
<label className="dm-orchestration-config__field dm-orchestration-config__field-inline">
|
|
964
|
+
<input
|
|
965
|
+
type="checkbox"
|
|
966
|
+
checked={config.required !== false}
|
|
967
|
+
disabled={disabled}
|
|
968
|
+
onChange={(e) => patchConfig({ required: e.target.checked })}
|
|
969
|
+
/>
|
|
970
|
+
<span>Required</span>
|
|
971
|
+
</label>
|
|
972
|
+
<label
|
|
973
|
+
className="dm-orchestration-config__field dm-orchestration-config__field-inline"
|
|
974
|
+
title="Network is granted only when both this and the row's networkAllow are on."
|
|
975
|
+
>
|
|
976
|
+
<input
|
|
977
|
+
type="checkbox"
|
|
978
|
+
checked={config.networkAccess === true}
|
|
979
|
+
disabled={disabled}
|
|
980
|
+
onChange={(e) => patchConfig({ networkAccess: e.target.checked })}
|
|
981
|
+
/>
|
|
982
|
+
<span>Network</span>
|
|
983
|
+
</label>
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
986
|
+
|
|
987
|
+
{activeTab === "configuration" && type === "ai-agent" && !(config.role || config.taskPrompt || config.required != null) && (
|
|
901
988
|
<div className="dm-orchestration-config__pane">
|
|
902
989
|
<label className="dm-orchestration-config__field">
|
|
903
990
|
<span>Model</span>
|