@vellumai/assistant 0.8.2 → 0.8.3

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 (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -1,441 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- mock.module("../util/logger.js", () => ({
4
- getLogger: () =>
5
- new Proxy({} as Record<string, unknown>, {
6
- get: () => () => {},
7
- }),
8
- }));
9
-
10
- import {
11
- createCallSession,
12
- createPendingQuestion,
13
- } from "../calls/call-store.js";
14
- import { getDb } from "../memory/db-connection.js";
15
- import { initializeDb } from "../memory/db-init.js";
16
- import {
17
- createGuardianActionDelivery,
18
- createGuardianActionRequest,
19
- finalizeFollowup,
20
- getFollowupDeliveriesByConversation,
21
- getFollowupDeliveriesByDestination,
22
- getGuardianActionRequest,
23
- markTimedOutWithReason,
24
- progressFollowupState,
25
- startFollowupFromExpiredRequest,
26
- updateDeliveryStatus,
27
- } from "../memory/guardian-action-store.js";
28
- import { conversations } from "../memory/schema.js";
29
- import { processGuardianFollowUpTurn } from "../runtime/guardian-action-conversation-turn.js";
30
- import type {
31
- GuardianFollowUpConversationContext,
32
- GuardianFollowUpConversationGenerator,
33
- GuardianFollowUpTurnResult,
34
- } from "../runtime/http-types.js";
35
-
36
- initializeDb();
37
-
38
- function ensureConversation(id: string): void {
39
- const db = getDb();
40
- const now = Date.now();
41
- db.insert(conversations)
42
- .values({
43
- id,
44
- title: `Conversation ${id}`,
45
- createdAt: now,
46
- updatedAt: now,
47
- })
48
- .run();
49
- }
50
-
51
- function resetTables(): void {
52
- const db = getDb();
53
- db.run("DELETE FROM guardian_action_deliveries");
54
- db.run("DELETE FROM guardian_action_requests");
55
- db.run("DELETE FROM call_pending_questions");
56
- db.run("DELETE FROM call_events");
57
- db.run("DELETE FROM call_sessions");
58
- db.run("DELETE FROM messages");
59
- db.run("DELETE FROM conversations");
60
- }
61
-
62
- function createAwaitingChoiceRequest(
63
- convId: string,
64
- opts?: {
65
- chatId?: string;
66
- externalUserId?: string;
67
- conversationId?: string;
68
- },
69
- ) {
70
- ensureConversation(convId);
71
- const session = createCallSession({
72
- conversationId: convId,
73
- provider: "twilio",
74
- fromNumber: "+15550001111",
75
- toNumber: "+15550002222",
76
- });
77
- const pq = createPendingQuestion(session.id, "What is the gate code?");
78
- const request = createGuardianActionRequest({
79
- kind: "ask_guardian",
80
- sourceChannel: "phone",
81
- sourceConversationId: convId,
82
- callSessionId: session.id,
83
- pendingQuestionId: pq.id,
84
- questionText: pq.questionText,
85
- expiresAt: Date.now() - 10_000,
86
- });
87
-
88
- const deliveryConvId = opts?.conversationId ?? `delivery-conv-${request.id}`;
89
- if (opts?.conversationId) {
90
- ensureConversation(opts.conversationId);
91
- } else {
92
- ensureConversation(deliveryConvId);
93
- }
94
- const delivery = createGuardianActionDelivery({
95
- requestId: request.id,
96
- destinationChannel: "telegram",
97
- destinationChatId: opts?.chatId ?? "chat-123",
98
- destinationExternalUserId: opts?.externalUserId ?? "user-456",
99
- destinationConversationId: deliveryConvId,
100
- });
101
- updateDeliveryStatus(delivery.id, "sent");
102
-
103
- // Expire the request
104
- markTimedOutWithReason(request.id, "call_timeout");
105
-
106
- // Start follow-up (transitions to awaiting_guardian_choice)
107
- startFollowupFromExpiredRequest(request.id, "The gate code is 1234");
108
-
109
- return {
110
- request: getGuardianActionRequest(request.id)!,
111
- delivery,
112
- deliveryConvId,
113
- };
114
- }
115
-
116
- // ---------------------------------------------------------------------------
117
- // Helpers for creating mock generators
118
- // ---------------------------------------------------------------------------
119
-
120
- function createMockGenerator(
121
- result: GuardianFollowUpTurnResult,
122
- ): GuardianFollowUpConversationGenerator {
123
- return async (_context: GuardianFollowUpConversationContext) => result;
124
- }
125
-
126
- function createFailingGenerator(): GuardianFollowUpConversationGenerator {
127
- return async () => {
128
- throw new Error("LLM provider unavailable");
129
- };
130
- }
131
-
132
- describe("guardian-action-conversation-turn", () => {
133
- beforeEach(() => {
134
- resetTables();
135
- });
136
-
137
- // ── processGuardianFollowUpTurn: classification ─────────────────────
138
-
139
- test('classifies "call them back" as call_back', async () => {
140
- const generator = createMockGenerator({
141
- disposition: "call_back",
142
- replyText: "Sure, I'll call them back right away.",
143
- });
144
-
145
- const result = await processGuardianFollowUpTurn(
146
- {
147
- questionText: "What is the gate code?",
148
- lateAnswerText: "The gate code is 1234",
149
- guardianReply: "Yes, call them back",
150
- },
151
- generator,
152
- );
153
-
154
- expect(result.disposition).toBe("call_back");
155
- expect(result.replyText).toBe("Sure, I'll call them back right away.");
156
- });
157
-
158
- test('classifies "never mind" as decline', async () => {
159
- const generator = createMockGenerator({
160
- disposition: "decline",
161
- replyText: "No problem. Let me know if you change your mind.",
162
- });
163
-
164
- const result = await processGuardianFollowUpTurn(
165
- {
166
- questionText: "What is the gate code?",
167
- lateAnswerText: "The gate code is 1234",
168
- guardianReply: "Never mind, forget it",
169
- },
170
- generator,
171
- );
172
-
173
- expect(result.disposition).toBe("decline");
174
- expect(result.replyText).toBe(
175
- "No problem. Let me know if you change your mind.",
176
- );
177
- });
178
-
179
- test("classifies ambiguous input as keep_pending with clarification", async () => {
180
- const generator = createMockGenerator({
181
- disposition: "keep_pending",
182
- replyText: "Would you like to call them back or send a text message?",
183
- });
184
-
185
- const result = await processGuardianFollowUpTurn(
186
- {
187
- questionText: "What is the gate code?",
188
- lateAnswerText: "The gate code is 1234",
189
- guardianReply: "hmm I dunno",
190
- },
191
- generator,
192
- );
193
-
194
- expect(result.disposition).toBe("keep_pending");
195
- expect(result.replyText).toContain("call them back");
196
- });
197
-
198
- // ── Failure modes ───────────────────────────────────────────────────
199
-
200
- test("generator failure returns keep_pending with safe fallback", async () => {
201
- const generator = createFailingGenerator();
202
-
203
- const result = await processGuardianFollowUpTurn(
204
- {
205
- questionText: "What is the gate code?",
206
- lateAnswerText: "The gate code is 1234",
207
- guardianReply: "Call them back please",
208
- },
209
- generator,
210
- );
211
-
212
- expect(result.disposition).toBe("keep_pending");
213
- expect(result.replyText.length).toBeGreaterThan(0);
214
- });
215
-
216
- test("no generator returns keep_pending with safe fallback", async () => {
217
- const result = await processGuardianFollowUpTurn(
218
- {
219
- questionText: "What is the gate code?",
220
- lateAnswerText: "The gate code is 1234",
221
- guardianReply: "Call them back",
222
- },
223
- undefined,
224
- );
225
-
226
- expect(result.disposition).toBe("keep_pending");
227
- expect(result.replyText.length).toBeGreaterThan(0);
228
- });
229
-
230
- test("generator returning empty replyText falls back to keep_pending", async () => {
231
- const generator = createMockGenerator({
232
- disposition: "call_back",
233
- replyText: "",
234
- });
235
-
236
- const result = await processGuardianFollowUpTurn(
237
- {
238
- questionText: "What is the gate code?",
239
- lateAnswerText: "The gate code is 1234",
240
- guardianReply: "Call them back",
241
- },
242
- generator,
243
- );
244
-
245
- expect(result.disposition).toBe("keep_pending");
246
- });
247
-
248
- test("generator returning invalid disposition falls back to keep_pending", async () => {
249
- const generator: GuardianFollowUpConversationGenerator = async () => {
250
- return {
251
- disposition:
252
- "invalid_value" as GuardianFollowUpTurnResult["disposition"],
253
- replyText: "Some reply",
254
- };
255
- };
256
-
257
- const result = await processGuardianFollowUpTurn(
258
- {
259
- questionText: "What is the gate code?",
260
- lateAnswerText: "The gate code is 1234",
261
- guardianReply: "Call them back",
262
- },
263
- generator,
264
- );
265
-
266
- expect(result.disposition).toBe("keep_pending");
267
- });
268
-
269
- test("reply text is always present in the result", async () => {
270
- // With generator
271
- const generatorResult = await processGuardianFollowUpTurn(
272
- {
273
- questionText: "What is the gate code?",
274
- lateAnswerText: "The gate code is 1234",
275
- guardianReply: "Call them",
276
- },
277
- createMockGenerator({
278
- disposition: "call_back",
279
- replyText: "Calling now!",
280
- }),
281
- );
282
- expect(typeof generatorResult.replyText).toBe("string");
283
- expect(generatorResult.replyText.length).toBeGreaterThan(0);
284
-
285
- // Without generator
286
- const fallbackResult = await processGuardianFollowUpTurn({
287
- questionText: "What is the gate code?",
288
- lateAnswerText: "The gate code is 1234",
289
- guardianReply: "Call them",
290
- });
291
- expect(typeof fallbackResult.replyText).toBe("string");
292
- expect(fallbackResult.replyText.length).toBeGreaterThan(0);
293
-
294
- // With failing generator
295
- const failResult = await processGuardianFollowUpTurn(
296
- {
297
- questionText: "What is the gate code?",
298
- lateAnswerText: "The gate code is 1234",
299
- guardianReply: "Call them",
300
- },
301
- createFailingGenerator(),
302
- );
303
- expect(typeof failResult.replyText).toBe("string");
304
- expect(failResult.replyText.length).toBeGreaterThan(0);
305
- });
306
-
307
- // ── Store queries for awaiting_guardian_choice ───────────────────────
308
-
309
- test("getFollowupDeliveriesByDestination returns deliveries in awaiting_guardian_choice", () => {
310
- const { request } = createAwaitingChoiceRequest("conv-turn-1", {
311
- chatId: "chat-abc",
312
- externalUserId: "user-xyz",
313
- });
314
-
315
- const deliveries = getFollowupDeliveriesByDestination(
316
- "telegram",
317
- "chat-abc",
318
- );
319
- expect(deliveries).toHaveLength(1);
320
- expect(deliveries[0].requestId).toBe(request.id);
321
- });
322
-
323
- test("getFollowupDeliveriesByDestination returns empty for non-matching channel", () => {
324
- createAwaitingChoiceRequest("conv-turn-2", { chatId: "chat-abc" });
325
-
326
- const deliveries = getFollowupDeliveriesByDestination("phone", "chat-abc");
327
- expect(deliveries).toHaveLength(0);
328
- });
329
-
330
- test("getFollowupDeliveriesByDestination returns empty for expired with followup_state=none", () => {
331
- // Create expired request WITHOUT starting follow-up
332
- ensureConversation("conv-turn-3");
333
- const session = createCallSession({
334
- conversationId: "conv-turn-3",
335
- provider: "twilio",
336
- fromNumber: "+15550001111",
337
- toNumber: "+15550002222",
338
- });
339
- const pq = createPendingQuestion(session.id, "Question?");
340
- const request = createGuardianActionRequest({
341
- kind: "ask_guardian",
342
- sourceChannel: "phone",
343
- sourceConversationId: "conv-turn-3",
344
- callSessionId: session.id,
345
- pendingQuestionId: pq.id,
346
- questionText: pq.questionText,
347
- expiresAt: Date.now() - 10_000,
348
- });
349
- ensureConversation(`delivery-conv-${request.id}`);
350
- const delivery = createGuardianActionDelivery({
351
- requestId: request.id,
352
- destinationChannel: "telegram",
353
- destinationChatId: "chat-none",
354
- destinationExternalUserId: "user-none",
355
- destinationConversationId: `delivery-conv-${request.id}`,
356
- });
357
- updateDeliveryStatus(delivery.id, "sent");
358
- markTimedOutWithReason(request.id, "call_timeout");
359
-
360
- // followup_state is 'none' — should not appear
361
- const deliveries = getFollowupDeliveriesByDestination(
362
- "telegram",
363
- "chat-none",
364
- );
365
- expect(deliveries).toHaveLength(0);
366
- });
367
-
368
- test("getFollowupDeliveriesByConversation returns delivery in awaiting_guardian_choice", () => {
369
- const { delivery, deliveryConvId } = createAwaitingChoiceRequest(
370
- "conv-turn-4",
371
- {
372
- conversationId: "mac-conv-1",
373
- },
374
- );
375
-
376
- const found = getFollowupDeliveriesByConversation(deliveryConvId);
377
- expect(found).toHaveLength(1);
378
- expect(found[0].id).toBe(delivery.id);
379
- });
380
-
381
- test("getFollowupDeliveriesByConversation returns empty for non-matching conversation", () => {
382
- createAwaitingChoiceRequest("conv-turn-5", {
383
- conversationId: "mac-conv-2",
384
- });
385
-
386
- const found = getFollowupDeliveriesByConversation("nonexistent-conv");
387
- expect(found).toHaveLength(0);
388
- });
389
-
390
- // ── State transitions from conversation engine results ──────────────
391
-
392
- test("call_back disposition transitions to dispatching with call_back action", () => {
393
- const { request } = createAwaitingChoiceRequest("conv-turn-6");
394
-
395
- // Simulate what the handler does with a call_back disposition
396
- const updated = progressFollowupState(
397
- request.id,
398
- "dispatching",
399
- "call_back",
400
- );
401
- expect(updated).not.toBeNull();
402
- expect(updated!.followupState).toBe("dispatching");
403
- expect(updated!.followupAction).toBe("call_back");
404
- });
405
-
406
- test("decline disposition finalizes to declined", () => {
407
- const { request } = createAwaitingChoiceRequest("conv-turn-8");
408
-
409
- const updated = finalizeFollowup(request.id, "declined");
410
- expect(updated).not.toBeNull();
411
- expect(updated!.followupState).toBe("declined");
412
- expect(updated!.followupCompletedAt).toBeGreaterThan(0);
413
- });
414
-
415
- test("keep_pending disposition does not change state", () => {
416
- const { request } = createAwaitingChoiceRequest("conv-turn-9");
417
-
418
- // No state change for keep_pending — just verify the state is still awaiting_guardian_choice
419
- const reloaded = getGuardianActionRequest(request.id);
420
- expect(reloaded!.followupState).toBe("awaiting_guardian_choice");
421
- });
422
-
423
- test("state transitions are atomic: second call_back after dispatching fails", () => {
424
- const { request } = createAwaitingChoiceRequest("conv-turn-10");
425
-
426
- const first = progressFollowupState(request.id, "dispatching", "call_back");
427
- expect(first).not.toBeNull();
428
-
429
- // Second attempt: already in dispatching, cannot re-enter dispatching
430
- const second = progressFollowupState(
431
- request.id,
432
- "dispatching",
433
- "call_back",
434
- );
435
- expect(second).toBeNull();
436
-
437
- // Original action preserved
438
- const reloaded = getGuardianActionRequest(request.id);
439
- expect(reloaded!.followupAction).toBe("call_back");
440
- });
441
- });
@@ -1,55 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
-
3
- let flagEnabled = false;
4
-
5
- mock.module("../../../config/assistant-feature-flags.js", () => ({
6
- isAssistantFeatureFlagEnabled: (_key: string, _config: unknown) =>
7
- flagEnabled,
8
- }));
9
-
10
- import { getRememberDescription } from "../tools.js";
11
-
12
- const stubConfig = {} as unknown as Parameters<
13
- typeof getRememberDescription
14
- >[0];
15
-
16
- describe("getRememberDescription", () => {
17
- beforeEach(() => {
18
- flagEnabled = false;
19
- });
20
-
21
- test("flag off — returns the default high-pressure description", () => {
22
- const desc = getRememberDescription(stubConfig);
23
- expect(desc).toContain("**CRITICAL:**");
24
- expect(desc).toContain("most frequently used tool");
25
- expect(desc).toContain("almost every turn");
26
- });
27
-
28
- test("flag on — returns the relaxed judgment-framing description", () => {
29
- flagEnabled = true;
30
- const desc = getRememberDescription(stubConfig);
31
- expect(desc).not.toContain("**CRITICAL:**");
32
- expect(desc).not.toContain("almost every turn");
33
- expect(desc).toContain("a retrospective pass");
34
- expect(desc).toContain("Use judgment");
35
- });
36
-
37
- test("the two variants differ", () => {
38
- flagEnabled = false;
39
- const off = getRememberDescription(stubConfig);
40
- flagEnabled = true;
41
- const on = getRememberDescription(stubConfig);
42
- expect(off).not.toBe(on);
43
- });
44
-
45
- test("corrections-are-priority language is preserved in BOTH variants", () => {
46
- flagEnabled = false;
47
- expect(getRememberDescription(stubConfig)).toMatch(
48
- /Corrections are.*highest priority/i,
49
- );
50
- flagEnabled = true;
51
- expect(getRememberDescription(stubConfig)).toMatch(
52
- /Corrections are.*highest priority/i,
53
- );
54
- });
55
- });
@@ -1,99 +0,0 @@
1
- /**
2
- * Guardian follow-up conversation engine.
3
- *
4
- * When a guardian replies to a post-timeout follow-up prompt (e.g. "would you
5
- * like to call them back?"), this engine classifies the
6
- * guardian's intent into a structured disposition and produces a natural reply.
7
- *
8
- * Dispositions:
9
- * - call_back: Guardian wants to call the original caller back
10
- * - decline: Guardian declines to follow up ("never mind", "no thanks")
11
- * - keep_pending: Intent is ambiguous — ask for clarification
12
- *
13
- * The engine uses the daemon-injected generator (LLM with tool calling) when
14
- * available, with a safe fallback that keeps the follow-up pending and returns
15
- * a retry prompt.
16
- */
17
-
18
- import { getLogger } from "../util/logger.js";
19
- import { getGuardianActionFallbackMessage } from "./guardian-action-message-composer.js";
20
- import type {
21
- GuardianFollowUpConversationContext,
22
- GuardianFollowUpConversationGenerator,
23
- GuardianFollowUpDisposition,
24
- GuardianFollowUpTurnResult,
25
- } from "./http-types.js";
26
-
27
- const log = getLogger("guardian-action-conversation-turn");
28
-
29
- // ---------------------------------------------------------------------------
30
- // Fallback text
31
- // ---------------------------------------------------------------------------
32
-
33
- const FALLBACK_RETRY_TEXT = getGuardianActionFallbackMessage({
34
- scenario: "guardian_followup_clarification",
35
- });
36
-
37
- const VALID_DISPOSITIONS: ReadonlySet<string> = new Set([
38
- "call_back",
39
- "decline",
40
- "keep_pending",
41
- ]);
42
-
43
- // ---------------------------------------------------------------------------
44
- // Public API
45
- // ---------------------------------------------------------------------------
46
-
47
- /**
48
- * Process one turn of the guardian follow-up conversation.
49
- *
50
- * Uses the daemon-injected generator when available; on failure or absence,
51
- * returns a safe keep_pending result with a retry prompt so the follow-up
52
- * stays open for the guardian to try again.
53
- */
54
- export async function processGuardianFollowUpTurn(
55
- context: GuardianFollowUpConversationContext,
56
- generator?: GuardianFollowUpConversationGenerator,
57
- ): Promise<GuardianFollowUpTurnResult> {
58
- if (!generator) {
59
- log.warn(
60
- "No guardian follow-up conversation generator available, using fallback",
61
- );
62
- return { disposition: "keep_pending", replyText: FALLBACK_RETRY_TEXT };
63
- }
64
-
65
- try {
66
- const result = await generator(context);
67
-
68
- // Validate the generator's output
69
- if (
70
- !result ||
71
- typeof result.replyText !== "string" ||
72
- result.replyText.trim().length === 0
73
- ) {
74
- log.warn(
75
- "Guardian follow-up generator returned invalid result (missing replyText)",
76
- );
77
- return { disposition: "keep_pending", replyText: FALLBACK_RETRY_TEXT };
78
- }
79
-
80
- if (!VALID_DISPOSITIONS.has(result.disposition)) {
81
- log.warn(
82
- { disposition: result.disposition },
83
- "Guardian follow-up generator returned invalid disposition",
84
- );
85
- return { disposition: "keep_pending", replyText: FALLBACK_RETRY_TEXT };
86
- }
87
-
88
- return {
89
- disposition: result.disposition as GuardianFollowUpDisposition,
90
- replyText: result.replyText.trim(),
91
- };
92
- } catch (err) {
93
- log.warn(
94
- { err },
95
- "Guardian follow-up conversation generator failed, using fallback",
96
- );
97
- return { disposition: "keep_pending", replyText: FALLBACK_RETRY_TEXT };
98
- }
99
- }