@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.
Files changed (23) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +16 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +49 -4
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +14 -1
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  23. 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>