@vellumai/assistant 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -1,384 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import type { SwarmPlan } from "../swarm/index.js";
4
- import {
5
- resolveSwarmLimits,
6
- SwarmPlanValidationError,
7
- validateAndNormalizePlan,
8
- } from "../swarm/index.js";
9
-
10
- const DEFAULT_LIMITS = resolveSwarmLimits({
11
- maxWorkers: 3,
12
- maxTasks: 8,
13
- maxRetriesPerTask: 1,
14
- workerTimeoutSec: 900,
15
- });
16
-
17
- function makePlan(overrides?: Partial<SwarmPlan>): SwarmPlan {
18
- return {
19
- objective: "Test objective",
20
- tasks: [
21
- {
22
- id: "task-1",
23
- role: "coder",
24
- objective: "Write code",
25
- dependencies: [],
26
- },
27
- ],
28
- ...overrides,
29
- };
30
- }
31
-
32
- describe("validateAndNormalizePlan", () => {
33
- test("accepts a valid single-task plan", () => {
34
- const plan = makePlan();
35
- const result = validateAndNormalizePlan(plan, DEFAULT_LIMITS);
36
- expect(result.tasks).toHaveLength(1);
37
- expect(result.tasks[0].id).toBe("task-1");
38
- });
39
-
40
- test("accepts a valid multi-task DAG", () => {
41
- const plan = makePlan({
42
- tasks: [
43
- {
44
- id: "research",
45
- role: "researcher",
46
- objective: "Research",
47
- dependencies: [],
48
- },
49
- {
50
- id: "code",
51
- role: "coder",
52
- objective: "Code",
53
- dependencies: ["research"],
54
- },
55
- {
56
- id: "review",
57
- role: "reviewer",
58
- objective: "Review",
59
- dependencies: ["code"],
60
- },
61
- ],
62
- });
63
- const result = validateAndNormalizePlan(plan, DEFAULT_LIMITS);
64
- expect(result.tasks).toHaveLength(3);
65
- });
66
-
67
- test("accepts parallel independent tasks", () => {
68
- const plan = makePlan({
69
- tasks: [
70
- { id: "a", role: "coder", objective: "Task A", dependencies: [] },
71
- { id: "b", role: "coder", objective: "Task B", dependencies: [] },
72
- {
73
- id: "c",
74
- role: "reviewer",
75
- objective: "Review both",
76
- dependencies: ["a", "b"],
77
- },
78
- ],
79
- });
80
- const result = validateAndNormalizePlan(plan, DEFAULT_LIMITS);
81
- expect(result.tasks).toHaveLength(3);
82
- });
83
-
84
- test("rejects empty tasks array", () => {
85
- expect(() =>
86
- validateAndNormalizePlan(makePlan({ tasks: [] }), DEFAULT_LIMITS),
87
- ).toThrow(SwarmPlanValidationError);
88
- });
89
-
90
- test("rejects duplicate task IDs", () => {
91
- const plan = makePlan({
92
- tasks: [
93
- { id: "dup", role: "coder", objective: "A", dependencies: [] },
94
- { id: "dup", role: "coder", objective: "B", dependencies: [] },
95
- ],
96
- });
97
- try {
98
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
99
- expect(true).toBe(false); // should not reach here
100
- } catch (e) {
101
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
102
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
103
- expect.stringContaining("Duplicate task id"),
104
- );
105
- }
106
- });
107
-
108
- test("rejects invalid role", () => {
109
- const plan = makePlan({
110
- tasks: [
111
- {
112
- id: "bad",
113
- role: "hacker" as any,
114
- objective: "Hack",
115
- dependencies: [],
116
- },
117
- ],
118
- });
119
- try {
120
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
121
- expect(true).toBe(false);
122
- } catch (e) {
123
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
124
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
125
- expect.stringContaining("invalid role"),
126
- );
127
- }
128
- });
129
-
130
- test("rejects unknown dependency reference", () => {
131
- const plan = makePlan({
132
- tasks: [
133
- {
134
- id: "a",
135
- role: "coder",
136
- objective: "A",
137
- dependencies: ["nonexistent"],
138
- },
139
- ],
140
- });
141
- try {
142
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
143
- expect(true).toBe(false);
144
- } catch (e) {
145
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
146
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
147
- expect.stringContaining('unknown task "nonexistent"'),
148
- );
149
- }
150
- });
151
-
152
- test("detects simple cycle (A -> B -> A)", () => {
153
- const plan = makePlan({
154
- tasks: [
155
- { id: "a", role: "coder", objective: "A", dependencies: ["b"] },
156
- { id: "b", role: "coder", objective: "B", dependencies: ["a"] },
157
- ],
158
- });
159
- try {
160
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
161
- expect(true).toBe(false);
162
- } catch (e) {
163
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
164
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
165
- expect.stringContaining("cycle"),
166
- );
167
- }
168
- });
169
-
170
- test("detects self-cycle", () => {
171
- const plan = makePlan({
172
- tasks: [{ id: "a", role: "coder", objective: "A", dependencies: ["a"] }],
173
- });
174
- try {
175
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
176
- expect(true).toBe(false);
177
- } catch (e) {
178
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
179
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
180
- expect.stringContaining("cycle"),
181
- );
182
- }
183
- });
184
-
185
- test("detects longer cycle (A -> B -> C -> A)", () => {
186
- const plan = makePlan({
187
- tasks: [
188
- { id: "a", role: "coder", objective: "A", dependencies: ["c"] },
189
- { id: "b", role: "coder", objective: "B", dependencies: ["a"] },
190
- { id: "c", role: "coder", objective: "C", dependencies: ["b"] },
191
- ],
192
- });
193
- try {
194
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
195
- expect(true).toBe(false);
196
- } catch (e) {
197
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
198
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
199
- expect.stringContaining("cycle"),
200
- );
201
- }
202
- });
203
-
204
- test("truncates tasks exceeding maxTasks limit", () => {
205
- const tasks = Array.from({ length: 12 }, (_, i) => ({
206
- id: `t${i}`,
207
- role: "coder" as const,
208
- objective: `Task ${i}`,
209
- dependencies: [] as string[],
210
- }));
211
- const plan = makePlan({ tasks });
212
- // Use a limit of 5
213
- const limits = resolveSwarmLimits({
214
- maxWorkers: 3,
215
- maxTasks: 5,
216
- maxRetriesPerTask: 1,
217
- workerTimeoutSec: 900,
218
- });
219
- const result = validateAndNormalizePlan(plan, limits);
220
- expect(result.tasks).toHaveLength(5);
221
- expect(result.tasks[0].id).toBe("t0");
222
- expect(result.tasks[4].id).toBe("t4");
223
- });
224
-
225
- test("strips dependencies that point to tasks removed by truncation", () => {
226
- const plan = makePlan({
227
- tasks: [
228
- { id: "t0", role: "coder", objective: "Task 0", dependencies: [] },
229
- { id: "t1", role: "coder", objective: "Task 1", dependencies: ["t4"] },
230
- {
231
- id: "t2",
232
- role: "reviewer",
233
- objective: "Task 2",
234
- dependencies: ["t0"],
235
- },
236
- { id: "t3", role: "coder", objective: "Task 3", dependencies: [] },
237
- { id: "t4", role: "coder", objective: "Task 4", dependencies: [] },
238
- ],
239
- });
240
- const limits = resolveSwarmLimits({
241
- maxWorkers: 3,
242
- maxTasks: 3,
243
- maxRetriesPerTask: 1,
244
- workerTimeoutSec: 900,
245
- });
246
-
247
- const result = validateAndNormalizePlan(plan, limits);
248
- expect(result.tasks).toHaveLength(3);
249
- const t1 = result.tasks.find((task) => task.id === "t1");
250
- expect(t1?.dependencies).toEqual([]);
251
- });
252
-
253
- test("rejects empty objective", () => {
254
- const plan = makePlan({ objective: "" });
255
- try {
256
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
257
- expect(true).toBe(false);
258
- } catch (e) {
259
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
260
- expect((e as SwarmPlanValidationError).issues).toContainEqual(
261
- expect.stringContaining("objective"),
262
- );
263
- }
264
- });
265
-
266
- test("normalizes missing dependencies to empty array", () => {
267
- const plan: SwarmPlan = {
268
- objective: "Test",
269
- tasks: [
270
- {
271
- id: "a",
272
- role: "coder",
273
- objective: "A",
274
- dependencies: undefined as any,
275
- },
276
- ],
277
- };
278
- const result = validateAndNormalizePlan(plan, DEFAULT_LIMITS);
279
- expect(result.tasks[0].dependencies).toEqual([]);
280
- });
281
-
282
- test("collects multiple issues in a single error", () => {
283
- const plan = makePlan({
284
- objective: "",
285
- tasks: [
286
- {
287
- id: "a",
288
- role: "invalid" as any,
289
- objective: "A",
290
- dependencies: ["nonexistent"],
291
- },
292
- { id: "a", role: "coder", objective: "B", dependencies: [] },
293
- ],
294
- });
295
- try {
296
- validateAndNormalizePlan(plan, DEFAULT_LIMITS);
297
- expect(true).toBe(false);
298
- } catch (e) {
299
- expect(e).toBeInstanceOf(SwarmPlanValidationError);
300
- const err = e as SwarmPlanValidationError;
301
- // Should have at least 3 issues: empty objective, invalid role, duplicate id
302
- expect(err.issues.length).toBeGreaterThanOrEqual(3);
303
- }
304
- });
305
-
306
- test("accepts all valid roles", () => {
307
- const roles = ["router", "researcher", "coder", "reviewer"] as const;
308
- for (const role of roles) {
309
- const plan = makePlan({
310
- tasks: [
311
- {
312
- id: `task-${role}`,
313
- role,
314
- objective: `${role} task`,
315
- dependencies: [],
316
- },
317
- ],
318
- });
319
- const result = validateAndNormalizePlan(plan, DEFAULT_LIMITS);
320
- expect(result.tasks[0].role).toBe(role);
321
- }
322
- });
323
- });
324
-
325
- describe("resolveSwarmLimits", () => {
326
- test("clamps maxWorkers to hard ceiling", () => {
327
- const limits = resolveSwarmLimits({
328
- maxWorkers: 100,
329
- maxTasks: 8,
330
- maxRetriesPerTask: 1,
331
- workerTimeoutSec: 900,
332
- });
333
- expect(limits.maxWorkers).toBe(6);
334
- });
335
-
336
- test("clamps maxTasks to hard ceiling", () => {
337
- const limits = resolveSwarmLimits({
338
- maxWorkers: 3,
339
- maxTasks: 100,
340
- maxRetriesPerTask: 1,
341
- workerTimeoutSec: 900,
342
- });
343
- expect(limits.maxTasks).toBe(20);
344
- });
345
-
346
- test("clamps maxRetriesPerTask to hard ceiling", () => {
347
- const limits = resolveSwarmLimits({
348
- maxWorkers: 3,
349
- maxTasks: 8,
350
- maxRetriesPerTask: 100,
351
- workerTimeoutSec: 900,
352
- });
353
- expect(limits.maxRetriesPerTask).toBe(3);
354
- });
355
-
356
- test("clamps zero/negative values to minimums", () => {
357
- const limits = resolveSwarmLimits({
358
- maxWorkers: 0,
359
- maxTasks: -1,
360
- maxRetriesPerTask: -5,
361
- workerTimeoutSec: 0,
362
- });
363
- expect(limits.maxWorkers).toBe(1);
364
- expect(limits.maxTasks).toBe(1);
365
- expect(limits.maxRetriesPerTask).toBe(0);
366
- expect(limits.workerTimeoutSec).toBe(1);
367
- });
368
-
369
- test("passes through valid values unchanged", () => {
370
- const limits = resolveSwarmLimits({
371
- maxWorkers: 3,
372
- maxTasks: 8,
373
- maxRetriesPerTask: 1,
374
- workerTimeoutSec: 900,
375
- });
376
- expect(limits).toEqual({
377
- maxWorkers: 3,
378
- maxTasks: 8,
379
- maxRetriesPerTask: 1,
380
- workerTimeoutSec: 900,
381
- roleTimeoutsSec: {},
382
- });
383
- });
384
- });
@@ -1,197 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- // ---------------------------------------------------------------------------
4
- // Gate that the SDK mock waits on, letting us hold a swarm active while
5
- // attempting a second invocation.
6
- // ---------------------------------------------------------------------------
7
-
8
- let gate: { promise: Promise<void>; resolve: () => void } | null = null;
9
-
10
- function openGate() {
11
- let resolve!: () => void;
12
- const promise = new Promise<void>((r) => {
13
- resolve = r;
14
- });
15
- gate = { promise, resolve };
16
- }
17
-
18
- function closeGate() {
19
- gate?.resolve();
20
- gate = null;
21
- }
22
-
23
- // ---------------------------------------------------------------------------
24
- // Mocks — declared before imports
25
- // ---------------------------------------------------------------------------
26
-
27
- mock.module("../util/logger.js", () => ({
28
- getLogger: () =>
29
- new Proxy({} as Record<string, unknown>, {
30
- get: () => () => {},
31
- }),
32
- }));
33
-
34
- mock.module("../config/loader.js", () => ({
35
- getConfig: () => ({
36
- ui: {},
37
-
38
- provider: "anthropic",
39
- providerOrder: ["anthropic"],
40
- swarm: {
41
- enabled: true,
42
- maxWorkers: 3,
43
- maxTasks: 8,
44
- maxRetriesPerTask: 1,
45
- workerTimeoutSec: 900,
46
- roleTimeoutsSec: {},
47
- plannerModelIntent: "latency-optimized",
48
- synthesizerModelIntent: "quality-optimized",
49
- },
50
- services: {
51
- inference: {
52
- mode: "your-own",
53
- provider: "anthropic",
54
- model: "claude-opus-4-6",
55
- },
56
- "image-generation": {
57
- mode: "your-own",
58
- provider: "gemini",
59
- model: "gemini-3.1-flash-image-preview",
60
- },
61
- "web-search": { mode: "your-own", provider: "inference-provider-native" },
62
- },
63
- }),
64
- }));
65
-
66
- const mockProvider = {
67
- name: "test",
68
- async sendMessage() {
69
- return {
70
- content: [
71
- {
72
- type: "text",
73
- text: '{"tasks":[{"id":"t1","role":"coder","objective":"Do it","dependencies":[]}]}',
74
- },
75
- ],
76
- model: "test",
77
- usage: { inputTokens: 10, outputTokens: 10 },
78
- stopReason: "end_turn",
79
- };
80
- },
81
- };
82
- mock.module("../security/secure-keys.js", () => ({
83
- getSecureKeyAsync: async () => "test-api-key",
84
- getProviderKeyAsync: async () => "test-api-key",
85
- }));
86
-
87
- mock.module("../providers/registry.js", () => ({
88
- getProvider: () => mockProvider,
89
- getFailoverProvider: () => mockProvider,
90
- }));
91
-
92
- mock.module("@anthropic-ai/claude-agent-sdk", () => ({
93
- query: () => ({
94
- async *[Symbol.asyncIterator]() {
95
- // If a gate is open, wait on it — this holds the swarm active
96
- if (gate) await gate.promise;
97
- yield {
98
- type: "result" as const,
99
- session_id: "test-session",
100
- subtype: "success" as const,
101
- result:
102
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
103
- };
104
- },
105
- }),
106
- }));
107
-
108
- import {
109
- _resetSwarmActive,
110
- swarmDelegateTool,
111
- } from "../tools/swarm/delegate.js";
112
- import type { ToolContext } from "../tools/types.js";
113
-
114
- function makeContext(overrides?: Partial<ToolContext>): ToolContext {
115
- return {
116
- conversationId: "test-conv",
117
- workingDir: "/tmp/test",
118
- trustClass: "guardian",
119
- onOutput: () => {},
120
- ...overrides,
121
- } as ToolContext;
122
- }
123
-
124
- describe("swarm recursion guard (concurrent)", () => {
125
- beforeEach(() => {
126
- _resetSwarmActive();
127
- closeGate();
128
- });
129
-
130
- test("rejects second invocation on same session while first is still active", async () => {
131
- openGate();
132
-
133
- // Start first swarm — it will pause at the SDK mock
134
- const first = swarmDelegateTool.execute(
135
- { objective: "First task" },
136
- makeContext({ conversationId: "session-A" }),
137
- );
138
-
139
- // Yield to let first reach the SDK gate (executeSwarm path)
140
- await new Promise((r) => setTimeout(r, 50));
141
-
142
- // Second invocation on the same session should be rejected
143
- const second = await swarmDelegateTool.execute(
144
- { objective: "Second task" },
145
- makeContext({ conversationId: "session-A" }),
146
- );
147
- expect(second.isError).toBe(true);
148
- expect(second.content).toContain("already executing");
149
-
150
- // Release the gate so first completes
151
- closeGate();
152
- const firstResult = await first;
153
- expect(firstResult.isError).toBeFalsy();
154
- });
155
-
156
- test("allows concurrent swarms on different sessions", async () => {
157
- openGate();
158
-
159
- // Start first swarm on session A
160
- const first = swarmDelegateTool.execute(
161
- { objective: "Conversation A task" },
162
- makeContext({ conversationId: "session-A" }),
163
- );
164
-
165
- // Yield to let first reach the gate
166
- await new Promise((r) => setTimeout(r, 50));
167
-
168
- // Release the gate before starting session B so both can complete
169
- closeGate();
170
-
171
- // Second swarm on a different session should succeed
172
- const second = await swarmDelegateTool.execute(
173
- { objective: "Conversation B task" },
174
- makeContext({ conversationId: "session-B" }),
175
- );
176
- expect(second.isError).toBeFalsy();
177
-
178
- const firstResult = await first;
179
- expect(firstResult.isError).toBeFalsy();
180
- });
181
-
182
- test("guard is released after first swarm completes", async () => {
183
- // Run and complete first swarm (no gate)
184
- const first = await swarmDelegateTool.execute(
185
- { objective: "First task" },
186
- makeContext({ conversationId: "session-A" }),
187
- );
188
- expect(first.isError).toBeFalsy();
189
-
190
- // Same session should now be allowed again
191
- const second = await swarmDelegateTool.execute(
192
- { objective: "Second task" },
193
- makeContext({ conversationId: "session-A" }),
194
- );
195
- expect(second.isError).toBeFalsy();
196
- });
197
- });