@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +179 -117
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +136 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/package.json +1 -1
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
* - lib/orchestration-run-trace.js (lower-level record parser)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
extractSwarmNodes,
|
|
21
|
+
isAgentSwarmGraph,
|
|
22
|
+
parseOrchestrationGraph,
|
|
23
|
+
redactSecretsFromText
|
|
24
|
+
} from "./orchestration-graph.js";
|
|
20
25
|
import { redactRunInputsEnvelope, summarizeRunInputs } from "./orchestration-run-inputs.js";
|
|
21
26
|
|
|
22
27
|
const RUN_LOG_BUNDLE_KIND = "growthub-sandbox-run-log-v1";
|
|
@@ -202,6 +207,388 @@ function buildExportsForRecord(record, stdoutText, stderrText, outputText) {
|
|
|
202
207
|
return { available, external: [] };
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Swarm cockpit projection (SWARM_RUN_CONTRACT_V1).
|
|
212
|
+
*
|
|
213
|
+
* Pure transformation of a sandbox run record carrying a `swarm` block
|
|
214
|
+
* (written by the agent-swarm-v1 runtime through sandbox-run) into the
|
|
215
|
+
* phase/agent tree the helper sidecar cockpit renders. Returns `null` for
|
|
216
|
+
* non-swarm records so existing runs are untouched.
|
|
217
|
+
*
|
|
218
|
+
* Telemetry is truthful: tokens/tools are null when the adapter did not
|
|
219
|
+
* report them — the UI renders "—", never an estimate. Totals are null when
|
|
220
|
+
* no agent reported a number.
|
|
221
|
+
*
|
|
222
|
+
* Same module rules as the rest of this file: no React, no fetch, no config
|
|
223
|
+
* writes, no localStorage, no CSS.
|
|
224
|
+
*/
|
|
225
|
+
// Truthful counts only: null/undefined (adapter reported nothing) stays
|
|
226
|
+
// null — clampNumber would coerce null to 0, which would be a fake metric.
|
|
227
|
+
function toTruthfulCount(value) {
|
|
228
|
+
if (value == null) return null;
|
|
229
|
+
const n = Number(value);
|
|
230
|
+
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function toAdapterReportedCount(entry, field) {
|
|
234
|
+
const meta = entry?.adapterMeta && typeof entry.adapterMeta === "object" ? entry.adapterMeta : null;
|
|
235
|
+
if (meta?.telemetrySource === "unreported" && meta?.[field] == null) return null;
|
|
236
|
+
if (entry?.[field] === 0 && meta && meta[field] == null) return null;
|
|
237
|
+
if (entry?.[field] === 0 && !meta && safeString(entry?.adapter).trim() === "local-agent-host") return null;
|
|
238
|
+
return toTruthfulCount(entry?.[field]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parsePersistedAgentHostTelemetry(entry) {
|
|
242
|
+
const stderr = safeString(entry?.stderr);
|
|
243
|
+
const total = stderr.match(/\b(?:total\s+tokens|tokens\s+used)\s*(?:[:=]|\r?\n)\s*([0-9][0-9,]*)/i);
|
|
244
|
+
const input = stderr.match(/\b(?:input|prompt)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
|
|
245
|
+
const output = stderr.match(/\b(?:output|completion)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
|
|
246
|
+
const tools = stderr.match(/\b(?:tool\s+calls?|tools\s+used)\s*[:=]\s*([0-9][0-9,]*)/i);
|
|
247
|
+
const toInt = (value) => {
|
|
248
|
+
if (value == null || value === "") return null;
|
|
249
|
+
const n = Number(String(value).replace(/,/g, ""));
|
|
250
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
|
|
251
|
+
};
|
|
252
|
+
const inputTokens = toInt(input?.[1]);
|
|
253
|
+
const outputTokens = toInt(output?.[1]);
|
|
254
|
+
const summed = inputTokens != null || outputTokens != null
|
|
255
|
+
? (inputTokens || 0) + (outputTokens || 0)
|
|
256
|
+
: null;
|
|
257
|
+
const tokens = toInt(total?.[1]) ?? summed;
|
|
258
|
+
return {
|
|
259
|
+
tokens,
|
|
260
|
+
tools: toInt(tools?.[1]) ?? (tokens != null ? 0 : null)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function titleizePhaseId(id) {
|
|
265
|
+
const text = safeString(id).trim() || "dispatch";
|
|
266
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function sumReportedOrNull(values) {
|
|
270
|
+
const reported = values.filter((n) => n != null);
|
|
271
|
+
return reported.length > 0 ? reported.reduce((sum, n) => sum + n, 0) : null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function deriveSwarmRunProjection(record) {
|
|
275
|
+
if (!record || typeof record !== "object") return null;
|
|
276
|
+
const swarm = record.swarm;
|
|
277
|
+
if (!swarm || typeof swarm !== "object") return null;
|
|
278
|
+
|
|
279
|
+
const summary = deriveRunSummary(record);
|
|
280
|
+
const tasks = Array.isArray(swarm.tasks) ? swarm.tasks.filter((t) => t && typeof t === "object") : [];
|
|
281
|
+
|
|
282
|
+
// Agents projected from a persisted record are terminal: `pending: false`
|
|
283
|
+
// means a missing count is "ran but never reported" → the UI shows "—".
|
|
284
|
+
// (Pending agents — from the graph skeleton or a live stream — show blank.)
|
|
285
|
+
const toAgent = (entry, fallbackId, fallbackLabel, transcriptParts, logNodeId) => {
|
|
286
|
+
const persistedTelemetry = parsePersistedAgentHostTelemetry(entry);
|
|
287
|
+
const tokens = persistedTelemetry.tokens ?? toAdapterReportedCount(entry, "tokens");
|
|
288
|
+
const tools = persistedTelemetry.tools ?? toAdapterReportedCount(entry, "tools");
|
|
289
|
+
return {
|
|
290
|
+
id: safeString(entry?.taskId || entry?.nodeId || fallbackId).trim() || fallbackId,
|
|
291
|
+
label: safeString(entry?.role || entry?.label || fallbackLabel).trim() || fallbackLabel,
|
|
292
|
+
status: safeString(entry?.status || "unknown").trim() || "unknown",
|
|
293
|
+
pending: false,
|
|
294
|
+
tokens,
|
|
295
|
+
tools,
|
|
296
|
+
durationMs: clampNumber(entry?.durationMs) ?? 0,
|
|
297
|
+
transcript: redactSecretsFromText(
|
|
298
|
+
transcriptParts.map((part) => safeString(part).trim()).filter(Boolean).join("\n\n")
|
|
299
|
+
),
|
|
300
|
+
logNodeId
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const phases = [];
|
|
305
|
+
const orchestrator = swarm.orchestrator && typeof swarm.orchestrator === "object" ? swarm.orchestrator : null;
|
|
306
|
+
if (orchestrator) {
|
|
307
|
+
phases.push({
|
|
308
|
+
id: "plan",
|
|
309
|
+
label: "Plan",
|
|
310
|
+
status: safeString(orchestrator.status || "unknown").trim() || "unknown",
|
|
311
|
+
agents: [
|
|
312
|
+
toAgent(
|
|
313
|
+
orchestrator,
|
|
314
|
+
"orchestrator",
|
|
315
|
+
"Orchestrator",
|
|
316
|
+
[orchestrator.error, orchestrator.plan],
|
|
317
|
+
"phase-orchestrator"
|
|
318
|
+
)
|
|
319
|
+
]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Group dispatch tasks by their declared phase id (author-named phases),
|
|
324
|
+
// falling back to the single "Dispatch" group for legacy records whose
|
|
325
|
+
// tasks carry no phaseId — those project identically to before.
|
|
326
|
+
const dispatchGroups = new Map();
|
|
327
|
+
tasks.forEach((task, index) => {
|
|
328
|
+
const phaseId = safeString(task.phaseId).trim() || "dispatch";
|
|
329
|
+
if (!dispatchGroups.has(phaseId)) dispatchGroups.set(phaseId, []);
|
|
330
|
+
dispatchGroups.get(phaseId).push(
|
|
331
|
+
toAgent(
|
|
332
|
+
task,
|
|
333
|
+
`task-${index + 1}`,
|
|
334
|
+
`Agent ${index + 1}`,
|
|
335
|
+
[task.error, task.stdout, task.stderr],
|
|
336
|
+
safeString(task.taskId || task.nodeId || `task-${index + 1}`).trim()
|
|
337
|
+
)
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
if (dispatchGroups.size === 0) dispatchGroups.set("dispatch", []);
|
|
341
|
+
for (const [phaseId, agents] of dispatchGroups) {
|
|
342
|
+
const status = agents.length === 0
|
|
343
|
+
? "failed"
|
|
344
|
+
: agents.every((a) => a.status === "completed")
|
|
345
|
+
? "completed"
|
|
346
|
+
: agents.some((a) => a.status === "failed")
|
|
347
|
+
? "failed"
|
|
348
|
+
: "info";
|
|
349
|
+
phases.push({ id: phaseId, label: titleizePhaseId(phaseId), status, agents });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const synthesis = swarm.synthesis && typeof swarm.synthesis === "object" ? swarm.synthesis : null;
|
|
353
|
+
if (synthesis) {
|
|
354
|
+
phases.push({
|
|
355
|
+
id: "synthesize",
|
|
356
|
+
label: "Synthesize",
|
|
357
|
+
status: safeString(synthesis.status || "unknown").trim() || "unknown",
|
|
358
|
+
agents: [
|
|
359
|
+
toAgent(
|
|
360
|
+
synthesis,
|
|
361
|
+
"synthesis",
|
|
362
|
+
synthesis.label || "Synthesizer",
|
|
363
|
+
[synthesis.error, synthesis.answer],
|
|
364
|
+
"phase-synthesis"
|
|
365
|
+
)
|
|
366
|
+
]
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const allAgents = phases.flatMap((phase) => phase.agents);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
runId: safeString(record.runId).trim(),
|
|
374
|
+
title: safeString(record.name || record.sandboxName).trim() || "agent-swarm",
|
|
375
|
+
status: summary.status,
|
|
376
|
+
elapsedMs: clampNumber(record.durationMs) ?? 0,
|
|
377
|
+
agentCount: allAgents.length,
|
|
378
|
+
totalTokens: sumReportedOrNull(allAgents.map((a) => a.tokens)),
|
|
379
|
+
totalTools: sumReportedOrNull(allAgents.map((a) => a.tools)),
|
|
380
|
+
phases
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Declared-phase skeleton (SWARM_RUN_CONTRACT_V1, parity P1).
|
|
386
|
+
*
|
|
387
|
+
* Projects the SAME projection shape as deriveSwarmRunProjection from the
|
|
388
|
+
* governed row's agent-swarm-v1 graph alone — before any run exists. Every
|
|
389
|
+
* phase and agent renders upfront with `status: "pending"`, `pending: true`
|
|
390
|
+
* (UI shows hollow dots and BLANK cells, reserving "—" for terminal
|
|
391
|
+
* never-reported). Author-named phases come from each subagent node's
|
|
392
|
+
* `config.phase` / `config.phaseId`; graphs that declare none fall back to
|
|
393
|
+
* the Plan / Dispatch / Synthesize derivation, converging exactly with the
|
|
394
|
+
* record projection for the same workflow.
|
|
395
|
+
*
|
|
396
|
+
* Pure: no React, no fetch, no config writes, no localStorage, no CSS.
|
|
397
|
+
*/
|
|
398
|
+
function deriveSwarmGraphProjection(graphLike, { title = "", runId = "" } = {}) {
|
|
399
|
+
const graph = parseOrchestrationGraph(graphLike);
|
|
400
|
+
if (!graph || !isAgentSwarmGraph(graph)) return null;
|
|
401
|
+
const extracted = extractSwarmNodes(graph);
|
|
402
|
+
if (!extracted) return null;
|
|
403
|
+
const { orchestrator, subagents, synthesis } = extracted;
|
|
404
|
+
|
|
405
|
+
const pendingAgent = (id, label) => ({
|
|
406
|
+
id: safeString(id).trim() || "agent",
|
|
407
|
+
label: safeString(label).trim() || "Agent",
|
|
408
|
+
status: "pending",
|
|
409
|
+
pending: true,
|
|
410
|
+
tokens: null,
|
|
411
|
+
tools: null,
|
|
412
|
+
durationMs: 0,
|
|
413
|
+
transcript: "",
|
|
414
|
+
logNodeId: ""
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const phases = [];
|
|
418
|
+
if (orchestrator) {
|
|
419
|
+
phases.push({
|
|
420
|
+
id: "plan",
|
|
421
|
+
label: "Plan",
|
|
422
|
+
status: "pending",
|
|
423
|
+
agents: [pendingAgent(orchestrator.id || "orchestrator", orchestrator.config?.role || "Orchestrator")]
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const dispatchGroups = new Map();
|
|
428
|
+
subagents.forEach((node, index) => {
|
|
429
|
+
const phaseId = safeString(node?.config?.phase || node?.config?.phaseId).trim().toLowerCase() || "dispatch";
|
|
430
|
+
if (!dispatchGroups.has(phaseId)) dispatchGroups.set(phaseId, []);
|
|
431
|
+
dispatchGroups.get(phaseId).push(
|
|
432
|
+
pendingAgent(node?.id || `task-${index + 1}`, node?.config?.role || node?.label || `Agent ${index + 1}`)
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
if (dispatchGroups.size === 0) dispatchGroups.set("dispatch", []);
|
|
436
|
+
for (const [phaseId, agents] of dispatchGroups) {
|
|
437
|
+
phases.push({ id: phaseId, label: titleizePhaseId(phaseId), status: "pending", agents });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (synthesis) {
|
|
441
|
+
phases.push({
|
|
442
|
+
id: "synthesize",
|
|
443
|
+
label: "Synthesize",
|
|
444
|
+
status: "pending",
|
|
445
|
+
agents: [pendingAgent(synthesis.id || "synthesis", synthesis.label || "Synthesizer")]
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
runId: safeString(runId).trim(),
|
|
451
|
+
title: safeString(title).trim() || "agent-swarm",
|
|
452
|
+
status: "pending",
|
|
453
|
+
elapsedMs: 0,
|
|
454
|
+
agentCount: phases.reduce((sum, phase) => sum + phase.agents.length, 0),
|
|
455
|
+
totalTokens: null,
|
|
456
|
+
totalTools: null,
|
|
457
|
+
phases
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function cloneSwarmProjection(projection) {
|
|
462
|
+
if (!projection || typeof projection !== "object") return null;
|
|
463
|
+
return {
|
|
464
|
+
...projection,
|
|
465
|
+
phases: Array.isArray(projection.phases)
|
|
466
|
+
? projection.phases.map((phase) => ({
|
|
467
|
+
...phase,
|
|
468
|
+
agents: Array.isArray(phase.agents)
|
|
469
|
+
? phase.agents.map((agent) => ({ ...agent }))
|
|
470
|
+
: []
|
|
471
|
+
}))
|
|
472
|
+
: []
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function findOrCreateLivePhase(projection, phaseId, label) {
|
|
477
|
+
const id = safeString(phaseId).trim() || "dispatch";
|
|
478
|
+
let phase = projection.phases.find((entry) => entry.id === id);
|
|
479
|
+
if (!phase) {
|
|
480
|
+
phase = { id, label: safeString(label).trim() || titleizePhaseId(id), status: "pending", agents: [] };
|
|
481
|
+
projection.phases.push(phase);
|
|
482
|
+
}
|
|
483
|
+
return phase;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function upsertLiveAgent(phase, incoming) {
|
|
487
|
+
if (!phase) return;
|
|
488
|
+
const fallbackId = safeString(incoming?.taskId || incoming?.nodeId || incoming?.id).trim() || "agent";
|
|
489
|
+
const id = fallbackId;
|
|
490
|
+
const label = safeString(incoming?.role || incoming?.label).trim() || "Agent";
|
|
491
|
+
const index = phase.agents.findIndex((agent) => agent.id === id || agent.logNodeId === id);
|
|
492
|
+
const prior = index >= 0 ? phase.agents[index] : {};
|
|
493
|
+
const status = safeString(incoming?.status || prior.status || "running").trim() || "running";
|
|
494
|
+
const next = {
|
|
495
|
+
id,
|
|
496
|
+
label: label || prior.label || "Agent",
|
|
497
|
+
status,
|
|
498
|
+
pending: status === "pending" || status === "running" || status === "executing",
|
|
499
|
+
tokens: toTruthfulCount(incoming?.tokens ?? prior.tokens),
|
|
500
|
+
tools: toTruthfulCount(incoming?.tools ?? prior.tools),
|
|
501
|
+
durationMs: clampNumber(incoming?.durationMs) ?? prior.durationMs ?? 0,
|
|
502
|
+
transcript: prior.transcript || "",
|
|
503
|
+
logNodeId: safeString(incoming?.nodeId || prior.logNodeId || id).trim() || id
|
|
504
|
+
};
|
|
505
|
+
if (index >= 0) phase.agents[index] = next;
|
|
506
|
+
else phase.agents.push(next);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function derivePhaseStatusFromAgents(phase) {
|
|
510
|
+
const agents = Array.isArray(phase?.agents) ? phase.agents : [];
|
|
511
|
+
if (agents.some((agent) => agent.status === "running" || agent.status === "executing")) return "running";
|
|
512
|
+
if (agents.some((agent) => agent.status === "failed")) return "failed";
|
|
513
|
+
if (agents.length > 0 && agents.every((agent) => agent.status === "completed")) return "completed";
|
|
514
|
+
return phase?.status || "pending";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Live swarm delta projection.
|
|
519
|
+
*
|
|
520
|
+
* This consumes the optional NDJSON events from POST /api/workspace/sandbox-run
|
|
521
|
+
* and returns the exact same shape as persisted-record projection. The final
|
|
522
|
+
* source of truth remains deriveSwarmRunProjection(record); this only hydrates
|
|
523
|
+
* the cosmetic Background Tasks card while the POST is still open.
|
|
524
|
+
*/
|
|
525
|
+
function deriveSwarmDeltaProjection(graphLike, events, { title = "", runId = "", elapsedMs = 0 } = {}) {
|
|
526
|
+
const base = cloneSwarmProjection(deriveSwarmGraphProjection(graphLike, { title, runId }));
|
|
527
|
+
if (!base) return null;
|
|
528
|
+
const list = Array.isArray(events) ? events.filter((event) => event && typeof event === "object") : [];
|
|
529
|
+
let status = "running";
|
|
530
|
+
let latestRunId = safeString(runId).trim() || base.runId;
|
|
531
|
+
|
|
532
|
+
for (const event of list) {
|
|
533
|
+
if (event.runId) latestRunId = safeString(event.runId).trim() || latestRunId;
|
|
534
|
+
const type = safeString(event.type).trim();
|
|
535
|
+
if (type === "swarm.run.started") {
|
|
536
|
+
status = "running";
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (type === "swarm.run.completed" || type === "swarm.run.failed") {
|
|
540
|
+
status = safeString(event.status || (type.endsWith("completed") ? "completed" : "failed")).trim() || status;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (type === "swarm.phase.started") {
|
|
544
|
+
const phase = findOrCreateLivePhase(base, event.phaseId, event.label);
|
|
545
|
+
phase.status = "running";
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (type === "swarm.phase.completed" || type === "swarm.phase.failed") {
|
|
549
|
+
const phase = findOrCreateLivePhase(base, event.phaseId, event.label);
|
|
550
|
+
phase.status = safeString(event.status || (type.endsWith("completed") ? "completed" : "failed")).trim() || phase.status;
|
|
551
|
+
if (event.agent && typeof event.agent === "object") upsertLiveAgent(phase, event.agent);
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (type === "swarm.task.started") {
|
|
555
|
+
const phase = findOrCreateLivePhase(base, event.phaseId, titleizePhaseId(event.phaseId));
|
|
556
|
+
phase.status = "running";
|
|
557
|
+
upsertLiveAgent(phase, {
|
|
558
|
+
taskId: event.taskId,
|
|
559
|
+
nodeId: event.nodeId,
|
|
560
|
+
role: event.role,
|
|
561
|
+
status: "running",
|
|
562
|
+
phaseId: event.phaseId
|
|
563
|
+
});
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
if (type === "swarm.task.completed" || type === "swarm.task.failed") {
|
|
567
|
+
const task = event.task && typeof event.task === "object" ? event.task : event;
|
|
568
|
+
const phase = findOrCreateLivePhase(base, task.phaseId || event.phaseId, titleizePhaseId(task.phaseId || event.phaseId));
|
|
569
|
+
upsertLiveAgent(phase, task);
|
|
570
|
+
phase.status = derivePhaseStatusFromAgents(phase);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const phase of base.phases) {
|
|
575
|
+
if (phase.status === "running") continue;
|
|
576
|
+
phase.status = derivePhaseStatusFromAgents(phase);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const allAgents = base.phases.flatMap((phase) => phase.agents);
|
|
580
|
+
return {
|
|
581
|
+
...base,
|
|
582
|
+
runId: latestRunId,
|
|
583
|
+
status,
|
|
584
|
+
elapsedMs: clampNumber(elapsedMs) ?? base.elapsedMs ?? 0,
|
|
585
|
+
agentCount: allAgents.length,
|
|
586
|
+
totalTokens: sumReportedOrNull(allAgents.map((agent) => agent.tokens)),
|
|
587
|
+
totalTools: sumReportedOrNull(allAgents.map((agent) => agent.tools)),
|
|
588
|
+
phases: base.phases
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
205
592
|
function normalizeRunConsoleRecord(record) {
|
|
206
593
|
if (!record || typeof record !== "object") return null;
|
|
207
594
|
const summary = deriveRunSummary(record);
|
|
@@ -298,11 +685,13 @@ function normalizeRunConsoleRecord(record) {
|
|
|
298
685
|
envRefsMissing: Array.isArray(record.envRefsMissing) ? record.envRefsMissing.slice() : [],
|
|
299
686
|
networkAllow: Boolean(record.networkAllow),
|
|
300
687
|
allowList: Array.isArray(record.allowList) ? record.allowList.slice() : [],
|
|
688
|
+
browserAccess: Boolean(record.browserAccess),
|
|
301
689
|
adapterMeta,
|
|
302
690
|
templateTrace
|
|
303
691
|
},
|
|
304
692
|
lineage,
|
|
305
693
|
swarm: record.swarm && typeof record.swarm === "object" ? record.swarm : null,
|
|
694
|
+
swarmRun: deriveSwarmRunProjection(record),
|
|
306
695
|
logTree: buildRunLogTree(record)
|
|
307
696
|
};
|
|
308
697
|
}
|
|
@@ -387,6 +776,24 @@ function formatRunDuration(ms) {
|
|
|
387
776
|
return `${minutes}m ${seconds}s`;
|
|
388
777
|
}
|
|
389
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Compact zero-padded duration for the swarm cockpit tables and cards —
|
|
781
|
+
* Claude Code Background-tasks format: "04s", "15s", "1m 04s". A separate
|
|
782
|
+
* formatter (not a change to formatRunDuration) so existing non-swarm
|
|
783
|
+
* run-console surfaces keep their ms-precision rendering untouched.
|
|
784
|
+
*/
|
|
785
|
+
function formatCompactRunDuration(ms) {
|
|
786
|
+
if (ms == null) return "—";
|
|
787
|
+
const n = clampNumber(ms);
|
|
788
|
+
if (n == null) return "—";
|
|
789
|
+
const totalSeconds = Math.max(0, Math.round(n / 1000));
|
|
790
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
791
|
+
const seconds = totalSeconds % 60;
|
|
792
|
+
const padded = String(seconds).padStart(2, "0");
|
|
793
|
+
if (minutes <= 0) return `${padded}s`;
|
|
794
|
+
return `${minutes}m ${padded}s`;
|
|
795
|
+
}
|
|
796
|
+
|
|
390
797
|
function downloadRunBundle({ record, runId, sourceId } = {}) {
|
|
391
798
|
const normalized = normalizeRunConsoleRecord(record || {});
|
|
392
799
|
return {
|
|
@@ -403,6 +810,10 @@ export {
|
|
|
403
810
|
DEFAULT_EXPORT_TARGETS,
|
|
404
811
|
normalizeRunConsoleRecord,
|
|
405
812
|
deriveRunSummary,
|
|
813
|
+
deriveSwarmRunProjection,
|
|
814
|
+
deriveSwarmGraphProjection,
|
|
815
|
+
deriveSwarmDeltaProjection,
|
|
816
|
+
formatCompactRunDuration,
|
|
406
817
|
deriveRunLifecycle,
|
|
407
818
|
buildRunLogTree,
|
|
408
819
|
buildRunTimeline,
|
|
@@ -34,15 +34,14 @@
|
|
|
34
34
|
* on-disk auth state; this module only records *readiness*, not secrets.
|
|
35
35
|
*
|
|
36
36
|
* The status semantics are deliberately conservative:
|
|
37
|
-
* - "active"
|
|
38
|
-
*
|
|
39
|
-
* - "reachable"
|
|
40
|
-
* authentication is NOT yet confirmed
|
|
37
|
+
* - "active" the selected pinned host CLI is callable and ready for the
|
|
38
|
+
* local agent-host bridge, or a clean login exit completed
|
|
39
|
+
* - "reachable" legacy metadata value treated as active by the UI
|
|
41
40
|
* - "stale" the binary printed auth-shaped failure output
|
|
42
41
|
* - "missing" binary not found on PATH
|
|
43
42
|
*
|
|
44
|
-
* A
|
|
45
|
-
*
|
|
43
|
+
* A successful host probe promotes to "active" because the sidecar represents
|
|
44
|
+
* selected local host readiness, not provider-account auth semantics.
|
|
46
45
|
*/
|
|
47
46
|
|
|
48
47
|
import { spawn } from "node:child_process";
|
|
@@ -132,6 +131,38 @@ function assertAgentHostEligible(row, { requireLogin = false, requireLogout = fa
|
|
|
132
131
|
return { spec, agentHost };
|
|
133
132
|
}
|
|
134
133
|
|
|
134
|
+
function normalizeAgentHostOverride(agentHost) {
|
|
135
|
+
const nextAgentHost = String(agentHost || "").trim();
|
|
136
|
+
if (!nextAgentHost) return "";
|
|
137
|
+
if (!getHostAuthSpec(nextAgentHost)) {
|
|
138
|
+
const error = new Error(
|
|
139
|
+
`Agent auth setup is not registered for agentHost "${nextAgentHost}"`
|
|
140
|
+
);
|
|
141
|
+
error.code = "SANDBOX_AGENT_AUTH_HOST_UNSUPPORTED";
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
return nextAgentHost;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applyAgentHostOverride(row, agentHost) {
|
|
148
|
+
if (!agentHost) return row;
|
|
149
|
+
return {
|
|
150
|
+
...row,
|
|
151
|
+
runLocality: "local",
|
|
152
|
+
adapter: "local-agent-host",
|
|
153
|
+
agentHost
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildAgentHostSelectionPatch(agentHost) {
|
|
158
|
+
if (!agentHost) return {};
|
|
159
|
+
return {
|
|
160
|
+
runLocality: "local",
|
|
161
|
+
adapter: "local-agent-host",
|
|
162
|
+
agentHost
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
function resolveHostBinary(row, spec) {
|
|
136
167
|
const candidates = [row?.agentCommand, row?.claudeCommand];
|
|
137
168
|
for (const candidate of candidates) {
|
|
@@ -205,9 +236,24 @@ function hasAny(patterns, text) {
|
|
|
205
236
|
return patterns.some((p) => p.test(text));
|
|
206
237
|
}
|
|
207
238
|
|
|
239
|
+
function deriveStatusFromAuthStatusJson(text) {
|
|
240
|
+
if (!text || typeof text !== "string") return null;
|
|
241
|
+
const trimmed = text.trim();
|
|
242
|
+
if (!trimmed.startsWith("{")) return null;
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(trimmed);
|
|
245
|
+
if (parsed && typeof parsed === "object" && typeof parsed.loggedIn === "boolean") {
|
|
246
|
+
return parsed.loggedIn ? "active" : "stale";
|
|
247
|
+
}
|
|
248
|
+
} catch {}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
208
252
|
function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", spawnError }) {
|
|
209
253
|
if (spawnError) return spawnError.notFound ? "missing" : null;
|
|
210
254
|
const combined = `${stdout}\n${stderr}`;
|
|
255
|
+
const jsonStatus = deriveStatusFromAuthStatusJson(stdout) || deriveStatusFromAuthStatusJson(stderr);
|
|
256
|
+
if (jsonStatus) return jsonStatus;
|
|
211
257
|
if (hasAny(UNKNOWN_SUBCOMMAND_PATTERNS, combined)) return null; // fall back
|
|
212
258
|
if (hasAny(STALE_AUTH_PATTERNS, combined)) return "stale";
|
|
213
259
|
if (exitCode === 0) return "active";
|
|
@@ -219,7 +265,7 @@ function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", s
|
|
|
219
265
|
|
|
220
266
|
function deriveStatusFromVersionProbe({ exitCode, stderr, spawnError }) {
|
|
221
267
|
if (spawnError) return spawnError.notFound ? "missing" : "unknown";
|
|
222
|
-
if (typeof exitCode === "number" && exitCode === 0) return "
|
|
268
|
+
if (typeof exitCode === "number" && exitCode === 0) return "active";
|
|
223
269
|
const text = String(stderr || "");
|
|
224
270
|
if (hasAny(STALE_AUTH_PATTERNS, text)) return "stale";
|
|
225
271
|
return "unknown";
|
|
@@ -237,8 +283,8 @@ function deriveLoginStatus({ exitCode, stderr, stdout, timedOut, spawnError }) {
|
|
|
237
283
|
function shortMessage({ status, label, exitCode, error, loginUrl }) {
|
|
238
284
|
const name = label || "Local agent CLI";
|
|
239
285
|
if (error) return `${name}: ${redactSecrets(String(error))}`;
|
|
240
|
-
if (status === "active") return loginUrl ? "Login completed." : "
|
|
241
|
-
if (status === "reachable") return "
|
|
286
|
+
if (status === "active") return loginUrl ? "Login completed." : "Active.";
|
|
287
|
+
if (status === "reachable") return "Active.";
|
|
242
288
|
if (status === "stale") return "Authentication needs setup. Run Login, then run the sandbox again.";
|
|
243
289
|
if (status === "missing") return `${name} not found. Install it and try again.`;
|
|
244
290
|
if (status === "checking") return `Checking ${name}…`;
|
|
@@ -329,16 +375,18 @@ function runCommand({ binary, args, cwd, timeoutMs, stdin }) {
|
|
|
329
375
|
// Public API — login / logout / status
|
|
330
376
|
// ──────────────────────────────────────────────────────────────────────────
|
|
331
377
|
|
|
332
|
-
async function runAgentLogin({ objectId, name }) {
|
|
378
|
+
async function runAgentLogin({ objectId, name, agentHost }) {
|
|
333
379
|
const workspaceConfig = await readWorkspaceConfig();
|
|
334
380
|
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
335
381
|
if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
|
|
336
382
|
if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
|
|
337
383
|
|
|
338
|
-
const
|
|
384
|
+
const selectedAgentHost = normalizeAgentHostOverride(agentHost);
|
|
385
|
+
const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
|
|
386
|
+
const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogin: true });
|
|
339
387
|
|
|
340
|
-
const binary = resolveHostBinary(
|
|
341
|
-
const cwd = resolveCwd(
|
|
388
|
+
const binary = resolveHostBinary(effectiveRow, spec);
|
|
389
|
+
const cwd = resolveCwd(effectiveRow);
|
|
342
390
|
const startedAt = Date.now();
|
|
343
391
|
|
|
344
392
|
const result = await runCommand({
|
|
@@ -356,20 +404,21 @@ async function runAgentLogin({ objectId, name }) {
|
|
|
356
404
|
|
|
357
405
|
const patch = buildRowPatch({
|
|
358
406
|
status,
|
|
359
|
-
provider:
|
|
407
|
+
provider: effectiveAgentHost,
|
|
360
408
|
checkedAt,
|
|
361
409
|
exitCode: result.exitCode,
|
|
362
410
|
loginUrl,
|
|
363
411
|
label: spec.label,
|
|
364
412
|
spawnError: result.spawnError
|
|
365
413
|
});
|
|
414
|
+
Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
|
|
366
415
|
|
|
367
416
|
await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
|
|
368
417
|
|
|
369
418
|
return {
|
|
370
419
|
ok: status === "active",
|
|
371
420
|
status,
|
|
372
|
-
provider:
|
|
421
|
+
provider: effectiveAgentHost,
|
|
373
422
|
label: spec.label,
|
|
374
423
|
binary,
|
|
375
424
|
cwd,
|
|
@@ -384,16 +433,18 @@ async function runAgentLogin({ objectId, name }) {
|
|
|
384
433
|
};
|
|
385
434
|
}
|
|
386
435
|
|
|
387
|
-
async function runAgentLogout({ objectId, name }) {
|
|
436
|
+
async function runAgentLogout({ objectId, name, agentHost }) {
|
|
388
437
|
const workspaceConfig = await readWorkspaceConfig();
|
|
389
438
|
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
390
439
|
if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
|
|
391
440
|
if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
|
|
392
441
|
|
|
393
|
-
const
|
|
442
|
+
const selectedAgentHost = normalizeAgentHostOverride(agentHost);
|
|
443
|
+
const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
|
|
444
|
+
const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogout: true });
|
|
394
445
|
|
|
395
|
-
const binary = resolveHostBinary(
|
|
396
|
-
const cwd = resolveCwd(
|
|
446
|
+
const binary = resolveHostBinary(effectiveRow, spec);
|
|
447
|
+
const cwd = resolveCwd(effectiveRow);
|
|
397
448
|
const startedAt = Date.now();
|
|
398
449
|
|
|
399
450
|
let exitCode = null;
|
|
@@ -422,7 +473,7 @@ async function runAgentLogout({ objectId, name }) {
|
|
|
422
473
|
|
|
423
474
|
const patch = buildRowPatch({
|
|
424
475
|
status,
|
|
425
|
-
provider:
|
|
476
|
+
provider: effectiveAgentHost,
|
|
426
477
|
checkedAt,
|
|
427
478
|
exitCode,
|
|
428
479
|
loginUrl: null,
|
|
@@ -432,13 +483,14 @@ async function runAgentLogout({ objectId, name }) {
|
|
|
432
483
|
patch.agentAuthLastMessage = spawnError?.notFound
|
|
433
484
|
? shortMessage({ status: "missing", label: spec.label })
|
|
434
485
|
: `${spec.label} logged out — auth will be required before next run.`;
|
|
486
|
+
Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
|
|
435
487
|
|
|
436
488
|
await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
|
|
437
489
|
|
|
438
490
|
return {
|
|
439
491
|
ok: !spawnError,
|
|
440
492
|
status,
|
|
441
|
-
provider:
|
|
493
|
+
provider: effectiveAgentHost,
|
|
442
494
|
label: spec.label,
|
|
443
495
|
binary,
|
|
444
496
|
cwd,
|
|
@@ -451,16 +503,18 @@ async function runAgentLogout({ objectId, name }) {
|
|
|
451
503
|
};
|
|
452
504
|
}
|
|
453
505
|
|
|
454
|
-
async function checkAgentStatus({ objectId, name }) {
|
|
506
|
+
async function checkAgentStatus({ objectId, name, agentHost }) {
|
|
455
507
|
const workspaceConfig = await readWorkspaceConfig();
|
|
456
508
|
const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
|
|
457
509
|
if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
|
|
458
510
|
if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
|
|
459
511
|
|
|
460
|
-
const
|
|
512
|
+
const selectedAgentHost = normalizeAgentHostOverride(agentHost);
|
|
513
|
+
const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
|
|
514
|
+
const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow);
|
|
461
515
|
|
|
462
|
-
const binary = resolveHostBinary(
|
|
463
|
-
const cwd = resolveCwd(
|
|
516
|
+
const binary = resolveHostBinary(effectiveRow, spec);
|
|
517
|
+
const cwd = resolveCwd(effectiveRow);
|
|
464
518
|
|
|
465
519
|
// Two-phase probe:
|
|
466
520
|
// 1. If the catalog declares an auth-status subcommand, try it first.
|
|
@@ -503,20 +557,21 @@ async function checkAgentStatus({ objectId, name }) {
|
|
|
503
557
|
|
|
504
558
|
const patch = buildRowPatch({
|
|
505
559
|
status,
|
|
506
|
-
provider:
|
|
560
|
+
provider: effectiveAgentHost,
|
|
507
561
|
checkedAt,
|
|
508
562
|
exitCode: usedResult.exitCode,
|
|
509
563
|
loginUrl: null,
|
|
510
564
|
label: spec.label,
|
|
511
565
|
spawnError: usedResult.spawnError
|
|
512
566
|
});
|
|
567
|
+
Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
|
|
513
568
|
|
|
514
569
|
await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
|
|
515
570
|
|
|
516
571
|
return {
|
|
517
572
|
ok: status === "active",
|
|
518
573
|
status,
|
|
519
|
-
provider:
|
|
574
|
+
provider: effectiveAgentHost,
|
|
520
575
|
label: spec.label,
|
|
521
576
|
binary,
|
|
522
577
|
cwd,
|