@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,463 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { resolveSwarmLimits } from "../swarm/limits.js";
4
- import type { OrchestratorEvent } from "../swarm/orchestrator.js";
5
- import { executeSwarm } from "../swarm/orchestrator.js";
6
- import type { SwarmPlan } from "../swarm/types.js";
7
- import type { SwarmWorkerBackend } from "../swarm/worker-backend.js";
8
-
9
- const DEFAULT_LIMITS = resolveSwarmLimits({
10
- maxWorkers: 3,
11
- maxTasks: 8,
12
- maxRetriesPerTask: 1,
13
- workerTimeoutSec: 900,
14
- });
15
-
16
- function makeBackend(
17
- overrides?: Partial<SwarmWorkerBackend>,
18
- ): SwarmWorkerBackend {
19
- return {
20
- name: "test-backend",
21
- isAvailable: () => true,
22
- runTask: async () => ({
23
- success: true,
24
- output:
25
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
26
- durationMs: 50,
27
- }),
28
- ...overrides,
29
- };
30
- }
31
-
32
- function makePlan(overrides?: Partial<SwarmPlan>): SwarmPlan {
33
- return {
34
- objective: "Test objective",
35
- tasks: [
36
- { id: "task-1", role: "coder", objective: "Do task 1", dependencies: [] },
37
- ],
38
- ...overrides,
39
- };
40
- }
41
-
42
- describe("executeSwarm", () => {
43
- test("executes a single-task plan successfully", async () => {
44
- const summary = await executeSwarm({
45
- plan: makePlan(),
46
- limits: DEFAULT_LIMITS,
47
- backend: makeBackend(),
48
- workingDir: "/tmp",
49
- });
50
-
51
- expect(summary.stats.totalTasks).toBe(1);
52
- expect(summary.stats.completed).toBe(1);
53
- expect(summary.stats.failed).toBe(0);
54
- expect(summary.stats.blocked).toBe(0);
55
- expect(summary.results).toHaveLength(1);
56
- expect(summary.results[0].status).toBe("completed");
57
- expect(summary.finalAnswer).toContain("task-1");
58
- });
59
-
60
- test("executes parallel independent tasks", async () => {
61
- const plan = makePlan({
62
- tasks: [
63
- { id: "a", role: "coder", objective: "A", dependencies: [] },
64
- { id: "b", role: "coder", objective: "B", dependencies: [] },
65
- ],
66
- });
67
-
68
- const summary = await executeSwarm({
69
- plan,
70
- limits: DEFAULT_LIMITS,
71
- backend: makeBackend(),
72
- workingDir: "/tmp",
73
- });
74
-
75
- expect(summary.stats.completed).toBe(2);
76
- expect(summary.stats.failed).toBe(0);
77
- });
78
-
79
- test("executes sequential tasks in dependency order", async () => {
80
- const executionOrder: string[] = [];
81
- const plan = makePlan({
82
- tasks: [
83
- {
84
- id: "research",
85
- role: "researcher",
86
- objective: "Research",
87
- dependencies: [],
88
- },
89
- {
90
- id: "code",
91
- role: "coder",
92
- objective: "Code",
93
- dependencies: ["research"],
94
- },
95
- {
96
- id: "review",
97
- role: "reviewer",
98
- objective: "Review",
99
- dependencies: ["code"],
100
- },
101
- ],
102
- });
103
-
104
- const backend = makeBackend({
105
- runTask: async (input) => {
106
- // Extract task id from prompt
107
- const match = input.prompt.match(/researcher|coder|reviewer/);
108
- if (match) executionOrder.push(match[0]);
109
- return {
110
- success: true,
111
- output:
112
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
113
- durationMs: 10,
114
- };
115
- },
116
- });
117
-
118
- const summary = await executeSwarm({
119
- plan,
120
- limits: DEFAULT_LIMITS,
121
- backend,
122
- workingDir: "/tmp",
123
- });
124
-
125
- expect(summary.stats.completed).toBe(3);
126
- expect(executionOrder).toEqual(["researcher", "coder", "reviewer"]);
127
- });
128
-
129
- test("blocks dependents when a task fails", async () => {
130
- const plan = makePlan({
131
- tasks: [
132
- { id: "a", role: "coder", objective: "A", dependencies: [] },
133
- { id: "b", role: "coder", objective: "B", dependencies: ["a"] },
134
- { id: "c", role: "coder", objective: "C", dependencies: ["b"] },
135
- ],
136
- });
137
-
138
- const backend = makeBackend({
139
- runTask: async () => ({
140
- success: false,
141
- output: "Failed",
142
- failureReason: "timeout",
143
- durationMs: 10,
144
- }),
145
- });
146
-
147
- const summary = await executeSwarm({
148
- plan,
149
- limits: resolveSwarmLimits({ ...DEFAULT_LIMITS, maxRetriesPerTask: 0 }),
150
- backend,
151
- workingDir: "/tmp",
152
- });
153
-
154
- expect(summary.stats.failed).toBe(1);
155
- expect(summary.stats.blocked).toBe(2);
156
- expect(summary.results).toHaveLength(1);
157
- expect(summary.results[0].taskId).toBe("a");
158
- });
159
-
160
- test("retries failed tasks up to maxRetriesPerTask", async () => {
161
- let callCount = 0;
162
- const plan = makePlan();
163
-
164
- const backend = makeBackend({
165
- runTask: async () => {
166
- callCount++;
167
- if (callCount <= 1) {
168
- return {
169
- success: false,
170
- output: "fail",
171
- failureReason: "timeout" as const,
172
- durationMs: 10,
173
- };
174
- }
175
- return {
176
- success: true,
177
- output:
178
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
179
- durationMs: 10,
180
- };
181
- },
182
- });
183
-
184
- const summary = await executeSwarm({
185
- plan,
186
- limits: resolveSwarmLimits({ ...DEFAULT_LIMITS, maxRetriesPerTask: 2 }),
187
- backend,
188
- workingDir: "/tmp",
189
- });
190
-
191
- expect(summary.stats.completed).toBe(1);
192
- expect(summary.results[0].retryCount).toBe(1);
193
- });
194
-
195
- test("emits orchestrator events", async () => {
196
- const events: OrchestratorEvent[] = [];
197
- const plan = makePlan();
198
-
199
- await executeSwarm({
200
- plan,
201
- limits: DEFAULT_LIMITS,
202
- backend: makeBackend(),
203
- workingDir: "/tmp",
204
- onStatus: (event) => events.push(event),
205
- });
206
-
207
- const kinds = events.map((e) => e.kind);
208
- expect(kinds).toContain("plan_created");
209
- expect(kinds).toContain("task_started");
210
- expect(kinds).toContain("task_completed");
211
- expect(kinds).toContain("synthesis_started");
212
- expect(kinds).toContain("done");
213
- });
214
-
215
- test("uses fallback markdown when no synthesis provider given", async () => {
216
- const summary = await executeSwarm({
217
- plan: makePlan(),
218
- limits: DEFAULT_LIMITS,
219
- backend: makeBackend(),
220
- workingDir: "/tmp",
221
- });
222
-
223
- expect(summary.finalAnswer).toContain("Swarm Results");
224
- expect(summary.finalAnswer).toContain("task-1");
225
- });
226
-
227
- test("reports totalDurationMs", async () => {
228
- const summary = await executeSwarm({
229
- plan: makePlan(),
230
- limits: DEFAULT_LIMITS,
231
- backend: makeBackend(),
232
- workingDir: "/tmp",
233
- });
234
-
235
- expect(summary.stats.totalDurationMs).toBeGreaterThanOrEqual(0);
236
- });
237
-
238
- test("uses markdown fallback when aborted without synthesis provider", async () => {
239
- const controller = new AbortController();
240
- let taskRun = false;
241
- const backend = makeBackend({
242
- runTask: async () => {
243
- taskRun = true;
244
- // Abort after the first task completes
245
- controller.abort();
246
- return {
247
- success: true,
248
- output:
249
- '```json\n{"summary":"Partial work done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
250
- durationMs: 10,
251
- };
252
- },
253
- });
254
-
255
- const plan = makePlan({
256
- tasks: [
257
- { id: "a", role: "coder", objective: "A", dependencies: [] },
258
- { id: "b", role: "coder", objective: "B", dependencies: ["a"] },
259
- ],
260
- });
261
-
262
- const summary = await executeSwarm({
263
- plan,
264
- limits: DEFAULT_LIMITS,
265
- backend,
266
- workingDir: "/tmp",
267
- signal: controller.signal,
268
- });
269
-
270
- expect(taskRun).toBe(true);
271
- // Should use markdown fallback with partial results, not a fixed cancellation string
272
- expect(summary.finalAnswer).toContain("Swarm Results");
273
- expect(summary.finalAnswer).toContain("a");
274
- expect(summary.finalAnswer).not.toContain("cancelled");
275
- });
276
-
277
- test("handles diamond dependency pattern", async () => {
278
- const plan = makePlan({
279
- tasks: [
280
- { id: "a", role: "coder", objective: "A", dependencies: [] },
281
- { id: "b", role: "coder", objective: "B", dependencies: ["a"] },
282
- { id: "c", role: "coder", objective: "C", dependencies: ["a"] },
283
- { id: "d", role: "coder", objective: "D", dependencies: ["b", "c"] },
284
- ],
285
- });
286
-
287
- const summary = await executeSwarm({
288
- plan,
289
- limits: DEFAULT_LIMITS,
290
- backend: makeBackend(),
291
- workingDir: "/tmp",
292
- });
293
-
294
- expect(summary.stats.completed).toBe(4);
295
- expect(summary.stats.blocked).toBe(0);
296
- });
297
-
298
- test("schedules dependents eagerly without waiting for batch peers", async () => {
299
- // A (slow) and B (fast) are independent. C depends only on B.
300
- // With eager scheduling, C should start while A is still running.
301
- const timeline: { taskId: string; event: "start" | "end" }[] = [];
302
-
303
- const plan = makePlan({
304
- tasks: [
305
- { id: "A", role: "coder", objective: "slow", dependencies: [] },
306
- { id: "B", role: "coder", objective: "fast", dependencies: [] },
307
- { id: "C", role: "coder", objective: "dep-on-B", dependencies: ["B"] },
308
- ],
309
- });
310
-
311
- const backend = makeBackend({
312
- runTask: async (input) => {
313
- const id = input.prompt.includes("slow")
314
- ? "A"
315
- : input.prompt.includes("fast")
316
- ? "B"
317
- : "C";
318
- timeline.push({ taskId: id, event: "start" });
319
- // A is deliberately slower than B so C should start before A finishes
320
- if (id === "A") await new Promise((r) => setTimeout(r, 80));
321
- else await new Promise((r) => setTimeout(r, 5));
322
- timeline.push({ taskId: id, event: "end" });
323
- return {
324
- success: true,
325
- output:
326
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
327
- durationMs: 10,
328
- };
329
- },
330
- });
331
-
332
- const summary = await executeSwarm({
333
- plan,
334
- limits: resolveSwarmLimits({ ...DEFAULT_LIMITS, maxWorkers: 3 }),
335
- backend,
336
- workingDir: "/tmp",
337
- });
338
-
339
- expect(summary.stats.completed).toBe(3);
340
-
341
- // C must start before A ends — proves eager scheduling
342
- const cStart = timeline.findIndex(
343
- (e) => e.taskId === "C" && e.event === "start",
344
- );
345
- const aEnd = timeline.findIndex(
346
- (e) => e.taskId === "A" && e.event === "end",
347
- );
348
- expect(cStart).toBeGreaterThan(-1);
349
- expect(aEnd).toBeGreaterThan(-1);
350
- expect(cStart).toBeLessThan(aEnd);
351
- });
352
-
353
- test("abort waits for in-flight workers before emitting done", async () => {
354
- const events: OrchestratorEvent[] = [];
355
- const controller = new AbortController();
356
-
357
- const plan = makePlan({
358
- tasks: [
359
- { id: "fast", role: "coder", objective: "fast", dependencies: [] },
360
- { id: "slow", role: "coder", objective: "slow", dependencies: [] },
361
- ],
362
- });
363
-
364
- const backend = makeBackend({
365
- runTask: async (input) => {
366
- if (input.prompt.includes("fast")) {
367
- // Fast task finishes quickly and triggers abort
368
- await new Promise((r) => setTimeout(r, 5));
369
- controller.abort();
370
- } else {
371
- // Slow task is still running when abort fires
372
- await new Promise((r) => setTimeout(r, 80));
373
- }
374
- return {
375
- success: true,
376
- output:
377
- '```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
378
- durationMs: 10,
379
- };
380
- },
381
- });
382
-
383
- const summary = await executeSwarm({
384
- plan,
385
- limits: resolveSwarmLimits({ ...DEFAULT_LIMITS, maxWorkers: 2 }),
386
- backend,
387
- workingDir: "/tmp",
388
- onStatus: (event) => events.push(event),
389
- signal: controller.signal,
390
- });
391
-
392
- const doneIdx = events.findIndex((e) => e.kind === "done");
393
- const lastCompletedIdx = events.reduce(
394
- (max, e, i) => (e.kind === "task_completed" ? i : max),
395
- -1,
396
- );
397
-
398
- // done must come after all task_completed events
399
- expect(doneIdx).toBeGreaterThan(-1);
400
- expect(lastCompletedIdx).toBeGreaterThan(-1);
401
- expect(doneIdx).toBeGreaterThan(lastCompletedIdx);
402
-
403
- // Both tasks should have completed (in-flight worker was waited on)
404
- expect(summary.stats.completed).toBe(2);
405
- });
406
-
407
- test("does not deadlock when onStatus callback throws on task_started", async () => {
408
- const plan = makePlan({
409
- tasks: [{ id: "a", role: "coder", objective: "A", dependencies: [] }],
410
- });
411
-
412
- // The onStatus callback throws on task_started, which happens inside
413
- // runTask before the worker runs. The .finally() guard on the
414
- // fire-and-forget promise should prevent a deadlock.
415
- const summary = await executeSwarm({
416
- plan,
417
- limits: DEFAULT_LIMITS,
418
- backend: makeBackend(),
419
- workingDir: "/tmp",
420
- onStatus: (event) => {
421
- if (event.kind === "task_started") throw new Error("boom");
422
- },
423
- });
424
-
425
- // The swarm should still complete (via the .finally guard) rather than hang
426
- expect(summary.stats.totalTasks).toBe(1);
427
- });
428
-
429
- test("does not double-decrement activeCount when onStatus throws on task_completed", async () => {
430
- // Regression: when onStatus threw inside processResult (after the task
431
- // finished), the old .catch() guard would decrement activeCount a second
432
- // time, driving it negative and causing early termination / incorrect stats.
433
- const plan = makePlan({
434
- tasks: [
435
- { id: "a", role: "coder", objective: "A", dependencies: [] },
436
- { id: "b", role: "coder", objective: "B", dependencies: [] },
437
- { id: "c", role: "coder", objective: "C", dependencies: ["a"] },
438
- ],
439
- });
440
-
441
- let throwCount = 0;
442
- const summary = await executeSwarm({
443
- plan,
444
- limits: resolveSwarmLimits({ ...DEFAULT_LIMITS, maxWorkers: 2 }),
445
- backend: makeBackend(),
446
- workingDir: "/tmp",
447
- onStatus: (event) => {
448
- // Throw only on the first task_completed to simulate the bug scenario
449
- if (event.kind === "task_completed" && throwCount === 0) {
450
- throwCount++;
451
- throw new Error("callback boom");
452
- }
453
- },
454
- });
455
-
456
- // Despite the throw, the remaining tasks should still run correctly.
457
- // With the old double-decrement bug, activeCount would go negative and
458
- // the orchestrator could terminate early or produce wrong stats.
459
- expect(summary.stats.totalTasks).toBe(3);
460
- // At least 2 tasks should complete (b always succeeds, plus either a or c)
461
- expect(summary.stats.completed).toBeGreaterThanOrEqual(2);
462
- });
463
- });