@growthub/cli 0.14.0 → 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 (23) 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-run/route.js +70 -9
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +18 -4
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +129 -3
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +48 -9
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +6 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  23. 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();
@@ -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,
@@ -844,6 +844,12 @@ function buildDefaultAgentSwarmGraph(options = {}) {
844
844
  role: String(agent.role || agent.id || "Subagent"),
845
845
  description: String(agent.description || "").trim(),
846
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
+ : {}),
847
853
  tools: Array.isArray(agent.tools) ? agent.tools.map((t) => String(t || "").trim()).filter(Boolean) : [],
848
854
  agentHost: String(agent.agentHost || agentHost || "").trim(),
849
855
  adapter: String(agent.adapter || "").trim(),