ultimate-pi 0.18.1 → 0.19.0
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/.agents/skills/harness-debate-plan/SKILL.md +1 -1
- package/.agents/skills/harness-decisions/SKILL.md +1 -2
- package/.agents/skills/harness-governor/SKILL.md +6 -5
- package/.pi/PACKAGING.md +4 -4
- package/.pi/SYSTEM.md +54 -120
- package/.pi/agents/harness/incident-recorder.md +0 -1
- package/.pi/agents/harness/planning/decompose.md +0 -2
- package/.pi/agents/harness/planning/execution-plan-author.md +0 -2
- package/.pi/agents/harness/planning/hypothesis-validator.md +0 -2
- package/.pi/agents/harness/planning/hypothesis.md +0 -2
- package/.pi/agents/harness/planning/implementation-researcher.md +0 -2
- package/.pi/agents/harness/planning/plan-adversary.md +0 -2
- package/.pi/agents/harness/planning/plan-evaluator.md +1 -3
- package/.pi/agents/harness/planning/planning-context.md +0 -2
- package/.pi/agents/harness/planning/review-integrator.md +0 -2
- package/.pi/agents/harness/planning/sprint-contract-auditor.md +0 -2
- package/.pi/agents/harness/planning/stack-researcher.md +0 -2
- package/.pi/agents/harness/reviewing/adversary.md +0 -2
- package/.pi/agents/harness/reviewing/evaluator.md +0 -2
- package/.pi/agents/harness/reviewing/tie-breaker.md +0 -2
- package/.pi/agents/harness/running/executor.md +0 -2
- package/.pi/agents/harness/sentrux-bootstrap.md +0 -1
- package/.pi/agents/harness/sentrux-steward.md +0 -2
- package/.pi/agents/harness/trace-librarian.md +0 -1
- package/.pi/extensions/00-posthog-network-bootstrap.ts +1 -1
- package/.pi/extensions/agt-kill-switch.ts +57 -0
- package/.pi/extensions/agt-prompt-guard.ts +32 -0
- package/.pi/extensions/custom-footer.ts +46 -145
- package/.pi/extensions/custom-header.ts +1 -1
- package/.pi/extensions/custom-system-prompt.ts +1 -1
- package/.pi/extensions/debate-orchestrator.ts +6 -6
- package/.pi/extensions/harness-ask-user.ts +7 -7
- package/.pi/extensions/harness-debate-tools.ts +26 -42
- package/.pi/extensions/harness-lens.ts +94 -0
- package/.pi/extensions/harness-plan-approval.ts +11 -11
- package/.pi/extensions/harness-run-context.ts +1070 -876
- package/.pi/extensions/harness-subagent-governance.ts +8 -0
- package/.pi/extensions/harness-subagent-submit.ts +34 -163
- package/.pi/extensions/harness-subagents.ts +3 -3
- package/.pi/extensions/harness-telemetry.ts +2 -2
- package/.pi/extensions/harness-web-tools.ts +2 -2
- package/.pi/extensions/policy-gate.ts +25 -5
- package/.pi/extensions/sentrux-rules-sync.ts +1 -1
- package/.pi/extensions/subagent-governance.ts +92 -0
- package/.pi/extensions/trace-recorder.ts +1 -1
- package/.pi/extensions/{ultimate-pi-vcc.ts → vcc-compaction.ts} +1 -1
- package/.pi/harness/README.md +6 -2
- package/.pi/harness/agents.manifest.json +22 -25
- package/.pi/harness/agents.policy.yaml +275 -0
- package/.pi/harness/docs/adrs/0030-inhouse-vcc-compaction.md +1 -1
- package/.pi/harness/docs/adrs/0035-plan-phase-review-gate.md +1 -1
- package/.pi/harness/docs/adrs/0045-harness-lens-minimal-contract.md +49 -0
- package/.pi/harness/docs/adrs/0046-agt-policy-engine.md +51 -0
- package/.pi/harness/docs/adrs/0047-agt-layered-security.md +39 -0
- package/.pi/harness/docs/adrs/0048-tool-call-hook-order.md +25 -0
- package/.pi/harness/docs/adrs/0049-agents-policy-manifest.md +36 -0
- package/.pi/harness/docs/adrs/README.md +5 -0
- package/.pi/harness/evolution/README.md +1 -2
- package/.pi/harness/examples/agents.policy.project.yaml +19 -0
- package/.pi/harness/examples/policies/custom-deny-bash.yaml +9 -0
- package/.pi/harness/policies/bash-denylists.yaml +5 -0
- package/.pi/harness/policies/defaults.yaml +51 -0
- package/.pi/harness/policies/orchestrator.yaml +18 -0
- package/.pi/harness/policies/phases.yaml +10 -0
- package/.pi/harness/policies/roles.yaml +5 -0
- package/.pi/harness/policies/web-guard.yaml +5 -0
- package/.pi/harness/policies/workflow-sequences.yaml +9 -0
- package/.pi/harness/sentrux/architecture.manifest.json +26 -4
- package/.pi/harness/specs/observation.schema.json +2 -1
- package/.pi/lib/agents-policy.d.mts +70 -0
- package/.pi/lib/agents-policy.mjs +325 -0
- package/.pi/lib/agents-policy.ts +19 -0
- package/.pi/lib/agt/audit-run-sink.ts +52 -0
- package/.pi/lib/agt/build-evaluation-context.ts +285 -0
- package/.pi/lib/agt/config.ts +28 -0
- package/.pi/lib/agt/delegation.ts +69 -0
- package/.pi/lib/agt/evaluate-policy.ts +56 -0
- package/.pi/lib/agt/identity-registry.ts +41 -0
- package/.pi/lib/agt/index.ts +55 -0
- package/.pi/lib/agt/kill-switch-state.ts +11 -0
- package/.pi/lib/agt/legacy-evaluate.ts +101 -0
- package/.pi/lib/agt/policy-engine.ts +154 -0
- package/.pi/lib/agt/rings.ts +21 -0
- package/.pi/lib/agt/sre-hooks.ts +45 -0
- package/.pi/lib/agt/trust-run-store.ts +26 -0
- package/.pi/lib/agt/workflow-history.ts +29 -0
- package/.pi/lib/agt-governance-active.ts +14 -0
- package/.pi/lib/agt-tool-guard.ts +78 -0
- package/.pi/lib/ask-user/dialog.ts +314 -0
- package/.pi/{extensions/lib → lib}/debate-bus-core.ts +10 -10
- package/.pi/{extensions/lib → lib}/debate-bus-state.ts +1 -1
- package/.pi/{extensions/lib → lib}/extension-load-guard.ts +13 -2
- package/.pi/lib/harness-agt-tool-guard.ts +5 -0
- package/.pi/{extensions/lib → lib}/harness-artifact-gate.ts +1 -1
- package/.pi/lib/harness-debate-core-deps.ts +14 -0
- package/.pi/lib/harness-debate-workflow-deps.ts +43 -0
- package/.pi/lib/harness-lens/.gitattributes +1 -0
- package/.pi/lib/harness-lens/clients/edit-autopatch.ts +88 -0
- package/.pi/lib/harness-lens/clients/file-kinds.ts +380 -0
- package/.pi/lib/harness-lens/clients/file-time.ts +215 -0
- package/.pi/lib/harness-lens/clients/file-utils.ts +484 -0
- package/.pi/lib/harness-lens/clients/format-service.ts +276 -0
- package/.pi/lib/harness-lens/clients/formatters.ts +1000 -0
- package/.pi/lib/harness-lens/clients/git-guard.ts +31 -0
- package/.pi/lib/harness-lens/clients/indent-retarget.ts +90 -0
- package/.pi/lib/harness-lens/clients/installer/index.ts +2368 -0
- package/.pi/lib/harness-lens/clients/latency-logger.ts +80 -0
- package/.pi/lib/harness-lens/clients/lens-config.ts +43 -0
- package/.pi/lib/harness-lens/clients/lens-events.ts +164 -0
- package/.pi/lib/harness-lens/clients/lsp/aggregation.ts +91 -0
- package/.pi/lib/harness-lens/clients/lsp/client.ts +1466 -0
- package/.pi/lib/harness-lens/clients/lsp/config.ts +216 -0
- package/.pi/lib/harness-lens/clients/lsp/edits.ts +297 -0
- package/.pi/lib/harness-lens/clients/lsp/index.ts +1355 -0
- package/.pi/lib/harness-lens/clients/lsp/interactive-install.ts +424 -0
- package/.pi/lib/harness-lens/clients/lsp/language.ts +223 -0
- package/.pi/lib/harness-lens/clients/lsp/launch.ts +939 -0
- package/.pi/lib/harness-lens/clients/lsp/lsp-index.ts +11 -0
- package/.pi/lib/harness-lens/clients/lsp/path-utils.ts +12 -0
- package/.pi/lib/harness-lens/clients/lsp/server-strategies.ts +81 -0
- package/.pi/lib/harness-lens/clients/lsp/server.ts +1971 -0
- package/.pi/lib/harness-lens/clients/path-utils.ts +182 -0
- package/.pi/lib/harness-lens/clients/pipeline.ts +360 -0
- package/.pi/lib/harness-lens/clients/project-profile.ts +117 -0
- package/.pi/lib/harness-lens/clients/runtime-agent-end.ts +112 -0
- package/.pi/lib/harness-lens/clients/runtime-config.ts +33 -0
- package/.pi/lib/harness-lens/clients/runtime-coordinator.ts +186 -0
- package/.pi/lib/harness-lens/clients/runtime-tool-result.ts +171 -0
- package/.pi/lib/harness-lens/clients/safe-spawn.ts +339 -0
- package/.pi/lib/harness-lens/clients/secrets-scanner.ts +214 -0
- package/.pi/lib/harness-lens/clients/tool-policy.ts +2072 -0
- package/.pi/lib/harness-lens/clients/types.ts +59 -0
- package/.pi/lib/harness-lens/clients/widget-state.ts +283 -0
- package/.pi/lib/harness-lens/index.ts +532 -0
- package/.pi/lib/harness-lens/tools/lsp-diagnostics.ts +706 -0
- package/.pi/lib/harness-lens/tools/lsp-navigation.ts +1246 -0
- package/.pi/{extensions/lib → lib}/harness-posthog.ts +3 -0
- package/.pi/lib/harness-run-context-responses.ts +9 -0
- package/.pi/lib/harness-run-context.ts +0 -2
- package/.pi/{extensions/lib/spawn-policy.ts → lib/harness-spawn-policy.ts} +1 -0
- package/.pi/{extensions/lib → lib}/harness-spawn-topology.ts +1 -1
- package/.pi/lib/harness-subagent-auth.ts +51 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-precheck.ts +10 -7
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-pipeline.ts +3 -3
- package/.pi/lib/harness-subagent-submit-register.ts +163 -0
- package/.pi/{extensions/lib → lib}/harness-subagent-submit-registry.ts +1 -37
- package/.pi/{extensions/lib → lib}/harness-subagents-bridge.ts +53 -14
- package/.pi/{extensions/lib → lib}/harness-subprocess-bootstrap.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/create-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/format-plan.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/plan-review.ts +162 -201
- package/.pi/{extensions/lib → lib}/plan-approval/render.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/resolve-disk.ts +2 -2
- package/.pi/{extensions/lib → lib}/plan-approval/types.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-approval/validate.ts +3 -3
- package/.pi/{extensions/lib → lib}/plan-debate-envelope.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-gate.ts +1 -1
- package/.pi/{extensions/lib → lib}/plan-debate-lane.ts +1 -4
- package/.pi/{extensions/lib → lib}/plan-messenger.ts +1 -1
- package/.pi/prompts/harness-plan.md +1 -1
- package/.pi/prompts/harness-setup.md +37 -64
- package/.pi/scripts/README.md +2 -5
- package/.pi/scripts/generate-agents-policy-yaml.mjs +148 -0
- package/.pi/scripts/harness-agents-manifest.mjs +60 -3
- package/.pi/scripts/harness-agt-doctor.ts +36 -0
- package/.pi/scripts/harness-cli-verify.sh +9 -2
- package/.pi/scripts/harness-verify.mjs +113 -39
- package/.pi/scripts/harness-web-policy-guard.mjs +2 -2
- package/.pi/scripts/validate-plan-dag.mjs +65 -74
- package/.pi/scripts/vendor-pi-vcc-settings.stub.ts +2 -2
- package/.pi/scripts/vendor-sync-pi-vcc.sh +1 -1
- package/.pi/skills/architecture/broker-domain/SKILL.md +65 -0
- package/.pi/skills/architecture/cqrs/SKILL.md +63 -0
- package/.pi/skills/architecture/event-driven/SKILL.md +60 -0
- package/.pi/skills/architecture/hexagonal-ports-adapters/SKILL.md +66 -0
- package/.pi/skills/architecture/layered/SKILL.md +68 -0
- package/.pi/skills/architecture/microkernel/SKILL.md +62 -0
- package/.pi/skills/architecture/microservices/SKILL.md +64 -0
- package/.pi/skills/architecture/modular-monolith/SKILL.md +65 -0
- package/.pi/skills/architecture/orchestration-driven-soa/SKILL.md +61 -0
- package/.pi/skills/architecture/pipeline/SKILL.md +63 -0
- package/.pi/skills/architecture/service-based/SKILL.md +64 -0
- package/.pi/skills/architecture/service-mesh/SKILL.md +60 -0
- package/.pi/skills/architecture/space-based/SKILL.md +60 -0
- package/.pi/skills/ast-grep/SKILL.md +40 -321
- package/.pi/skills/delivery/debugging-discipline/SKILL.md +36 -0
- package/.pi/skills/delivery/documentation-update/SKILL.md +33 -0
- package/.pi/skills/delivery/requirements-to-implementation/SKILL.md +34 -0
- package/.pi/skills/delivery/risk-based-verification/SKILL.md +43 -0
- package/.pi/skills/delivery/tradeoff-analysis/SKILL.md +34 -0
- package/.pi/skills/engineering/api-contract-design/SKILL.md +38 -0
- package/.pi/skills/engineering/cohesion-coupling/SKILL.md +43 -0
- package/.pi/skills/engineering/complexity-control/SKILL.md +31 -0
- package/.pi/skills/engineering/defensive-programming/SKILL.md +38 -0
- package/.pi/skills/engineering/dependency-management/SKILL.md +29 -0
- package/.pi/skills/engineering/domain-modeling/SKILL.md +32 -0
- package/.pi/skills/engineering/error-handling/SKILL.md +37 -0
- package/.pi/skills/engineering/legacy-code-seams/SKILL.md +35 -0
- package/.pi/skills/engineering/naming-and-intent/SKILL.md +29 -0
- package/.pi/skills/engineering/refactoring-safe-evolution/SKILL.md +35 -0
- package/.pi/skills/engineering/routine-function-design/SKILL.md +34 -0
- package/.pi/skills/engineering/small-change-discipline/SKILL.md +35 -0
- package/.pi/skills/lsp-navigation/SKILL.md +89 -0
- package/.pi/skills/quality/code-review-self-check/SKILL.md +35 -0
- package/.pi/skills/quality/privacy-data-handling/SKILL.md +26 -0
- package/.pi/skills/quality/security-review/SKILL.md +34 -0
- package/.pi/skills/quality/test-strategy/SKILL.md +33 -0
- package/.pi/skills/quality/testability-design/SKILL.md +33 -0
- package/.pi/skills/systems/concurrency-safety/SKILL.md +32 -0
- package/.pi/skills/systems/data-modeling-migrations/SKILL.md +31 -0
- package/.pi/skills/systems/observability-instrumentation/SKILL.md +32 -0
- package/.pi/skills/systems/performance-measurement/SKILL.md +35 -0
- package/.pi/skills/systems/reliability-design/SKILL.md +32 -0
- package/.sentrux/rules.toml +20 -4
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +14 -0
- package/README.md +3 -12
- package/THIRD_PARTY_NOTICES.md +12 -21
- package/package.json +15 -7
- package/vendor/pi-subagents/src/agents.ts +45 -1
- package/vendor/pi-subagents/src/subagents.ts +866 -811
- package/vendor/pi-vcc/src/core/brief.ts +68 -99
- package/vendor/pi-vcc/src/core/settings.ts +2 -2
- package/.agents/skills/caveman/SKILL.md +0 -67
- package/.pi/agents/harness/meta-optimizer.md +0 -36
- package/.pi/extensions/lib/ask-user/dialog.ts +0 -260
- package/.pi/extensions/lib/harness-subagent-auth.ts +0 -207
- package/.pi/extensions/lib/harness-subagent-policy.ts +0 -236
- package/.pi/extensions/pi-model-router-harness.ts +0 -42
- package/.pi/harness/evolution/meta-optimizer.mjs +0 -99
- package/.pi/harness/specs/router-tuning-proposal.schema.json +0 -114
- package/.pi/model-router.example.json +0 -36
- package/.pi/prompts/harness-critic.md +0 -10
- package/.pi/prompts/harness-eval.md +0 -10
- package/.pi/prompts/harness-router-tune.md +0 -52
- package/.pi/scripts/harness-generate-model-router.mjs +0 -327
- package/.pi/scripts/harness-model-router-routing.test.mjs +0 -97
- package/.pi/scripts/harness-sync-model-router.mjs +0 -97
- package/.pi/scripts/vendor-sync-pi-model-router.sh +0 -47
- package/vendor/pi-model-router/.prettierignore +0 -4
- package/vendor/pi-model-router/.prettierrc +0 -5
- package/vendor/pi-model-router/AGENTS.md +0 -39
- package/vendor/pi-model-router/LICENSE +0 -21
- package/vendor/pi-model-router/README.md +0 -99
- package/vendor/pi-model-router/UPSTREAM_PIN.md +0 -10
- package/vendor/pi-model-router/docs/ARCHITECTURE.md +0 -54
- package/vendor/pi-model-router/extensions/commands.ts +0 -720
- package/vendor/pi-model-router/extensions/config.ts +0 -348
- package/vendor/pi-model-router/extensions/constants.ts +0 -1
- package/vendor/pi-model-router/extensions/index.ts +0 -478
- package/vendor/pi-model-router/extensions/provider.ts +0 -580
- package/vendor/pi-model-router/extensions/routing.ts +0 -564
- package/vendor/pi-model-router/extensions/state.ts +0 -52
- package/vendor/pi-model-router/extensions/types.ts +0 -95
- package/vendor/pi-model-router/extensions/ui.ts +0 -144
- package/vendor/pi-model-router/model-router.example.json +0 -48
- package/vendor/pi-model-router/package.json +0 -48
- package/vendor/pi-model-router/tsconfig.json +0 -16
- /package/.pi/{prompts → harness/docs}/planning-rubrics.md +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/fallback.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/render.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/types.ts +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate-core.mjs +0 -0
- /package/.pi/{extensions/lib → lib}/ask-user/validate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-cocoindex-refresh.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-paths.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-spawn-budget.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-vcc-settings.ts +0 -0
- /package/.pi/{extensions/lib → lib}/harness-web/run-cli.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/dialog.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval/schema.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-approval-readiness.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-eligibility.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-focus.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-id.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-lanes.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-round-status.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-debate-write-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-gate.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-review-integrator-rules.ts +0 -0
- /package/.pi/{extensions/lib → lib}/plan-scope-guard.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-client.ts +0 -0
- /package/.pi/{extensions/lib → lib}/posthog-node.d.ts +0 -0
|
@@ -42,7 +42,9 @@ export interface SpawnAuthForward {
|
|
|
42
42
|
|
|
43
43
|
export interface HarnessSubagentsOptions {
|
|
44
44
|
packageRoot?: string;
|
|
45
|
-
/** Absolute path to
|
|
45
|
+
/** Absolute path to subprocess governance extension (AGT + harness submit tools). */
|
|
46
|
+
subprocessGovernanceExtensionPath?: string;
|
|
47
|
+
/** @deprecated Use subprocessGovernanceExtensionPath */
|
|
46
48
|
harnessSubprocessExtensionPath?: string;
|
|
47
49
|
/** Extra env vars per subprocess (e.g. HARNESS_RUN_ID, HARNESS_RUN_DIR). */
|
|
48
50
|
resolveSubprocessEnv?: (
|
|
@@ -313,7 +315,7 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
313
315
|
for (const msg of messages) {
|
|
314
316
|
if (msg.role === "assistant") {
|
|
315
317
|
for (const part of msg.content) {
|
|
316
|
-
if (part.type === "text") items.push({ type: "text", text: part.text });
|
|
318
|
+
if (part.type === "text") items.push({ type: "text" as const, text: part.text });
|
|
317
319
|
else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
|
|
318
320
|
}
|
|
319
321
|
}
|
|
@@ -408,6 +410,204 @@ function buildSpawnEnv(
|
|
|
408
410
|
return env;
|
|
409
411
|
}
|
|
410
412
|
|
|
413
|
+
function unknownAgentResult(
|
|
414
|
+
agents: AgentConfig[],
|
|
415
|
+
agentName: string,
|
|
416
|
+
task: string,
|
|
417
|
+
step: number | undefined,
|
|
418
|
+
): SingleResult {
|
|
419
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
420
|
+
return {
|
|
421
|
+
agent: agentName,
|
|
422
|
+
agentSource: "unknown",
|
|
423
|
+
task,
|
|
424
|
+
exitCode: 1,
|
|
425
|
+
messages: [],
|
|
426
|
+
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
427
|
+
usage: {
|
|
428
|
+
input: 0,
|
|
429
|
+
output: 0,
|
|
430
|
+
cacheRead: 0,
|
|
431
|
+
cacheWrite: 0,
|
|
432
|
+
cost: 0,
|
|
433
|
+
contextTokens: 0,
|
|
434
|
+
turns: 0,
|
|
435
|
+
},
|
|
436
|
+
step,
|
|
437
|
+
finalOutput: "",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function buildAgentArgs(
|
|
442
|
+
agent: AgentConfig,
|
|
443
|
+
spawnAuth: SpawnAuthForward | undefined,
|
|
444
|
+
subagentsOptions: HarnessSubagentsOptions | undefined,
|
|
445
|
+
): string[] {
|
|
446
|
+
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
447
|
+
if (agent.model) args.push("--model", agent.model);
|
|
448
|
+
else if (spawnAuth) args.push("--model", spawnAuth.modelRef);
|
|
449
|
+
if (spawnAuth?.apiKey) args.push("--api-key", spawnAuth.apiKey);
|
|
450
|
+
if (agent.thinking) args.push("--thinking", agent.thinking);
|
|
451
|
+
|
|
452
|
+
const governanceExt =
|
|
453
|
+
agent.extensionsOff &&
|
|
454
|
+
(subagentsOptions?.subprocessGovernanceExtensionPath ??
|
|
455
|
+
subagentsOptions?.harnessSubprocessExtensionPath);
|
|
456
|
+
if (agent.extensionsOff) {
|
|
457
|
+
args.push("--no-extensions");
|
|
458
|
+
if (governanceExt) args.push("-e", governanceExt);
|
|
459
|
+
if (agent.skillsOff) args.push("--no-skills");
|
|
460
|
+
}
|
|
461
|
+
if (agent.tools?.length) args.push("--tools", agent.tools.join(","));
|
|
462
|
+
else if (agent.extensionsOff) args.push("--no-tools");
|
|
463
|
+
return args;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function appendSubagentEvent(
|
|
467
|
+
currentResult: SingleResult,
|
|
468
|
+
event: any,
|
|
469
|
+
emitUpdate: () => void,
|
|
470
|
+
): void {
|
|
471
|
+
if (event.type === "message_end" && event.message) {
|
|
472
|
+
const msg = event.message as Message;
|
|
473
|
+
currentResult.messages.push(msg);
|
|
474
|
+
if (msg.role === "assistant") {
|
|
475
|
+
currentResult.usage.turns += 1;
|
|
476
|
+
const usage = msg.usage;
|
|
477
|
+
if (usage) {
|
|
478
|
+
currentResult.usage.input += usage.input || 0;
|
|
479
|
+
currentResult.usage.output += usage.output || 0;
|
|
480
|
+
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
481
|
+
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
482
|
+
currentResult.usage.cost += usage.cost?.total || 0;
|
|
483
|
+
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
484
|
+
}
|
|
485
|
+
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
486
|
+
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
487
|
+
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
488
|
+
}
|
|
489
|
+
emitUpdate();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
493
|
+
currentResult.messages.push(event.message as Message);
|
|
494
|
+
emitUpdate();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function parseSubagentLine(
|
|
499
|
+
line: string,
|
|
500
|
+
currentResult: SingleResult,
|
|
501
|
+
emitUpdate: () => void,
|
|
502
|
+
): void {
|
|
503
|
+
if (!line.trim()) return;
|
|
504
|
+
let event: any;
|
|
505
|
+
try {
|
|
506
|
+
event = JSON.parse(line);
|
|
507
|
+
} catch {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
appendSubagentEvent(currentResult, event, emitUpdate);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function cleanupTempPromptFiles(
|
|
514
|
+
tmpPromptPath: string | null,
|
|
515
|
+
tmpPromptDir: string | null,
|
|
516
|
+
): void {
|
|
517
|
+
if (tmpPromptPath) {
|
|
518
|
+
try {
|
|
519
|
+
fs.unlinkSync(tmpPromptPath);
|
|
520
|
+
} catch {
|
|
521
|
+
/* ignore */
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (tmpPromptDir) {
|
|
525
|
+
try {
|
|
526
|
+
fs.rmdirSync(tmpPromptDir);
|
|
527
|
+
} catch {
|
|
528
|
+
/* ignore */
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function runSubagentProcess(
|
|
534
|
+
invocation: { command: string; args: string[] },
|
|
535
|
+
runtime: {
|
|
536
|
+
cwd: string;
|
|
537
|
+
env: NodeJS.ProcessEnv;
|
|
538
|
+
timeoutMs?: number;
|
|
539
|
+
signal?: AbortSignal;
|
|
540
|
+
},
|
|
541
|
+
currentResult: SingleResult,
|
|
542
|
+
emitUpdate: () => void,
|
|
543
|
+
): Promise<{ exitCode: number; wasAborted: boolean; timedOut: boolean }> {
|
|
544
|
+
let wasAborted = false;
|
|
545
|
+
let timedOut = false;
|
|
546
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
547
|
+
let settled = false;
|
|
548
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
549
|
+
const finish = (code: number) => {
|
|
550
|
+
if (settled) return;
|
|
551
|
+
settled = true;
|
|
552
|
+
if (timeout) clearTimeout(timeout);
|
|
553
|
+
resolve(code);
|
|
554
|
+
};
|
|
555
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
556
|
+
cwd: runtime.cwd,
|
|
557
|
+
env: runtime.env,
|
|
558
|
+
detached: process.platform !== "win32",
|
|
559
|
+
shell: false,
|
|
560
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
561
|
+
});
|
|
562
|
+
let buffer = "";
|
|
563
|
+
if (runtime.timeoutMs != null) {
|
|
564
|
+
timeout = setTimeout(() => {
|
|
565
|
+
timedOut = true;
|
|
566
|
+
currentResult.timedOut = true;
|
|
567
|
+
currentResult.stopReason = "timeout";
|
|
568
|
+
currentResult.errorMessage = `Subagent timed out after ${runtime.timeoutMs}ms`;
|
|
569
|
+
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}Subagent timed out after ${runtime.timeoutMs}ms.`;
|
|
570
|
+
emitUpdate();
|
|
571
|
+
terminateProcess(proc);
|
|
572
|
+
}, runtime.timeoutMs);
|
|
573
|
+
timeout.unref();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
proc.stdout.on("data", (data) => {
|
|
577
|
+
buffer += data.toString();
|
|
578
|
+
const lines = buffer.split("\n");
|
|
579
|
+
buffer = lines.pop() || "";
|
|
580
|
+
for (const line of lines) parseSubagentLine(line, currentResult, emitUpdate);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
proc.stderr.on("data", (data) => {
|
|
584
|
+
currentResult.stderr += data.toString();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
proc.on("close", (code) => {
|
|
588
|
+
if (buffer.trim()) parseSubagentLine(buffer, currentResult, emitUpdate);
|
|
589
|
+
finish(timedOut ? 124 : (code ?? 0));
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
proc.on("error", (error) => {
|
|
593
|
+
currentResult.errorMessage = error.message;
|
|
594
|
+
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}${error.message}`;
|
|
595
|
+
finish(1);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (!runtime.signal) return;
|
|
599
|
+
const killProc = () => {
|
|
600
|
+
wasAborted = true;
|
|
601
|
+
currentResult.stopReason = "aborted";
|
|
602
|
+
currentResult.errorMessage = "Subagent was aborted";
|
|
603
|
+
terminateProcess(proc);
|
|
604
|
+
};
|
|
605
|
+
if (runtime.signal.aborted) killProc();
|
|
606
|
+
else runtime.signal.addEventListener("abort", killProc, { once: true });
|
|
607
|
+
});
|
|
608
|
+
return { exitCode, wasAborted, timedOut };
|
|
609
|
+
}
|
|
610
|
+
|
|
411
611
|
async function runSingleAgent(
|
|
412
612
|
defaultCwd: string,
|
|
413
613
|
agents: AgentConfig[],
|
|
@@ -424,43 +624,9 @@ async function runSingleAgent(
|
|
|
424
624
|
subagentsOptions?: HarnessSubagentsOptions,
|
|
425
625
|
): Promise<SingleResult> {
|
|
426
626
|
const agent = agents.find((a) => a.name === agentName);
|
|
627
|
+
if (!agent) return unknownAgentResult(agents, agentName, task, step);
|
|
427
628
|
|
|
428
|
-
|
|
429
|
-
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
430
|
-
return {
|
|
431
|
-
agent: agentName,
|
|
432
|
-
agentSource: "unknown",
|
|
433
|
-
task,
|
|
434
|
-
exitCode: 1,
|
|
435
|
-
messages: [],
|
|
436
|
-
stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
|
|
437
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
438
|
-
step,
|
|
439
|
-
finalOutput: "",
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
444
|
-
if (agent.model) args.push("--model", agent.model);
|
|
445
|
-
else if (spawnAuth) args.push("--model", spawnAuth.modelRef);
|
|
446
|
-
if (spawnAuth?.apiKey) args.push("--api-key", spawnAuth.apiKey);
|
|
447
|
-
if (agent.thinking) args.push("--thinking", agent.thinking);
|
|
448
|
-
const harnessExt =
|
|
449
|
-
agent.extensionsOff &&
|
|
450
|
-
agent.name.startsWith("harness/") &&
|
|
451
|
-
subagentsOptions?.harnessSubprocessExtensionPath;
|
|
452
|
-
if (agent.extensionsOff) {
|
|
453
|
-
args.push("--no-extensions");
|
|
454
|
-
if (harnessExt) {
|
|
455
|
-
args.push("-e", harnessExt);
|
|
456
|
-
}
|
|
457
|
-
if (agent.skillsOff) args.push("--no-skills");
|
|
458
|
-
}
|
|
459
|
-
if (agent.tools && agent.tools.length > 0) {
|
|
460
|
-
args.push("--tools", agent.tools.join(","));
|
|
461
|
-
} else if (agent.extensionsOff) {
|
|
462
|
-
args.push("--no-tools");
|
|
463
|
-
}
|
|
629
|
+
const args = buildAgentArgs(agent, spawnAuth, subagentsOptions);
|
|
464
630
|
const extraEnv = subagentsOptions?.resolveSubprocessEnv?.(task, agent);
|
|
465
631
|
const spawnEnv = buildSpawnEnv(packageRoot, {
|
|
466
632
|
...extraEnv,
|
|
@@ -469,7 +635,6 @@ async function runSingleAgent(
|
|
|
469
635
|
|
|
470
636
|
let tmpPromptDir: string | null = null;
|
|
471
637
|
let tmpPromptPath: string | null = null;
|
|
472
|
-
|
|
473
638
|
const currentResult: SingleResult = {
|
|
474
639
|
agent: agentName,
|
|
475
640
|
agentSource: agent.source,
|
|
@@ -477,20 +642,26 @@ async function runSingleAgent(
|
|
|
477
642
|
exitCode: 0,
|
|
478
643
|
messages: [],
|
|
479
644
|
stderr: "",
|
|
480
|
-
usage: {
|
|
645
|
+
usage: {
|
|
646
|
+
input: 0,
|
|
647
|
+
output: 0,
|
|
648
|
+
cacheRead: 0,
|
|
649
|
+
cacheWrite: 0,
|
|
650
|
+
cost: 0,
|
|
651
|
+
contextTokens: 0,
|
|
652
|
+
turns: 0,
|
|
653
|
+
},
|
|
481
654
|
model: agent.model,
|
|
482
655
|
step,
|
|
483
656
|
timeoutMs,
|
|
484
657
|
};
|
|
485
|
-
|
|
486
658
|
const emitUpdate = () => {
|
|
487
659
|
currentResult.finalOutput = getFinalOutput(currentResult.messages);
|
|
488
|
-
if (onUpdate)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}
|
|
660
|
+
if (!onUpdate) return;
|
|
661
|
+
onUpdate({
|
|
662
|
+
content: [{ type: "text" as const, text: currentResult.finalOutput || "(running...)" }],
|
|
663
|
+
details: makeDetails([currentResult]),
|
|
664
|
+
});
|
|
494
665
|
};
|
|
495
666
|
|
|
496
667
|
try {
|
|
@@ -500,130 +671,27 @@ async function runSingleAgent(
|
|
|
500
671
|
tmpPromptPath = tmp.filePath;
|
|
501
672
|
args.push("--append-system-prompt", tmpPromptPath);
|
|
502
673
|
}
|
|
503
|
-
|
|
504
674
|
args.push(`Task: ${task}`);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const invocation = getPiInvocation(args);
|
|
510
|
-
let settled = false;
|
|
511
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
512
|
-
const finish = (code: number) => {
|
|
513
|
-
if (settled) return;
|
|
514
|
-
settled = true;
|
|
515
|
-
if (timeout) clearTimeout(timeout);
|
|
516
|
-
resolve(code);
|
|
517
|
-
};
|
|
518
|
-
const proc = spawn(invocation.command, invocation.args, {
|
|
675
|
+
const invocation = getPiInvocation(args);
|
|
676
|
+
const runtimeResult = await runSubagentProcess(
|
|
677
|
+
invocation,
|
|
678
|
+
{
|
|
519
679
|
cwd: cwd ?? defaultCwd,
|
|
520
680
|
env: spawnEnv,
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
timedOut = true;
|
|
529
|
-
currentResult.timedOut = true;
|
|
530
|
-
currentResult.stopReason = "timeout";
|
|
531
|
-
currentResult.errorMessage = `Subagent timed out after ${timeoutMs}ms`;
|
|
532
|
-
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}Subagent timed out after ${timeoutMs}ms.`;
|
|
533
|
-
emitUpdate();
|
|
534
|
-
terminateProcess(proc);
|
|
535
|
-
}, timeoutMs);
|
|
536
|
-
timeout.unref();
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
const processLine = (line: string) => {
|
|
540
|
-
if (!line.trim()) return;
|
|
541
|
-
let event: any;
|
|
542
|
-
try {
|
|
543
|
-
event = JSON.parse(line);
|
|
544
|
-
} catch {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (event.type === "message_end" && event.message) {
|
|
549
|
-
const msg = event.message as Message;
|
|
550
|
-
currentResult.messages.push(msg);
|
|
551
|
-
|
|
552
|
-
if (msg.role === "assistant") {
|
|
553
|
-
currentResult.usage.turns++;
|
|
554
|
-
const usage = msg.usage;
|
|
555
|
-
if (usage) {
|
|
556
|
-
currentResult.usage.input += usage.input || 0;
|
|
557
|
-
currentResult.usage.output += usage.output || 0;
|
|
558
|
-
currentResult.usage.cacheRead += usage.cacheRead || 0;
|
|
559
|
-
currentResult.usage.cacheWrite += usage.cacheWrite || 0;
|
|
560
|
-
currentResult.usage.cost += usage.cost?.total || 0;
|
|
561
|
-
currentResult.usage.contextTokens = usage.totalTokens || 0;
|
|
562
|
-
}
|
|
563
|
-
if (!currentResult.model && msg.model) currentResult.model = msg.model;
|
|
564
|
-
if (msg.stopReason) currentResult.stopReason = msg.stopReason;
|
|
565
|
-
if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
|
|
566
|
-
}
|
|
567
|
-
emitUpdate();
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (event.type === "tool_result_end" && event.message) {
|
|
571
|
-
currentResult.messages.push(event.message as Message);
|
|
572
|
-
emitUpdate();
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
proc.stdout.on("data", (data) => {
|
|
577
|
-
buffer += data.toString();
|
|
578
|
-
const lines = buffer.split("\n");
|
|
579
|
-
buffer = lines.pop() || "";
|
|
580
|
-
for (const line of lines) processLine(line);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
proc.stderr.on("data", (data) => {
|
|
584
|
-
currentResult.stderr += data.toString();
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
proc.on("close", (code) => {
|
|
588
|
-
if (buffer.trim()) processLine(buffer);
|
|
589
|
-
finish(timedOut ? 124 : (code ?? 0));
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
proc.on("error", (error) => {
|
|
593
|
-
currentResult.errorMessage = error.message;
|
|
594
|
-
currentResult.stderr += `${currentResult.stderr ? "\n" : ""}${error.message}`;
|
|
595
|
-
finish(1);
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
if (signal) {
|
|
599
|
-
const killProc = () => {
|
|
600
|
-
wasAborted = true;
|
|
601
|
-
currentResult.stopReason = "aborted";
|
|
602
|
-
currentResult.errorMessage = "Subagent was aborted";
|
|
603
|
-
terminateProcess(proc);
|
|
604
|
-
};
|
|
605
|
-
if (signal.aborted) killProc();
|
|
606
|
-
else signal.addEventListener("abort", killProc, { once: true });
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
currentResult.exitCode = exitCode;
|
|
681
|
+
timeoutMs,
|
|
682
|
+
signal,
|
|
683
|
+
},
|
|
684
|
+
currentResult,
|
|
685
|
+
emitUpdate,
|
|
686
|
+
);
|
|
687
|
+
currentResult.exitCode = runtimeResult.exitCode;
|
|
611
688
|
currentResult.finalOutput = getFinalOutput(currentResult.messages);
|
|
612
|
-
if (wasAborted && !timedOut)
|
|
689
|
+
if (runtimeResult.wasAborted && !runtimeResult.timedOut) {
|
|
690
|
+
throw new Error("Subagent was aborted");
|
|
691
|
+
}
|
|
613
692
|
return currentResult;
|
|
614
693
|
} finally {
|
|
615
|
-
|
|
616
|
-
try {
|
|
617
|
-
fs.unlinkSync(tmpPromptPath);
|
|
618
|
-
} catch {
|
|
619
|
-
/* ignore */
|
|
620
|
-
}
|
|
621
|
-
if (tmpPromptDir)
|
|
622
|
-
try {
|
|
623
|
-
fs.rmdirSync(tmpPromptDir);
|
|
624
|
-
} catch {
|
|
625
|
-
/* ignore */
|
|
626
|
-
}
|
|
694
|
+
cleanupTempPromptFiles(tmpPromptPath, tmpPromptDir);
|
|
627
695
|
}
|
|
628
696
|
}
|
|
629
697
|
|
|
@@ -696,6 +764,575 @@ function truncateSubagentDetails(
|
|
|
696
764
|
};
|
|
697
765
|
}
|
|
698
766
|
|
|
767
|
+
type SubagentToolParams = {
|
|
768
|
+
agent?: string;
|
|
769
|
+
task?: string;
|
|
770
|
+
tasks?: Array<{ agent: string; task: string; cwd?: string; timeoutMs?: number }>;
|
|
771
|
+
chain?: Array<{ agent: string; task: string; cwd?: string; timeoutMs?: number }>;
|
|
772
|
+
aggregator?: { agent: string; task: string; cwd?: string; timeoutMs?: number };
|
|
773
|
+
agentScope?: AgentScope;
|
|
774
|
+
confirmProjectAgents?: boolean;
|
|
775
|
+
cwd?: string;
|
|
776
|
+
timeoutMs?: number;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
type SubagentExecuteContext = {
|
|
780
|
+
toolCallId: string;
|
|
781
|
+
params: SubagentToolParams;
|
|
782
|
+
signal: AbortSignal | undefined;
|
|
783
|
+
onUpdate: OnUpdateCallback | undefined;
|
|
784
|
+
ctx: ExtensionContext;
|
|
785
|
+
agents: AgentConfig[];
|
|
786
|
+
discovery: ReturnType<typeof discoverAgents>;
|
|
787
|
+
agentScope: AgentScope;
|
|
788
|
+
defaultTimeoutMs: number | undefined;
|
|
789
|
+
packageRoot?: string;
|
|
790
|
+
options: HarnessSubagentsOptions;
|
|
791
|
+
resolveSpawnAuth: (agentName: string) => Promise<SpawnAuthForward | undefined>;
|
|
792
|
+
makeDetails: (
|
|
793
|
+
mode: "single" | "parallel" | "chain",
|
|
794
|
+
) => (results: SingleResult[], aggregator?: SingleResult) => SubagentDetails;
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
function collectHarnessAgents(params: SubagentToolParams): string[] {
|
|
798
|
+
const harnessAgents: string[] = [];
|
|
799
|
+
if (params.agent?.startsWith("harness/")) harnessAgents.push(params.agent);
|
|
800
|
+
for (const task of params.tasks ?? []) {
|
|
801
|
+
if (task.agent.startsWith("harness/")) harnessAgents.push(task.agent);
|
|
802
|
+
}
|
|
803
|
+
for (const step of params.chain ?? []) {
|
|
804
|
+
if (step.agent.startsWith("harness/")) harnessAgents.push(step.agent);
|
|
805
|
+
}
|
|
806
|
+
if (params.aggregator?.agent.startsWith("harness/")) {
|
|
807
|
+
harnessAgents.push(params.aggregator.agent);
|
|
808
|
+
}
|
|
809
|
+
return harnessAgents;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function modeInfo(params: SubagentToolParams): {
|
|
813
|
+
hasChain: boolean;
|
|
814
|
+
hasTasks: boolean;
|
|
815
|
+
hasSingle: boolean;
|
|
816
|
+
modeCount: number;
|
|
817
|
+
} {
|
|
818
|
+
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
819
|
+
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
820
|
+
const hasSingle = Boolean(params.agent && params.task);
|
|
821
|
+
return {
|
|
822
|
+
hasChain,
|
|
823
|
+
hasTasks,
|
|
824
|
+
hasSingle,
|
|
825
|
+
modeCount: Number(hasChain) + Number(hasTasks) + Number(hasSingle),
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function maybeConfirmProjectAgents(
|
|
830
|
+
execCtx: SubagentExecuteContext,
|
|
831
|
+
mode: "single" | "parallel" | "chain",
|
|
832
|
+
): Promise<boolean> {
|
|
833
|
+
const { params, ctx, agents, discovery, agentScope } = execCtx;
|
|
834
|
+
if (!ctx.hasUI) return true;
|
|
835
|
+
if (agentScope !== "project" && agentScope !== "both") return true;
|
|
836
|
+
if (!params.confirmProjectAgents) return true;
|
|
837
|
+
|
|
838
|
+
const requested = new Set<string>();
|
|
839
|
+
if (params.agent) requested.add(params.agent);
|
|
840
|
+
if (params.aggregator) requested.add(params.aggregator.agent);
|
|
841
|
+
for (const task of params.tasks ?? []) requested.add(task.agent);
|
|
842
|
+
for (const step of params.chain ?? []) requested.add(step.agent);
|
|
843
|
+
|
|
844
|
+
const projectAgents = Array.from(requested)
|
|
845
|
+
.map((name) => agents.find((a) => a.name === name))
|
|
846
|
+
.filter((a): a is AgentConfig => a?.source === "project");
|
|
847
|
+
if (projectAgents.length === 0) return true;
|
|
848
|
+
|
|
849
|
+
const names = projectAgents.map((a) => a.name).join(", ");
|
|
850
|
+
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
851
|
+
const ok = await ctx.ui.confirm(
|
|
852
|
+
"Run project-local agents?",
|
|
853
|
+
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
854
|
+
);
|
|
855
|
+
if (ok) return true;
|
|
856
|
+
|
|
857
|
+
execCtx.onUpdate?.({
|
|
858
|
+
content: [{ type: "text" as const, text: "Canceled: project-local agents not approved." }],
|
|
859
|
+
details: execCtx.makeDetails(mode)([]),
|
|
860
|
+
});
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
async function executeChainMode(execCtx: SubagentExecuteContext) {
|
|
865
|
+
const { params, ctx, toolCallId, signal, defaultTimeoutMs, onUpdate } = execCtx;
|
|
866
|
+
const chain = params.chain ?? [];
|
|
867
|
+
const results: SingleResult[] = [];
|
|
868
|
+
let previousOutput = "";
|
|
869
|
+
const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, chain.length));
|
|
870
|
+
try {
|
|
871
|
+
for (let i = 0; i < chain.length; i++) {
|
|
872
|
+
const step = chain[i];
|
|
873
|
+
status.update(chainStatus(i + 1, chain.length, step.agent));
|
|
874
|
+
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
875
|
+
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
876
|
+
? (partial) => {
|
|
877
|
+
const current = partial.details?.results[0];
|
|
878
|
+
if (!current) return;
|
|
879
|
+
onUpdate({
|
|
880
|
+
content: partial.content,
|
|
881
|
+
details: execCtx.makeDetails("chain")([...results, current]),
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
: undefined;
|
|
885
|
+
const result = await runSingleAgent(
|
|
886
|
+
ctx.cwd,
|
|
887
|
+
execCtx.agents,
|
|
888
|
+
step.agent,
|
|
889
|
+
taskWithContext,
|
|
890
|
+
step.cwd,
|
|
891
|
+
i + 1,
|
|
892
|
+
signal,
|
|
893
|
+
step.timeoutMs ?? defaultTimeoutMs,
|
|
894
|
+
chainUpdate,
|
|
895
|
+
execCtx.makeDetails("chain"),
|
|
896
|
+
execCtx.packageRoot,
|
|
897
|
+
await execCtx.resolveSpawnAuth(step.agent),
|
|
898
|
+
execCtx.options,
|
|
899
|
+
);
|
|
900
|
+
results.push(result);
|
|
901
|
+
const errored =
|
|
902
|
+
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
903
|
+
if (errored) {
|
|
904
|
+
const errorMsg =
|
|
905
|
+
result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
906
|
+
return {
|
|
907
|
+
content: [{ type: "text" as const, text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
908
|
+
details: execCtx.makeDetails("chain")(results),
|
|
909
|
+
isError: true,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
previousOutput = getResultFinalOutput(result);
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
content: [{ type: "text" as const, text: getResultFinalOutput(results[results.length - 1]) || "(no output)" }],
|
|
916
|
+
details: execCtx.makeDetails("chain")(results),
|
|
917
|
+
};
|
|
918
|
+
} finally {
|
|
919
|
+
status.clear();
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function makeParallelPlaceholder(task: { agent: string; task: string }): SingleResult {
|
|
924
|
+
return {
|
|
925
|
+
agent: task.agent,
|
|
926
|
+
agentSource: "unknown",
|
|
927
|
+
task: task.task,
|
|
928
|
+
exitCode: -1,
|
|
929
|
+
messages: [],
|
|
930
|
+
stderr: "",
|
|
931
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
932
|
+
finalOutput: "",
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async function executeParallelMode(execCtx: SubagentExecuteContext) {
|
|
937
|
+
const { params, ctx, toolCallId, signal, defaultTimeoutMs, onUpdate } = execCtx;
|
|
938
|
+
const tasks = params.tasks ?? [];
|
|
939
|
+
if (tasks.length > MAX_PARALLEL_TASKS) {
|
|
940
|
+
return {
|
|
941
|
+
content: [{ type: "text" as const, text: `Too many parallel tasks (${tasks.length}). Max is ${MAX_PARALLEL_TASKS}.` }],
|
|
942
|
+
details: execCtx.makeDetails("parallel")([]),
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const status = startSubagentStatus(ctx, toolCallId, parallelStatus(0, tasks.length, tasks.length));
|
|
947
|
+
try {
|
|
948
|
+
const allResults = tasks.map(makeParallelPlaceholder);
|
|
949
|
+
let doneCount = 0;
|
|
950
|
+
let runningCount = tasks.length;
|
|
951
|
+
const emitParallelUpdate = () => {
|
|
952
|
+
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
953
|
+
if (!onUpdate) return;
|
|
954
|
+
onUpdate({
|
|
955
|
+
content: [{ type: "text" as const, text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...` }],
|
|
956
|
+
details: execCtx.makeDetails("parallel")([...allResults]),
|
|
957
|
+
});
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
const results = await mapWithConcurrencyLimit(tasks, MAX_CONCURRENCY, async (task, index) => {
|
|
961
|
+
const result = await runSingleAgent(
|
|
962
|
+
ctx.cwd,
|
|
963
|
+
execCtx.agents,
|
|
964
|
+
task.agent,
|
|
965
|
+
task.task,
|
|
966
|
+
task.cwd,
|
|
967
|
+
undefined,
|
|
968
|
+
signal,
|
|
969
|
+
task.timeoutMs ?? defaultTimeoutMs,
|
|
970
|
+
(partial) => {
|
|
971
|
+
const current = partial.details?.results[0];
|
|
972
|
+
if (!current) return;
|
|
973
|
+
allResults[index] = { ...current, exitCode: -1 };
|
|
974
|
+
emitParallelUpdate();
|
|
975
|
+
},
|
|
976
|
+
execCtx.makeDetails("parallel"),
|
|
977
|
+
execCtx.packageRoot,
|
|
978
|
+
await execCtx.resolveSpawnAuth(task.agent),
|
|
979
|
+
execCtx.options,
|
|
980
|
+
);
|
|
981
|
+
allResults[index] = result;
|
|
982
|
+
doneCount += 1;
|
|
983
|
+
runningCount -= 1;
|
|
984
|
+
emitParallelUpdate();
|
|
985
|
+
return result;
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
let aggregatorResult: SingleResult | undefined;
|
|
989
|
+
if (params.aggregator) {
|
|
990
|
+
const aggregator = params.aggregator;
|
|
991
|
+
status.update(fanInStatus(aggregator.agent));
|
|
992
|
+
const fanInContext = buildFanInContext(results);
|
|
993
|
+
const aggregatorTask = aggregator.task.includes("{previous}")
|
|
994
|
+
? aggregator.task.replace(/\{previous\}/g, fanInContext)
|
|
995
|
+
: `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
|
|
996
|
+
aggregatorResult = await runSingleAgent(
|
|
997
|
+
ctx.cwd,
|
|
998
|
+
execCtx.agents,
|
|
999
|
+
aggregator.agent,
|
|
1000
|
+
aggregatorTask,
|
|
1001
|
+
aggregator.cwd,
|
|
1002
|
+
undefined,
|
|
1003
|
+
signal,
|
|
1004
|
+
aggregator.timeoutMs ?? defaultTimeoutMs,
|
|
1005
|
+
(partial) => {
|
|
1006
|
+
status.update(fanInStatus(aggregator.agent));
|
|
1007
|
+
const current = partial.details?.results[0];
|
|
1008
|
+
if (!onUpdate || !current) return;
|
|
1009
|
+
onUpdate({
|
|
1010
|
+
content: partial.content,
|
|
1011
|
+
details: execCtx.makeDetails("parallel")(results, current),
|
|
1012
|
+
});
|
|
1013
|
+
},
|
|
1014
|
+
execCtx.makeDetails("parallel"),
|
|
1015
|
+
execCtx.packageRoot,
|
|
1016
|
+
await execCtx.resolveSpawnAuth(aggregator.agent),
|
|
1017
|
+
execCtx.options,
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
1022
|
+
const summaries = results.map((r) => {
|
|
1023
|
+
const summary = getResultFinalOutput(r) || r.errorMessage || r.stderr.trim();
|
|
1024
|
+
const preview = summary.slice(0, 160) + (summary.length > 160 ? "..." : "");
|
|
1025
|
+
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
|
1026
|
+
});
|
|
1027
|
+
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
1028
|
+
const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
1029
|
+
return {
|
|
1030
|
+
content: [
|
|
1031
|
+
{
|
|
1032
|
+
type: "text" as const,
|
|
1033
|
+
text: aggregatorResult
|
|
1034
|
+
? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
|
|
1035
|
+
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
1036
|
+
},
|
|
1037
|
+
],
|
|
1038
|
+
details: execCtx.makeDetails("parallel")(results, aggregatorResult),
|
|
1039
|
+
isError: aggregatorResult
|
|
1040
|
+
? aggregatorResult.exitCode !== 0 ||
|
|
1041
|
+
aggregatorResult.stopReason === "error" ||
|
|
1042
|
+
aggregatorResult.stopReason === "aborted"
|
|
1043
|
+
: undefined,
|
|
1044
|
+
};
|
|
1045
|
+
} finally {
|
|
1046
|
+
status.clear();
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async function executeSingleMode(execCtx: SubagentExecuteContext) {
|
|
1051
|
+
const { params, ctx, toolCallId, signal } = execCtx;
|
|
1052
|
+
const status = startSubagentStatus(ctx, toolCallId, singleStatus(params.agent || "..."));
|
|
1053
|
+
try {
|
|
1054
|
+
const result = await runSingleAgent(
|
|
1055
|
+
ctx.cwd,
|
|
1056
|
+
execCtx.agents,
|
|
1057
|
+
params.agent || "",
|
|
1058
|
+
params.task || "",
|
|
1059
|
+
params.cwd,
|
|
1060
|
+
undefined,
|
|
1061
|
+
signal,
|
|
1062
|
+
params.timeoutMs ?? execCtx.defaultTimeoutMs,
|
|
1063
|
+
execCtx.onUpdate,
|
|
1064
|
+
execCtx.makeDetails("single"),
|
|
1065
|
+
execCtx.packageRoot,
|
|
1066
|
+
await execCtx.resolveSpawnAuth(params.agent || ""),
|
|
1067
|
+
execCtx.options,
|
|
1068
|
+
);
|
|
1069
|
+
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
1070
|
+
if (isError) {
|
|
1071
|
+
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
1072
|
+
return {
|
|
1073
|
+
content: [{ type: "text" as const, text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
1074
|
+
details: execCtx.makeDetails("single")([result]),
|
|
1075
|
+
isError: true,
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
content: [{ type: "text" as const, text: getResultFinalOutput(result) || "(no output)" }],
|
|
1080
|
+
details: execCtx.makeDetails("single")([result]),
|
|
1081
|
+
};
|
|
1082
|
+
} finally {
|
|
1083
|
+
status.clear();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function aggregateUsage(results: SingleResult[]) {
|
|
1088
|
+
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
1089
|
+
for (const r of results) {
|
|
1090
|
+
total.input += r.usage.input;
|
|
1091
|
+
total.output += r.usage.output;
|
|
1092
|
+
total.cacheRead += r.usage.cacheRead;
|
|
1093
|
+
total.cacheWrite += r.usage.cacheWrite;
|
|
1094
|
+
total.cost += r.usage.cost;
|
|
1095
|
+
total.turns += r.usage.turns;
|
|
1096
|
+
}
|
|
1097
|
+
return total;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function createDisplayItemRenderer(theme: any, expanded: boolean) {
|
|
1101
|
+
return (items: DisplayItem[], limit?: number): string => {
|
|
1102
|
+
const toShow = limit ? items.slice(-limit) : items;
|
|
1103
|
+
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
1104
|
+
let text = "";
|
|
1105
|
+
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
1106
|
+
for (const item of toShow) {
|
|
1107
|
+
if (item.type === "text") {
|
|
1108
|
+
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
|
1109
|
+
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
1113
|
+
}
|
|
1114
|
+
return text.trimEnd();
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function renderSingleSubagentResult(details: SubagentDetails, expanded: boolean, theme: any, mdTheme: any, renderDisplayItems: (items: DisplayItem[], limit?: number) => string): any {
|
|
1119
|
+
const r = details.results[0];
|
|
1120
|
+
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
1121
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1122
|
+
const displayItems = getDisplayItems(r.messages);
|
|
1123
|
+
const finalOutput = getResultFinalOutput(r);
|
|
1124
|
+
if (!expanded) {
|
|
1125
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
1126
|
+
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
1127
|
+
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
1128
|
+
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1129
|
+
else {
|
|
1130
|
+
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
1131
|
+
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1132
|
+
}
|
|
1133
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1134
|
+
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
1135
|
+
return new Text(text, 0, 0);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const container = new Container();
|
|
1139
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
1140
|
+
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
1141
|
+
container.addChild(new Text(header, 0, 0));
|
|
1142
|
+
if (isError && r.errorMessage) container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
1143
|
+
container.addChild(new Spacer(1));
|
|
1144
|
+
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
1145
|
+
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
1146
|
+
container.addChild(new Spacer(1));
|
|
1147
|
+
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
1148
|
+
if (displayItems.length === 0 && !finalOutput) {
|
|
1149
|
+
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
1150
|
+
} else {
|
|
1151
|
+
for (const item of displayItems) {
|
|
1152
|
+
if (item.type !== "toolCall") continue;
|
|
1153
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
|
|
1154
|
+
}
|
|
1155
|
+
if (finalOutput) {
|
|
1156
|
+
container.addChild(new Spacer(1));
|
|
1157
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1161
|
+
if (usageStr) {
|
|
1162
|
+
container.addChild(new Spacer(1));
|
|
1163
|
+
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
1164
|
+
}
|
|
1165
|
+
return container;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function renderChainSubagentResult(details: SubagentDetails, expanded: boolean, theme: any, mdTheme: any, renderDisplayItems: (items: DisplayItem[], limit?: number) => string): any {
|
|
1169
|
+
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1170
|
+
const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1171
|
+
if (!expanded) {
|
|
1172
|
+
let text = icon + " " + theme.fg("toolTitle", theme.bold("chain ")) + theme.fg("accent", `${successCount}/${details.results.length} steps`);
|
|
1173
|
+
for (const r of details.results) {
|
|
1174
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1175
|
+
const displayItems = getDisplayItems(r.messages);
|
|
1176
|
+
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1177
|
+
text += displayItems.length === 0 ? `\n${theme.fg("muted", "(no output)")}` : `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1178
|
+
}
|
|
1179
|
+
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1180
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1181
|
+
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1182
|
+
return new Text(text, 0, 0);
|
|
1183
|
+
}
|
|
1184
|
+
const container = new Container();
|
|
1185
|
+
container.addChild(new Text(icon + " " + theme.fg("toolTitle", theme.bold("chain ")) + theme.fg("accent", `${successCount}/${details.results.length} steps`), 0, 0));
|
|
1186
|
+
for (const r of details.results) {
|
|
1187
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1188
|
+
const displayItems = getDisplayItems(r.messages);
|
|
1189
|
+
const finalOutput = getResultFinalOutput(r);
|
|
1190
|
+
container.addChild(new Spacer(1));
|
|
1191
|
+
container.addChild(new Text(`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0));
|
|
1192
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
1193
|
+
for (const item of displayItems) {
|
|
1194
|
+
if (item.type !== "toolCall") continue;
|
|
1195
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
|
|
1196
|
+
}
|
|
1197
|
+
if (finalOutput) {
|
|
1198
|
+
container.addChild(new Spacer(1));
|
|
1199
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1200
|
+
}
|
|
1201
|
+
const stepUsage = formatUsageStats(r.usage, r.model);
|
|
1202
|
+
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
1203
|
+
}
|
|
1204
|
+
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1205
|
+
if (usageStr) {
|
|
1206
|
+
container.addChild(new Spacer(1));
|
|
1207
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
1208
|
+
}
|
|
1209
|
+
return container;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function renderParallelExpandedSubagentResult(details: SubagentDetails, theme: any, mdTheme: any): any {
|
|
1213
|
+
const container = new Container();
|
|
1214
|
+
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1215
|
+
const aggregator = details.aggregator;
|
|
1216
|
+
const status = aggregator
|
|
1217
|
+
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
1218
|
+
: `${successCount}/${details.results.length} tasks`;
|
|
1219
|
+
container.addChild(new Text(`${theme.fg("success", "✓")} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`, 0, 0));
|
|
1220
|
+
|
|
1221
|
+
for (const r of details.results) {
|
|
1222
|
+
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1223
|
+
const displayItems = getDisplayItems(r.messages);
|
|
1224
|
+
const finalOutput = getResultFinalOutput(r);
|
|
1225
|
+
container.addChild(new Spacer(1));
|
|
1226
|
+
container.addChild(new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0));
|
|
1227
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
1228
|
+
for (const item of displayItems) {
|
|
1229
|
+
if (item.type !== "toolCall") continue;
|
|
1230
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
|
|
1231
|
+
}
|
|
1232
|
+
if (finalOutput) {
|
|
1233
|
+
container.addChild(new Spacer(1));
|
|
1234
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1235
|
+
}
|
|
1236
|
+
const taskUsage = formatUsageStats(r.usage, r.model);
|
|
1237
|
+
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (aggregator) {
|
|
1241
|
+
const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1242
|
+
const displayItems = getDisplayItems(aggregator.messages);
|
|
1243
|
+
const finalOutput = getResultFinalOutput(aggregator);
|
|
1244
|
+
container.addChild(new Spacer(1));
|
|
1245
|
+
container.addChild(new Text(`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`, 0, 0));
|
|
1246
|
+
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
|
|
1247
|
+
for (const item of displayItems) {
|
|
1248
|
+
if (item.type !== "toolCall") continue;
|
|
1249
|
+
container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
|
|
1250
|
+
}
|
|
1251
|
+
if (finalOutput) {
|
|
1252
|
+
container.addChild(new Spacer(1));
|
|
1253
|
+
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1254
|
+
}
|
|
1255
|
+
const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model);
|
|
1256
|
+
if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
1260
|
+
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
1261
|
+
if (usageStr) {
|
|
1262
|
+
container.addChild(new Spacer(1));
|
|
1263
|
+
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
1264
|
+
}
|
|
1265
|
+
return container;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function renderParallelSubagentResult(details: SubagentDetails, expanded: boolean, theme: any, mdTheme: any, renderDisplayItems: (items: DisplayItem[], limit?: number) => string): any {
|
|
1269
|
+
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
1270
|
+
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1271
|
+
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
|
1272
|
+
const aggregator = details.aggregator;
|
|
1273
|
+
const aggregatorRunning = aggregator?.exitCode === -1;
|
|
1274
|
+
const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
|
|
1275
|
+
const isRunning = running > 0 || aggregatorRunning;
|
|
1276
|
+
if (expanded && !isRunning) {
|
|
1277
|
+
return renderParallelExpandedSubagentResult(details, theme, mdTheme);
|
|
1278
|
+
}
|
|
1279
|
+
const icon = isRunning ? theme.fg("warning", "⏳") : failCount > 0 || aggregatorFailed ? theme.fg("warning", "◐") : theme.fg("success", "✓");
|
|
1280
|
+
const status = isRunning
|
|
1281
|
+
? aggregatorRunning
|
|
1282
|
+
? `${successCount + failCount}/${details.results.length} done, fan-in running`
|
|
1283
|
+
: `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
1284
|
+
: aggregator
|
|
1285
|
+
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
1286
|
+
: `${successCount}/${details.results.length} tasks`;
|
|
1287
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
1288
|
+
for (const r of details.results) {
|
|
1289
|
+
const rIcon = r.exitCode === -1 ? theme.fg("warning", "⏳") : r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1290
|
+
const displayItems = getDisplayItems(r.messages);
|
|
1291
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1292
|
+
if (displayItems.length === 0) text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1293
|
+
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1294
|
+
}
|
|
1295
|
+
if (aggregator) {
|
|
1296
|
+
const rIcon = aggregator.exitCode === -1 ? theme.fg("warning", "⏳") : aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1297
|
+
const displayItems = getDisplayItems(aggregator.messages);
|
|
1298
|
+
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
1299
|
+
if (displayItems.length === 0) text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1300
|
+
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1301
|
+
}
|
|
1302
|
+
if (!isRunning) {
|
|
1303
|
+
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
1304
|
+
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
1305
|
+
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1306
|
+
}
|
|
1307
|
+
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1308
|
+
return new Text(text, 0, 0);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function renderSubagentResult(
|
|
1312
|
+
result: AgentToolResult<SubagentDetails>,
|
|
1313
|
+
expanded: boolean,
|
|
1314
|
+
theme: any,
|
|
1315
|
+
): any {
|
|
1316
|
+
const details = result.details as SubagentDetails | undefined;
|
|
1317
|
+
if (!details || details.results.length === 0) {
|
|
1318
|
+
const text = result.content[0];
|
|
1319
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1320
|
+
}
|
|
1321
|
+
const mdTheme = getMarkdownTheme();
|
|
1322
|
+
const renderDisplayItems = createDisplayItemRenderer(theme, expanded);
|
|
1323
|
+
if (details.mode === "single" && details.results.length === 1) {
|
|
1324
|
+
return renderSingleSubagentResult(details, expanded, theme, mdTheme, renderDisplayItems);
|
|
1325
|
+
}
|
|
1326
|
+
if (details.mode === "chain") {
|
|
1327
|
+
return renderChainSubagentResult(details, expanded, theme, mdTheme, renderDisplayItems);
|
|
1328
|
+
}
|
|
1329
|
+
if (details.mode === "parallel") {
|
|
1330
|
+
return renderParallelSubagentResult(details, expanded, theme, mdTheme, renderDisplayItems);
|
|
1331
|
+
}
|
|
1332
|
+
const text = result.content[0];
|
|
1333
|
+
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
699
1336
|
export function createSubagentsExtension(
|
|
700
1337
|
pi: ExtensionAPI,
|
|
701
1338
|
options: HarnessSubagentsOptions = {},
|
|
@@ -725,22 +1362,32 @@ export function createSubagentsExtension(
|
|
|
725
1362
|
],
|
|
726
1363
|
parameters: SubagentParams,
|
|
727
1364
|
|
|
728
|
-
async execute(toolCallId,
|
|
1365
|
+
async execute(toolCallId, rawParams, signal, onUpdate, ctx) {
|
|
729
1366
|
const startedAt = Date.now();
|
|
730
|
-
const
|
|
731
|
-
|
|
1367
|
+
const params = rawParams as SubagentToolParams;
|
|
1368
|
+
const agentScope: AgentScope = params.agentScope ?? defaultScope;
|
|
732
1369
|
const discovery = discoverAgents(ctx.cwd, agentScope, packageRoot);
|
|
733
1370
|
const agents = discovery.agents;
|
|
734
|
-
const confirmProjectAgents =
|
|
735
|
-
params.confirmProjectAgents ?? defaultConfirm;
|
|
736
1371
|
const defaultTimeoutMs = params.timeoutMs ?? ENV_TIMEOUT_MS;
|
|
737
|
-
|
|
738
|
-
|
|
1372
|
+
const effectiveConfirmProjectAgents =
|
|
1373
|
+
params.confirmProjectAgents ?? defaultConfirm;
|
|
1374
|
+
const harnessAgents = collectHarnessAgents(params);
|
|
1375
|
+
const makeDetails =
|
|
1376
|
+
(mode: "single" | "parallel" | "chain") =>
|
|
1377
|
+
(results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
|
|
1378
|
+
mode,
|
|
1379
|
+
agentScope,
|
|
1380
|
+
projectAgentsDir: discovery.projectAgentsDir,
|
|
1381
|
+
results,
|
|
1382
|
+
aggregator,
|
|
1383
|
+
});
|
|
1384
|
+
const resolveSpawnAuth = async (
|
|
1385
|
+
agentName: string,
|
|
1386
|
+
): Promise<SpawnAuthForward | undefined> => {
|
|
739
1387
|
if (!options.resolveSpawnAuth) return undefined;
|
|
740
1388
|
const agent = agents.find((a) => a.name === agentName);
|
|
741
1389
|
if (!agent) return undefined;
|
|
742
|
-
|
|
743
|
-
return forward;
|
|
1390
|
+
return options.resolveSpawnAuth(ctx, agent);
|
|
744
1391
|
};
|
|
745
1392
|
|
|
746
1393
|
if (options.beforeExecute) {
|
|
@@ -751,12 +1398,7 @@ export function createSubagentsExtension(
|
|
|
751
1398
|
);
|
|
752
1399
|
if (!gate.ok) {
|
|
753
1400
|
return {
|
|
754
|
-
content: [
|
|
755
|
-
{
|
|
756
|
-
type: "text",
|
|
757
|
-
text: gate.message ?? "Subagent spawn blocked by harness policy.",
|
|
758
|
-
},
|
|
759
|
-
],
|
|
1401
|
+
content: [{ type: "text" as const, text: gate.message ?? "Subagent spawn blocked by harness policy." }],
|
|
760
1402
|
details: {
|
|
761
1403
|
mode: "single",
|
|
762
1404
|
agentScope,
|
|
@@ -768,326 +1410,61 @@ export function createSubagentsExtension(
|
|
|
768
1410
|
}
|
|
769
1411
|
}
|
|
770
1412
|
|
|
771
|
-
const harnessAgents: string[] = [];
|
|
772
|
-
if (params.agent?.startsWith("harness/")) harnessAgents.push(params.agent);
|
|
773
|
-
if (params.tasks)
|
|
774
|
-
for (const t of params.tasks)
|
|
775
|
-
if (t.agent.startsWith("harness/")) harnessAgents.push(t.agent);
|
|
776
|
-
if (params.chain)
|
|
777
|
-
for (const c of params.chain)
|
|
778
|
-
if (c.agent.startsWith("harness/")) harnessAgents.push(c.agent);
|
|
779
|
-
if (params.aggregator?.agent.startsWith("harness/"))
|
|
780
|
-
harnessAgents.push(params.aggregator.agent);
|
|
781
1413
|
options.onSpawnStart?.(harnessAgents.length);
|
|
782
|
-
|
|
783
1414
|
try {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
(results: SingleResult[], aggregator?: SingleResult): SubagentDetails => ({
|
|
792
|
-
mode,
|
|
793
|
-
agentScope,
|
|
794
|
-
projectAgentsDir: discovery.projectAgentsDir,
|
|
795
|
-
results,
|
|
796
|
-
aggregator,
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
if (modeCount !== 1 || (params.aggregator && !hasTasks)) {
|
|
800
|
-
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
801
|
-
const reason =
|
|
802
|
-
modeCount !== 1
|
|
803
|
-
? "Provide exactly one mode."
|
|
804
|
-
: "Aggregator is only valid with parallel tasks.";
|
|
805
|
-
return {
|
|
806
|
-
content: [
|
|
807
|
-
{
|
|
808
|
-
type: "text",
|
|
809
|
-
text: `Invalid parameters. ${reason}\nAvailable agents: ${available}`,
|
|
810
|
-
},
|
|
811
|
-
],
|
|
812
|
-
details: makeDetails("single")([]),
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if ((agentScope === "project" || agentScope === "both") && confirmProjectAgents && ctx.hasUI) {
|
|
817
|
-
const requestedAgentNames = new Set<string>();
|
|
818
|
-
if (params.chain) for (const step of params.chain) requestedAgentNames.add(step.agent);
|
|
819
|
-
if (params.tasks) for (const t of params.tasks) requestedAgentNames.add(t.agent);
|
|
820
|
-
if (params.aggregator) requestedAgentNames.add(params.aggregator.agent);
|
|
821
|
-
if (params.agent) requestedAgentNames.add(params.agent);
|
|
822
|
-
|
|
823
|
-
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
824
|
-
.map((name) => agents.find((a) => a.name === name))
|
|
825
|
-
.filter((a): a is AgentConfig => a?.source === "project");
|
|
826
|
-
|
|
827
|
-
if (projectAgentsRequested.length > 0) {
|
|
828
|
-
const names = projectAgentsRequested.map((a) => a.name).join(", ");
|
|
829
|
-
const dir = discovery.projectAgentsDir ?? "(unknown)";
|
|
830
|
-
const ok = await ctx.ui.confirm(
|
|
831
|
-
"Run project-local agents?",
|
|
832
|
-
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
|
833
|
-
);
|
|
834
|
-
if (!ok)
|
|
835
|
-
return {
|
|
836
|
-
content: [{ type: "text", text: "Canceled: project-local agents not approved." }],
|
|
837
|
-
details: makeDetails(hasChain ? "chain" : hasTasks ? "parallel" : "single")([]),
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (params.chain && params.chain.length > 0) {
|
|
843
|
-
const results: SingleResult[] = [];
|
|
844
|
-
let previousOutput = "";
|
|
845
|
-
const status = startSubagentStatus(ctx, toolCallId, chainStatus(0, params.chain.length));
|
|
846
|
-
|
|
847
|
-
try {
|
|
848
|
-
for (let i = 0; i < params.chain.length; i++) {
|
|
849
|
-
const step = params.chain[i];
|
|
850
|
-
status.update(chainStatus(i + 1, params.chain.length, step.agent));
|
|
851
|
-
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
852
|
-
|
|
853
|
-
// Create update callback that includes all previous results
|
|
854
|
-
const chainUpdate: OnUpdateCallback | undefined = onUpdate
|
|
855
|
-
? (partial) => {
|
|
856
|
-
// Combine completed results with current streaming result
|
|
857
|
-
const currentResult = partial.details?.results[0];
|
|
858
|
-
if (currentResult) {
|
|
859
|
-
const allResults = [...results, currentResult];
|
|
860
|
-
onUpdate({
|
|
861
|
-
content: partial.content,
|
|
862
|
-
details: makeDetails("chain")(allResults),
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
: undefined;
|
|
867
|
-
|
|
868
|
-
const result = await runSingleAgent(
|
|
869
|
-
ctx.cwd,
|
|
870
|
-
agents,
|
|
871
|
-
step.agent,
|
|
872
|
-
taskWithContext,
|
|
873
|
-
step.cwd,
|
|
874
|
-
i + 1,
|
|
875
|
-
signal,
|
|
876
|
-
step.timeoutMs ?? defaultTimeoutMs,
|
|
877
|
-
chainUpdate,
|
|
878
|
-
makeDetails("chain"),
|
|
879
|
-
packageRoot,
|
|
880
|
-
await resolveSpawnAuth(step.agent),
|
|
881
|
-
options,
|
|
882
|
-
);
|
|
883
|
-
results.push(result);
|
|
884
|
-
|
|
885
|
-
const isError =
|
|
886
|
-
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
887
|
-
if (isError) {
|
|
888
|
-
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
889
|
-
return {
|
|
890
|
-
content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
|
|
891
|
-
details: makeDetails("chain")(results),
|
|
892
|
-
isError: true,
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
previousOutput = getResultFinalOutput(result);
|
|
896
|
-
}
|
|
1415
|
+
const mode = modeInfo(params);
|
|
1416
|
+
if (mode.modeCount !== 1 || (params.aggregator && !mode.hasTasks)) {
|
|
1417
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
1418
|
+
const reason =
|
|
1419
|
+
mode.modeCount !== 1
|
|
1420
|
+
? "Provide exactly one mode."
|
|
1421
|
+
: "Aggregator is only valid with parallel tasks.";
|
|
897
1422
|
return {
|
|
898
|
-
content: [{ type: "text", text:
|
|
899
|
-
details: makeDetails("
|
|
1423
|
+
content: [{ type: "text" as const, text: `Invalid parameters. ${reason}\nAvailable agents: ${available}` }],
|
|
1424
|
+
details: makeDetails("single")([]),
|
|
900
1425
|
};
|
|
901
|
-
} finally {
|
|
902
|
-
status.clear();
|
|
903
1426
|
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
if (params.tasks && params.tasks.length > 0) {
|
|
907
|
-
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
908
|
-
return {
|
|
909
|
-
content: [
|
|
910
|
-
{
|
|
911
|
-
type: "text",
|
|
912
|
-
text: `Too many parallel tasks (${params.tasks.length}). Max is ${MAX_PARALLEL_TASKS}.`,
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
details: makeDetails("parallel")([]),
|
|
916
|
-
};
|
|
917
1427
|
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
let doneCount = 0;
|
|
939
|
-
let runningCount = params.tasks.length;
|
|
940
|
-
|
|
941
|
-
const emitParallelUpdate = () => {
|
|
942
|
-
status.update(parallelStatus(doneCount, allResults.length, runningCount));
|
|
943
|
-
if (onUpdate) {
|
|
944
|
-
onUpdate({
|
|
945
|
-
content: [
|
|
946
|
-
{
|
|
947
|
-
type: "text",
|
|
948
|
-
text: `Parallel: ${doneCount}/${allResults.length} done, ${runningCount} running...`,
|
|
949
|
-
},
|
|
950
|
-
],
|
|
951
|
-
details: makeDetails("parallel")([...allResults]),
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
const results = await mapWithConcurrencyLimit(params.tasks, MAX_CONCURRENCY, async (t, index) => {
|
|
957
|
-
const result = await runSingleAgent(
|
|
958
|
-
ctx.cwd,
|
|
959
|
-
agents,
|
|
960
|
-
t.agent,
|
|
961
|
-
t.task,
|
|
962
|
-
t.cwd,
|
|
963
|
-
undefined,
|
|
964
|
-
signal,
|
|
965
|
-
t.timeoutMs ?? defaultTimeoutMs,
|
|
966
|
-
// Per-task update callback
|
|
967
|
-
(partial) => {
|
|
968
|
-
if (partial.details?.results[0]) {
|
|
969
|
-
allResults[index] = { ...partial.details.results[0], exitCode: -1 };
|
|
970
|
-
emitParallelUpdate();
|
|
971
|
-
}
|
|
972
|
-
},
|
|
973
|
-
makeDetails("parallel"),
|
|
974
|
-
packageRoot,
|
|
975
|
-
await resolveSpawnAuth(t.agent),
|
|
976
|
-
options,
|
|
977
|
-
);
|
|
978
|
-
allResults[index] = result;
|
|
979
|
-
doneCount += 1;
|
|
980
|
-
runningCount -= 1;
|
|
981
|
-
emitParallelUpdate();
|
|
982
|
-
return result;
|
|
983
|
-
});
|
|
1428
|
+
const execCtx: SubagentExecuteContext = {
|
|
1429
|
+
toolCallId,
|
|
1430
|
+
params: {
|
|
1431
|
+
...params,
|
|
1432
|
+
confirmProjectAgents: effectiveConfirmProjectAgents,
|
|
1433
|
+
},
|
|
1434
|
+
signal,
|
|
1435
|
+
onUpdate,
|
|
1436
|
+
ctx,
|
|
1437
|
+
agents,
|
|
1438
|
+
discovery,
|
|
1439
|
+
agentScope,
|
|
1440
|
+
defaultTimeoutMs,
|
|
1441
|
+
packageRoot,
|
|
1442
|
+
options,
|
|
1443
|
+
resolveSpawnAuth,
|
|
1444
|
+
makeDetails,
|
|
1445
|
+
};
|
|
984
1446
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
? aggregator.task.replace(/\{previous\}/g, fanInContext)
|
|
992
|
-
: `${aggregator.task}\n\nParallel task outputs:\n\n${fanInContext}`;
|
|
993
|
-
aggregatorResult = await runSingleAgent(
|
|
994
|
-
ctx.cwd,
|
|
995
|
-
agents,
|
|
996
|
-
aggregator.agent,
|
|
997
|
-
aggregatorTask,
|
|
998
|
-
aggregator.cwd,
|
|
999
|
-
undefined,
|
|
1000
|
-
signal,
|
|
1001
|
-
aggregator.timeoutMs ?? defaultTimeoutMs,
|
|
1002
|
-
(partial) => {
|
|
1003
|
-
status.update(fanInStatus(aggregator.agent));
|
|
1004
|
-
if (onUpdate && partial.details?.results[0]) {
|
|
1005
|
-
onUpdate({
|
|
1006
|
-
content: partial.content,
|
|
1007
|
-
details: makeDetails("parallel")(results, partial.details.results[0]),
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
},
|
|
1011
|
-
makeDetails("parallel"),
|
|
1012
|
-
packageRoot,
|
|
1013
|
-
await resolveSpawnAuth(aggregator.agent),
|
|
1014
|
-
options,
|
|
1015
|
-
);
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
1019
|
-
const summaries = results.map((r) => {
|
|
1020
|
-
const output = getResultFinalOutput(r);
|
|
1021
|
-
const error = r.errorMessage || r.stderr.trim();
|
|
1022
|
-
const summaryText = output || error;
|
|
1023
|
-
const preview = summaryText.slice(0, 160) + (summaryText.length > 160 ? "..." : "");
|
|
1024
|
-
return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
|
|
1025
|
-
});
|
|
1026
|
-
const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
|
|
1027
|
-
const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
|
|
1447
|
+
const uiMode: "single" | "parallel" | "chain" = mode.hasChain
|
|
1448
|
+
? "chain"
|
|
1449
|
+
: mode.hasTasks
|
|
1450
|
+
? "parallel"
|
|
1451
|
+
: "single";
|
|
1452
|
+
if (!(await maybeConfirmProjectAgents(execCtx, uiMode))) {
|
|
1028
1453
|
return {
|
|
1029
|
-
content: [
|
|
1030
|
-
|
|
1031
|
-
type: "text",
|
|
1032
|
-
text: aggregatorResult
|
|
1033
|
-
? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
|
|
1034
|
-
: `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
|
|
1035
|
-
},
|
|
1036
|
-
],
|
|
1037
|
-
details: makeDetails("parallel")(results, aggregatorResult),
|
|
1038
|
-
isError: aggregatorResult
|
|
1039
|
-
? aggregatorResult.exitCode !== 0 ||
|
|
1040
|
-
aggregatorResult.stopReason === "error" ||
|
|
1041
|
-
aggregatorResult.stopReason === "aborted"
|
|
1042
|
-
: undefined,
|
|
1454
|
+
content: [{ type: "text" as const, text: "Canceled: project-local agents not approved." }],
|
|
1455
|
+
details: makeDetails(uiMode)([]),
|
|
1043
1456
|
};
|
|
1044
|
-
} finally {
|
|
1045
|
-
status.clear();
|
|
1046
1457
|
}
|
|
1047
|
-
}
|
|
1048
1458
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
try {
|
|
1053
|
-
const result = await runSingleAgent(
|
|
1054
|
-
ctx.cwd,
|
|
1055
|
-
agents,
|
|
1056
|
-
params.agent,
|
|
1057
|
-
params.task,
|
|
1058
|
-
params.cwd,
|
|
1059
|
-
undefined,
|
|
1060
|
-
signal,
|
|
1061
|
-
params.timeoutMs ?? defaultTimeoutMs,
|
|
1062
|
-
onUpdate,
|
|
1063
|
-
makeDetails("single"),
|
|
1064
|
-
packageRoot,
|
|
1065
|
-
await resolveSpawnAuth(params.agent),
|
|
1066
|
-
options,
|
|
1067
|
-
);
|
|
1068
|
-
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
1069
|
-
if (isError) {
|
|
1070
|
-
const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
|
|
1071
|
-
return {
|
|
1072
|
-
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
1073
|
-
details: makeDetails("single")([result]),
|
|
1074
|
-
isError: true,
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
return {
|
|
1078
|
-
content: [{ type: "text", text: getResultFinalOutput(result) || "(no output)" }],
|
|
1079
|
-
details: makeDetails("single")([result]),
|
|
1080
|
-
};
|
|
1081
|
-
} finally {
|
|
1082
|
-
status.clear();
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1459
|
+
if (mode.hasChain) return executeChainMode(execCtx);
|
|
1460
|
+
if (mode.hasTasks) return executeParallelMode(execCtx);
|
|
1461
|
+
if (mode.hasSingle) return executeSingleMode(execCtx);
|
|
1085
1462
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1463
|
+
const available = agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
1464
|
+
return {
|
|
1465
|
+
content: [{ type: "text" as const, text: `Invalid parameters. Available agents: ${available}` }],
|
|
1466
|
+
details: makeDetails("single")([]),
|
|
1467
|
+
};
|
|
1091
1468
|
} finally {
|
|
1092
1469
|
options.onSpawnEnd?.(harnessAgents.length);
|
|
1093
1470
|
const mode = params.chain?.length
|
|
@@ -1157,329 +1534,7 @@ export function createSubagentsExtension(
|
|
|
1157
1534
|
},
|
|
1158
1535
|
|
|
1159
1536
|
renderResult(result, { expanded }, theme, _context) {
|
|
1160
|
-
|
|
1161
|
-
if (!details || details.results.length === 0) {
|
|
1162
|
-
const text = result.content[0];
|
|
1163
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
const mdTheme = getMarkdownTheme();
|
|
1167
|
-
|
|
1168
|
-
const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
|
|
1169
|
-
const toShow = limit ? items.slice(-limit) : items;
|
|
1170
|
-
const skipped = limit && items.length > limit ? items.length - limit : 0;
|
|
1171
|
-
let text = "";
|
|
1172
|
-
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
1173
|
-
for (const item of toShow) {
|
|
1174
|
-
if (item.type === "text") {
|
|
1175
|
-
const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
|
|
1176
|
-
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
1177
|
-
} else {
|
|
1178
|
-
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
return text.trimEnd();
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
if (details.mode === "single" && details.results.length === 1) {
|
|
1185
|
-
const r = details.results[0];
|
|
1186
|
-
const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
|
|
1187
|
-
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
1188
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1189
|
-
const finalOutput = getResultFinalOutput(r);
|
|
1190
|
-
|
|
1191
|
-
if (expanded) {
|
|
1192
|
-
const container = new Container();
|
|
1193
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
1194
|
-
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
1195
|
-
container.addChild(new Text(header, 0, 0));
|
|
1196
|
-
if (isError && r.errorMessage)
|
|
1197
|
-
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
1198
|
-
container.addChild(new Spacer(1));
|
|
1199
|
-
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
1200
|
-
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
1201
|
-
container.addChild(new Spacer(1));
|
|
1202
|
-
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
1203
|
-
if (displayItems.length === 0 && !finalOutput) {
|
|
1204
|
-
container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
|
|
1205
|
-
} else {
|
|
1206
|
-
for (const item of displayItems) {
|
|
1207
|
-
if (item.type === "toolCall")
|
|
1208
|
-
container.addChild(
|
|
1209
|
-
new Text(
|
|
1210
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1211
|
-
0,
|
|
1212
|
-
0,
|
|
1213
|
-
),
|
|
1214
|
-
);
|
|
1215
|
-
}
|
|
1216
|
-
if (finalOutput) {
|
|
1217
|
-
container.addChild(new Spacer(1));
|
|
1218
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1222
|
-
if (usageStr) {
|
|
1223
|
-
container.addChild(new Spacer(1));
|
|
1224
|
-
container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
|
|
1225
|
-
}
|
|
1226
|
-
return container;
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
1230
|
-
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
1231
|
-
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
1232
|
-
else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1233
|
-
else {
|
|
1234
|
-
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
1235
|
-
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1236
|
-
}
|
|
1237
|
-
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1238
|
-
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
1239
|
-
return new Text(text, 0, 0);
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
const aggregateUsage = (results: SingleResult[]) => {
|
|
1243
|
-
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
1244
|
-
for (const r of results) {
|
|
1245
|
-
total.input += r.usage.input;
|
|
1246
|
-
total.output += r.usage.output;
|
|
1247
|
-
total.cacheRead += r.usage.cacheRead;
|
|
1248
|
-
total.cacheWrite += r.usage.cacheWrite;
|
|
1249
|
-
total.cost += r.usage.cost;
|
|
1250
|
-
total.turns += r.usage.turns;
|
|
1251
|
-
}
|
|
1252
|
-
return total;
|
|
1253
|
-
};
|
|
1254
|
-
|
|
1255
|
-
if (details.mode === "chain") {
|
|
1256
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1257
|
-
const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1258
|
-
|
|
1259
|
-
if (expanded) {
|
|
1260
|
-
const container = new Container();
|
|
1261
|
-
container.addChild(
|
|
1262
|
-
new Text(
|
|
1263
|
-
icon +
|
|
1264
|
-
" " +
|
|
1265
|
-
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1266
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`),
|
|
1267
|
-
0,
|
|
1268
|
-
0,
|
|
1269
|
-
),
|
|
1270
|
-
);
|
|
1271
|
-
|
|
1272
|
-
for (const r of details.results) {
|
|
1273
|
-
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1274
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1275
|
-
const finalOutput = getResultFinalOutput(r);
|
|
1276
|
-
|
|
1277
|
-
container.addChild(new Spacer(1));
|
|
1278
|
-
container.addChild(
|
|
1279
|
-
new Text(
|
|
1280
|
-
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
1281
|
-
0,
|
|
1282
|
-
0,
|
|
1283
|
-
),
|
|
1284
|
-
);
|
|
1285
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
1286
|
-
|
|
1287
|
-
// Show tool calls
|
|
1288
|
-
for (const item of displayItems) {
|
|
1289
|
-
if (item.type === "toolCall") {
|
|
1290
|
-
container.addChild(
|
|
1291
|
-
new Text(
|
|
1292
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1293
|
-
0,
|
|
1294
|
-
0,
|
|
1295
|
-
),
|
|
1296
|
-
);
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
// Show final output as markdown
|
|
1301
|
-
if (finalOutput) {
|
|
1302
|
-
container.addChild(new Spacer(1));
|
|
1303
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
const stepUsage = formatUsageStats(r.usage, r.model);
|
|
1307
|
-
if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1311
|
-
if (usageStr) {
|
|
1312
|
-
container.addChild(new Spacer(1));
|
|
1313
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
1314
|
-
}
|
|
1315
|
-
return container;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Collapsed view
|
|
1319
|
-
let text =
|
|
1320
|
-
icon +
|
|
1321
|
-
" " +
|
|
1322
|
-
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
1323
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`);
|
|
1324
|
-
for (const r of details.results) {
|
|
1325
|
-
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1326
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1327
|
-
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1328
|
-
if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
|
|
1329
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1330
|
-
}
|
|
1331
|
-
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
1332
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1333
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1334
|
-
return new Text(text, 0, 0);
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
if (details.mode === "parallel") {
|
|
1338
|
-
const running = details.results.filter((r) => r.exitCode === -1).length;
|
|
1339
|
-
const successCount = details.results.filter((r) => r.exitCode === 0).length;
|
|
1340
|
-
const failCount = details.results.filter((r) => r.exitCode > 0).length;
|
|
1341
|
-
const aggregator = details.aggregator;
|
|
1342
|
-
const aggregatorRunning = aggregator?.exitCode === -1;
|
|
1343
|
-
const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
|
|
1344
|
-
const isRunning = running > 0 || aggregatorRunning;
|
|
1345
|
-
const icon = isRunning
|
|
1346
|
-
? theme.fg("warning", "⏳")
|
|
1347
|
-
: failCount > 0 || aggregatorFailed
|
|
1348
|
-
? theme.fg("warning", "◐")
|
|
1349
|
-
: theme.fg("success", "✓");
|
|
1350
|
-
const status = isRunning
|
|
1351
|
-
? aggregatorRunning
|
|
1352
|
-
? `${successCount + failCount}/${details.results.length} done, fan-in running`
|
|
1353
|
-
: `${successCount + failCount}/${details.results.length} done, ${running} running`
|
|
1354
|
-
: aggregator
|
|
1355
|
-
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
1356
|
-
: `${successCount}/${details.results.length} tasks`;
|
|
1357
|
-
|
|
1358
|
-
if (expanded && !isRunning) {
|
|
1359
|
-
const container = new Container();
|
|
1360
|
-
container.addChild(
|
|
1361
|
-
new Text(
|
|
1362
|
-
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
1363
|
-
0,
|
|
1364
|
-
0,
|
|
1365
|
-
),
|
|
1366
|
-
);
|
|
1367
|
-
|
|
1368
|
-
for (const r of details.results) {
|
|
1369
|
-
const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1370
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1371
|
-
const finalOutput = getResultFinalOutput(r);
|
|
1372
|
-
|
|
1373
|
-
container.addChild(new Spacer(1));
|
|
1374
|
-
container.addChild(
|
|
1375
|
-
new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
|
|
1376
|
-
);
|
|
1377
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
1378
|
-
|
|
1379
|
-
// Show tool calls
|
|
1380
|
-
for (const item of displayItems) {
|
|
1381
|
-
if (item.type === "toolCall") {
|
|
1382
|
-
container.addChild(
|
|
1383
|
-
new Text(
|
|
1384
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1385
|
-
0,
|
|
1386
|
-
0,
|
|
1387
|
-
),
|
|
1388
|
-
);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
// Show final output as markdown
|
|
1393
|
-
if (finalOutput) {
|
|
1394
|
-
container.addChild(new Spacer(1));
|
|
1395
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const taskUsage = formatUsageStats(r.usage, r.model);
|
|
1399
|
-
if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
if (aggregator) {
|
|
1403
|
-
const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1404
|
-
const displayItems = getDisplayItems(aggregator.messages);
|
|
1405
|
-
const finalOutput = getResultFinalOutput(aggregator);
|
|
1406
|
-
|
|
1407
|
-
container.addChild(new Spacer(1));
|
|
1408
|
-
container.addChild(
|
|
1409
|
-
new Text(
|
|
1410
|
-
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
|
|
1411
|
-
0,
|
|
1412
|
-
0,
|
|
1413
|
-
),
|
|
1414
|
-
);
|
|
1415
|
-
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
|
|
1416
|
-
for (const item of displayItems) {
|
|
1417
|
-
if (item.type === "toolCall") {
|
|
1418
|
-
container.addChild(
|
|
1419
|
-
new Text(
|
|
1420
|
-
theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
|
|
1421
|
-
0,
|
|
1422
|
-
0,
|
|
1423
|
-
),
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
if (finalOutput) {
|
|
1428
|
-
container.addChild(new Spacer(1));
|
|
1429
|
-
container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
|
|
1430
|
-
}
|
|
1431
|
-
const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model);
|
|
1432
|
-
if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
1436
|
-
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
1437
|
-
if (usageStr) {
|
|
1438
|
-
container.addChild(new Spacer(1));
|
|
1439
|
-
container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
|
|
1440
|
-
}
|
|
1441
|
-
return container;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
// Collapsed view (or still running)
|
|
1445
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
1446
|
-
for (const r of details.results) {
|
|
1447
|
-
const rIcon =
|
|
1448
|
-
r.exitCode === -1
|
|
1449
|
-
? theme.fg("warning", "⏳")
|
|
1450
|
-
: r.exitCode === 0
|
|
1451
|
-
? theme.fg("success", "✓")
|
|
1452
|
-
: theme.fg("error", "✗");
|
|
1453
|
-
const displayItems = getDisplayItems(r.messages);
|
|
1454
|
-
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
1455
|
-
if (displayItems.length === 0)
|
|
1456
|
-
text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1457
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1458
|
-
}
|
|
1459
|
-
if (aggregator) {
|
|
1460
|
-
const rIcon =
|
|
1461
|
-
aggregator.exitCode === -1
|
|
1462
|
-
? theme.fg("warning", "⏳")
|
|
1463
|
-
: aggregator.exitCode === 0
|
|
1464
|
-
? theme.fg("success", "✓")
|
|
1465
|
-
: theme.fg("error", "✗");
|
|
1466
|
-
const displayItems = getDisplayItems(aggregator.messages);
|
|
1467
|
-
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
1468
|
-
if (displayItems.length === 0)
|
|
1469
|
-
text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
|
|
1470
|
-
else text += `\n${renderDisplayItems(displayItems, 5)}`;
|
|
1471
|
-
}
|
|
1472
|
-
if (!isRunning) {
|
|
1473
|
-
const usageResults = aggregator ? [...details.results, aggregator] : details.results;
|
|
1474
|
-
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
1475
|
-
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
1476
|
-
}
|
|
1477
|
-
if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1478
|
-
return new Text(text, 0, 0);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
const text = result.content[0];
|
|
1482
|
-
return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
|
|
1537
|
+
return renderSubagentResult(result as AgentToolResult<SubagentDetails>, expanded, theme);
|
|
1483
1538
|
},
|
|
1484
1539
|
});
|
|
1485
1540
|
}
|