@axiom-lattice/gateway 3.0.2 → 3.0.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 (155) hide show
  1. package/dist/index.js +31 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +31 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
package/src/routes/a2a.ts DELETED
@@ -1,851 +0,0 @@
1
- import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2
- import { v4 } from "uuid";
3
- import {
4
- agentInstanceManager,
5
- } from "@axiom-lattice/core";
6
- import { MessageChunkTypes } from "@axiom-lattice/protocols";
7
- import type {
8
- AgentCard,
9
- A2ATask,
10
- A2ATaskState,
11
- A2ATaskSendRequest,
12
- A2AMessage,
13
- A2APart,
14
- A2AAuthContext,
15
- A2AApiKeyStore,
16
- A2AApiKeyRecord,
17
- CreateA2AApiKeyInput,
18
- A2AApiKeyEntry,
19
- } from "@axiom-lattice/protocols";
20
-
21
- // ─── Task Tracking ────────────────────────────────────────────────────────
22
-
23
- interface TaskRecord {
24
- id: string;
25
- threadId: string;
26
- contextId?: string;
27
- referenceTaskIds?: string[];
28
- messageId?: string;
29
- assistantId: string;
30
- tenantId: string;
31
- projectId?: string;
32
- workspaceId?: string;
33
- status: A2ATaskState;
34
- createdAt: string;
35
- updatedAt: string;
36
- history: A2AMessage[];
37
- }
38
-
39
- const taskStore = new Map<string, TaskRecord>();
40
-
41
- const MAX_CONTEXT_THREADS = 100;
42
- const contextThreadMap = new Map<string, string>();
43
-
44
- function cacheContextThread(contextId: string, threadId: string): void {
45
- if (contextThreadMap.size >= MAX_CONTEXT_THREADS) {
46
- const oldest = contextThreadMap.keys().next().value;
47
- if (oldest !== undefined) contextThreadMap.delete(oldest);
48
- }
49
- contextThreadMap.set(contextId, threadId);
50
- }
51
-
52
- // ─── Key Store ────────────────────────────────────────────────────────────
53
-
54
- let _a2aKeyStore: A2AApiKeyStore | null = null;
55
- let _storeKeyMap: Map<string, A2AApiKeyEntry> = new Map();
56
-
57
- export function setA2AKeyStore(store: A2AApiKeyStore): void {
58
- _a2aKeyStore = store;
59
- }
60
-
61
- /**
62
- * Reload the cached key map from the store.
63
- * Call after any CRUD mutation or at startup.
64
- */
65
- export async function refreshStoreKeyMap(): Promise<void> {
66
- if (!_a2aKeyStore) return;
67
- _storeKeyMap = await _a2aKeyStore.loadIntoMap();
68
- }
69
-
70
- // ─── Config ───────────────────────────────────────────────────────────────
71
-
72
- function parseEnvKeyMap(): Map<string, A2AApiKeyEntry> {
73
- const keysEnv = process.env.A2A_API_KEYS || "";
74
- const defaultTenantId = process.env.A2A_DEFAULT_TENANT_ID || "a2a-default-tenant";
75
- const defaultProjectId = process.env.A2A_DEFAULT_PROJECT_ID || "";
76
- const defaultWorkspaceId = process.env.A2A_DEFAULT_WORKSPACE_ID || "";
77
- const map = new Map<string, A2AApiKeyEntry>();
78
-
79
- if (keysEnv) {
80
- keysEnv.split(",").forEach((entry) => {
81
- const trimmed = entry.trim();
82
- if (!trimmed) return;
83
- const parts = trimmed.split(":");
84
- const key = parts[0];
85
- if (!key) return;
86
- map.set(key, {
87
- key,
88
- tenantId: parts[1]?.trim() || defaultTenantId,
89
- projectId: parts[2]?.trim() || defaultProjectId || undefined,
90
- workspaceId: parts[3]?.trim() || defaultWorkspaceId || undefined,
91
- });
92
- });
93
- }
94
-
95
- return map;
96
- }
97
-
98
- function getA2AConfig() {
99
- const envMap = parseEnvKeyMap();
100
- // Store keys take priority; env keys serve as fallback
101
- const apiKeyMap = new Map([..._storeKeyMap, ...envMap]);
102
-
103
- return {
104
- agentName: process.env.A2A_AGENT_NAME || "Axiom Lattice Agent",
105
- agentDescription:
106
- process.env.A2A_AGENT_DESCRIPTION ||
107
- "AI agent powered by Axiom Lattice framework",
108
- organization: process.env.A2A_ORGANIZATION || "Axiom Lattice",
109
- organizationUrl: process.env.A2A_ORGANIZATION_URL || "https://axiom-lattice.ai",
110
- version: process.env.A2A_VERSION || "1.0.0",
111
- defaultAssistantId: process.env.A2A_DEFAULT_AGENT_ID || "",
112
- defaultTenantId: process.env.A2A_DEFAULT_TENANT_ID || "a2a-default-tenant",
113
- defaultProjectId: process.env.A2A_DEFAULT_PROJECT_ID || "",
114
- defaultWorkspaceId: process.env.A2A_DEFAULT_WORKSPACE_ID || "",
115
- apiKeyMap,
116
- };
117
- }
118
-
119
- // ─── Auth ─────────────────────────────────────────────────────────────────
120
-
121
- function authenticateA2A(
122
- request: FastifyRequest,
123
- ): A2AAuthContext {
124
- const config = getA2AConfig();
125
- const noAuthRequired = config.apiKeyMap.size === 0;
126
-
127
- const token =
128
- request.headers.authorization?.startsWith("Bearer ")
129
- ? request.headers.authorization.substring(7)
130
- : (request.headers["x-api-key"] as string | undefined);
131
-
132
- if (!token) {
133
- return {
134
- authenticated: noAuthRequired,
135
- tenantId: config.defaultTenantId,
136
- projectId: config.defaultProjectId || undefined,
137
- workspaceId: config.defaultWorkspaceId || undefined,
138
- };
139
- }
140
-
141
- const entry = config.apiKeyMap.get(token);
142
- if (entry) {
143
- return {
144
- authenticated: true,
145
- apiKey: token,
146
- tenantId: entry.tenantId,
147
- projectId: entry.projectId,
148
- workspaceId: entry.workspaceId,
149
- source: request.headers.authorization?.startsWith("Bearer ") ? "bearer" : "x-api-key",
150
- };
151
- }
152
-
153
- return { authenticated: false };
154
- }
155
-
156
- function requireAuth(
157
- request: FastifyRequest,
158
- reply: FastifyReply,
159
- ): A2AAuthContext {
160
- const auth = authenticateA2A(request);
161
- if (!auth.authenticated) {
162
- reply.status(401).send({
163
- error: "Unauthorized",
164
- message: "Valid API key required via Bearer token or X-API-Key header",
165
- });
166
- }
167
- return auth;
168
- }
169
-
170
- // ─── Agent Card ───────────────────────────────────────────────────────────
171
-
172
- function buildAgentCard(baseUrl: string): AgentCard {
173
- const config = getA2AConfig();
174
- return {
175
- name: config.agentName,
176
- description: config.agentDescription,
177
- url: `${baseUrl}/api/a2a`,
178
- provider: {
179
- organization: config.organization,
180
- url: config.organizationUrl,
181
- },
182
- version: config.version,
183
- capabilities: {
184
- streaming: true,
185
- pushNotifications: false,
186
- stateTransitionHistory: false,
187
- },
188
- defaultInputModes: ["text", "text/plain"],
189
- defaultOutputModes: ["text", "text/plain", "text/markdown"],
190
- skills: [],
191
- };
192
- }
193
-
194
- // ─── Helpers ──────────────────────────────────────────────────────────────
195
-
196
- function extractTextFromParts(parts: A2APart[]): string {
197
- return parts
198
- .filter((p): p is { type: "text"; text: string } => p.type === "text")
199
- .map((p) => p.text)
200
- .join("\n");
201
- }
202
-
203
- function buildTextPart(text: string): A2APart {
204
- return { type: "text", text };
205
- }
206
-
207
- function isoNow(): string {
208
- return new Date().toISOString();
209
- }
210
-
211
- type SendEventFn = (event: string, data: Record<string, unknown>) => void;
212
-
213
- async function streamChunksAsA2A(
214
- stream: AsyncIterable<{ type: unknown; data?: { content?: string } }>,
215
- taskId: string,
216
- contextId: string | undefined,
217
- sendEvent: SendEventFn,
218
- onInterrupt?: () => void,
219
- ): Promise<{ text: string; interrupted: boolean }> {
220
- let accumulatedText = "";
221
- let interrupted = false;
222
-
223
- for await (const chunk of stream) {
224
- const chunkText = (chunk as { data?: { content?: string } }).data?.content || "";
225
- const chunkType = (chunk as { type: string }).type;
226
-
227
- if (chunkType === MessageChunkTypes.INTERRUPT) {
228
- sendEvent("status-update", {
229
- id: taskId,
230
- contextId,
231
- status: {
232
- state: "input-required",
233
- message: {
234
- role: "agent",
235
- parts: [buildTextPart(accumulatedText)],
236
- },
237
- timestamp: isoNow(),
238
- },
239
- final: false,
240
- });
241
- onInterrupt?.();
242
- interrupted = true;
243
- break;
244
- }
245
-
246
- if (
247
- chunkType === MessageChunkTypes.AI ||
248
- chunkType === MessageChunkTypes.TOOL
249
- ) {
250
- if (chunkText) {
251
- accumulatedText += chunkText;
252
- sendEvent("status-update", {
253
- id: taskId,
254
- contextId,
255
- status: {
256
- state: "working",
257
- message: {
258
- role: "agent",
259
- parts: [buildTextPart(chunkText)],
260
- },
261
- timestamp: isoNow(),
262
- },
263
- final: false,
264
- });
265
- }
266
- }
267
- }
268
-
269
- return { text: accumulatedText, interrupted };
270
- }
271
-
272
- // ─── Task Status ──────────────────────────────────────────────────────────
273
-
274
- async function getTaskStatus(taskId: string): Promise<A2ATask | null> {
275
- const record = taskStore.get(taskId);
276
- if (!record) return null;
277
-
278
- return {
279
- id: record.id,
280
- status: {
281
- state: record.status,
282
- timestamp: record.updatedAt,
283
- },
284
- artifacts: [],
285
- history: record.history,
286
- };
287
- }
288
-
289
- // ─── Handlers ─────────────────────────────────────────────────────────────
290
-
291
- async function handleAgentCard(
292
- request: FastifyRequest,
293
- reply: FastifyReply,
294
- ): Promise<void> {
295
- const protocol =
296
- (request.headers["x-forwarded-proto"] as string) ||
297
- request.protocol;
298
- const host = request.hostname;
299
- const baseUrl = `${protocol}://${host}`;
300
- const card = buildAgentCard(baseUrl);
301
- reply.status(200).send(card);
302
- }
303
-
304
- async function handleTasksGet(
305
- request: FastifyRequest<{ Params: { taskId: string } }>,
306
- reply: FastifyReply,
307
- ): Promise<void> {
308
- const auth = requireAuth(request, reply);
309
- if (!auth.authenticated) return;
310
- const task = await getTaskStatus(request.params.taskId);
311
- if (!task) {
312
- reply.status(404).send({ error: "Task not found" });
313
- return;
314
- }
315
- reply.status(200).send(task);
316
- }
317
-
318
- async function handleTasksSend(
319
- request: FastifyRequest<{ Body: A2ATaskSendRequest }>,
320
- reply: FastifyReply,
321
- ): Promise<void> {
322
- const auth = requireAuth(request, reply);
323
- if (!auth.authenticated) return;
324
-
325
- const config = getA2AConfig();
326
- const body = request.body as A2ATaskSendRequest;
327
-
328
- if (!body.message || !body.message.parts || body.message.parts.length === 0) {
329
- reply.status(400).send({ error: "message.parts is required" });
330
- return;
331
- }
332
-
333
- const text = extractTextFromParts(body.message.parts);
334
- if (!text.trim()) {
335
- reply.status(400).send({ error: "message must contain text content" });
336
- return;
337
- }
338
-
339
- const assistantId =
340
- (request.headers["x-a2a-agent-id"] as string) ||
341
- config.defaultAssistantId;
342
-
343
- if (!assistantId) {
344
- reply.status(500).send({
345
- error: "No agent configured",
346
- message:
347
- "Set A2A_DEFAULT_AGENT_ID env var or provide x-a2a-agent-id header",
348
- });
349
- return;
350
- }
351
-
352
- const contextId = body.message.contextId;
353
- const referenceTaskIds = body.message.referenceTaskIds;
354
- const taskId = body.id || v4();
355
-
356
- let threadId: string;
357
- let resolvedReferenceTaskIds: string[] | undefined;
358
-
359
- // Explicit task continuation: if body.id matches a stored task
360
- if (body.id) {
361
- const existingTask = taskStore.get(body.id);
362
- if (existingTask) {
363
- if (existingTask.status === "input-required") {
364
- threadId = existingTask.threadId;
365
- resolvedReferenceTaskIds = referenceTaskIds;
366
- } else if (
367
- existingTask.status === "completed" ||
368
- existingTask.status === "failed" ||
369
- existingTask.status === "canceled"
370
- ) {
371
- threadId = v4();
372
- resolvedReferenceTaskIds = referenceTaskIds
373
- ? [...referenceTaskIds, body.id]
374
- : [body.id];
375
- } else {
376
- threadId = v4();
377
- resolvedReferenceTaskIds = referenceTaskIds;
378
- }
379
- } else {
380
- threadId = v4();
381
- resolvedReferenceTaskIds = referenceTaskIds
382
- ? [...referenceTaskIds, body.id]
383
- : [body.id];
384
- }
385
- } else if (contextId && contextThreadMap.has(contextId)) {
386
- threadId = contextThreadMap.get(contextId)!;
387
- console.warn({
388
- event: "a2a:deprecated:contextId_thread_reuse",
389
- message: "contextId-based thread reuse is deprecated; use referenceTaskIds and application-layer conversation state instead",
390
- contextId,
391
- threadId,
392
- });
393
- resolvedReferenceTaskIds = referenceTaskIds;
394
- } else {
395
- threadId = v4();
396
- resolvedReferenceTaskIds = referenceTaskIds;
397
- }
398
-
399
- const tenantId = auth.tenantId || config.defaultTenantId;
400
-
401
- const now = isoNow();
402
-
403
- const record: TaskRecord = {
404
- id: taskId,
405
- threadId,
406
- contextId,
407
- referenceTaskIds: resolvedReferenceTaskIds,
408
- assistantId,
409
- tenantId,
410
- projectId: auth.projectId || undefined,
411
- workspaceId: auth.workspaceId || undefined,
412
- status: "working",
413
- createdAt: now,
414
- updatedAt: now,
415
- history: [
416
- {
417
- role: "user",
418
- parts: body.message.parts,
419
- },
420
- ],
421
- };
422
-
423
- taskStore.set(taskId, record);
424
-
425
- let agent;
426
- try {
427
- agent = agentInstanceManager.getAgent({
428
- assistant_id: assistantId,
429
- thread_id: threadId,
430
- tenant_id: tenantId,
431
- workspace_id: auth.workspaceId || undefined,
432
- project_id: auth.projectId || undefined,
433
- });
434
- } catch (err) {
435
- record.status = "failed";
436
- record.updatedAt = isoNow();
437
- reply.status(500).send({
438
- id: taskId,
439
- error: "Failed to initialize agent",
440
- message: err instanceof Error ? err.message : String(err),
441
- });
442
- return;
443
- }
444
-
445
- reply.hijack();
446
- reply.raw.writeHead(200, {
447
- "Content-Type": "text/event-stream",
448
- "Cache-Control": "no-cache",
449
- Connection: "keep-alive",
450
- "Access-Control-Allow-Origin": "*",
451
- });
452
-
453
- const sendEvent = (event: string, data: Record<string, unknown>): void => {
454
- reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
455
- };
456
-
457
- try {
458
- const result = await agent.addMessage({
459
- input: { message: text },
460
- });
461
-
462
- record.messageId = result.messageId;
463
-
464
- const stream = agent.chunkStream(result.messageId, [
465
- MessageChunkTypes.MESSAGE_COMPLETED,
466
- ]);
467
-
468
- const { text: accumulatedText, interrupted } = await streamChunksAsA2A(stream, taskId, contextId, sendEvent, () => {
469
- record.status = "input-required";
470
- record.updatedAt = isoNow();
471
- });
472
-
473
- if (!interrupted) {
474
- record.status = "completed";
475
- record.updatedAt = isoNow();
476
- record.history.push({
477
- role: "agent",
478
- parts: [buildTextPart(accumulatedText)],
479
- });
480
-
481
- sendEvent("status-update", {
482
- id: taskId,
483
- contextId,
484
- status: {
485
- state: "completed",
486
- timestamp: isoNow(),
487
- },
488
- final: true,
489
- });
490
- }
491
- } catch (err) {
492
- record.status = "failed";
493
- record.updatedAt = isoNow();
494
-
495
- sendEvent("error", {
496
- code: "EXECUTION_ERROR",
497
- message: err instanceof Error ? err.message : String(err),
498
- });
499
-
500
- sendEvent("status-update", {
501
- id: taskId,
502
- contextId,
503
- status: {
504
- state: "failed",
505
- timestamp: isoNow(),
506
- },
507
- final: true,
508
- });
509
- } finally {
510
- reply.raw.end();
511
- }
512
- }
513
-
514
- async function handleTasksCancel(
515
- request: FastifyRequest<{ Params: { taskId: string } }>,
516
- reply: FastifyReply,
517
- ): Promise<void> {
518
- const auth = requireAuth(request, reply);
519
- if (!auth.authenticated) return;
520
- const { taskId } = request.params;
521
- const record = taskStore.get(taskId);
522
-
523
- if (!record) {
524
- reply.status(404).send({ error: "Task not found" });
525
- return;
526
- }
527
-
528
- if (record.status === "completed" || record.status === "failed" || record.status === "canceled") {
529
- reply.status(409).send({
530
- error: "Task is already in a terminal state",
531
- task: { id: record.id, status: record.status },
532
- });
533
- return;
534
- }
535
-
536
- try {
537
- const agent = agentInstanceManager.getAgent({
538
- assistant_id: record.assistantId,
539
- thread_id: record.threadId,
540
- tenant_id: record.tenantId,
541
- workspace_id: record.workspaceId,
542
- project_id: record.projectId,
543
- });
544
- await agent.abort();
545
- } catch (err) {
546
- // Agent session may have already been cleaned up
547
- console.warn({
548
- event: "a2a:cancel:no_agent",
549
- taskId,
550
- threadId: record.threadId,
551
- error: err instanceof Error ? err.message : String(err),
552
- });
553
- }
554
-
555
- record.status = "canceled";
556
- record.updatedAt = isoNow();
557
-
558
- reply.status(200).send({
559
- id: taskId,
560
- status: {
561
- state: "canceled",
562
- timestamp: record.updatedAt,
563
- },
564
- });
565
- }
566
-
567
- async function handleTasksStream(
568
- request: FastifyRequest<{ Params: { taskId: string } }>,
569
- reply: FastifyReply,
570
- ): Promise<void> {
571
- const auth = requireAuth(request, reply);
572
- if (!auth.authenticated) return;
573
-
574
- const { taskId } = request.params;
575
- const record = taskStore.get(taskId);
576
-
577
- if (!record) {
578
- reply.status(404).send({ error: "Task not found" });
579
- return;
580
- }
581
-
582
- let agent;
583
- try {
584
- agent = agentInstanceManager.getAgent({
585
- assistant_id: record.assistantId,
586
- thread_id: record.threadId,
587
- tenant_id: record.tenantId,
588
- workspace_id: record.workspaceId,
589
- project_id: record.projectId,
590
- });
591
- } catch {
592
- reply.status(404).send({
593
- error: "Agent session not found. The task may have already completed.",
594
- });
595
- return;
596
- }
597
-
598
- reply.hijack();
599
- reply.raw.writeHead(200, {
600
- "Content-Type": "text/event-stream",
601
- "Cache-Control": "no-cache",
602
- Connection: "keep-alive",
603
- "Access-Control-Allow-Origin": "*",
604
- });
605
-
606
- const sendEvent = (event: string, data: Record<string, unknown>): void => {
607
- reply.raw.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
608
- };
609
-
610
- if (!record.messageId) {
611
- sendEvent("status-update", {
612
- id: taskId,
613
- contextId: record.contextId,
614
- status: {
615
- state: record.status,
616
- timestamp: record.updatedAt,
617
- },
618
- final: true,
619
- });
620
- reply.raw.end();
621
- return;
622
- }
623
-
624
- try {
625
- const stream = agent.chunkStream(record.messageId, [
626
- MessageChunkTypes.MESSAGE_COMPLETED,
627
- ]);
628
-
629
- const { interrupted } = await streamChunksAsA2A(stream, taskId, record.contextId, sendEvent);
630
-
631
- if (!interrupted) {
632
- sendEvent("status-update", {
633
- id: taskId,
634
- contextId: record.contextId,
635
- status: {
636
- state: record.status,
637
- timestamp: record.updatedAt,
638
- },
639
- final: true,
640
- });
641
- }
642
- } catch (err) {
643
- sendEvent("error", {
644
- code: "STREAM_ERROR",
645
- message: err instanceof Error ? err.message : String(err),
646
- });
647
- } finally {
648
- reply.raw.end();
649
- }
650
- }
651
-
652
- // ─── API Key Management Handlers ─────────────────────────────────────────
653
-
654
- async function handleApiKeyList(
655
- request: FastifyRequest<{ Querystring: { tenantId?: string; limit?: string; offset?: string } }>,
656
- reply: FastifyReply,
657
- ): Promise<void> {
658
- const auth = requireAuth(request, reply);
659
- if (!auth.authenticated) return;
660
-
661
- if (!_a2aKeyStore) {
662
- reply.status(501).send({ error: "Key store not configured" });
663
- return;
664
- }
665
-
666
- const records = await _a2aKeyStore.list({
667
- tenantId: request.query.tenantId,
668
- limit: request.query.limit ? parseInt(request.query.limit, 10) : undefined,
669
- offset: request.query.offset ? parseInt(request.query.offset, 10) : undefined,
670
- });
671
-
672
- reply.status(200).send({
673
- success: true,
674
- data: {
675
- records: records.map((r) => ({ ...r, key: r.key.slice(0, 8) + "..." })),
676
- total: records.length,
677
- },
678
- });
679
- }
680
-
681
- async function handleApiKeyCreate(
682
- request: FastifyRequest<{ Body: Partial<CreateA2AApiKeyInput> }>,
683
- reply: FastifyReply,
684
- ): Promise<void> {
685
- const auth = requireAuth(request, reply);
686
- if (!auth.authenticated) return;
687
-
688
- if (!_a2aKeyStore) {
689
- reply.status(501).send({ error: "Key store not configured" });
690
- return;
691
- }
692
-
693
- const body = request.body as Partial<CreateA2AApiKeyInput>;
694
- // Scope from request body / headers; fallback to auth context
695
- const tenantId =
696
- body.tenantId ||
697
- (request.headers["x-tenant-id"] as string) ||
698
- auth.tenantId;
699
- const projectId =
700
- body.projectId || (request.headers["x-project-id"] as string) || auth.projectId || "default";
701
- const workspaceId =
702
- body.workspaceId || (request.headers["x-workspace-id"] as string) || auth.workspaceId;
703
-
704
- if (!tenantId) {
705
- reply.status(400).send({
706
- error: "tenantId is required",
707
- message: "Provide via body.tenantId, x-tenant-id header, or authenticate with a scoped API key",
708
- });
709
- return;
710
- }
711
-
712
- const record = await _a2aKeyStore.create({
713
- tenantId,
714
- projectId,
715
- workspaceId,
716
- label: body.label,
717
- });
718
- await refreshStoreKeyMap();
719
-
720
- reply.status(201).send({ success: true, data: record });
721
- }
722
-
723
- async function handleApiKeyDelete(
724
- request: FastifyRequest<{ Params: { id: string } }>,
725
- reply: FastifyReply,
726
- ): Promise<void> {
727
- const auth = requireAuth(request, reply);
728
- if (!auth.authenticated) return;
729
-
730
- if (!_a2aKeyStore) {
731
- reply.status(501).send({ error: "Key store not configured" });
732
- return;
733
- }
734
-
735
- await _a2aKeyStore.delete(request.params.id);
736
- await refreshStoreKeyMap();
737
-
738
- reply.status(200).send({ success: true });
739
- }
740
-
741
- async function handleApiKeyDisable(
742
- request: FastifyRequest<{ Params: { id: string } }>,
743
- reply: FastifyReply,
744
- ): Promise<void> {
745
- const auth = requireAuth(request, reply);
746
- if (!auth.authenticated) return;
747
-
748
- if (!_a2aKeyStore) {
749
- reply.status(501).send({ error: "Key store not configured" });
750
- return;
751
- }
752
-
753
- await _a2aKeyStore.disable(request.params.id);
754
- await refreshStoreKeyMap();
755
-
756
- reply.status(200).send({ success: true });
757
- }
758
-
759
- async function handleApiKeyEnable(
760
- request: FastifyRequest<{ Params: { id: string } }>,
761
- reply: FastifyReply,
762
- ): Promise<void> {
763
- const auth = requireAuth(request, reply);
764
- if (!auth.authenticated) return;
765
-
766
- if (!_a2aKeyStore) {
767
- reply.status(501).send({ error: "Key store not configured" });
768
- return;
769
- }
770
-
771
- await _a2aKeyStore.enable(request.params.id);
772
- await refreshStoreKeyMap();
773
-
774
- reply.status(200).send({ success: true });
775
- }
776
-
777
- async function handleApiKeyRotate(
778
- request: FastifyRequest<{ Params: { id: string } }>,
779
- reply: FastifyReply,
780
- ): Promise<void> {
781
- const auth = requireAuth(request, reply);
782
- if (!auth.authenticated) return;
783
-
784
- if (!_a2aKeyStore) {
785
- reply.status(501).send({ error: "Key store not configured" });
786
- return;
787
- }
788
-
789
- const record = await _a2aKeyStore.rotate(request.params.id);
790
- await refreshStoreKeyMap();
791
-
792
- reply.status(200).send({ success: true, data: record });
793
- }
794
-
795
- // ─── Route Registration ───────────────────────────────────────────────────
796
-
797
- export function registerA2ARoutes(app: FastifyInstance): void {
798
- app.get("/api/a2a/.well-known/agent.json", handleAgentCard);
799
- app.get("/api/a2a/.well-known/agent-card.json", handleAgentCard);
800
-
801
- app.post<{ Body: A2ATaskSendRequest }>(
802
- "/api/a2a/tasks/send",
803
- handleTasksSend,
804
- );
805
-
806
- app.get<{ Params: { taskId: string } }>(
807
- "/api/a2a/tasks/:taskId",
808
- handleTasksGet,
809
- );
810
-
811
- app.get<{ Params: { taskId: string } }>(
812
- "/api/a2a/tasks/:taskId/stream",
813
- handleTasksStream,
814
- );
815
-
816
- app.post<{ Params: { taskId: string } }>(
817
- "/api/a2a/tasks/:taskId/cancel",
818
- handleTasksCancel,
819
- );
820
-
821
- // A2A API Key management
822
- app.get<{ Querystring: { tenantId?: string; limit?: string; offset?: string } }>(
823
- "/api/a2a/keys",
824
- handleApiKeyList,
825
- );
826
-
827
- app.post<{ Body: Partial<CreateA2AApiKeyInput> }>(
828
- "/api/a2a/keys",
829
- handleApiKeyCreate,
830
- );
831
-
832
- app.delete<{ Params: { id: string } }>(
833
- "/api/a2a/keys/:id",
834
- handleApiKeyDelete,
835
- );
836
-
837
- app.post<{ Params: { id: string } }>(
838
- "/api/a2a/keys/:id/disable",
839
- handleApiKeyDisable,
840
- );
841
-
842
- app.post<{ Params: { id: string } }>(
843
- "/api/a2a/keys/:id/enable",
844
- handleApiKeyEnable,
845
- );
846
-
847
- app.post<{ Params: { id: string } }>(
848
- "/api/a2a/keys/:id/rotate",
849
- handleApiKeyRotate,
850
- );
851
- }