@vellumai/assistant 0.6.0 → 0.6.1

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 (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -0,0 +1,390 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ // Mock conversation-crud before importing tool executors that depend on it.
4
+ mock.module("../memory/conversation-crud.js", () => ({
5
+ getConversationType: () => "default",
6
+ setConversationOriginChannelIfUnset: () => {},
7
+ updateConversationContextWindow: () => {},
8
+ deleteMessageById: () => {},
9
+ updateConversationTitle: () => {},
10
+ updateConversationUsage: () => {},
11
+ addMessage: () => ({ id: "mock-msg-id" }),
12
+ getConversation: () => ({
13
+ id: "conv-1",
14
+ contextSummary: null,
15
+ contextCompactedMessageCount: 0,
16
+ totalInputTokens: 0,
17
+ totalOutputTokens: 0,
18
+ totalEstimatedCost: 0,
19
+ title: null,
20
+ }),
21
+ provenanceFromTrustContext: () => ({
22
+ source: "user",
23
+ trustContext: undefined,
24
+ }),
25
+ getConversationOriginInterface: () => null,
26
+ getConversationOriginChannel: () => null,
27
+ getMessages: () => null,
28
+ createConversation: () => ({ id: "mock-conv" }),
29
+ }));
30
+
31
+ import { isToolActiveForContext } from "../daemon/conversation-tool-setup.js";
32
+ import { getSubagentManager } from "../subagent/index.js";
33
+ import { SubagentManager } from "../subagent/manager.js";
34
+ import type { SubagentState } from "../subagent/types.js";
35
+ import {
36
+ executeSubagentNotifyParent,
37
+ notifyParentTool,
38
+ } from "../tools/subagent/notify-parent.js";
39
+
40
+ // ── Shared helpers ──────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Inject a fake subagent into the singleton manager so tool executors
44
+ * can find it. Uses the same private-internals trick as the other tests.
45
+ */
46
+ function injectSubagent(
47
+ manager: SubagentManager,
48
+ subagentId: string,
49
+ parentConversationId: string,
50
+ status: SubagentState["status"] = "running",
51
+ overrides: Partial<SubagentState> = {},
52
+ ): SubagentState {
53
+ const internals = manager as unknown as {
54
+ subagents: Map<
55
+ string,
56
+ {
57
+ conversation: unknown;
58
+ state: SubagentState;
59
+ parentSendToClient: () => void;
60
+ }
61
+ >;
62
+ parentToChildren: Map<string, Set<string>>;
63
+ };
64
+ const state: SubagentState = {
65
+ config: {
66
+ id: subagentId,
67
+ parentConversationId,
68
+ label: "Test",
69
+ objective: "test",
70
+ },
71
+ status,
72
+ conversationId: `conv-${subagentId}`,
73
+ createdAt: Date.now(),
74
+ usage: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
75
+ ...overrides,
76
+ };
77
+ const fakeConversation = {
78
+ abort: () => {},
79
+ dispose: () => {},
80
+ messages: [],
81
+ sendToClient: () => {},
82
+ usageStats: { inputTokens: 0, outputTokens: 0, estimatedCost: 0 },
83
+ enqueueMessage: () => ({ queued: false }),
84
+ persistUserMessage: () => "msg-1",
85
+ runAgentLoop: async () => {},
86
+ };
87
+ internals.subagents.set(subagentId, {
88
+ conversation: fakeConversation,
89
+ state,
90
+ parentSendToClient: () => {},
91
+ });
92
+ if (!internals.parentToChildren.has(parentConversationId)) {
93
+ internals.parentToChildren.set(parentConversationId, new Set());
94
+ }
95
+ internals.parentToChildren.get(parentConversationId)!.add(subagentId);
96
+ return state;
97
+ }
98
+
99
+ function makeContext(
100
+ conversationId: string,
101
+ extras: Record<string, unknown> = {},
102
+ ) {
103
+ return {
104
+ workingDir: "/tmp",
105
+ conversationId,
106
+ trustClass: "guardian" as const,
107
+ ...extras,
108
+ } as import("../tools/types.js").ToolContext;
109
+ }
110
+
111
+ // ── Tool definition ────────────────────────────────────────────────
112
+
113
+ describe("notify_parent tool definition", () => {
114
+ test("has correct core tool definition", () => {
115
+ const def = notifyParentTool.getDefinition();
116
+ const schema = def.input_schema as Record<string, unknown>;
117
+ expect(def.name).toBe("notify_parent");
118
+ expect(schema.required).toContain("message");
119
+ expect(
120
+ (schema.properties as Record<string, Record<string, unknown>>).urgency
121
+ .enum,
122
+ ).toEqual([
123
+ "info",
124
+ "important",
125
+ "blocked",
126
+ ]);
127
+ expect(notifyParentTool.category).toBe("orchestration");
128
+ });
129
+
130
+ test("is hidden from non-subagent context", () => {
131
+ const ctx = {
132
+ isSubagent: false,
133
+ preactivatedSkillIds: [],
134
+ skillProjectionState: new Map(),
135
+ skillProjectionCache: new Map(),
136
+ coreToolNames: new Set<string>(),
137
+ toolsDisabledDepth: 0,
138
+ } as unknown as import("../daemon/conversation-tool-setup.js").SkillProjectionContext;
139
+ expect(isToolActiveForContext("notify_parent", ctx)).toBe(false);
140
+ });
141
+
142
+ test("is hidden when isSubagent is undefined", () => {
143
+ const ctx = {
144
+ preactivatedSkillIds: [],
145
+ skillProjectionState: new Map(),
146
+ skillProjectionCache: new Map(),
147
+ coreToolNames: new Set<string>(),
148
+ toolsDisabledDepth: 0,
149
+ } as unknown as import("../daemon/conversation-tool-setup.js").SkillProjectionContext;
150
+ expect(isToolActiveForContext("notify_parent", ctx)).toBe(false);
151
+ });
152
+
153
+ test("is visible to subagent context", () => {
154
+ const ctx = {
155
+ isSubagent: true,
156
+ preactivatedSkillIds: [],
157
+ skillProjectionState: new Map(),
158
+ skillProjectionCache: new Map(),
159
+ coreToolNames: new Set<string>(),
160
+ toolsDisabledDepth: 0,
161
+ } as unknown as import("../daemon/conversation-tool-setup.js").SkillProjectionContext;
162
+ expect(isToolActiveForContext("notify_parent", ctx)).toBe(true);
163
+ });
164
+ });
165
+
166
+ // ── executeSubagentNotifyParent ────────────────────────────────────
167
+
168
+ describe("executeSubagentNotifyParent", () => {
169
+ test("rejects calls from non-subagent conversations", async () => {
170
+ const result = await executeSubagentNotifyParent(
171
+ { message: "Found something important" },
172
+ makeContext("not-a-subagent-conv"),
173
+ );
174
+ expect(result.isError).toBe(true);
175
+ expect(result.content).toContain("Could not notify parent");
176
+ expect(result.content).toContain("only available to subagents");
177
+ });
178
+
179
+ test("succeeds when called from a subagent conversation", async () => {
180
+ const manager = getSubagentManager();
181
+ const subagentId = "notify-sub-1";
182
+ const parentConversationId = "notify-parent-1";
183
+ injectSubagent(manager, subagentId, parentConversationId, "running");
184
+
185
+ // Wire up the onSubagentFinished callback.
186
+ let capturedMessage = "";
187
+ manager.onSubagentFinished = (
188
+ _parentId: string,
189
+ message: string,
190
+ ) => {
191
+ capturedMessage = message;
192
+ };
193
+
194
+ try {
195
+ const result = await executeSubagentNotifyParent(
196
+ { message: "Found key results", urgency: "important" },
197
+ makeContext(`conv-${subagentId}`),
198
+ );
199
+ expect(result.isError).toBe(false);
200
+ const parsed = JSON.parse(result.content);
201
+ expect(parsed.sent).toBe(true);
202
+ expect(parsed.urgency).toBe("important");
203
+ expect(capturedMessage).toContain("Found key results");
204
+ } finally {
205
+ manager.onSubagentFinished = undefined;
206
+ }
207
+ });
208
+
209
+ test("formats message with label and urgency", async () => {
210
+ const manager = getSubagentManager();
211
+ const subagentId = "notify-format-1";
212
+ const parentConversationId = "notify-format-parent";
213
+ injectSubagent(manager, subagentId, parentConversationId, "running", {
214
+ config: {
215
+ id: subagentId,
216
+ parentConversationId,
217
+ label: "Research Task",
218
+ objective: "research",
219
+ },
220
+ });
221
+
222
+ let capturedMessage = "";
223
+ manager.onSubagentFinished = (
224
+ _parentId: string,
225
+ message: string,
226
+ ) => {
227
+ capturedMessage = message;
228
+ };
229
+
230
+ try {
231
+ await executeSubagentNotifyParent(
232
+ { message: "Preliminary findings ready", urgency: "info" },
233
+ makeContext(`conv-${subagentId}`),
234
+ );
235
+ expect(capturedMessage).toBe(
236
+ '[Subagent "Research Task" — info] Preliminary findings ready',
237
+ );
238
+ } finally {
239
+ manager.onSubagentFinished = undefined;
240
+ }
241
+ });
242
+
243
+ test("returns error when message is empty", async () => {
244
+ const result = await executeSubagentNotifyParent(
245
+ { message: "" },
246
+ makeContext("some-conv"),
247
+ );
248
+ expect(result.isError).toBe(true);
249
+ expect(result.content).toContain('"message" is required');
250
+ });
251
+
252
+ test("returns error when message is missing", async () => {
253
+ const result = await executeSubagentNotifyParent(
254
+ {},
255
+ makeContext("some-conv"),
256
+ );
257
+ expect(result.isError).toBe(true);
258
+ expect(result.content).toContain('"message" is required');
259
+ });
260
+
261
+ test("defaults urgency to info when not provided", async () => {
262
+ const manager = getSubagentManager();
263
+ const subagentId = "notify-default-urg-1";
264
+ const parentConversationId = "notify-default-urg-parent";
265
+ injectSubagent(manager, subagentId, parentConversationId, "running");
266
+
267
+ manager.onSubagentFinished = () => {};
268
+
269
+ try {
270
+ const result = await executeSubagentNotifyParent(
271
+ { message: "Progress update" },
272
+ makeContext(`conv-${subagentId}`),
273
+ );
274
+ expect(result.isError).toBe(false);
275
+ const parsed = JSON.parse(result.content);
276
+ expect(parsed.urgency).toBe("info");
277
+ } finally {
278
+ manager.onSubagentFinished = undefined;
279
+ }
280
+ });
281
+
282
+ test("appends guidance hint for blocked urgency", async () => {
283
+ const manager = getSubagentManager();
284
+ const subagentId = "notify-blocked-1";
285
+ const parentConversationId = "notify-blocked-parent";
286
+ injectSubagent(manager, subagentId, parentConversationId, "running");
287
+
288
+ let capturedMessage = "";
289
+ manager.onSubagentFinished = (
290
+ _parentId: string,
291
+ message: string,
292
+ ) => {
293
+ capturedMessage = message;
294
+ };
295
+
296
+ try {
297
+ await executeSubagentNotifyParent(
298
+ { message: "Need API key to proceed", urgency: "blocked" },
299
+ makeContext(`conv-${subagentId}`),
300
+ );
301
+ expect(capturedMessage).toContain("Need API key to proceed");
302
+ expect(capturedMessage).toContain(
303
+ "Use subagent_message to send guidance to this subagent.",
304
+ );
305
+ } finally {
306
+ manager.onSubagentFinished = undefined;
307
+ }
308
+ });
309
+ });
310
+
311
+ // ── Manager-level tests ────────────────────────────────────────────
312
+
313
+ describe("SubagentManager.notifyParent", () => {
314
+ test("returns false for terminal subagents", () => {
315
+ const manager = getSubagentManager();
316
+
317
+ for (const terminalStatus of [
318
+ "completed",
319
+ "failed",
320
+ "aborted",
321
+ ] as const) {
322
+ const subagentId = `notify-terminal-${terminalStatus}`;
323
+ const parentConversationId = `notify-terminal-parent-${terminalStatus}`;
324
+ injectSubagent(
325
+ manager,
326
+ subagentId,
327
+ parentConversationId,
328
+ terminalStatus,
329
+ );
330
+
331
+ manager.onSubagentFinished = () => {};
332
+
333
+ try {
334
+ const result = manager.notifyParent(
335
+ `conv-${subagentId}`,
336
+ "Should not arrive",
337
+ "info",
338
+ );
339
+ expect(result).toBe(false);
340
+ } finally {
341
+ manager.onSubagentFinished = undefined;
342
+ }
343
+ }
344
+ });
345
+
346
+ test("returns false when onSubagentFinished is not wired", () => {
347
+ const manager = getSubagentManager();
348
+ const subagentId = "notify-no-callback-1";
349
+ const parentConversationId = "notify-no-callback-parent";
350
+ injectSubagent(manager, subagentId, parentConversationId, "running");
351
+
352
+ manager.onSubagentFinished = undefined;
353
+
354
+ const result = manager.notifyParent(
355
+ `conv-${subagentId}`,
356
+ "Test message",
357
+ "info",
358
+ );
359
+ expect(result).toBe(false);
360
+ });
361
+ });
362
+
363
+ describe("SubagentManager.getParentInfo", () => {
364
+ test("returns undefined for unknown conversationIds", () => {
365
+ const manager = getSubagentManager();
366
+ const result = manager.getParentInfo("nonexistent-conversation-id");
367
+ expect(result).toBeUndefined();
368
+ });
369
+
370
+ test("returns parent info for known subagent conversationId", () => {
371
+ const manager = getSubagentManager();
372
+ const subagentId = "parent-info-sub-1";
373
+ const parentConversationId = "parent-info-parent-1";
374
+ injectSubagent(manager, subagentId, parentConversationId, "running", {
375
+ config: {
376
+ id: subagentId,
377
+ parentConversationId,
378
+ label: "Info Lookup",
379
+ objective: "look things up",
380
+ },
381
+ });
382
+
383
+ const info = manager.getParentInfo(`conv-${subagentId}`);
384
+ expect(info).toBeDefined();
385
+ expect(info!.parentConversationId).toBe(parentConversationId);
386
+ expect(info!.subagentId).toBe(subagentId);
387
+ expect(info!.label).toBe("Info Lookup");
388
+ expect(typeof info!.parentSendToClient).toBe("function");
389
+ });
390
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ mergeSkillIds,
5
+ SUBAGENT_ROLE_REGISTRY,
6
+ type SubagentRole,
7
+ } from "../subagent/index.js";
8
+
9
+ /** All roles defined in the SubagentRole union. */
10
+ const ALL_ROLES: SubagentRole[] = ["general", "researcher", "coder", "planner"];
11
+
12
+ describe("SUBAGENT_ROLE_REGISTRY", () => {
13
+ test("covers all values in the SubagentRole union", () => {
14
+ const registryKeys = Object.keys(SUBAGENT_ROLE_REGISTRY);
15
+ expect(registryKeys.sort()).toEqual([...ALL_ROLES].sort());
16
+ expect(registryKeys).toHaveLength(ALL_ROLES.length);
17
+ });
18
+
19
+ test("every role has a non-empty systemPromptPreamble", () => {
20
+ for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
21
+ expect(config.systemPromptPreamble.length).toBeGreaterThan(0);
22
+ }
23
+ });
24
+
25
+ test("general has allowedTools: undefined", () => {
26
+ expect(SUBAGENT_ROLE_REGISTRY.general.allowedTools).toBeUndefined();
27
+ });
28
+
29
+ test("all non-general roles have allowedTools as a non-empty array", () => {
30
+ for (const role of ALL_ROLES) {
31
+ if (role === "general") continue;
32
+ const config = SUBAGENT_ROLE_REGISTRY[role];
33
+ expect(Array.isArray(config.allowedTools)).toBe(true);
34
+ expect(config.allowedTools!.length).toBeGreaterThan(0);
35
+ }
36
+ });
37
+
38
+ test('every role with allowedTools includes "notify_parent"', () => {
39
+ for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
40
+ if (config.allowedTools !== undefined) {
41
+ expect(config.allowedTools).toContain("notify_parent");
42
+ }
43
+ }
44
+ });
45
+
46
+ test('no role includes "skill_execute" (replaced by core tools)', () => {
47
+ for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
48
+ if (config.allowedTools !== undefined) {
49
+ expect(config.allowedTools).not.toContain("skill_execute");
50
+ }
51
+ }
52
+ });
53
+
54
+ test('researcher includes "recall" for memory access', () => {
55
+ const tools = SUBAGENT_ROLE_REGISTRY.researcher.allowedTools!;
56
+ expect(tools).toContain("recall");
57
+ });
58
+
59
+ test('coder includes "recall" for memory access', () => {
60
+ expect(SUBAGENT_ROLE_REGISTRY.coder.allowedTools!).toContain("recall");
61
+ });
62
+
63
+ test('planner includes "recall" for memory access', () => {
64
+ expect(SUBAGENT_ROLE_REGISTRY.planner.allowedTools!).toContain("recall");
65
+ });
66
+
67
+ test("no role references the old memory_recall tool name", () => {
68
+ for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
69
+ if (config.allowedTools !== undefined) {
70
+ expect(config.allowedTools).not.toContain("memory_recall");
71
+ }
72
+ }
73
+ });
74
+
75
+ test("every role has empty skillIds (no skill preactivation)", () => {
76
+ for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
77
+ expect(config.skillIds).toEqual([]);
78
+ }
79
+ });
80
+
81
+ test('researcher and planner include "file_list"', () => {
82
+ expect(SUBAGENT_ROLE_REGISTRY.researcher.allowedTools).toContain(
83
+ "file_list",
84
+ );
85
+ expect(SUBAGENT_ROLE_REGISTRY.planner.allowedTools).toContain("file_list");
86
+ });
87
+ });
88
+
89
+ describe("mergeSkillIds", () => {
90
+ test("removes duplicates between role and config skill IDs", () => {
91
+ expect(mergeSkillIds(["a", "b"], ["b", "c"])).toEqual(["a", "b", "c"]);
92
+ });
93
+
94
+ test("returns only role skills when config is undefined", () => {
95
+ expect(mergeSkillIds(["subagent"], undefined)).toEqual(["subagent"]);
96
+ });
97
+
98
+ test("includes caller-provided extras alongside role skills", () => {
99
+ expect(mergeSkillIds(["subagent"], ["custom-skill"])).toEqual([
100
+ "subagent",
101
+ "custom-skill",
102
+ ]);
103
+ });
104
+
105
+ test("returns empty array when both inputs are empty", () => {
106
+ expect(mergeSkillIds([], undefined)).toEqual([]);
107
+ });
108
+ });
@@ -0,0 +1,71 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ isToolActiveForContext,
5
+ type SkillProjectionContext,
6
+ SUBAGENT_ONLY_TOOL_NAMES,
7
+ } from "../daemon/conversation-tool-setup.js";
8
+
9
+ const TEST_TOOL_NAME = "__test_subagent_only_tool__";
10
+
11
+ describe("subagent-only tool filtering", () => {
12
+ beforeEach(() => {
13
+ SUBAGENT_ONLY_TOOL_NAMES.add(TEST_TOOL_NAME);
14
+ });
15
+
16
+ afterEach(() => {
17
+ SUBAGENT_ONLY_TOOL_NAMES.delete(TEST_TOOL_NAME);
18
+ });
19
+
20
+ test("hides subagent-only tools from main conversations (isSubagent=false)", () => {
21
+ const ctx: SkillProjectionContext = {
22
+ skillProjectionState: new Map(),
23
+ skillProjectionCache: {},
24
+ coreToolNames: new Set(),
25
+ toolsDisabledDepth: 0,
26
+ hasNoClient: false,
27
+ isSubagent: false,
28
+ };
29
+
30
+ expect(isToolActiveForContext(TEST_TOOL_NAME, ctx)).toBe(false);
31
+ });
32
+
33
+ test("hides subagent-only tools when isSubagent is undefined", () => {
34
+ const ctx: SkillProjectionContext = {
35
+ skillProjectionState: new Map(),
36
+ skillProjectionCache: {},
37
+ coreToolNames: new Set(),
38
+ toolsDisabledDepth: 0,
39
+ hasNoClient: false,
40
+ };
41
+
42
+ expect(isToolActiveForContext(TEST_TOOL_NAME, ctx)).toBe(false);
43
+ });
44
+
45
+ test("shows subagent-only tools to subagent conversations (isSubagent=true)", () => {
46
+ const ctx: SkillProjectionContext = {
47
+ skillProjectionState: new Map(),
48
+ skillProjectionCache: {},
49
+ coreToolNames: new Set(),
50
+ toolsDisabledDepth: 0,
51
+ hasNoClient: true,
52
+ isSubagent: true,
53
+ };
54
+
55
+ expect(isToolActiveForContext(TEST_TOOL_NAME, ctx)).toBe(true);
56
+ });
57
+
58
+ test("does not affect regular tools when isSubagent is false", () => {
59
+ const ctx: SkillProjectionContext = {
60
+ skillProjectionState: new Map(),
61
+ skillProjectionCache: {},
62
+ coreToolNames: new Set(),
63
+ toolsDisabledDepth: 0,
64
+ hasNoClient: false,
65
+ isSubagent: false,
66
+ };
67
+
68
+ // A regular tool not in SUBAGENT_ONLY_TOOL_NAMES should still be active
69
+ expect(isToolActiveForContext("bash", ctx)).toBe(true);
70
+ });
71
+ });