@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
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Env Status V1 — the honest, secret-safe "which referenced env keys actually
3
+ * resolve right now" signal.
4
+ *
5
+ * The creation cockpit (api-registry drawer) cannot read process.env in the
6
+ * browser, so auth readiness must come from a server signal. This module is the
7
+ * pure core of `GET /api/workspace/env-status`: given the governed config and
8
+ * the runtime environment it returns the set of *referenced* auth/env ref slugs
9
+ * whose candidate keys resolve to a value — slugs only, never a value.
10
+ *
11
+ * Pure + env-injectable so it is deterministically testable.
12
+ */
13
+
14
+ import { describePostgresAdapter } from "./adapters/persistence/postgres.js";
15
+ import { describeQstashKvAdapter } from "./adapters/persistence/qstash-kv.js";
16
+ import { describeProviderManagedAdapter } from "./adapters/persistence/provider-managed.js";
17
+
18
+ function clean(value) {
19
+ return String(value == null ? "" : value).trim();
20
+ }
21
+
22
+ /** Canonical UPPER_SNAKE candidate expansion for a logical ref. */
23
+ function envKeyCandidates(ref) {
24
+ const token = clean(ref).replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
25
+ if (!token) return [];
26
+ return Array.from(new Set([token, `${token}_API_KEY`, `${token}_TOKEN`]));
27
+ }
28
+
29
+ /**
30
+ * Collect every auth/env ref slug referenced by the governed config:
31
+ * - api-registry rows: authRef
32
+ * - data-source rows: authRef
33
+ * - sandbox-environment rows: envRefs (comma-separated)
34
+ * Returns the original ref strings (deduped), preserving the operator's casing
35
+ * so the cockpit can match them against a registry row's authRef.
36
+ */
37
+ function collectReferencedRefs(workspaceConfig) {
38
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
39
+ const refs = new Set();
40
+ for (const object of objects) {
41
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
42
+ if (object?.objectType === "api-registry" || object?.objectType === "data-source") {
43
+ for (const row of rows) {
44
+ const ref = clean(row?.authRef);
45
+ if (ref) refs.add(ref);
46
+ }
47
+ }
48
+ if (object?.objectType === "sandbox-environment") {
49
+ for (const row of rows) {
50
+ for (const part of clean(row?.envRefs).split(",")) {
51
+ const ref = clean(part);
52
+ if (ref) refs.add(ref);
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return Array.from(refs);
58
+ }
59
+
60
+ /**
61
+ * Return the referenced refs whose candidate keys resolve in `env`.
62
+ * `env` is injectable (defaults to process.env). Never returns a value.
63
+ */
64
+ function computeConfiguredEnvRefs(workspaceConfig, env = process.env) {
65
+ const source = env && typeof env === "object" ? env : {};
66
+ const resolves = (ref) => envKeyCandidates(ref).some((key) => Boolean(source[key]));
67
+ return collectReferencedRefs(workspaceConfig).filter(resolves);
68
+ }
69
+
70
+ /**
71
+ * Persistence/serverless adapter env-readiness — single-sourced from the real
72
+ * thin-adapter descriptors (postgres / qstash-kv / provider-managed). These are
73
+ * the durable-runtime layers a serverless workflow needs; the cockpit surfaces
74
+ * exactly which are env-ready so "make this workflow persistent + scheduled"
75
+ * has an honest, actionable signal. Slugs/booleans only — never a value.
76
+ */
77
+ function listPersistenceAdapterReadiness(env = process.env) {
78
+ const source = env && typeof env === "object" ? env : {};
79
+ const descriptors = [describePostgresAdapter(), describeQstashKvAdapter(), describeProviderManagedAdapter()];
80
+ return descriptors.map((d) => {
81
+ const requiredEnv = Array.isArray(d.requiredEnv) ? d.requiredEnv : [];
82
+ const missingEnv = requiredEnv.filter((k) => !source[k]);
83
+ return {
84
+ id: d.id,
85
+ label: d.label,
86
+ mode: d.mode,
87
+ requiredEnv,
88
+ // provider-managed needs no env (the deploy provider owns persistence).
89
+ configured: requiredEnv.length === 0 ? true : missingEnv.length === 0,
90
+ missingEnv,
91
+ };
92
+ });
93
+ }
94
+
95
+ export {
96
+ envKeyCandidates,
97
+ collectReferencedRefs,
98
+ computeConfiguredEnvRefs,
99
+ listPersistenceAdapterReadiness,
100
+ };
@@ -63,6 +63,44 @@ const MAX_SUBAGENT_OUTPUT_FOR_SYNTH = 4096;
63
63
  const PROMPT_CAPABLE_ADAPTERS = new Set(["local-agent-host", "local-intelligence"]);
64
64
  const OUTCOME_SCORE_RE = /OUTCOME_SCORE\s*[:=]\s*([01](?:\.\d+)?|\.\d+)/i;
65
65
 
66
+ function cleanEventString(value) {
67
+ return redactSecretsFromText(String(value ?? "").trim());
68
+ }
69
+
70
+ function emitSwarmDelta(executionContext, payload) {
71
+ const emit = executionContext?.onEvent;
72
+ if (typeof emit !== "function") return;
73
+ try {
74
+ emit({
75
+ kind: "growthub-sandbox-run-delta-v1",
76
+ emittedAt: new Date().toISOString(),
77
+ runId: executionContext.runId || "",
78
+ ...payload
79
+ });
80
+ } catch {
81
+ // Observability is best-effort; execution must not fail because the
82
+ // browser disconnected or a cosmetic stream consumer rejected an event.
83
+ }
84
+ }
85
+
86
+ function taskDeltaFromResult(task) {
87
+ return {
88
+ taskId: cleanEventString(task?.taskId || task?.nodeId || ""),
89
+ nodeId: cleanEventString(task?.nodeId || task?.taskId || ""),
90
+ role: cleanEventString(task?.role || "Subagent"),
91
+ phaseId: cleanEventString(task?.phaseId || "dispatch") || "dispatch",
92
+ status: cleanEventString(task?.status || "unknown") || "unknown",
93
+ adapter: cleanEventString(task?.adapter || ""),
94
+ agentHost: cleanEventString(task?.agentHost || ""),
95
+ durationMs: Number.isFinite(Number(task?.durationMs)) ? Number(task.durationMs) : 0,
96
+ tokens: task?.tokens ?? null,
97
+ tools: task?.tools ?? null,
98
+ startedAt: cleanEventString(task?.startedAt || ""),
99
+ endedAt: cleanEventString(task?.endedAt || ""),
100
+ error: cleanEventString(task?.error || "")
101
+ };
102
+ }
103
+
66
104
  function clampPositiveInt(value, fallback) {
67
105
  const n = Number(value);
68
106
  if (!Number.isFinite(n) || n <= 0) return fallback;
@@ -114,6 +152,21 @@ function chooseAdapterIdForSubagent({ subagentConfig, fallbackAdapterId, fallbac
114
152
  };
115
153
  }
116
154
 
155
+ /**
156
+ * Truthful telemetry extraction (SWARM_RUN_CONTRACT_V1). Tokens / tool counts
157
+ * come ONLY from the adapter's reported metadata — when the adapter does not
158
+ * report a number the value is null, never an estimate.
159
+ */
160
+ function extractAdapterTelemetry(result) {
161
+ const meta = result?.adapterMeta && typeof result.adapterMeta === "object" ? result.adapterMeta : {};
162
+ const tokens = meta.tokens == null || meta.tokens === "" ? null : Number(meta.tokens);
163
+ const tools = meta.tools == null || meta.tools === "" ? null : Number(meta.tools);
164
+ return {
165
+ tokens: Number.isFinite(tokens) && tokens >= 0 ? tokens : null,
166
+ tools: Number.isFinite(tools) && tools >= 0 ? tools : null
167
+ };
168
+ }
169
+
117
170
  function describeSubagent(node) {
118
171
  const cfg = node?.config || {};
119
172
  const role = String(cfg.role || node?.label || node?.id || "subagent").trim();
@@ -240,7 +293,8 @@ async function runThroughAdapter({
240
293
  envRefsMissing,
241
294
  runId,
242
295
  name,
243
- ranAt
296
+ ranAt,
297
+ intelligence
244
298
  }) {
245
299
  const adapter = getSandboxAdapter(adapterId);
246
300
  if (!adapter) {
@@ -281,7 +335,20 @@ async function runThroughAdapter({
281
335
  envRefSlugs,
282
336
  envRefsMissing,
283
337
  workdir,
284
- ranAt: ranAt || new Date(startedAt).toISOString()
338
+ ranAt: ranAt || new Date(startedAt).toISOString(),
339
+ // local-intelligence speaks the intelligenceSandbox envelope — the
340
+ // phase prompt travels as userIntent; model/endpoint settings come
341
+ // from the governed row (slugs/URLs only, never secret values).
342
+ ...(adapterId === "local-intelligence"
343
+ ? {
344
+ intelligenceSandbox: {
345
+ userIntent: command,
346
+ localModel: String(intelligence?.localModel || "").trim(),
347
+ localEndpoint: String(intelligence?.localEndpoint || "").trim(),
348
+ intelligenceAdapterMode: String(intelligence?.intelligenceAdapterMode || "ollama").trim() || "ollama"
349
+ }
350
+ }
351
+ : {})
285
352
  });
286
353
  } catch (error) {
287
354
  return {
@@ -324,6 +391,7 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
324
391
  const command = buildOrchestratorCommand({ orchestratorNode, subagents, inputPayload });
325
392
  const env = { ...(executionContext.env || {}), GROWTHUB_SWARM_PHASE: "orchestrator" };
326
393
  const startedAt = Date.now();
394
+ const startedAtIso = new Date(startedAt).toISOString();
327
395
  const result = await runThroughAdapter({
328
396
  adapterId: resolved.adapterId,
329
397
  agentHost: resolved.agentHost,
@@ -335,11 +403,13 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
335
403
  env,
336
404
  envRefSlugs: executionContext.envRefSlugs || [],
337
405
  envRefsMissing: executionContext.envRefsMissing || [],
406
+ intelligence: executionContext.intelligence,
338
407
  runId: `${executionContext.runId}_orchestrator`,
339
408
  name: `${executionContext.sandboxName || "swarm"}::orchestrator`
340
409
  });
341
410
  const stdout = redactSecretsFromText(result?.stdout || "");
342
411
  const errorText = redactSecretsFromText(result?.error || "");
412
+ const telemetry = extractAdapterTelemetry(result);
343
413
  return {
344
414
  status: result?.ok === true && !errorText ? "completed" : "failed",
345
415
  error: errorText,
@@ -349,6 +419,11 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
349
419
  output: stdout,
350
420
  stderr: redactSecretsFromText(result?.stderr || ""),
351
421
  plan: stdout,
422
+ tokens: telemetry.tokens,
423
+ tools: telemetry.tools,
424
+ startedAt: startedAtIso,
425
+ endedAt: new Date().toISOString(),
426
+ phaseId: "plan",
352
427
  adapterMeta: { ...(result?.adapterMeta || {}), swarmPhase: "orchestrator" }
353
428
  };
354
429
  }
@@ -364,6 +439,10 @@ async function dispatchSubagentTask({
364
439
  const required = subagentConfig.required !== false;
365
440
  const taskId = subagentNode?.id || `task-${taskIndex + 1}`;
366
441
  const role = String(subagentConfig.role || subagentNode?.label || "subagent");
442
+ // Author-named phase (config.phase / config.phaseId) travels onto the task
443
+ // so the record projection groups under the same phases the declared
444
+ // skeleton shows. Absent → "dispatch", exactly as before.
445
+ const phaseId = String(subagentConfig.phase || subagentConfig.phaseId || "").trim().toLowerCase() || "dispatch";
367
446
 
368
447
  const resolved = chooseAdapterIdForSubagent({
369
448
  subagentConfig,
@@ -371,6 +450,7 @@ async function dispatchSubagentTask({
371
450
  fallbackAgentHost: executionContext.agentHost
372
451
  });
373
452
  if (!resolved.adapterId || resolved.error) {
453
+ const gateAt = new Date().toISOString();
374
454
  return {
375
455
  taskId,
376
456
  nodeId: taskId,
@@ -383,6 +463,11 @@ async function dispatchSubagentTask({
383
463
  stdout: "",
384
464
  stderr: "",
385
465
  error: resolved.error || "no prompt-capable adapter resolved for subagent",
466
+ tokens: null,
467
+ tools: null,
468
+ startedAt: gateAt,
469
+ endedAt: gateAt,
470
+ phaseId,
386
471
  adapterMeta: { swarmSubagent: true, reason: "adapter-gate" }
387
472
  };
388
473
  }
@@ -407,11 +492,13 @@ async function dispatchSubagentTask({
407
492
  env,
408
493
  envRefSlugs: executionContext.envRefSlugs || [],
409
494
  envRefsMissing: executionContext.envRefsMissing || [],
495
+ intelligence: executionContext.intelligence,
410
496
  runId: `${executionContext.runId}_${taskId}`,
411
497
  name: `${executionContext.sandboxName || "swarm"}::${taskId}`
412
498
  });
413
499
  const errorText = redactSecretsFromText(result?.error || "");
414
500
  const ok = result?.ok === true && !errorText;
501
+ const telemetry = extractAdapterTelemetry(result);
415
502
  return {
416
503
  taskId,
417
504
  nodeId: taskId,
@@ -425,6 +512,11 @@ async function dispatchSubagentTask({
425
512
  stdout: redactSecretsFromText(result?.stdout || ""),
426
513
  stderr: redactSecretsFromText(result?.stderr || ""),
427
514
  error: errorText,
515
+ tokens: telemetry.tokens,
516
+ tools: telemetry.tools,
517
+ startedAt: new Date(startedAt).toISOString(),
518
+ endedAt: new Date().toISOString(),
519
+ phaseId,
428
520
  adapterMeta: { ...(result?.adapterMeta || {}), swarmSubagent: true }
429
521
  };
430
522
  }
@@ -467,6 +559,7 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
467
559
  const command = buildSynthesisCommand({ synthesisNode, swarmConfig, tasks, inputPayload });
468
560
  const env = { ...(executionContext.env || {}), GROWTHUB_SWARM_PHASE: "synthesis" };
469
561
  const startedAt = Date.now();
562
+ const startedAtIso = new Date(startedAt).toISOString();
470
563
  const result = await runThroughAdapter({
471
564
  adapterId: resolved.adapterId,
472
565
  agentHost: resolved.agentHost,
@@ -478,6 +571,7 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
478
571
  env,
479
572
  envRefSlugs: executionContext.envRefSlugs || [],
480
573
  envRefsMissing: executionContext.envRefsMissing || [],
574
+ intelligence: executionContext.intelligence,
481
575
  runId: `${executionContext.runId}_synthesis`,
482
576
  name: `${executionContext.sandboxName || "swarm"}::synthesis`
483
577
  });
@@ -485,6 +579,7 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
485
579
  const errorText = redactSecretsFromText(result?.error || "");
486
580
  const match = stdout.match(OUTCOME_SCORE_RE);
487
581
  const parsedOutcomeScore = match ? clamp01(match[1]) : null;
582
+ const telemetry = extractAdapterTelemetry(result);
488
583
  return {
489
584
  status: result?.ok === true && !errorText ? "completed" : "failed",
490
585
  ranSynthesis: true,
@@ -495,6 +590,11 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
495
590
  adapter: resolved.adapterId,
496
591
  agentHost: resolved.agentHost || "",
497
592
  parsedOutcomeScore,
593
+ tokens: telemetry.tokens,
594
+ tools: telemetry.tools,
595
+ startedAt: startedAtIso,
596
+ endedAt: new Date().toISOString(),
597
+ phaseId: "synthesize",
498
598
  adapterMeta: { ...(result?.adapterMeta || {}), swarmPhase: "synthesis" }
499
599
  };
500
600
  }
@@ -723,18 +823,63 @@ async function runAgentSwarmGraphIfPresent({
723
823
  networkAllow: executionContext?.networkAllow === true,
724
824
  allowList: executionContext?.allowList || [],
725
825
  timeoutMs: clampPositiveInt(timeoutMs, DEFAULT_SUBAGENT_TIMEOUT_MS),
726
- sandboxName: executionContext?.sandboxName || row?.Name || "swarm"
826
+ sandboxName: executionContext?.sandboxName || row?.Name || "swarm",
827
+ onEvent: executionContext?.onEvent,
828
+ // Row-configured local-intelligence settings (model id, endpoint URL,
829
+ // adapter mode) — configuration only, never secret values.
830
+ intelligence: {
831
+ localModel: String(row?.localModel || "").trim(),
832
+ localEndpoint: String(row?.localEndpoint || "").trim(),
833
+ intelligenceAdapterMode: String(row?.intelligenceAdapterMode || "").trim() || "ollama"
834
+ }
727
835
  };
728
836
 
729
837
  const startedAt = Date.now();
838
+ emitSwarmDelta(ctx, {
839
+ type: "swarm.run.started",
840
+ status: "running",
841
+ name: cleanEventString(ctx.sandboxName),
842
+ adapter: cleanEventString(ctx.adapterId),
843
+ agentHost: cleanEventString(ctx.agentHost),
844
+ maxConcurrency,
845
+ phaseCount: 3,
846
+ taskCount: subagents.length
847
+ });
730
848
 
731
849
  // Phase 1: Plan ----------------------------------------------------------
850
+ emitSwarmDelta(ctx, {
851
+ type: "swarm.phase.started",
852
+ phaseId: "plan",
853
+ label: "Plan",
854
+ status: "running"
855
+ });
732
856
  const orchestratorResult = await runOrchestratorPhase({
733
857
  orchestratorNode: orchestrator,
734
858
  subagents,
735
859
  inputPayload: manualPayload,
736
860
  executionContext: ctx
737
861
  });
862
+ emitSwarmDelta(ctx, {
863
+ type: orchestratorResult.status === "failed" ? "swarm.phase.failed" : "swarm.phase.completed",
864
+ phaseId: "plan",
865
+ label: "Plan",
866
+ status: orchestratorResult.status,
867
+ agent: {
868
+ taskId: cleanEventString(orchestrator.id || "orchestrator"),
869
+ nodeId: cleanEventString(orchestrator.id || "orchestrator"),
870
+ role: cleanEventString(orchestrator?.config?.role || "Orchestrator"),
871
+ phaseId: "plan",
872
+ status: orchestratorResult.status,
873
+ adapter: cleanEventString(orchestratorResult.adapter || ""),
874
+ agentHost: cleanEventString(orchestratorResult.agentHost || ""),
875
+ durationMs: Number(orchestratorResult.durationMs) || 0,
876
+ tokens: orchestratorResult.tokens ?? null,
877
+ tools: orchestratorResult.tools ?? null,
878
+ startedAt: cleanEventString(orchestratorResult.startedAt || ""),
879
+ endedAt: cleanEventString(orchestratorResult.endedAt || ""),
880
+ error: cleanEventString(orchestratorResult.error || "")
881
+ }
882
+ });
738
883
 
739
884
  if (orchestratorResult.status === "failed" && String(orchestratorResult.error || "").length > 0) {
740
885
  const durationMs = Date.now() - startedAt;
@@ -747,6 +892,13 @@ async function runAgentSwarmGraphIfPresent({
747
892
  outcomeOk: false,
748
893
  synthesisResult: null
749
894
  });
895
+ emitSwarmDelta(ctx, {
896
+ type: "swarm.run.failed",
897
+ status: "failed",
898
+ phaseFailed: "plan",
899
+ durationMs,
900
+ error: cleanEventString(orchestratorResult.error || "")
901
+ });
750
902
  return {
751
903
  ok: false,
752
904
  exitCode: 1,
@@ -790,23 +942,68 @@ async function runAgentSwarmGraphIfPresent({
790
942
  // Phase 2: Dispatch ------------------------------------------------------
791
943
  let observedParallelism = 0;
792
944
  let activeNow = 0;
945
+ emitSwarmDelta(ctx, {
946
+ type: "swarm.phase.started",
947
+ phaseId: "dispatch",
948
+ label: "Dispatch",
949
+ status: "running",
950
+ taskCount: subagents.length
951
+ });
793
952
  const tasks = await runSubagentsWithConcurrency(subagents, maxConcurrency, async (subagentNode, index) => {
794
953
  activeNow += 1;
795
954
  if (activeNow > observedParallelism) observedParallelism = activeNow;
955
+ const subagentConfig = subagentNode?.config || {};
956
+ const taskId = subagentNode?.id || `task-${index + 1}`;
957
+ const phaseId = String(subagentConfig.phase || subagentConfig.phaseId || "").trim().toLowerCase() || "dispatch";
958
+ emitSwarmDelta(ctx, {
959
+ type: "swarm.task.started",
960
+ phaseId,
961
+ taskId: cleanEventString(taskId),
962
+ nodeId: cleanEventString(taskId),
963
+ role: cleanEventString(subagentConfig.role || subagentNode?.label || `Agent ${index + 1}`),
964
+ status: "running",
965
+ active: activeNow,
966
+ observedParallelism
967
+ });
796
968
  try {
797
- return await dispatchSubagentTask({
969
+ const task = await dispatchSubagentTask({
798
970
  subagentNode,
799
971
  orchestratorPlan: orchestratorResult.plan,
800
972
  inputPayload: manualPayload,
801
973
  executionContext: ctx,
802
974
  taskIndex: index
803
975
  });
976
+ emitSwarmDelta(ctx, {
977
+ type: task.status === "completed" ? "swarm.task.completed" : "swarm.task.failed",
978
+ phaseId: task.phaseId || phaseId,
979
+ task: taskDeltaFromResult(task)
980
+ });
981
+ return task;
804
982
  } finally {
805
983
  activeNow -= 1;
806
984
  }
807
985
  });
986
+ const dispatchStatus = tasks.length > 0 && tasks.every((t) => t.status === "completed")
987
+ ? "completed"
988
+ : tasks.some((t) => t.status === "failed")
989
+ ? "failed"
990
+ : "completed";
991
+ emitSwarmDelta(ctx, {
992
+ type: dispatchStatus === "failed" ? "swarm.phase.failed" : "swarm.phase.completed",
993
+ phaseId: "dispatch",
994
+ label: "Dispatch",
995
+ status: dispatchStatus,
996
+ taskCount: tasks.length,
997
+ observedParallelism
998
+ });
808
999
 
809
1000
  // Phase 3: Synthesize ----------------------------------------------------
1001
+ emitSwarmDelta(ctx, {
1002
+ type: "swarm.phase.started",
1003
+ phaseId: "synthesize",
1004
+ label: "Synthesize",
1005
+ status: "running"
1006
+ });
810
1007
  const synthesisResult = await runSynthesisPhase({
811
1008
  synthesisNode: synthesis,
812
1009
  swarmConfig,
@@ -814,6 +1011,29 @@ async function runAgentSwarmGraphIfPresent({
814
1011
  inputPayload: manualPayload,
815
1012
  executionContext: ctx
816
1013
  });
1014
+ emitSwarmDelta(ctx, {
1015
+ type: synthesisResult.status === "failed" ? "swarm.phase.failed" : "swarm.phase.completed",
1016
+ phaseId: "synthesize",
1017
+ label: "Synthesize",
1018
+ status: synthesisResult.status,
1019
+ agent: synthesisResult.ranSynthesis
1020
+ ? {
1021
+ taskId: cleanEventString(synthesis?.id || "synthesis"),
1022
+ nodeId: cleanEventString(synthesis?.id || "synthesis"),
1023
+ role: cleanEventString(synthesis?.label || "Synthesizer"),
1024
+ phaseId: "synthesize",
1025
+ status: synthesisResult.status,
1026
+ adapter: cleanEventString(synthesisResult.adapter || ""),
1027
+ agentHost: cleanEventString(synthesisResult.agentHost || ""),
1028
+ durationMs: Number(synthesisResult.durationMs) || 0,
1029
+ tokens: synthesisResult.tokens ?? null,
1030
+ tools: synthesisResult.tools ?? null,
1031
+ startedAt: cleanEventString(synthesisResult.startedAt || ""),
1032
+ endedAt: cleanEventString(synthesisResult.endedAt || ""),
1033
+ error: cleanEventString(synthesisResult.error || "")
1034
+ }
1035
+ : null
1036
+ });
817
1037
 
818
1038
  const durationMs = Date.now() - startedAt;
819
1039
  const requiredTasks = tasks.filter((t) => t.required);
@@ -863,6 +1083,15 @@ async function runAgentSwarmGraphIfPresent({
863
1083
  errorText = `synthesizer returned OUTCOME_SCORE ${semanticScore} (< 0.5)`;
864
1084
  }
865
1085
 
1086
+ emitSwarmDelta(ctx, {
1087
+ type: outcomeOk ? "swarm.run.completed" : "swarm.run.failed",
1088
+ status: swarmStatus,
1089
+ durationMs,
1090
+ observedParallelism: observedParallelism || (subagents.length === 1 ? 1 : 0),
1091
+ reward,
1092
+ error: cleanEventString(errorText || "")
1093
+ });
1094
+
866
1095
  return {
867
1096
  ok: outcomeOk,
868
1097
  exitCode: outcomeOk ? 0 : 1,
@@ -888,6 +1117,12 @@ async function runAgentSwarmGraphIfPresent({
888
1117
  adapter: orchestratorResult.adapter,
889
1118
  agentHost: orchestratorResult.agentHost,
890
1119
  durationMs: orchestratorResult.durationMs,
1120
+ adapterMeta: orchestratorResult.adapterMeta || null,
1121
+ tokens: orchestratorResult.tokens ?? null,
1122
+ tools: orchestratorResult.tools ?? null,
1123
+ startedAt: orchestratorResult.startedAt || "",
1124
+ endedAt: orchestratorResult.endedAt || "",
1125
+ phaseId: "plan",
891
1126
  plan: clampText(orchestratorResult.plan, 4000)
892
1127
  },
893
1128
  tasks,
@@ -902,6 +1137,12 @@ async function runAgentSwarmGraphIfPresent({
902
1137
  adapter: synthesisResult.adapter,
903
1138
  agentHost: synthesisResult.agentHost,
904
1139
  durationMs: synthesisResult.durationMs,
1140
+ adapterMeta: synthesisResult.adapterMeta || null,
1141
+ tokens: synthesisResult.tokens ?? null,
1142
+ tools: synthesisResult.tools ?? null,
1143
+ startedAt: synthesisResult.startedAt || "",
1144
+ endedAt: synthesisResult.endedAt || "",
1145
+ phaseId: "synthesize",
905
1146
  answer: clampText(synthesisResult.output, 4000),
906
1147
  parsedOutcomeScore: synthesisResult.parsedOutcomeScore
907
1148
  }
@@ -914,6 +1155,7 @@ async function runAgentSwarmGraphIfPresent({
914
1155
  export {
915
1156
  runAgentSwarmGraphIfPresent,
916
1157
  computeRewardTelemetry,
1158
+ extractAdapterTelemetry,
917
1159
  buildOrchestratorCommand,
918
1160
  buildSubtaskCommand,
919
1161
  buildSynthesisCommand,
@@ -364,6 +364,67 @@ function buildSandboxRowFromApiRegistry(workspaceConfig, registryRow, options =
364
364
  };
365
365
  }
366
366
 
367
+ /**
368
+ * Find existing data-source rows that already resolve through a given API
369
+ * Registry integration (by `registryId`). Mirrors findSandboxRowsForRegistry so
370
+ * the drawer can refuse to create a duplicate Data Source for the same API.
371
+ */
372
+ function findDataSourceRowsForRegistry(workspaceConfig, integrationId) {
373
+ const id = String(integrationId || "").trim();
374
+ if (!id) return [];
375
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
376
+ const rows = [];
377
+ for (const object of objects) {
378
+ if (object?.objectType !== "data-source") continue;
379
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
380
+ if (String(row?.registryId || "").trim() === id) rows.push(row);
381
+ }
382
+ }
383
+ return rows;
384
+ }
385
+
386
+ /**
387
+ * Build a governed Data Source row from a tested API Registry row. The Data
388
+ * Source references the registry entry by `registryId` (the existing
389
+ * resolver-binding relation) and keeps auth as an `authRef` slug only — the
390
+ * secret never lands on the row. Shape matches the OBJECT_TYPE_PRESETS
391
+ * "data-source" columns so it slots straight into the data-source table.
392
+ */
393
+ function buildDataSourceRowFromApiRegistry(workspaceConfig, registryRow, options = {}) {
394
+ const integrationId = String(registryRow?.integrationId || "").trim();
395
+ const baseName = String(options.name || registryRow?.Name || integrationId || "Data Source").trim();
396
+ const name = baseName.endsWith(" Source") ? baseName : `${baseName} Source`;
397
+ const entityType = String(
398
+ options.entityType || registryRow?.entityTypes || "records"
399
+ ).split(",")[0].trim() || "records";
400
+ const sourceId = String(
401
+ options.sourceId || slugifyName(`${integrationId || baseName}-${entityType}`) || slugifyName(baseName)
402
+ ).trim();
403
+ const sourceStorage = String(options.sourceStorage || "workspace-source-records").trim();
404
+ return {
405
+ Name: name,
406
+ slug: options.slug || slugifyName(name) || slugifyName(integrationId),
407
+ objectType: "data-source",
408
+ registryId: integrationId,
409
+ endpoint: String(registryRow?.endpoint || "").trim(),
410
+ authRef: String(options.authRef || registryRow?.authRef || integrationId).trim(),
411
+ baseUrl: String(registryRow?.baseUrl || "").trim(),
412
+ method: String(registryRow?.method || "GET").trim().toUpperCase(),
413
+ status: "draft",
414
+ lastTested: "",
415
+ lastResponse: "",
416
+ entityType,
417
+ sourceId,
418
+ sourceStorage,
419
+ resolverTemplateId: String(options.resolverTemplateId || registryRow?.resolverTemplateId || "").trim(),
420
+ description: String(
421
+ options.description
422
+ || registryRow?.description
423
+ || `Data Source for ${integrationId || baseName} — resolves ${entityType} through the API Registry resolver. authRef ${String(options.authRef || registryRow?.authRef || integrationId).trim()} only; secrets resolve server-side.`
424
+ ).trim()
425
+ };
426
+ }
427
+
367
428
  function extractNodeByType(graph, type) {
368
429
  const parsed = parseOrchestrationGraph(graph) || graph;
369
430
  if (!parsed?.nodes) return null;
@@ -783,6 +844,12 @@ function buildDefaultAgentSwarmGraph(options = {}) {
783
844
  role: String(agent.role || agent.id || "Subagent"),
784
845
  description: String(agent.description || "").trim(),
785
846
  taskPrompt: String(agent.taskPrompt || "").trim(),
847
+ // Optional author-named phase — groups this subagent in the cockpit
848
+ // (e.g. "ping" / "echo" / "verify"). Absent → the single Dispatch
849
+ // phase, exactly as before.
850
+ ...(String(agent.phase || agent.phaseId || "").trim()
851
+ ? { phase: String(agent.phase || agent.phaseId).trim().toLowerCase() }
852
+ : {}),
786
853
  tools: Array.isArray(agent.tools) ? agent.tools.map((t) => String(t || "").trim()).filter(Boolean) : [],
787
854
  agentHost: String(agent.agentHost || agentHost || "").trim(),
788
855
  adapter: String(agent.adapter || "").trim(),
@@ -918,6 +985,8 @@ export {
918
985
  getNextCanonicalNodeId,
919
986
  addCanonicalNodeToGraph,
920
987
  buildSandboxRowFromApiRegistry,
988
+ buildDataSourceRowFromApiRegistry,
989
+ findDataSourceRowsForRegistry,
921
990
  extractApiRegistryCallNode,
922
991
  extractInputNode,
923
992
  extractTransformConfig,