@vellumai/assistant 0.5.6 → 0.5.8
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +3 -2
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/memory.md +13 -11
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +120 -1
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -2
- package/src/__tests__/config-schema.test.ts +3 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +59 -30
- package/src/__tests__/credential-execution-managed-contract.test.ts +35 -20
- package/src/__tests__/credential-security-e2e.test.ts +1 -67
- package/src/__tests__/credential-security-invariants.test.ts +6 -50
- package/src/__tests__/credentials-cli.test.ts +82 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +2 -7
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +95 -272
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -7
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +220 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +90 -8
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +129 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +34 -5
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +38 -5
- package/src/calls/voice-session-bridge.ts +7 -12
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +128 -82
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +525 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +47 -2
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +5 -11
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +5 -5
- package/src/config/env.ts +21 -14
- package/src/config/feature-flag-registry.json +49 -9
- package/src/config/loader.ts +106 -42
- package/src/config/schema.ts +9 -29
- package/src/config/schemas/calls.ts +30 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +1 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +21 -1
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +234 -51
- package/src/daemon/message-types/conversations.ts +4 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +32 -95
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/app-store.ts +31 -0
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +16 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +328 -321
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +63 -6
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +98 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/decision-engine.ts +4 -1
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +194 -0
- package/src/prompts/system-prompt.ts +44 -4
- package/src/prompts/templates/SOUL.md +10 -0
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +29 -179
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +17 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +31 -3
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +19 -30
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +46 -14
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +96 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +75 -29
- package/src/security/ces-rpc-credential-backend.ts +86 -0
- package/src/security/credential-backend.ts +22 -92
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +113 -115
- package/src/skills/catalog-install.ts +6 -32
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/executor.ts +0 -4
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/types.ts +0 -8
- package/src/util/browser.ts +15 -0
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +4 -51
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +96 -0
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +27 -5
- package/src/workspace/migrations/registry.ts +12 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
import type { ModelIntent, Provider } from "../providers/types.js";
|
|
2
|
-
import { getLogger } from "../util/logger.js";
|
|
3
|
-
import {
|
|
4
|
-
isCheckpointCompatible,
|
|
5
|
-
loadCheckpoint,
|
|
6
|
-
removeCheckpoint,
|
|
7
|
-
writeCheckpoint,
|
|
8
|
-
} from "./checkpoint.js";
|
|
9
|
-
import { detectCycles } from "./graph-utils.js";
|
|
10
|
-
import type { SwarmLimits } from "./limits.js";
|
|
11
|
-
import { getTimeoutForRole } from "./limits.js";
|
|
12
|
-
import { synthesizeResults } from "./synthesizer.js";
|
|
13
|
-
import type {
|
|
14
|
-
SwarmExecutionSummary,
|
|
15
|
-
SwarmPlan,
|
|
16
|
-
SwarmTaskNode,
|
|
17
|
-
SwarmTaskResult,
|
|
18
|
-
} from "./types.js";
|
|
19
|
-
import type { SwarmWorkerBackend } from "./worker-backend.js";
|
|
20
|
-
import { runWorkerTask } from "./worker-runner.js";
|
|
21
|
-
|
|
22
|
-
const log = getLogger("swarm-orchestrator");
|
|
23
|
-
|
|
24
|
-
export type OrchestratorEventKind =
|
|
25
|
-
| "plan_created"
|
|
26
|
-
| "task_started"
|
|
27
|
-
| "task_completed"
|
|
28
|
-
| "task_failed"
|
|
29
|
-
| "task_blocked"
|
|
30
|
-
| "synthesis_started"
|
|
31
|
-
| "done";
|
|
32
|
-
|
|
33
|
-
export interface OrchestratorEvent {
|
|
34
|
-
kind: OrchestratorEventKind;
|
|
35
|
-
taskId?: string;
|
|
36
|
-
message?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type OrchestratorStatusCallback = (event: OrchestratorEvent) => void;
|
|
40
|
-
|
|
41
|
-
export interface ExecuteSwarmOptions {
|
|
42
|
-
plan: SwarmPlan;
|
|
43
|
-
limits: SwarmLimits;
|
|
44
|
-
backend: SwarmWorkerBackend;
|
|
45
|
-
workingDir: string;
|
|
46
|
-
modelIntent?: string;
|
|
47
|
-
/** Provider + model intent for final synthesis. */
|
|
48
|
-
synthesisProvider?: Provider;
|
|
49
|
-
synthesisModelIntent?: ModelIntent;
|
|
50
|
-
onStatus?: OrchestratorStatusCallback;
|
|
51
|
-
signal?: AbortSignal;
|
|
52
|
-
/** Stable identifier for this swarm run, used for checkpoint persistence. */
|
|
53
|
-
runId?: string;
|
|
54
|
-
/** When true, attempt to resume from the last checkpoint for this runId. */
|
|
55
|
-
resume?: boolean;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Execute a validated swarm plan with parallel DAG scheduling,
|
|
60
|
-
* bounded concurrency, and per-task retries.
|
|
61
|
-
*/
|
|
62
|
-
export async function executeSwarm(
|
|
63
|
-
opts: ExecuteSwarmOptions,
|
|
64
|
-
): Promise<SwarmExecutionSummary> {
|
|
65
|
-
const {
|
|
66
|
-
plan,
|
|
67
|
-
limits,
|
|
68
|
-
backend,
|
|
69
|
-
workingDir,
|
|
70
|
-
modelIntent,
|
|
71
|
-
onStatus,
|
|
72
|
-
signal,
|
|
73
|
-
runId,
|
|
74
|
-
resume,
|
|
75
|
-
} = opts;
|
|
76
|
-
const startTime = Date.now();
|
|
77
|
-
|
|
78
|
-
// Safety net: reject cyclic plans even if the caller skipped validation
|
|
79
|
-
const cycleNodes = detectCycles(plan.tasks);
|
|
80
|
-
if (cycleNodes) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Swarm plan contains a dependency cycle: ${cycleNodes.join(" -> ")}`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
onStatus?.({
|
|
87
|
-
kind: "plan_created",
|
|
88
|
-
message: `Plan with ${plan.tasks.length} tasks`,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const results = new Map<string, SwarmTaskResult>();
|
|
92
|
-
const blocked = new Set<string>();
|
|
93
|
-
|
|
94
|
-
// Restore from checkpoint if resuming
|
|
95
|
-
if (runId && resume) {
|
|
96
|
-
const checkpoint = loadCheckpoint(runId);
|
|
97
|
-
if (checkpoint && isCheckpointCompatible(checkpoint, plan)) {
|
|
98
|
-
for (const result of checkpoint.results) {
|
|
99
|
-
results.set(result.taskId, result);
|
|
100
|
-
}
|
|
101
|
-
for (const taskId of checkpoint.blockedTaskIds) {
|
|
102
|
-
blocked.add(taskId);
|
|
103
|
-
}
|
|
104
|
-
const restored = checkpoint.results.length;
|
|
105
|
-
const restoredBlocked = checkpoint.blockedTaskIds.length;
|
|
106
|
-
log.info({ runId, restored, restoredBlocked }, "Resumed from checkpoint");
|
|
107
|
-
onStatus?.({
|
|
108
|
-
kind: "plan_created",
|
|
109
|
-
message: `Resumed ${restored} completed tasks from checkpoint`,
|
|
110
|
-
});
|
|
111
|
-
} else if (checkpoint) {
|
|
112
|
-
log.warn(
|
|
113
|
-
{ runId },
|
|
114
|
-
"Checkpoint incompatible with current plan, starting fresh",
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Build adjacency for dependency tracking
|
|
120
|
-
const dependents = new Map<string, string[]>();
|
|
121
|
-
for (const task of plan.tasks) {
|
|
122
|
-
dependents.set(task.id, []);
|
|
123
|
-
}
|
|
124
|
-
for (const task of plan.tasks) {
|
|
125
|
-
for (const dep of task.dependencies) {
|
|
126
|
-
dependents.get(dep)?.push(task.id);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Determine initial ready tasks — skip tasks already completed or blocked
|
|
131
|
-
// from a restored checkpoint
|
|
132
|
-
const ready: SwarmTaskNode[] = [];
|
|
133
|
-
const remaining = new Map<string, SwarmTaskNode>();
|
|
134
|
-
const pendingDeps = new Map<string, Set<string>>();
|
|
135
|
-
|
|
136
|
-
for (const task of plan.tasks) {
|
|
137
|
-
if (results.has(task.id) || blocked.has(task.id)) continue;
|
|
138
|
-
remaining.set(task.id, task);
|
|
139
|
-
// Remove already-completed dependencies from this task's pending set
|
|
140
|
-
const unresolvedDeps = task.dependencies.filter((d) => !results.has(d));
|
|
141
|
-
if (unresolvedDeps.length === 0) {
|
|
142
|
-
ready.push(task);
|
|
143
|
-
} else {
|
|
144
|
-
pendingDeps.set(task.id, new Set(unresolvedDeps));
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Concurrent DAG executor — schedule tasks as soon as their prerequisites
|
|
149
|
-
// finish, bounded by maxWorkers. Unlike wave/batch execution, a newly
|
|
150
|
-
// unblocked task can start immediately when a worker slot opens up rather
|
|
151
|
-
// than waiting for the entire previous batch to complete.
|
|
152
|
-
let activeCount = 0;
|
|
153
|
-
let resolve: (() => void) | null = null;
|
|
154
|
-
|
|
155
|
-
// Resolves whenever a running task completes (or the ready queue is refilled)
|
|
156
|
-
// so the main loop can re-evaluate whether to launch more work.
|
|
157
|
-
function signalProgress(): void {
|
|
158
|
-
if (resolve) {
|
|
159
|
-
const r = resolve;
|
|
160
|
-
resolve = null;
|
|
161
|
-
r();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function waitForProgress(): Promise<void> {
|
|
166
|
-
return new Promise<void>((r) => {
|
|
167
|
-
resolve = r;
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function processResult(result: SwarmTaskResult): void {
|
|
172
|
-
results.set(result.taskId, result);
|
|
173
|
-
remaining.delete(result.taskId);
|
|
174
|
-
|
|
175
|
-
if (result.status === "completed") {
|
|
176
|
-
onStatus?.({ kind: "task_completed", taskId: result.taskId });
|
|
177
|
-
// Immediately unblock dependents so they enter the ready queue
|
|
178
|
-
for (const depId of dependents.get(result.taskId) ?? []) {
|
|
179
|
-
const pending = pendingDeps.get(depId);
|
|
180
|
-
if (pending) {
|
|
181
|
-
pending.delete(result.taskId);
|
|
182
|
-
if (pending.size === 0) {
|
|
183
|
-
pendingDeps.delete(depId);
|
|
184
|
-
const task = plan.tasks.find((t) => t.id === depId);
|
|
185
|
-
if (task && !blocked.has(depId)) {
|
|
186
|
-
ready.push(task);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
onStatus?.({ kind: "task_failed", taskId: result.taskId });
|
|
193
|
-
blockDependents(result.taskId, dependents, blocked, onStatus);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (runId) {
|
|
197
|
-
writeCheckpoint(runId, plan, results, blocked);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function runTask(task: SwarmTaskNode): Promise<void> {
|
|
202
|
-
onStatus?.({ kind: "task_started", taskId: task.id });
|
|
203
|
-
|
|
204
|
-
const depOutputs = task.dependencies
|
|
205
|
-
.map((depId) => {
|
|
206
|
-
const r = results.get(depId);
|
|
207
|
-
return r ? { taskId: depId, summary: r.summary } : null;
|
|
208
|
-
})
|
|
209
|
-
.filter((d): d is { taskId: string; summary: string } => d != null);
|
|
210
|
-
|
|
211
|
-
let result: SwarmTaskResult;
|
|
212
|
-
try {
|
|
213
|
-
const taskTimeoutMs = getTimeoutForRole(limits, task.role) * 1000;
|
|
214
|
-
|
|
215
|
-
result = await runWorkerTask({
|
|
216
|
-
task,
|
|
217
|
-
upstreamContext: plan.objective,
|
|
218
|
-
dependencyOutputs: depOutputs,
|
|
219
|
-
backend,
|
|
220
|
-
workingDir,
|
|
221
|
-
modelIntent,
|
|
222
|
-
timeoutMs: taskTimeoutMs,
|
|
223
|
-
signal,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
let retries = 0;
|
|
227
|
-
while (
|
|
228
|
-
result.status === "failed" &&
|
|
229
|
-
retries < limits.maxRetriesPerTask &&
|
|
230
|
-
!signal?.aborted
|
|
231
|
-
) {
|
|
232
|
-
retries++;
|
|
233
|
-
// Exponential backoff with ±25% jitter to prevent thundering herd
|
|
234
|
-
const baseDelayMs = 1000 * Math.pow(2, retries - 1);
|
|
235
|
-
const jitter = baseDelayMs * 0.25 * (2 * Math.random() - 1);
|
|
236
|
-
await abortableSleep(baseDelayMs + jitter, signal);
|
|
237
|
-
if (signal?.aborted) break;
|
|
238
|
-
result = await runWorkerTask({
|
|
239
|
-
task,
|
|
240
|
-
upstreamContext: plan.objective,
|
|
241
|
-
dependencyOutputs: depOutputs,
|
|
242
|
-
backend,
|
|
243
|
-
workingDir,
|
|
244
|
-
modelIntent,
|
|
245
|
-
timeoutMs: taskTimeoutMs,
|
|
246
|
-
signal,
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
result.retryCount = retries;
|
|
250
|
-
|
|
251
|
-
if (result.status === "failed") {
|
|
252
|
-
log.error(
|
|
253
|
-
{ taskId: task.id, error: result.summary, retries },
|
|
254
|
-
"Swarm task execution failed",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
} catch (err) {
|
|
258
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
259
|
-
log.error(
|
|
260
|
-
{ taskId: task.id, error: error.message, stack: error.stack },
|
|
261
|
-
"Swarm task execution failed unexpectedly",
|
|
262
|
-
);
|
|
263
|
-
result = {
|
|
264
|
-
taskId: task.id,
|
|
265
|
-
status: "failed",
|
|
266
|
-
summary: `Unexpected error: ${error.message}`,
|
|
267
|
-
artifacts: [],
|
|
268
|
-
issues: [error.message],
|
|
269
|
-
nextSteps: [],
|
|
270
|
-
rawOutput: "",
|
|
271
|
-
durationMs: 0,
|
|
272
|
-
retryCount: 0,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
processResult(result);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
while (ready.length > 0 || activeCount > 0) {
|
|
280
|
-
if (signal?.aborted) break;
|
|
281
|
-
|
|
282
|
-
// Launch as many ready tasks as worker slots allow
|
|
283
|
-
while (ready.length > 0 && activeCount < limits.maxWorkers) {
|
|
284
|
-
const task = ready.shift()!;
|
|
285
|
-
activeCount++;
|
|
286
|
-
// Fire-and-forget — completion is handled inside runTask.
|
|
287
|
-
// .finally() ensures activeCount is decremented exactly once
|
|
288
|
-
// regardless of where an error occurs (e.g. a throwing onStatus
|
|
289
|
-
// callback inside processResult). .catch() suppresses the
|
|
290
|
-
// unhandled rejection for fire-and-forget usage.
|
|
291
|
-
runTask(task)
|
|
292
|
-
.finally(() => {
|
|
293
|
-
activeCount--;
|
|
294
|
-
signalProgress();
|
|
295
|
-
})
|
|
296
|
-
.catch((err) => {
|
|
297
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
298
|
-
log.error(
|
|
299
|
-
{ taskId: task.id, error: error.message, stack: error.stack },
|
|
300
|
-
"Swarm task runner failed",
|
|
301
|
-
);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Nothing left to launch and nothing running — we're done
|
|
306
|
-
if (activeCount === 0 && ready.length === 0) break;
|
|
307
|
-
|
|
308
|
-
// Wait until a running task completes (or a new task becomes ready)
|
|
309
|
-
await waitForProgress();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Let in-flight workers settle before finalizing — if we broke out of the
|
|
313
|
-
// loop due to abort, workers may still be running and would otherwise emit
|
|
314
|
-
// events (task_completed / task_failed) after the 'done' event.
|
|
315
|
-
while (activeCount > 0) {
|
|
316
|
-
await waitForProgress();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Mark any remaining tasks that were never reached as blocked
|
|
320
|
-
for (const [taskId] of remaining) {
|
|
321
|
-
if (!results.has(taskId) && !blocked.has(taskId)) {
|
|
322
|
-
blocked.add(taskId);
|
|
323
|
-
onStatus?.({ kind: "task_blocked", taskId });
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Synthesize final answer (skip LLM synthesis when aborted, but still
|
|
328
|
-
// build the markdown fallback so partial results are preserved)
|
|
329
|
-
const allResults = Array.from(results.values());
|
|
330
|
-
|
|
331
|
-
let finalAnswer: string;
|
|
332
|
-
if (opts.synthesisProvider && !signal?.aborted) {
|
|
333
|
-
onStatus?.({ kind: "synthesis_started" });
|
|
334
|
-
finalAnswer = await synthesizeResults({
|
|
335
|
-
objective: plan.objective,
|
|
336
|
-
results: allResults,
|
|
337
|
-
provider: opts.synthesisProvider,
|
|
338
|
-
modelIntent: opts.synthesisModelIntent ?? "quality-optimized",
|
|
339
|
-
});
|
|
340
|
-
} else {
|
|
341
|
-
if (!signal?.aborted) onStatus?.({ kind: "synthesis_started" });
|
|
342
|
-
finalAnswer = buildMarkdownFallback(plan.objective, allResults);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const totalDurationMs = Date.now() - startTime;
|
|
346
|
-
|
|
347
|
-
// Clean up checkpoint on successful completion (not on abort, so it can be resumed)
|
|
348
|
-
if (runId && !signal?.aborted) {
|
|
349
|
-
removeCheckpoint(runId);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
onStatus?.({ kind: "done", message: `Completed in ${totalDurationMs}ms` });
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
objective: plan.objective,
|
|
356
|
-
plan,
|
|
357
|
-
results: allResults,
|
|
358
|
-
finalAnswer,
|
|
359
|
-
stats: {
|
|
360
|
-
totalTasks: plan.tasks.length,
|
|
361
|
-
completed: allResults.filter((r) => r.status === "completed").length,
|
|
362
|
-
failed: allResults.filter((r) => r.status === "failed").length,
|
|
363
|
-
blocked: blocked.size,
|
|
364
|
-
totalDurationMs,
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function blockDependents(
|
|
370
|
-
taskId: string,
|
|
371
|
-
dependents: Map<string, string[]>,
|
|
372
|
-
blocked: Set<string>,
|
|
373
|
-
onStatus?: OrchestratorStatusCallback,
|
|
374
|
-
): void {
|
|
375
|
-
for (const depId of dependents.get(taskId) ?? []) {
|
|
376
|
-
if (!blocked.has(depId)) {
|
|
377
|
-
blocked.add(depId);
|
|
378
|
-
onStatus?.({ kind: "task_blocked", taskId: depId });
|
|
379
|
-
blockDependents(depId, dependents, blocked, onStatus);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/** Resolves after `ms` milliseconds, or immediately if the signal fires first. */
|
|
385
|
-
function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
386
|
-
if (signal?.aborted) return Promise.resolve();
|
|
387
|
-
return new Promise<void>((resolve) => {
|
|
388
|
-
const timer = setTimeout(done, ms);
|
|
389
|
-
signal?.addEventListener("abort", done, { once: true });
|
|
390
|
-
function done() {
|
|
391
|
-
clearTimeout(timer);
|
|
392
|
-
signal?.removeEventListener("abort", done);
|
|
393
|
-
resolve();
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function buildMarkdownFallback(
|
|
399
|
-
objective: string,
|
|
400
|
-
results: SwarmTaskResult[],
|
|
401
|
-
): string {
|
|
402
|
-
const lines: string[] = [`## Swarm Results: ${objective}`, ""];
|
|
403
|
-
|
|
404
|
-
const completed = results.filter((r) => r.status === "completed");
|
|
405
|
-
const failed = results.filter((r) => r.status === "failed");
|
|
406
|
-
|
|
407
|
-
if (completed.length > 0) {
|
|
408
|
-
lines.push("### Completed Tasks");
|
|
409
|
-
for (const r of completed) {
|
|
410
|
-
lines.push(`- **${r.taskId}**: ${r.summary}`);
|
|
411
|
-
}
|
|
412
|
-
lines.push("");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (failed.length > 0) {
|
|
416
|
-
lines.push("### Failed Tasks");
|
|
417
|
-
for (const r of failed) {
|
|
418
|
-
lines.push(`- **${r.taskId}**: ${r.summary}`);
|
|
419
|
-
}
|
|
420
|
-
lines.push("");
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return lines.join("\n");
|
|
424
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { getLogger } from "../util/logger.js";
|
|
2
|
-
import { detectCycles } from "./graph-utils.js";
|
|
3
|
-
import type { SwarmLimits } from "./limits.js";
|
|
4
|
-
import type { SwarmPlan, SwarmTaskNode } from "./types.js";
|
|
5
|
-
import { VALID_SWARM_ROLES } from "./types.js";
|
|
6
|
-
|
|
7
|
-
const log = getLogger("swarm:plan-validator");
|
|
8
|
-
|
|
9
|
-
export class SwarmPlanValidationError extends Error {
|
|
10
|
-
constructor(
|
|
11
|
-
message: string,
|
|
12
|
-
public readonly issues: string[],
|
|
13
|
-
) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = "SwarmPlanValidationError";
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Validate and normalize a swarm plan. Returns the validated plan or throws
|
|
21
|
-
* SwarmPlanValidationError with all detected issues.
|
|
22
|
-
*/
|
|
23
|
-
export function validateAndNormalizePlan(
|
|
24
|
-
plan: SwarmPlan,
|
|
25
|
-
limits: SwarmLimits,
|
|
26
|
-
): SwarmPlan {
|
|
27
|
-
const issues: string[] = [];
|
|
28
|
-
|
|
29
|
-
// --- Basic structure ---
|
|
30
|
-
if (!plan.objective || typeof plan.objective !== "string") {
|
|
31
|
-
issues.push("Plan must have a non-empty objective string.");
|
|
32
|
-
}
|
|
33
|
-
if (!Array.isArray(plan.tasks) || plan.tasks.length === 0) {
|
|
34
|
-
issues.push("Plan must have at least one task.");
|
|
35
|
-
throw new SwarmPlanValidationError("Plan validation failed", issues);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- Normalize dependencies early (before any checks that iterate them) ---
|
|
39
|
-
let tasks: SwarmTaskNode[] = plan.tasks.map((t) => ({
|
|
40
|
-
...t,
|
|
41
|
-
dependencies: Array.isArray(t.dependencies) ? t.dependencies : [],
|
|
42
|
-
}));
|
|
43
|
-
const originalIds = new Set(tasks.map((task) => task.id));
|
|
44
|
-
|
|
45
|
-
// --- Task count limit ---
|
|
46
|
-
if (tasks.length > limits.maxTasks) {
|
|
47
|
-
log.warn(
|
|
48
|
-
{ original: tasks.length, max: limits.maxTasks },
|
|
49
|
-
"Plan truncated to task limit",
|
|
50
|
-
);
|
|
51
|
-
tasks = tasks.slice(0, limits.maxTasks);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// --- Unique IDs ---
|
|
55
|
-
const ids = new Set<string>();
|
|
56
|
-
for (const task of tasks) {
|
|
57
|
-
if (!task.id || typeof task.id !== "string") {
|
|
58
|
-
issues.push(`Task has invalid or empty id.`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (ids.has(task.id)) {
|
|
62
|
-
issues.push(`Duplicate task id: "${task.id}".`);
|
|
63
|
-
}
|
|
64
|
-
ids.add(task.id);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// --- Valid roles ---
|
|
68
|
-
for (const task of tasks) {
|
|
69
|
-
if (!VALID_SWARM_ROLES.includes(task.role)) {
|
|
70
|
-
issues.push(
|
|
71
|
-
`Task "${task.id}" has invalid role "${
|
|
72
|
-
task.role
|
|
73
|
-
}". Valid roles: ${VALID_SWARM_ROLES.join(", ")}.`,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// --- Strip orphaned dependency references (from truncation) and report real errors ---
|
|
79
|
-
for (const task of tasks) {
|
|
80
|
-
const valid: string[] = [];
|
|
81
|
-
for (const dep of task.dependencies) {
|
|
82
|
-
if (!ids.has(dep)) {
|
|
83
|
-
if (!originalIds.has(dep)) {
|
|
84
|
-
issues.push(`Task "${task.id}" depends on unknown task "${dep}".`);
|
|
85
|
-
}
|
|
86
|
-
} else {
|
|
87
|
-
valid.push(dep);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
task.dependencies = valid;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// --- Cycle detection (Kahn's algorithm) ---
|
|
94
|
-
if (!hasDuplicateIds(tasks) && issues.length === 0) {
|
|
95
|
-
const cycleNodes = detectCycles(tasks);
|
|
96
|
-
if (cycleNodes) {
|
|
97
|
-
issues.push(
|
|
98
|
-
`Plan contains a dependency cycle: ${cycleNodes.join(" -> ")}.`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (issues.length > 0) {
|
|
104
|
-
throw new SwarmPlanValidationError("Plan validation failed", issues);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return { ...plan, tasks };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function hasDuplicateIds(tasks: SwarmTaskNode[]): boolean {
|
|
111
|
-
const seen = new Set<string>();
|
|
112
|
-
for (const t of tasks) {
|
|
113
|
-
if (seen.has(t.id)) return true;
|
|
114
|
-
seen.add(t.id);
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import type { Message, ModelIntent, Provider } from "../providers/types.js";
|
|
2
|
-
import { parseJsonSafe } from "../util/json.js";
|
|
3
|
-
import type { SwarmLimits } from "./limits.js";
|
|
4
|
-
import { validateAndNormalizePlan } from "./plan-validator.js";
|
|
5
|
-
import {
|
|
6
|
-
buildPlannerUserMessage,
|
|
7
|
-
ROUTER_SYSTEM_PROMPT,
|
|
8
|
-
} from "./router-prompts.js";
|
|
9
|
-
import type { SwarmPlan, SwarmTaskNode } from "./types.js";
|
|
10
|
-
import { VALID_SWARM_ROLES } from "./types.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Generate a validated swarm plan from a user objective using an LLM.
|
|
14
|
-
* Falls back to a single-coder plan if generation or validation fails.
|
|
15
|
-
*/
|
|
16
|
-
export async function generatePlan(opts: {
|
|
17
|
-
objective: string;
|
|
18
|
-
provider: Provider;
|
|
19
|
-
modelIntent: ModelIntent;
|
|
20
|
-
limits: SwarmLimits;
|
|
21
|
-
}): Promise<SwarmPlan> {
|
|
22
|
-
const { objective, provider, modelIntent, limits } = opts;
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
const messages: Message[] = [
|
|
26
|
-
{
|
|
27
|
-
role: "user",
|
|
28
|
-
content: [
|
|
29
|
-
{
|
|
30
|
-
type: "text",
|
|
31
|
-
text: buildPlannerUserMessage(objective, limits.maxTasks),
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
},
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const response = await provider.sendMessage(
|
|
38
|
-
messages,
|
|
39
|
-
undefined,
|
|
40
|
-
ROUTER_SYSTEM_PROMPT,
|
|
41
|
-
{ config: { max_tokens: 2048, modelIntent } },
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Extract text from response
|
|
45
|
-
const textBlock = response.content.find((b) => b.type === "text");
|
|
46
|
-
if (!textBlock || textBlock.type !== "text") {
|
|
47
|
-
return makeFallbackPlan(objective);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const rawPlan = parsePlanJSON(textBlock.text);
|
|
51
|
-
if (!rawPlan) {
|
|
52
|
-
return makeFallbackPlan(objective);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const validatedTasks = validateRawTasks(rawPlan.tasks);
|
|
56
|
-
if (validatedTasks.length === 0) {
|
|
57
|
-
return makeFallbackPlan(objective);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const plan: SwarmPlan = {
|
|
61
|
-
objective,
|
|
62
|
-
tasks: validatedTasks,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
return validateAndNormalizePlan(plan, limits);
|
|
66
|
-
} catch {
|
|
67
|
-
return makeFallbackPlan(objective);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Parse the LLM output as a plan JSON. Handles bare JSON objects and
|
|
73
|
-
* fenced code blocks (tries all fenced blocks, not just the first).
|
|
74
|
-
*/
|
|
75
|
-
export function parsePlanJSON(raw: string): {
|
|
76
|
-
tasks: Array<{
|
|
77
|
-
id: string;
|
|
78
|
-
role: string;
|
|
79
|
-
objective: string;
|
|
80
|
-
dependencies: string[];
|
|
81
|
-
}>;
|
|
82
|
-
} | null {
|
|
83
|
-
// Try all fenced code blocks — LLMs sometimes emit non-JSON blocks before the plan
|
|
84
|
-
const fencedRegex = /```(?:json)?\s*\n?([\s\S]*?)\n?```/g;
|
|
85
|
-
let match;
|
|
86
|
-
while ((match = fencedRegex.exec(raw)) != null) {
|
|
87
|
-
const result = tryParsePlan(match[1]);
|
|
88
|
-
if (result) return result;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Fall back to parsing the raw string as bare JSON
|
|
92
|
-
return tryParsePlan(raw);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function tryParsePlan(jsonStr: string): {
|
|
96
|
-
tasks: Array<{
|
|
97
|
-
id: string;
|
|
98
|
-
role: string;
|
|
99
|
-
objective: string;
|
|
100
|
-
dependencies: string[];
|
|
101
|
-
}>;
|
|
102
|
-
} | null {
|
|
103
|
-
const parsed = parseJsonSafe<{ tasks?: unknown }>(jsonStr.trim());
|
|
104
|
-
if (parsed && Array.isArray(parsed.tasks)) {
|
|
105
|
-
return parsed as {
|
|
106
|
-
tasks: Array<{
|
|
107
|
-
id: string;
|
|
108
|
-
role: string;
|
|
109
|
-
objective: string;
|
|
110
|
-
dependencies: string[];
|
|
111
|
-
}>;
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Validate that raw parsed task objects have the required fields and correct types
|
|
119
|
-
* before treating them as SwarmTaskNode[]. Filters out malformed entries so a
|
|
120
|
-
* partially valid LLM response can still produce a usable plan.
|
|
121
|
-
*/
|
|
122
|
-
function validateRawTasks(raw: unknown[]): SwarmTaskNode[] {
|
|
123
|
-
const validRoles = new Set<string>(VALID_SWARM_ROLES);
|
|
124
|
-
const validated: SwarmTaskNode[] = [];
|
|
125
|
-
|
|
126
|
-
for (const item of raw) {
|
|
127
|
-
if (item == null || typeof item !== "object") continue;
|
|
128
|
-
const t = item as Record<string, unknown>;
|
|
129
|
-
|
|
130
|
-
if (typeof t.id !== "string" || t.id === "") continue;
|
|
131
|
-
if (typeof t.role !== "string" || !validRoles.has(t.role)) continue;
|
|
132
|
-
if (typeof t.objective !== "string" || t.objective === "") continue;
|
|
133
|
-
|
|
134
|
-
validated.push({
|
|
135
|
-
id: t.id,
|
|
136
|
-
role: t.role as SwarmTaskNode["role"],
|
|
137
|
-
objective: t.objective,
|
|
138
|
-
dependencies: Array.isArray(t.dependencies)
|
|
139
|
-
? t.dependencies.filter((d): d is string => typeof d === "string")
|
|
140
|
-
: [],
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return validated;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Deterministic fallback: a single coder task for the full objective.
|
|
149
|
-
*/
|
|
150
|
-
export function makeFallbackPlan(objective: string): SwarmPlan {
|
|
151
|
-
return {
|
|
152
|
-
objective,
|
|
153
|
-
tasks: [
|
|
154
|
-
{
|
|
155
|
-
id: "fallback-coder",
|
|
156
|
-
role: "coder",
|
|
157
|
-
objective,
|
|
158
|
-
dependencies: [],
|
|
159
|
-
},
|
|
160
|
-
],
|
|
161
|
-
};
|
|
162
|
-
}
|