attocode 0.2.4 → 0.2.5
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/CHANGELOG.md +56 -1
- package/dist/src/adapters.d.ts +2 -1
- package/dist/src/adapters.d.ts.map +1 -1
- package/dist/src/adapters.js +60 -2
- package/dist/src/adapters.js.map +1 -1
- package/dist/src/agent/agent-builder.d.ts +117 -0
- package/dist/src/agent/agent-builder.d.ts.map +1 -0
- package/dist/src/agent/agent-builder.js +204 -0
- package/dist/src/agent/agent-builder.js.map +1 -0
- package/dist/src/agent/feature-initializer.d.ts +80 -0
- package/dist/src/agent/feature-initializer.d.ts.map +1 -0
- package/dist/src/agent/feature-initializer.js +677 -0
- package/dist/src/agent/feature-initializer.js.map +1 -0
- package/dist/src/agent/index.d.ts +13 -0
- package/dist/src/agent/index.d.ts.map +1 -0
- package/dist/src/agent/index.js +13 -0
- package/dist/src/agent/index.js.map +1 -0
- package/dist/src/agent/message-builder.d.ts +50 -0
- package/dist/src/agent/message-builder.d.ts.map +1 -0
- package/dist/src/agent/message-builder.js +173 -0
- package/dist/src/agent/message-builder.js.map +1 -0
- package/dist/src/agent/session-api.d.ts +94 -0
- package/dist/src/agent/session-api.d.ts.map +1 -0
- package/dist/src/agent/session-api.js +262 -0
- package/dist/src/agent/session-api.js.map +1 -0
- package/dist/src/agent-tools/lsp-file-tools.d.ts +1 -1
- package/dist/src/agent-tools/lsp-file-tools.d.ts.map +1 -1
- package/dist/src/agent.d.ts +14 -115
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +36 -1177
- package/dist/src/agent.js.map +1 -1
- package/dist/src/cli.js +1 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands/handler.d.ts.map +1 -1
- package/dist/src/commands/handler.js +8 -7
- package/dist/src/commands/handler.js.map +1 -1
- package/dist/src/commands/init.js +1 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/config/schema.d.ts +6 -6
- package/dist/src/core/execution-loop.d.ts.map +1 -1
- package/dist/src/core/execution-loop.js +155 -16
- package/dist/src/core/execution-loop.js.map +1 -1
- package/dist/src/core/response-handler.d.ts.map +1 -1
- package/dist/src/core/response-handler.js +3 -2
- package/dist/src/core/response-handler.js.map +1 -1
- package/dist/src/core/subagent-spawner.d.ts.map +1 -1
- package/dist/src/core/subagent-spawner.js +13 -6
- package/dist/src/core/subagent-spawner.js.map +1 -1
- package/dist/src/core/tool-executor.d.ts.map +1 -1
- package/dist/src/core/tool-executor.js +7 -2
- package/dist/src/core/tool-executor.js.map +1 -1
- package/dist/src/core/types.d.ts +1 -0
- package/dist/src/core/types.d.ts.map +1 -1
- package/dist/src/core/types.js.map +1 -1
- package/dist/src/integrations/agents/agent-registry.d.ts +262 -0
- package/dist/src/integrations/agents/agent-registry.d.ts.map +1 -0
- package/dist/src/integrations/agents/agent-registry.js +686 -0
- package/dist/src/integrations/agents/agent-registry.js.map +1 -0
- package/dist/src/integrations/agents/async-subagent.d.ts +135 -0
- package/dist/src/integrations/agents/async-subagent.d.ts.map +1 -0
- package/dist/src/integrations/agents/async-subagent.js +213 -0
- package/dist/src/integrations/agents/async-subagent.js.map +1 -0
- package/dist/src/integrations/agents/complexity-classifier.d.ts +86 -0
- package/dist/src/integrations/agents/complexity-classifier.d.ts.map +1 -0
- package/dist/src/integrations/agents/complexity-classifier.js +233 -0
- package/dist/src/integrations/agents/complexity-classifier.js.map +1 -0
- package/dist/src/integrations/agents/delegation-protocol.d.ts +86 -0
- package/dist/src/integrations/agents/delegation-protocol.d.ts.map +1 -0
- package/dist/src/integrations/agents/delegation-protocol.js +127 -0
- package/dist/src/integrations/agents/delegation-protocol.js.map +1 -0
- package/dist/src/integrations/agents/multi-agent.d.ts +150 -0
- package/dist/src/integrations/agents/multi-agent.d.ts.map +1 -0
- package/dist/src/integrations/agents/multi-agent.js +306 -0
- package/dist/src/integrations/agents/multi-agent.js.map +1 -0
- package/dist/src/integrations/agents/result-synthesizer.d.ts +389 -0
- package/dist/src/integrations/agents/result-synthesizer.d.ts.map +1 -0
- package/dist/src/integrations/agents/result-synthesizer.js +951 -0
- package/dist/src/integrations/agents/result-synthesizer.js.map +1 -0
- package/dist/src/integrations/agents/shared-blackboard.d.ts +406 -0
- package/dist/src/integrations/agents/shared-blackboard.d.ts.map +1 -0
- package/dist/src/integrations/agents/shared-blackboard.js +757 -0
- package/dist/src/integrations/agents/shared-blackboard.js.map +1 -0
- package/dist/src/integrations/agents/subagent-output-store.d.ts +91 -0
- package/dist/src/integrations/agents/subagent-output-store.d.ts.map +1 -0
- package/dist/src/integrations/agents/subagent-output-store.js +257 -0
- package/dist/src/integrations/agents/subagent-output-store.js.map +1 -0
- package/dist/src/integrations/budget/budget-pool.d.ts +115 -0
- package/dist/src/integrations/budget/budget-pool.d.ts.map +1 -0
- package/dist/src/integrations/budget/budget-pool.js +205 -0
- package/dist/src/integrations/budget/budget-pool.js.map +1 -0
- package/dist/src/integrations/budget/cancellation.d.ts +229 -0
- package/dist/src/integrations/budget/cancellation.d.ts.map +1 -0
- package/dist/src/integrations/budget/cancellation.js +520 -0
- package/dist/src/integrations/budget/cancellation.js.map +1 -0
- package/dist/src/integrations/budget/dynamic-budget.d.ts +81 -0
- package/dist/src/integrations/budget/dynamic-budget.d.ts.map +1 -0
- package/dist/src/integrations/budget/dynamic-budget.js +151 -0
- package/dist/src/integrations/budget/dynamic-budget.js.map +1 -0
- package/dist/src/integrations/budget/economics.d.ts +435 -0
- package/dist/src/integrations/budget/economics.d.ts.map +1 -0
- package/dist/src/integrations/budget/economics.js +1007 -0
- package/dist/src/integrations/budget/economics.js.map +1 -0
- package/dist/src/integrations/budget/injection-budget.d.ts +71 -0
- package/dist/src/integrations/budget/injection-budget.d.ts.map +1 -0
- package/dist/src/integrations/budget/injection-budget.js +137 -0
- package/dist/src/integrations/budget/injection-budget.js.map +1 -0
- package/dist/src/integrations/budget/loop-detector.d.ts +105 -0
- package/dist/src/integrations/budget/loop-detector.d.ts.map +1 -0
- package/dist/src/integrations/budget/loop-detector.js +287 -0
- package/dist/src/integrations/budget/loop-detector.js.map +1 -0
- package/dist/src/integrations/budget/phase-tracker.d.ts +114 -0
- package/dist/src/integrations/budget/phase-tracker.d.ts.map +1 -0
- package/dist/src/integrations/budget/phase-tracker.js +262 -0
- package/dist/src/integrations/budget/phase-tracker.js.map +1 -0
- package/dist/src/integrations/budget/resources.d.ts +182 -0
- package/dist/src/integrations/budget/resources.d.ts.map +1 -0
- package/dist/src/integrations/budget/resources.js +318 -0
- package/dist/src/integrations/budget/resources.js.map +1 -0
- package/dist/src/integrations/context/auto-compaction.d.ts +210 -0
- package/dist/src/integrations/context/auto-compaction.d.ts.map +1 -0
- package/dist/src/integrations/context/auto-compaction.js +477 -0
- package/dist/src/integrations/context/auto-compaction.js.map +1 -0
- package/dist/src/integrations/context/code-analyzer.d.ts +71 -0
- package/dist/src/integrations/context/code-analyzer.d.ts.map +1 -0
- package/dist/src/integrations/context/code-analyzer.js +448 -0
- package/dist/src/integrations/context/code-analyzer.js.map +1 -0
- package/dist/src/integrations/context/code-selector.d.ts +78 -0
- package/dist/src/integrations/context/code-selector.d.ts.map +1 -0
- package/dist/src/integrations/context/code-selector.js +649 -0
- package/dist/src/integrations/context/code-selector.js.map +1 -0
- package/dist/src/integrations/context/codebase-ast.d.ts +138 -0
- package/dist/src/integrations/context/codebase-ast.d.ts.map +1 -0
- package/dist/src/integrations/context/codebase-ast.js +818 -0
- package/dist/src/integrations/context/codebase-ast.js.map +1 -0
- package/dist/src/integrations/context/codebase-context.d.ts +473 -0
- package/dist/src/integrations/context/codebase-context.d.ts.map +1 -0
- package/dist/src/integrations/context/codebase-context.js +685 -0
- package/dist/src/integrations/context/codebase-context.js.map +1 -0
- package/dist/src/integrations/context/compaction.d.ts +191 -0
- package/dist/src/integrations/context/compaction.d.ts.map +1 -0
- package/dist/src/integrations/context/compaction.js +384 -0
- package/dist/src/integrations/context/compaction.js.map +1 -0
- package/dist/src/integrations/context/context-engineering.d.ts +274 -0
- package/dist/src/integrations/context/context-engineering.d.ts.map +1 -0
- package/dist/src/integrations/context/context-engineering.js +437 -0
- package/dist/src/integrations/context/context-engineering.js.map +1 -0
- package/dist/src/integrations/context/file-cache.d.ts +97 -0
- package/dist/src/integrations/context/file-cache.d.ts.map +1 -0
- package/dist/src/integrations/context/file-cache.js +218 -0
- package/dist/src/integrations/context/file-cache.js.map +1 -0
- package/dist/src/integrations/context/semantic-cache.d.ts +178 -0
- package/dist/src/integrations/context/semantic-cache.d.ts.map +1 -0
- package/dist/src/integrations/context/semantic-cache.js +372 -0
- package/dist/src/integrations/context/semantic-cache.js.map +1 -0
- package/dist/src/integrations/index.d.ts +72 -68
- package/dist/src/integrations/index.d.ts.map +1 -1
- package/dist/src/integrations/index.js +76 -68
- package/dist/src/integrations/index.js.map +1 -1
- package/dist/src/integrations/lsp/lsp.d.ts +196 -0
- package/dist/src/integrations/lsp/lsp.d.ts.map +1 -0
- package/dist/src/integrations/lsp/lsp.js +583 -0
- package/dist/src/integrations/lsp/lsp.js.map +1 -0
- package/dist/src/integrations/mcp/mcp-client.d.ts +279 -0
- package/dist/src/integrations/mcp/mcp-client.d.ts.map +1 -0
- package/dist/src/integrations/mcp/mcp-client.js +755 -0
- package/dist/src/integrations/mcp/mcp-client.js.map +1 -0
- package/dist/src/integrations/mcp/mcp-custom-tools.d.ts +102 -0
- package/dist/src/integrations/mcp/mcp-custom-tools.d.ts.map +1 -0
- package/dist/src/integrations/mcp/mcp-custom-tools.js +232 -0
- package/dist/src/integrations/mcp/mcp-custom-tools.js.map +1 -0
- package/dist/src/integrations/mcp/mcp-tool-search.d.ts +77 -0
- package/dist/src/integrations/mcp/mcp-tool-search.d.ts.map +1 -0
- package/dist/src/integrations/mcp/mcp-tool-search.js +220 -0
- package/dist/src/integrations/mcp/mcp-tool-search.js.map +1 -0
- package/dist/src/integrations/mcp/mcp-tool-validator.d.ts +60 -0
- package/dist/src/integrations/mcp/mcp-tool-validator.d.ts.map +1 -0
- package/dist/src/integrations/mcp/mcp-tool-validator.js +141 -0
- package/dist/src/integrations/mcp/mcp-tool-validator.js.map +1 -0
- package/dist/src/integrations/persistence/codebase-repository.d.ts +45 -0
- package/dist/src/integrations/persistence/codebase-repository.d.ts.map +1 -0
- package/dist/src/integrations/persistence/codebase-repository.js +81 -0
- package/dist/src/integrations/persistence/codebase-repository.js.map +1 -0
- package/dist/src/integrations/persistence/goal-repository.d.ts +71 -0
- package/dist/src/integrations/persistence/goal-repository.d.ts.map +1 -0
- package/dist/src/integrations/persistence/goal-repository.js +184 -0
- package/dist/src/integrations/persistence/goal-repository.js.map +1 -0
- package/dist/src/integrations/persistence/history.d.ts +72 -0
- package/dist/src/integrations/persistence/history.d.ts.map +1 -0
- package/dist/src/integrations/persistence/history.js +165 -0
- package/dist/src/integrations/persistence/history.js.map +1 -0
- package/dist/src/integrations/persistence/persistence.d.ts +49 -0
- package/dist/src/integrations/persistence/persistence.d.ts.map +1 -0
- package/dist/src/integrations/persistence/persistence.js +197 -0
- package/dist/src/integrations/persistence/persistence.js.map +1 -0
- package/dist/src/integrations/persistence/session-repository.d.ts +212 -0
- package/dist/src/integrations/persistence/session-repository.d.ts.map +1 -0
- package/dist/src/integrations/persistence/session-repository.js +770 -0
- package/dist/src/integrations/persistence/session-repository.js.map +1 -0
- package/dist/src/integrations/persistence/session-store.d.ts +184 -0
- package/dist/src/integrations/persistence/session-store.d.ts.map +1 -0
- package/dist/src/integrations/persistence/session-store.js +346 -0
- package/dist/src/integrations/persistence/session-store.js.map +1 -0
- package/dist/src/integrations/persistence/sqlite-store.d.ts +453 -0
- package/dist/src/integrations/persistence/sqlite-store.d.ts.map +1 -0
- package/dist/src/integrations/persistence/sqlite-store.js +676 -0
- package/dist/src/integrations/persistence/sqlite-store.js.map +1 -0
- package/dist/src/integrations/persistence/worker-repository.d.ts +65 -0
- package/dist/src/integrations/persistence/worker-repository.d.ts.map +1 -0
- package/dist/src/integrations/persistence/worker-repository.js +183 -0
- package/dist/src/integrations/persistence/worker-repository.js.map +1 -0
- package/dist/src/integrations/quality/auto-checkpoint.d.ts +98 -0
- package/dist/src/integrations/quality/auto-checkpoint.d.ts.map +1 -0
- package/dist/src/integrations/quality/auto-checkpoint.js +252 -0
- package/dist/src/integrations/quality/auto-checkpoint.js.map +1 -0
- package/dist/src/integrations/quality/dead-letter-queue.d.ts +233 -0
- package/dist/src/integrations/quality/dead-letter-queue.d.ts.map +1 -0
- package/dist/src/integrations/quality/dead-letter-queue.js +543 -0
- package/dist/src/integrations/quality/dead-letter-queue.js.map +1 -0
- package/dist/src/integrations/quality/health-check.d.ts +218 -0
- package/dist/src/integrations/quality/health-check.d.ts.map +1 -0
- package/dist/src/integrations/quality/health-check.js +415 -0
- package/dist/src/integrations/quality/health-check.js.map +1 -0
- package/dist/src/integrations/quality/learning-store.d.ts +291 -0
- package/dist/src/integrations/quality/learning-store.d.ts.map +1 -0
- package/dist/src/integrations/quality/learning-store.js +646 -0
- package/dist/src/integrations/quality/learning-store.js.map +1 -0
- package/dist/src/integrations/quality/self-improvement.d.ts +90 -0
- package/dist/src/integrations/quality/self-improvement.d.ts.map +1 -0
- package/dist/src/integrations/quality/self-improvement.js +229 -0
- package/dist/src/integrations/quality/self-improvement.js.map +1 -0
- package/dist/src/integrations/quality/tool-recommendation.d.ts +61 -0
- package/dist/src/integrations/quality/tool-recommendation.d.ts.map +1 -0
- package/dist/src/integrations/quality/tool-recommendation.js +268 -0
- package/dist/src/integrations/quality/tool-recommendation.js.map +1 -0
- package/dist/src/integrations/safety/bash-policy.d.ts +33 -0
- package/dist/src/integrations/safety/bash-policy.d.ts.map +1 -0
- package/dist/src/integrations/safety/bash-policy.js +144 -0
- package/dist/src/integrations/safety/bash-policy.js.map +1 -0
- package/dist/src/integrations/safety/edit-validator.d.ts +30 -0
- package/dist/src/integrations/safety/edit-validator.d.ts.map +1 -0
- package/dist/src/integrations/safety/edit-validator.js +87 -0
- package/dist/src/integrations/safety/edit-validator.js.map +1 -0
- package/dist/src/integrations/safety/execution-policy.d.ts +189 -0
- package/dist/src/integrations/safety/execution-policy.d.ts.map +1 -0
- package/dist/src/integrations/safety/execution-policy.js +352 -0
- package/dist/src/integrations/safety/execution-policy.js.map +1 -0
- package/dist/src/integrations/safety/policy-engine.d.ts +55 -0
- package/dist/src/integrations/safety/policy-engine.d.ts.map +1 -0
- package/dist/src/integrations/safety/policy-engine.js +247 -0
- package/dist/src/integrations/safety/policy-engine.js.map +1 -0
- package/dist/src/integrations/safety/safety.d.ts +174 -0
- package/dist/src/integrations/safety/safety.d.ts.map +1 -0
- package/dist/src/integrations/safety/safety.js +470 -0
- package/dist/src/integrations/safety/safety.js.map +1 -0
- package/dist/src/integrations/safety/sandbox/basic.d.ts +81 -0
- package/dist/src/integrations/safety/sandbox/basic.d.ts.map +1 -0
- package/dist/src/integrations/safety/sandbox/basic.js +335 -0
- package/dist/src/integrations/safety/sandbox/basic.js.map +1 -0
- package/dist/src/integrations/safety/sandbox/docker.d.ts +94 -0
- package/dist/src/integrations/safety/sandbox/docker.d.ts.map +1 -0
- package/dist/src/integrations/safety/sandbox/docker.js +294 -0
- package/dist/src/integrations/safety/sandbox/docker.js.map +1 -0
- package/dist/src/integrations/safety/sandbox/index.d.ts +188 -0
- package/dist/src/integrations/safety/sandbox/index.d.ts.map +1 -0
- package/dist/src/integrations/safety/sandbox/index.js +386 -0
- package/dist/src/integrations/safety/sandbox/index.js.map +1 -0
- package/dist/src/integrations/safety/sandbox/landlock.d.ts +59 -0
- package/dist/src/integrations/safety/sandbox/landlock.d.ts.map +1 -0
- package/dist/src/integrations/safety/sandbox/landlock.js +329 -0
- package/dist/src/integrations/safety/sandbox/landlock.js.map +1 -0
- package/dist/src/integrations/safety/sandbox/seatbelt.d.ts +68 -0
- package/dist/src/integrations/safety/sandbox/seatbelt.d.ts.map +1 -0
- package/dist/src/integrations/safety/sandbox/seatbelt.js +298 -0
- package/dist/src/integrations/safety/sandbox/seatbelt.js.map +1 -0
- package/dist/src/integrations/safety/type-checker.d.ts +53 -0
- package/dist/src/integrations/safety/type-checker.d.ts.map +1 -0
- package/dist/src/integrations/safety/type-checker.js +142 -0
- package/dist/src/integrations/safety/type-checker.js.map +1 -0
- package/dist/src/integrations/skills/skill-executor.d.ts +113 -0
- package/dist/src/integrations/skills/skill-executor.d.ts.map +1 -0
- package/dist/src/integrations/skills/skill-executor.js +270 -0
- package/dist/src/integrations/skills/skill-executor.js.map +1 -0
- package/dist/src/integrations/skills/skills.d.ts +262 -0
- package/dist/src/integrations/skills/skills.d.ts.map +1 -0
- package/dist/src/integrations/skills/skills.js +602 -0
- package/dist/src/integrations/skills/skills.js.map +1 -0
- package/dist/src/integrations/streaming/pty-shell.d.ts +169 -0
- package/dist/src/integrations/streaming/pty-shell.d.ts.map +1 -0
- package/dist/src/integrations/streaming/pty-shell.js +367 -0
- package/dist/src/integrations/streaming/pty-shell.js.map +1 -0
- package/dist/src/integrations/streaming/streaming.d.ts +102 -0
- package/dist/src/integrations/streaming/streaming.d.ts.map +1 -0
- package/dist/src/integrations/streaming/streaming.js +362 -0
- package/dist/src/integrations/streaming/streaming.js.map +1 -0
- package/dist/src/integrations/swarm/index.d.ts +2 -1
- package/dist/src/integrations/swarm/index.d.ts.map +1 -1
- package/dist/src/integrations/swarm/index.js +2 -0
- package/dist/src/integrations/swarm/index.js.map +1 -1
- package/dist/src/integrations/swarm/model-selector.js +1 -1
- package/dist/src/integrations/swarm/model-selector.js.map +1 -1
- package/dist/src/integrations/swarm/swarm-budget.d.ts +1 -1
- package/dist/src/integrations/swarm/swarm-budget.d.ts.map +1 -1
- package/dist/src/integrations/swarm/swarm-budget.js +1 -1
- package/dist/src/integrations/swarm/swarm-budget.js.map +1 -1
- package/dist/src/integrations/swarm/swarm-config-loader.d.ts.map +1 -1
- package/dist/src/integrations/swarm/swarm-config-loader.js +7 -0
- package/dist/src/integrations/swarm/swarm-config-loader.js.map +1 -1
- package/dist/src/integrations/swarm/swarm-events.d.ts +1 -1
- package/dist/src/integrations/swarm/swarm-events.d.ts.map +1 -1
- package/dist/src/integrations/swarm/swarm-execution.d.ts +27 -0
- package/dist/src/integrations/swarm/swarm-execution.d.ts.map +1 -0
- package/dist/src/integrations/swarm/swarm-execution.js +1021 -0
- package/dist/src/integrations/swarm/swarm-execution.js.map +1 -0
- package/dist/src/integrations/swarm/swarm-helpers.d.ts +26 -0
- package/dist/src/integrations/swarm/swarm-helpers.d.ts.map +1 -0
- package/dist/src/integrations/swarm/swarm-helpers.js +95 -0
- package/dist/src/integrations/swarm/swarm-helpers.js.map +1 -0
- package/dist/src/integrations/swarm/swarm-lifecycle.d.ts +100 -0
- package/dist/src/integrations/swarm/swarm-lifecycle.d.ts.map +1 -0
- package/dist/src/integrations/swarm/swarm-lifecycle.js +922 -0
- package/dist/src/integrations/swarm/swarm-lifecycle.js.map +1 -0
- package/dist/src/integrations/swarm/swarm-orchestrator.d.ts +84 -203
- package/dist/src/integrations/swarm/swarm-orchestrator.d.ts.map +1 -1
- package/dist/src/integrations/swarm/swarm-orchestrator.js +251 -2870
- package/dist/src/integrations/swarm/swarm-orchestrator.js.map +1 -1
- package/dist/src/integrations/swarm/swarm-quality-gate.js +1 -1
- package/dist/src/integrations/swarm/swarm-quality-gate.js.map +1 -1
- package/dist/src/integrations/swarm/swarm-recovery.d.ts +75 -0
- package/dist/src/integrations/swarm/swarm-recovery.d.ts.map +1 -0
- package/dist/src/integrations/swarm/swarm-recovery.js +550 -0
- package/dist/src/integrations/swarm/swarm-recovery.js.map +1 -0
- package/dist/src/integrations/swarm/swarm-state-store.d.ts.map +1 -1
- package/dist/src/integrations/swarm/swarm-state-store.js +6 -0
- package/dist/src/integrations/swarm/swarm-state-store.js.map +1 -1
- package/dist/src/integrations/swarm/task-queue.d.ts +1 -1
- package/dist/src/integrations/swarm/task-queue.d.ts.map +1 -1
- package/dist/src/integrations/swarm/task-queue.js +28 -1
- package/dist/src/integrations/swarm/task-queue.js.map +1 -1
- package/dist/src/integrations/swarm/types.d.ts +17 -5
- package/dist/src/integrations/swarm/types.d.ts.map +1 -1
- package/dist/src/integrations/swarm/types.js.map +1 -1
- package/dist/src/integrations/swarm/worker-pool.d.ts +1 -1
- package/dist/src/integrations/swarm/worker-pool.d.ts.map +1 -1
- package/dist/src/integrations/swarm/worker-pool.js +13 -9
- package/dist/src/integrations/swarm/worker-pool.js.map +1 -1
- package/dist/src/integrations/tasks/dependency-analyzer.d.ts +34 -0
- package/dist/src/integrations/tasks/dependency-analyzer.d.ts.map +1 -0
- package/dist/src/integrations/tasks/dependency-analyzer.js +232 -0
- package/dist/src/integrations/tasks/dependency-analyzer.js.map +1 -0
- package/dist/src/integrations/tasks/interactive-planning.d.ts +322 -0
- package/dist/src/integrations/tasks/interactive-planning.d.ts.map +1 -0
- package/dist/src/integrations/tasks/interactive-planning.js +655 -0
- package/dist/src/integrations/tasks/interactive-planning.js.map +1 -0
- package/dist/src/integrations/tasks/pending-plan.d.ts +196 -0
- package/dist/src/integrations/tasks/pending-plan.d.ts.map +1 -0
- package/dist/src/integrations/tasks/pending-plan.js +431 -0
- package/dist/src/integrations/tasks/pending-plan.js.map +1 -0
- package/dist/src/integrations/tasks/planning.d.ts +115 -0
- package/dist/src/integrations/tasks/planning.d.ts.map +1 -0
- package/dist/src/integrations/tasks/planning.js +413 -0
- package/dist/src/integrations/tasks/planning.js.map +1 -0
- package/dist/src/integrations/tasks/smart-decomposer.d.ts +316 -0
- package/dist/src/integrations/tasks/smart-decomposer.d.ts.map +1 -0
- package/dist/src/integrations/tasks/smart-decomposer.js +661 -0
- package/dist/src/integrations/tasks/smart-decomposer.js.map +1 -0
- package/dist/src/integrations/tasks/task-manager.d.ts +164 -0
- package/dist/src/integrations/tasks/task-manager.d.ts.map +1 -0
- package/dist/src/integrations/tasks/task-manager.js +383 -0
- package/dist/src/integrations/tasks/task-manager.js.map +1 -0
- package/dist/src/integrations/tasks/task-splitter.d.ts +56 -0
- package/dist/src/integrations/tasks/task-splitter.d.ts.map +1 -0
- package/dist/src/integrations/tasks/task-splitter.js +537 -0
- package/dist/src/integrations/tasks/task-splitter.js.map +1 -0
- package/dist/src/integrations/tasks/verification-gate.d.ts +103 -0
- package/dist/src/integrations/tasks/verification-gate.d.ts.map +1 -0
- package/dist/src/integrations/tasks/verification-gate.js +193 -0
- package/dist/src/integrations/tasks/verification-gate.js.map +1 -0
- package/dist/src/integrations/tasks/work-log.d.ts +87 -0
- package/dist/src/integrations/tasks/work-log.d.ts.map +1 -0
- package/dist/src/integrations/tasks/work-log.js +275 -0
- package/dist/src/integrations/tasks/work-log.js.map +1 -0
- package/dist/src/integrations/utilities/capabilities.d.ts +160 -0
- package/dist/src/integrations/utilities/capabilities.d.ts.map +1 -0
- package/dist/src/integrations/utilities/capabilities.js +426 -0
- package/dist/src/integrations/utilities/capabilities.js.map +1 -0
- package/dist/src/integrations/utilities/diff-utils.d.ts +105 -0
- package/dist/src/integrations/utilities/diff-utils.d.ts.map +1 -0
- package/dist/src/integrations/utilities/diff-utils.js +497 -0
- package/dist/src/integrations/utilities/diff-utils.js.map +1 -0
- package/dist/src/integrations/utilities/environment-facts.d.ts +52 -0
- package/dist/src/integrations/utilities/environment-facts.d.ts.map +1 -0
- package/dist/src/integrations/utilities/environment-facts.js +84 -0
- package/dist/src/integrations/utilities/environment-facts.js.map +1 -0
- package/dist/src/integrations/utilities/file-change-tracker.d.ts +162 -0
- package/dist/src/integrations/utilities/file-change-tracker.d.ts.map +1 -0
- package/dist/src/integrations/utilities/file-change-tracker.js +538 -0
- package/dist/src/integrations/utilities/file-change-tracker.js.map +1 -0
- package/dist/src/integrations/utilities/graph-visualization.d.ts +72 -0
- package/dist/src/integrations/utilities/graph-visualization.d.ts.map +1 -0
- package/dist/src/integrations/utilities/graph-visualization.js +383 -0
- package/dist/src/integrations/utilities/graph-visualization.js.map +1 -0
- package/dist/src/integrations/utilities/hierarchical-config.d.ts +215 -0
- package/dist/src/integrations/utilities/hierarchical-config.d.ts.map +1 -0
- package/dist/src/integrations/utilities/hierarchical-config.js +504 -0
- package/dist/src/integrations/utilities/hierarchical-config.js.map +1 -0
- package/dist/src/integrations/utilities/hooks.d.ts +116 -0
- package/dist/src/integrations/utilities/hooks.d.ts.map +1 -0
- package/dist/src/integrations/utilities/hooks.js +410 -0
- package/dist/src/integrations/utilities/hooks.js.map +1 -0
- package/dist/src/integrations/utilities/ignore.d.ts +143 -0
- package/dist/src/integrations/utilities/ignore.d.ts.map +1 -0
- package/dist/src/integrations/utilities/ignore.js +417 -0
- package/dist/src/integrations/utilities/ignore.js.map +1 -0
- package/dist/src/integrations/utilities/image-renderer.d.ts +119 -0
- package/dist/src/integrations/utilities/image-renderer.d.ts.map +1 -0
- package/dist/src/integrations/utilities/image-renderer.js +306 -0
- package/dist/src/integrations/utilities/image-renderer.js.map +1 -0
- package/dist/src/integrations/utilities/logger.d.ts +104 -0
- package/dist/src/integrations/utilities/logger.d.ts.map +1 -0
- package/dist/src/integrations/utilities/logger.js +219 -0
- package/dist/src/integrations/utilities/logger.js.map +1 -0
- package/dist/src/integrations/utilities/memory.d.ts +116 -0
- package/dist/src/integrations/utilities/memory.d.ts.map +1 -0
- package/dist/src/integrations/utilities/memory.js +311 -0
- package/dist/src/integrations/utilities/memory.js.map +1 -0
- package/dist/src/integrations/utilities/observability.d.ts +162 -0
- package/dist/src/integrations/utilities/observability.d.ts.map +1 -0
- package/dist/src/integrations/utilities/observability.js +407 -0
- package/dist/src/integrations/utilities/observability.js.map +1 -0
- package/dist/src/integrations/utilities/openrouter-pricing.d.ts +67 -0
- package/dist/src/integrations/utilities/openrouter-pricing.d.ts.map +1 -0
- package/dist/src/integrations/utilities/openrouter-pricing.js +166 -0
- package/dist/src/integrations/utilities/openrouter-pricing.js.map +1 -0
- package/dist/src/integrations/utilities/react.d.ts +139 -0
- package/dist/src/integrations/utilities/react.d.ts.map +1 -0
- package/dist/src/integrations/utilities/react.js +273 -0
- package/dist/src/integrations/utilities/react.js.map +1 -0
- package/dist/src/integrations/utilities/retry.d.ts +132 -0
- package/dist/src/integrations/utilities/retry.d.ts.map +1 -0
- package/dist/src/integrations/utilities/retry.js +233 -0
- package/dist/src/integrations/utilities/retry.js.map +1 -0
- package/dist/src/integrations/utilities/routing.d.ts +118 -0
- package/dist/src/integrations/utilities/routing.d.ts.map +1 -0
- package/dist/src/integrations/utilities/routing.js +348 -0
- package/dist/src/integrations/utilities/routing.js.map +1 -0
- package/dist/src/integrations/utilities/rules.d.ts +131 -0
- package/dist/src/integrations/utilities/rules.d.ts.map +1 -0
- package/dist/src/integrations/utilities/rules.js +284 -0
- package/dist/src/integrations/utilities/rules.js.map +1 -0
- package/dist/src/integrations/utilities/sourcegraph.d.ts +169 -0
- package/dist/src/integrations/utilities/sourcegraph.d.ts.map +1 -0
- package/dist/src/integrations/utilities/sourcegraph.js +379 -0
- package/dist/src/integrations/utilities/sourcegraph.js.map +1 -0
- package/dist/src/integrations/utilities/thinking-strategy.d.ts +52 -0
- package/dist/src/integrations/utilities/thinking-strategy.d.ts.map +1 -0
- package/dist/src/integrations/utilities/thinking-strategy.js +129 -0
- package/dist/src/integrations/utilities/thinking-strategy.js.map +1 -0
- package/dist/src/integrations/utilities/thread-manager.d.ts +199 -0
- package/dist/src/integrations/utilities/thread-manager.d.ts.map +1 -0
- package/dist/src/integrations/utilities/thread-manager.js +357 -0
- package/dist/src/integrations/utilities/thread-manager.js.map +1 -0
- package/dist/src/integrations/utilities/token-estimate.d.ts +11 -0
- package/dist/src/integrations/utilities/token-estimate.d.ts.map +1 -0
- package/dist/src/integrations/utilities/token-estimate.js +14 -0
- package/dist/src/integrations/utilities/token-estimate.js.map +1 -0
- package/dist/src/main.js +10 -4
- package/dist/src/main.js.map +1 -1
- package/dist/src/modes/repl.d.ts.map +1 -1
- package/dist/src/modes/repl.js +22 -5
- package/dist/src/modes/repl.js.map +1 -1
- package/dist/src/modes/tui.d.ts.map +1 -1
- package/dist/src/modes/tui.js +23 -6
- package/dist/src/modes/tui.js.map +1 -1
- package/dist/src/modes.js +1 -1
- package/dist/src/modes.js.map +1 -1
- package/dist/src/observability/tracer.js +1 -1
- package/dist/src/observability/tracer.js.map +1 -1
- package/dist/src/persistence/schema.d.ts +2 -0
- package/dist/src/persistence/schema.d.ts.map +1 -1
- package/dist/src/persistence/schema.js +31 -0
- package/dist/src/persistence/schema.js.map +1 -1
- package/dist/src/providers/adapters/anthropic.d.ts +6 -0
- package/dist/src/providers/adapters/anthropic.d.ts.map +1 -1
- package/dist/src/providers/adapters/anthropic.js +99 -15
- package/dist/src/providers/adapters/anthropic.js.map +1 -1
- package/dist/src/providers/adapters/azure.d.ts +74 -0
- package/dist/src/providers/adapters/azure.d.ts.map +1 -0
- package/dist/src/providers/adapters/azure.js +354 -0
- package/dist/src/providers/adapters/azure.js.map +1 -0
- package/dist/src/providers/adapters/mock.d.ts +16 -2
- package/dist/src/providers/adapters/mock.d.ts.map +1 -1
- package/dist/src/providers/adapters/mock.js +44 -3
- package/dist/src/providers/adapters/mock.js.map +1 -1
- package/dist/src/providers/adapters/openai.d.ts +6 -1
- package/dist/src/providers/adapters/openai.d.ts.map +1 -1
- package/dist/src/providers/adapters/openai.js +39 -8
- package/dist/src/providers/adapters/openai.js.map +1 -1
- package/dist/src/providers/adapters/openrouter.d.ts +6 -0
- package/dist/src/providers/adapters/openrouter.d.ts.map +1 -1
- package/dist/src/providers/adapters/openrouter.js +73 -3
- package/dist/src/providers/adapters/openrouter.js.map +1 -1
- package/dist/src/providers/provider.js +1 -1
- package/dist/src/providers/provider.js.map +1 -1
- package/dist/src/providers/resilient-provider.js +1 -1
- package/dist/src/providers/resilient-provider.js.map +1 -1
- package/dist/src/providers/types.d.ts +23 -2
- package/dist/src/providers/types.d.ts.map +1 -1
- package/dist/src/session-picker.d.ts +1 -1
- package/dist/src/session-picker.d.ts.map +1 -1
- package/dist/src/session-picker.js +1 -1
- package/dist/src/session-picker.js.map +1 -1
- package/dist/src/shared/budget-tracker.js +1 -1
- package/dist/src/shared/budget-tracker.js.map +1 -1
- package/dist/src/tools/agent.d.ts +1 -1
- package/dist/src/tools/agent.d.ts.map +1 -1
- package/dist/src/tools/bash.js +1 -1
- package/dist/src/tools/bash.js.map +1 -1
- package/dist/src/tools/file.js +1 -1
- package/dist/src/tools/file.js.map +1 -1
- package/dist/src/tools/permission.js +2 -2
- package/dist/src/tools/permission.js.map +1 -1
- package/dist/src/tools/registry.d.ts +1 -1
- package/dist/src/tools/registry.d.ts.map +1 -1
- package/dist/src/tools/registry.js +1 -1
- package/dist/src/tools/registry.js.map +1 -1
- package/dist/src/tools/tasks.d.ts +1 -1
- package/dist/src/tools/tasks.d.ts.map +1 -1
- package/dist/src/tools/undo.d.ts +1 -1
- package/dist/src/tools/undo.d.ts.map +1 -1
- package/dist/src/tracing/cache-boundary-tracker.d.ts.map +1 -1
- package/dist/src/tracing/cache-boundary-tracker.js +2 -2
- package/dist/src/tracing/cache-boundary-tracker.js.map +1 -1
- package/dist/src/tracing/trace-collector.d.ts +22 -0
- package/dist/src/tracing/trace-collector.d.ts.map +1 -1
- package/dist/src/tracing/trace-collector.js +27 -3
- package/dist/src/tracing/trace-collector.js.map +1 -1
- package/dist/src/tracing/types.d.ts +17 -1
- package/dist/src/tracing/types.d.ts.map +1 -1
- package/dist/src/tracing/types.js.map +1 -1
- package/dist/src/tricks/failure-evidence.js +1 -1
- package/dist/src/tricks/failure-evidence.js.map +1 -1
- package/dist/src/tricks/recitation.d.ts.map +1 -1
- package/dist/src/tricks/recitation.js +2 -1
- package/dist/src/tricks/recitation.js.map +1 -1
- package/dist/src/tricks/recursive-context.d.ts.map +1 -1
- package/dist/src/tricks/recursive-context.js +2 -2
- package/dist/src/tricks/recursive-context.js.map +1 -1
- package/dist/src/tricks/reversible-compaction.d.ts.map +1 -1
- package/dist/src/tricks/reversible-compaction.js +6 -2
- package/dist/src/tricks/reversible-compaction.js.map +1 -1
- package/dist/src/tui/app.d.ts +3 -3
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +86 -14
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/components/CollapsibleDiffView.d.ts +1 -1
- package/dist/src/tui/components/CollapsibleDiffView.d.ts.map +1 -1
- package/dist/src/tui/components/DiagnosticsPanel.d.ts +24 -0
- package/dist/src/tui/components/DiagnosticsPanel.d.ts.map +1 -0
- package/dist/src/tui/components/DiagnosticsPanel.js +47 -0
- package/dist/src/tui/components/DiagnosticsPanel.js.map +1 -0
- package/dist/src/tui/components/DiffView.d.ts +1 -1
- package/dist/src/tui/components/DiffView.d.ts.map +1 -1
- package/dist/src/tui/components/ErrorBoundary.js +1 -1
- package/dist/src/tui/components/ErrorBoundary.js.map +1 -1
- package/dist/src/tui/components/TasksPanel.d.ts +1 -1
- package/dist/src/tui/components/TasksPanel.d.ts.map +1 -1
- package/dist/src/tui/event-display.js +1 -1
- package/dist/src/tui/event-display.js.map +1 -1
- package/dist/src/tui/index.js +1 -1
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/transparency-aggregator.d.ts +13 -0
- package/dist/src/tui/transparency-aggregator.d.ts.map +1 -1
- package/dist/src/tui/transparency-aggregator.js +21 -0
- package/dist/src/tui/transparency-aggregator.js.map +1 -1
- package/dist/src/types.d.ts +27 -2
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -15,106 +15,33 @@
|
|
|
15
15
|
* - Model health tracking and failover
|
|
16
16
|
* - State persistence and resume
|
|
17
17
|
* - Orchestrator decision logging
|
|
18
|
+
*
|
|
19
|
+
* Phase 3a: Heavy logic extracted into:
|
|
20
|
+
* - swarm-lifecycle.ts — Decomposition, planning, verification, resume, synthesis, helpers
|
|
21
|
+
* - swarm-execution.ts — Wave dispatch loop, task completion handling
|
|
22
|
+
* - swarm-recovery.ts — Error recovery, resilience, circuit breaker, adaptive stagger
|
|
18
23
|
*/
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
import { createResultSynthesizer } from '../result-synthesizer.js';
|
|
23
|
-
import { taskResultToAgentOutput, DEFAULT_SWARM_CONFIG, getTaskTypeConfig } from './types.js';
|
|
24
|
+
import { createSmartDecomposer, parseDecompositionResponse, validateDecomposition } from '../tasks/smart-decomposer.js';
|
|
25
|
+
import { createResultSynthesizer } from '../agents/result-synthesizer.js';
|
|
26
|
+
import { DEFAULT_SWARM_CONFIG } from './types.js';
|
|
24
27
|
import { createSwarmTaskQueue } from './task-queue.js';
|
|
25
28
|
import { createSwarmBudgetPool } from './swarm-budget.js';
|
|
26
29
|
import { createSwarmWorkerPool } from './worker-pool.js';
|
|
27
|
-
import {
|
|
28
|
-
import { ModelHealthTracker, selectAlternativeModel } from './model-selector.js';
|
|
30
|
+
import { ModelHealthTracker } from './model-selector.js';
|
|
29
31
|
import { SwarmStateStore } from './swarm-state-store.js';
|
|
30
32
|
import { createSharedContextState } from '../../shared/shared-context-state.js';
|
|
31
33
|
import { createSharedEconomicsState } from '../../shared/shared-economics-state.js';
|
|
32
34
|
import { createSharedContextEngine } from '../../shared/context-engine.js';
|
|
33
|
-
import {
|
|
34
|
-
// ───
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'ran out of budget', 'no changes were made', 'no files were modified',
|
|
44
|
-
'no files were created', 'failed to complete', 'before research could begin',
|
|
45
|
-
'i was unable to', 'i could not', 'unfortunately i',
|
|
46
|
-
];
|
|
47
|
-
const BOILERPLATE_INDICATORS = [
|
|
48
|
-
'task completed successfully', 'i have completed the task',
|
|
49
|
-
'the task has been completed', 'done', 'completed', 'finished',
|
|
50
|
-
'no issues found', 'everything looks good', 'all tasks completed',
|
|
51
|
-
];
|
|
52
|
-
function hasFutureIntentLanguage(content) {
|
|
53
|
-
const trimmed = content.trim();
|
|
54
|
-
if (!trimmed)
|
|
55
|
-
return false;
|
|
56
|
-
const lower = trimmed.toLowerCase();
|
|
57
|
-
const completionSignals = /\b(done|completed|finished|created|saved|wrote|implemented|fixed|updated|added)\b/;
|
|
58
|
-
if (completionSignals.test(lower))
|
|
59
|
-
return false;
|
|
60
|
-
const futureIntentPatterns = [
|
|
61
|
-
/\b(i\s+will|i'll|let me)\s+(create|write|save|update|modify|fix|add|edit|implement|change|run|execute|build|continue)\b/,
|
|
62
|
-
/\b(i\s+need to|i\s+should|i\s+can)\s+(create|write|update|modify|fix|add|edit|implement|continue)\b/,
|
|
63
|
-
/\b(next step|remaining work|still need|to be done)\b/,
|
|
64
|
-
/\b(i am going to|i'm going to)\b/,
|
|
65
|
-
];
|
|
66
|
-
return futureIntentPatterns.some(p => p.test(lower));
|
|
67
|
-
}
|
|
68
|
-
function repoLooksUnscaffolded(baseDir) {
|
|
69
|
-
try {
|
|
70
|
-
const packageJson = path.join(baseDir, 'package.json');
|
|
71
|
-
const srcDir = path.join(baseDir, 'src');
|
|
72
|
-
if (!fs.existsSync(packageJson) && !fs.existsSync(srcDir)) {
|
|
73
|
-
return true;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
return false;
|
|
80
|
-
}
|
|
81
|
-
export function isHollowCompletion(spawnResult, taskType, swarmConfig) {
|
|
82
|
-
// Timeout uses toolCalls === -1, not hollow
|
|
83
|
-
if ((spawnResult.metrics.toolCalls ?? 0) === -1)
|
|
84
|
-
return false;
|
|
85
|
-
const toolCalls = spawnResult.metrics.toolCalls ?? 0;
|
|
86
|
-
// Truly empty completions: zero tools AND trivial output
|
|
87
|
-
// P4: Higher threshold (120 chars) + configurable via SwarmConfig
|
|
88
|
-
const hollowThreshold = swarmConfig?.hollowOutputThreshold ?? 120;
|
|
89
|
-
if (toolCalls === 0
|
|
90
|
-
&& (spawnResult.output?.trim().length ?? 0) < hollowThreshold) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
// P4: Boilerplate detection — zero tools AND short output that's just boilerplate
|
|
94
|
-
if (toolCalls === 0 && (spawnResult.output?.trim().length ?? 0) < 300) {
|
|
95
|
-
const outputLower = (spawnResult.output ?? '').toLowerCase().trim();
|
|
96
|
-
if (BOILERPLATE_INDICATORS.some(b => outputLower.includes(b))) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// "Success" that admits failure: worker claims success but output contains failure language
|
|
101
|
-
if (spawnResult.success) {
|
|
102
|
-
const outputLower = (spawnResult.output ?? '').toLowerCase();
|
|
103
|
-
if (FAILURE_INDICATORS.some(f => outputLower.includes(f))) {
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// V7: Use configurable requiresToolCalls from TaskTypeConfig.
|
|
108
|
-
// For action-oriented tasks (implement/test/refactor/etc), zero tool calls is ALWAYS hollow.
|
|
109
|
-
if (taskType) {
|
|
110
|
-
const typeConfig = getTaskTypeConfig(taskType, swarmConfig);
|
|
111
|
-
if (typeConfig.requiresToolCalls && toolCalls === 0) {
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
// ─── Orchestrator ──────────────────────────────────────────────────────────
|
|
35
|
+
import { calculateCost } from '../utilities/openrouter-pricing.js';
|
|
36
|
+
// ─── Extracted Module Imports ───────────────────────────────────────────
|
|
37
|
+
import { decomposeTask, planExecution, verifyIntegration, handleVerificationFailure, resumeExecution, synthesizeOutputs, saveCheckpoint, buildStats, buildSummary, buildErrorResult, detectFoundationTasks, buildArtifactInventory, skipRemainingTasks, } from './swarm-lifecycle.js';
|
|
38
|
+
import { executeWaves as executeWavesImpl, executeWave as executeWaveImpl, } from './swarm-execution.js';
|
|
39
|
+
import { finalRescuePass, midSwarmReplan, } from './swarm-recovery.js';
|
|
40
|
+
// ─── Helpers (extracted to break circular dependency) ─────────────────────
|
|
41
|
+
import { repoLooksUnscaffolded, } from './swarm-helpers.js';
|
|
42
|
+
// Re-export for backward compatibility
|
|
43
|
+
export { isHollowCompletion, FAILURE_INDICATORS, hasFutureIntentLanguage, BOILERPLATE_INDICATORS, repoLooksUnscaffolded } from './swarm-helpers.js';
|
|
44
|
+
// ─── Orchestrator ──────────────────────────────────────────────────────
|
|
118
45
|
export class SwarmOrchestrator {
|
|
119
46
|
config;
|
|
120
47
|
provider;
|
|
@@ -153,39 +80,34 @@ export class SwarmOrchestrator {
|
|
|
153
80
|
healthTracker;
|
|
154
81
|
stateStore;
|
|
155
82
|
spawnAgentFn;
|
|
156
|
-
// Circuit breaker: pause all dispatch after too many 429s
|
|
157
|
-
recentRateLimits = [];
|
|
158
|
-
circuitBreakerUntil = 0;
|
|
159
|
-
static CIRCUIT_BREAKER_WINDOW_MS = 30_000;
|
|
160
|
-
static CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
161
|
-
static CIRCUIT_BREAKER_PAUSE_MS = 15_000;
|
|
162
|
-
// P3: Per-model quality gate circuit breaker (replaces global circuit breaker)
|
|
163
|
-
perModelQualityRejections = new Map();
|
|
164
|
-
qualityGateDisabledModels = new Set();
|
|
165
|
-
static QUALITY_CIRCUIT_BREAKER_THRESHOLD = 5;
|
|
166
83
|
// Hollow completion streak: early termination when single-model swarm produces only hollows
|
|
167
84
|
hollowStreak = 0;
|
|
168
|
-
static HOLLOW_STREAK_THRESHOLD = 3;
|
|
169
85
|
// V7: Global dispatch + hollow ratio tracking for multi-model termination
|
|
170
86
|
totalDispatches = 0;
|
|
171
87
|
totalHollows = 0;
|
|
172
|
-
// Hollow ratio warning (fired once, then suppressed to avoid log spam)
|
|
173
|
-
hollowRatioWarned = false;
|
|
174
|
-
// P7: Adaptive dispatch stagger — increases on rate limits, decreases on success
|
|
175
|
-
adaptiveStaggerMs = 0; // Initialized from config in constructor
|
|
176
|
-
// F25: Consecutive timeout tracking per task — early-fail after limit
|
|
177
|
-
taskTimeoutCounts = new Map();
|
|
178
88
|
// Original prompt for re-planning on resume
|
|
179
89
|
originalPrompt = '';
|
|
180
90
|
// Mid-swarm re-planning: only once per swarm execution
|
|
181
91
|
hasReplanned = false;
|
|
92
|
+
// Recovery state (circuit breaker, stagger, quality gate breaker, etc.)
|
|
93
|
+
recoveryState;
|
|
182
94
|
constructor(config, provider, agentRegistry, spawnAgentFn, blackboard) {
|
|
183
95
|
this.config = { ...DEFAULT_SWARM_CONFIG, ...config };
|
|
184
96
|
this.provider = provider;
|
|
185
97
|
this.blackboard = blackboard;
|
|
186
98
|
this.spawnAgentFn = spawnAgentFn;
|
|
187
99
|
this.healthTracker = new ModelHealthTracker();
|
|
188
|
-
|
|
100
|
+
// Initialize recovery state
|
|
101
|
+
const initialStagger = this.config.dispatchStaggerMs ?? 500;
|
|
102
|
+
this.recoveryState = {
|
|
103
|
+
recentRateLimits: [],
|
|
104
|
+
circuitBreakerUntil: 0,
|
|
105
|
+
perModelQualityRejections: new Map(),
|
|
106
|
+
qualityGateDisabledModels: new Set(),
|
|
107
|
+
adaptiveStaggerMs: initialStagger,
|
|
108
|
+
taskTimeoutCounts: new Map(),
|
|
109
|
+
hollowRatioWarned: false,
|
|
110
|
+
};
|
|
189
111
|
// Phase 3.1+3.2: Shared context & economics for cross-worker learning
|
|
190
112
|
this.sharedContextState = createSharedContextState({
|
|
191
113
|
staticPrefix: 'You are a swarm worker agent.',
|
|
@@ -328,7 +250,7 @@ Rules:
|
|
|
328
250
|
reasoning: `Parse error: ${parseError}. Response preview (first 500 chars): ${snippet}`,
|
|
329
251
|
},
|
|
330
252
|
});
|
|
331
|
-
// Retry with explicit JSON instruction
|
|
253
|
+
// Retry with explicit JSON instruction
|
|
332
254
|
const retryResponse = await this.provider.chat([
|
|
333
255
|
{ role: 'system', content: systemPrompt },
|
|
334
256
|
{ role: 'user', content: `${task}\n\nIMPORTANT: Your previous attempt was truncated or could not be parsed (${parseError}). Return ONLY a raw JSON object with NO markdown formatting, NO explanation text, NO code fences. The JSON must have a "subtasks" array with at least 2 entries matching the schema above. Keep subtask descriptions concise to avoid truncation.` },
|
|
@@ -399,7 +321,6 @@ Rules:
|
|
|
399
321
|
listener(event);
|
|
400
322
|
}
|
|
401
323
|
catch (err) {
|
|
402
|
-
// Don't let listener errors break the orchestrator, but log for debugging
|
|
403
324
|
const msg = err instanceof Error ? err.message : String(err);
|
|
404
325
|
if (process.env.DEBUG) {
|
|
405
326
|
console.error(`[SwarmOrchestrator] Listener error on ${event.type}: ${msg}`);
|
|
@@ -413,12 +334,10 @@ Rules:
|
|
|
413
334
|
trackOrchestratorUsage(response, purpose) {
|
|
414
335
|
if (!response.usage)
|
|
415
336
|
return;
|
|
416
|
-
// Handle both raw API fields (total_tokens, prompt_tokens, completion_tokens)
|
|
417
|
-
// and ChatResponse fields (inputTokens, outputTokens)
|
|
418
337
|
const input = response.usage.prompt_tokens ?? response.usage.inputTokens ?? 0;
|
|
419
338
|
const output = response.usage.completion_tokens ?? response.usage.outputTokens ?? 0;
|
|
420
339
|
const tokens = response.usage.total_tokens ?? (input + output);
|
|
421
|
-
const cost = response.usage.cost ??
|
|
340
|
+
const cost = response.usage.cost ?? calculateCost(this.config.orchestratorModel, input, output);
|
|
422
341
|
this.orchestratorTokens += tokens;
|
|
423
342
|
this.orchestratorCost += cost;
|
|
424
343
|
this.orchestratorCalls++;
|
|
@@ -430,39 +349,108 @@ Rules:
|
|
|
430
349
|
cost,
|
|
431
350
|
});
|
|
432
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Build the OrchestratorInternals interface for extracted functions.
|
|
354
|
+
*/
|
|
355
|
+
getInternals() {
|
|
356
|
+
return {
|
|
357
|
+
config: this.config,
|
|
358
|
+
provider: this.provider,
|
|
359
|
+
blackboard: this.blackboard,
|
|
360
|
+
sharedContextState: this.sharedContextState,
|
|
361
|
+
sharedEconomicsState: this.sharedEconomicsState,
|
|
362
|
+
sharedContextEngine: this.sharedContextEngine,
|
|
363
|
+
taskQueue: this.taskQueue,
|
|
364
|
+
budgetPool: this.budgetPool,
|
|
365
|
+
workerPool: this.workerPool,
|
|
366
|
+
decomposer: this._decomposer,
|
|
367
|
+
synthesizer: this.synthesizer,
|
|
368
|
+
listeners: this.listeners,
|
|
369
|
+
errors: this.errors,
|
|
370
|
+
cancelled: this.cancelled,
|
|
371
|
+
currentPhase: this.currentPhase,
|
|
372
|
+
totalTokens: this.totalTokens,
|
|
373
|
+
totalCost: this.totalCost,
|
|
374
|
+
qualityRejections: this.qualityRejections,
|
|
375
|
+
retries: this.retries,
|
|
376
|
+
startTime: this.startTime,
|
|
377
|
+
modelUsage: this.modelUsage,
|
|
378
|
+
orchestratorTokens: this.orchestratorTokens,
|
|
379
|
+
orchestratorCost: this.orchestratorCost,
|
|
380
|
+
orchestratorCalls: this.orchestratorCalls,
|
|
381
|
+
plan: this.plan,
|
|
382
|
+
waveReviews: this.waveReviews,
|
|
383
|
+
verificationResult: this.verificationResult,
|
|
384
|
+
artifactInventory: this.artifactInventory,
|
|
385
|
+
orchestratorDecisions: this.orchestratorDecisions,
|
|
386
|
+
healthTracker: this.healthTracker,
|
|
387
|
+
stateStore: this.stateStore,
|
|
388
|
+
spawnAgentFn: this.spawnAgentFn,
|
|
389
|
+
hollowStreak: this.hollowStreak,
|
|
390
|
+
totalDispatches: this.totalDispatches,
|
|
391
|
+
totalHollows: this.totalHollows,
|
|
392
|
+
originalPrompt: this.originalPrompt,
|
|
393
|
+
hasReplanned: this.hasReplanned,
|
|
394
|
+
emit: (event) => this.emit(event),
|
|
395
|
+
trackOrchestratorUsage: (response, purpose) => this.trackOrchestratorUsage(response, purpose),
|
|
396
|
+
logDecision: (phase, decision, reasoning) => this.logDecision(phase, decision, reasoning),
|
|
397
|
+
executeWaves: () => this.executeWavesDelegate(),
|
|
398
|
+
executeWave: (tasks) => this.executeWaveDelegate(tasks),
|
|
399
|
+
finalRescuePass: () => this.finalRescuePassDelegate(),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Sync mutable state back from internals after an extracted function call.
|
|
404
|
+
* The internals object holds references to mutable objects (arrays, maps),
|
|
405
|
+
* but primitive values need syncing back.
|
|
406
|
+
*/
|
|
407
|
+
syncFromInternals(ctx) {
|
|
408
|
+
this.cancelled = ctx.cancelled;
|
|
409
|
+
this.currentPhase = ctx.currentPhase;
|
|
410
|
+
this.totalTokens = ctx.totalTokens;
|
|
411
|
+
this.totalCost = ctx.totalCost;
|
|
412
|
+
this.qualityRejections = ctx.qualityRejections;
|
|
413
|
+
this.retries = ctx.retries;
|
|
414
|
+
this.orchestratorTokens = ctx.orchestratorTokens;
|
|
415
|
+
this.orchestratorCost = ctx.orchestratorCost;
|
|
416
|
+
this.orchestratorCalls = ctx.orchestratorCalls;
|
|
417
|
+
this.plan = ctx.plan;
|
|
418
|
+
this.verificationResult = ctx.verificationResult;
|
|
419
|
+
this.artifactInventory = ctx.artifactInventory;
|
|
420
|
+
this.hollowStreak = ctx.hollowStreak;
|
|
421
|
+
this.totalDispatches = ctx.totalDispatches;
|
|
422
|
+
this.totalHollows = ctx.totalHollows;
|
|
423
|
+
this.originalPrompt = ctx.originalPrompt;
|
|
424
|
+
this.hasReplanned = ctx.hasReplanned;
|
|
425
|
+
}
|
|
433
426
|
/**
|
|
434
427
|
* Execute the full swarm pipeline for a task.
|
|
435
|
-
*
|
|
436
|
-
* V2 pipeline:
|
|
437
|
-
* 1. Check for resume
|
|
438
|
-
* 2. Decompose
|
|
439
|
-
* 3. Plan (acceptance criteria + verification plan)
|
|
440
|
-
* 4. Schedule into waves
|
|
441
|
-
* 5. Execute waves with review
|
|
442
|
-
* 6. Verify integration
|
|
443
|
-
* 7. Fix-up loop if verification fails
|
|
444
|
-
* 8. Synthesize
|
|
445
|
-
* 9. Checkpoint (final)
|
|
446
428
|
*/
|
|
447
429
|
async execute(task) {
|
|
448
430
|
this.startTime = Date.now();
|
|
449
431
|
this.originalPrompt = task;
|
|
450
432
|
try {
|
|
433
|
+
const ctx = this.getInternals();
|
|
451
434
|
// V2: Check for resume
|
|
452
435
|
if (this.config.resumeSessionId && this.stateStore) {
|
|
453
|
-
|
|
436
|
+
const resumeResult = await resumeExecution(ctx, task, () => midSwarmReplan(ctx));
|
|
437
|
+
this.syncFromInternals(ctx);
|
|
438
|
+
if (resumeResult)
|
|
439
|
+
return resumeResult;
|
|
440
|
+
// null means no checkpoint found, fall through to normal execute
|
|
454
441
|
}
|
|
455
442
|
// Phase 1: Decompose
|
|
456
443
|
this.currentPhase = 'decomposing';
|
|
444
|
+
ctx.currentPhase = 'decomposing';
|
|
457
445
|
this.emit({ type: 'swarm.phase.progress', phase: 'decomposing', message: 'Decomposing task into subtasks...' });
|
|
458
|
-
const decomposeOutcome = await
|
|
446
|
+
const decomposeOutcome = await decomposeTask(ctx, task);
|
|
447
|
+
this.syncFromInternals(ctx);
|
|
459
448
|
if (!decomposeOutcome.result) {
|
|
460
449
|
this.currentPhase = 'failed';
|
|
461
|
-
return
|
|
450
|
+
return buildErrorResult(ctx, `Decomposition failed: ${decomposeOutcome.failureReason}`);
|
|
462
451
|
}
|
|
463
452
|
let decomposition = decomposeOutcome.result;
|
|
464
453
|
// If repository is mostly empty, force a scaffold-first dependency chain
|
|
465
|
-
// so implementation tasks don't immediately fail on missing files.
|
|
466
454
|
if (repoLooksUnscaffolded(this.config.facts?.workingDirectory ?? process.cwd())) {
|
|
467
455
|
const scaffoldTask = decomposition.subtasks.find(st => /\b(scaffold|bootstrap|initialize|setup|set up|project scaffold)\b/i.test(st.description));
|
|
468
456
|
if (scaffoldTask) {
|
|
@@ -476,18 +464,18 @@ Rules:
|
|
|
476
464
|
this.logDecision('scaffold-first', `Repo appears unscaffolded; enforcing scaffold task ${scaffoldTask.id} as prerequisite`, '');
|
|
477
465
|
}
|
|
478
466
|
}
|
|
479
|
-
// F5: Validate decomposition
|
|
467
|
+
// F5: Validate decomposition
|
|
480
468
|
const validation = validateDecomposition(decomposition);
|
|
481
469
|
if (validation.warnings.length > 0) {
|
|
482
470
|
this.logDecision('decomposition-validation', `Warnings: ${validation.warnings.join('; ')}`, '');
|
|
483
471
|
}
|
|
484
472
|
if (!validation.valid) {
|
|
485
473
|
this.logDecision('decomposition-validation', `Invalid decomposition: ${validation.issues.join('; ')}`, 'Retrying...');
|
|
486
|
-
|
|
487
|
-
|
|
474
|
+
const retryOutcome = await decomposeTask(ctx, `${task}\n\nIMPORTANT: Previous decomposition was invalid: ${validation.issues.join('. ')}. Fix these issues.`);
|
|
475
|
+
this.syncFromInternals(ctx);
|
|
488
476
|
if (!retryOutcome.result) {
|
|
489
477
|
this.currentPhase = 'failed';
|
|
490
|
-
return
|
|
478
|
+
return buildErrorResult(ctx, `Decomposition validation failed: ${validation.issues.join('; ')}`);
|
|
491
479
|
}
|
|
492
480
|
decomposition = retryOutcome.result;
|
|
493
481
|
const retryValidation = validateDecomposition(decomposition);
|
|
@@ -497,36 +485,30 @@ Rules:
|
|
|
497
485
|
}
|
|
498
486
|
// Phase 2: Schedule into waves
|
|
499
487
|
this.currentPhase = 'scheduling';
|
|
488
|
+
ctx.currentPhase = 'scheduling';
|
|
500
489
|
this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Scheduling ${decomposition.subtasks.length} subtasks into waves...` });
|
|
501
490
|
this.taskQueue.loadFromDecomposition(decomposition, this.config);
|
|
502
|
-
// F3: Dynamic orchestrator reserve scaling
|
|
503
|
-
// More subtasks = more quality gate calls, synthesis work, and review overhead.
|
|
504
|
-
// Formula: max(configured ratio, 5% per subtask), capped at 40%.
|
|
491
|
+
// F3: Dynamic orchestrator reserve scaling
|
|
505
492
|
const subtaskCount = decomposition.subtasks.length;
|
|
506
493
|
const dynamicReserveRatio = Math.min(0.40, Math.max(this.config.orchestratorReserveRatio, subtaskCount * 0.05));
|
|
507
494
|
if (dynamicReserveRatio > this.config.orchestratorReserveRatio) {
|
|
508
495
|
this.logDecision('budget-scaling', `Scaled orchestrator reserve from ${(this.config.orchestratorReserveRatio * 100).toFixed(0)}% to ${(dynamicReserveRatio * 100).toFixed(0)}% for ${subtaskCount} subtasks`, '');
|
|
509
496
|
}
|
|
510
|
-
// Foundation task detection
|
|
511
|
-
|
|
512
|
-
//
|
|
513
|
-
this.detectFoundationTasks();
|
|
514
|
-
// D3/F1: Probe model capability before dispatch (default: true)
|
|
497
|
+
// Foundation task detection
|
|
498
|
+
detectFoundationTasks(ctx);
|
|
499
|
+
// D3/F1: Probe model capability before dispatch
|
|
515
500
|
if (this.config.probeModels !== false) {
|
|
516
501
|
await this.probeModelCapability();
|
|
517
|
-
// F15/F23: Handle all-models-failed probe scenario
|
|
518
|
-
// Resolve strategy: explicit probeFailureStrategy > legacy ignoreProbeFailures > default 'warn-and-try'
|
|
519
502
|
const probeStrategy = this.config.probeFailureStrategy
|
|
520
503
|
?? (this.config.ignoreProbeFailures ? 'warn-and-try' : 'warn-and-try');
|
|
521
504
|
const uniqueModels = [...new Set(this.config.workers.map(w => w.model))];
|
|
522
505
|
const healthyModels = this.healthTracker.getHealthy(uniqueModels);
|
|
523
506
|
if (healthyModels.length === 0 && uniqueModels.length > 0) {
|
|
524
507
|
if (probeStrategy === 'abort') {
|
|
525
|
-
// Hard abort — no tasks dispatched
|
|
526
508
|
const reason = `All ${uniqueModels.length} worker model(s) failed capability probes — no model can make tool calls. Aborting swarm to prevent budget waste. Fix model configuration and retry.`;
|
|
527
509
|
this.logDecision('probe-abort', reason, `Models tested: ${uniqueModels.join(', ')}`);
|
|
528
510
|
this.emit({ type: 'swarm.abort', reason });
|
|
529
|
-
|
|
511
|
+
skipRemainingTasks(ctx, reason);
|
|
530
512
|
const totalTasks = this.taskQueue.getStats().total;
|
|
531
513
|
const abortStats = {
|
|
532
514
|
completedTasks: 0, failedTasks: 0, skippedTasks: totalTasks,
|
|
@@ -542,27 +524,28 @@ Rules:
|
|
|
542
524
|
};
|
|
543
525
|
}
|
|
544
526
|
else {
|
|
545
|
-
// F23: warn-and-try — log warning, reset health, let real tasks prove capability
|
|
546
527
|
this.logDecision('probe-warning', `All ${uniqueModels.length} model(s) failed probe — continuing anyway (strategy: warn-and-try)`, 'Will abort after first real task failure if model cannot use tools');
|
|
547
|
-
// Reset health so dispatch doesn't skip all models
|
|
548
528
|
for (const model of uniqueModels) {
|
|
549
529
|
this.healthTracker.recordSuccess(model, 0);
|
|
550
530
|
}
|
|
551
531
|
}
|
|
552
532
|
}
|
|
553
533
|
}
|
|
554
|
-
// Emit skip events when tasks are cascade-skipped
|
|
534
|
+
// Emit skip events when tasks are cascade-skipped
|
|
555
535
|
this.taskQueue.setOnCascadeSkip((skippedTaskId, reason) => {
|
|
556
536
|
this.emit({ type: 'swarm.task.skipped', taskId: skippedTaskId, reason });
|
|
557
537
|
});
|
|
558
538
|
const stats = this.taskQueue.getStats();
|
|
559
539
|
this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Scheduled ${stats.total} tasks in ${this.taskQueue.getTotalWaves()} waves` });
|
|
560
|
-
// V2: Phase 2.5: Plan execution
|
|
540
|
+
// V2: Phase 2.5: Plan execution
|
|
561
541
|
let planPromise;
|
|
562
542
|
if (this.config.enablePlanning) {
|
|
563
543
|
this.currentPhase = 'planning';
|
|
544
|
+
ctx.currentPhase = 'planning';
|
|
564
545
|
this.emit({ type: 'swarm.phase.progress', phase: 'planning', message: 'Creating acceptance criteria...' });
|
|
565
|
-
planPromise =
|
|
546
|
+
planPromise = planExecution(ctx, task, decomposition).then(() => {
|
|
547
|
+
this.syncFromInternals(ctx);
|
|
548
|
+
}).catch(err => {
|
|
566
549
|
this.logDecision('planning', 'Planning failed (non-fatal)', err.message);
|
|
567
550
|
});
|
|
568
551
|
}
|
|
@@ -576,41 +559,50 @@ Rules:
|
|
|
576
559
|
maxCost: this.config.maxCost,
|
|
577
560
|
},
|
|
578
561
|
});
|
|
579
|
-
// Emit tasks AFTER swarm.start
|
|
580
|
-
// (swarm.start clears tasks/edges, so loading before it would lose them)
|
|
562
|
+
// Emit tasks AFTER swarm.start
|
|
581
563
|
this.emit({
|
|
582
564
|
type: 'swarm.tasks.loaded',
|
|
583
565
|
tasks: this.taskQueue.getAllTasks(),
|
|
584
566
|
});
|
|
585
|
-
// Phase 3: Execute waves
|
|
567
|
+
// Phase 3: Execute waves
|
|
586
568
|
this.currentPhase = 'executing';
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
569
|
+
ctx.currentPhase = 'executing';
|
|
570
|
+
await this.executeWavesDelegate();
|
|
571
|
+
this.syncFromInternals(ctx);
|
|
572
|
+
// V10: Final rescue pass
|
|
573
|
+
if (!this.cancelled) {
|
|
574
|
+
await this.finalRescuePassDelegate();
|
|
575
|
+
this.syncFromInternals(ctx);
|
|
576
|
+
}
|
|
577
|
+
// Ensure planning completed
|
|
592
578
|
if (planPromise)
|
|
593
579
|
await planPromise;
|
|
594
|
-
// Post-wave artifact audit
|
|
595
|
-
this.artifactInventory =
|
|
580
|
+
// Post-wave artifact audit
|
|
581
|
+
this.artifactInventory = buildArtifactInventory(ctx);
|
|
596
582
|
// V2: Phase 3.5: Verify integration
|
|
597
583
|
if (this.config.enableVerification && this.plan?.integrationTestPlan) {
|
|
598
584
|
this.currentPhase = 'verifying';
|
|
599
|
-
|
|
585
|
+
ctx.currentPhase = 'verifying';
|
|
586
|
+
const verification = await verifyIntegration(ctx, this.plan.integrationTestPlan);
|
|
587
|
+
this.syncFromInternals(ctx);
|
|
600
588
|
if (!verification.passed) {
|
|
601
|
-
await
|
|
589
|
+
await handleVerificationFailure(ctx, verification, task);
|
|
590
|
+
this.syncFromInternals(ctx);
|
|
602
591
|
}
|
|
603
592
|
}
|
|
604
593
|
// Phase 4: Synthesize results
|
|
605
594
|
this.currentPhase = 'synthesizing';
|
|
606
|
-
|
|
595
|
+
ctx.currentPhase = 'synthesizing';
|
|
596
|
+
const synthesisResult = await synthesizeOutputs(ctx);
|
|
597
|
+
this.syncFromInternals(ctx);
|
|
607
598
|
this.currentPhase = 'completed';
|
|
608
|
-
|
|
599
|
+
ctx.currentPhase = 'completed';
|
|
600
|
+
const executionStats = buildStats(ctx);
|
|
609
601
|
// V2: Final checkpoint
|
|
610
|
-
|
|
602
|
+
saveCheckpoint(ctx, 'final');
|
|
611
603
|
const hasArtifacts = (this.artifactInventory?.totalFiles ?? 0) > 0;
|
|
612
604
|
this.emit({ type: 'swarm.complete', stats: executionStats, errors: this.errors, artifactInventory: this.artifactInventory });
|
|
613
|
-
// Success requires completing at least 70% of tasks
|
|
605
|
+
// Success requires completing at least 70% of tasks
|
|
614
606
|
const completionRatio = executionStats.totalTasks > 0
|
|
615
607
|
? executionStats.completedTasks / executionStats.totalTasks
|
|
616
608
|
: 0;
|
|
@@ -622,7 +614,7 @@ Rules:
|
|
|
622
614
|
partialFailure: executionStats.failedTasks > 0,
|
|
623
615
|
synthesisResult: synthesisResult ?? undefined,
|
|
624
616
|
artifactInventory: this.artifactInventory,
|
|
625
|
-
summary:
|
|
617
|
+
summary: buildSummary(ctx, executionStats),
|
|
626
618
|
tasks: this.taskQueue.getAllTasks(),
|
|
627
619
|
stats: executionStats,
|
|
628
620
|
errors: this.errors,
|
|
@@ -637,2742 +629,131 @@ Rules:
|
|
|
637
629
|
recovered: false,
|
|
638
630
|
});
|
|
639
631
|
this.emit({ type: 'swarm.error', error: message, phase: 'execution' });
|
|
640
|
-
|
|
632
|
+
const ctx = this.getInternals();
|
|
633
|
+
return buildErrorResult(ctx, message);
|
|
641
634
|
}
|
|
642
635
|
finally {
|
|
643
636
|
this.workerPool.cleanup();
|
|
644
637
|
}
|
|
645
638
|
}
|
|
646
639
|
/**
|
|
647
|
-
*
|
|
648
|
-
*/
|
|
649
|
-
async decompose(task) {
|
|
650
|
-
try {
|
|
651
|
-
const repoMap = this.config.codebaseContext?.getRepoMap() ?? undefined;
|
|
652
|
-
const result = await this._decomposer.decompose(task, {
|
|
653
|
-
repoMap,
|
|
654
|
-
});
|
|
655
|
-
if (result.subtasks.length < 2) {
|
|
656
|
-
const reason = result.subtasks.length === 0
|
|
657
|
-
? `Decomposition produced 0 subtasks (model: ${this.config.orchestratorModel}).`
|
|
658
|
-
: `Decomposition produced only ${result.subtasks.length} subtask — too few for swarm mode.`;
|
|
659
|
-
this.logDecision('decomposition', `Insufficient subtasks: ${result.subtasks.length}`, reason);
|
|
660
|
-
try {
|
|
661
|
-
const lastResortResult = await this.lastResortDecompose(task);
|
|
662
|
-
if (lastResortResult && lastResortResult.subtasks.length >= 2) {
|
|
663
|
-
this.logDecision('decomposition', `Last-resort decomposition succeeded: ${lastResortResult.subtasks.length} subtasks`, 'Recovered from insufficient primary decomposition');
|
|
664
|
-
return { result: lastResortResult };
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
catch (error) {
|
|
668
|
-
this.logDecision('decomposition', 'Last-resort decomposition failed after insufficient primary decomposition', error.message);
|
|
669
|
-
}
|
|
670
|
-
const fallback = this.buildEmergencyDecomposition(task, reason);
|
|
671
|
-
this.emit({
|
|
672
|
-
type: 'swarm.phase.progress',
|
|
673
|
-
phase: 'decomposing',
|
|
674
|
-
message: `Using emergency decomposition fallback (${this.classifyDecompositionFailure(reason)})`,
|
|
675
|
-
});
|
|
676
|
-
this.logDecision('decomposition', `Using emergency scaffold decomposition: ${fallback.subtasks.length} subtasks`, 'Swarm will continue with deterministic fallback tasks');
|
|
677
|
-
return { result: fallback };
|
|
678
|
-
}
|
|
679
|
-
// Non-LLM result means decomposer fell back to heuristic mode.
|
|
680
|
-
// Prefer a simplified LLM decomposition, but continue with heuristic fallback when needed.
|
|
681
|
-
if (!result.metadata.llmAssisted) {
|
|
682
|
-
this.logDecision('decomposition', 'Heuristic decomposition detected — attempting last-resort simplified LLM decomposition', `Model: ${this.config.orchestratorModel}`);
|
|
683
|
-
try {
|
|
684
|
-
const lastResortResult = await this.lastResortDecompose(task);
|
|
685
|
-
if (lastResortResult && lastResortResult.subtasks.length >= 2) {
|
|
686
|
-
this.logDecision('decomposition', `Last-resort decomposition succeeded: ${lastResortResult.subtasks.length} subtasks`, 'Simplified prompt worked');
|
|
687
|
-
return { result: lastResortResult };
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
catch (error) {
|
|
691
|
-
this.logDecision('decomposition', 'Last-resort decomposition also failed', error.message);
|
|
692
|
-
}
|
|
693
|
-
this.logDecision('decomposition', `Continuing with heuristic decomposition: ${result.subtasks.length} subtasks`, 'Fallback is acceptable; do not abort swarm');
|
|
694
|
-
this.emit({
|
|
695
|
-
type: 'swarm.phase.progress',
|
|
696
|
-
phase: 'decomposing',
|
|
697
|
-
message: `Continuing with heuristic decomposition (${this.classifyDecompositionFailure('heuristic fallback')})`,
|
|
698
|
-
});
|
|
699
|
-
return { result };
|
|
700
|
-
}
|
|
701
|
-
// Flat-DAG detection: warn when all tasks land in wave 0 with no dependencies
|
|
702
|
-
const hasAnyDependency = result.subtasks.some(s => s.dependencies.length > 0);
|
|
703
|
-
if (!hasAnyDependency && result.subtasks.length >= 3) {
|
|
704
|
-
this.logDecision('decomposition', `Flat DAG: ${result.subtasks.length} tasks, zero dependencies`, 'All tasks will execute in wave 0 without ordering');
|
|
705
|
-
}
|
|
706
|
-
return { result };
|
|
707
|
-
}
|
|
708
|
-
catch (error) {
|
|
709
|
-
const message = error.message;
|
|
710
|
-
this.errors.push({
|
|
711
|
-
phase: 'decomposition',
|
|
712
|
-
message,
|
|
713
|
-
recovered: true,
|
|
714
|
-
});
|
|
715
|
-
const fallback = this.buildEmergencyDecomposition(task, `Decomposition threw an error: ${message}`);
|
|
716
|
-
this.emit({
|
|
717
|
-
type: 'swarm.phase.progress',
|
|
718
|
-
phase: 'decomposing',
|
|
719
|
-
message: `Decomposition fallback due to ${this.classifyDecompositionFailure(message)}`,
|
|
720
|
-
});
|
|
721
|
-
this.logDecision('decomposition', `Decomposition threw error; using emergency scaffold decomposition (${fallback.subtasks.length} subtasks)`, message);
|
|
722
|
-
return { result: fallback };
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
classifyDecompositionFailure(message) {
|
|
726
|
-
const m = message.toLowerCase();
|
|
727
|
-
if (m.includes('429') || m.includes('too many requests') || m.includes('rate limit')) {
|
|
728
|
-
return 'rate_limit';
|
|
729
|
-
}
|
|
730
|
-
if (m.includes('402') || m.includes('spend limit') || m.includes('key limit exceeded') || m.includes('insufficient credits')) {
|
|
731
|
-
return 'provider_budget_limit';
|
|
732
|
-
}
|
|
733
|
-
if (m.includes('parse') || m.includes('json') || m.includes('subtasks')) {
|
|
734
|
-
return 'parse_failure';
|
|
735
|
-
}
|
|
736
|
-
if (m.includes('invalid') || m.includes('validation')) {
|
|
737
|
-
return 'validation_failure';
|
|
738
|
-
}
|
|
739
|
-
return 'other';
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Deterministic decomposition fallback when all LLM decomposition paths fail.
|
|
743
|
-
* Keeps swarm mode alive with visible scaffolding tasks instead of aborting.
|
|
640
|
+
* Get live status for TUI.
|
|
744
641
|
*/
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
const taskLabel = task.trim().slice(0, 140) || 'requested task';
|
|
748
|
-
const repoMap = this.config.codebaseContext?.getRepoMap();
|
|
749
|
-
const topFiles = repoMap
|
|
750
|
-
? Array.from(repoMap.chunks.values())
|
|
751
|
-
.sort((a, b) => b.importance - a.importance)
|
|
752
|
-
.slice(0, 10)
|
|
753
|
-
.map(c => c.filePath)
|
|
754
|
-
: [];
|
|
755
|
-
const subtasks = [
|
|
756
|
-
{
|
|
757
|
-
id: 'task-fb-0',
|
|
758
|
-
description: `Scaffold implementation plan and identify target files for: ${taskLabel}`,
|
|
759
|
-
status: 'ready',
|
|
760
|
-
dependencies: [],
|
|
761
|
-
complexity: 2,
|
|
762
|
-
type: 'design',
|
|
763
|
-
parallelizable: true,
|
|
764
|
-
relevantFiles: topFiles.slice(0, 5),
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
id: 'task-fb-1',
|
|
768
|
-
description: `Implement core code changes for: ${taskLabel}`,
|
|
769
|
-
status: 'blocked',
|
|
770
|
-
dependencies: ['task-fb-0'],
|
|
771
|
-
complexity: 5,
|
|
772
|
-
type: 'implement',
|
|
773
|
-
parallelizable: false,
|
|
774
|
-
relevantFiles: topFiles.slice(0, 8),
|
|
775
|
-
},
|
|
776
|
-
{
|
|
777
|
-
id: 'task-fb-2',
|
|
778
|
-
description: `Add or update tests and run validation for: ${taskLabel}`,
|
|
779
|
-
status: 'blocked',
|
|
780
|
-
dependencies: ['task-fb-1'],
|
|
781
|
-
complexity: 3,
|
|
782
|
-
type: 'test',
|
|
783
|
-
parallelizable: false,
|
|
784
|
-
relevantFiles: topFiles.slice(0, 8),
|
|
785
|
-
},
|
|
786
|
-
{
|
|
787
|
-
id: 'task-fb-3',
|
|
788
|
-
description: `Integrate results and produce final summary for: ${taskLabel}`,
|
|
789
|
-
status: 'blocked',
|
|
790
|
-
dependencies: ['task-fb-1', 'task-fb-2'],
|
|
791
|
-
complexity: 2,
|
|
792
|
-
type: 'integrate',
|
|
793
|
-
parallelizable: false,
|
|
794
|
-
relevantFiles: topFiles.slice(0, 5),
|
|
795
|
-
},
|
|
796
|
-
];
|
|
797
|
-
const dependencyGraph = normalizer.buildDependencyGraph(subtasks);
|
|
798
|
-
const conflicts = normalizer.detectConflicts(subtasks);
|
|
642
|
+
getStatus() {
|
|
643
|
+
const stats = this.taskQueue.getStats();
|
|
799
644
|
return {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
llmAssisted: false,
|
|
645
|
+
phase: this.cancelled ? 'failed' : this.currentPhase,
|
|
646
|
+
currentWave: this.taskQueue.getCurrentWave() + 1,
|
|
647
|
+
totalWaves: this.taskQueue.getTotalWaves(),
|
|
648
|
+
activeWorkers: this.workerPool.getActiveWorkerStatus(),
|
|
649
|
+
queue: stats,
|
|
650
|
+
budget: {
|
|
651
|
+
tokensUsed: this.totalTokens + this.orchestratorTokens,
|
|
652
|
+
tokensTotal: this.config.totalBudget,
|
|
653
|
+
costUsed: this.totalCost + this.orchestratorCost,
|
|
654
|
+
costTotal: this.config.maxCost,
|
|
811
655
|
},
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
*/
|
|
818
|
-
async lastResortDecompose(task) {
|
|
819
|
-
// Include codebase grounding if repo map is available
|
|
820
|
-
let codebaseHint = '';
|
|
821
|
-
const repoMap = this.config.codebaseContext?.getRepoMap();
|
|
822
|
-
if (repoMap) {
|
|
823
|
-
const topFiles = Array.from(repoMap.chunks.values())
|
|
824
|
-
.sort((a, b) => b.importance - a.importance)
|
|
825
|
-
.slice(0, 10)
|
|
826
|
-
.map(c => c.filePath);
|
|
827
|
-
codebaseHint = `\nKey project files: ${topFiles.join(', ')}\nReference actual files in subtask descriptions.`;
|
|
828
|
-
}
|
|
829
|
-
const simplifiedPrompt = `Break this task into 2-6 subtasks. Return ONLY raw JSON, no markdown.
|
|
830
|
-
|
|
831
|
-
{"subtasks":[{"description":"...","type":"implement","complexity":3,"dependencies":[],"parallelizable":true,"relevantFiles":["src/..."]}],"strategy":"adaptive","reasoning":"..."}
|
|
832
|
-
|
|
833
|
-
Rules:
|
|
834
|
-
- dependencies: integer indices (e.g. [0] means depends on first subtask)
|
|
835
|
-
- type: one of research/implement/test/design/refactor/integrate/merge
|
|
836
|
-
- At least 2 subtasks${codebaseHint}`;
|
|
837
|
-
const response = await this.provider.chat([
|
|
838
|
-
{ role: 'system', content: simplifiedPrompt },
|
|
839
|
-
{ role: 'user', content: task },
|
|
840
|
-
], {
|
|
841
|
-
model: this.config.orchestratorModel,
|
|
842
|
-
maxTokens: 4096, // Short — avoids truncation
|
|
843
|
-
temperature: 0.1, // Very deterministic
|
|
844
|
-
});
|
|
845
|
-
this.trackOrchestratorUsage(response, 'decompose-last-resort');
|
|
846
|
-
const parsed = parseDecompositionResponse(response.content);
|
|
847
|
-
if (parsed.subtasks.length < 2)
|
|
848
|
-
return null;
|
|
849
|
-
// Build a proper SmartDecompositionResult from the parsed LLM output
|
|
850
|
-
const decomposer = createSmartDecomposer({ detectConflicts: true });
|
|
851
|
-
const subtasks = parsed.subtasks.map((s, index) => ({
|
|
852
|
-
id: `task-lr-${index}`,
|
|
853
|
-
description: s.description,
|
|
854
|
-
status: (s.dependencies.length > 0 ? 'blocked' : 'ready'),
|
|
855
|
-
dependencies: s.dependencies.map((d) => `task-lr-${d}`),
|
|
856
|
-
complexity: s.complexity,
|
|
857
|
-
type: s.type,
|
|
858
|
-
parallelizable: s.parallelizable,
|
|
859
|
-
relevantFiles: s.relevantFiles,
|
|
860
|
-
suggestedRole: s.suggestedRole,
|
|
861
|
-
}));
|
|
862
|
-
const dependencyGraph = decomposer.buildDependencyGraph(subtasks);
|
|
863
|
-
const conflicts = decomposer.detectConflicts(subtasks);
|
|
864
|
-
return {
|
|
865
|
-
originalTask: task,
|
|
866
|
-
subtasks,
|
|
867
|
-
dependencyGraph,
|
|
868
|
-
conflicts,
|
|
869
|
-
strategy: parsed.strategy,
|
|
870
|
-
totalComplexity: subtasks.reduce((sum, t) => sum + t.complexity, 0),
|
|
871
|
-
totalEstimatedTokens: subtasks.length * 5000,
|
|
872
|
-
metadata: {
|
|
873
|
-
decomposedAt: new Date(),
|
|
874
|
-
codebaseAware: false,
|
|
875
|
-
llmAssisted: true, // This IS LLM-assisted, just simplified
|
|
656
|
+
orchestrator: {
|
|
657
|
+
tokens: this.orchestratorTokens,
|
|
658
|
+
cost: this.orchestratorCost,
|
|
659
|
+
calls: this.orchestratorCalls,
|
|
660
|
+
model: this.config.orchestratorModel,
|
|
876
661
|
},
|
|
877
662
|
};
|
|
878
663
|
}
|
|
879
|
-
// ─── V2: Planning Phase ───────────────────────────────────────────────
|
|
880
664
|
/**
|
|
881
|
-
*
|
|
882
|
-
* Graceful: if planning fails, continues without criteria.
|
|
665
|
+
* Cancel the swarm execution.
|
|
883
666
|
*/
|
|
884
|
-
async
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
?? this.config.plannerModel ?? this.config.orchestratorModel;
|
|
889
|
-
this.emit({ type: 'swarm.role.action', role: 'manager', action: 'plan', model: plannerModel });
|
|
890
|
-
this.logDecision('planning', `Creating acceptance criteria (manager: ${plannerModel})`, `Task has ${decomposition.subtasks.length} subtasks, planning to ensure quality`);
|
|
891
|
-
const taskList = decomposition.subtasks
|
|
892
|
-
.map(s => `- [${s.id}] (${s.type}): ${s.description}`)
|
|
893
|
-
.join('\n');
|
|
894
|
-
const response = await this.provider.chat([
|
|
895
|
-
{
|
|
896
|
-
role: 'system',
|
|
897
|
-
content: `You are a project quality planner. Given a task and its decomposition into subtasks, create:
|
|
898
|
-
1. Acceptance criteria for each subtask (what "done" looks like)
|
|
899
|
-
2. An integration test plan (bash commands to verify the combined result works)
|
|
900
|
-
|
|
901
|
-
Respond with valid JSON:
|
|
902
|
-
{
|
|
903
|
-
"acceptanceCriteria": [
|
|
904
|
-
{ "taskId": "st-0", "criteria": ["criterion 1", "criterion 2"] }
|
|
905
|
-
],
|
|
906
|
-
"integrationTestPlan": {
|
|
907
|
-
"description": "What this test plan verifies",
|
|
908
|
-
"steps": [
|
|
909
|
-
{ "description": "Check if files exist", "command": "ls src/parser.js", "expectedResult": "file listed", "required": true }
|
|
910
|
-
],
|
|
911
|
-
"successCriteria": "All required steps pass"
|
|
912
|
-
},
|
|
913
|
-
"reasoning": "Why this plan was chosen"
|
|
914
|
-
}`,
|
|
915
|
-
},
|
|
916
|
-
{
|
|
917
|
-
role: 'user',
|
|
918
|
-
content: `Task: ${task}\n\nSubtasks:\n${taskList}`,
|
|
919
|
-
},
|
|
920
|
-
], {
|
|
921
|
-
model: plannerModel,
|
|
922
|
-
maxTokens: 3000,
|
|
923
|
-
temperature: 0.3,
|
|
924
|
-
});
|
|
925
|
-
this.trackOrchestratorUsage(response, 'plan');
|
|
926
|
-
const parsed = this.parseJSON(response.content);
|
|
927
|
-
if (parsed) {
|
|
928
|
-
this.plan = {
|
|
929
|
-
acceptanceCriteria: parsed.acceptanceCriteria ?? [],
|
|
930
|
-
integrationTestPlan: parsed.integrationTestPlan,
|
|
931
|
-
reasoning: parsed.reasoning ?? '',
|
|
932
|
-
};
|
|
933
|
-
this.emit({
|
|
934
|
-
type: 'swarm.plan.complete',
|
|
935
|
-
criteriaCount: this.plan.acceptanceCriteria.length,
|
|
936
|
-
hasIntegrationPlan: !!this.plan.integrationTestPlan,
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
catch (error) {
|
|
941
|
-
// Graceful fallback: continue without plan
|
|
942
|
-
this.errors.push({
|
|
943
|
-
phase: 'planning',
|
|
944
|
-
message: `Planning failed (non-fatal): ${error.message}`,
|
|
945
|
-
recovered: true,
|
|
946
|
-
});
|
|
947
|
-
}
|
|
667
|
+
async cancel() {
|
|
668
|
+
this.cancelled = true;
|
|
669
|
+
this.currentPhase = 'failed';
|
|
670
|
+
await this.workerPool.cancelAll();
|
|
948
671
|
}
|
|
949
|
-
// ───
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (!
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
// V3: Manager role handles wave review
|
|
959
|
-
const managerModel = this.config.hierarchy?.manager?.model
|
|
960
|
-
?? this.config.plannerModel ?? this.config.orchestratorModel;
|
|
961
|
-
const managerPersona = this.config.hierarchy?.manager?.persona;
|
|
962
|
-
this.emit({ type: 'swarm.role.action', role: 'manager', action: 'review', model: managerModel, wave: waveIndex + 1 });
|
|
963
|
-
this.emit({ type: 'swarm.review.start', wave: waveIndex + 1 });
|
|
964
|
-
this.logDecision('review', `Reviewing wave ${waveIndex + 1} outputs (manager: ${managerModel})`, 'Checking task outputs against acceptance criteria');
|
|
965
|
-
const completedTasks = this.taskQueue.getAllTasks()
|
|
966
|
-
.filter(t => t.status === 'completed' && t.wave === waveIndex);
|
|
967
|
-
if (completedTasks.length === 0) {
|
|
968
|
-
return { wave: waveIndex, assessment: 'good', taskAssessments: [], fixupTasks: [] };
|
|
969
|
-
}
|
|
970
|
-
// Build review prompt
|
|
971
|
-
const taskSummaries = completedTasks.map(t => {
|
|
972
|
-
const criteria = this.plan?.acceptanceCriteria.find(c => c.taskId === t.id);
|
|
973
|
-
return `Task ${t.id}: ${t.description}
|
|
974
|
-
Output: ${t.result?.output?.slice(0, 500) ?? 'No output'}
|
|
975
|
-
Acceptance criteria: ${criteria?.criteria.join('; ') ?? 'None set'}`;
|
|
976
|
-
}).join('\n\n');
|
|
977
|
-
const reviewModel = managerModel;
|
|
978
|
-
const reviewSystemPrompt = managerPersona
|
|
979
|
-
? `${managerPersona}\n\nYou are reviewing completed worker outputs. Assess each task against its acceptance criteria.\nRespond with JSON:`
|
|
980
|
-
: `You are reviewing completed worker outputs. Assess each task against its acceptance criteria.\nRespond with JSON:`;
|
|
981
|
-
const response = await this.provider.chat([
|
|
982
|
-
{
|
|
983
|
-
role: 'system',
|
|
984
|
-
content: `${reviewSystemPrompt}
|
|
985
|
-
{
|
|
986
|
-
"assessment": "good" | "needs-fixes" | "critical-issues",
|
|
987
|
-
"taskAssessments": [
|
|
988
|
-
{ "taskId": "st-0", "passed": true, "feedback": "optional feedback" }
|
|
989
|
-
],
|
|
990
|
-
"fixupInstructions": [
|
|
991
|
-
{ "fixesTaskId": "st-0", "description": "What to fix", "instructions": "Specific fix instructions" }
|
|
992
|
-
]
|
|
993
|
-
}`,
|
|
994
|
-
},
|
|
995
|
-
{ role: 'user', content: `Review these wave ${waveIndex + 1} outputs:\n\n${taskSummaries}` },
|
|
996
|
-
], { model: reviewModel, maxTokens: 2000, temperature: 0.3 });
|
|
997
|
-
this.trackOrchestratorUsage(response, 'review');
|
|
998
|
-
const parsed = this.parseJSON(response.content);
|
|
999
|
-
if (!parsed)
|
|
1000
|
-
return null;
|
|
1001
|
-
// Create fix-up tasks
|
|
1002
|
-
const fixupTasks = [];
|
|
1003
|
-
if (parsed.fixupInstructions) {
|
|
1004
|
-
for (const fix of parsed.fixupInstructions) {
|
|
1005
|
-
const fixupId = `fixup-${fix.fixesTaskId}-${Date.now()}`;
|
|
1006
|
-
const originalTask = this.taskQueue.getTask(fix.fixesTaskId);
|
|
1007
|
-
const fixupTask = {
|
|
1008
|
-
id: fixupId,
|
|
1009
|
-
description: fix.description,
|
|
1010
|
-
type: originalTask?.type ?? 'implement',
|
|
1011
|
-
dependencies: [fix.fixesTaskId],
|
|
1012
|
-
status: 'ready',
|
|
1013
|
-
complexity: 3,
|
|
1014
|
-
wave: waveIndex,
|
|
1015
|
-
attempts: 0,
|
|
1016
|
-
fixesTaskId: fix.fixesTaskId,
|
|
1017
|
-
fixInstructions: fix.instructions,
|
|
1018
|
-
};
|
|
1019
|
-
fixupTasks.push(fixupTask);
|
|
1020
|
-
this.emit({ type: 'swarm.fixup.spawned', taskId: fixupId, fixesTaskId: fix.fixesTaskId, description: fix.description });
|
|
1021
|
-
}
|
|
1022
|
-
if (fixupTasks.length > 0) {
|
|
1023
|
-
this.taskQueue.addFixupTasks(fixupTasks);
|
|
1024
|
-
// V5: Re-emit full task list so dashboard picks up fixup tasks + edges
|
|
1025
|
-
this.emit({
|
|
1026
|
-
type: 'swarm.tasks.loaded',
|
|
1027
|
-
tasks: this.taskQueue.getAllTasks(),
|
|
1028
|
-
});
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
const result = {
|
|
1032
|
-
wave: waveIndex,
|
|
1033
|
-
assessment: parsed.assessment ?? 'good',
|
|
1034
|
-
taskAssessments: parsed.taskAssessments ?? [],
|
|
1035
|
-
fixupTasks,
|
|
1036
|
-
};
|
|
1037
|
-
this.waveReviews.push(result);
|
|
1038
|
-
this.emit({
|
|
1039
|
-
type: 'swarm.review.complete',
|
|
1040
|
-
wave: waveIndex + 1,
|
|
1041
|
-
assessment: result.assessment,
|
|
1042
|
-
fixupCount: fixupTasks.length,
|
|
1043
|
-
});
|
|
1044
|
-
return result;
|
|
1045
|
-
}
|
|
1046
|
-
catch (error) {
|
|
1047
|
-
// Graceful: continue without review
|
|
1048
|
-
this.errors.push({
|
|
1049
|
-
phase: 'review',
|
|
1050
|
-
message: `Wave review failed (non-fatal): ${error.message}`,
|
|
1051
|
-
recovered: true,
|
|
1052
|
-
});
|
|
1053
|
-
return null;
|
|
672
|
+
// ─── D3: Model Capability Probing ─────────────────────────────────────
|
|
673
|
+
async probeModelCapability() {
|
|
674
|
+
const uniqueModels = new Set(this.config.workers.map(w => w.model));
|
|
675
|
+
this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Probing ${uniqueModels.size} model(s) for tool-calling capability...` });
|
|
676
|
+
const supportsTools = 'chatWithTools' in this.provider
|
|
677
|
+
&& typeof this.provider.chatWithTools === 'function';
|
|
678
|
+
if (!supportsTools) {
|
|
679
|
+
this.logDecision('model-probe', 'Provider does not support chatWithTools — skipping probe', '');
|
|
680
|
+
return;
|
|
1054
681
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
for (
|
|
1070
|
-
const step = testPlan.steps[i];
|
|
682
|
+
const providerWithTools = this.provider;
|
|
683
|
+
const probeTools = [{
|
|
684
|
+
type: 'function',
|
|
685
|
+
function: {
|
|
686
|
+
name: 'read_file',
|
|
687
|
+
description: 'Read a file from disk',
|
|
688
|
+
parameters: {
|
|
689
|
+
type: 'object',
|
|
690
|
+
properties: { path: { type: 'string', description: 'File path' } },
|
|
691
|
+
required: ['path'],
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
}];
|
|
695
|
+
const probeTimeout = this.config.probeTimeoutMs ?? 60_000;
|
|
696
|
+
for (const model of uniqueModels) {
|
|
1071
697
|
try {
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
698
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Probe timeout (${probeTimeout}ms)`)), probeTimeout));
|
|
699
|
+
const response = await Promise.race([
|
|
700
|
+
providerWithTools.chatWithTools([
|
|
701
|
+
{ role: 'system', content: 'You are a test probe. Call the read_file tool with path "package.json".' },
|
|
702
|
+
{ role: 'user', content: 'Read package.json.' },
|
|
703
|
+
], { model, maxTokens: 200, temperature: 0, tools: probeTools, tool_choice: 'required' }),
|
|
704
|
+
timeoutPromise,
|
|
705
|
+
]);
|
|
706
|
+
const hasToolCall = (response.toolCalls?.length ?? 0) > 0;
|
|
707
|
+
if (!hasToolCall) {
|
|
708
|
+
this.healthTracker.markUnhealthy(model);
|
|
709
|
+
this.logDecision('model-probe', `Model ${model} failed probe (no tool calls)`, 'Marked unhealthy');
|
|
1079
710
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const output = `Error: ${error.message}`;
|
|
1084
|
-
stepResults.push({ step, passed: false, output });
|
|
1085
|
-
if (step.required)
|
|
1086
|
-
allRequiredPassed = false;
|
|
1087
|
-
this.emit({ type: 'swarm.verify.step', stepIndex: i, description: step.description, passed: false });
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
const verificationResult = {
|
|
1091
|
-
passed: allRequiredPassed,
|
|
1092
|
-
stepResults,
|
|
1093
|
-
summary: allRequiredPassed
|
|
1094
|
-
? `All ${stepResults.filter(r => r.passed).length}/${stepResults.length} steps passed`
|
|
1095
|
-
: `${stepResults.filter(r => !r.passed).length}/${stepResults.length} steps failed`,
|
|
1096
|
-
};
|
|
1097
|
-
this.verificationResult = verificationResult;
|
|
1098
|
-
this.emit({ type: 'swarm.verify.complete', result: verificationResult });
|
|
1099
|
-
return verificationResult;
|
|
1100
|
-
}
|
|
1101
|
-
/**
|
|
1102
|
-
* Handle verification failure: create fix-up tasks and re-verify.
|
|
1103
|
-
*/
|
|
1104
|
-
async handleVerificationFailure(verification, task) {
|
|
1105
|
-
const maxRetries = this.config.maxVerificationRetries ?? 2;
|
|
1106
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1107
|
-
this.logDecision('verification', `Verification failed, fix-up attempt ${attempt + 1}/${maxRetries}`, `${verification.stepResults.filter(r => !r.passed).length} steps failed`);
|
|
1108
|
-
// Ask orchestrator what to fix
|
|
1109
|
-
try {
|
|
1110
|
-
const failedSteps = verification.stepResults
|
|
1111
|
-
.filter(r => !r.passed)
|
|
1112
|
-
.map(r => `- ${r.step.description}: ${r.output}`)
|
|
1113
|
-
.join('\n');
|
|
1114
|
-
const response = await this.provider.chat([
|
|
1115
|
-
{
|
|
1116
|
-
role: 'system',
|
|
1117
|
-
content: `Verification failed. Analyze the failures and create fix-up tasks.
|
|
1118
|
-
Respond with JSON: { "fixups": [{ "description": "what to fix", "type": "implement" }] }`,
|
|
1119
|
-
},
|
|
1120
|
-
{ role: 'user', content: `Original task: ${task}\n\nFailed verifications:\n${failedSteps}` },
|
|
1121
|
-
], { model: this.config.plannerModel ?? this.config.orchestratorModel, maxTokens: 1500, temperature: 0.3 });
|
|
1122
|
-
this.trackOrchestratorUsage(response, 'verification-fixup');
|
|
1123
|
-
const parsed = this.parseJSON(response.content);
|
|
1124
|
-
if (parsed?.fixups && parsed.fixups.length > 0) {
|
|
1125
|
-
const fixupTasks = parsed.fixups.map((f, i) => ({
|
|
1126
|
-
id: `verify-fix-${attempt}-${i}-${Date.now()}`,
|
|
1127
|
-
description: f.description,
|
|
1128
|
-
type: (f.type ?? 'implement'),
|
|
1129
|
-
dependencies: [],
|
|
1130
|
-
status: 'ready',
|
|
1131
|
-
complexity: 4,
|
|
1132
|
-
wave: this.taskQueue.getCurrentWave(),
|
|
1133
|
-
attempts: 0,
|
|
1134
|
-
fixesTaskId: 'verification',
|
|
1135
|
-
fixInstructions: f.description,
|
|
1136
|
-
}));
|
|
1137
|
-
this.taskQueue.addFixupTasks(fixupTasks);
|
|
1138
|
-
// V5: Re-emit full task list so dashboard picks up verification fixup tasks
|
|
1139
|
-
this.emit({
|
|
1140
|
-
type: 'swarm.tasks.loaded',
|
|
1141
|
-
tasks: this.taskQueue.getAllTasks(),
|
|
1142
|
-
});
|
|
1143
|
-
// Execute fix-up wave
|
|
1144
|
-
this.currentPhase = 'executing';
|
|
1145
|
-
await this.executeWave(fixupTasks);
|
|
1146
|
-
// Re-verify
|
|
1147
|
-
this.currentPhase = 'verifying';
|
|
1148
|
-
verification = await this.verifyIntegration(this.plan.integrationTestPlan);
|
|
1149
|
-
if (verification.passed)
|
|
1150
|
-
return;
|
|
711
|
+
else {
|
|
712
|
+
this.healthTracker.recordSuccess(model, 0);
|
|
713
|
+
this.logDecision('model-probe', `Model ${model} passed probe`, '');
|
|
1151
714
|
}
|
|
1152
715
|
}
|
|
1153
716
|
catch {
|
|
1154
|
-
|
|
717
|
+
this.healthTracker.markUnhealthy(model);
|
|
718
|
+
this.logDecision('model-probe', `Model ${model} probe errored`, 'Marked unhealthy');
|
|
1155
719
|
}
|
|
1156
720
|
}
|
|
1157
721
|
}
|
|
1158
|
-
// ─── V2:
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
this.logDecision('resume', 'No checkpoint found, starting fresh', `Session: ${this.config.resumeSessionId}`);
|
|
1166
|
-
// Clear resume flag and execute normally
|
|
1167
|
-
this.config.resumeSessionId = undefined;
|
|
1168
|
-
return this.execute(task);
|
|
1169
|
-
}
|
|
1170
|
-
this.logDecision('resume', `Resuming from wave ${checkpoint.currentWave}`, `Session: ${checkpoint.sessionId}`);
|
|
1171
|
-
this.emit({ type: 'swarm.state.resume', sessionId: checkpoint.sessionId, fromWave: checkpoint.currentWave });
|
|
1172
|
-
// Restore state
|
|
1173
|
-
if (checkpoint.originalPrompt)
|
|
1174
|
-
this.originalPrompt = checkpoint.originalPrompt;
|
|
1175
|
-
if (checkpoint.plan)
|
|
1176
|
-
this.plan = checkpoint.plan;
|
|
1177
|
-
if (checkpoint.modelHealth.length > 0)
|
|
1178
|
-
this.healthTracker.restore(checkpoint.modelHealth);
|
|
1179
|
-
this.orchestratorDecisions = checkpoint.decisions ?? [];
|
|
1180
|
-
this.errors = checkpoint.errors ?? [];
|
|
1181
|
-
this.totalTokens = checkpoint.stats.totalTokens;
|
|
1182
|
-
this.totalCost = checkpoint.stats.totalCost;
|
|
1183
|
-
this.qualityRejections = checkpoint.stats.qualityRejections;
|
|
1184
|
-
this.retries = checkpoint.stats.retries;
|
|
1185
|
-
// Restore shared context & economics state from checkpoint
|
|
1186
|
-
if (checkpoint.sharedContext) {
|
|
1187
|
-
this.sharedContextState.restoreFrom(checkpoint.sharedContext);
|
|
1188
|
-
}
|
|
1189
|
-
if (checkpoint.sharedEconomics) {
|
|
1190
|
-
this.sharedEconomicsState.restoreFrom(checkpoint.sharedEconomics);
|
|
1191
|
-
}
|
|
1192
|
-
// Restore task queue
|
|
1193
|
-
this.taskQueue.restoreFromCheckpoint({
|
|
1194
|
-
taskStates: checkpoint.taskStates,
|
|
1195
|
-
waves: checkpoint.waves,
|
|
1196
|
-
currentWave: checkpoint.currentWave,
|
|
1197
|
-
});
|
|
1198
|
-
// Reset orphaned dispatched tasks — their workers died with the previous process
|
|
1199
|
-
const resetIds = this.taskQueue.reconcileStaleDispatched({
|
|
1200
|
-
staleAfterMs: 0,
|
|
1201
|
-
activeTaskIds: new Set(),
|
|
1202
|
-
});
|
|
1203
|
-
const resetCount = resetIds.length;
|
|
1204
|
-
for (const taskId of resetIds) {
|
|
1205
|
-
const task = this.taskQueue.getTask(taskId);
|
|
1206
|
-
if (!task)
|
|
1207
|
-
continue;
|
|
1208
|
-
// Preserve at least 1 retry attempt
|
|
1209
|
-
task.attempts = Math.min(task.attempts, Math.max(0, this.config.workerRetries - 1));
|
|
1210
|
-
}
|
|
1211
|
-
if (resetCount > 0) {
|
|
1212
|
-
this.logDecision('resume', `Reset ${resetCount} orphaned dispatched tasks to ready`, 'Workers died with previous process');
|
|
1213
|
-
}
|
|
1214
|
-
// Reset skipped tasks whose dependencies are now satisfied
|
|
1215
|
-
let unskippedCount = 0;
|
|
1216
|
-
for (const task of this.taskQueue.getAllTasks()) {
|
|
1217
|
-
if (task.status === 'skipped') {
|
|
1218
|
-
const deps = task.dependencies.map(id => this.taskQueue.getTask(id));
|
|
1219
|
-
const allDepsSatisfied = deps.every(d => d && (d.status === 'completed' || d.status === 'decomposed'));
|
|
1220
|
-
if (allDepsSatisfied) {
|
|
1221
|
-
task.status = 'ready';
|
|
1222
|
-
task.attempts = 0;
|
|
1223
|
-
task.rescueContext = 'Recovered on resume — dependencies now satisfied';
|
|
1224
|
-
unskippedCount++;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
// Also reset failed tasks that have retry budget
|
|
1229
|
-
for (const task of this.taskQueue.getAllTasks()) {
|
|
1230
|
-
if (task.status === 'failed') {
|
|
1231
|
-
task.status = 'ready';
|
|
1232
|
-
task.attempts = Math.min(task.attempts, Math.max(0, this.config.workerRetries - 1));
|
|
1233
|
-
unskippedCount++;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
if (unskippedCount > 0) {
|
|
1237
|
-
this.logDecision('resume', `Recovered ${unskippedCount} skipped/failed tasks`, 'Fresh retry on resume');
|
|
1238
|
-
}
|
|
1239
|
-
// If many tasks are still stuck after un-skip, trigger re-plan
|
|
1240
|
-
const resumeStats = this.taskQueue.getStats();
|
|
1241
|
-
const stuckCount = resumeStats.failed + resumeStats.skipped;
|
|
1242
|
-
const totalAttempted = resumeStats.completed + stuckCount;
|
|
1243
|
-
if (totalAttempted > 0 && stuckCount / totalAttempted > 0.4) {
|
|
1244
|
-
this.logDecision('resume-replan', `${stuckCount}/${totalAttempted} tasks still stuck after resume — triggering re-plan`, '');
|
|
1245
|
-
this.hasReplanned = false; // Allow re-plan on resume
|
|
1246
|
-
await this.midSwarmReplan();
|
|
1247
|
-
}
|
|
1248
|
-
// Continue from where we left off
|
|
1249
|
-
this.currentPhase = 'executing';
|
|
1250
|
-
await this.executeWaves();
|
|
1251
|
-
// V10: Final rescue pass — attempt to recover cascade-skipped tasks with lenient mode
|
|
1252
|
-
if (!this.cancelled)
|
|
1253
|
-
await this.finalRescuePass();
|
|
1254
|
-
// Post-wave artifact audit
|
|
1255
|
-
this.artifactInventory = this.buildArtifactInventory();
|
|
1256
|
-
// Continue with verification and synthesis as normal
|
|
1257
|
-
if (this.config.enableVerification && this.plan?.integrationTestPlan) {
|
|
1258
|
-
this.currentPhase = 'verifying';
|
|
1259
|
-
const verification = await this.verifyIntegration(this.plan.integrationTestPlan);
|
|
1260
|
-
if (!verification.passed) {
|
|
1261
|
-
await this.handleVerificationFailure(verification, task);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
this.currentPhase = 'synthesizing';
|
|
1265
|
-
const synthesisResult = await this.synthesize();
|
|
1266
|
-
this.currentPhase = 'completed';
|
|
1267
|
-
const executionStats = this.buildStats();
|
|
1268
|
-
this.checkpoint('final');
|
|
1269
|
-
const hasArtifacts = (this.artifactInventory?.totalFiles ?? 0) > 0;
|
|
1270
|
-
this.emit({ type: 'swarm.complete', stats: executionStats, errors: this.errors, artifactInventory: this.artifactInventory });
|
|
1271
|
-
// Success requires completing at least 70% of tasks (not just > 0)
|
|
1272
|
-
const completionRatio = executionStats.totalTasks > 0
|
|
1273
|
-
? executionStats.completedTasks / executionStats.totalTasks
|
|
1274
|
-
: 0;
|
|
1275
|
-
const isSuccess = completionRatio >= 0.7;
|
|
1276
|
-
const isPartialSuccess = !isSuccess && executionStats.completedTasks > 0;
|
|
1277
|
-
return {
|
|
1278
|
-
success: isSuccess,
|
|
1279
|
-
partialSuccess: isPartialSuccess || (!executionStats.completedTasks && hasArtifacts),
|
|
1280
|
-
partialFailure: executionStats.failedTasks > 0,
|
|
1281
|
-
synthesisResult: synthesisResult ?? undefined,
|
|
1282
|
-
artifactInventory: this.artifactInventory,
|
|
1283
|
-
summary: this.buildSummary(executionStats),
|
|
1284
|
-
tasks: this.taskQueue.getAllTasks(),
|
|
1285
|
-
stats: executionStats,
|
|
1286
|
-
errors: this.errors,
|
|
722
|
+
// ─── V2: Decision Logging ─────────────────────────────────────────────
|
|
723
|
+
logDecision(phase, decision, reasoning) {
|
|
724
|
+
const entry = {
|
|
725
|
+
timestamp: Date.now(),
|
|
726
|
+
phase,
|
|
727
|
+
decision,
|
|
728
|
+
reasoning,
|
|
1287
729
|
};
|
|
730
|
+
this.orchestratorDecisions.push(entry);
|
|
731
|
+
this.emit({ type: 'swarm.orchestrator.decision', decision: entry });
|
|
1288
732
|
}
|
|
1289
|
-
// ───
|
|
733
|
+
// ─── Delegation Methods ───────────────────────────────────────────────
|
|
1290
734
|
/**
|
|
1291
|
-
*
|
|
735
|
+
* Delegate to executeWaves in swarm-execution.ts.
|
|
1292
736
|
*/
|
|
1293
|
-
async
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
while (waveIndex < totalWaves && !this.cancelled) {
|
|
1298
|
-
const activeTaskIds = new Set(this.workerPool.getActiveWorkerStatus().map(w => w.taskId));
|
|
1299
|
-
const recovered = this.taskQueue.reconcileStaleDispatched({
|
|
1300
|
-
staleAfterMs: dispatchLeaseStaleMs,
|
|
1301
|
-
activeTaskIds,
|
|
1302
|
-
});
|
|
1303
|
-
if (recovered.length > 0) {
|
|
1304
|
-
this.logDecision('lease-recovery', `Recovered ${recovered.length} stale dispatched task(s)`, recovered.join(', '));
|
|
1305
|
-
}
|
|
1306
|
-
const readyTasks = this.taskQueue.getReadyTasks();
|
|
1307
|
-
const queueStats = this.taskQueue.getStats();
|
|
1308
|
-
// F18: Skip empty waves — if no tasks are ready and none are running,
|
|
1309
|
-
// remaining tasks are all blocked/failed/skipped. Break instead of
|
|
1310
|
-
// running useless review cycles.
|
|
1311
|
-
if (readyTasks.length === 0 && queueStats.running === 0 && queueStats.ready === 0) {
|
|
1312
|
-
this.logDecision('wave-skip', `Skipping waves ${waveIndex + 1}-${totalWaves}: no dispatchable tasks remain`, `Stats: ${queueStats.completed} completed, ${queueStats.failed} failed, ${queueStats.skipped} skipped`);
|
|
1313
|
-
break;
|
|
1314
|
-
}
|
|
1315
|
-
this.emit({
|
|
1316
|
-
type: 'swarm.wave.start',
|
|
1317
|
-
wave: waveIndex + 1,
|
|
1318
|
-
totalWaves,
|
|
1319
|
-
taskCount: readyTasks.length,
|
|
1320
|
-
});
|
|
1321
|
-
// Dispatch tasks up to concurrency limit
|
|
1322
|
-
await this.executeWave(readyTasks);
|
|
1323
|
-
// Wave complete stats
|
|
1324
|
-
const afterStats = this.taskQueue.getStats();
|
|
1325
|
-
const waveCompleted = afterStats.completed - (queueStats.completed);
|
|
1326
|
-
const waveFailed = afterStats.failed - (queueStats.failed);
|
|
1327
|
-
const waveSkipped = afterStats.skipped - (queueStats.skipped);
|
|
1328
|
-
this.emit({
|
|
1329
|
-
type: 'swarm.wave.complete',
|
|
1330
|
-
wave: waveIndex + 1,
|
|
1331
|
-
totalWaves,
|
|
1332
|
-
completed: waveCompleted,
|
|
1333
|
-
failed: waveFailed,
|
|
1334
|
-
skipped: waveSkipped,
|
|
1335
|
-
});
|
|
1336
|
-
// Wave failure recovery: if ALL tasks in a wave failed, retry with adapted context
|
|
1337
|
-
if (waveCompleted === 0 && waveFailed > 0 && readyTasks.length > 0) {
|
|
1338
|
-
this.emit({ type: 'swarm.wave.allFailed', wave: waveIndex + 1 });
|
|
1339
|
-
this.logDecision('wave-recovery', `Entire wave ${waveIndex + 1} failed (${waveFailed} tasks)`, 'Checking if budget allows retry with adapted strategy');
|
|
1340
|
-
// Re-queue failed tasks with retry context if budget allows
|
|
1341
|
-
const budgetRemaining = this.budgetPool.hasCapacity();
|
|
1342
|
-
const failedWaveTasks = readyTasks.filter(t => {
|
|
1343
|
-
const task = this.taskQueue.getTask(t.id);
|
|
1344
|
-
return task && task.status === 'failed' && task.attempts < (this.config.workerRetries + 1);
|
|
1345
|
-
});
|
|
1346
|
-
if (budgetRemaining && failedWaveTasks.length > 0) {
|
|
1347
|
-
for (const t of failedWaveTasks) {
|
|
1348
|
-
const task = this.taskQueue.getTask(t.id);
|
|
1349
|
-
if (!task)
|
|
1350
|
-
continue;
|
|
1351
|
-
task.status = 'ready';
|
|
1352
|
-
task.retryContext = {
|
|
1353
|
-
previousFeedback: 'All tasks in this batch failed. Try a fundamentally different approach — the previous strategy did not work.',
|
|
1354
|
-
previousScore: 0,
|
|
1355
|
-
attempt: task.attempts,
|
|
1356
|
-
previousModel: task.assignedModel,
|
|
1357
|
-
swarmProgress: this.getSwarmProgressSummary(),
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
this.logDecision('wave-recovery', `Re-queued ${failedWaveTasks.length} tasks with adapted retry context`, 'Budget allows retry');
|
|
1361
|
-
// Re-execute the wave with adapted tasks
|
|
1362
|
-
await this.executeWave(failedWaveTasks.map(t => this.taskQueue.getTask(t.id)).filter(t => t.status === 'ready'));
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
// F5: Adaptive re-decomposition — if < 50% of wave tasks succeeded,
|
|
1366
|
-
// the decomposition may be structurally flawed. Log for observability.
|
|
1367
|
-
// (Full re-decomposition of remaining work would require re-architecting the queue,
|
|
1368
|
-
// so we log the signal and let wave retry + fixup handle recovery.)
|
|
1369
|
-
const waveTotal = waveCompleted + waveFailed + waveSkipped;
|
|
1370
|
-
const waveSuccessRate = waveTotal > 0 ? waveCompleted / waveTotal : 0;
|
|
1371
|
-
if (waveSuccessRate < 0.5 && waveTotal >= 2) {
|
|
1372
|
-
this.logDecision('decomposition-quality', `Wave ${waveIndex + 1} success rate ${(waveSuccessRate * 100).toFixed(0)}% (${waveCompleted}/${waveTotal})`, 'Low success rate may indicate decomposition quality issues');
|
|
1373
|
-
}
|
|
1374
|
-
// V2: Review wave outputs
|
|
1375
|
-
const review = await this.reviewWave(waveIndex);
|
|
1376
|
-
if (review && review.fixupTasks.length > 0) {
|
|
1377
|
-
// Execute fix-up tasks immediately
|
|
1378
|
-
await this.executeWave(review.fixupTasks);
|
|
1379
|
-
}
|
|
1380
|
-
// Rescue cascade-skipped tasks that can still run
|
|
1381
|
-
// (after wave review + fixup, some skipped tasks may now be viable)
|
|
1382
|
-
const rescued = this.rescueCascadeSkipped();
|
|
1383
|
-
if (rescued.length > 0) {
|
|
1384
|
-
this.logDecision('cascade-rescue', `Rescued ${rescued.length} cascade-skipped tasks after wave ${waveIndex + 1}`, rescued.map(t => t.id).join(', '));
|
|
1385
|
-
await this.executeWave(rescued);
|
|
1386
|
-
}
|
|
1387
|
-
// Reset quality circuit breaker at wave boundary — each wave gets a fresh chance.
|
|
1388
|
-
// Within a wave, rejections accumulate properly so the breaker can trip.
|
|
1389
|
-
// Between waves, we reset so each wave gets a fresh quality evaluation window.
|
|
1390
|
-
// (The within-wave reset at quality-gate-passed is kept — that's correct.)
|
|
1391
|
-
if (this.qualityGateDisabledModels.size > 0) {
|
|
1392
|
-
this.qualityGateDisabledModels.clear();
|
|
1393
|
-
this.perModelQualityRejections.clear();
|
|
1394
|
-
this.logDecision('quality-circuit-breaker', `Re-enabled quality gates for all models at wave ${waveIndex + 1} boundary`, 'Each wave gets a fresh quality evaluation window');
|
|
1395
|
-
}
|
|
1396
|
-
// F3: Log budget reallocation after wave completion.
|
|
1397
|
-
// SharedBudgetPool already returns unused tokens via release(), but we log it
|
|
1398
|
-
// for observability so operators can see how budget flows between waves.
|
|
1399
|
-
const budgetStats = this.budgetPool.getStats();
|
|
1400
|
-
this.logDecision('budget-reallocation', `After wave ${waveIndex + 1}: ${budgetStats.tokensRemaining} tokens remaining (${(budgetStats.utilization * 100).toFixed(0)}% utilized)`, '');
|
|
1401
|
-
this.budgetPool.reallocateUnused(budgetStats.tokensRemaining);
|
|
1402
|
-
// F21: Mid-swarm situational assessment — evaluate success rate and budget health,
|
|
1403
|
-
// optionally triage low-priority tasks to conserve budget for critical path.
|
|
1404
|
-
await this.assessAndAdapt(waveIndex);
|
|
1405
|
-
// V2: Checkpoint after each wave
|
|
1406
|
-
this.checkpoint(`wave-${waveIndex}`);
|
|
1407
|
-
// Advance to next wave
|
|
1408
|
-
if (!this.taskQueue.advanceWave())
|
|
1409
|
-
break;
|
|
1410
|
-
waveIndex++;
|
|
1411
|
-
}
|
|
737
|
+
async executeWavesDelegate() {
|
|
738
|
+
const ctx = this.getInternals();
|
|
739
|
+
await executeWavesImpl(ctx, this.recoveryState, () => this.getStatus());
|
|
740
|
+
this.syncFromInternals(ctx);
|
|
1412
741
|
}
|
|
1413
742
|
/**
|
|
1414
|
-
*
|
|
743
|
+
* Delegate to executeWave in swarm-execution.ts.
|
|
1415
744
|
*/
|
|
1416
|
-
async
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
// Circuit breaker: wait if tripped
|
|
1421
|
-
if (this.isCircuitBreakerActive()) {
|
|
1422
|
-
const waitMs = this.circuitBreakerUntil - Date.now();
|
|
1423
|
-
if (waitMs > 0)
|
|
1424
|
-
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
1425
|
-
continue; // Re-check after wait
|
|
1426
|
-
}
|
|
1427
|
-
const task = tasks[taskIndex];
|
|
1428
|
-
await this.dispatchTask(task);
|
|
1429
|
-
taskIndex++;
|
|
1430
|
-
// Stagger dispatches to avoid rate limit storms
|
|
1431
|
-
if (taskIndex < tasks.length && this.workerPool.availableSlots > 0) {
|
|
1432
|
-
await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
// Process completions and dispatch more tasks as slots open
|
|
1436
|
-
while (this.workerPool.activeCount > 0 && !this.cancelled) {
|
|
1437
|
-
const completed = await this.workerPool.waitForAny();
|
|
1438
|
-
if (!completed)
|
|
1439
|
-
break;
|
|
1440
|
-
// H2: Use per-task startedAt for accurate duration (not orchestrator startTime)
|
|
1441
|
-
await this.handleTaskCompletion(completed.taskId, completed.result, completed.startedAt);
|
|
1442
|
-
// Emit budget update
|
|
1443
|
-
this.emitBudgetUpdate();
|
|
1444
|
-
// Emit status update
|
|
1445
|
-
this.emitStatusUpdate();
|
|
1446
|
-
// Dispatch more tasks if slots available and tasks remain
|
|
1447
|
-
while (taskIndex < tasks.length && this.workerPool.availableSlots > 0 && !this.cancelled) {
|
|
1448
|
-
const task = tasks[taskIndex];
|
|
1449
|
-
if (task.status === 'ready') {
|
|
1450
|
-
await this.dispatchTask(task);
|
|
1451
|
-
// Stagger dispatches to avoid rate limit storms
|
|
1452
|
-
if (taskIndex + 1 < tasks.length && this.workerPool.availableSlots > 0) {
|
|
1453
|
-
await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
taskIndex++;
|
|
1457
|
-
}
|
|
1458
|
-
// Also check for cross-wave ready tasks to fill slots (skip if circuit breaker active)
|
|
1459
|
-
if (this.workerPool.availableSlots > 0 && !this.isCircuitBreakerActive()) {
|
|
1460
|
-
const moreReady = this.taskQueue.getAllReadyTasks()
|
|
1461
|
-
.filter(t => !this.workerPool.getActiveWorkerStatus().some(w => w.taskId === t.id));
|
|
1462
|
-
for (let i = 0; i < moreReady.length; i++) {
|
|
1463
|
-
if (this.workerPool.availableSlots <= 0)
|
|
1464
|
-
break;
|
|
1465
|
-
await this.dispatchTask(moreReady[i]);
|
|
1466
|
-
// Stagger dispatches to avoid rate limit storms
|
|
1467
|
-
if (i + 1 < moreReady.length && this.workerPool.availableSlots > 0) {
|
|
1468
|
-
await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
// F20: Re-dispatch pass — after all workers finish, budget may have been freed
|
|
1474
|
-
// by completed tasks. Try to dispatch any still-ready tasks (e.g., those paused
|
|
1475
|
-
// by budget exhaustion earlier).
|
|
1476
|
-
if (!this.cancelled && this.budgetPool.hasCapacity()) {
|
|
1477
|
-
const stillReady = this.taskQueue.getAllReadyTasks()
|
|
1478
|
-
.filter(t => !this.workerPool.getActiveWorkerStatus().some(w => w.taskId === t.id));
|
|
1479
|
-
if (stillReady.length > 0) {
|
|
1480
|
-
this.logDecision('budget-redispatch', `Budget freed after wave — re-dispatching ${stillReady.length} ready task(s)`, `Budget: ${JSON.stringify(this.budgetPool.getStats())}`);
|
|
1481
|
-
for (const task of stillReady) {
|
|
1482
|
-
if (this.workerPool.availableSlots <= 0 || !this.budgetPool.hasCapacity())
|
|
1483
|
-
break;
|
|
1484
|
-
await this.dispatchTask(task);
|
|
1485
|
-
if (this.workerPool.availableSlots > 0) {
|
|
1486
|
-
await new Promise(resolve => setTimeout(resolve, this.getStaggerMs()));
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
// Wait for these re-dispatched tasks to complete
|
|
1490
|
-
while (this.workerPool.activeCount > 0 && !this.cancelled) {
|
|
1491
|
-
const completed = await this.workerPool.waitForAny();
|
|
1492
|
-
if (!completed)
|
|
1493
|
-
break;
|
|
1494
|
-
await this.handleTaskCompletion(completed.taskId, completed.result, completed.startedAt);
|
|
1495
|
-
this.emitBudgetUpdate();
|
|
1496
|
-
this.emitStatusUpdate();
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
745
|
+
async executeWaveDelegate(tasks) {
|
|
746
|
+
const ctx = this.getInternals();
|
|
747
|
+
await executeWaveImpl(ctx, this.recoveryState, tasks, () => this.getStatus());
|
|
748
|
+
this.syncFromInternals(ctx);
|
|
1500
749
|
}
|
|
1501
750
|
/**
|
|
1502
|
-
*
|
|
1503
|
-
* Selects the worker once and passes it through to avoid double-selection.
|
|
751
|
+
* Delegate to finalRescuePass in swarm-recovery.ts.
|
|
1504
752
|
*/
|
|
1505
|
-
async
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
// V10: Try resilience recovery if task had previous attempts (prior worker may have produced artifacts)
|
|
1510
|
-
this.logDecision('no-worker', `${task.id}: no worker for type ${task.type}`, '');
|
|
1511
|
-
if (task.attempts > 0) {
|
|
1512
|
-
const syntheticTaskResult = { success: false, output: '', tokensUsed: 0, costUsed: 0, durationMs: 0, model: 'none' };
|
|
1513
|
-
const syntheticSpawn = { success: false, output: '', metrics: { tokens: 0, duration: 0, toolCalls: 0 } };
|
|
1514
|
-
if (await this.tryResilienceRecovery(task, task.id, syntheticTaskResult, syntheticSpawn)) {
|
|
1515
|
-
return;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
this.taskQueue.markFailedWithoutCascade(task.id, 0);
|
|
1519
|
-
this.taskQueue.triggerCascadeSkip(task.id);
|
|
1520
|
-
this.emit({
|
|
1521
|
-
type: 'swarm.task.failed',
|
|
1522
|
-
taskId: task.id,
|
|
1523
|
-
error: `No worker available for task type: ${task.type}`,
|
|
1524
|
-
attempt: task.attempts,
|
|
1525
|
-
maxAttempts: 0,
|
|
1526
|
-
willRetry: false,
|
|
1527
|
-
failureMode: 'error',
|
|
1528
|
-
});
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
try {
|
|
1532
|
-
// Pre-dispatch auto-split for critical-path bottlenecks
|
|
1533
|
-
if (this.shouldAutoSplit(task)) {
|
|
1534
|
-
try {
|
|
1535
|
-
const splitResult = await this.judgeSplit(task);
|
|
1536
|
-
if (splitResult.shouldSplit && splitResult.subtasks) {
|
|
1537
|
-
task.status = 'dispatched'; // Required for replaceWithSubtasks
|
|
1538
|
-
this.taskQueue.replaceWithSubtasks(task.id, splitResult.subtasks);
|
|
1539
|
-
this.emit({
|
|
1540
|
-
type: 'swarm.task.resilience',
|
|
1541
|
-
taskId: task.id,
|
|
1542
|
-
strategy: 'auto-split',
|
|
1543
|
-
succeeded: true,
|
|
1544
|
-
reason: `Pre-dispatch split into ${splitResult.subtasks.length} parallel subtasks`,
|
|
1545
|
-
artifactsFound: 0,
|
|
1546
|
-
toolCalls: 0,
|
|
1547
|
-
});
|
|
1548
|
-
return; // Subtasks now in queue, will be dispatched this wave
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
catch (err) {
|
|
1552
|
-
this.logDecision('auto-split', `${task.id}: split judge failed — ${err.message}`, '');
|
|
1553
|
-
// Fall through to normal dispatch
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
this.totalDispatches++;
|
|
1557
|
-
const dispatchedModel = task.assignedModel ?? worker.model;
|
|
1558
|
-
this.taskQueue.markDispatched(task.id, dispatchedModel);
|
|
1559
|
-
if (task.assignedModel && task.assignedModel !== worker.model) {
|
|
1560
|
-
this.logDecision('failover', `Dispatching ${task.id} with failover model ${task.assignedModel} (worker default: ${worker.model})`, 'Retry model override is active');
|
|
1561
|
-
}
|
|
1562
|
-
// Pass the pre-selected worker to avoid double-selection in dispatch()
|
|
1563
|
-
await this.workerPool.dispatch(task, worker);
|
|
1564
|
-
this.emit({
|
|
1565
|
-
type: 'swarm.task.dispatched',
|
|
1566
|
-
taskId: task.id,
|
|
1567
|
-
description: task.description,
|
|
1568
|
-
model: dispatchedModel,
|
|
1569
|
-
workerName: worker.name,
|
|
1570
|
-
toolCount: worker.allowedTools?.length ?? -1, // -1 = all tools
|
|
1571
|
-
tools: worker.allowedTools,
|
|
1572
|
-
retryContext: task.retryContext,
|
|
1573
|
-
fromModel: task.retryContext ? task.retryContext.previousModel : undefined,
|
|
1574
|
-
attempts: task.attempts,
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
catch (error) {
|
|
1578
|
-
const errorMsg = error.message;
|
|
1579
|
-
// F20: Budget exhaustion is NOT a task failure — the task is fine, we just ran out of money.
|
|
1580
|
-
// Reset status to ready so it can be picked up if budget becomes available
|
|
1581
|
-
// (e.g., after tokens are released from completing tasks).
|
|
1582
|
-
if (errorMsg.includes('Budget pool exhausted')) {
|
|
1583
|
-
task.status = 'ready';
|
|
1584
|
-
this.logDecision('budget-pause', `Cannot dispatch ${task.id}: budget exhausted — task kept ready for potential re-dispatch`, `Budget stats: ${JSON.stringify(this.budgetPool.getStats())}`);
|
|
1585
|
-
return;
|
|
1586
|
-
}
|
|
1587
|
-
this.errors.push({
|
|
1588
|
-
taskId: task.id,
|
|
1589
|
-
phase: 'dispatch',
|
|
1590
|
-
message: errorMsg,
|
|
1591
|
-
recovered: false,
|
|
1592
|
-
});
|
|
1593
|
-
this.logDecision('dispatch-error', `${task.id}: dispatch failed: ${errorMsg.slice(0, 100)}`, `attempts: ${task.attempts}`);
|
|
1594
|
-
// V10: Try resilience recovery if task had previous attempts (prior worker may have produced artifacts)
|
|
1595
|
-
if (task.attempts > 0) {
|
|
1596
|
-
const syntheticTaskResult = { success: false, output: '', tokensUsed: 0, costUsed: 0, durationMs: 0, model: 'none' };
|
|
1597
|
-
const syntheticSpawn = { success: false, output: '', metrics: { tokens: 0, duration: 0, toolCalls: 0 } };
|
|
1598
|
-
if (await this.tryResilienceRecovery(task, task.id, syntheticTaskResult, syntheticSpawn)) {
|
|
1599
|
-
this.errors[this.errors.length - 1].recovered = true;
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
this.taskQueue.markFailedWithoutCascade(task.id, 0);
|
|
1604
|
-
this.taskQueue.triggerCascadeSkip(task.id);
|
|
1605
|
-
this.emit({
|
|
1606
|
-
type: 'swarm.task.failed',
|
|
1607
|
-
taskId: task.id,
|
|
1608
|
-
error: errorMsg,
|
|
1609
|
-
attempt: task.attempts,
|
|
1610
|
-
maxAttempts: 1 + this.config.workerRetries,
|
|
1611
|
-
willRetry: false,
|
|
1612
|
-
failureMode: 'error',
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
/**
|
|
1617
|
-
* Handle a completed task: quality gate, bookkeeping, retry logic, model health, failover.
|
|
1618
|
-
*/
|
|
1619
|
-
async handleTaskCompletion(taskId, spawnResult, startedAt) {
|
|
1620
|
-
const task = this.taskQueue.getTask(taskId);
|
|
1621
|
-
if (!task)
|
|
1622
|
-
return;
|
|
1623
|
-
// Guard: task was terminally resolved while its worker was running — ignore the result
|
|
1624
|
-
// F4: But NOT if pendingCascadeSkip — those results are evaluated below
|
|
1625
|
-
if ((task.status === 'skipped' || task.status === 'failed') && !task.pendingCascadeSkip)
|
|
1626
|
-
return;
|
|
1627
|
-
// V7: Global dispatch cap — prevent any single task from burning budget.
|
|
1628
|
-
// Try resilience recovery (micro-decompose, degraded acceptance) before hard-failing.
|
|
1629
|
-
const maxDispatches = this.config.maxDispatchesPerTask ?? 5;
|
|
1630
|
-
if (task.attempts >= maxDispatches) {
|
|
1631
|
-
const durationMs = Date.now() - startedAt;
|
|
1632
|
-
const taskResult = this.workerPool.toTaskResult(spawnResult, task, durationMs);
|
|
1633
|
-
this.totalTokens += taskResult.tokensUsed;
|
|
1634
|
-
this.totalCost += taskResult.costUsed;
|
|
1635
|
-
// Try resilience recovery before hard fail
|
|
1636
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
this.taskQueue.markFailedWithoutCascade(taskId, 0);
|
|
1640
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
1641
|
-
this.emit({
|
|
1642
|
-
type: 'swarm.task.failed',
|
|
1643
|
-
taskId,
|
|
1644
|
-
error: `Dispatch cap reached (${maxDispatches} attempts)`,
|
|
1645
|
-
attempt: task.attempts,
|
|
1646
|
-
maxAttempts: maxDispatches,
|
|
1647
|
-
willRetry: false,
|
|
1648
|
-
failureMode: task.failureMode,
|
|
1649
|
-
});
|
|
1650
|
-
this.logDecision('dispatch-cap', `${taskId}: hard cap reached (${task.attempts}/${maxDispatches})`, 'No more retries — resilience recovery also failed');
|
|
1651
|
-
return;
|
|
1652
|
-
}
|
|
1653
|
-
const durationMs = Date.now() - startedAt;
|
|
1654
|
-
const taskResult = this.workerPool.toTaskResult(spawnResult, task, durationMs);
|
|
1655
|
-
// Track model usage
|
|
1656
|
-
const model = task.assignedModel ?? 'unknown';
|
|
1657
|
-
const usage = this.modelUsage.get(model) ?? { tasks: 0, tokens: 0, cost: 0 };
|
|
1658
|
-
usage.tasks++;
|
|
1659
|
-
usage.tokens += taskResult.tokensUsed;
|
|
1660
|
-
usage.cost += taskResult.costUsed;
|
|
1661
|
-
this.modelUsage.set(model, usage);
|
|
1662
|
-
this.totalTokens += taskResult.tokensUsed;
|
|
1663
|
-
this.totalCost += taskResult.costUsed;
|
|
1664
|
-
// Log per-worker budget utilization for orchestrator visibility
|
|
1665
|
-
if (taskResult.budgetUtilization) {
|
|
1666
|
-
this.logDecision('budget-utilization', `${taskId}: token ${taskResult.budgetUtilization.tokenPercent}%, iter ${taskResult.budgetUtilization.iterationPercent}%`, `model=${model}, tokens=${taskResult.tokensUsed}, duration=${durationMs}ms`);
|
|
1667
|
-
}
|
|
1668
|
-
// V10: Emit per-attempt event for full decision traceability
|
|
1669
|
-
this.emit({
|
|
1670
|
-
type: 'swarm.task.attempt',
|
|
1671
|
-
taskId,
|
|
1672
|
-
attempt: task.attempts,
|
|
1673
|
-
model,
|
|
1674
|
-
success: spawnResult.success,
|
|
1675
|
-
durationMs,
|
|
1676
|
-
toolCalls: spawnResult.metrics.toolCalls ?? 0,
|
|
1677
|
-
failureMode: !spawnResult.success ? task.failureMode : undefined,
|
|
1678
|
-
qualityScore: taskResult.qualityScore,
|
|
1679
|
-
output: taskResult.output.slice(0, 500),
|
|
1680
|
-
});
|
|
1681
|
-
if (!spawnResult.success) {
|
|
1682
|
-
// V2: Record model health
|
|
1683
|
-
const failure = classifySwarmFailure(spawnResult.output, spawnResult.metrics.toolCalls);
|
|
1684
|
-
const { failureClass, retryable, errorType, failureMode, reason } = failure;
|
|
1685
|
-
const isTimeout = failureMode === 'timeout';
|
|
1686
|
-
const isRateLimited = failureClass === 'rate_limited';
|
|
1687
|
-
const isSpendLimit = failureClass === 'provider_spend_limit';
|
|
1688
|
-
const isNonRetryable = !retryable;
|
|
1689
|
-
this.healthTracker.recordFailure(model, errorType);
|
|
1690
|
-
this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
|
|
1691
|
-
// P6: Tag failure mode for cascade threshold awareness
|
|
1692
|
-
task.failureMode = failureMode;
|
|
1693
|
-
// Feed circuit breaker only for retryable rate limiting
|
|
1694
|
-
if (isRateLimited) {
|
|
1695
|
-
this.recordRateLimit();
|
|
1696
|
-
}
|
|
1697
|
-
// F25a: Consecutive timeout tracking — early-fail after N consecutive timeouts
|
|
1698
|
-
if (isTimeout) {
|
|
1699
|
-
const count = (this.taskTimeoutCounts.get(taskId) ?? 0) + 1;
|
|
1700
|
-
this.taskTimeoutCounts.set(taskId, count);
|
|
1701
|
-
const timeoutLimit = this.config.consecutiveTimeoutLimit ?? 3;
|
|
1702
|
-
this.logDecision('timeout-tracking', `${taskId}: consecutive timeout ${count}/${timeoutLimit}`, '');
|
|
1703
|
-
if (count >= timeoutLimit) {
|
|
1704
|
-
// F25b: Try model failover before giving up
|
|
1705
|
-
let failoverSucceeded = false;
|
|
1706
|
-
if (this.config.enableModelFailover) {
|
|
1707
|
-
const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
|
|
1708
|
-
const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
|
|
1709
|
-
if (alternative) {
|
|
1710
|
-
this.emit({
|
|
1711
|
-
type: 'swarm.model.failover',
|
|
1712
|
-
taskId,
|
|
1713
|
-
fromModel: model,
|
|
1714
|
-
toModel: alternative.model,
|
|
1715
|
-
reason: 'consecutive-timeouts',
|
|
1716
|
-
});
|
|
1717
|
-
task.assignedModel = alternative.model;
|
|
1718
|
-
this.taskTimeoutCounts.set(taskId, 0); // Reset counter for new model
|
|
1719
|
-
this.logDecision('failover', `Timeout failover ${taskId}: ${model} → ${alternative.model}`, `${count} consecutive timeouts`);
|
|
1720
|
-
failoverSucceeded = true;
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
if (!failoverSucceeded) {
|
|
1724
|
-
// No alternative model — try resilience recovery before hard fail.
|
|
1725
|
-
// Timeouts often produce artifacts (worker WAS working, just ran out of time).
|
|
1726
|
-
task.failureMode = 'timeout';
|
|
1727
|
-
const taskResult = this.workerPool.toTaskResult(spawnResult, task, Date.now() - startedAt);
|
|
1728
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
1729
|
-
this.taskTimeoutCounts.delete(taskId);
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
this.taskQueue.markFailedWithoutCascade(taskId, 0);
|
|
1733
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
1734
|
-
this.emit({
|
|
1735
|
-
type: 'swarm.task.failed',
|
|
1736
|
-
taskId,
|
|
1737
|
-
error: `${count} consecutive timeouts — no alternative model available`,
|
|
1738
|
-
attempt: task.attempts,
|
|
1739
|
-
maxAttempts: maxDispatches,
|
|
1740
|
-
willRetry: false,
|
|
1741
|
-
failureMode: 'timeout',
|
|
1742
|
-
failureClass: 'timeout',
|
|
1743
|
-
retrySuppressed: true,
|
|
1744
|
-
retryReason: 'Consecutive timeout limit reached with no alternative model',
|
|
1745
|
-
});
|
|
1746
|
-
this.logDecision('timeout-early-fail', `${taskId}: ${count} consecutive timeouts, no alt model — resilience recovery also failed`, '');
|
|
1747
|
-
this.taskTimeoutCounts.delete(taskId);
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
else {
|
|
1753
|
-
// Non-timeout failure — reset the counter
|
|
1754
|
-
this.taskTimeoutCounts.delete(taskId);
|
|
1755
|
-
}
|
|
1756
|
-
// V2: Model failover on retryable rate limits
|
|
1757
|
-
if (isRateLimited && this.config.enableModelFailover) {
|
|
1758
|
-
const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
|
|
1759
|
-
const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
|
|
1760
|
-
if (alternative) {
|
|
1761
|
-
this.emit({
|
|
1762
|
-
type: 'swarm.model.failover',
|
|
1763
|
-
taskId,
|
|
1764
|
-
fromModel: model,
|
|
1765
|
-
toModel: alternative.model,
|
|
1766
|
-
reason: errorType,
|
|
1767
|
-
});
|
|
1768
|
-
task.assignedModel = alternative.model;
|
|
1769
|
-
this.logDecision('failover', `Switched ${taskId} from ${model} to ${alternative.model}`, `${errorType} error`);
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
// V5/V7: Store error context so retry gets different prompt
|
|
1773
|
-
if (!(isRateLimited || isSpendLimit)) {
|
|
1774
|
-
// V7: Timeout-specific feedback — the worker WAS working, just ran out of time
|
|
1775
|
-
const timeoutSeconds = isTimeout ? Math.round(durationMs / 1000) : 0;
|
|
1776
|
-
task.retryContext = {
|
|
1777
|
-
previousFeedback: isTimeout
|
|
1778
|
-
? `Previous attempt timed out after ${timeoutSeconds}s. You must complete this task more efficiently — work faster, use fewer tool calls, and produce your result sooner.`
|
|
1779
|
-
: spawnResult.output.slice(0, 2000),
|
|
1780
|
-
previousScore: 0,
|
|
1781
|
-
attempt: task.attempts,
|
|
1782
|
-
previousModel: model,
|
|
1783
|
-
previousFiles: taskResult.filesModified,
|
|
1784
|
-
swarmProgress: this.getSwarmProgressSummary(),
|
|
1785
|
-
};
|
|
1786
|
-
// Phase 3.1: Report failure to shared context engine for cross-worker learning
|
|
1787
|
-
this.sharedContextEngine.reportFailure(taskId, {
|
|
1788
|
-
action: task.description.slice(0, 200),
|
|
1789
|
-
error: spawnResult.output.slice(0, 500),
|
|
1790
|
-
});
|
|
1791
|
-
}
|
|
1792
|
-
// V7: Reset hollow streak on non-hollow failure (error is not a hollow completion)
|
|
1793
|
-
this.hollowStreak = 0;
|
|
1794
|
-
// Worker failed — use higher retry limit for rate limit errors.
|
|
1795
|
-
// V7: Fixup tasks get capped retries, foundation tasks get +1.
|
|
1796
|
-
const baseRetries = this.getEffectiveRetries(task);
|
|
1797
|
-
const retryLimit = isNonRetryable
|
|
1798
|
-
? 0
|
|
1799
|
-
: isRateLimited
|
|
1800
|
-
? Math.min(this.config.rateLimitRetries ?? 3, baseRetries + 1)
|
|
1801
|
-
: baseRetries;
|
|
1802
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, retryLimit);
|
|
1803
|
-
if (isNonRetryable) {
|
|
1804
|
-
this.logDecision('retry-suppressed', `${taskId}: ${failureClass}`, reason);
|
|
1805
|
-
}
|
|
1806
|
-
if (canRetry) {
|
|
1807
|
-
this.retries++;
|
|
1808
|
-
// Non-blocking cooldown: set retryAfter timestamp instead of blocking
|
|
1809
|
-
if (isRateLimited) {
|
|
1810
|
-
const baseDelay = this.config.retryBaseDelayMs ?? 5000;
|
|
1811
|
-
const cooldownMs = Math.min(baseDelay * Math.pow(2, task.attempts - 1), 30000);
|
|
1812
|
-
this.taskQueue.setRetryAfter(taskId, cooldownMs);
|
|
1813
|
-
this.logDecision('rate-limit-cooldown', `${taskId}: ${errorType} cooldown ${cooldownMs}ms, model ${model}`, '');
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
else if (!isRateLimited) {
|
|
1817
|
-
// Resilience recovery for non-rate-limit errors (micro-decompose + degraded acceptance)
|
|
1818
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
// Recovery failed — NOW trigger cascade
|
|
1822
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
1823
|
-
}
|
|
1824
|
-
else {
|
|
1825
|
-
// Rate-limit exhaustion — trigger cascade
|
|
1826
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
1827
|
-
}
|
|
1828
|
-
this.emit({
|
|
1829
|
-
type: 'swarm.task.failed',
|
|
1830
|
-
taskId,
|
|
1831
|
-
error: spawnResult.output.slice(0, 200),
|
|
1832
|
-
attempt: task.attempts,
|
|
1833
|
-
maxAttempts: 1 + this.config.workerRetries,
|
|
1834
|
-
willRetry: canRetry,
|
|
1835
|
-
toolCalls: spawnResult.metrics.toolCalls,
|
|
1836
|
-
failoverModel: task.assignedModel !== model ? task.assignedModel : undefined,
|
|
1837
|
-
failureMode: task.failureMode,
|
|
1838
|
-
failureClass,
|
|
1839
|
-
retrySuppressed: isNonRetryable,
|
|
1840
|
-
retryReason: reason,
|
|
1841
|
-
});
|
|
1842
|
-
return;
|
|
1843
|
-
}
|
|
1844
|
-
// V6: Hollow completion detection — workers that "succeed" without doing any work
|
|
1845
|
-
// Must check BEFORE recording success, otherwise hollow completions inflate health scores
|
|
1846
|
-
if (isHollowCompletion(spawnResult, task.type, this.config)) {
|
|
1847
|
-
// F4: Hollow result + pendingCascadeSkip — honor the skip immediately, no retry
|
|
1848
|
-
if (task.pendingCascadeSkip) {
|
|
1849
|
-
task.pendingCascadeSkip = undefined;
|
|
1850
|
-
task.status = 'skipped';
|
|
1851
|
-
this.totalHollows++;
|
|
1852
|
-
this.logDecision('cascade-skip', `${taskId}: pending cascade skip honored (hollow completion)`, '');
|
|
1853
|
-
this.emit({ type: 'swarm.task.skipped', taskId, reason: 'cascade skip honored — hollow completion' });
|
|
1854
|
-
return;
|
|
1855
|
-
}
|
|
1856
|
-
// P6: Tag failure mode for cascade threshold awareness
|
|
1857
|
-
task.failureMode = 'hollow';
|
|
1858
|
-
// Record hollow completion so hollow-prone models accumulate hollow-specific records
|
|
1859
|
-
// and get deprioritized by the model selector (also records generic failure internally)
|
|
1860
|
-
this.healthTracker.recordHollow(model);
|
|
1861
|
-
const admitsFailure = spawnResult.success && FAILURE_INDICATORS.some(f => (spawnResult.output ?? '').toLowerCase().includes(f));
|
|
1862
|
-
task.retryContext = {
|
|
1863
|
-
previousFeedback: admitsFailure
|
|
1864
|
-
? 'Previous attempt reported success but admitted failure (e.g., "budget exhausted", "unable to complete"). You MUST execute tool calls and produce concrete output this time.'
|
|
1865
|
-
: 'Previous attempt produced no meaningful output. Try again with a concrete approach.',
|
|
1866
|
-
previousScore: 1,
|
|
1867
|
-
attempt: task.attempts,
|
|
1868
|
-
previousModel: model,
|
|
1869
|
-
previousFiles: taskResult.filesModified,
|
|
1870
|
-
swarmProgress: this.getSwarmProgressSummary(),
|
|
1871
|
-
};
|
|
1872
|
-
// Phase 3.1: Report hollow completion to shared context engine
|
|
1873
|
-
this.sharedContextEngine.reportFailure(taskId, {
|
|
1874
|
-
action: task.description.slice(0, 200),
|
|
1875
|
-
error: 'Hollow completion: worker produced no meaningful output',
|
|
1876
|
-
});
|
|
1877
|
-
// Model failover for hollow completions — same pattern as quality failover
|
|
1878
|
-
if (this.config.enableModelFailover) {
|
|
1879
|
-
const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
|
|
1880
|
-
const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
|
|
1881
|
-
if (alternative) {
|
|
1882
|
-
this.emit({
|
|
1883
|
-
type: 'swarm.model.failover',
|
|
1884
|
-
taskId,
|
|
1885
|
-
fromModel: model,
|
|
1886
|
-
toModel: alternative.model,
|
|
1887
|
-
reason: 'hollow-completion',
|
|
1888
|
-
});
|
|
1889
|
-
task.assignedModel = alternative.model;
|
|
1890
|
-
this.logDecision('failover', `Hollow failover ${taskId}: ${model} → ${alternative.model}`, 'Model produced hollow completion');
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
const hollowRetries = this.getEffectiveRetries(task);
|
|
1894
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, hollowRetries);
|
|
1895
|
-
if (canRetry) {
|
|
1896
|
-
this.retries++;
|
|
1897
|
-
}
|
|
1898
|
-
else {
|
|
1899
|
-
// Retries exhausted — try shared resilience recovery (micro-decompose, degraded acceptance)
|
|
1900
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
// Recovery failed — NOW trigger cascade
|
|
1904
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
1905
|
-
}
|
|
1906
|
-
this.emit({
|
|
1907
|
-
type: 'swarm.task.failed',
|
|
1908
|
-
taskId,
|
|
1909
|
-
error: 'Hollow completion: worker used no tools',
|
|
1910
|
-
attempt: task.attempts,
|
|
1911
|
-
maxAttempts: 1 + this.config.workerRetries,
|
|
1912
|
-
willRetry: canRetry,
|
|
1913
|
-
toolCalls: spawnResult.metrics.toolCalls,
|
|
1914
|
-
failoverModel: task.assignedModel !== model ? task.assignedModel : undefined,
|
|
1915
|
-
failureMode: 'hollow',
|
|
1916
|
-
});
|
|
1917
|
-
this.hollowStreak++;
|
|
1918
|
-
this.totalHollows++;
|
|
1919
|
-
this.logDecision('hollow-completion', `${taskId}: worker completed with 0 tool calls (streak: ${this.hollowStreak}, total hollows: ${this.totalHollows}/${this.totalDispatches})`, canRetry ? 'Marking as failed for retry' : 'Retries exhausted — hard fail');
|
|
1920
|
-
// B2: Hollow streak handling — only terminate if enableHollowTermination is explicitly on
|
|
1921
|
-
if (this.hollowStreak >= SwarmOrchestrator.HOLLOW_STREAK_THRESHOLD) {
|
|
1922
|
-
const uniqueModels = new Set(this.config.workers.map(w => w.model));
|
|
1923
|
-
const singleModel = uniqueModels.size === 1;
|
|
1924
|
-
const onlyModel = [...uniqueModels][0];
|
|
1925
|
-
const modelUnhealthy = singleModel && !this.healthTracker.getAllRecords().find(r => r.model === onlyModel)?.healthy;
|
|
1926
|
-
if (singleModel && modelUnhealthy) {
|
|
1927
|
-
if (this.config.enableHollowTermination) {
|
|
1928
|
-
this.logDecision('early-termination', `Terminating swarm: ${this.hollowStreak} consecutive hollow completions on sole model ${onlyModel}`, 'Single-model swarm with unhealthy model — enableHollowTermination is on');
|
|
1929
|
-
this.skipRemainingTasks(`Single-model hollow streak (${this.hollowStreak}x on ${onlyModel})`);
|
|
1930
|
-
}
|
|
1931
|
-
else {
|
|
1932
|
-
this.logDecision('stall-mode', `${this.hollowStreak} consecutive hollows on sole model ${onlyModel} — entering stall mode`, 'Will attempt model failover or simplified retry on next dispatch');
|
|
1933
|
-
// Reset streak to allow more attempts with adjusted strategy
|
|
1934
|
-
this.hollowStreak = 0;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
// V7: Multi-model hollow ratio — warn but don't terminate unless opt-in
|
|
1939
|
-
const minDispatches = this.config.hollowTerminationMinDispatches ?? 8;
|
|
1940
|
-
const threshold = this.config.hollowTerminationRatio ?? 0.55;
|
|
1941
|
-
if (this.totalDispatches >= minDispatches) {
|
|
1942
|
-
const ratio = this.totalHollows / this.totalDispatches;
|
|
1943
|
-
if (ratio > threshold) {
|
|
1944
|
-
if (this.config.enableHollowTermination) {
|
|
1945
|
-
this.logDecision('early-termination', `Terminating swarm: hollow ratio ${(ratio * 100).toFixed(0)}% (${this.totalHollows}/${this.totalDispatches})`, `Exceeds threshold ${(threshold * 100).toFixed(0)}% after ${minDispatches}+ dispatches — enableHollowTermination is on`);
|
|
1946
|
-
this.skipRemainingTasks(`Hollow ratio ${(ratio * 100).toFixed(0)}% — models cannot execute tasks`);
|
|
1947
|
-
}
|
|
1948
|
-
else if (!this.hollowRatioWarned) {
|
|
1949
|
-
this.hollowRatioWarned = true;
|
|
1950
|
-
this.logDecision('stall-warning', `Hollow ratio ${(ratio * 100).toFixed(0)}% (${this.totalHollows}/${this.totalDispatches})`, 'High hollow rate but continuing — tasks may still recover via resilience');
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
// F4: Task had pendingCascadeSkip but produced non-hollow results.
|
|
1957
|
-
// Run pre-flight checks — if the output is good, accept it instead of skipping.
|
|
1958
|
-
if (task.pendingCascadeSkip) {
|
|
1959
|
-
const cachedReport = checkArtifacts(task);
|
|
1960
|
-
const preFlight = runPreFlightChecks(task, taskResult, this.config, cachedReport);
|
|
1961
|
-
if (preFlight && !preFlight.passed) {
|
|
1962
|
-
// Output is garbage — honor the cascade skip
|
|
1963
|
-
task.pendingCascadeSkip = undefined;
|
|
1964
|
-
task.status = 'skipped';
|
|
1965
|
-
this.logDecision('cascade-skip', `${taskId}: pending cascade skip honored (pre-flight failed: ${preFlight.feedback})`, '');
|
|
1966
|
-
this.emit({ type: 'swarm.task.skipped', taskId, reason: `cascade skip honored — output failed pre-flight: ${preFlight.feedback}` });
|
|
1967
|
-
return;
|
|
1968
|
-
}
|
|
1969
|
-
// Output is good — clear the flag and accept the result
|
|
1970
|
-
task.pendingCascadeSkip = undefined;
|
|
1971
|
-
task.status = 'dispatched'; // Reset so markCompleted works
|
|
1972
|
-
this.logDecision('cascade-skip', `${taskId}: pending cascade skip overridden — worker produced valid output`, '');
|
|
1973
|
-
}
|
|
1974
|
-
// Record model health on success (only for non-hollow completions)
|
|
1975
|
-
this.healthTracker.recordSuccess(model, durationMs);
|
|
1976
|
-
this.decreaseStagger(); // P7: Speed up on success
|
|
1977
|
-
// Run quality gate if enabled — skip under API pressure, skip if circuit breaker tripped,
|
|
1978
|
-
// and let the final attempt through without quality gate (so tasks produce *something*)
|
|
1979
|
-
// Foundation tasks get +1 retry to reduce cascade failure risk.
|
|
1980
|
-
const effectiveRetries = this.getEffectiveRetries(task);
|
|
1981
|
-
const recentRLCount = this.recentRateLimits.filter(t => t > Date.now() - 30_000).length;
|
|
1982
|
-
const isLastAttempt = task.attempts >= (effectiveRetries + 1);
|
|
1983
|
-
const shouldRunQualityGate = this.config.qualityGates
|
|
1984
|
-
&& !this.qualityGateDisabledModels.has(model)
|
|
1985
|
-
&& !isLastAttempt
|
|
1986
|
-
&& Date.now() >= this.circuitBreakerUntil
|
|
1987
|
-
&& recentRLCount < 2;
|
|
1988
|
-
// C1: Pre-compute artifact report once — shared by quality gate and pre-flight checks
|
|
1989
|
-
const cachedArtifactReport = checkArtifacts(task);
|
|
1990
|
-
if (shouldRunQualityGate) {
|
|
1991
|
-
// V3: Judge role handles quality gates
|
|
1992
|
-
const judgeModel = this.config.hierarchy?.judge?.model
|
|
1993
|
-
?? this.config.qualityGateModel ?? this.config.orchestratorModel;
|
|
1994
|
-
const judgeConfig = {
|
|
1995
|
-
model: judgeModel,
|
|
1996
|
-
persona: this.config.hierarchy?.judge?.persona,
|
|
1997
|
-
};
|
|
1998
|
-
this.emit({ type: 'swarm.role.action', role: 'judge', action: 'quality-gate', model: judgeModel, taskId });
|
|
1999
|
-
// Extract file artifacts from worker output for quality gate visibility.
|
|
2000
|
-
// When workers create files via write_file/edit_file, the judge needs to see
|
|
2001
|
-
// the actual content — not just the worker's text claims about what was created.
|
|
2002
|
-
const fileArtifacts = this.extractFileArtifacts(task, taskResult);
|
|
2003
|
-
// Foundation tasks get a relaxed quality threshold (threshold - 1, min 2)
|
|
2004
|
-
// to reduce the chance of cascade-skipping the entire swarm.
|
|
2005
|
-
const baseThreshold = this.config.qualityThreshold ?? 3;
|
|
2006
|
-
const qualityThreshold = task.isFoundation ? Math.max(2, baseThreshold - 1) : baseThreshold;
|
|
2007
|
-
const quality = await evaluateWorkerOutput(this.provider, judgeModel, task, taskResult, judgeConfig, qualityThreshold, (resp, purpose) => this.trackOrchestratorUsage(resp, purpose), fileArtifacts, this.config, cachedArtifactReport);
|
|
2008
|
-
taskResult.qualityScore = quality.score;
|
|
2009
|
-
taskResult.qualityFeedback = quality.feedback;
|
|
2010
|
-
// F11: Foundation tasks that barely pass the relaxed threshold get concrete validation.
|
|
2011
|
-
// A 2/5 foundation task with truncated output will cascade-poison all dependents.
|
|
2012
|
-
if (quality.passed && task.isFoundation && quality.score <= baseThreshold - 1) {
|
|
2013
|
-
const concreteResult = runConcreteChecks(task, taskResult);
|
|
2014
|
-
if (!concreteResult.passed) {
|
|
2015
|
-
quality.passed = false;
|
|
2016
|
-
quality.feedback += ` [F11: foundation task barely passed (${quality.score}/${baseThreshold}) but concrete validation failed: ${concreteResult.issues.join('; ')}]`;
|
|
2017
|
-
this.logDecision('foundation-concrete-gate', `${taskId}: foundation task scored ${quality.score} (relaxed threshold ${qualityThreshold}) but concrete checks failed — rejecting`, concreteResult.issues.join('; '));
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
if (!quality.passed) {
|
|
2021
|
-
// F7: Gate error fallback — when LLM judge fails, use concrete validation
|
|
2022
|
-
// If concrete checks pass, tentatively accept the result instead of rejecting.
|
|
2023
|
-
if (quality.gateError && (this.config.enableConcreteValidation !== false)) {
|
|
2024
|
-
const concreteResult = runConcreteChecks(task, taskResult);
|
|
2025
|
-
if (concreteResult.passed) {
|
|
2026
|
-
// Concrete validation passed — tentatively accept despite gate error
|
|
2027
|
-
this.logDecision('gate-error-fallback', `${taskId}: gate error but concrete checks passed — tentatively accepting`, quality.gateErrorMessage ?? 'unknown');
|
|
2028
|
-
taskResult.qualityScore = quality.score;
|
|
2029
|
-
taskResult.qualityFeedback = `${quality.feedback} [concrete validation passed — tentative accept]`;
|
|
2030
|
-
// Fall through to success path (don't return)
|
|
2031
|
-
}
|
|
2032
|
-
else {
|
|
2033
|
-
// Both gate and concrete failed — reject
|
|
2034
|
-
this.logDecision('gate-error-fallback', `${taskId}: gate error AND concrete checks failed — rejecting`, `Concrete issues: ${concreteResult.issues.join('; ')}`);
|
|
2035
|
-
// Fall through to normal rejection below
|
|
2036
|
-
}
|
|
2037
|
-
// If concrete passed, skip the rejection path
|
|
2038
|
-
if (concreteResult.passed) {
|
|
2039
|
-
this.perModelQualityRejections.delete(model);
|
|
2040
|
-
// Jump to success path below
|
|
2041
|
-
}
|
|
2042
|
-
else {
|
|
2043
|
-
// Proceed with normal rejection
|
|
2044
|
-
this.qualityRejections++;
|
|
2045
|
-
task.failureMode = 'quality';
|
|
2046
|
-
this.healthTracker.recordQualityRejection(model, quality.score);
|
|
2047
|
-
this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
|
|
2048
|
-
this.hollowStreak = 0;
|
|
2049
|
-
task.retryContext = {
|
|
2050
|
-
previousFeedback: `Gate error + concrete validation failed: ${concreteResult.issues.join('; ')}`,
|
|
2051
|
-
previousScore: quality.score,
|
|
2052
|
-
attempt: task.attempts,
|
|
2053
|
-
previousModel: model,
|
|
2054
|
-
previousFiles: taskResult.filesModified,
|
|
2055
|
-
swarmProgress: this.getSwarmProgressSummary(),
|
|
2056
|
-
};
|
|
2057
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2058
|
-
if (canRetry) {
|
|
2059
|
-
this.retries++;
|
|
2060
|
-
}
|
|
2061
|
-
else {
|
|
2062
|
-
// Retries exhausted — try resilience recovery before cascade-skip
|
|
2063
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
// Recovery failed — NOW trigger cascade
|
|
2067
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2068
|
-
}
|
|
2069
|
-
this.emit({
|
|
2070
|
-
type: 'swarm.quality.rejected',
|
|
2071
|
-
taskId,
|
|
2072
|
-
score: quality.score,
|
|
2073
|
-
feedback: quality.feedback,
|
|
2074
|
-
artifactCount: fileArtifacts.length,
|
|
2075
|
-
outputLength: taskResult.output.length,
|
|
2076
|
-
preFlightReject: false,
|
|
2077
|
-
filesOnDisk: checkArtifactsEnhanced(task, taskResult).files.filter(f => f.exists && f.sizeBytes > 0).length,
|
|
2078
|
-
});
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
else if (!quality.gateError) {
|
|
2083
|
-
// Normal quality rejection (LLM judge rejected, no gate error)
|
|
2084
|
-
this.qualityRejections++;
|
|
2085
|
-
// P6: Tag failure mode for cascade threshold awareness
|
|
2086
|
-
task.failureMode = 'quality';
|
|
2087
|
-
// P1: Quality rejections update model health — undo premature recordSuccess
|
|
2088
|
-
this.healthTracker.recordQualityRejection(model, quality.score);
|
|
2089
|
-
this.emit({ type: 'swarm.model.health', record: { model, ...this.getModelHealthSummary(model) } });
|
|
2090
|
-
// V7: Quality rejection is NOT hollow — worker did work, just poorly
|
|
2091
|
-
this.hollowStreak = 0;
|
|
2092
|
-
// F7: Per-model circuit breaker → "pre-flight only mode" instead of fully disabling gates.
|
|
2093
|
-
// After threshold rejections, skip LLM judge but keep pre-flight mandatory.
|
|
2094
|
-
if (!quality.preFlightReject) {
|
|
2095
|
-
const modelRejections = (this.perModelQualityRejections.get(model) ?? 0) + 1;
|
|
2096
|
-
this.perModelQualityRejections.set(model, modelRejections);
|
|
2097
|
-
if (modelRejections >= SwarmOrchestrator.QUALITY_CIRCUIT_BREAKER_THRESHOLD) {
|
|
2098
|
-
this.qualityGateDisabledModels.add(model);
|
|
2099
|
-
this.logDecision('quality-circuit-breaker', `Switched model ${model} to pre-flight-only mode after ${modelRejections} rejections`, 'Skipping LLM judge but keeping pre-flight checks mandatory');
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
// V5: Attach feedback so retry prompt includes it
|
|
2103
|
-
task.retryContext = {
|
|
2104
|
-
previousFeedback: quality.feedback,
|
|
2105
|
-
previousScore: quality.score,
|
|
2106
|
-
attempt: task.attempts,
|
|
2107
|
-
previousModel: model,
|
|
2108
|
-
previousFiles: taskResult.filesModified,
|
|
2109
|
-
swarmProgress: this.getSwarmProgressSummary(),
|
|
2110
|
-
};
|
|
2111
|
-
// Phase 3.1: Report quality rejection to shared context engine
|
|
2112
|
-
this.sharedContextEngine.reportFailure(taskId, {
|
|
2113
|
-
action: task.description.slice(0, 200),
|
|
2114
|
-
error: `Quality gate rejection (score ${quality.score}): ${quality.feedback.slice(0, 300)}`,
|
|
2115
|
-
});
|
|
2116
|
-
// V5: Model failover on quality rejection — but NOT on artifact auto-fails
|
|
2117
|
-
// P1: Widened from score<=1 to score<threshold so failover triggers on any rejection
|
|
2118
|
-
if (quality.score < qualityThreshold && this.config.enableModelFailover && !quality.artifactAutoFail) {
|
|
2119
|
-
const capability = getTaskTypeConfig(task.type, this.config).capability ?? 'code';
|
|
2120
|
-
const alternative = selectAlternativeModel(this.config.workers, model, capability, this.healthTracker);
|
|
2121
|
-
if (alternative) {
|
|
2122
|
-
this.emit({
|
|
2123
|
-
type: 'swarm.model.failover',
|
|
2124
|
-
taskId,
|
|
2125
|
-
fromModel: model,
|
|
2126
|
-
toModel: alternative.model,
|
|
2127
|
-
reason: `quality-score-${quality.score}`,
|
|
2128
|
-
});
|
|
2129
|
-
task.assignedModel = alternative.model;
|
|
2130
|
-
this.logDecision('failover', `Quality failover ${taskId}: ${model} → ${alternative.model}`, `Score ${quality.score}/5`);
|
|
2131
|
-
}
|
|
2132
|
-
}
|
|
2133
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2134
|
-
if (canRetry) {
|
|
2135
|
-
this.retries++;
|
|
2136
|
-
}
|
|
2137
|
-
else {
|
|
2138
|
-
// Retries exhausted — try resilience recovery before cascade-skip
|
|
2139
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2140
|
-
return;
|
|
2141
|
-
}
|
|
2142
|
-
// Recovery failed — NOW trigger cascade
|
|
2143
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2144
|
-
}
|
|
2145
|
-
// M1: Only emit quality.rejected (not duplicate task.failed)
|
|
2146
|
-
this.emit({
|
|
2147
|
-
type: 'swarm.quality.rejected',
|
|
2148
|
-
taskId,
|
|
2149
|
-
score: quality.score,
|
|
2150
|
-
feedback: quality.feedback,
|
|
2151
|
-
artifactCount: fileArtifacts.length,
|
|
2152
|
-
outputLength: taskResult.output.length,
|
|
2153
|
-
preFlightReject: quality.preFlightReject,
|
|
2154
|
-
filesOnDisk: checkArtifactsEnhanced(task, taskResult).files.filter(f => f.exists && f.sizeBytes > 0).length,
|
|
2155
|
-
});
|
|
2156
|
-
return;
|
|
2157
|
-
}
|
|
2158
|
-
else {
|
|
2159
|
-
// gateError=true but concrete validation disabled — reject
|
|
2160
|
-
this.qualityRejections++;
|
|
2161
|
-
task.failureMode = 'quality';
|
|
2162
|
-
this.hollowStreak = 0;
|
|
2163
|
-
task.retryContext = {
|
|
2164
|
-
previousFeedback: quality.feedback,
|
|
2165
|
-
previousScore: quality.score,
|
|
2166
|
-
attempt: task.attempts,
|
|
2167
|
-
previousModel: model,
|
|
2168
|
-
previousFiles: taskResult.filesModified,
|
|
2169
|
-
swarmProgress: this.getSwarmProgressSummary(),
|
|
2170
|
-
};
|
|
2171
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2172
|
-
if (canRetry) {
|
|
2173
|
-
this.retries++;
|
|
2174
|
-
}
|
|
2175
|
-
else {
|
|
2176
|
-
// Retries exhausted — try resilience recovery before cascade-skip
|
|
2177
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2178
|
-
return;
|
|
2179
|
-
}
|
|
2180
|
-
// Recovery failed — NOW trigger cascade
|
|
2181
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2182
|
-
}
|
|
2183
|
-
this.emit({
|
|
2184
|
-
type: 'swarm.quality.rejected',
|
|
2185
|
-
taskId,
|
|
2186
|
-
score: quality.score,
|
|
2187
|
-
feedback: quality.feedback,
|
|
2188
|
-
artifactCount: fileArtifacts.length,
|
|
2189
|
-
outputLength: taskResult.output.length,
|
|
2190
|
-
preFlightReject: false,
|
|
2191
|
-
filesOnDisk: checkArtifactsEnhanced(task, taskResult).files.filter(f => f.exists && f.sizeBytes > 0).length,
|
|
2192
|
-
});
|
|
2193
|
-
return;
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
// Quality passed — reset per-model rejection counter
|
|
2197
|
-
this.perModelQualityRejections.delete(model);
|
|
2198
|
-
}
|
|
2199
|
-
// F7: When quality gate was skipped (last attempt, pre-flight-only mode, API pressure),
|
|
2200
|
-
// still run pre-flight + concrete checks so obviously broken outputs don't slip through.
|
|
2201
|
-
// C1: Use cached artifact report to avoid double filesystem scan.
|
|
2202
|
-
if (!shouldRunQualityGate && this.config.qualityGates) {
|
|
2203
|
-
const preFlight = runPreFlightChecks(task, taskResult, this.config, cachedArtifactReport);
|
|
2204
|
-
if (preFlight && !preFlight.passed) {
|
|
2205
|
-
taskResult.qualityScore = preFlight.score;
|
|
2206
|
-
taskResult.qualityFeedback = preFlight.feedback;
|
|
2207
|
-
this.qualityRejections++;
|
|
2208
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2209
|
-
if (canRetry) {
|
|
2210
|
-
this.retries++;
|
|
2211
|
-
}
|
|
2212
|
-
else {
|
|
2213
|
-
// Retries exhausted — try resilience recovery before cascade-skip
|
|
2214
|
-
this.logDecision('preflight-reject', `${taskId}: pre-flight failed: ${preFlight.feedback}`, '');
|
|
2215
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2216
|
-
return;
|
|
2217
|
-
}
|
|
2218
|
-
// Recovery failed — NOW trigger cascade
|
|
2219
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2220
|
-
}
|
|
2221
|
-
this.emit({
|
|
2222
|
-
type: 'swarm.quality.rejected',
|
|
2223
|
-
taskId,
|
|
2224
|
-
score: preFlight.score,
|
|
2225
|
-
feedback: preFlight.feedback,
|
|
2226
|
-
artifactCount: 0,
|
|
2227
|
-
outputLength: taskResult.output.length,
|
|
2228
|
-
preFlightReject: true,
|
|
2229
|
-
});
|
|
2230
|
-
return;
|
|
2231
|
-
}
|
|
2232
|
-
// F2: Run concrete validation when pre-flight passes but gate was skipped
|
|
2233
|
-
if (this.config.enableConcreteValidation !== false) {
|
|
2234
|
-
const concreteResult = runConcreteChecks(task, taskResult);
|
|
2235
|
-
if (!concreteResult.passed) {
|
|
2236
|
-
taskResult.qualityScore = 2;
|
|
2237
|
-
taskResult.qualityFeedback = `Concrete validation failed: ${concreteResult.issues.join('; ')}`;
|
|
2238
|
-
this.qualityRejections++;
|
|
2239
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2240
|
-
if (canRetry) {
|
|
2241
|
-
this.retries++;
|
|
2242
|
-
}
|
|
2243
|
-
else {
|
|
2244
|
-
// Retries exhausted — try resilience recovery before cascade-skip
|
|
2245
|
-
this.logDecision('concrete-reject', `${taskId}: concrete validation failed: ${concreteResult.issues.join('; ')}`, '');
|
|
2246
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2247
|
-
return;
|
|
2248
|
-
}
|
|
2249
|
-
// Recovery failed — NOW trigger cascade
|
|
2250
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2251
|
-
}
|
|
2252
|
-
this.emit({
|
|
2253
|
-
type: 'swarm.quality.rejected',
|
|
2254
|
-
taskId,
|
|
2255
|
-
score: 2,
|
|
2256
|
-
feedback: taskResult.qualityFeedback,
|
|
2257
|
-
artifactCount: 0,
|
|
2258
|
-
outputLength: taskResult.output.length,
|
|
2259
|
-
preFlightReject: false,
|
|
2260
|
-
});
|
|
2261
|
-
return;
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
// Final completion guard: block "narrative success" for action tasks.
|
|
2266
|
-
const completionGuard = this.config.completionGuard ?? {};
|
|
2267
|
-
const rejectFutureIntentOutputs = completionGuard.rejectFutureIntentOutputs ?? true;
|
|
2268
|
-
const requireConcreteArtifactsForActionTasks = completionGuard.requireConcreteArtifactsForActionTasks ?? true;
|
|
2269
|
-
const typeConfig = getTaskTypeConfig(task.type, this.config);
|
|
2270
|
-
const artifactReport = checkArtifactsEnhanced(task, taskResult);
|
|
2271
|
-
const filesOnDisk = artifactReport.files.filter(f => f.exists && f.sizeBytes > 0).length;
|
|
2272
|
-
const hasConcreteArtifacts = filesOnDisk > 0 || (taskResult.filesModified?.length ?? 0) > 0;
|
|
2273
|
-
const isActionTask = !!typeConfig.requiresToolCalls;
|
|
2274
|
-
if (rejectFutureIntentOutputs && hasFutureIntentLanguage(taskResult.output ?? '')) {
|
|
2275
|
-
taskResult.qualityScore = 1;
|
|
2276
|
-
taskResult.qualityFeedback = 'Completion rejected: output indicates pending, unexecuted work';
|
|
2277
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2278
|
-
if (canRetry) {
|
|
2279
|
-
this.retries++;
|
|
2280
|
-
}
|
|
2281
|
-
else {
|
|
2282
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2286
|
-
}
|
|
2287
|
-
this.emit({
|
|
2288
|
-
type: 'swarm.quality.rejected',
|
|
2289
|
-
taskId,
|
|
2290
|
-
score: 1,
|
|
2291
|
-
feedback: taskResult.qualityFeedback,
|
|
2292
|
-
artifactCount: filesOnDisk,
|
|
2293
|
-
outputLength: taskResult.output.length,
|
|
2294
|
-
preFlightReject: true,
|
|
2295
|
-
filesOnDisk,
|
|
2296
|
-
});
|
|
2297
|
-
return;
|
|
2298
|
-
}
|
|
2299
|
-
if (requireConcreteArtifactsForActionTasks && isActionTask && !hasConcreteArtifacts) {
|
|
2300
|
-
taskResult.qualityScore = 1;
|
|
2301
|
-
taskResult.qualityFeedback = 'Completion rejected: action task produced no concrete artifacts';
|
|
2302
|
-
const canRetry = this.taskQueue.markFailedWithoutCascade(taskId, effectiveRetries);
|
|
2303
|
-
if (canRetry) {
|
|
2304
|
-
this.retries++;
|
|
2305
|
-
}
|
|
2306
|
-
else {
|
|
2307
|
-
if (await this.tryResilienceRecovery(task, taskId, taskResult, spawnResult)) {
|
|
2308
|
-
return;
|
|
2309
|
-
}
|
|
2310
|
-
this.taskQueue.triggerCascadeSkip(taskId);
|
|
2311
|
-
}
|
|
2312
|
-
this.emit({
|
|
2313
|
-
type: 'swarm.quality.rejected',
|
|
2314
|
-
taskId,
|
|
2315
|
-
score: 1,
|
|
2316
|
-
feedback: taskResult.qualityFeedback,
|
|
2317
|
-
artifactCount: filesOnDisk,
|
|
2318
|
-
outputLength: taskResult.output.length,
|
|
2319
|
-
preFlightReject: true,
|
|
2320
|
-
filesOnDisk,
|
|
2321
|
-
});
|
|
2322
|
-
return;
|
|
2323
|
-
}
|
|
2324
|
-
// Task passed — mark completed
|
|
2325
|
-
this.taskQueue.markCompleted(taskId, taskResult);
|
|
2326
|
-
this.hollowStreak = 0;
|
|
2327
|
-
// F25: Clear timeout counter on success
|
|
2328
|
-
this.taskTimeoutCounts.delete(taskId);
|
|
2329
|
-
// H6: Post findings to blackboard with error handling
|
|
2330
|
-
if (this.blackboard && taskResult.findings) {
|
|
2331
|
-
try {
|
|
2332
|
-
for (const finding of taskResult.findings) {
|
|
2333
|
-
this.blackboard.post(`swarm-worker-${taskId}`, {
|
|
2334
|
-
topic: `swarm.task.${task.type}`,
|
|
2335
|
-
content: finding,
|
|
2336
|
-
type: 'progress',
|
|
2337
|
-
confidence: (taskResult.qualityScore ?? 3) / 5,
|
|
2338
|
-
tags: ['swarm', task.type],
|
|
2339
|
-
relatedFiles: task.targetFiles,
|
|
2340
|
-
});
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
catch {
|
|
2344
|
-
// Don't crash orchestrator on blackboard failures
|
|
2345
|
-
this.errors.push({
|
|
2346
|
-
taskId,
|
|
2347
|
-
phase: 'execution',
|
|
2348
|
-
message: 'Failed to post findings to blackboard',
|
|
2349
|
-
recovered: true,
|
|
2350
|
-
});
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
this.emit({
|
|
2354
|
-
type: 'swarm.task.completed',
|
|
2355
|
-
taskId,
|
|
2356
|
-
success: true,
|
|
2357
|
-
tokensUsed: taskResult.tokensUsed,
|
|
2358
|
-
costUsed: taskResult.costUsed,
|
|
2359
|
-
durationMs: taskResult.durationMs,
|
|
2360
|
-
qualityScore: taskResult.qualityScore,
|
|
2361
|
-
qualityFeedback: taskResult.qualityFeedback,
|
|
2362
|
-
output: taskResult.output,
|
|
2363
|
-
closureReport: taskResult.closureReport,
|
|
2364
|
-
toolCalls: spawnResult.metrics.toolCalls,
|
|
2365
|
-
});
|
|
2366
|
-
}
|
|
2367
|
-
/**
|
|
2368
|
-
* Phase 4: Synthesize all completed task outputs.
|
|
2369
|
-
*/
|
|
2370
|
-
async synthesize() {
|
|
2371
|
-
const tasks = this.taskQueue.getAllTasks();
|
|
2372
|
-
const outputs = tasks
|
|
2373
|
-
.filter(t => t.status === 'completed')
|
|
2374
|
-
.map(t => taskResultToAgentOutput(t, this.config))
|
|
2375
|
-
.filter((o) => o !== null);
|
|
2376
|
-
if (outputs.length === 0)
|
|
2377
|
-
return null;
|
|
2378
|
-
try {
|
|
2379
|
-
return await this.synthesizer.synthesize(outputs);
|
|
2380
|
-
}
|
|
2381
|
-
catch (error) {
|
|
2382
|
-
this.errors.push({
|
|
2383
|
-
phase: 'synthesis',
|
|
2384
|
-
message: error.message,
|
|
2385
|
-
recovered: true,
|
|
2386
|
-
});
|
|
2387
|
-
// Fallback: concatenate outputs
|
|
2388
|
-
return this.synthesizer.synthesizeFindings(outputs);
|
|
2389
|
-
}
|
|
2390
|
-
}
|
|
2391
|
-
/**
|
|
2392
|
-
* Get live status for TUI.
|
|
2393
|
-
*/
|
|
2394
|
-
// M5: Use explicit phase tracking instead of inferring from queue state
|
|
2395
|
-
getStatus() {
|
|
2396
|
-
const stats = this.taskQueue.getStats();
|
|
2397
|
-
return {
|
|
2398
|
-
phase: this.cancelled ? 'failed' : this.currentPhase,
|
|
2399
|
-
currentWave: this.taskQueue.getCurrentWave() + 1,
|
|
2400
|
-
totalWaves: this.taskQueue.getTotalWaves(),
|
|
2401
|
-
activeWorkers: this.workerPool.getActiveWorkerStatus(),
|
|
2402
|
-
queue: stats,
|
|
2403
|
-
budget: {
|
|
2404
|
-
tokensUsed: this.totalTokens + this.orchestratorTokens,
|
|
2405
|
-
tokensTotal: this.config.totalBudget,
|
|
2406
|
-
costUsed: this.totalCost + this.orchestratorCost,
|
|
2407
|
-
costTotal: this.config.maxCost,
|
|
2408
|
-
},
|
|
2409
|
-
orchestrator: {
|
|
2410
|
-
tokens: this.orchestratorTokens,
|
|
2411
|
-
cost: this.orchestratorCost,
|
|
2412
|
-
calls: this.orchestratorCalls,
|
|
2413
|
-
model: this.config.orchestratorModel,
|
|
2414
|
-
},
|
|
2415
|
-
};
|
|
2416
|
-
}
|
|
2417
|
-
/**
|
|
2418
|
-
* Cancel the swarm execution.
|
|
2419
|
-
* M6: Wait for active workers before cleanup.
|
|
2420
|
-
*/
|
|
2421
|
-
async cancel() {
|
|
2422
|
-
this.cancelled = true;
|
|
2423
|
-
this.currentPhase = 'failed';
|
|
2424
|
-
await this.workerPool.cancelAll();
|
|
2425
|
-
}
|
|
2426
|
-
// ─── D3: Model Capability Probing ─────────────────────────────────────
|
|
2427
|
-
/**
|
|
2428
|
-
* D3/F23: Probe each unique model to verify it can make tool calls.
|
|
2429
|
-
* Models that fail the probe are marked unhealthy so they're skipped in dispatch.
|
|
2430
|
-
*
|
|
2431
|
-
* F23 fix: Uses chatWithTools() with actual tool definitions instead of
|
|
2432
|
-
* plain chat() which never included tools in the API request.
|
|
2433
|
-
*/
|
|
2434
|
-
async probeModelCapability() {
|
|
2435
|
-
const uniqueModels = new Set(this.config.workers.map(w => w.model));
|
|
2436
|
-
this.emit({ type: 'swarm.phase.progress', phase: 'scheduling', message: `Probing ${uniqueModels.size} model(s) for tool-calling capability...` });
|
|
2437
|
-
// F23: Check if provider supports native tool calling
|
|
2438
|
-
const supportsTools = 'chatWithTools' in this.provider
|
|
2439
|
-
&& typeof this.provider.chatWithTools === 'function';
|
|
2440
|
-
if (!supportsTools) {
|
|
2441
|
-
// Provider doesn't support chatWithTools — skip probe entirely.
|
|
2442
|
-
// Workers will rely on text-based tool parsing fallback.
|
|
2443
|
-
this.logDecision('model-probe', 'Provider does not support chatWithTools — skipping probe', '');
|
|
2444
|
-
return;
|
|
2445
|
-
}
|
|
2446
|
-
const providerWithTools = this.provider;
|
|
2447
|
-
const probeTools = [{
|
|
2448
|
-
type: 'function',
|
|
2449
|
-
function: {
|
|
2450
|
-
name: 'read_file',
|
|
2451
|
-
description: 'Read a file from disk',
|
|
2452
|
-
parameters: {
|
|
2453
|
-
type: 'object',
|
|
2454
|
-
properties: { path: { type: 'string', description: 'File path' } },
|
|
2455
|
-
required: ['path'],
|
|
2456
|
-
},
|
|
2457
|
-
},
|
|
2458
|
-
}];
|
|
2459
|
-
// F24: Configurable probe timeout — generous default for slow models/connections
|
|
2460
|
-
const probeTimeout = this.config.probeTimeoutMs ?? 60_000;
|
|
2461
|
-
for (const model of uniqueModels) {
|
|
2462
|
-
try {
|
|
2463
|
-
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Probe timeout (${probeTimeout}ms)`)), probeTimeout));
|
|
2464
|
-
const response = await Promise.race([
|
|
2465
|
-
providerWithTools.chatWithTools([
|
|
2466
|
-
{ role: 'system', content: 'You are a test probe. Call the read_file tool with path "package.json".' },
|
|
2467
|
-
{ role: 'user', content: 'Read package.json.' },
|
|
2468
|
-
], { model, maxTokens: 200, temperature: 0, tools: probeTools, tool_choice: 'required' }),
|
|
2469
|
-
timeoutPromise,
|
|
2470
|
-
]);
|
|
2471
|
-
const hasToolCall = (response.toolCalls?.length ?? 0) > 0;
|
|
2472
|
-
if (!hasToolCall) {
|
|
2473
|
-
// F19: Directly mark unhealthy — probe failure is definitive evidence
|
|
2474
|
-
this.healthTracker.markUnhealthy(model);
|
|
2475
|
-
this.logDecision('model-probe', `Model ${model} failed probe (no tool calls)`, 'Marked unhealthy');
|
|
2476
|
-
}
|
|
2477
|
-
else {
|
|
2478
|
-
this.healthTracker.recordSuccess(model, 0);
|
|
2479
|
-
this.logDecision('model-probe', `Model ${model} passed probe`, '');
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
catch {
|
|
2483
|
-
// F19: Directly mark unhealthy on probe error (includes timeout)
|
|
2484
|
-
this.healthTracker.markUnhealthy(model);
|
|
2485
|
-
this.logDecision('model-probe', `Model ${model} probe errored`, 'Marked unhealthy');
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
// ─── Circuit Breaker ────────────────────────────────────────────────
|
|
2490
|
-
/**
|
|
2491
|
-
* Record a rate limit hit and check if the circuit breaker should trip.
|
|
2492
|
-
*/
|
|
2493
|
-
recordRateLimit() {
|
|
2494
|
-
const now = Date.now();
|
|
2495
|
-
this.recentRateLimits.push(now);
|
|
2496
|
-
this.increaseStagger(); // P7: Back off on rate limits
|
|
2497
|
-
// Prune entries older than the window
|
|
2498
|
-
const cutoff = now - SwarmOrchestrator.CIRCUIT_BREAKER_WINDOW_MS;
|
|
2499
|
-
this.recentRateLimits = this.recentRateLimits.filter(t => t > cutoff);
|
|
2500
|
-
if (this.recentRateLimits.length >= SwarmOrchestrator.CIRCUIT_BREAKER_THRESHOLD) {
|
|
2501
|
-
this.circuitBreakerUntil = now + SwarmOrchestrator.CIRCUIT_BREAKER_PAUSE_MS;
|
|
2502
|
-
this.emit({
|
|
2503
|
-
type: 'swarm.circuit.open',
|
|
2504
|
-
recentCount: this.recentRateLimits.length,
|
|
2505
|
-
pauseMs: SwarmOrchestrator.CIRCUIT_BREAKER_PAUSE_MS,
|
|
2506
|
-
});
|
|
2507
|
-
this.logDecision('circuit-breaker', 'Tripped — pausing all dispatch', `${this.recentRateLimits.length} rate limits in ${SwarmOrchestrator.CIRCUIT_BREAKER_WINDOW_MS / 1000}s window`);
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
/**
|
|
2511
|
-
* Check if the circuit breaker is currently active.
|
|
2512
|
-
* Returns true if dispatch should be paused.
|
|
2513
|
-
*/
|
|
2514
|
-
isCircuitBreakerActive() {
|
|
2515
|
-
if (Date.now() < this.circuitBreakerUntil)
|
|
2516
|
-
return true;
|
|
2517
|
-
if (this.circuitBreakerUntil > 0) {
|
|
2518
|
-
// Circuit just closed
|
|
2519
|
-
this.circuitBreakerUntil = 0;
|
|
2520
|
-
this.emit({ type: 'swarm.circuit.closed' });
|
|
2521
|
-
}
|
|
2522
|
-
return false;
|
|
2523
|
-
}
|
|
2524
|
-
// ─── P7: Adaptive Stagger ────────────────────────────────────────────
|
|
2525
|
-
/** P7: Get current stagger delay (adapts based on rate limit / success signals). */
|
|
2526
|
-
getStaggerMs() {
|
|
2527
|
-
return this.adaptiveStaggerMs;
|
|
2528
|
-
}
|
|
2529
|
-
/** P7: Increase stagger on rate limit (×1.5, capped at 10s). */
|
|
2530
|
-
increaseStagger() {
|
|
2531
|
-
this.adaptiveStaggerMs = Math.min(this.adaptiveStaggerMs * 1.5, 10_000);
|
|
2532
|
-
}
|
|
2533
|
-
/** P7: Decrease stagger on success (×0.9, floor at 200ms). */
|
|
2534
|
-
decreaseStagger() {
|
|
2535
|
-
this.adaptiveStaggerMs = Math.max(this.adaptiveStaggerMs * 0.9, 200);
|
|
2536
|
-
}
|
|
2537
|
-
// ─── V2: Decision Logging ─────────────────────────────────────────────
|
|
2538
|
-
logDecision(phase, decision, reasoning) {
|
|
2539
|
-
const entry = {
|
|
2540
|
-
timestamp: Date.now(),
|
|
2541
|
-
phase,
|
|
2542
|
-
decision,
|
|
2543
|
-
reasoning,
|
|
2544
|
-
};
|
|
2545
|
-
this.orchestratorDecisions.push(entry);
|
|
2546
|
-
this.emit({ type: 'swarm.orchestrator.decision', decision: entry });
|
|
2547
|
-
}
|
|
2548
|
-
// ─── V2: Persistence ──────────────────────────────────────────────────
|
|
2549
|
-
checkpoint(_label) {
|
|
2550
|
-
if (!this.config.enablePersistence || !this.stateStore)
|
|
2551
|
-
return;
|
|
2552
|
-
try {
|
|
2553
|
-
const queueState = this.taskQueue.getCheckpointState();
|
|
2554
|
-
this.stateStore.saveCheckpoint({
|
|
2555
|
-
sessionId: this.stateStore.id,
|
|
2556
|
-
timestamp: Date.now(),
|
|
2557
|
-
phase: this.currentPhase,
|
|
2558
|
-
plan: this.plan,
|
|
2559
|
-
taskStates: queueState.taskStates,
|
|
2560
|
-
waves: queueState.waves,
|
|
2561
|
-
currentWave: queueState.currentWave,
|
|
2562
|
-
stats: {
|
|
2563
|
-
totalTokens: this.totalTokens + this.orchestratorTokens,
|
|
2564
|
-
totalCost: this.totalCost + this.orchestratorCost,
|
|
2565
|
-
qualityRejections: this.qualityRejections,
|
|
2566
|
-
retries: this.retries,
|
|
2567
|
-
},
|
|
2568
|
-
modelHealth: this.healthTracker.getAllRecords(),
|
|
2569
|
-
decisions: this.orchestratorDecisions,
|
|
2570
|
-
errors: this.errors,
|
|
2571
|
-
originalPrompt: this.originalPrompt,
|
|
2572
|
-
sharedContext: this.sharedContextState.toJSON(),
|
|
2573
|
-
sharedEconomics: this.sharedEconomicsState.toJSON(),
|
|
2574
|
-
});
|
|
2575
|
-
this.emit({
|
|
2576
|
-
type: 'swarm.state.checkpoint',
|
|
2577
|
-
sessionId: this.stateStore.id,
|
|
2578
|
-
wave: this.taskQueue.getCurrentWave(),
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
catch (error) {
|
|
2582
|
-
this.errors.push({
|
|
2583
|
-
phase: 'persistence',
|
|
2584
|
-
message: `Checkpoint failed (non-fatal): ${error.message}`,
|
|
2585
|
-
recovered: true,
|
|
2586
|
-
});
|
|
2587
|
-
}
|
|
2588
|
-
}
|
|
2589
|
-
// ─── Private Helpers ───────────────────────────────────────────────────
|
|
2590
|
-
emitBudgetUpdate() {
|
|
2591
|
-
this.emit({
|
|
2592
|
-
type: 'swarm.budget.update',
|
|
2593
|
-
tokensUsed: this.totalTokens + this.orchestratorTokens,
|
|
2594
|
-
tokensTotal: this.config.totalBudget,
|
|
2595
|
-
costUsed: this.totalCost + this.orchestratorCost,
|
|
2596
|
-
costTotal: this.config.maxCost,
|
|
2597
|
-
});
|
|
2598
|
-
}
|
|
2599
|
-
emitStatusUpdate() {
|
|
2600
|
-
this.emit({ type: 'swarm.status', status: this.getStatus() });
|
|
2601
|
-
}
|
|
2602
|
-
buildStats() {
|
|
2603
|
-
const queueStats = this.taskQueue.getStats();
|
|
2604
|
-
return {
|
|
2605
|
-
totalTasks: queueStats.total,
|
|
2606
|
-
completedTasks: queueStats.completed,
|
|
2607
|
-
failedTasks: queueStats.failed,
|
|
2608
|
-
skippedTasks: queueStats.skipped,
|
|
2609
|
-
totalWaves: this.taskQueue.getTotalWaves(),
|
|
2610
|
-
totalTokens: this.totalTokens + this.orchestratorTokens,
|
|
2611
|
-
totalCost: this.totalCost + this.orchestratorCost,
|
|
2612
|
-
totalDurationMs: Date.now() - this.startTime,
|
|
2613
|
-
qualityRejections: this.qualityRejections,
|
|
2614
|
-
retries: this.retries,
|
|
2615
|
-
modelUsage: this.modelUsage,
|
|
2616
|
-
};
|
|
2617
|
-
}
|
|
2618
|
-
buildSummary(stats) {
|
|
2619
|
-
const parts = [
|
|
2620
|
-
`Swarm execution complete:`,
|
|
2621
|
-
` Tasks: ${stats.completedTasks}/${stats.totalTasks} completed, ${stats.failedTasks} failed, ${stats.skippedTasks} skipped`,
|
|
2622
|
-
` Waves: ${stats.totalWaves}`,
|
|
2623
|
-
` Tokens: ${(stats.totalTokens / 1000).toFixed(0)}k`,
|
|
2624
|
-
` Cost: $${stats.totalCost.toFixed(4)}`,
|
|
2625
|
-
` Duration: ${(stats.totalDurationMs / 1000).toFixed(1)}s`,
|
|
2626
|
-
];
|
|
2627
|
-
if (stats.qualityRejections > 0) {
|
|
2628
|
-
parts.push(` Quality rejections: ${stats.qualityRejections}`);
|
|
2629
|
-
}
|
|
2630
|
-
if (stats.retries > 0) {
|
|
2631
|
-
parts.push(` Retries: ${stats.retries}`);
|
|
2632
|
-
}
|
|
2633
|
-
if (this.verificationResult) {
|
|
2634
|
-
parts.push(` Verification: ${this.verificationResult.passed ? 'PASSED' : 'FAILED'}`);
|
|
2635
|
-
}
|
|
2636
|
-
// Artifact inventory: show what files actually exist on disk regardless of task status
|
|
2637
|
-
if (this.artifactInventory && this.artifactInventory.totalFiles > 0) {
|
|
2638
|
-
parts.push(` Files on disk: ${this.artifactInventory.totalFiles} files (${(this.artifactInventory.totalBytes / 1024).toFixed(1)}KB)`);
|
|
2639
|
-
for (const f of this.artifactInventory.files.slice(0, 15)) {
|
|
2640
|
-
parts.push(` ${f.path}: ${f.sizeBytes}B`);
|
|
2641
|
-
}
|
|
2642
|
-
if (this.artifactInventory.files.length > 15) {
|
|
2643
|
-
parts.push(` ... and ${this.artifactInventory.files.length - 15} more`);
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
return parts.join('\n');
|
|
2647
|
-
}
|
|
2648
|
-
buildErrorResult(message) {
|
|
2649
|
-
return {
|
|
2650
|
-
success: false,
|
|
2651
|
-
summary: `Swarm failed: ${message}`,
|
|
2652
|
-
tasks: this.taskQueue.getAllTasks(),
|
|
2653
|
-
stats: this.buildStats(),
|
|
2654
|
-
errors: this.errors,
|
|
2655
|
-
};
|
|
2656
|
-
}
|
|
2657
|
-
/** Parse JSON from LLM response, handling markdown code blocks. */
|
|
2658
|
-
parseJSON(content) {
|
|
2659
|
-
try {
|
|
2660
|
-
// Strip markdown code blocks if present
|
|
2661
|
-
let json = content;
|
|
2662
|
-
const codeBlockMatch = content.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
2663
|
-
if (codeBlockMatch) {
|
|
2664
|
-
json = codeBlockMatch[1];
|
|
2665
|
-
}
|
|
2666
|
-
return JSON.parse(json);
|
|
2667
|
-
}
|
|
2668
|
-
catch {
|
|
2669
|
-
return null;
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
/**
|
|
2673
|
-
* Detect foundation tasks: tasks that are a dependency of 2+ downstream tasks.
|
|
2674
|
-
* These are critical single-points-of-failure — mark them for extra resilience.
|
|
2675
|
-
*/
|
|
2676
|
-
detectFoundationTasks() {
|
|
2677
|
-
const allTasks = this.taskQueue.getAllTasks();
|
|
2678
|
-
const dependentCounts = new Map();
|
|
2679
|
-
for (const task of allTasks) {
|
|
2680
|
-
for (const depId of task.dependencies) {
|
|
2681
|
-
dependentCounts.set(depId, (dependentCounts.get(depId) ?? 0) + 1);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
for (const task of allTasks) {
|
|
2685
|
-
const dependentCount = dependentCounts.get(task.id) ?? 0;
|
|
2686
|
-
if (dependentCount >= 2) {
|
|
2687
|
-
task.isFoundation = true;
|
|
2688
|
-
this.logDecision('scheduling', `Foundation task: ${task.id} (${dependentCount} dependents)`, 'Extra retries and relaxed quality threshold applied');
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
/**
|
|
2693
|
-
* Extract file artifacts from a worker's output for quality gate visibility.
|
|
2694
|
-
* Reads actual file content from disk so the judge can verify real work,
|
|
2695
|
-
* not just text claims about what was created.
|
|
2696
|
-
*/
|
|
2697
|
-
extractFileArtifacts(task, taskResult) {
|
|
2698
|
-
const artifacts = [];
|
|
2699
|
-
const seen = new Set();
|
|
2700
|
-
// Collect file paths from multiple sources
|
|
2701
|
-
const candidatePaths = [];
|
|
2702
|
-
// 1. filesModified from structured closure report
|
|
2703
|
-
if (taskResult.filesModified) {
|
|
2704
|
-
candidatePaths.push(...taskResult.filesModified);
|
|
2705
|
-
}
|
|
2706
|
-
// 2. targetFiles from task definition
|
|
2707
|
-
if (task.targetFiles) {
|
|
2708
|
-
candidatePaths.push(...task.targetFiles);
|
|
2709
|
-
}
|
|
2710
|
-
// 3. Extract file paths mentioned in worker output (e.g., "Created src/foo.ts")
|
|
2711
|
-
const filePathPattern = /(?:created|wrote|modified|edited|updated)\s+["`']?([^\s"`',]+\.\w+)/gi;
|
|
2712
|
-
let match;
|
|
2713
|
-
while ((match = filePathPattern.exec(taskResult.output)) !== null) {
|
|
2714
|
-
candidatePaths.push(match[1]);
|
|
2715
|
-
}
|
|
2716
|
-
// Resolve against the target project directory, not CWD
|
|
2717
|
-
const baseDir = this.config.facts?.workingDirectory ?? process.cwd();
|
|
2718
|
-
// Read previews from disk
|
|
2719
|
-
for (const filePath of candidatePaths) {
|
|
2720
|
-
if (seen.has(filePath))
|
|
2721
|
-
continue;
|
|
2722
|
-
seen.add(filePath);
|
|
2723
|
-
try {
|
|
2724
|
-
const resolved = path.resolve(baseDir, filePath);
|
|
2725
|
-
if (fs.existsSync(resolved)) {
|
|
2726
|
-
const content = fs.readFileSync(resolved, 'utf-8');
|
|
2727
|
-
if (content.length > 0) {
|
|
2728
|
-
artifacts.push({ path: filePath, preview: content.slice(0, 2000) });
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
}
|
|
2732
|
-
catch {
|
|
2733
|
-
// Skip unreadable files
|
|
2734
|
-
}
|
|
2735
|
-
// Limit to 10 files to keep prompt size reasonable
|
|
2736
|
-
if (artifacts.length >= 10)
|
|
2737
|
-
break;
|
|
2738
|
-
}
|
|
2739
|
-
return artifacts;
|
|
2740
|
-
}
|
|
2741
|
-
/**
|
|
2742
|
-
* Build an inventory of filesystem artifacts produced during swarm execution.
|
|
2743
|
-
* Scans all tasks' targetFiles and readFiles to check what actually exists on disk.
|
|
2744
|
-
* This reveals work done by workers even when tasks "failed" (timeout, quality gate, etc.).
|
|
2745
|
-
*/
|
|
2746
|
-
buildArtifactInventory() {
|
|
2747
|
-
const allFiles = new Set();
|
|
2748
|
-
for (const task of this.taskQueue.getAllTasks()) {
|
|
2749
|
-
for (const f of (task.targetFiles ?? []))
|
|
2750
|
-
allFiles.add(f);
|
|
2751
|
-
for (const f of (task.readFiles ?? []))
|
|
2752
|
-
allFiles.add(f);
|
|
2753
|
-
}
|
|
2754
|
-
const baseDir = this.config.facts?.workingDirectory ?? process.cwd();
|
|
2755
|
-
const artifacts = [];
|
|
2756
|
-
for (const filePath of allFiles) {
|
|
2757
|
-
try {
|
|
2758
|
-
const resolved = path.resolve(baseDir, filePath);
|
|
2759
|
-
if (fs.existsSync(resolved)) {
|
|
2760
|
-
const stats = fs.statSync(resolved);
|
|
2761
|
-
if (stats.isFile() && stats.size > 0) {
|
|
2762
|
-
artifacts.push({ path: filePath, sizeBytes: stats.size, exists: true });
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
catch { /* skip unreadable files */ }
|
|
2767
|
-
}
|
|
2768
|
-
return {
|
|
2769
|
-
files: artifacts,
|
|
2770
|
-
totalFiles: artifacts.length,
|
|
2771
|
-
totalBytes: artifacts.reduce((s, a) => s + a.sizeBytes, 0),
|
|
2772
|
-
};
|
|
2773
|
-
}
|
|
2774
|
-
/**
|
|
2775
|
-
* Skip all remaining pending/ready tasks (used for early termination).
|
|
2776
|
-
*/
|
|
2777
|
-
skipRemainingTasks(reason) {
|
|
2778
|
-
for (const task of this.taskQueue.getAllTasks()) {
|
|
2779
|
-
if (task.status === 'pending' || task.status === 'ready') {
|
|
2780
|
-
task.status = 'skipped';
|
|
2781
|
-
this.emit({ type: 'swarm.task.skipped', taskId: task.id, reason });
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
/**
|
|
2786
|
-
* F21: Mid-swarm situational assessment after each wave.
|
|
2787
|
-
* Evaluates success rate and budget health, triages low-priority tasks when budget is tight.
|
|
2788
|
-
* Also detects stalled progress and triggers mid-swarm re-planning.
|
|
2789
|
-
*/
|
|
2790
|
-
async assessAndAdapt(waveIndex) {
|
|
2791
|
-
const stats = this.taskQueue.getStats();
|
|
2792
|
-
const budgetStats = this.budgetPool.getStats();
|
|
2793
|
-
// 1. Calculate success rate for this swarm run
|
|
2794
|
-
const successRate = stats.completed / Math.max(1, stats.completed + stats.failed + stats.skipped);
|
|
2795
|
-
// 2. Budget efficiency: tokens spent per completed task
|
|
2796
|
-
const tokensPerTask = stats.completed > 0
|
|
2797
|
-
? (this.totalTokens / stats.completed)
|
|
2798
|
-
: Infinity;
|
|
2799
|
-
// 3. Remaining budget vs remaining tasks
|
|
2800
|
-
const remainingTasks = stats.total - stats.completed - stats.failed - stats.skipped;
|
|
2801
|
-
const estimatedTokensNeeded = remainingTasks * tokensPerTask;
|
|
2802
|
-
const budgetSufficient = budgetStats.tokensRemaining > estimatedTokensNeeded * 0.5;
|
|
2803
|
-
// Log the assessment for observability
|
|
2804
|
-
this.logDecision('mid-swarm-assessment', `After wave ${waveIndex + 1}: ${stats.completed}/${stats.total} completed (${(successRate * 100).toFixed(0)}%), ` +
|
|
2805
|
-
`${remainingTasks} remaining, ${budgetStats.tokensRemaining} tokens left`, budgetSufficient ? 'Budget looks sufficient' : 'Budget may be insufficient for remaining tasks');
|
|
2806
|
-
// 4. If budget is tight, prioritize: skip low-value remaining tasks
|
|
2807
|
-
// Only triage if we have actual data (at least one completion to estimate from)
|
|
2808
|
-
if (!budgetSufficient && remainingTasks > 1 && stats.completed > 0) {
|
|
2809
|
-
// Prefer pausing over skipping: if workers are still running, wait for budget release
|
|
2810
|
-
const runningCount = stats.running ?? 0;
|
|
2811
|
-
if (runningCount > 0) {
|
|
2812
|
-
this.logDecision('budget-wait', 'Budget tight but workers still running — waiting for budget release', `${runningCount} workers active, ${budgetStats.tokensRemaining} tokens remaining`);
|
|
2813
|
-
return;
|
|
2814
|
-
}
|
|
2815
|
-
const expendableTasks = this.findExpendableTasks();
|
|
2816
|
-
// Hard cap: never skip more than 20% of remaining tasks in one triage pass
|
|
2817
|
-
const maxSkips = Math.max(1, Math.floor(remainingTasks * 0.2));
|
|
2818
|
-
if (expendableTasks.length > 0) {
|
|
2819
|
-
let currentEstimate = estimatedTokensNeeded;
|
|
2820
|
-
let skipped = 0;
|
|
2821
|
-
for (const task of expendableTasks) {
|
|
2822
|
-
if (skipped >= maxSkips)
|
|
2823
|
-
break;
|
|
2824
|
-
// Stop trimming once we're within budget
|
|
2825
|
-
if (currentEstimate * 0.7 <= budgetStats.tokensRemaining)
|
|
2826
|
-
break;
|
|
2827
|
-
task.status = 'skipped';
|
|
2828
|
-
skipped++;
|
|
2829
|
-
this.emit({ type: 'swarm.task.skipped', taskId: task.id,
|
|
2830
|
-
reason: 'Budget conservation: skipping low-priority task to protect critical path' });
|
|
2831
|
-
this.logDecision('budget-triage', `Skipping ${task.id} (${task.type}, complexity ${task.complexity}) to conserve budget`, `${remainingTasks} tasks remain, ${budgetStats.tokensRemaining} tokens`);
|
|
2832
|
-
currentEstimate -= tokensPerTask;
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
}
|
|
2836
|
-
// 5. Stall detection: if progress ratio is too low, trigger re-plan
|
|
2837
|
-
const attemptedTasks = stats.completed + stats.failed + stats.skipped;
|
|
2838
|
-
if (attemptedTasks >= 5) {
|
|
2839
|
-
const progressRatio = stats.completed / Math.max(1, attemptedTasks);
|
|
2840
|
-
if (progressRatio < 0.4) {
|
|
2841
|
-
this.logDecision('stall-detected', `Progress stalled: ${stats.completed}/${attemptedTasks} tasks succeeded (${(progressRatio * 100).toFixed(0)}%)`, 'Triggering mid-swarm re-plan');
|
|
2842
|
-
this.emit({
|
|
2843
|
-
type: 'swarm.stall',
|
|
2844
|
-
progressRatio,
|
|
2845
|
-
attempted: attemptedTasks,
|
|
2846
|
-
completed: stats.completed,
|
|
2847
|
-
});
|
|
2848
|
-
await this.midSwarmReplan();
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
/**
|
|
2853
|
-
* F21: Find expendable tasks — leaf tasks (no dependents) with lowest complexity.
|
|
2854
|
-
* These are the safest to skip when budget is tight.
|
|
2855
|
-
* Only tasks with complexity <= 2 are considered expendable.
|
|
2856
|
-
*/
|
|
2857
|
-
findExpendableTasks() {
|
|
2858
|
-
const allTasks = this.taskQueue.getAllTasks();
|
|
2859
|
-
// Build reverse dependency map: which tasks depend on each task?
|
|
2860
|
-
const dependentCounts = new Map();
|
|
2861
|
-
for (const task of allTasks) {
|
|
2862
|
-
for (const depId of task.dependencies) {
|
|
2863
|
-
dependentCounts.set(depId, (dependentCounts.get(depId) ?? 0) + 1);
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
// Expendable = pending/ready, never attempted, no dependents, not foundation,
|
|
2867
|
-
// complexity <= 2 (simple leaf tasks only), lowest complexity first
|
|
2868
|
-
return allTasks
|
|
2869
|
-
.filter(t => (t.status === 'pending' || t.status === 'ready') &&
|
|
2870
|
-
t.attempts === 0 &&
|
|
2871
|
-
!t.isFoundation &&
|
|
2872
|
-
(t.complexity ?? 5) <= 2 &&
|
|
2873
|
-
(dependentCounts.get(t.id) ?? 0) === 0)
|
|
2874
|
-
.sort((a, b) => (a.complexity ?? 5) - (b.complexity ?? 5));
|
|
2875
|
-
}
|
|
2876
|
-
/**
|
|
2877
|
-
* Mid-swarm re-planning: when progress stalls, ask LLM to re-plan remaining work.
|
|
2878
|
-
* Creates simpler replacement tasks for stuck/failed work, building on what's already done.
|
|
2879
|
-
* Only triggers once per swarm execution to avoid infinite re-planning loops.
|
|
2880
|
-
*/
|
|
2881
|
-
async midSwarmReplan() {
|
|
2882
|
-
if (this.hasReplanned)
|
|
2883
|
-
return;
|
|
2884
|
-
this.hasReplanned = true;
|
|
2885
|
-
const allTasks = this.taskQueue.getAllTasks();
|
|
2886
|
-
const completed = allTasks.filter(t => t.status === 'completed' || t.status === 'decomposed');
|
|
2887
|
-
const stuck = allTasks.filter(t => t.status === 'failed' || t.status === 'skipped');
|
|
2888
|
-
if (stuck.length === 0)
|
|
2889
|
-
return;
|
|
2890
|
-
const completedSummary = completed.map(t => `- ${t.description} [${t.type}] → completed${t.degraded ? ' (degraded)' : ''}`).join('\n') || '(none)';
|
|
2891
|
-
const stuckSummary = stuck.map(t => `- ${t.description} [${t.type}] → ${t.status} (${t.failureMode ?? 'unknown'})`).join('\n');
|
|
2892
|
-
const artifactInventory = this.buildArtifactInventory();
|
|
2893
|
-
const artifactSummary = artifactInventory.files.map(f => `- ${f.path} (${f.sizeBytes}B)`).join('\n') || '(none)';
|
|
2894
|
-
const replanPrompt = `The swarm is stalled. Here's the situation:
|
|
2895
|
-
|
|
2896
|
-
COMPLETED WORK:
|
|
2897
|
-
${completedSummary}
|
|
2898
|
-
|
|
2899
|
-
FILES ON DISK:
|
|
2900
|
-
${artifactSummary}
|
|
2901
|
-
|
|
2902
|
-
STUCK TASKS (failed or skipped):
|
|
2903
|
-
${stuckSummary}
|
|
2904
|
-
|
|
2905
|
-
Re-plan the remaining work. Create new subtasks that:
|
|
2906
|
-
1. Build on what's already completed (don't redo work)
|
|
2907
|
-
2. Are more focused in scope (but assign realistic complexity for the work involved — don't underestimate)
|
|
2908
|
-
3. Can succeed independently (minimize dependencies)
|
|
2909
|
-
|
|
2910
|
-
Return JSON: { "subtasks": [{ "description": "...", "type": "implement|test|research|review|document|refactor", "complexity": 1-5, "dependencies": [], "relevantFiles": [] }] }
|
|
2911
|
-
Return ONLY the JSON object, no other text.`;
|
|
2912
|
-
try {
|
|
2913
|
-
const response = await this.provider.chat([{ role: 'user', content: replanPrompt }]);
|
|
2914
|
-
this.trackOrchestratorUsage(response, 'mid-swarm-replan');
|
|
2915
|
-
const content = response.content ?? '';
|
|
2916
|
-
const jsonMatch = content.match(/\{[\s\S]*"subtasks"[\s\S]*\}/);
|
|
2917
|
-
if (!jsonMatch) {
|
|
2918
|
-
this.logDecision('replan-failed', 'LLM produced no parseable re-plan JSON', content.slice(0, 200));
|
|
2919
|
-
return;
|
|
2920
|
-
}
|
|
2921
|
-
const parsed = JSON.parse(jsonMatch[0]);
|
|
2922
|
-
if (!parsed.subtasks || parsed.subtasks.length === 0) {
|
|
2923
|
-
this.logDecision('replan-failed', 'LLM produced empty subtask list', '');
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
// Add new tasks from re-plan into current wave
|
|
2927
|
-
const newTasks = this.taskQueue.addReplanTasks(parsed.subtasks, this.taskQueue.getCurrentWave());
|
|
2928
|
-
this.logDecision('replan-success', `Re-planned ${stuck.length} stuck tasks into ${newTasks.length} new tasks`, newTasks.map(t => t.description).join('; '));
|
|
2929
|
-
this.emit({
|
|
2930
|
-
type: 'swarm.replan',
|
|
2931
|
-
stuckCount: stuck.length,
|
|
2932
|
-
newTaskCount: newTasks.length,
|
|
2933
|
-
});
|
|
2934
|
-
this.emit({
|
|
2935
|
-
type: 'swarm.orchestrator.decision',
|
|
2936
|
-
decision: {
|
|
2937
|
-
timestamp: Date.now(),
|
|
2938
|
-
phase: 'replan',
|
|
2939
|
-
decision: `Re-planned ${stuck.length} stuck tasks into ${newTasks.length} new tasks`,
|
|
2940
|
-
reasoning: newTasks.map(t => `${t.id}: ${t.description}`).join('; '),
|
|
2941
|
-
},
|
|
2942
|
-
});
|
|
2943
|
-
}
|
|
2944
|
-
catch (error) {
|
|
2945
|
-
this.logDecision('replan-failed', `Re-plan LLM call failed: ${error.message}`, '');
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
/**
|
|
2949
|
-
* Rescue cascade-skipped tasks that can still run.
|
|
2950
|
-
* After cascade-skip fires, assess whether skipped tasks can still be attempted:
|
|
2951
|
-
* - If all OTHER dependencies completed and the failed dep's artifacts exist on disk → un-skip
|
|
2952
|
-
* - If the task has no strict data dependency on the failed task (different file targets) → un-skip with warning
|
|
2953
|
-
*/
|
|
2954
|
-
rescueCascadeSkipped(lenient = false) {
|
|
2955
|
-
const skippedTasks = this.taskQueue.getSkippedTasks();
|
|
2956
|
-
const rescued = [];
|
|
2957
|
-
for (const task of skippedTasks) {
|
|
2958
|
-
if (task.dependencies.length === 0)
|
|
2959
|
-
continue;
|
|
2960
|
-
let completedDeps = 0;
|
|
2961
|
-
let failedDepsWithArtifacts = 0;
|
|
2962
|
-
let failedDepsWithoutArtifacts = 0;
|
|
2963
|
-
let skippedDepsBlockedBySkipped = 0;
|
|
2964
|
-
let totalDeps = 0;
|
|
2965
|
-
const failedDepDescriptions = [];
|
|
2966
|
-
for (const depId of task.dependencies) {
|
|
2967
|
-
const dep = this.taskQueue.getTask(depId);
|
|
2968
|
-
if (!dep)
|
|
2969
|
-
continue;
|
|
2970
|
-
totalDeps++;
|
|
2971
|
-
if (dep.status === 'completed' || dep.status === 'decomposed') {
|
|
2972
|
-
completedDeps++;
|
|
2973
|
-
}
|
|
2974
|
-
else if (dep.status === 'failed' || dep.status === 'skipped') {
|
|
2975
|
-
// V10: In lenient mode, use checkArtifactsEnhanced for broader detection
|
|
2976
|
-
const artifactReport = lenient ? checkArtifactsEnhanced(dep) : checkArtifacts(dep);
|
|
2977
|
-
if (artifactReport && artifactReport.files.filter(f => f.exists && f.sizeBytes > 0).length > 0) {
|
|
2978
|
-
failedDepsWithArtifacts++;
|
|
2979
|
-
failedDepDescriptions.push(`${dep.description} (failed but ${artifactReport.files.filter(f => f.exists && f.sizeBytes > 0).length} artifacts exist)`);
|
|
2980
|
-
}
|
|
2981
|
-
else {
|
|
2982
|
-
// Check if this dep's target files exist on disk (may have been created by earlier attempt)
|
|
2983
|
-
const targetFiles = dep.targetFiles ?? [];
|
|
2984
|
-
const existingFiles = targetFiles.filter(f => {
|
|
2985
|
-
try {
|
|
2986
|
-
const resolved = path.resolve(this.config.facts?.workingDirectory ?? process.cwd(), f);
|
|
2987
|
-
return fs.statSync(resolved).size > 0;
|
|
2988
|
-
}
|
|
2989
|
-
catch {
|
|
2990
|
-
return false;
|
|
2991
|
-
}
|
|
2992
|
-
});
|
|
2993
|
-
if (existingFiles.length > 0) {
|
|
2994
|
-
failedDepsWithArtifacts++;
|
|
2995
|
-
failedDepDescriptions.push(`${dep.description} (failed but ${existingFiles.length}/${targetFiles.length} target files exist)`);
|
|
2996
|
-
}
|
|
2997
|
-
else {
|
|
2998
|
-
// Check if skipped task's targets don't overlap with the failed dep's targets
|
|
2999
|
-
const taskTargets = new Set(task.targetFiles ?? []);
|
|
3000
|
-
const depTargets = new Set(dep.targetFiles ?? []);
|
|
3001
|
-
const hasOverlap = [...taskTargets].some(f => depTargets.has(f));
|
|
3002
|
-
if (!hasOverlap && taskTargets.size > 0) {
|
|
3003
|
-
// Different file targets — task probably doesn't need the failed dep's output
|
|
3004
|
-
failedDepsWithArtifacts++;
|
|
3005
|
-
failedDepDescriptions.push(`${dep.description} (failed, no file overlap — likely independent)`);
|
|
3006
|
-
}
|
|
3007
|
-
else if (lenient && dep.status === 'skipped') {
|
|
3008
|
-
// V10: In lenient mode, count skipped-by-skipped deps separately
|
|
3009
|
-
// (transitive cascade — the dep itself was a victim, not truly broken)
|
|
3010
|
-
skippedDepsBlockedBySkipped++;
|
|
3011
|
-
failedDepDescriptions.push(`${dep.description} (skipped — transitive cascade victim)`);
|
|
3012
|
-
}
|
|
3013
|
-
else {
|
|
3014
|
-
failedDepsWithoutArtifacts++;
|
|
3015
|
-
}
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
3020
|
-
// Rescue condition:
|
|
3021
|
-
// Normal: all failed deps have artifacts or are independent, AND at least some deps completed
|
|
3022
|
-
// Lenient: tolerate up to 1 truly-missing dep, and count transitive cascade victims as recoverable
|
|
3023
|
-
const effectiveWithout = failedDepsWithoutArtifacts;
|
|
3024
|
-
const maxMissing = lenient ? 1 : 0;
|
|
3025
|
-
const hasEnoughContext = lenient ? (completedDeps + failedDepsWithArtifacts + skippedDepsBlockedBySkipped > 0) : (completedDeps > 0);
|
|
3026
|
-
if (totalDeps > 0 && effectiveWithout <= maxMissing && hasEnoughContext) {
|
|
3027
|
-
const rescueContext = `Rescued from cascade-skip${lenient ? ' (lenient)' : ''}: ${completedDeps}/${totalDeps} deps completed, ` +
|
|
3028
|
-
`${failedDepsWithArtifacts} failed deps have artifacts${skippedDepsBlockedBySkipped > 0 ? `, ${skippedDepsBlockedBySkipped} transitive cascade victims` : ''}. ${failedDepDescriptions.join('; ')}`;
|
|
3029
|
-
this.taskQueue.rescueTask(task.id, rescueContext);
|
|
3030
|
-
rescued.push(task);
|
|
3031
|
-
this.logDecision('cascade-rescue', `${task.id}: rescued from cascade-skip${lenient ? ' (lenient)' : ''}`, rescueContext);
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
return rescued;
|
|
3035
|
-
}
|
|
3036
|
-
/**
|
|
3037
|
-
* Final rescue pass — runs after executeWaves() finishes.
|
|
3038
|
-
* Uses lenient mode to rescue cascade-skipped tasks that have partial context.
|
|
3039
|
-
* Re-dispatches rescued tasks in a final wave.
|
|
3040
|
-
*/
|
|
3041
|
-
async finalRescuePass() {
|
|
3042
|
-
const skipped = this.taskQueue.getSkippedTasks();
|
|
3043
|
-
if (skipped.length === 0)
|
|
3044
|
-
return;
|
|
3045
|
-
this.logDecision('final-rescue', `${skipped.length} skipped tasks — running final rescue pass`, '');
|
|
3046
|
-
const rescued = this.rescueCascadeSkipped(true); // lenient=true
|
|
3047
|
-
if (rescued.length > 0) {
|
|
3048
|
-
this.logDecision('final-rescue', `Rescued ${rescued.length} tasks`, rescued.map(t => t.id).join(', '));
|
|
3049
|
-
await this.executeWave(rescued);
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
/**
|
|
3053
|
-
* Try resilience recovery strategies before hard-failing a task.
|
|
3054
|
-
* Called from dispatch-cap, timeout, hollow, and error paths to avoid bypassing resilience.
|
|
3055
|
-
*
|
|
3056
|
-
* Strategies (in order):
|
|
3057
|
-
* 1. Micro-decomposition — break complex failing tasks into subtasks
|
|
3058
|
-
* 2. Degraded acceptance — accept partial work if artifacts exist on disk
|
|
3059
|
-
*
|
|
3060
|
-
* Returns true if recovery succeeded (caller should return), false if hard-fail should proceed.
|
|
3061
|
-
*/
|
|
3062
|
-
async tryResilienceRecovery(task, taskId, taskResult, spawnResult) {
|
|
3063
|
-
// Strategy 1: Micro-decompose complex tasks into smaller subtasks
|
|
3064
|
-
// V10: Lowered threshold from >= 6 to >= 4 so moderately complex tasks can be recovered
|
|
3065
|
-
if ((task.complexity ?? 0) >= 4 && task.attempts >= 2 && this.budgetPool.hasCapacity()) {
|
|
3066
|
-
const subtasks = await this.microDecompose(task);
|
|
3067
|
-
if (subtasks && subtasks.length >= 2) {
|
|
3068
|
-
// Reset task status so replaceWithSubtasks can mark it as decomposed
|
|
3069
|
-
task.status = 'dispatched';
|
|
3070
|
-
this.taskQueue.replaceWithSubtasks(taskId, subtasks);
|
|
3071
|
-
this.logDecision('micro-decompose', `${taskId}: decomposed into ${subtasks.length} subtasks after ${task.attempts} failures`, subtasks.map(s => `${s.id}: ${s.description.slice(0, 60)}`).join('; '));
|
|
3072
|
-
this.emit({
|
|
3073
|
-
type: 'swarm.task.failed',
|
|
3074
|
-
taskId,
|
|
3075
|
-
error: `Micro-decomposed into ${subtasks.length} subtasks`,
|
|
3076
|
-
attempt: task.attempts,
|
|
3077
|
-
maxAttempts: this.config.maxDispatchesPerTask ?? 5,
|
|
3078
|
-
willRetry: false,
|
|
3079
|
-
toolCalls: spawnResult.metrics.toolCalls,
|
|
3080
|
-
failureMode: task.failureMode,
|
|
3081
|
-
});
|
|
3082
|
-
this.emit({
|
|
3083
|
-
type: 'swarm.task.resilience',
|
|
3084
|
-
taskId,
|
|
3085
|
-
strategy: 'micro-decompose',
|
|
3086
|
-
succeeded: true,
|
|
3087
|
-
reason: `Decomposed into ${subtasks.length} subtasks after ${task.attempts} failures`,
|
|
3088
|
-
artifactsFound: 0,
|
|
3089
|
-
toolCalls: spawnResult.metrics.toolCalls ?? 0,
|
|
3090
|
-
});
|
|
3091
|
-
return true;
|
|
3092
|
-
}
|
|
3093
|
-
// Micro-decompose was attempted but didn't produce usable subtasks
|
|
3094
|
-
if ((task.complexity ?? 0) < 4) {
|
|
3095
|
-
this.logDecision('resilience-skip', `${taskId}: skipped micro-decompose — complexity ${task.complexity} < 4`, '');
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
// Strategy 2: Degraded acceptance — check if any attempt produced files on disk.
|
|
3099
|
-
// V10: Use checkArtifactsEnhanced for broader detection (filesModified, closureReport, output)
|
|
3100
|
-
const artifactReport = checkArtifactsEnhanced(task, taskResult);
|
|
3101
|
-
const existingArtifacts = artifactReport.files.filter(f => f.exists && f.sizeBytes > 0);
|
|
3102
|
-
const hasArtifacts = existingArtifacts.length > 0;
|
|
3103
|
-
// V10: Fix timeout detection — toolCalls=-1 means timeout (worker WAS working)
|
|
3104
|
-
const toolCalls = spawnResult.metrics.toolCalls ?? 0;
|
|
3105
|
-
const hadToolCalls = toolCalls > 0 || toolCalls === -1
|
|
3106
|
-
|| (taskResult.filesModified && taskResult.filesModified.length > 0);
|
|
3107
|
-
const isNarrativeOnly = hasFutureIntentLanguage(taskResult.output ?? '');
|
|
3108
|
-
const typeConfig = getTaskTypeConfig(task.type, this.config);
|
|
3109
|
-
const actionTaskNeedsArtifacts = (this.config.completionGuard?.requireConcreteArtifactsForActionTasks ?? true)
|
|
3110
|
-
&& !!typeConfig.requiresToolCalls;
|
|
3111
|
-
const allowDegradedWithoutArtifacts = !actionTaskNeedsArtifacts && hadToolCalls && !isNarrativeOnly;
|
|
3112
|
-
if (hasArtifacts || allowDegradedWithoutArtifacts) {
|
|
3113
|
-
// Accept with degraded flag — prevents cascade-skip of dependents
|
|
3114
|
-
taskResult.success = true;
|
|
3115
|
-
taskResult.degraded = true;
|
|
3116
|
-
taskResult.qualityScore = 2; // Capped at low quality
|
|
3117
|
-
taskResult.qualityFeedback = 'Degraded acceptance: retries exhausted but filesystem artifacts exist';
|
|
3118
|
-
task.degraded = true;
|
|
3119
|
-
// Reset status so markCompleted works (markFailed may have set it to 'failed')
|
|
3120
|
-
task.status = 'dispatched';
|
|
3121
|
-
this.taskQueue.markCompleted(taskId, taskResult);
|
|
3122
|
-
this.hollowStreak = 0;
|
|
3123
|
-
this.logDecision('degraded-acceptance', `${taskId}: accepted as degraded — ${existingArtifacts.length} artifacts on disk, ${toolCalls} tool calls`, 'Prevents cascade-skip of dependent tasks');
|
|
3124
|
-
this.emit({
|
|
3125
|
-
type: 'swarm.task.completed',
|
|
3126
|
-
taskId,
|
|
3127
|
-
success: true,
|
|
3128
|
-
tokensUsed: taskResult.tokensUsed,
|
|
3129
|
-
costUsed: taskResult.costUsed,
|
|
3130
|
-
durationMs: taskResult.durationMs,
|
|
3131
|
-
qualityScore: 2,
|
|
3132
|
-
qualityFeedback: 'Degraded acceptance',
|
|
3133
|
-
output: taskResult.output,
|
|
3134
|
-
toolCalls: spawnResult.metrics.toolCalls,
|
|
3135
|
-
});
|
|
3136
|
-
this.emit({
|
|
3137
|
-
type: 'swarm.task.resilience',
|
|
3138
|
-
taskId,
|
|
3139
|
-
strategy: 'degraded-acceptance',
|
|
3140
|
-
succeeded: true,
|
|
3141
|
-
reason: `${existingArtifacts.length} artifacts on disk, ${toolCalls} tool calls`,
|
|
3142
|
-
artifactsFound: existingArtifacts.length,
|
|
3143
|
-
toolCalls,
|
|
3144
|
-
});
|
|
3145
|
-
return true;
|
|
3146
|
-
}
|
|
3147
|
-
// Both strategies failed — log exhaustion for traceability
|
|
3148
|
-
this.logDecision('resilience-exhausted', `${taskId}: no recovery — artifacts: ${existingArtifacts.length}, toolCalls: ${toolCalls}, filesModified: ${taskResult.filesModified?.length ?? 0}`, '');
|
|
3149
|
-
this.emit({
|
|
3150
|
-
type: 'swarm.task.resilience',
|
|
3151
|
-
taskId,
|
|
3152
|
-
strategy: 'none',
|
|
3153
|
-
succeeded: false,
|
|
3154
|
-
reason: `No artifacts found, toolCalls=${toolCalls}, filesModified=${taskResult.filesModified?.length ?? 0}`,
|
|
3155
|
-
artifactsFound: existingArtifacts.length,
|
|
3156
|
-
toolCalls,
|
|
3157
|
-
});
|
|
3158
|
-
return false;
|
|
3159
|
-
}
|
|
3160
|
-
/**
|
|
3161
|
-
* Micro-decompose a complex task into 2-3 smaller subtasks using the LLM.
|
|
3162
|
-
* Called when a complex task (complexity >= 6) fails 2+ times with the same failure mode.
|
|
3163
|
-
* Returns null if decomposition doesn't make sense or LLM can't produce valid subtasks.
|
|
3164
|
-
*/
|
|
3165
|
-
async microDecompose(task) {
|
|
3166
|
-
if ((task.complexity ?? 0) < 4)
|
|
3167
|
-
return null;
|
|
3168
|
-
try {
|
|
3169
|
-
const prompt = `Task "${task.description}" failed ${task.attempts} times on model ${task.assignedModel ?? 'unknown'}.
|
|
3170
|
-
The task has complexity ${task.complexity}/10 and type "${task.type}".
|
|
3171
|
-
${task.targetFiles?.length ? `Target files: ${task.targetFiles.join(', ')}` : ''}
|
|
3172
|
-
|
|
3173
|
-
Break this task into 2-3 smaller, independent subtasks that each handle a portion of the work.
|
|
3174
|
-
Each subtask MUST be simpler (complexity <= ${Math.ceil(task.complexity / 2)}).
|
|
3175
|
-
Each subtask should be self-contained and produce concrete file changes.
|
|
3176
|
-
|
|
3177
|
-
Return JSON ONLY (no markdown, no explanation):
|
|
3178
|
-
{
|
|
3179
|
-
"subtasks": [
|
|
3180
|
-
{ "description": "...", "type": "${task.type}", "targetFiles": ["..."], "complexity": <number> }
|
|
3181
|
-
]
|
|
3182
|
-
}`;
|
|
3183
|
-
const response = await this.provider.chat([
|
|
3184
|
-
{ role: 'system', content: 'You are a task decomposition assistant. Return only valid JSON.' },
|
|
3185
|
-
{ role: 'user', content: prompt },
|
|
3186
|
-
], {
|
|
3187
|
-
model: this.config.orchestratorModel,
|
|
3188
|
-
maxTokens: 2000,
|
|
3189
|
-
temperature: 0.3,
|
|
3190
|
-
});
|
|
3191
|
-
this.trackOrchestratorUsage(response, 'micro-decompose');
|
|
3192
|
-
// Parse response — handle markdown code blocks
|
|
3193
|
-
let jsonStr = response.content.trim();
|
|
3194
|
-
const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
3195
|
-
if (codeBlockMatch)
|
|
3196
|
-
jsonStr = codeBlockMatch[1].trim();
|
|
3197
|
-
const parsed = JSON.parse(jsonStr);
|
|
3198
|
-
if (!parsed.subtasks || !Array.isArray(parsed.subtasks) || parsed.subtasks.length < 2) {
|
|
3199
|
-
return null;
|
|
3200
|
-
}
|
|
3201
|
-
const subtasks = parsed.subtasks.map((sub, idx) => ({
|
|
3202
|
-
id: `${task.id}-sub${idx + 1}`,
|
|
3203
|
-
description: sub.description,
|
|
3204
|
-
type: sub.type ?? task.type,
|
|
3205
|
-
dependencies: [], // Will be set by replaceWithSubtasks
|
|
3206
|
-
status: 'ready',
|
|
3207
|
-
complexity: Math.min(sub.complexity ?? Math.ceil(task.complexity / 2), task.complexity - 1),
|
|
3208
|
-
wave: task.wave,
|
|
3209
|
-
targetFiles: sub.targetFiles ?? [],
|
|
3210
|
-
readFiles: task.readFiles,
|
|
3211
|
-
attempts: 0,
|
|
3212
|
-
}));
|
|
3213
|
-
return subtasks;
|
|
3214
|
-
}
|
|
3215
|
-
catch (error) {
|
|
3216
|
-
this.logDecision('micro-decompose', `${task.id}: micro-decomposition failed — ${error.message}`, 'Falling through to normal failure path');
|
|
3217
|
-
return null;
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
// ─── Pre-Dispatch Auto-Split ──────────────────────────────────────────────
|
|
3221
|
-
/**
|
|
3222
|
-
* Heuristic pre-filter: should this task be considered for auto-split?
|
|
3223
|
-
* Cheap check — no LLM call. Returns true if all conditions are met.
|
|
3224
|
-
*/
|
|
3225
|
-
shouldAutoSplit(task) {
|
|
3226
|
-
const cfg = this.config.autoSplit;
|
|
3227
|
-
if (cfg?.enabled === false)
|
|
3228
|
-
return false;
|
|
3229
|
-
const floor = cfg?.complexityFloor ?? 6;
|
|
3230
|
-
const splittable = cfg?.splittableTypes ?? ['implement', 'refactor', 'test'];
|
|
3231
|
-
// Only first attempts — retries use micro-decompose
|
|
3232
|
-
if (task.attempts > 0)
|
|
3233
|
-
return false;
|
|
3234
|
-
// Complexity check
|
|
3235
|
-
if ((task.complexity ?? 0) < floor)
|
|
3236
|
-
return false;
|
|
3237
|
-
// Type check
|
|
3238
|
-
if (!splittable.includes(task.type))
|
|
3239
|
-
return false;
|
|
3240
|
-
// Must be on critical path (foundation task)
|
|
3241
|
-
if (!task.isFoundation)
|
|
3242
|
-
return false;
|
|
3243
|
-
// Budget capacity check
|
|
3244
|
-
if (!this.budgetPool.hasCapacity())
|
|
3245
|
-
return false;
|
|
3246
|
-
return true;
|
|
3247
|
-
}
|
|
3248
|
-
/**
|
|
3249
|
-
* LLM judge call: ask the orchestrator model whether and how to split a task.
|
|
3250
|
-
* Returns { shouldSplit: false } or { shouldSplit: true, subtasks: [...] }.
|
|
3251
|
-
*/
|
|
3252
|
-
async judgeSplit(task) {
|
|
3253
|
-
const maxSubs = this.config.autoSplit?.maxSubtasks ?? 4;
|
|
3254
|
-
const prompt = `You are evaluating whether a task should be split into parallel subtasks before dispatch.
|
|
3255
|
-
|
|
3256
|
-
TASK: "${task.description}"
|
|
3257
|
-
TYPE: ${task.type}
|
|
3258
|
-
COMPLEXITY: ${task.complexity}/10
|
|
3259
|
-
TARGET FILES: ${task.targetFiles?.join(', ') || 'none specified'}
|
|
3260
|
-
DOWNSTREAM DEPENDENTS: This is a foundation task — other tasks are waiting on it.
|
|
3261
|
-
|
|
3262
|
-
Should this task be split into 2-${maxSubs} parallel subtasks that different workers can execute simultaneously?
|
|
3263
|
-
|
|
3264
|
-
SPLIT if:
|
|
3265
|
-
- The task involves multiple independent pieces of work (e.g., different files, different functions, different concerns)
|
|
3266
|
-
- Parallel execution would meaningfully reduce wall-clock time
|
|
3267
|
-
- The subtasks can produce useful output independently
|
|
3268
|
-
|
|
3269
|
-
DO NOT SPLIT if:
|
|
3270
|
-
- The work is conceptually atomic (one function, one algorithm, tightly coupled logic)
|
|
3271
|
-
- The subtasks would need to coordinate on the same files/functions
|
|
3272
|
-
- Splitting would add more overhead than it saves
|
|
3273
|
-
|
|
3274
|
-
Return JSON ONLY:
|
|
3275
|
-
{
|
|
3276
|
-
"shouldSplit": true/false,
|
|
3277
|
-
"reason": "brief explanation",
|
|
3278
|
-
"subtasks": [
|
|
3279
|
-
{ "description": "...", "type": "${task.type}", "targetFiles": ["..."], "complexity": <number 1-10> }
|
|
3280
|
-
]
|
|
3281
|
-
}
|
|
3282
|
-
If shouldSplit is false, omit subtasks.`;
|
|
3283
|
-
const response = await this.provider.chat([
|
|
3284
|
-
{ role: 'system', content: 'You are a task planning judge. Return only valid JSON.' },
|
|
3285
|
-
{ role: 'user', content: prompt },
|
|
3286
|
-
], {
|
|
3287
|
-
model: this.config.orchestratorModel,
|
|
3288
|
-
maxTokens: 1500,
|
|
3289
|
-
temperature: 0.2,
|
|
3290
|
-
});
|
|
3291
|
-
this.trackOrchestratorUsage(response, 'auto-split-judge');
|
|
3292
|
-
// Parse response — reuse markdown code block stripping from microDecompose
|
|
3293
|
-
let jsonStr = response.content.trim();
|
|
3294
|
-
const codeBlockMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
3295
|
-
if (codeBlockMatch)
|
|
3296
|
-
jsonStr = codeBlockMatch[1].trim();
|
|
3297
|
-
const parsed = JSON.parse(jsonStr);
|
|
3298
|
-
if (!parsed.shouldSplit) {
|
|
3299
|
-
this.logDecision('auto-split', `${task.id}: judge says no split — ${parsed.reason}`, '');
|
|
3300
|
-
return { shouldSplit: false };
|
|
3301
|
-
}
|
|
3302
|
-
if (!parsed.subtasks || !Array.isArray(parsed.subtasks) || parsed.subtasks.length < 2) {
|
|
3303
|
-
return { shouldSplit: false };
|
|
3304
|
-
}
|
|
3305
|
-
// Build SwarmTask[] from judge output (same pattern as microDecompose)
|
|
3306
|
-
const subtasks = parsed.subtasks.slice(0, maxSubs).map((sub, idx) => ({
|
|
3307
|
-
id: `${task.id}-split${idx + 1}`,
|
|
3308
|
-
description: sub.description,
|
|
3309
|
-
type: sub.type ?? task.type,
|
|
3310
|
-
dependencies: [],
|
|
3311
|
-
status: 'ready',
|
|
3312
|
-
complexity: Math.max(3, Math.min(sub.complexity ?? Math.ceil(task.complexity / 2), task.complexity - 1)),
|
|
3313
|
-
wave: task.wave,
|
|
3314
|
-
targetFiles: sub.targetFiles ?? [],
|
|
3315
|
-
readFiles: task.readFiles,
|
|
3316
|
-
attempts: 0,
|
|
3317
|
-
rescueContext: `Auto-split from ${task.id} (original complexity ${task.complexity})`,
|
|
3318
|
-
}));
|
|
3319
|
-
this.logDecision('auto-split', `${task.id}: split into ${subtasks.length} subtasks — ${parsed.reason}`, subtasks.map(s => `${s.id}: ${s.description.slice(0, 60)}`).join('; '));
|
|
3320
|
-
return { shouldSplit: true, subtasks };
|
|
3321
|
-
}
|
|
3322
|
-
/**
|
|
3323
|
-
* V7: Compute effective retry limit for a task.
|
|
3324
|
-
* F10: Fixup tasks get max 2 retries (3 attempts total) — one full model-failover cycle.
|
|
3325
|
-
* Foundation tasks get +1 retry to reduce cascade failure risk.
|
|
3326
|
-
*/
|
|
3327
|
-
getEffectiveRetries(task) {
|
|
3328
|
-
const isFixup = 'fixesTaskId' in task;
|
|
3329
|
-
if (isFixup)
|
|
3330
|
-
return 2; // Fixup tasks: 2 retries max (3 attempts total)
|
|
3331
|
-
return task.isFoundation ? this.config.workerRetries + 1 : this.config.workerRetries;
|
|
3332
|
-
}
|
|
3333
|
-
/**
|
|
3334
|
-
* F22: Build a brief summary of swarm progress for retry context.
|
|
3335
|
-
* Helps retrying workers understand what the swarm has already accomplished.
|
|
3336
|
-
*/
|
|
3337
|
-
getSwarmProgressSummary() {
|
|
3338
|
-
const allTasks = this.taskQueue.getAllTasks();
|
|
3339
|
-
const completed = allTasks.filter(t => t.status === 'completed');
|
|
3340
|
-
if (completed.length === 0)
|
|
3341
|
-
return '';
|
|
3342
|
-
const lines = [];
|
|
3343
|
-
for (const task of completed) {
|
|
3344
|
-
const score = task.result?.qualityScore ? ` (${task.result.qualityScore}/5)` : '';
|
|
3345
|
-
lines.push(`- ${task.id}: ${task.description.slice(0, 80)}${score}`);
|
|
3346
|
-
}
|
|
3347
|
-
// Collect files created by completed tasks
|
|
3348
|
-
const files = new Set();
|
|
3349
|
-
const baseDir = this.config.facts?.workingDirectory ?? process.cwd();
|
|
3350
|
-
for (const task of completed) {
|
|
3351
|
-
for (const f of (task.result?.filesModified ?? []))
|
|
3352
|
-
files.add(f);
|
|
3353
|
-
for (const f of (task.targetFiles ?? [])) {
|
|
3354
|
-
try {
|
|
3355
|
-
const resolved = path.resolve(baseDir, f);
|
|
3356
|
-
if (fs.existsSync(resolved))
|
|
3357
|
-
files.add(f);
|
|
3358
|
-
}
|
|
3359
|
-
catch { /* skip */ }
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
const parts = [`The following tasks have completed successfully:\n${lines.join('\n')}`];
|
|
3363
|
-
if (files.size > 0) {
|
|
3364
|
-
parts.push(`Files already created/modified: ${[...files].slice(0, 20).join(', ')}`);
|
|
3365
|
-
parts.push('You can build on these existing files.');
|
|
3366
|
-
}
|
|
3367
|
-
return parts.join('\n');
|
|
3368
|
-
}
|
|
3369
|
-
/** Get a model health summary for emitting events. */
|
|
3370
|
-
getModelHealthSummary(model) {
|
|
3371
|
-
const records = this.healthTracker.getAllRecords();
|
|
3372
|
-
const record = records.find(r => r.model === model);
|
|
3373
|
-
return record
|
|
3374
|
-
? { successes: record.successes, failures: record.failures, rateLimits: record.rateLimits, lastRateLimit: record.lastRateLimit, averageLatencyMs: record.averageLatencyMs, healthy: record.healthy }
|
|
3375
|
-
: { successes: 0, failures: 0, rateLimits: 0, averageLatencyMs: 0, healthy: true };
|
|
753
|
+
async finalRescuePassDelegate() {
|
|
754
|
+
const ctx = this.getInternals();
|
|
755
|
+
await finalRescuePass(ctx, (tasks) => this.executeWaveDelegate(tasks));
|
|
756
|
+
this.syncFromInternals(ctx);
|
|
3376
757
|
}
|
|
3377
758
|
}
|
|
3378
759
|
/**
|