@growthub/cli 0.14.0 → 0.14.2

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 (40) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  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 +179 -117
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  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 +136 -3
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  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-schema.js +6 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  40. package/package.json +1 -1
@@ -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();
@@ -235,12 +288,14 @@ async function runThroughAdapter({
235
288
  timeoutMs,
236
289
  networkAllow,
237
290
  allowList,
291
+ browserAccess,
238
292
  env,
239
293
  envRefSlugs,
240
294
  envRefsMissing,
241
295
  runId,
242
296
  name,
243
- ranAt
297
+ ranAt,
298
+ intelligence
244
299
  }) {
245
300
  const adapter = getSandboxAdapter(adapterId);
246
301
  if (!adapter) {
@@ -277,11 +332,25 @@ async function runThroughAdapter({
277
332
  timeoutMs,
278
333
  networkAllow,
279
334
  allowList,
335
+ browserAccess: browserAccess === true,
280
336
  env,
281
337
  envRefSlugs,
282
338
  envRefsMissing,
283
339
  workdir,
284
- ranAt: ranAt || new Date(startedAt).toISOString()
340
+ ranAt: ranAt || new Date(startedAt).toISOString(),
341
+ // local-intelligence speaks the intelligenceSandbox envelope — the
342
+ // phase prompt travels as userIntent; model/endpoint settings come
343
+ // from the governed row (slugs/URLs only, never secret values).
344
+ ...(adapterId === "local-intelligence"
345
+ ? {
346
+ intelligenceSandbox: {
347
+ userIntent: command,
348
+ localModel: String(intelligence?.localModel || "").trim(),
349
+ localEndpoint: String(intelligence?.localEndpoint || "").trim(),
350
+ intelligenceAdapterMode: String(intelligence?.intelligenceAdapterMode || "ollama").trim() || "ollama"
351
+ }
352
+ }
353
+ : {})
285
354
  });
286
355
  } catch (error) {
287
356
  return {
@@ -324,6 +393,7 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
324
393
  const command = buildOrchestratorCommand({ orchestratorNode, subagents, inputPayload });
325
394
  const env = { ...(executionContext.env || {}), GROWTHUB_SWARM_PHASE: "orchestrator" };
326
395
  const startedAt = Date.now();
396
+ const startedAtIso = new Date(startedAt).toISOString();
327
397
  const result = await runThroughAdapter({
328
398
  adapterId: resolved.adapterId,
329
399
  agentHost: resolved.agentHost,
@@ -332,14 +402,17 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
332
402
  timeoutMs: clampPositiveInt(orchestratorNode?.config?.timeoutMs, DEFAULT_ORCHESTRATOR_TIMEOUT_MS),
333
403
  networkAllow: executionContext.networkAllow === true,
334
404
  allowList: executionContext.allowList || [],
405
+ browserAccess: executionContext.browserAccess === true,
335
406
  env,
336
407
  envRefSlugs: executionContext.envRefSlugs || [],
337
408
  envRefsMissing: executionContext.envRefsMissing || [],
409
+ intelligence: executionContext.intelligence,
338
410
  runId: `${executionContext.runId}_orchestrator`,
339
411
  name: `${executionContext.sandboxName || "swarm"}::orchestrator`
340
412
  });
341
413
  const stdout = redactSecretsFromText(result?.stdout || "");
342
414
  const errorText = redactSecretsFromText(result?.error || "");
415
+ const telemetry = extractAdapterTelemetry(result);
343
416
  return {
344
417
  status: result?.ok === true && !errorText ? "completed" : "failed",
345
418
  error: errorText,
@@ -349,6 +422,11 @@ async function runOrchestratorPhase({ orchestratorNode, subagents, inputPayload,
349
422
  output: stdout,
350
423
  stderr: redactSecretsFromText(result?.stderr || ""),
351
424
  plan: stdout,
425
+ tokens: telemetry.tokens,
426
+ tools: telemetry.tools,
427
+ startedAt: startedAtIso,
428
+ endedAt: new Date().toISOString(),
429
+ phaseId: "plan",
352
430
  adapterMeta: { ...(result?.adapterMeta || {}), swarmPhase: "orchestrator" }
353
431
  };
354
432
  }
@@ -364,6 +442,10 @@ async function dispatchSubagentTask({
364
442
  const required = subagentConfig.required !== false;
365
443
  const taskId = subagentNode?.id || `task-${taskIndex + 1}`;
366
444
  const role = String(subagentConfig.role || subagentNode?.label || "subagent");
445
+ // Author-named phase (config.phase / config.phaseId) travels onto the task
446
+ // so the record projection groups under the same phases the declared
447
+ // skeleton shows. Absent → "dispatch", exactly as before.
448
+ const phaseId = String(subagentConfig.phase || subagentConfig.phaseId || "").trim().toLowerCase() || "dispatch";
367
449
 
368
450
  const resolved = chooseAdapterIdForSubagent({
369
451
  subagentConfig,
@@ -371,6 +453,7 @@ async function dispatchSubagentTask({
371
453
  fallbackAgentHost: executionContext.agentHost
372
454
  });
373
455
  if (!resolved.adapterId || resolved.error) {
456
+ const gateAt = new Date().toISOString();
374
457
  return {
375
458
  taskId,
376
459
  nodeId: taskId,
@@ -383,6 +466,11 @@ async function dispatchSubagentTask({
383
466
  stdout: "",
384
467
  stderr: "",
385
468
  error: resolved.error || "no prompt-capable adapter resolved for subagent",
469
+ tokens: null,
470
+ tools: null,
471
+ startedAt: gateAt,
472
+ endedAt: gateAt,
473
+ phaseId,
386
474
  adapterMeta: { swarmSubagent: true, reason: "adapter-gate" }
387
475
  };
388
476
  }
@@ -403,15 +491,20 @@ async function dispatchSubagentTask({
403
491
  command,
404
492
  timeoutMs: clampPositiveInt(subagentConfig.timeoutMs, executionContext.timeoutMs || DEFAULT_SUBAGENT_TIMEOUT_MS),
405
493
  networkAllow: subagentConfig.networkAccess === true && executionContext.networkAllow === true,
494
+ // Browser is a superset of network: a subagent only inherits the row's
495
+ // browser access through the same node-level network gate.
496
+ browserAccess: subagentConfig.networkAccess === true && executionContext.browserAccess === true,
406
497
  allowList: executionContext.allowList || [],
407
498
  env,
408
499
  envRefSlugs: executionContext.envRefSlugs || [],
409
500
  envRefsMissing: executionContext.envRefsMissing || [],
501
+ intelligence: executionContext.intelligence,
410
502
  runId: `${executionContext.runId}_${taskId}`,
411
503
  name: `${executionContext.sandboxName || "swarm"}::${taskId}`
412
504
  });
413
505
  const errorText = redactSecretsFromText(result?.error || "");
414
506
  const ok = result?.ok === true && !errorText;
507
+ const telemetry = extractAdapterTelemetry(result);
415
508
  return {
416
509
  taskId,
417
510
  nodeId: taskId,
@@ -425,6 +518,11 @@ async function dispatchSubagentTask({
425
518
  stdout: redactSecretsFromText(result?.stdout || ""),
426
519
  stderr: redactSecretsFromText(result?.stderr || ""),
427
520
  error: errorText,
521
+ tokens: telemetry.tokens,
522
+ tools: telemetry.tools,
523
+ startedAt: new Date(startedAt).toISOString(),
524
+ endedAt: new Date().toISOString(),
525
+ phaseId,
428
526
  adapterMeta: { ...(result?.adapterMeta || {}), swarmSubagent: true }
429
527
  };
430
528
  }
@@ -467,6 +565,7 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
467
565
  const command = buildSynthesisCommand({ synthesisNode, swarmConfig, tasks, inputPayload });
468
566
  const env = { ...(executionContext.env || {}), GROWTHUB_SWARM_PHASE: "synthesis" };
469
567
  const startedAt = Date.now();
568
+ const startedAtIso = new Date(startedAt).toISOString();
470
569
  const result = await runThroughAdapter({
471
570
  adapterId: resolved.adapterId,
472
571
  agentHost: resolved.agentHost,
@@ -475,9 +574,11 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
475
574
  timeoutMs: clampPositiveInt(cfg.timeoutMs, DEFAULT_SYNTHESIS_TIMEOUT_MS),
476
575
  networkAllow: executionContext.networkAllow === true,
477
576
  allowList: executionContext.allowList || [],
577
+ browserAccess: executionContext.browserAccess === true,
478
578
  env,
479
579
  envRefSlugs: executionContext.envRefSlugs || [],
480
580
  envRefsMissing: executionContext.envRefsMissing || [],
581
+ intelligence: executionContext.intelligence,
481
582
  runId: `${executionContext.runId}_synthesis`,
482
583
  name: `${executionContext.sandboxName || "swarm"}::synthesis`
483
584
  });
@@ -485,6 +586,7 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
485
586
  const errorText = redactSecretsFromText(result?.error || "");
486
587
  const match = stdout.match(OUTCOME_SCORE_RE);
487
588
  const parsedOutcomeScore = match ? clamp01(match[1]) : null;
589
+ const telemetry = extractAdapterTelemetry(result);
488
590
  return {
489
591
  status: result?.ok === true && !errorText ? "completed" : "failed",
490
592
  ranSynthesis: true,
@@ -495,6 +597,11 @@ async function runSynthesisPhase({ synthesisNode, swarmConfig, tasks, inputPaylo
495
597
  adapter: resolved.adapterId,
496
598
  agentHost: resolved.agentHost || "",
497
599
  parsedOutcomeScore,
600
+ tokens: telemetry.tokens,
601
+ tools: telemetry.tools,
602
+ startedAt: startedAtIso,
603
+ endedAt: new Date().toISOString(),
604
+ phaseId: "synthesize",
498
605
  adapterMeta: { ...(result?.adapterMeta || {}), swarmPhase: "synthesis" }
499
606
  };
500
607
  }
@@ -722,19 +829,65 @@ async function runAgentSwarmGraphIfPresent({
722
829
  envRefsMissing: executionContext?.envRefsMissing || [],
723
830
  networkAllow: executionContext?.networkAllow === true,
724
831
  allowList: executionContext?.allowList || [],
832
+ browserAccess: executionContext?.browserAccess === true,
725
833
  timeoutMs: clampPositiveInt(timeoutMs, DEFAULT_SUBAGENT_TIMEOUT_MS),
726
- sandboxName: executionContext?.sandboxName || row?.Name || "swarm"
834
+ sandboxName: executionContext?.sandboxName || row?.Name || "swarm",
835
+ onEvent: executionContext?.onEvent,
836
+ // Row-configured local-intelligence settings (model id, endpoint URL,
837
+ // adapter mode) — configuration only, never secret values.
838
+ intelligence: {
839
+ localModel: String(row?.localModel || "").trim(),
840
+ localEndpoint: String(row?.localEndpoint || "").trim(),
841
+ intelligenceAdapterMode: String(row?.intelligenceAdapterMode || "").trim() || "ollama"
842
+ }
727
843
  };
728
844
 
729
845
  const startedAt = Date.now();
846
+ emitSwarmDelta(ctx, {
847
+ type: "swarm.run.started",
848
+ status: "running",
849
+ name: cleanEventString(ctx.sandboxName),
850
+ adapter: cleanEventString(ctx.adapterId),
851
+ agentHost: cleanEventString(ctx.agentHost),
852
+ maxConcurrency,
853
+ phaseCount: 3,
854
+ taskCount: subagents.length
855
+ });
730
856
 
731
857
  // Phase 1: Plan ----------------------------------------------------------
858
+ emitSwarmDelta(ctx, {
859
+ type: "swarm.phase.started",
860
+ phaseId: "plan",
861
+ label: "Plan",
862
+ status: "running"
863
+ });
732
864
  const orchestratorResult = await runOrchestratorPhase({
733
865
  orchestratorNode: orchestrator,
734
866
  subagents,
735
867
  inputPayload: manualPayload,
736
868
  executionContext: ctx
737
869
  });
870
+ emitSwarmDelta(ctx, {
871
+ type: orchestratorResult.status === "failed" ? "swarm.phase.failed" : "swarm.phase.completed",
872
+ phaseId: "plan",
873
+ label: "Plan",
874
+ status: orchestratorResult.status,
875
+ agent: {
876
+ taskId: cleanEventString(orchestrator.id || "orchestrator"),
877
+ nodeId: cleanEventString(orchestrator.id || "orchestrator"),
878
+ role: cleanEventString(orchestrator?.config?.role || "Orchestrator"),
879
+ phaseId: "plan",
880
+ status: orchestratorResult.status,
881
+ adapter: cleanEventString(orchestratorResult.adapter || ""),
882
+ agentHost: cleanEventString(orchestratorResult.agentHost || ""),
883
+ durationMs: Number(orchestratorResult.durationMs) || 0,
884
+ tokens: orchestratorResult.tokens ?? null,
885
+ tools: orchestratorResult.tools ?? null,
886
+ startedAt: cleanEventString(orchestratorResult.startedAt || ""),
887
+ endedAt: cleanEventString(orchestratorResult.endedAt || ""),
888
+ error: cleanEventString(orchestratorResult.error || "")
889
+ }
890
+ });
738
891
 
739
892
  if (orchestratorResult.status === "failed" && String(orchestratorResult.error || "").length > 0) {
740
893
  const durationMs = Date.now() - startedAt;
@@ -747,6 +900,13 @@ async function runAgentSwarmGraphIfPresent({
747
900
  outcomeOk: false,
748
901
  synthesisResult: null
749
902
  });
903
+ emitSwarmDelta(ctx, {
904
+ type: "swarm.run.failed",
905
+ status: "failed",
906
+ phaseFailed: "plan",
907
+ durationMs,
908
+ error: cleanEventString(orchestratorResult.error || "")
909
+ });
750
910
  return {
751
911
  ok: false,
752
912
  exitCode: 1,
@@ -790,23 +950,68 @@ async function runAgentSwarmGraphIfPresent({
790
950
  // Phase 2: Dispatch ------------------------------------------------------
791
951
  let observedParallelism = 0;
792
952
  let activeNow = 0;
953
+ emitSwarmDelta(ctx, {
954
+ type: "swarm.phase.started",
955
+ phaseId: "dispatch",
956
+ label: "Dispatch",
957
+ status: "running",
958
+ taskCount: subagents.length
959
+ });
793
960
  const tasks = await runSubagentsWithConcurrency(subagents, maxConcurrency, async (subagentNode, index) => {
794
961
  activeNow += 1;
795
962
  if (activeNow > observedParallelism) observedParallelism = activeNow;
963
+ const subagentConfig = subagentNode?.config || {};
964
+ const taskId = subagentNode?.id || `task-${index + 1}`;
965
+ const phaseId = String(subagentConfig.phase || subagentConfig.phaseId || "").trim().toLowerCase() || "dispatch";
966
+ emitSwarmDelta(ctx, {
967
+ type: "swarm.task.started",
968
+ phaseId,
969
+ taskId: cleanEventString(taskId),
970
+ nodeId: cleanEventString(taskId),
971
+ role: cleanEventString(subagentConfig.role || subagentNode?.label || `Agent ${index + 1}`),
972
+ status: "running",
973
+ active: activeNow,
974
+ observedParallelism
975
+ });
796
976
  try {
797
- return await dispatchSubagentTask({
977
+ const task = await dispatchSubagentTask({
798
978
  subagentNode,
799
979
  orchestratorPlan: orchestratorResult.plan,
800
980
  inputPayload: manualPayload,
801
981
  executionContext: ctx,
802
982
  taskIndex: index
803
983
  });
984
+ emitSwarmDelta(ctx, {
985
+ type: task.status === "completed" ? "swarm.task.completed" : "swarm.task.failed",
986
+ phaseId: task.phaseId || phaseId,
987
+ task: taskDeltaFromResult(task)
988
+ });
989
+ return task;
804
990
  } finally {
805
991
  activeNow -= 1;
806
992
  }
807
993
  });
994
+ const dispatchStatus = tasks.length > 0 && tasks.every((t) => t.status === "completed")
995
+ ? "completed"
996
+ : tasks.some((t) => t.status === "failed")
997
+ ? "failed"
998
+ : "completed";
999
+ emitSwarmDelta(ctx, {
1000
+ type: dispatchStatus === "failed" ? "swarm.phase.failed" : "swarm.phase.completed",
1001
+ phaseId: "dispatch",
1002
+ label: "Dispatch",
1003
+ status: dispatchStatus,
1004
+ taskCount: tasks.length,
1005
+ observedParallelism
1006
+ });
808
1007
 
809
1008
  // Phase 3: Synthesize ----------------------------------------------------
1009
+ emitSwarmDelta(ctx, {
1010
+ type: "swarm.phase.started",
1011
+ phaseId: "synthesize",
1012
+ label: "Synthesize",
1013
+ status: "running"
1014
+ });
810
1015
  const synthesisResult = await runSynthesisPhase({
811
1016
  synthesisNode: synthesis,
812
1017
  swarmConfig,
@@ -814,6 +1019,29 @@ async function runAgentSwarmGraphIfPresent({
814
1019
  inputPayload: manualPayload,
815
1020
  executionContext: ctx
816
1021
  });
1022
+ emitSwarmDelta(ctx, {
1023
+ type: synthesisResult.status === "failed" ? "swarm.phase.failed" : "swarm.phase.completed",
1024
+ phaseId: "synthesize",
1025
+ label: "Synthesize",
1026
+ status: synthesisResult.status,
1027
+ agent: synthesisResult.ranSynthesis
1028
+ ? {
1029
+ taskId: cleanEventString(synthesis?.id || "synthesis"),
1030
+ nodeId: cleanEventString(synthesis?.id || "synthesis"),
1031
+ role: cleanEventString(synthesis?.label || "Synthesizer"),
1032
+ phaseId: "synthesize",
1033
+ status: synthesisResult.status,
1034
+ adapter: cleanEventString(synthesisResult.adapter || ""),
1035
+ agentHost: cleanEventString(synthesisResult.agentHost || ""),
1036
+ durationMs: Number(synthesisResult.durationMs) || 0,
1037
+ tokens: synthesisResult.tokens ?? null,
1038
+ tools: synthesisResult.tools ?? null,
1039
+ startedAt: cleanEventString(synthesisResult.startedAt || ""),
1040
+ endedAt: cleanEventString(synthesisResult.endedAt || ""),
1041
+ error: cleanEventString(synthesisResult.error || "")
1042
+ }
1043
+ : null
1044
+ });
817
1045
 
818
1046
  const durationMs = Date.now() - startedAt;
819
1047
  const requiredTasks = tasks.filter((t) => t.required);
@@ -863,6 +1091,15 @@ async function runAgentSwarmGraphIfPresent({
863
1091
  errorText = `synthesizer returned OUTCOME_SCORE ${semanticScore} (< 0.5)`;
864
1092
  }
865
1093
 
1094
+ emitSwarmDelta(ctx, {
1095
+ type: outcomeOk ? "swarm.run.completed" : "swarm.run.failed",
1096
+ status: swarmStatus,
1097
+ durationMs,
1098
+ observedParallelism: observedParallelism || (subagents.length === 1 ? 1 : 0),
1099
+ reward,
1100
+ error: cleanEventString(errorText || "")
1101
+ });
1102
+
866
1103
  return {
867
1104
  ok: outcomeOk,
868
1105
  exitCode: outcomeOk ? 0 : 1,
@@ -888,6 +1125,12 @@ async function runAgentSwarmGraphIfPresent({
888
1125
  adapter: orchestratorResult.adapter,
889
1126
  agentHost: orchestratorResult.agentHost,
890
1127
  durationMs: orchestratorResult.durationMs,
1128
+ adapterMeta: orchestratorResult.adapterMeta || null,
1129
+ tokens: orchestratorResult.tokens ?? null,
1130
+ tools: orchestratorResult.tools ?? null,
1131
+ startedAt: orchestratorResult.startedAt || "",
1132
+ endedAt: orchestratorResult.endedAt || "",
1133
+ phaseId: "plan",
891
1134
  plan: clampText(orchestratorResult.plan, 4000)
892
1135
  },
893
1136
  tasks,
@@ -902,6 +1145,12 @@ async function runAgentSwarmGraphIfPresent({
902
1145
  adapter: synthesisResult.adapter,
903
1146
  agentHost: synthesisResult.agentHost,
904
1147
  durationMs: synthesisResult.durationMs,
1148
+ adapterMeta: synthesisResult.adapterMeta || null,
1149
+ tokens: synthesisResult.tokens ?? null,
1150
+ tools: synthesisResult.tools ?? null,
1151
+ startedAt: synthesisResult.startedAt || "",
1152
+ endedAt: synthesisResult.endedAt || "",
1153
+ phaseId: "synthesize",
905
1154
  answer: clampText(synthesisResult.output, 4000),
906
1155
  parsedOutcomeScore: synthesisResult.parsedOutcomeScore
907
1156
  }
@@ -914,6 +1163,7 @@ async function runAgentSwarmGraphIfPresent({
914
1163
  export {
915
1164
  runAgentSwarmGraphIfPresent,
916
1165
  computeRewardTelemetry,
1166
+ extractAdapterTelemetry,
917
1167
  buildOrchestratorCommand,
918
1168
  buildSubtaskCommand,
919
1169
  buildSynthesisCommand,
@@ -281,6 +281,9 @@ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs,
281
281
 
282
282
  const apiNode = extractApiRegistryCallNode(graph);
283
283
  if (!apiNode?.config) {
284
+ if ((Array.isArray(graph.nodes) ? graph.nodes : []).some((node) => node?.type === "ai-agent")) {
285
+ return null;
286
+ }
284
287
  return {
285
288
  ok: false,
286
289
  exitCode: 1,
@@ -157,9 +157,10 @@ function validateOrchestrationGraph(graph) {
157
157
  } else {
158
158
  const hasThinAdapter = graph.nodes.some((n) => n?.type === "thinAdapter");
159
159
  const hasApi = graph.nodes.some((n) => n?.type === "api-registry-call");
160
+ const hasAiAgent = graph.nodes.some((n) => n?.type === "ai-agent");
160
161
  const hasResult = graph.nodes.some((n) => n?.type === "tool-result");
161
- if (!hasThinAdapter && !hasApi) errors.push("orchestrationGraph requires an api-registry-call node");
162
- if (!hasThinAdapter && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
162
+ if (!hasThinAdapter && !hasApi && !hasAiAgent) errors.push("orchestrationGraph requires an executable node");
163
+ if (!hasThinAdapter && !hasAiAgent && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
163
164
  }
164
165
  }
165
166
  if (!Array.isArray(graph.edges)) {
@@ -712,6 +713,7 @@ function buildCanonicalNode(nodeId, registryRow = {}, options = {}) {
712
713
  function getNextCanonicalNodeId(graph) {
713
714
  const parsed = parseOrchestrationGraph(graph) || graph;
714
715
  if ((parsed?.nodes || []).some((n) => n?.type === "thinAdapter")) return null;
716
+ if ((parsed?.nodes || []).some((n) => n?.type === "ai-agent")) return null;
715
717
  const ids = new Set((parsed?.nodes || []).map((n) => String(n.id)));
716
718
  for (const id of CANONICAL_NODE_ORDER) {
717
719
  if (!ids.has(id)) return id;
@@ -844,6 +846,12 @@ function buildDefaultAgentSwarmGraph(options = {}) {
844
846
  role: String(agent.role || agent.id || "Subagent"),
845
847
  description: String(agent.description || "").trim(),
846
848
  taskPrompt: String(agent.taskPrompt || "").trim(),
849
+ // Optional author-named phase — groups this subagent in the cockpit
850
+ // (e.g. "ping" / "echo" / "verify"). Absent → the single Dispatch
851
+ // phase, exactly as before.
852
+ ...(String(agent.phase || agent.phaseId || "").trim()
853
+ ? { phase: String(agent.phase || agent.phaseId).trim().toLowerCase() }
854
+ : {}),
847
855
  tools: Array.isArray(agent.tools) ? agent.tools.map((t) => String(t || "").trim()).filter(Boolean) : [],
848
856
  agentHost: String(agent.agentHost || agentHost || "").trim(),
849
857
  adapter: String(agent.adapter || "").trim(),