@vellumai/assistant 0.10.3-staging.2 → 0.10.4-staging.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 (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -1,137 +0,0 @@
1
- /**
2
- * End-to-end integration test: drives the REAL agent loop with all first-party
3
- * defaults registered (so the advisor plugin's hooks + tool are live), and lets
4
- * the REAL consult run. Only the provider boundary is stubbed:
5
- * - the executor's provider (a scripted mock provider), and
6
- * - `getConfiguredProvider`, so the advisor sub-call resolves to a second
7
- * scripted mock provider instead of a configured one.
8
- *
9
- * The advisor's inference genuinely runs through `consult` → `sendMessage`
10
- * against the injected advisor provider, exercising the full hook-capture →
11
- * tool.execute → consult → routed-inference path through the loop.
12
- */
13
- import { beforeEach, describe, expect, mock, test } from "bun:test";
14
-
15
- import {
16
- createMockProvider,
17
- textResponse,
18
- } from "../../../../__tests__/helpers/mock-provider.js";
19
- import { AgentLoop } from "../../../../agent/loop.js";
20
- import type {
21
- ContentBlock,
22
- Message,
23
- ProviderResponse,
24
- } from "../../../../providers/types.js";
25
-
26
- const ADVICE = "ADVICE: use a channel-based worker pool with graceful drain.";
27
- let advisorProvider: ReturnType<typeof createMockProvider> | null = null;
28
-
29
- const realProviderSendMessage =
30
- await import("../../../../providers/provider-send-message.js");
31
- mock.module("../../../../providers/provider-send-message.js", () => ({
32
- ...realProviderSendMessage,
33
- getConfiguredProvider: async () => advisorProvider?.provider ?? null,
34
- }));
35
-
36
- const { resetPluginRegistryAndRegisterDefaults } =
37
- await import("../../index.js");
38
- const advisorTool = (await import("../tools/advisor.js")).default;
39
-
40
- const userTurn: Message = {
41
- role: "user",
42
- content: [{ type: "text", text: "build a worker pool" }],
43
- };
44
-
45
- function textOf(content: ReadonlyArray<ContentBlock>): string {
46
- return content.map((b) => (b.type === "text" ? b.text : "")).join("");
47
- }
48
-
49
- describe("advisor — agent-loop integration", () => {
50
- beforeEach(() => {
51
- resetPluginRegistryAndRegisterDefaults();
52
- advisorProvider = createMockProvider([textResponse(ADVICE)]);
53
- });
54
-
55
- test("model calls advisor → hooks capture → consult routes through inference → advice returns", async () => {
56
- const consultThenText: ProviderResponse = {
57
- content: [
58
- { type: "text", text: "Let me consult the advisor." },
59
- { type: "tool_use", id: "call-1", name: "advisor", input: {} },
60
- ],
61
- model: "mock-model",
62
- usage: { inputTokens: 10, outputTokens: 5 },
63
- stopReason: "tool_use",
64
- };
65
- const { provider } = createMockProvider([
66
- consultThenText,
67
- textResponse("Done — implemented the worker pool."),
68
- ]);
69
-
70
- const conversationId = "advisor-itest";
71
- const loop = new AgentLoop({
72
- provider,
73
- systemPrompt: "You are a coding agent.",
74
- conversationId,
75
- tools: [
76
- {
77
- name: "advisor",
78
- description: "",
79
- input_schema: { type: "object", properties: {} },
80
- },
81
- ],
82
- toolExecutor: async (name, input) =>
83
- name === "advisor"
84
- ? advisorTool.execute!(input, { conversationId } as never)
85
- : { content: `unknown tool ${name}`, isError: true },
86
- });
87
-
88
- const { history } = await loop.run({
89
- requestId: "req-1",
90
- messages: [userTurn],
91
- onEvent: () => {},
92
- callSite: "mainAgent",
93
- trust: { sourceChannel: "vellum", trustClass: "unknown" },
94
- });
95
-
96
- // The advisor sub-call happened, routed through the dedicated advisor call site.
97
- expect(advisorProvider!.calls).toHaveLength(1);
98
- const sub = advisorProvider!.calls[0];
99
- expect(sub.options?.config?.callSite).toBe("advisor");
100
- // No `advisorProfile` configured in the default test config, so no override
101
- // is passed and the `advisor` call site resolves its own default profile.
102
- expect(sub.options?.config?.overrideProfile).toBeUndefined();
103
- expect(sub.options?.config?.tool_choice).toEqual({ type: "none" });
104
- // No advisor-specific output cap — the resolver applies the profile budget.
105
- expect(sub.options?.config?.max_tokens).toBeUndefined();
106
-
107
- // The advisor saw the captured transcript (task present; pending tool_use stripped).
108
- expect(textOf(sub.messages[0].content)).toContain("build a worker pool");
109
- expect(textOf(sub.messages[sub.messages.length - 1].content)).toContain(
110
- "focused strategic guidance",
111
- );
112
- // It also saw the model's CURRENT turn — the text it wrote right before the
113
- // `advisor` tool_use — which `post-model-call` lifts out of `ctx.content`.
114
- const transcript = sub.messages.map((m) => textOf(m.content)).join("\n");
115
- expect(transcript).toContain("Let me consult the advisor.");
116
-
117
- // The advisor saw the executor's system prompt (via pre-model-call).
118
- expect(sub.options?.systemPrompt).toContain("senior advisor");
119
- expect(sub.options?.systemPrompt).toContain("You are a coding agent.");
120
-
121
- // The advice flowed back into the executor's history as the tool result.
122
- const toolResult = history
123
- .flatMap((m) => m.content)
124
- .find((b) => b.type === "tool_result");
125
- expect((toolResult as { content: string }).content).toContain(
126
- "channel-based worker pool",
127
- );
128
-
129
- // The loop completed with the final answer.
130
- const lastAssistant = [...history]
131
- .reverse()
132
- .find((m) => m.role === "assistant")!;
133
- expect(textOf(lastAssistant.content)).toContain(
134
- "implemented the worker pool",
135
- );
136
- });
137
- });
@@ -1,314 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- import type { Message } from "../../../../providers/types.js";
4
-
5
- // Stub the provider resolution; spread the real module so `extractAllText` /
6
- // `userMessage` (which the consult also uses) keep working.
7
- let sendMessageArgs: Record<string, unknown> | null = null;
8
- let responseText = "Use a channel-based worker pool; drain on shutdown.";
9
- let sendMessageError: Error | null = null;
10
- let providerResolves = true;
11
- let providerSupportsWeb = false;
12
- let streamDeltas: string[] = [];
13
- let streamEvents: Array<Record<string, unknown>> = [];
14
-
15
- const fakeProvider = {
16
- name: "mock-advisor-provider",
17
- get supportsNativeWebSearch() {
18
- return providerSupportsWeb;
19
- },
20
- async sendMessage(messages: unknown, options: unknown) {
21
- sendMessageArgs = { messages, options } as Record<string, unknown>;
22
- if (sendMessageError) throw sendMessageError;
23
- const onEvent = (
24
- options as { onEvent?: (e: Record<string, unknown>) => void }
25
- ).onEvent;
26
- if (onEvent) {
27
- // Activity (search/thinking) streams before the final advice text.
28
- for (const ev of streamEvents) onEvent(ev);
29
- for (const text of streamDeltas) onEvent({ type: "text_delta", text });
30
- }
31
- return {
32
- content: [{ type: "text", text: responseText }],
33
- model: "mock-model",
34
- usage: { inputTokens: 1, outputTokens: 1 },
35
- stopReason: "end_turn",
36
- };
37
- },
38
- };
39
-
40
- const realPsm = await import("../../../../providers/provider-send-message.js");
41
- mock.module("../../../../providers/provider-send-message.js", () => ({
42
- ...realPsm,
43
- getConfiguredProvider: async () => (providerResolves ? fakeProvider : null),
44
- }));
45
-
46
- // Keep the tool tests focused on the consult wiring: stub the context pack so
47
- // they don't reach into the registry / workspace / memory sources (those have
48
- // their own coverage). The consult itself never imports this module.
49
- mock.module("../context-pack.js", () => ({
50
- buildAdvisorContext: async () => null,
51
- deriveRecallQuery: () => null,
52
- }));
53
-
54
- const { consultAdvisor } = await import("../consult.js");
55
- const advisorTool = (await import("../tools/advisor.js")).default;
56
- const { recordSystemPrompt, recordMessages, resetAdvisorStateForTests } =
57
- await import("../advisor-state-store.js");
58
-
59
- const userMsg = (t: string): Message => ({
60
- role: "user",
61
- content: [{ type: "text", text: t }],
62
- });
63
-
64
- function optionConfig(): Record<string, unknown> {
65
- const options = sendMessageArgs?.options as Record<string, unknown>;
66
- return options.config as Record<string, unknown>;
67
- }
68
-
69
- beforeEach(() => {
70
- sendMessageArgs = null;
71
- responseText = "Use a channel-based worker pool; drain on shutdown.";
72
- sendMessageError = null;
73
- providerResolves = true;
74
- providerSupportsWeb = false;
75
- streamDeltas = [];
76
- streamEvents = [];
77
- resetAdvisorStateForTests();
78
- });
79
-
80
- describe("consultAdvisor", () => {
81
- test("routes through the advisor call site, tools off, returns advice", async () => {
82
- const messages: Message[] = [
83
- userMsg("build a worker pool"),
84
- {
85
- role: "assistant",
86
- content: [
87
- { type: "thinking", thinking: "secret", signature: "s" },
88
- { type: "text", text: "let me consult the advisor" },
89
- { type: "tool_use", id: "t1", name: "advisor", input: {} },
90
- ],
91
- },
92
- ];
93
-
94
- const advice = await consultAdvisor({
95
- systemPrompt: "You are a coding agent.",
96
- messages,
97
- });
98
-
99
- expect(advice).toBe(responseText);
100
-
101
- const config = optionConfig();
102
- expect(config.callSite).toBe("advisor");
103
- // No `advisorProfile` is configured in the default test config, so the
104
- // consult passes no override and the `advisor` call site resolves to its
105
- // default profile (`quality-optimized`).
106
- expect(config.overrideProfile).toBeUndefined();
107
- expect(config.tool_choice).toEqual({ type: "none" });
108
- // No advisor-specific output cap — the resolver applies the profile budget.
109
- expect(config.max_tokens).toBeUndefined();
110
-
111
- const sent = sendMessageArgs?.messages as Message[];
112
- expect(sent[0]).toEqual(userMsg("build a worker pool"));
113
- expect(sent[1]).toEqual({
114
- role: "assistant",
115
- content: [{ type: "text", text: "let me consult the advisor" }],
116
- });
117
- const lastText = (sent[sent.length - 1].content[0] as { text: string })
118
- .text;
119
- expect(lastText).toContain("focused strategic guidance");
120
- // The request carries no word limit.
121
- expect(lastText).not.toContain("words");
122
-
123
- const options = sendMessageArgs?.options as { systemPrompt: string };
124
- expect(options.systemPrompt).toContain("senior advisor");
125
- expect(options.systemPrompt).toContain("You are a coding agent.");
126
- });
127
-
128
- test("stays tool-less when the provider has no native web search", async () => {
129
- providerSupportsWeb = false;
130
- await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
131
- const options = sendMessageArgs?.options as { tools?: unknown };
132
- expect(options.tools).toBeUndefined();
133
- expect(optionConfig().tool_choice).toEqual({ type: "none" });
134
- });
135
-
136
- test("enables native web search when the provider supports it", async () => {
137
- providerSupportsWeb = true;
138
- await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
139
-
140
- const options = sendMessageArgs?.options as {
141
- tools?: Array<{ name: string }>;
142
- };
143
- expect(options.tools?.map((t) => t.name)).toEqual(["web_search"]);
144
- // tool_choice must not be `none`, or the provider suppresses its server tool.
145
- expect(optionConfig().tool_choice).toEqual({ type: "auto" });
146
- });
147
-
148
- test("streams web-search activity to onText, not just the final advice", async () => {
149
- providerSupportsWeb = true;
150
- streamEvents = [
151
- { type: "server_tool_start", name: "web_search", toolUseId: "s1", input: {} },
152
- {
153
- type: "server_tool_complete",
154
- toolUseId: "s1",
155
- isError: false,
156
- resolvedInput: { query: "vellum streaming" },
157
- },
158
- ];
159
- streamDeltas = ["Here is ", "the advice."];
160
- const chunks: string[] = [];
161
-
162
- await consultAdvisor({
163
- systemPrompt: null,
164
- messages: [userMsg("hi")],
165
- onText: (c) => chunks.push(c),
166
- });
167
-
168
- const joined = chunks.join("");
169
- // The drawer isn't silent during the search prefix...
170
- expect(joined).toContain("Searching the web");
171
- expect(joined).toContain("Searched: vellum streaming");
172
- // ...and the advice text still streams.
173
- expect(joined).toContain("Here is the advice.");
174
- });
175
-
176
- test("surfaces a failure note (not 'Searched') when a web search errors", async () => {
177
- providerSupportsWeb = true;
178
- streamEvents = [
179
- { type: "server_tool_start", name: "web_search", toolUseId: "s1", input: {} },
180
- {
181
- type: "server_tool_complete",
182
- toolUseId: "s1",
183
- isError: true,
184
- errorCode: "query_too_long",
185
- resolvedInput: { query: "an overly long query" },
186
- },
187
- ];
188
- streamDeltas = ["Proceeding without search."];
189
- const chunks: string[] = [];
190
-
191
- await consultAdvisor({
192
- systemPrompt: null,
193
- messages: [userMsg("hi")],
194
- onText: (c) => chunks.push(c),
195
- });
196
-
197
- const joined = chunks.join("");
198
- expect(joined).toContain("Web search failed");
199
- expect(joined).not.toContain("🔎 Searched:");
200
- // The consult still continues and streams its guidance.
201
- expect(joined).toContain("Proceeding without search.");
202
- });
203
-
204
- test("streams the model's reasoning summary to onText", async () => {
205
- streamEvents = [{ type: "thinking_delta", thinking: "weighing tradeoffs" }];
206
- const chunks: string[] = [];
207
- await consultAdvisor({
208
- systemPrompt: null,
209
- messages: [userMsg("hi")],
210
- onText: (c) => chunks.push(c),
211
- });
212
- expect(chunks.join("")).toContain("weighing tradeoffs");
213
- });
214
-
215
- test("embeds the runtime context in the advisor system prompt", async () => {
216
- await consultAdvisor({
217
- systemPrompt: "You are a coding agent.",
218
- messages: [userMsg("hi")],
219
- runtimeContext: "## Available tools\n- bash — run commands",
220
- });
221
- const options = sendMessageArgs?.options as { systemPrompt: string };
222
- expect(options.systemPrompt).toContain("<agent_runtime_context>");
223
- expect(options.systemPrompt).toContain("- bash — run commands");
224
- });
225
-
226
- test("soft-fails when no provider is configured", async () => {
227
- providerResolves = false;
228
- const advice = await consultAdvisor({
229
- systemPrompt: null,
230
- messages: [userMsg("hi")],
231
- });
232
- expect(advice).toContain("no inference provider");
233
- });
234
-
235
- test("returns a notice when there is no usable transcript", async () => {
236
- const advice = await consultAdvisor({ systemPrompt: null, messages: [] });
237
- expect(advice).toContain("no conversation context");
238
- expect(sendMessageArgs).toBeNull();
239
- });
240
-
241
- test("falls back to a notice when the advisor returns blank text", async () => {
242
- responseText = " ";
243
- const advice = await consultAdvisor({
244
- systemPrompt: null,
245
- messages: [userMsg("hi")],
246
- });
247
- expect(advice).toContain("no guidance");
248
- });
249
-
250
- test("streams the model's text deltas to `onText` as it generates", async () => {
251
- streamDeltas = ["Use a ", "channel-based ", "worker pool."];
252
- const chunks: string[] = [];
253
-
254
- const advice = await consultAdvisor({
255
- systemPrompt: null,
256
- messages: [userMsg("hi")],
257
- onText: (c) => chunks.push(c),
258
- });
259
-
260
- // Each visible delta is forwarded live...
261
- expect(chunks).toEqual(["Use a ", "channel-based ", "worker pool."]);
262
- // ...and the complete guidance is still returned.
263
- expect(advice).toBe(responseText);
264
- });
265
-
266
- test("registers no `onEvent` sink when `onText` is absent", async () => {
267
- streamDeltas = ["x"];
268
- await consultAdvisor({ systemPrompt: null, messages: [userMsg("hi")] });
269
- const options = sendMessageArgs?.options as { onEvent?: unknown };
270
- expect(options.onEvent).toBeUndefined();
271
- });
272
- });
273
-
274
- describe("advisor tool.execute", () => {
275
- test("reads the captured transcript and returns guidance as a non-error result", async () => {
276
- recordSystemPrompt("c1", "You are a coding agent.");
277
- recordMessages("c1", [userMsg("build a worker pool")]);
278
-
279
- const result = await advisorTool.execute?.({}, {
280
- conversationId: "c1",
281
- } as never);
282
-
283
- expect(result?.isError).toBe(false);
284
- expect(result?.content).toBe(responseText);
285
- });
286
-
287
- test("degrades to a benign result (never throws) when the consult fails", async () => {
288
- recordMessages("c2", [userMsg("hi")]);
289
- sendMessageError = new Error("kaboom");
290
-
291
- const result = await advisorTool.execute?.({}, {
292
- conversationId: "c2",
293
- } as never);
294
-
295
- expect(result?.isError).toBe(false);
296
- expect(result?.content).toContain("advisor unavailable");
297
- expect(result?.content).toContain("kaboom");
298
- });
299
-
300
- test("streams the consult live via `ctx.onOutput`", async () => {
301
- recordMessages("c3", [userMsg("hi")]);
302
- streamDeltas = ["plan: ", "do X"];
303
- const out: string[] = [];
304
-
305
- const result = await advisorTool.execute?.({}, {
306
- conversationId: "c3",
307
- onOutput: (c: string) => out.push(c),
308
- } as never);
309
-
310
- expect(out).toEqual(["plan: ", "do X"]);
311
- expect(result?.isError).toBe(false);
312
- expect(result?.content).toBe(responseText);
313
- });
314
- });
@@ -1,106 +0,0 @@
1
- /**
2
- * Personal-memory gating for the advisor context pack: NOW.md and PKB must only
3
- * reach the advisor when the turn's trust admits personal memory (and, for
4
- * NOW.md, when the scratchpad-injection toggle is on) — the same policy the
5
- * runtime memory injectors apply. Without it, a low-risk advisor consult on a
6
- * remote/trusted-contact turn could forward private content the main agent
7
- * would never receive.
8
- *
9
- * Mocks are isolated to this file (the test runner runs each file in its own
10
- * process), so the broad module stubs here don't leak into other suites.
11
- */
12
- import { beforeEach, describe, expect, mock, test } from "bun:test";
13
-
14
- let personalAllowed = false;
15
- let scratchpadEnabled = true;
16
- let gateArg: unknown = null;
17
-
18
- mock.module("../../../../daemon/trust-context.js", () => ({
19
- isPersonalMemoryAllowed: (trust: unknown) => {
20
- gateArg = trust;
21
- return personalAllowed;
22
- },
23
- }));
24
- mock.module("../../../../daemon/now-scratchpad.js", () => ({
25
- readNowScratchpad: () => "NOW-CONTENT",
26
- }));
27
- mock.module("../../../../memory/pkb/context.js", () => ({
28
- readPkbContext: () => "PKB-CONTENT",
29
- }));
30
- mock.module("../../../../config/loader.js", () => ({
31
- getConfig: () => ({
32
- memory: {
33
- retrieval: { scratchpadInjection: { enabled: scratchpadEnabled } },
34
- },
35
- llm: {},
36
- }),
37
- }));
38
- // Keep every other section empty so the assertions isolate NOW.md / PKB.
39
- mock.module("../../../../daemon/conversation-workspace.js", () => ({
40
- resolveWorkspaceTopLevelContext: () => null,
41
- }));
42
- mock.module("../../../../daemon/conversation-runtime-assembly.js", () => ({
43
- buildActiveDocuments: () => null,
44
- }));
45
- mock.module("../../../../runtime/capabilities.js", () => ({
46
- resolveCapabilities: () => ({ canAccessMemory: false }),
47
- }));
48
- mock.module("../../../../config/skills.js", () => ({
49
- loadSkillCatalog: () => [],
50
- }));
51
-
52
- const { buildAdvisorContext } = await import("../context-pack.js");
53
-
54
- const sources = {
55
- conversationId: "c1",
56
- workingDir: "/tmp",
57
- // A remote, non-guardian per-turn snapshot — the case the live-state read
58
- // could have wrongly elevated.
59
- trustClass: "unknown" as const,
60
- sourceChannel: "telegram",
61
- transcript: [],
62
- allowedToolNames: new Set<string>(),
63
- };
64
-
65
- beforeEach(() => {
66
- personalAllowed = false;
67
- scratchpadEnabled = true;
68
- gateArg = null;
69
- });
70
-
71
- describe("advisor context pack — personal-memory gating", () => {
72
- test("withholds NOW.md and PKB when personal memory is disallowed", async () => {
73
- personalAllowed = false;
74
- const ctx = (await buildAdvisorContext(sources)) ?? "";
75
- expect(ctx).not.toContain("NOW-CONTENT");
76
- expect(ctx).not.toContain("PKB-CONTENT");
77
- });
78
-
79
- test("includes NOW.md and PKB when allowed and the scratchpad toggle is on", async () => {
80
- personalAllowed = true;
81
- scratchpadEnabled = true;
82
- const ctx = await buildAdvisorContext(sources);
83
- expect(ctx).toContain("NOW-CONTENT");
84
- expect(ctx).toContain("PKB-CONTENT");
85
- });
86
-
87
- test("withholds NOW.md when the scratchpad toggle is off, PKB still allowed", async () => {
88
- personalAllowed = true;
89
- scratchpadEnabled = false;
90
- const ctx = (await buildAdvisorContext(sources)) ?? "";
91
- expect(ctx).not.toContain("NOW-CONTENT");
92
- expect(ctx).toContain("PKB-CONTENT");
93
- });
94
-
95
- test("feeds the gate the per-turn trust snapshot, not live conversation state", async () => {
96
- personalAllowed = true;
97
- await buildAdvisorContext(sources);
98
- // The gate must see exactly the snapshot threaded from ToolContext —
99
- // trustClass + executionChannel — so a concurrent live-trust change can't
100
- // elevate this invocation.
101
- expect(gateArg).toEqual({
102
- sourceChannel: "telegram",
103
- trustClass: "unknown",
104
- });
105
- });
106
- });
@@ -1,60 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import type { Message } from "../../../../providers/types.js";
4
- import { buildAdvisorContext, deriveRecallQuery } from "../context-pack.js";
5
-
6
- const userMsg = (t: string): Message => ({
7
- role: "user",
8
- content: [{ type: "text", text: t }],
9
- });
10
-
11
- describe("deriveRecallQuery", () => {
12
- test("returns the most recent user message text", () => {
13
- const query = deriveRecallQuery([
14
- userMsg("the original task"),
15
- { role: "assistant", content: [{ type: "text", text: "ok" }] },
16
- userMsg("the latest question"),
17
- ]);
18
- expect(query).toBe("the latest question");
19
- });
20
-
21
- test("returns null when there is no user text", () => {
22
- expect(
23
- deriveRecallQuery([
24
- { role: "assistant", content: [{ type: "text", text: "hi" }] },
25
- ]),
26
- ).toBeNull();
27
- expect(deriveRecallQuery([])).toBeNull();
28
- });
29
- });
30
-
31
- describe("buildAdvisorContext", () => {
32
- test("lists the agent's available tools, skipping the advisor itself", async () => {
33
- const context = await buildAdvisorContext({
34
- conversationId: "ctx-1",
35
- workingDir: "/tmp/does-not-exist",
36
- allowedToolNames: new Set(["bash", "advisor", "read_file"]),
37
- trustClass: "unknown",
38
- transcript: [userMsg("hi")],
39
- });
40
-
41
- expect(context).toContain("## Available tools");
42
- expect(context).toContain("- bash");
43
- expect(context).toContain("- read_file");
44
- // The advisor advises; it never tells the agent to consult itself.
45
- expect(context).not.toContain("- advisor");
46
- });
47
-
48
- test("omits the tools section when no tools are available", async () => {
49
- const context = await buildAdvisorContext({
50
- conversationId: "ctx-2",
51
- workingDir: "/tmp/does-not-exist",
52
- allowedToolNames: new Set(),
53
- trustClass: "unknown",
54
- transcript: [],
55
- });
56
- // Other sources (e.g. the skills catalog) may still contribute, but with no
57
- // allowed tools the tools section must not appear.
58
- if (context !== null) expect(context).not.toContain("## Available tools");
59
- });
60
- });