@axiom-lattice/gateway 3.0.9 → 4.0.0

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.
@@ -0,0 +1,1376 @@
1
+ import {
2
+ saveProjectFile
3
+ } from "./chunk-IDOEIBCF.mjs";
4
+ import {
5
+ A2AAuthError
6
+ } from "./chunk-2HOCF46L.mjs";
7
+
8
+ // src/routes/a2a-standard.ts
9
+ import { v4 as uuidv42 } from "uuid";
10
+ import {
11
+ A2AError,
12
+ DefaultExecutionEventBus as DefaultExecutionEventBus2,
13
+ DefaultRequestHandler,
14
+ JsonRpcTransportHandler,
15
+ RequestContext
16
+ } from "@a2a-js/sdk/server";
17
+ import { createTaskLifecycleService, getStoreLattice } from "@axiom-lattice/core";
18
+
19
+ // src/services/a2a/A2AAgentCardBuilder.ts
20
+ function buildAgentCardForAssistant(params) {
21
+ const { assistant, exposure, baseUrl } = params;
22
+ const jsonRpcUrl = `${baseUrl}/api/a2a/agents/${assistant.id}/jsonrpc`;
23
+ const skills = (exposure?.skills ?? []).map((s) => ({
24
+ id: s.id,
25
+ name: s.name,
26
+ description: s.description,
27
+ tags: s.tags ?? [],
28
+ examples: s.examples ?? []
29
+ }));
30
+ return {
31
+ name: assistant.name,
32
+ description: assistant.description ?? "",
33
+ url: jsonRpcUrl,
34
+ protocolVersion: "0.3.0",
35
+ version: "1.0.0",
36
+ defaultInputModes: exposure?.inputModes ?? ["text"],
37
+ defaultOutputModes: exposure?.outputModes ?? ["text"],
38
+ capabilities: {
39
+ streaming: true,
40
+ pushNotifications: false,
41
+ stateTransitionHistory: true
42
+ },
43
+ skills,
44
+ additionalInterfaces: [{ transport: "JSONRPC", url: jsonRpcUrl }],
45
+ securitySchemes: {
46
+ bearer: {
47
+ type: "http",
48
+ scheme: "Bearer",
49
+ description: "A2A API key sent as an Authorization Bearer token"
50
+ },
51
+ apiKeyHeader: {
52
+ type: "apiKey",
53
+ in: "header",
54
+ name: "x-api-key",
55
+ description: "A2A API key sent via the x-api-key header"
56
+ }
57
+ },
58
+ security: [{ bearer: [] }, { apiKeyHeader: [] }]
59
+ };
60
+ }
61
+
62
+ // src/services/a2a/A2ATaskStoreAdapter.ts
63
+ import { createHash } from "crypto";
64
+ import { isDeepStrictEqual } from "util";
65
+ var TASK_STATUS_TO_A2A_STATE = {
66
+ pending: "submitted",
67
+ in_progress: "working",
68
+ review: "input-required",
69
+ interrupted: "input-required",
70
+ completed: "completed",
71
+ failed: "failed",
72
+ cancelled: "canceled"
73
+ };
74
+ var TITLE_MAX_LENGTH = 80;
75
+ var A2ATaskScopeConflictError = class extends Error {
76
+ constructor(taskId) {
77
+ super(`A2A task ${taskId} belongs to another scope`);
78
+ this.code = "A2A_TASK_SCOPE_CONFLICT";
79
+ this.name = "A2ATaskScopeConflictError";
80
+ this.taskId = taskId;
81
+ }
82
+ };
83
+ function mapTaskStatusToA2AState(status) {
84
+ return TASK_STATUS_TO_A2A_STATE[status];
85
+ }
86
+ function asString(value) {
87
+ return typeof value === "string" ? value : void 0;
88
+ }
89
+ function asStringArray(value) {
90
+ if (!Array.isArray(value)) return void 0;
91
+ const strings = value.filter((item) => typeof item === "string");
92
+ return strings.length > 0 ? strings : void 0;
93
+ }
94
+ function extractText(parts) {
95
+ return parts.filter((part) => part.kind === "text").map((part) => part.text).join("\n");
96
+ }
97
+ function truncate(text, maxLength) {
98
+ if (text.length <= maxLength) return text;
99
+ return `${text.slice(0, maxLength)}\u2026`;
100
+ }
101
+ function extractTitleAndDescription(task) {
102
+ const firstUserMessage = task.history?.find((message) => message.role === "user");
103
+ const text = firstUserMessage ? extractText(firstUserMessage.parts) : "";
104
+ return {
105
+ title: truncate(text, TITLE_MAX_LENGTH),
106
+ description: text ? [
107
+ "## Objective",
108
+ "",
109
+ text,
110
+ "",
111
+ "## Acceptance Criteria",
112
+ "",
113
+ "Complete the requested work and return the result through A2A."
114
+ ].join("\n") : ""
115
+ };
116
+ }
117
+ function isFileRef(value) {
118
+ return typeof value === "object" && value !== null && typeof value.uri === "string";
119
+ }
120
+ function artifactFileRefs(artifacts) {
121
+ const refs = [];
122
+ for (const artifact of artifacts ?? []) {
123
+ for (const part of artifact.parts) {
124
+ if (part.kind !== "file") continue;
125
+ const file = part.file;
126
+ if (!("uri" in file)) continue;
127
+ refs.push({
128
+ uri: file.uri,
129
+ ...file.name ? { name: file.name } : {},
130
+ ...file.mimeType ? { mimeType: file.mimeType } : {},
131
+ addedBy: "agent"
132
+ });
133
+ }
134
+ }
135
+ return refs;
136
+ }
137
+ function extractFiles(task) {
138
+ const inputFiles = task.metadata?.inputFiles;
139
+ const inputRefs = Array.isArray(inputFiles) ? inputFiles.filter(isFileRef) : [];
140
+ return [...inputRefs, ...artifactFileRefs(task.artifacts)];
141
+ }
142
+ function extractArtifactText(artifacts) {
143
+ return (artifacts ?? []).map((artifact) => extractText(artifact.parts)).filter((text) => text.length > 0).join("\n");
144
+ }
145
+ function extractFinalTurnAgentText(history) {
146
+ const messages = history ?? [];
147
+ let lastUserIndex = -1;
148
+ for (let i = 0; i < messages.length; i += 1) {
149
+ if (messages[i].role === "user") lastUserIndex = i;
150
+ }
151
+ return messages.slice(lastUserIndex + 1).filter((message) => message.role === "agent").map((message) => extractText(message.parts)).filter((text) => text.length > 0).join("");
152
+ }
153
+ function isFailureState(state) {
154
+ return state === "failed" || state === "rejected";
155
+ }
156
+ function extractFailureReason(task) {
157
+ if (!isFailureState(task.status.state) || !task.status.message) return void 0;
158
+ return extractText(task.status.message.parts);
159
+ }
160
+ function observationEventKey(task, result, failureReason) {
161
+ const digest = createHash("sha256").update(JSON.stringify({
162
+ state: task.status.state,
163
+ timestamp: task.status.timestamp,
164
+ message: task.status.message,
165
+ result,
166
+ failureReason
167
+ })).digest("hex");
168
+ return `a2a-status-observation:${task.id}:${digest}`;
169
+ }
170
+ function snapshotEventKey(task) {
171
+ const digest = createHash("sha256").update(JSON.stringify(task)).digest("hex");
172
+ return `a2a-task-snapshot:${task.id}:${digest}`;
173
+ }
174
+ function isGovernedObservation(state) {
175
+ return state === "completed" || state === "failed" || state === "rejected" || state === "input-required" || state === "canceled";
176
+ }
177
+ function isA2ATaskSnapshot(value) {
178
+ return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.status === "object" && value.status !== null;
179
+ }
180
+ function snapshotSequence(value) {
181
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
182
+ }
183
+ var A2ATaskStoreAdapter = class {
184
+ constructor(options) {
185
+ this.snapshotSequences = /* @__PURE__ */ new Map();
186
+ this.taskStore = options.taskStore;
187
+ this.workItemStore = options.workItemStore;
188
+ this.lifecycle = options.lifecycle;
189
+ this.scope = options.scope;
190
+ }
191
+ /**
192
+ * Persist an SDK task by projecting it onto a `TaskItem`, creating it when
193
+ * the id is unseen or updating it otherwise. A full snapshot of the SDK task
194
+ * is stored under `context.a2aTask` so `load` can restore it.
195
+ */
196
+ async save(task) {
197
+ const { tenantId } = this.scope;
198
+ const { title, description } = extractTitleAndDescription(task);
199
+ const files = extractFiles(task);
200
+ const result = task.status.state === "completed" ? extractArtifactText(task.artifacts) || extractFinalTurnAgentText(task.history) : void 0;
201
+ const failureReason = extractFailureReason(task);
202
+ const context = {
203
+ a2aContextId: task.contextId,
204
+ threadId: asString(task.metadata?.threadId),
205
+ a2aTask: task
206
+ };
207
+ let existing = await this.taskStore.getById(tenantId, task.id);
208
+ if (existing) {
209
+ this.requireScope(existing, task.id);
210
+ await this.recordSnapshot(task, existing);
211
+ } else {
212
+ try {
213
+ existing = await this.taskStore.create({
214
+ id: task.id,
215
+ tenantId,
216
+ ownerType: "agent",
217
+ ownerId: this.scope.assistantId,
218
+ projectId: this.scope.projectId,
219
+ title,
220
+ description,
221
+ sourceId: "a2a",
222
+ status: "pending",
223
+ priority: "medium",
224
+ dependencies: asStringArray(task.metadata?.dependencies),
225
+ files,
226
+ context
227
+ });
228
+ } catch (error) {
229
+ existing = await this.taskStore.getById(tenantId, task.id);
230
+ if (!existing) throw error;
231
+ this.requireScope(existing, task.id);
232
+ }
233
+ await this.recordSnapshot(task, existing);
234
+ }
235
+ const actor = `a2a:${this.scope.assistantId}`;
236
+ const threadId = asString(task.metadata?.threadId);
237
+ if (task.status.state === "working") {
238
+ if (existing.status === "pending") {
239
+ await this.requireLifecycleSuccess("start", task.id, await this.lifecycle.startTask({
240
+ tenantId,
241
+ taskId: task.id,
242
+ actor,
243
+ ...threadId ? { threadId } : {}
244
+ }));
245
+ }
246
+ return;
247
+ }
248
+ if (!isGovernedObservation(task.status.state)) return;
249
+ await this.recordObservation(task, existing, result, failureReason);
250
+ if (["completed", "failed", "cancelled"].includes(existing.status)) return;
251
+ if (task.status.state === "input-required" && this.isExactInterruptionReplay(task, existing)) return;
252
+ if (existing.status === "pending") {
253
+ const started = await this.requireLifecycleSuccess("start", task.id, await this.lifecycle.startTask({
254
+ tenantId,
255
+ taskId: task.id,
256
+ actor,
257
+ ...threadId ? { threadId } : {}
258
+ }));
259
+ existing = started.task;
260
+ }
261
+ await this.applyObservation(task, failureReason);
262
+ }
263
+ async recordSnapshot(task, item) {
264
+ const incomingSequence = snapshotSequence(task.metadata?.snapshotSequence) ?? 0;
265
+ const currentSequence = this.snapshotSequences.get(task.id) ?? 0;
266
+ const sequence = Math.max(incomingSequence, currentSequence) + 1;
267
+ this.snapshotSequences.set(task.id, sequence);
268
+ const sequencedTask = {
269
+ ...task,
270
+ metadata: { ...task.metadata ?? {}, snapshotSequence: sequence }
271
+ };
272
+ const eventKey = snapshotEventKey(sequencedTask);
273
+ await this.workItemStore.createIfAbsentByEventKey({
274
+ tenantId: this.scope.tenantId,
275
+ taskId: task.id,
276
+ projectId: this.scope.projectId,
277
+ action: "activity",
278
+ actor: `a2a:${this.scope.assistantId}`,
279
+ summary: "A2A task snapshot",
280
+ detail: { type: "a2a_task_snapshot", snapshotSequence: sequence, eventKey, task: sequencedTask },
281
+ eventKey,
282
+ ...asString(task.metadata?.threadId) ? { threadId: asString(task.metadata?.threadId) } : {},
283
+ ...item.workspaceId ? { workspaceId: item.workspaceId } : {}
284
+ });
285
+ }
286
+ async recordObservation(task, item, result, failureReason) {
287
+ await this.workItemStore.createIfAbsentByEventKey({
288
+ tenantId: this.scope.tenantId,
289
+ taskId: task.id,
290
+ projectId: this.scope.projectId,
291
+ action: "activity",
292
+ actor: `a2a:${this.scope.assistantId}`,
293
+ summary: `A2A observed ${task.status.state}`,
294
+ detail: {
295
+ type: "a2a_status_observation",
296
+ state: task.status.state,
297
+ ...result === void 0 ? {} : { result },
298
+ ...failureReason === void 0 ? {} : { failureReason },
299
+ ...task.status.message ? { message: extractText(task.status.message.parts) } : {}
300
+ },
301
+ eventKey: observationEventKey(task, result, failureReason),
302
+ ...asString(task.metadata?.threadId) ? { threadId: asString(task.metadata?.threadId) } : {},
303
+ ...item.workspaceId ? { workspaceId: item.workspaceId } : {}
304
+ });
305
+ }
306
+ async applyObservation(task, failureReason) {
307
+ const actor = `a2a:${this.scope.assistantId}`;
308
+ const threadId = asString(task.metadata?.threadId);
309
+ if (task.status.state === "canceled") {
310
+ await this.requireLifecycleSuccess("cancel", task.id, await this.lifecycle.cancelTask({
311
+ tenantId: this.scope.tenantId,
312
+ taskId: task.id,
313
+ actor,
314
+ summary: "A2A observed canceled",
315
+ ...threadId ? { threadId } : {}
316
+ }));
317
+ return;
318
+ }
319
+ if (isFailureState(task.status.state)) {
320
+ await this.requireLifecycleSuccess("fail", task.id, await this.lifecycle.failTask({
321
+ tenantId: this.scope.tenantId,
322
+ taskId: task.id,
323
+ failureReason: failureReason?.trim() || `A2A observed ${task.status.state}`,
324
+ actor,
325
+ ...threadId ? { threadId } : {}
326
+ }));
327
+ return;
328
+ }
329
+ if (task.status.state === "input-required") {
330
+ await this.requireLifecycleSuccess("interrupt", task.id, await this.lifecycle.interruptTask({
331
+ tenantId: this.scope.tenantId,
332
+ taskId: task.id,
333
+ type: "missing_input",
334
+ summary: task.status.message ? extractText(task.status.message.parts).trim() || "A2A requires input" : "A2A requires input",
335
+ actor,
336
+ ...threadId ? { threadId } : {}
337
+ }));
338
+ return;
339
+ }
340
+ const canonical = await this.taskStore.getById(this.scope.tenantId, task.id);
341
+ if (!canonical || !this.isInScope(canonical)) {
342
+ throw new Error(`A2A completed observation cannot reconcile missing task ${task.id}`);
343
+ }
344
+ if (canonical.status === "completed") return;
345
+ throw new Error(
346
+ `A2A completed observation conflicts with canonical ${canonical.status} task ${task.id}`
347
+ );
348
+ }
349
+ async requireLifecycleSuccess(operation, taskId, result) {
350
+ if (result.success) {
351
+ if (result.warnings?.length) {
352
+ console.warn({ event: "a2a:lifecycle:warnings", operation, taskId, warnings: result.warnings });
353
+ }
354
+ return result;
355
+ }
356
+ throw new Error(`A2A lifecycle ${operation} failed: ${result.code}: ${result.error}`);
357
+ }
358
+ isExactInterruptionReplay(task, item) {
359
+ const observed = task.metadata?.interruption;
360
+ const canonical = item.context?.interruption;
361
+ if (item.status !== "interrupted" || !canonical || typeof canonical !== "object") return false;
362
+ if (observed && typeof observed === "object") return isDeepStrictEqual(observed, canonical);
363
+ const observedSummary = task.status.message ? extractText(task.status.message.parts).trim() : "";
364
+ return observedSummary.length > 0 && canonical.summary === observedSummary;
365
+ }
366
+ /**
367
+ * Restore an SDK task from the stored snapshot, overlaying the current
368
+ * `TaskItem.status` onto `task.status.state`. Returns `undefined` when the
369
+ * task is unknown to the tenant or has no A2A snapshot.
370
+ */
371
+ async load(taskId) {
372
+ const item = await this.taskStore.getById(this.scope.tenantId, taskId);
373
+ if (!item || !this.isInScope(item)) return void 0;
374
+ const pageSize = 100;
375
+ let selected;
376
+ for (let offset = 0; ; offset += pageSize) {
377
+ const activities = await this.workItemStore.list({
378
+ tenantId: this.scope.tenantId,
379
+ taskId,
380
+ action: "activity",
381
+ order: "desc",
382
+ limit: pageSize,
383
+ offset
384
+ });
385
+ for (const activity of activities) {
386
+ if (activity.detail?.type !== "a2a_task_snapshot" || !isA2ATaskSnapshot(activity.detail.task)) continue;
387
+ const sequence = snapshotSequence(activity.detail.snapshotSequence) ?? snapshotSequence(activity.detail.task.metadata?.snapshotSequence);
388
+ const candidate = {
389
+ task: activity.detail.task,
390
+ sequence,
391
+ eventKey: asString(activity.detail.eventKey) ?? activity.eventKey ?? snapshotEventKey(activity.detail.task),
392
+ statusTimestamp: Date.parse(activity.detail.task.status.timestamp ?? ""),
393
+ createdAt: activity.createdAt,
394
+ id: activity.id
395
+ };
396
+ if (!selected || this.isLaterSnapshot(candidate, selected)) selected = candidate;
397
+ }
398
+ if (activities.length < pageSize) break;
399
+ }
400
+ const snapshot = selected?.task ?? item.context?.a2aTask;
401
+ if (!isA2ATaskSnapshot(snapshot)) return void 0;
402
+ const restoredSequence = selected?.sequence ?? snapshotSequence(snapshot.metadata?.snapshotSequence);
403
+ if (restoredSequence !== void 0) {
404
+ this.snapshotSequences.set(taskId, Math.max(this.snapshotSequences.get(taskId) ?? 0, restoredSequence));
405
+ }
406
+ const state = mapTaskStatusToA2AState(item.status);
407
+ const interruption = item.context?.interruption;
408
+ const metadata = {
409
+ ...snapshot.metadata ?? {},
410
+ ...interruption && typeof interruption === "object" ? { interruption } : {}
411
+ };
412
+ const nonResultArtifacts = (snapshot.artifacts ?? []).filter((artifact) => artifact.name !== "result");
413
+ const artifacts = item.status === "completed" && item.result?.trim() ? [...nonResultArtifacts, {
414
+ artifactId: `${item.id}:result`,
415
+ name: "result",
416
+ parts: [{ kind: "text", text: item.result }]
417
+ }] : nonResultArtifacts;
418
+ const message = state === "failed" && item.failureReason?.trim() ? {
419
+ kind: "message",
420
+ messageId: `${item.id}:failure`,
421
+ role: "agent",
422
+ parts: [{ kind: "text", text: item.failureReason }]
423
+ } : interruption && typeof interruption === "object" && typeof interruption.summary === "string" ? {
424
+ kind: "message",
425
+ messageId: `${item.id}:interruption`,
426
+ role: "agent",
427
+ parts: [{ kind: "text", text: interruption.summary }]
428
+ } : snapshot.status.message;
429
+ return {
430
+ ...snapshot,
431
+ metadata,
432
+ ...artifacts.length > 0 ? { artifacts } : { artifacts: void 0 },
433
+ status: {
434
+ ...snapshot.status,
435
+ state,
436
+ ...message ? { message } : {}
437
+ }
438
+ };
439
+ }
440
+ isInScope(item) {
441
+ return item.tenantId === this.scope.tenantId && item.projectId === this.scope.projectId && item.ownerType === "agent" && item.ownerId === this.scope.assistantId && item.sourceId === "a2a";
442
+ }
443
+ requireScope(item, taskId) {
444
+ if (!this.isInScope(item)) throw new A2ATaskScopeConflictError(taskId);
445
+ }
446
+ isLaterSnapshot(candidate, selected) {
447
+ if (candidate.sequence !== void 0 || selected.sequence !== void 0) {
448
+ if (candidate.sequence === void 0) return false;
449
+ if (selected.sequence === void 0) return true;
450
+ if (candidate.sequence !== selected.sequence) return candidate.sequence > selected.sequence;
451
+ const candidateTimestamp = Number.isNaN(candidate.statusTimestamp) ? Number.NEGATIVE_INFINITY : candidate.statusTimestamp;
452
+ const selectedTimestamp = Number.isNaN(selected.statusTimestamp) ? Number.NEGATIVE_INFINITY : selected.statusTimestamp;
453
+ if (candidateTimestamp !== selectedTimestamp) return candidateTimestamp > selectedTimestamp;
454
+ return candidate.eventKey > selected.eventKey;
455
+ }
456
+ return candidate.createdAt.getTime() > selected.createdAt.getTime() || candidate.createdAt.getTime() === selected.createdAt.getTime() && candidate.id > selected.id;
457
+ }
458
+ };
459
+
460
+ // src/services/a2a/AxiomAgentExecutor.ts
461
+ import { v4 as uuidv4 } from "uuid";
462
+ import { agentInstanceManager } from "@axiom-lattice/core";
463
+ import { MessageChunkTypes } from "@axiom-lattice/protocols";
464
+
465
+ // src/services/a2a/a2aFileRefs.ts
466
+ function filePartsToRefs(parts, addedBy) {
467
+ const refs = [];
468
+ for (const part of parts) {
469
+ if (part.kind !== "file") continue;
470
+ const file = part.file;
471
+ if ("bytes" in file) {
472
+ throw new Error(
473
+ "A2A file parts with inline bytes are not supported by this pure function \u2014 bytes ingest requires filePartsToRefsWithIngest with an A2AFileIngestor; provide a file uri otherwise"
474
+ );
475
+ }
476
+ refs.push({
477
+ uri: file.uri,
478
+ ...file.name ? { name: file.name } : {},
479
+ ...file.mimeType ? { mimeType: file.mimeType } : {},
480
+ addedBy
481
+ });
482
+ }
483
+ return refs;
484
+ }
485
+ function formatFileRefsSection(refs) {
486
+ const lines = refs.map((ref) => {
487
+ const label = ref.name ?? ref.uri;
488
+ const mime = ref.mimeType ? ` (${ref.mimeType})` : "";
489
+ return `- ${label}${mime}: ${ref.uri}`;
490
+ });
491
+ return ["[\u9644\u4EF6]", ...lines].join("\n");
492
+ }
493
+ async function filePartsToRefsWithIngest(parts, addedBy, ingestor, dedupeKeyBase, projectId) {
494
+ if (!ingestor) {
495
+ return filePartsToRefs(parts, addedBy);
496
+ }
497
+ const refs = [];
498
+ for (let index = 0; index < parts.length; index += 1) {
499
+ const part = parts[index];
500
+ if (part.kind !== "file") continue;
501
+ const dedupeKey = `${dedupeKeyBase}:${index}`;
502
+ refs.push(await ingestor.ingest(part, dedupeKey, projectId, addedBy));
503
+ }
504
+ return refs;
505
+ }
506
+
507
+ // src/services/a2a/AxiomAgentExecutor.ts
508
+ function isoNow() {
509
+ return (/* @__PURE__ */ new Date()).toISOString();
510
+ }
511
+ function asString2(value) {
512
+ return typeof value === "string" ? value : void 0;
513
+ }
514
+ function extractText2(parts) {
515
+ return parts.filter((part) => part.kind === "text").map((part) => part.text).join("\n");
516
+ }
517
+ function agentMessage(text) {
518
+ return {
519
+ kind: "message",
520
+ messageId: uuidv4(),
521
+ role: "agent",
522
+ parts: [{ kind: "text", text }]
523
+ };
524
+ }
525
+ var _AxiomAgentExecutor = class _AxiomAgentExecutor {
526
+ constructor(options) {
527
+ this.scope = options.scope;
528
+ this.assistant = options.assistant;
529
+ this.taskStore = options.taskStore;
530
+ this.ingestor = options.ingestor;
531
+ }
532
+ /**
533
+ * Execute the incoming message on the bound internal agent and publish the
534
+ * resulting task/status/artifact events onto the bus.
535
+ */
536
+ async execute(ctx, bus) {
537
+ const execution = { canceled: false };
538
+ const taskKey = this.taskKey(ctx.taskId);
539
+ const active = _AxiomAgentExecutor.executions.get(taskKey) ?? /* @__PURE__ */ new Set();
540
+ active.add(execution);
541
+ _AxiomAgentExecutor.executions.set(taskKey, active);
542
+ const text = extractText2(ctx.userMessage.parts);
543
+ const existing = ctx.task;
544
+ const threadId = asString2(existing?.metadata?.threadId) ?? uuidv4();
545
+ const dependencies = ctx.referenceTasks?.map((task) => task.id) ?? [];
546
+ const createTask = (inputFiles) => ({
547
+ kind: "task",
548
+ id: ctx.taskId,
549
+ contextId: ctx.contextId,
550
+ ...existing?.history ? { history: existing.history } : {},
551
+ ...existing?.artifacts ? { artifacts: existing.artifacts } : {},
552
+ status: { state: "working", timestamp: isoNow() },
553
+ metadata: { threadId, dependencies, inputFiles }
554
+ });
555
+ const publishFailed = (err) => {
556
+ bus.publish({
557
+ kind: "status-update",
558
+ taskId: ctx.taskId,
559
+ contextId: ctx.contextId,
560
+ status: {
561
+ state: "failed",
562
+ message: agentMessage(err instanceof Error ? err.message : String(err)),
563
+ timestamp: isoNow()
564
+ },
565
+ final: true
566
+ });
567
+ };
568
+ try {
569
+ try {
570
+ if (!this.taskStore) {
571
+ throw new Error("A2A governed execution requires a canonical task store");
572
+ }
573
+ this.assertGovernedTaskCapability();
574
+ } catch (err) {
575
+ const initialTask2 = createTask([]);
576
+ if (this.taskStore) await this.taskStore.save(initialTask2);
577
+ bus.publish(initialTask2);
578
+ publishFailed(err);
579
+ return;
580
+ }
581
+ const interruption = existing?.metadata?.interruption;
582
+ const reviewInterruption = interruption && typeof interruption === "object" && interruption.type === "review_required";
583
+ const reviewCommand = reviewInterruption ? this.reviewCommand(text) : void 0;
584
+ if (reviewInterruption && !reviewCommand) {
585
+ bus.publish(agentMessage("Review response must be exactly 'approve', 'reject', or 'reject: <note>'."));
586
+ return;
587
+ }
588
+ let inputFiles;
589
+ try {
590
+ inputFiles = await filePartsToRefsWithIngest(
591
+ ctx.userMessage.parts,
592
+ "user",
593
+ this.ingestor,
594
+ ctx.taskId,
595
+ this.scope.projectId
596
+ );
597
+ } catch (err) {
598
+ const initialTask2 = createTask([]);
599
+ if (this.taskStore) await this.taskStore.save(initialTask2);
600
+ bus.publish(initialTask2);
601
+ publishFailed(err);
602
+ return;
603
+ }
604
+ const initialTask = createTask(inputFiles);
605
+ await this.taskStore.save(initialTask);
606
+ bus.publish(initialTask);
607
+ try {
608
+ const agent = agentInstanceManager.getAgent({
609
+ assistant_id: this.scope.assistantId,
610
+ thread_id: threadId,
611
+ tenant_id: this.scope.tenantId,
612
+ project_id: this.scope.projectId
613
+ });
614
+ const requestText = inputFiles.length > 0 ? `${text}
615
+
616
+ ${formatFileRefsSection(inputFiles)}` : text;
617
+ const messageText = [
618
+ `A2A governed task instruction: get and update the existing TaskItem ${ctx.taskId}; do not create a replacement task.`,
619
+ "Before doing the requested work, initialize its canonical description through manage_task with ## Objective, ## Acceptance Criteria, and ## Belief State sections.",
620
+ "Keep lifecycle status truthful. Complete only through manage_task update with a nonblank result and structured beliefImpact; use interrupted, failed, or cancelled when appropriate.",
621
+ "",
622
+ requestText
623
+ ].join("\n");
624
+ const continuationCommand = interruption && typeof interruption === "object" ? { resume: text } : void 0;
625
+ const { messageId } = await agent.addMessage({
626
+ input: { message: messageText },
627
+ ...reviewCommand || continuationCommand ? { command: reviewCommand ?? continuationCommand } : {},
628
+ custom_run_config: { taskId: ctx.taskId }
629
+ });
630
+ const stream = agent.chunkStream(messageId, [
631
+ MessageChunkTypes.MESSAGE_COMPLETED
632
+ ]);
633
+ for await (const chunk of stream) {
634
+ if (execution.canceled) return;
635
+ const chunkType = chunk.type;
636
+ const chunkText = chunk.data?.content ?? "";
637
+ if (chunkType === MessageChunkTypes.INTERRUPT) {
638
+ break;
639
+ }
640
+ if (chunkType === MessageChunkTypes.AI || chunkType === MessageChunkTypes.TOOL) {
641
+ if (chunkText) {
642
+ bus.publish({
643
+ kind: "status-update",
644
+ taskId: ctx.taskId,
645
+ contextId: ctx.contextId,
646
+ status: {
647
+ state: "working",
648
+ message: agentMessage(chunkText),
649
+ timestamp: isoNow()
650
+ },
651
+ final: false
652
+ });
653
+ }
654
+ }
655
+ }
656
+ if (execution.canceled) return;
657
+ const canonical = await this.taskStore.load(ctx.taskId);
658
+ const canonicalState = canonical?.status.state;
659
+ if (canonicalState === "completed") {
660
+ const resultArtifact = canonical?.artifacts?.find((artifact) => artifact.name === "result");
661
+ if (!resultArtifact) {
662
+ throw new Error(`Canonical completed task ${ctx.taskId} has no result artifact`);
663
+ }
664
+ if (execution.canceled) return;
665
+ bus.publish({
666
+ kind: "artifact-update",
667
+ taskId: ctx.taskId,
668
+ contextId: ctx.contextId,
669
+ artifact: resultArtifact
670
+ });
671
+ }
672
+ if (canonicalState === "completed" || canonicalState === "failed" || canonicalState === "canceled") {
673
+ if (execution.canceled) return;
674
+ bus.publish({
675
+ kind: "status-update",
676
+ taskId: ctx.taskId,
677
+ contextId: ctx.contextId,
678
+ status: {
679
+ state: canonicalState,
680
+ ...canonical?.status.message ? { message: canonical.status.message } : {},
681
+ timestamp: isoNow()
682
+ },
683
+ final: true
684
+ });
685
+ return;
686
+ }
687
+ if (canonicalState === "input-required") {
688
+ bus.publish({
689
+ kind: "status-update",
690
+ taskId: ctx.taskId,
691
+ contextId: ctx.contextId,
692
+ status: {
693
+ state: "input-required",
694
+ ...canonical?.status.message ? { message: canonical.status.message } : {},
695
+ timestamp: isoNow()
696
+ },
697
+ final: true
698
+ });
699
+ return;
700
+ }
701
+ bus.publish({
702
+ kind: "status-update",
703
+ taskId: ctx.taskId,
704
+ contextId: ctx.contextId,
705
+ status: {
706
+ state: "failed",
707
+ message: agentMessage("Agent did not complete governed task lifecycle"),
708
+ timestamp: isoNow()
709
+ },
710
+ final: true
711
+ });
712
+ } catch (err) {
713
+ publishFailed(err);
714
+ }
715
+ } finally {
716
+ active.delete(execution);
717
+ if (active.size === 0) _AxiomAgentExecutor.executions.delete(taskKey);
718
+ }
719
+ }
720
+ reviewCommand(text) {
721
+ const trimmed = text.trim();
722
+ const normalized = trimmed.toLowerCase();
723
+ if (normalized === "approve") return { resume: { action: "approve" } };
724
+ if (normalized === "reject") {
725
+ return { resume: { action: "reject", data: { note: "Changes requested." } } };
726
+ }
727
+ const prefixedNote = /^reject:\s*(.+)$/is.exec(trimmed)?.[1]?.trim();
728
+ if (prefixedNote) {
729
+ return { resume: { action: "reject", data: { note: prefixedNote } } };
730
+ }
731
+ return void 0;
732
+ }
733
+ taskKey(taskId) {
734
+ return `${this.scope.tenantId}:${this.scope.projectId}:${this.scope.assistantId}:${taskId}`;
735
+ }
736
+ assertGovernedTaskCapability() {
737
+ const graph = this.assistant.graphDefinition;
738
+ const middleware = graph && typeof graph === "object" ? graph.middleware : void 0;
739
+ const taskMiddleware = Array.isArray(middleware) ? middleware.find((entry) => !!entry && typeof entry === "object" && entry.type === "task" && entry.enabled === true) : void 0;
740
+ const allowedTools = taskMiddleware?.allowedTools;
741
+ const exposesManageTask = taskMiddleware && (allowedTools === void 0 || Array.isArray(allowedTools) && (allowedTools.length === 0 || allowedTools.includes("manage_task")));
742
+ if (!exposesManageTask) {
743
+ throw new Error("A2A execution requires enabled TaskMiddleware exposing manage_task");
744
+ }
745
+ }
746
+ /**
747
+ * Cancel a running task: resolve its thread id from the task store, abort the
748
+ * underlying agent, and publish the terminal `canceled` event.
749
+ */
750
+ async cancelTask(taskId, bus) {
751
+ for (const execution of _AxiomAgentExecutor.executions.get(this.taskKey(taskId)) ?? []) {
752
+ execution.canceled = true;
753
+ }
754
+ const task = this.taskStore ? await this.taskStore.load(taskId) : void 0;
755
+ const threadId = task ? asString2(task.metadata?.threadId) : void 0;
756
+ if (threadId) {
757
+ try {
758
+ const agent = agentInstanceManager.getAgent({
759
+ assistant_id: this.scope.assistantId,
760
+ thread_id: threadId,
761
+ tenant_id: this.scope.tenantId,
762
+ project_id: this.scope.projectId
763
+ });
764
+ await agent.abort();
765
+ } catch (err) {
766
+ console.warn({
767
+ event: "a2a:cancel:no_agent",
768
+ taskId,
769
+ threadId,
770
+ error: err instanceof Error ? err.message : String(err)
771
+ });
772
+ }
773
+ }
774
+ bus.publish({
775
+ kind: "status-update",
776
+ taskId,
777
+ contextId: task?.contextId ?? "",
778
+ status: { state: "canceled", timestamp: isoNow() },
779
+ final: true
780
+ });
781
+ }
782
+ };
783
+ _AxiomAgentExecutor.executions = /* @__PURE__ */ new Map();
784
+ var AxiomAgentExecutor = _AxiomAgentExecutor;
785
+
786
+ // src/services/a2a/A2AFileIngestor.ts
787
+ var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
788
+ var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
789
+ ".exe",
790
+ ".sh",
791
+ ".bat",
792
+ ".cmd",
793
+ ".com",
794
+ ".msi",
795
+ ".js",
796
+ ".mjs",
797
+ ".cjs",
798
+ ".ps1",
799
+ ".vbs",
800
+ ".dll",
801
+ ".so",
802
+ ".dylib"
803
+ ]);
804
+ var EXECUTABLE_MIME_TYPES = /* @__PURE__ */ new Set([
805
+ "application/x-msdownload",
806
+ "application/x-msdos-program",
807
+ "application/x-executable",
808
+ "application/x-sh",
809
+ "application/x-bat",
810
+ "application/x-shellscript",
811
+ "text/x-shellscript",
812
+ "application/javascript",
813
+ "application/x-javascript",
814
+ "text/javascript"
815
+ ]);
816
+ function decodeBase64(value) {
817
+ return new Uint8Array(Buffer.from(value, "base64"));
818
+ }
819
+ function maxEncodedLengthFor(maxBytes) {
820
+ return Math.ceil(maxBytes * 4 / 3) + 4;
821
+ }
822
+ function sanitizePathSegment(segment, fallback) {
823
+ const cleaned = segment.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\.{2,}/g, ".").replace(/^[.-]+|[.-]+$/g, "");
824
+ return cleaned.length > 0 ? cleaned : fallback;
825
+ }
826
+ function storageLocationFor(dedupeKey, name) {
827
+ const separatorIndex = dedupeKey.lastIndexOf(":");
828
+ const keyBase = separatorIndex >= 0 ? dedupeKey.slice(0, separatorIndex) : dedupeKey;
829
+ const keyIndex = separatorIndex >= 0 ? dedupeKey.slice(separatorIndex + 1) : "0";
830
+ return {
831
+ path: `a2a/${sanitizePathSegment(keyBase, "task")}`,
832
+ name: `${sanitizePathSegment(keyIndex, "0")}-${sanitizePathSegment(name, "file")}`
833
+ };
834
+ }
835
+ function extensionOf(name) {
836
+ return name.includes(".") ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
837
+ }
838
+ function assertAllowedFileType(name, mimeType) {
839
+ if (EXECUTABLE_EXTENSIONS.has(extensionOf(name))) {
840
+ throw new Error(`Refusing to ingest executable file "${name}"`);
841
+ }
842
+ if (mimeType && EXECUTABLE_MIME_TYPES.has(mimeType.toLowerCase())) {
843
+ throw new Error(`Refusing to ingest executable file "${name}"`);
844
+ }
845
+ }
846
+ var A2AFileIngestor = class {
847
+ constructor(options) {
848
+ this.cache = /* @__PURE__ */ new Map();
849
+ this.saveProjectFile = options.saveProjectFile;
850
+ this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
851
+ }
852
+ /**
853
+ * Project a single file part onto a {@link TaskFileRef}, ingesting bytes when
854
+ * present. Idempotent per `dedupeKey`: a repeated key returns the cached URI
855
+ * without re-writing storage, and even across ingestor instances the key
856
+ * maps to a deterministic storage path (`a2a/<taskId>/<partIndex>-<name>`),
857
+ * so request retries overwrite the same object.
858
+ *
859
+ * NOTE: the key is based on `taskId` + part index rather than the internal
860
+ * message id because the executor must ingest files *before* `addMessage`
861
+ * returns a messageId. A client retrying with a different taskId for the
862
+ * same message writes a second copy; that is accepted and documented.
863
+ */
864
+ async ingest(part, dedupeKey, projectId, addedBy) {
865
+ const file = part.file;
866
+ if ("uri" in file) {
867
+ return {
868
+ uri: file.uri,
869
+ ...file.name ? { name: file.name } : {},
870
+ ...file.mimeType ? { mimeType: file.mimeType } : {},
871
+ addedBy
872
+ };
873
+ }
874
+ const name = file.name ?? "file";
875
+ assertAllowedFileType(name, file.mimeType);
876
+ if (file.bytes.length > maxEncodedLengthFor(this.maxBytes)) {
877
+ throw new Error(
878
+ `A2A file part exceeds the ${this.maxBytes} byte ingest limit`
879
+ );
880
+ }
881
+ const bytes = decodeBase64(file.bytes);
882
+ if (bytes.length > this.maxBytes) {
883
+ throw new Error(
884
+ `A2A file part exceeds the ${this.maxBytes} byte ingest limit`
885
+ );
886
+ }
887
+ const cached = this.cache.get(dedupeKey);
888
+ if (cached !== void 0) {
889
+ return {
890
+ uri: cached,
891
+ ...file.name ? { name: file.name } : {},
892
+ ...file.mimeType ? { mimeType: file.mimeType } : {},
893
+ addedBy
894
+ };
895
+ }
896
+ const location = storageLocationFor(dedupeKey, name);
897
+ const { uri } = await this.saveProjectFile({
898
+ projectId,
899
+ name: location.name,
900
+ path: location.path,
901
+ bytes,
902
+ ...file.mimeType ? { mimeType: file.mimeType } : {}
903
+ });
904
+ this.cache.set(dedupeKey, uri);
905
+ return {
906
+ uri,
907
+ ...file.name ? { name: file.name } : {},
908
+ ...file.mimeType ? { mimeType: file.mimeType } : {},
909
+ addedBy
910
+ };
911
+ }
912
+ };
913
+
914
+ // src/services/a2a/A2ARuntimeRegistry.ts
915
+ import {
916
+ DefaultExecutionEventBus
917
+ } from "@a2a-js/sdk/server";
918
+ var DEFAULT_MAX_SCOPES = 1e3;
919
+ var DEFAULT_IDLE_TTL_MS = 15 * 60 * 1e3;
920
+ var A2ARuntimeCapacityError = class extends Error {
921
+ constructor(maxScopes) {
922
+ super(`A2A runtime scope capacity of ${maxScopes} is exhausted`);
923
+ this.name = "A2ARuntimeCapacityError";
924
+ }
925
+ };
926
+ var ScopedExecutionEventBusManager = class {
927
+ constructor(touch) {
928
+ this.buses = /* @__PURE__ */ new Map();
929
+ this.touch = touch;
930
+ }
931
+ createOrGetByTaskId(taskId) {
932
+ this.touch();
933
+ const existing = this.buses.get(taskId);
934
+ if (existing) return existing;
935
+ const bus = new DefaultExecutionEventBus();
936
+ this.buses.set(taskId, bus);
937
+ return bus;
938
+ }
939
+ getByTaskId(taskId) {
940
+ this.touch();
941
+ return this.buses.get(taskId);
942
+ }
943
+ cleanupByTaskId(taskId) {
944
+ this.touch();
945
+ const bus = this.buses.get(taskId);
946
+ bus?.removeAllListeners();
947
+ this.buses.delete(taskId);
948
+ }
949
+ get activeBusCount() {
950
+ return this.buses.size;
951
+ }
952
+ close() {
953
+ for (const bus of this.buses.values()) {
954
+ bus.removeAllListeners();
955
+ }
956
+ this.buses.clear();
957
+ }
958
+ };
959
+ var A2ARuntimeRegistry = class {
960
+ constructor(options = {}) {
961
+ this.entries = /* @__PURE__ */ new Map();
962
+ this.closed = false;
963
+ this.maxScopes = options.maxScopes ?? DEFAULT_MAX_SCOPES;
964
+ this.idleTtlMs = options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
965
+ this.now = options.now ?? Date.now;
966
+ }
967
+ /**
968
+ * Acquire and pin the runtime manager for an authorization scope.
969
+ *
970
+ * @param scope Exact tenant, project, and assistant authorization scope.
971
+ * @returns A lease whose manager is shared only within that scope.
972
+ */
973
+ acquire(scope) {
974
+ if (this.closed) throw new Error("A2A runtime registry is closed");
975
+ const now = this.now();
976
+ this.pruneExpired(now);
977
+ const key = JSON.stringify([
978
+ scope.tenantId,
979
+ scope.projectId,
980
+ scope.assistantId
981
+ ]);
982
+ const existing = this.entries.get(key);
983
+ const entry = existing ?? this.createEntry(key, now);
984
+ entry.lastAccessedAt = now;
985
+ entry.leaseCount += 1;
986
+ let released = false;
987
+ return {
988
+ manager: entry.manager,
989
+ release: () => {
990
+ if (released) return;
991
+ released = true;
992
+ entry.leaseCount -= 1;
993
+ entry.lastAccessedAt = this.now();
994
+ }
995
+ };
996
+ }
997
+ close() {
998
+ if (this.closed) return;
999
+ this.closed = true;
1000
+ for (const entry of this.entries.values()) {
1001
+ entry.manager.close();
1002
+ }
1003
+ this.entries.clear();
1004
+ }
1005
+ pruneExpired(now) {
1006
+ for (const [key, entry] of this.entries) {
1007
+ if (!this.isActive(entry) && now - entry.lastAccessedAt > this.idleTtlMs) {
1008
+ entry.manager.close();
1009
+ this.entries.delete(key);
1010
+ }
1011
+ }
1012
+ }
1013
+ makeCapacity() {
1014
+ if (this.entries.size < this.maxScopes) return;
1015
+ let oldest;
1016
+ for (const candidate of this.entries) {
1017
+ if (this.isActive(candidate[1])) continue;
1018
+ if (!oldest || candidate[1].lastAccessedAt < oldest[1].lastAccessedAt) {
1019
+ oldest = candidate;
1020
+ }
1021
+ }
1022
+ if (!oldest) throw new A2ARuntimeCapacityError(this.maxScopes);
1023
+ oldest[1].manager.close();
1024
+ this.entries.delete(oldest[0]);
1025
+ }
1026
+ createEntry(key, now) {
1027
+ this.makeCapacity();
1028
+ const entry = {
1029
+ lastAccessedAt: now,
1030
+ leaseCount: 0,
1031
+ manager: new ScopedExecutionEventBusManager(() => {
1032
+ entry.lastAccessedAt = this.now();
1033
+ })
1034
+ };
1035
+ this.entries.set(key, entry);
1036
+ return entry;
1037
+ }
1038
+ isActive(entry) {
1039
+ return entry.leaseCount > 0 || entry.manager.activeBusCount > 0;
1040
+ }
1041
+ };
1042
+
1043
+ // src/routes/a2a-standard.ts
1044
+ function getHeader(value) {
1045
+ if (Array.isArray(value)) return value[0];
1046
+ return value;
1047
+ }
1048
+ function getTenantHeader(request) {
1049
+ return getHeader(request.headers["x-tenant-id"]) ?? "default";
1050
+ }
1051
+ function getSessionUser(request) {
1052
+ return request.user;
1053
+ }
1054
+ function getSessionTenantId(request) {
1055
+ const userTenantId = getSessionUser(request)?.tenantId;
1056
+ if (userTenantId) return userTenantId;
1057
+ return getTenantHeader(request);
1058
+ }
1059
+ function buildBaseUrl(request) {
1060
+ const protocol = getHeader(request.headers["x-forwarded-proto"]) ?? request.protocol;
1061
+ const forwardedHost = getHeader(request.headers["x-forwarded-host"]);
1062
+ const host = forwardedHost ? forwardedHost.split(",")[0].trim() : request.host;
1063
+ return `${protocol}://${host}`;
1064
+ }
1065
+ function getExposure(assistant) {
1066
+ const graphDefinition = assistant.graphDefinition;
1067
+ return graphDefinition?.a2aExposure;
1068
+ }
1069
+ function makeFileIngestor(tenantId, maxFileBytes) {
1070
+ return new A2AFileIngestor({
1071
+ saveProjectFile: (params) => saveProjectFile({ tenantId, ...params }),
1072
+ ...maxFileBytes !== void 0 ? { maxBytes: maxFileBytes } : {}
1073
+ });
1074
+ }
1075
+ function extractRequestId(body) {
1076
+ if (typeof body === "object" && body !== null && "id" in body) {
1077
+ const id = body.id;
1078
+ if (typeof id === "string" || typeof id === "number" || id === null) {
1079
+ return id;
1080
+ }
1081
+ }
1082
+ return null;
1083
+ }
1084
+ function authErrorToRpcCode(statusCode) {
1085
+ switch (statusCode) {
1086
+ case 401:
1087
+ return -32001;
1088
+ case 403:
1089
+ return -32002;
1090
+ case 404:
1091
+ return -32003;
1092
+ default:
1093
+ return -32603;
1094
+ }
1095
+ }
1096
+ function jsonRpcError(id, code, message) {
1097
+ return { jsonrpc: "2.0", id, error: { code, message } };
1098
+ }
1099
+ function isAsyncGenerator(value) {
1100
+ return typeof value === "object" && value !== null && typeof value[Symbol.asyncIterator] === "function";
1101
+ }
1102
+ var SSE_HEADERS = {
1103
+ "Content-Type": "text/event-stream",
1104
+ "Cache-Control": "no-cache",
1105
+ Connection: "keep-alive",
1106
+ "Access-Control-Allow-Origin": "*",
1107
+ "A2A-Version": "0.3"
1108
+ };
1109
+ function registerA2AStandardRoutes(app, deps) {
1110
+ const authService = deps.authService;
1111
+ const getTaskStore = deps.getTaskStoreLattice ?? (() => getStoreLattice("default", "task").store);
1112
+ const getTaskWorkItemStore = deps.getTaskWorkItemStore ?? (() => getStoreLattice("default", "taskWorkItem").store);
1113
+ const getAssistantStore = deps.getAssistantStore ?? (() => getStoreLattice("default", "assistant").store);
1114
+ const getProjectStore = deps.getProjectStore ?? (() => getStoreLattice("default", "project").store);
1115
+ const getTenantStore = deps.getTenantStore ?? (() => {
1116
+ try {
1117
+ return getStoreLattice("default", "tenant").store;
1118
+ } catch {
1119
+ return void 0;
1120
+ }
1121
+ });
1122
+ async function findExposedAssistant(request, assistantId) {
1123
+ const tenantId = getSessionTenantId(request);
1124
+ const direct = await getAssistantStore().getAssistantById(
1125
+ tenantId,
1126
+ assistantId
1127
+ );
1128
+ if (direct) return direct;
1129
+ const tenantStore = getTenantStore();
1130
+ if (!tenantStore) return null;
1131
+ const tenants = await tenantStore.getAllTenants();
1132
+ for (const tenant of tenants) {
1133
+ if (tenant.id === tenantId) continue;
1134
+ const found = await getAssistantStore().getAssistantById(
1135
+ tenant.id,
1136
+ assistantId
1137
+ );
1138
+ if (found) return found;
1139
+ }
1140
+ return null;
1141
+ }
1142
+ const runtimeRegistry = deps.runtimeRegistry ?? new A2ARuntimeRegistry();
1143
+ app.addHook("onClose", async () => {
1144
+ runtimeRegistry.close();
1145
+ });
1146
+ app.get(
1147
+ "/api/a2a/agents/:assistantId/.well-known/agent-card.json",
1148
+ async (request, reply) => {
1149
+ const assistant = await findExposedAssistant(
1150
+ request,
1151
+ request.params.assistantId
1152
+ );
1153
+ if (!assistant || getExposure(assistant)?.enabled !== true) {
1154
+ reply.status(404).send({ error: "Not found" });
1155
+ return;
1156
+ }
1157
+ reply.header("A2A-Version", "0.3");
1158
+ reply.status(200).send(
1159
+ buildAgentCardForAssistant({
1160
+ assistant,
1161
+ exposure: getExposure(assistant),
1162
+ baseUrl: buildBaseUrl(request)
1163
+ })
1164
+ );
1165
+ }
1166
+ );
1167
+ app.post(
1168
+ "/api/a2a/agents/:assistantId/jsonrpc",
1169
+ async (request, reply) => {
1170
+ const requestId = extractRequestId(request.body);
1171
+ const auth = await authService.authenticate(
1172
+ getHeader(request.headers.authorization) ?? getHeader(request.headers["x-api-key"])
1173
+ );
1174
+ if (!auth.authenticated) {
1175
+ reply.status(401).send(jsonRpcError(requestId, -32001, "Unauthorized"));
1176
+ return;
1177
+ }
1178
+ let assistant;
1179
+ try {
1180
+ assistant = await authService.assertExposedAgent(
1181
+ auth,
1182
+ request.params.assistantId
1183
+ );
1184
+ } catch (err) {
1185
+ if (err instanceof A2AAuthError) {
1186
+ reply.status(err.statusCode).send(
1187
+ jsonRpcError(
1188
+ requestId,
1189
+ authErrorToRpcCode(err.statusCode),
1190
+ err.message
1191
+ )
1192
+ );
1193
+ return;
1194
+ }
1195
+ request.log.error({ err }, "a2a:jsonrpc:auth");
1196
+ reply.status(500).send(jsonRpcError(requestId, -32603, "Internal error"));
1197
+ return;
1198
+ }
1199
+ const scope = {
1200
+ tenantId: auth.tenantId ?? "default",
1201
+ projectId: auth.projectId ?? "",
1202
+ assistantId: assistant.id
1203
+ };
1204
+ const internalTaskStore = getTaskStore();
1205
+ const workItemStore = getTaskWorkItemStore();
1206
+ const taskStore = new A2ATaskStoreAdapter({
1207
+ taskStore: internalTaskStore,
1208
+ workItemStore,
1209
+ lifecycle: createTaskLifecycleService({
1210
+ taskStore: internalTaskStore,
1211
+ workItemStore
1212
+ }),
1213
+ scope
1214
+ });
1215
+ const ingestor = makeFileIngestor(scope.tenantId, deps.maxFileBytes);
1216
+ const executor = new AxiomAgentExecutor({ scope, assistant, taskStore, ingestor });
1217
+ const card = buildAgentCardForAssistant({
1218
+ assistant,
1219
+ exposure: getExposure(assistant),
1220
+ baseUrl: buildBaseUrl(request)
1221
+ });
1222
+ let runtimeLease;
1223
+ try {
1224
+ runtimeLease = runtimeRegistry.acquire(scope);
1225
+ } catch (err) {
1226
+ if (err instanceof A2ARuntimeCapacityError) {
1227
+ request.log.error({ err, scope }, "a2a:jsonrpc:runtime_capacity");
1228
+ reply.status(503).send(jsonRpcError(requestId, -32603, "A2A runtime capacity exhausted"));
1229
+ return;
1230
+ }
1231
+ throw err;
1232
+ }
1233
+ let result;
1234
+ try {
1235
+ const handler = new DefaultRequestHandler(
1236
+ card,
1237
+ taskStore,
1238
+ executor,
1239
+ runtimeLease.manager
1240
+ );
1241
+ const transport = new JsonRpcTransportHandler(handler);
1242
+ reply.header("A2A-Version", "0.3");
1243
+ result = await transport.handle(request.body);
1244
+ } catch (err) {
1245
+ runtimeLease.release();
1246
+ request.log.error({ err }, "a2a:jsonrpc:handler");
1247
+ reply.status(500).send(jsonRpcError(requestId, -32603, "Internal error"));
1248
+ return;
1249
+ }
1250
+ if (isAsyncGenerator(result)) {
1251
+ reply.hijack();
1252
+ reply.raw.writeHead(200, SSE_HEADERS);
1253
+ try {
1254
+ for await (const evt of result) {
1255
+ reply.raw.write(`data: ${JSON.stringify(evt)}
1256
+
1257
+ `);
1258
+ }
1259
+ } catch (err) {
1260
+ request.log.error({ err }, "a2a:jsonrpc:stream");
1261
+ const errorResponse = {
1262
+ jsonrpc: "2.0",
1263
+ id: requestId,
1264
+ error: A2AError.internalError(
1265
+ err instanceof Error ? err.message : "Streaming error."
1266
+ ).toJSONRPCError()
1267
+ };
1268
+ reply.raw.write(`event: error
1269
+ data: ${JSON.stringify(errorResponse)}
1270
+
1271
+ `);
1272
+ } finally {
1273
+ runtimeLease.release();
1274
+ reply.raw.end();
1275
+ }
1276
+ return;
1277
+ }
1278
+ runtimeLease.release();
1279
+ reply.send(result);
1280
+ }
1281
+ );
1282
+ app.post(
1283
+ "/api/a2a/agents/:assistantId/test-call",
1284
+ async (request, reply) => {
1285
+ const user = getSessionUser(request);
1286
+ if (!user?.tenantId) {
1287
+ reply.status(401).send({ success: false, error: "Unauthorized" });
1288
+ return;
1289
+ }
1290
+ const tenantId = user.tenantId;
1291
+ const body = request.body ?? {};
1292
+ const { projectId, message } = body;
1293
+ if (!projectId) {
1294
+ reply.status(400).send({ error: "projectId is required" });
1295
+ return;
1296
+ }
1297
+ if (!message || !message.trim()) {
1298
+ reply.status(400).send({ error: "message is required" });
1299
+ return;
1300
+ }
1301
+ const assistant = await getAssistantStore().getAssistantById(
1302
+ tenantId,
1303
+ request.params.assistantId
1304
+ );
1305
+ if (!assistant) {
1306
+ reply.status(404).send({ error: "Assistant not found" });
1307
+ return;
1308
+ }
1309
+ const project = await getProjectStore().getProjectById(
1310
+ tenantId,
1311
+ projectId
1312
+ );
1313
+ if (!project || project.tenantId !== tenantId) {
1314
+ reply.status(403).send({ error: "Project not found" });
1315
+ return;
1316
+ }
1317
+ const scope = {
1318
+ tenantId,
1319
+ projectId,
1320
+ assistantId: assistant.id
1321
+ };
1322
+ const ingestor = makeFileIngestor(scope.tenantId, deps.maxFileBytes);
1323
+ const internalTaskStore = getTaskStore();
1324
+ const workItemStore = getTaskWorkItemStore();
1325
+ const taskStore = new A2ATaskStoreAdapter({
1326
+ taskStore: internalTaskStore,
1327
+ workItemStore,
1328
+ lifecycle: createTaskLifecycleService({
1329
+ taskStore: internalTaskStore,
1330
+ workItemStore
1331
+ }),
1332
+ scope
1333
+ });
1334
+ const executor = new AxiomAgentExecutor({ scope, assistant, taskStore, ingestor });
1335
+ const taskId = uuidv42();
1336
+ const contextId = uuidv42();
1337
+ const userMessage = {
1338
+ kind: "message",
1339
+ messageId: uuidv42(),
1340
+ role: "user",
1341
+ parts: [{ kind: "text", text: message }]
1342
+ };
1343
+ const requestContext = new RequestContext(userMessage, taskId, contextId);
1344
+ const bus = new DefaultExecutionEventBus2();
1345
+ reply.hijack();
1346
+ reply.raw.writeHead(200, SSE_HEADERS);
1347
+ const sendEvent = (event, data) => {
1348
+ reply.raw.write(`event: ${event}
1349
+ data: ${JSON.stringify(data)}
1350
+
1351
+ `);
1352
+ };
1353
+ bus.on("event", (event) => {
1354
+ sendEvent(event.kind, event);
1355
+ });
1356
+ try {
1357
+ await executor.execute(requestContext, bus);
1358
+ } catch (err) {
1359
+ request.log.error({ err }, "a2a:test-call:execution");
1360
+ sendEvent("status-update", {
1361
+ kind: "status-update",
1362
+ taskId,
1363
+ contextId,
1364
+ status: { state: "failed", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
1365
+ final: true
1366
+ });
1367
+ } finally {
1368
+ reply.raw.end();
1369
+ }
1370
+ }
1371
+ );
1372
+ }
1373
+ export {
1374
+ registerA2AStandardRoutes
1375
+ };
1376
+ //# sourceMappingURL=a2a-standard-U2XYG45L.mjs.map