@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
@@ -0,0 +1,246 @@
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
+ import { mock } from "bun:test";
3
+
4
+ mock.module("../../util/logger.js", () => ({
5
+ getLogger: () =>
6
+ new Proxy({} as Record<string, unknown>, {
7
+ get: () => () => {},
8
+ }),
9
+ truncateForLog: (value: string) => value,
10
+ }));
11
+
12
+ import { getDb, getSqliteFrom } from "../../memory/db-connection.js";
13
+ import { initializeDb } from "../../memory/db-init.js";
14
+ import type { A2AMessage, Artifact } from "../protocol-types.js";
15
+ import {
16
+ completeWithArtifacts,
17
+ createTask,
18
+ getPushUrl,
19
+ getTask,
20
+ linkConversation,
21
+ updateState,
22
+ } from "../task-store.js";
23
+
24
+ initializeDb();
25
+
26
+ function makeRequestMessage(overrides?: Partial<A2AMessage>): A2AMessage {
27
+ return {
28
+ message_id: crypto.randomUUID(),
29
+ role: "user",
30
+ parts: [{ kind: "text", text: "Hello from sender" }],
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe("a2a-task-store", () => {
36
+ beforeEach(() => {
37
+ const raw = getSqliteFrom(getDb());
38
+ raw.run("DELETE FROM a2a_tasks");
39
+ });
40
+
41
+ // ── State transitions ───────────────────────────────────────────
42
+
43
+ test("createTask returns a task in submitted state", () => {
44
+ const msg = makeRequestMessage();
45
+ const task = createTask({
46
+ senderAssistantId: "assistant-123",
47
+ requestMessage: msg,
48
+ });
49
+
50
+ expect(task.id).toBeTruthy();
51
+ expect(task.status.state).toBe("submitted");
52
+ expect(task.artifacts).toBeUndefined();
53
+ });
54
+
55
+ test("submitted -> working -> completed lifecycle", () => {
56
+ const task = createTask({
57
+ senderAssistantId: "assistant-123",
58
+ requestMessage: makeRequestMessage(),
59
+ });
60
+
61
+ const working = updateState(task.id, "working", "Processing...");
62
+ expect(working.status.state).toBe("working");
63
+ expect(working.status.message).toBeDefined();
64
+
65
+ const completed = updateState(task.id, "completed");
66
+ expect(completed.status.state).toBe("completed");
67
+ });
68
+
69
+ test("cannot transition from terminal state (completed -> working)", () => {
70
+ const task = createTask({
71
+ senderAssistantId: "assistant-123",
72
+ requestMessage: makeRequestMessage(),
73
+ });
74
+
75
+ updateState(task.id, "completed");
76
+
77
+ expect(() => updateState(task.id, "working")).toThrow(/terminal state/);
78
+ });
79
+
80
+ test("cannot transition from failed state", () => {
81
+ const task = createTask({
82
+ senderAssistantId: "assistant-123",
83
+ requestMessage: makeRequestMessage(),
84
+ });
85
+
86
+ updateState(task.id, "failed");
87
+
88
+ expect(() => updateState(task.id, "working")).toThrow(/terminal state/);
89
+ });
90
+
91
+ test("cannot transition from canceled state", () => {
92
+ const task = createTask({
93
+ senderAssistantId: "assistant-123",
94
+ requestMessage: makeRequestMessage(),
95
+ });
96
+
97
+ updateState(task.id, "canceled");
98
+
99
+ expect(() => updateState(task.id, "submitted")).toThrow(/terminal state/);
100
+ });
101
+
102
+ test("cannot transition from rejected state", () => {
103
+ const task = createTask({
104
+ senderAssistantId: "assistant-123",
105
+ requestMessage: makeRequestMessage(),
106
+ });
107
+
108
+ updateState(task.id, "rejected");
109
+
110
+ expect(() => updateState(task.id, "working")).toThrow(/terminal state/);
111
+ });
112
+
113
+ // ── Artifact serialization round-trip ─────────────────────────
114
+
115
+ test("completeWithArtifacts stores and retrieves artifacts via JSON round-trip", () => {
116
+ const task = createTask({
117
+ senderAssistantId: "assistant-123",
118
+ requestMessage: makeRequestMessage(),
119
+ });
120
+
121
+ updateState(task.id, "working");
122
+
123
+ const artifacts: Artifact[] = [
124
+ {
125
+ artifact_id: "art-1",
126
+ parts: [{ kind: "text", text: "Result data" }],
127
+ metadata: { score: 0.95 },
128
+ },
129
+ {
130
+ artifact_id: "art-2",
131
+ parts: [
132
+ { kind: "data", data: { key: "value" } },
133
+ {
134
+ kind: "file",
135
+ url: "https://example.com/file.txt",
136
+ filename: "file.txt",
137
+ },
138
+ ],
139
+ },
140
+ ];
141
+
142
+ const completed = completeWithArtifacts(task.id, artifacts);
143
+ expect(completed.status.state).toBe("completed");
144
+ expect(completed.artifacts).toHaveLength(2);
145
+ expect(completed.artifacts![0].artifact_id).toBe("art-1");
146
+ expect(completed.artifacts![0].metadata).toEqual({ score: 0.95 });
147
+ expect(completed.artifacts![1].parts).toHaveLength(2);
148
+
149
+ // Verify round-trip via fresh getTask
150
+ const fetched = getTask(task.id);
151
+ expect(fetched).not.toBeNull();
152
+ expect(fetched!.artifacts).toEqual(completed.artifacts);
153
+ });
154
+
155
+ test("completeWithArtifacts rejects terminal task", () => {
156
+ const task = createTask({
157
+ senderAssistantId: "assistant-123",
158
+ requestMessage: makeRequestMessage(),
159
+ });
160
+
161
+ updateState(task.id, "completed");
162
+
163
+ expect(() =>
164
+ completeWithArtifacts(task.id, [
165
+ { artifact_id: "art-1", parts: [{ kind: "text", text: "late" }] },
166
+ ]),
167
+ ).toThrow(/terminal state/);
168
+ });
169
+
170
+ // ── Push URL storage and retrieval ────────────────────────────
171
+
172
+ test("push URL is stored and retrieved", () => {
173
+ const task = createTask({
174
+ senderAssistantId: "assistant-123",
175
+ requestMessage: makeRequestMessage(),
176
+ pushUrl: "https://example.com/push",
177
+ });
178
+
179
+ const url = getPushUrl(task.id);
180
+ expect(url).toBe("https://example.com/push");
181
+ });
182
+
183
+ test("getPushUrl returns null when no push URL set", () => {
184
+ const task = createTask({
185
+ senderAssistantId: "assistant-123",
186
+ requestMessage: makeRequestMessage(),
187
+ });
188
+
189
+ const url = getPushUrl(task.id);
190
+ expect(url).toBeNull();
191
+ });
192
+
193
+ test("getPushUrl returns null for unknown task ID", () => {
194
+ const url = getPushUrl("nonexistent-id");
195
+ expect(url).toBeNull();
196
+ });
197
+
198
+ // ── getTask ───────────────────────────────────────────────────
199
+
200
+ test("getTask returns null for unknown ID", () => {
201
+ const result = getTask("nonexistent-id");
202
+ expect(result).toBeNull();
203
+ });
204
+
205
+ test("getTask returns task with context_id when provided", () => {
206
+ const task = createTask({
207
+ contextId: "ctx-456",
208
+ senderAssistantId: "assistant-123",
209
+ requestMessage: makeRequestMessage(),
210
+ });
211
+
212
+ const fetched = getTask(task.id);
213
+ expect(fetched).not.toBeNull();
214
+ expect(fetched!.context_id).toBe("ctx-456");
215
+ });
216
+
217
+ // ── linkConversation ──────────────────────────────────────────
218
+
219
+ test("linkConversation associates a conversation ID", () => {
220
+ const task = createTask({
221
+ senderAssistantId: "assistant-123",
222
+ requestMessage: makeRequestMessage(),
223
+ });
224
+
225
+ linkConversation(task.id, "conv-789");
226
+
227
+ // Verify via raw DB since getTask doesn't expose conversationId
228
+ const raw = getSqliteFrom(getDb());
229
+ const row = raw
230
+ .prepare("SELECT conversation_id FROM a2a_tasks WHERE id = ?")
231
+ .get(task.id) as { conversation_id: string };
232
+ expect(row.conversation_id).toBe("conv-789");
233
+ });
234
+
235
+ test("linkConversation throws for unknown task ID", () => {
236
+ expect(() => linkConversation("nonexistent-id", "conv-123")).toThrow(
237
+ /not found/,
238
+ );
239
+ });
240
+
241
+ // ── updateState error cases ───────────────────────────────────
242
+
243
+ test("updateState throws for unknown task ID", () => {
244
+ expect(() => updateState("nonexistent-id", "working")).toThrow(/not found/);
245
+ });
246
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A2A v1.0 agent card builder.
3
+ *
4
+ * `buildAgentCard()` constructs a spec-compliant agent card from explicit
5
+ * parameters. `getAgentCard()` is a convenience wrapper that reads the
6
+ * assistant name and public base URL from workspace config.
7
+ */
8
+
9
+ import { getConfig } from "../config/loader.js";
10
+ import { getAssistantName } from "../daemon/identity-helpers.js";
11
+ import { getPublicBaseUrl } from "../inbound/public-ingress-urls.js";
12
+ import type { AgentCard } from "./protocol-types.js";
13
+
14
+ export interface BuildAgentCardParams {
15
+ assistantName: string;
16
+ assistantDescription?: string;
17
+ baseUrl: string;
18
+ }
19
+
20
+ export function buildAgentCard(params: BuildAgentCardParams): AgentCard {
21
+ return {
22
+ name: params.assistantName,
23
+ description:
24
+ params.assistantDescription ??
25
+ `${params.assistantName} — a Vellum AI assistant`,
26
+ version: "1.0.0",
27
+ supported_interfaces: [
28
+ {
29
+ url: `${params.baseUrl}/a2a/message:send`,
30
+ protocol_binding: "JSONRPC",
31
+ protocol_version: "1.0",
32
+ },
33
+ ],
34
+ capabilities: {
35
+ streaming: false,
36
+ push_notifications: true,
37
+ extended_agent_card: false,
38
+ },
39
+ default_input_modes: ["text/plain"],
40
+ default_output_modes: ["text/plain"],
41
+ skills: [
42
+ {
43
+ id: "conversation",
44
+ name: "General conversation",
45
+ description: "Send a message and receive a response",
46
+ tags: ["chat"],
47
+ },
48
+ ],
49
+ };
50
+ }
51
+
52
+ export function getAgentCard(): AgentCard {
53
+ const config = getConfig();
54
+ const assistantName = getAssistantName() ?? "Vellum Assistant";
55
+ const baseUrl = getPublicBaseUrl(config);
56
+
57
+ return buildAgentCard({ assistantName, baseUrl });
58
+ }
@@ -0,0 +1,8 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
2
+ import type { AssistantConfig } from "../config/schema.js";
3
+
4
+ const A2A_FLAG_KEY = "a2a-channel" as const;
5
+
6
+ export function isA2AEnabled(config: AssistantConfig): boolean {
7
+ return isAssistantFeatureFlagEnabled(A2A_FLAG_KEY, config);
8
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A2A v1.0 protocol constants.
3
+ */
4
+
5
+ import type { TaskState } from "./protocol-types.js";
6
+
7
+ export const A2A_VERSION = "1.0";
8
+ export const A2A_CONTENT_TYPE = "application/a2a+json";
9
+ export const A2A_VERSION_HEADER = "A2A-Version";
10
+ export const AGENT_CARD_PATH = "/.well-known/agent-card.json";
11
+
12
+ /**
13
+ * Task states that represent a terminal (final) condition.
14
+ * Once a task reaches one of these states it will not transition further.
15
+ */
16
+ export const TERMINAL_TASK_STATES: ReadonlySet<TaskState> = new Set([
17
+ "completed",
18
+ "failed",
19
+ "canceled",
20
+ "rejected",
21
+ ]);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A2A JSON-RPC error codes and helper functions.
3
+ */
4
+
5
+ import type { JsonRpcResponse } from "./protocol-types.js";
6
+
7
+ // ── Standard JSON-RPC error codes ───────────────────────────────────
8
+
9
+ export const PARSE_ERROR = -32700;
10
+ export const INVALID_REQUEST = -32600;
11
+ export const METHOD_NOT_FOUND = -32601;
12
+ export const INVALID_PARAMS = -32602;
13
+ export const INTERNAL_ERROR = -32603;
14
+
15
+ // ── A2A-specific error codes ────────────────────────────────────────
16
+
17
+ export const TASK_NOT_FOUND = -32001;
18
+ export const TASK_NOT_CANCELABLE = -32002;
19
+ export const PUSH_NOTIFICATION_NOT_SUPPORTED = -32003;
20
+ export const UNSUPPORTED_OPERATION = -32004;
21
+
22
+ // ── Helpers ─────────────────────────────────────────────────────────
23
+
24
+ export function makeJsonRpcError(
25
+ id: string | number | null,
26
+ code: number,
27
+ message: string,
28
+ data?: unknown,
29
+ ): JsonRpcResponse {
30
+ return {
31
+ jsonrpc: "2.0",
32
+ id,
33
+ error: {
34
+ code,
35
+ message,
36
+ ...(data !== undefined && { data }),
37
+ },
38
+ };
39
+ }
40
+
41
+ export function makeJsonRpcSuccess(
42
+ id: string | number | null,
43
+ result: unknown,
44
+ ): JsonRpcResponse {
45
+ return {
46
+ jsonrpc: "2.0",
47
+ id,
48
+ result,
49
+ };
50
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * A2A v1.0 protocol type definitions.
3
+ *
4
+ * Implemented directly from the A2A spec — no SDK dependency.
5
+ * See https://google.github.io/A2A/
6
+ */
7
+
8
+ // ── Agent Card ──────────────────────────────────────────────────────
9
+
10
+ export interface AgentCard {
11
+ name: string;
12
+ description: string;
13
+ version: string;
14
+ supported_interfaces: AgentInterface[];
15
+ capabilities: AgentCapabilities;
16
+ default_input_modes: string[];
17
+ default_output_modes: string[];
18
+ skills: AgentSkill[];
19
+ security_schemes?: Record<string, unknown>;
20
+ security_requirements?: Record<string, string[]>[];
21
+ }
22
+
23
+ export interface AgentInterface {
24
+ url: string;
25
+ protocol_binding: string;
26
+ protocol_version: string;
27
+ }
28
+
29
+ export interface AgentSkill {
30
+ id: string;
31
+ name: string;
32
+ description: string;
33
+ tags: string[];
34
+ }
35
+
36
+ export interface AgentCapabilities {
37
+ streaming: boolean;
38
+ push_notifications: boolean;
39
+ extended_agent_card: boolean;
40
+ }
41
+
42
+ // ── Messages ────────────────────────────────────────────────────────
43
+
44
+ export interface A2AMessage {
45
+ message_id: string;
46
+ context_id?: string;
47
+ task_id?: string;
48
+ role: "user" | "agent";
49
+ parts: Part[];
50
+ metadata?: Record<string, unknown>;
51
+ }
52
+
53
+ export type Part = TextPart | DataPart | FilePart;
54
+
55
+ export interface TextPart {
56
+ kind: "text";
57
+ text: string;
58
+ }
59
+
60
+ export interface DataPart {
61
+ kind: "data";
62
+ data: Record<string, unknown>;
63
+ media_type?: string;
64
+ }
65
+
66
+ export interface FilePart {
67
+ kind: "file";
68
+ url?: string;
69
+ raw?: string;
70
+ filename?: string;
71
+ media_type?: string;
72
+ }
73
+
74
+ // ── Tasks ───────────────────────────────────────────────────────────
75
+
76
+ export type TaskState =
77
+ | "submitted"
78
+ | "working"
79
+ | "completed"
80
+ | "failed"
81
+ | "canceled"
82
+ | "input_required"
83
+ | "rejected";
84
+
85
+ export interface TaskStatus {
86
+ state: TaskState;
87
+ message?: A2AMessage;
88
+ timestamp: string;
89
+ }
90
+
91
+ export interface A2ATask {
92
+ id: string;
93
+ context_id?: string;
94
+ status: TaskStatus;
95
+ artifacts?: Artifact[];
96
+ history?: A2AMessage[];
97
+ metadata?: Record<string, unknown>;
98
+ }
99
+
100
+ // ── Artifacts ───────────────────────────────────────────────────────
101
+
102
+ export interface Artifact {
103
+ artifact_id: string;
104
+ parts: Part[];
105
+ metadata?: Record<string, unknown>;
106
+ }
107
+
108
+ // ── Requests / Responses ────────────────────────────────────────────
109
+
110
+ export interface TaskPushNotificationConfig {
111
+ url: string;
112
+ authentication?: Record<string, unknown>;
113
+ }
114
+
115
+ export interface SendMessageConfiguration {
116
+ accepted_output_modes?: string[];
117
+ history_length?: number;
118
+ return_immediately?: boolean;
119
+ task_push_notification_config?: TaskPushNotificationConfig;
120
+ }
121
+
122
+ export interface SendMessageRequest {
123
+ message: A2AMessage;
124
+ configuration?: SendMessageConfiguration;
125
+ }
126
+
127
+ export type SendMessageResponse = { task: A2ATask } | { message: A2AMessage };
128
+
129
+ // ── Push Events ─────────────────────────────────────────────────────
130
+
131
+ export interface TaskStatusUpdateEvent {
132
+ task_id: string;
133
+ status: TaskStatus;
134
+ final: boolean;
135
+ }
136
+
137
+ export interface TaskArtifactUpdateEvent {
138
+ task_id: string;
139
+ artifact: Artifact;
140
+ }
141
+
142
+ // ── JSON-RPC ────────────────────────────────────────────────────────
143
+
144
+ export interface JsonRpcRequest {
145
+ jsonrpc: "2.0";
146
+ id: string | number;
147
+ method: string;
148
+ params?: unknown;
149
+ }
150
+
151
+ export interface JsonRpcError {
152
+ code: number;
153
+ message: string;
154
+ data?: unknown;
155
+ }
156
+
157
+ export interface JsonRpcResponse {
158
+ jsonrpc: "2.0";
159
+ id: string | number | null;
160
+ result?: unknown;
161
+ error?: JsonRpcError;
162
+ }