@axiom-lattice/gateway 6.0.1 → 7.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.
- package/README.md +0 -1
- package/dist/a2a-standard-DP53K7RC.mjs +3416 -0
- package/dist/a2a-standard-DP53K7RC.mjs.map +1 -0
- package/dist/{chunk-YKTXW6F6.mjs → chunk-2SR4F3ZD.mjs} +65 -1
- package/dist/chunk-2SR4F3ZD.mjs.map +1 -0
- package/dist/{chunk-QUT2DS4R.mjs → chunk-J6B2VFJ7.mjs} +17 -4
- package/dist/chunk-J6B2VFJ7.mjs.map +1 -0
- package/dist/embed/embed-page-AgentDetailSideApp-KNVDO3FZ.js +1 -0
- package/dist/embed/embed-page-AgentGovernanceWorkbench-QSKWVADP.js +1 -0
- package/dist/embed/embed-page-MessageGraph-VGXX4OWU.js +6 -0
- package/dist/embed/embed-page-ReactChart-PIISVBLU.js +1 -0
- package/dist/embed/{embed-page-ReactInfographic-5P36JLSX.js → embed-page-ReactInfographic-677NVKOU.js} +1 -1
- package/dist/embed/embed-page-SkillFlow-JLI5MEZE.js +1 -0
- package/dist/embed/{embed-page-TasksSideApp-DA4LUTMD.js → embed-page-TasksSideApp-D7FFG2OC.js} +1 -1
- package/dist/embed/embed-page-ThreadMaintenancePanel-FSQVVIIG.js +1 -0
- package/dist/embed/{embed-page-TopologyRuntimeView-NFBHAICN.js → embed-page-TopologyRuntimeView-IS6HEO4F.js} +1 -1
- package/dist/embed/{embed-page-WorkflowAutomationView-43YGTXGF.js → embed-page-WorkflowAutomationView-IDVSQTJB.js} +1 -1
- package/dist/embed/embed-page-all-JEPF6WHD.js +1 -0
- package/dist/embed/{embed-page-architectureDiagram-VXUJARFQ-THUN2PFL.js → embed-page-architectureDiagram-VXUJARFQ-TYJ3NO2K.js} +1 -1
- package/dist/embed/embed-page-aspnet-GIV4ESY6.js +1 -0
- package/dist/embed/{embed-page-blockDiagram-VD42YOAC-K7OX6LIO.js → embed-page-blockDiagram-VD42YOAC-L4J3TZWR.js} +1 -1
- package/dist/embed/{embed-page-c4Diagram-YG6GDRKO-DJDAF5AO.js → embed-page-c4Diagram-YG6GDRKO-CP2IXU3Z.js} +1 -1
- package/dist/embed/{embed-page-chunk-FD2PRLMW.js → embed-page-chunk-2VJ7T35I.js} +1 -1
- package/dist/embed/embed-page-chunk-37LGEEJU.js +21 -0
- package/dist/embed/embed-page-chunk-436REIWY.js +1 -0
- package/dist/embed/embed-page-chunk-4W34QOXT.js +517 -0
- package/dist/embed/{embed-page-chunk-JUZPOR5Y.js → embed-page-chunk-5WWOKLDB.js} +1 -1
- package/dist/embed/{embed-page-chunk-HYPA2PHA.js → embed-page-chunk-6IIMW7OG.js} +1 -1
- package/dist/embed/{embed-page-chunk-XKGF5D6L.js → embed-page-chunk-7CJDHJEH.js} +1 -1
- package/dist/embed/{embed-page-chunk-J5AHS4PO.js → embed-page-chunk-7GLQRV3J.js} +1 -1
- package/dist/embed/{embed-page-chunk-SQ4GANYE.js → embed-page-chunk-AEDAOTEO.js} +1 -1
- package/dist/embed/embed-page-chunk-BANVRQT7.js +322 -0
- package/dist/embed/{embed-page-chunk-C4ZV4IXK.js → embed-page-chunk-BOEX26H7.js} +2 -2
- package/dist/embed/{embed-page-chunk-A2AHDN6K.js → embed-page-chunk-DQJVWZOB.js} +1 -1
- package/dist/embed/embed-page-chunk-EYYKNTX7.js +8 -0
- package/dist/embed/{embed-page-chunk-DTRFJLVZ.js → embed-page-chunk-F5QVZLK5.js} +1 -1
- package/dist/embed/{embed-page-chunk-GKHIPXZ2.js → embed-page-chunk-FEMHLB6I.js} +1 -1
- package/dist/embed/{embed-page-chunk-UHH575CK.js → embed-page-chunk-JFMBLJXY.js} +1 -1
- package/dist/embed/{embed-page-chunk-G2RO6IBN.js → embed-page-chunk-KXVXKWED.js} +1 -1
- package/dist/embed/embed-page-chunk-N66H4DNH.js +6657 -0
- package/dist/embed/{embed-page-chunk-WTOAQDQN.js → embed-page-chunk-NFMLKSPM.js} +1 -1
- package/dist/embed/{embed-page-chunk-PLNJRORR.js → embed-page-chunk-NVFNRWRN.js} +1 -1
- package/dist/embed/{embed-page-chunk-V2GBFWLG.js → embed-page-chunk-S2KVN3RB.js} +1 -1
- package/dist/embed/{embed-page-chunk-QULNM5O2.js → embed-page-chunk-SRMYHWL3.js} +1 -1
- package/dist/embed/{embed-page-chunk-D5GP7FHH.js → embed-page-chunk-TVVAPR7I.js} +1 -1
- package/dist/embed/{embed-page-chunk-WFUFTRWA.js → embed-page-chunk-WK77HIEN.js} +1 -1
- package/dist/embed/{embed-page-chunk-XSKNS3LY.js → embed-page-chunk-WWXRO4ZL.js} +1 -1
- package/dist/embed/{embed-page-chunk-BVSC3MSO.js → embed-page-chunk-XFV3N2LD.js} +1 -1
- package/dist/embed/{embed-page-chunk-VY6MAPHR.js → embed-page-chunk-XSKDYUNQ.js} +1 -1
- package/dist/embed/{embed-page-chunk-V7R2Y72J.js → embed-page-chunk-YJUCDIYK.js} +1 -1
- package/dist/embed/embed-page-chunk-ZQQFWEWO.js +1 -0
- package/dist/embed/embed-page-classDiagram-2ON5EDUG-MHNSVPAN.js +1 -0
- package/dist/embed/embed-page-classDiagram-v2-WZHVMYZB-QCMEVMYH.js +1 -0
- package/dist/embed/{embed-page-code-highlighter-IPU5ZNPJ.js → embed-page-code-highlighter-66CSPTFK.js} +1 -1
- package/dist/embed/embed-page-cshtml-RYMZMLKB.js +1 -0
- package/dist/embed/{embed-page-dagre-6UL2VRFP-UQFZMKYW.js → embed-page-dagre-6UL2VRFP-LTZ2DVVO.js} +1 -1
- package/dist/embed/{embed-page-diagram-PSM6KHXK-4IKXWNA4.js → embed-page-diagram-PSM6KHXK-JHYXSMCB.js} +1 -1
- package/dist/embed/{embed-page-diagram-QEK2KX5R-245T32UF.js → embed-page-diagram-QEK2KX5R-2YGDFFTU.js} +1 -1
- package/dist/embed/{embed-page-diagram-S2PKOQOG-BOA5MJA5.js → embed-page-diagram-S2PKOQOG-6EW6MDJW.js} +1 -1
- package/dist/embed/{embed-page-en-WB2EPS63.js → embed-page-en-2SWQHPAC.js} +1 -1
- package/dist/embed/embed-page-en-3Z4N5KXB.js +1 -0
- package/dist/embed/embed-page-en-7IHJFS63.js +1 -0
- package/dist/embed/embed-page-en-7NF4GCC2.js +1 -0
- package/dist/embed/embed-page-en-BLF5THT6.js +1 -0
- package/dist/embed/embed-page-en-ETUCVCSC.js +1 -0
- package/dist/embed/{embed-page-en-UWBIHR6I.js → embed-page-en-H4P63IAK.js} +1 -1
- package/dist/embed/embed-page-en-WUFULCUS.js +1 -0
- package/dist/embed/{embed-page-en-S4MXFPIU.js → embed-page-en-X6B6RVNL.js} +1 -1
- package/dist/embed/embed-page-en-XUMHP5CL.js +1 -0
- package/dist/embed/{embed-page-erDiagram-Q2GNP2WA-S4J4SQPR.js → embed-page-erDiagram-Q2GNP2WA-KSUS6MHW.js} +1 -1
- package/dist/embed/{embed-page-flowDiagram-NV44I4VS-ZFIJXKMW.js → embed-page-flowDiagram-NV44I4VS-AYXRDKAJ.js} +1 -1
- package/dist/embed/{embed-page-ganttDiagram-JELNMOA3-7MSIEWL4.js → embed-page-ganttDiagram-JELNMOA3-AV7N2N3T.js} +1 -1
- package/dist/embed/{embed-page-gitGraphDiagram-NY62KEGX-F6KE6PZK.js → embed-page-gitGraphDiagram-NY62KEGX-IBVOXVO5.js} +1 -1
- package/dist/embed/embed-page-infoDiagram-WHAUD3N6-ZULKOSOZ.js +2 -0
- package/dist/embed/{embed-page-journeyDiagram-XKPGCS4Q-6VTW6GQ3.js → embed-page-journeyDiagram-XKPGCS4Q-3GILI7Y4.js} +1 -1
- package/dist/embed/{embed-page-kanban-definition-3W4ZIXB7-7IZYCLQZ.js → embed-page-kanban-definition-3W4ZIXB7-UGO3J7IH.js} +1 -1
- package/dist/embed/{embed-page-mermaid-R7ZBUE3D.js → embed-page-mermaid-KEBBKHQH.js} +2 -2
- package/dist/embed/{embed-page-mindmap-definition-VGOIOE7T-GKTOT34A.js → embed-page-mindmap-definition-VGOIOE7T-VXXZJS3Z.js} +1 -1
- package/dist/embed/{embed-page-pieDiagram-ADFJNKIX-VDPALO42.js → embed-page-pieDiagram-ADFJNKIX-ZFAWYA2N.js} +1 -1
- package/dist/embed/{embed-page-requirementDiagram-UZGBJVZJ-LTZH6RTD.js → embed-page-requirementDiagram-UZGBJVZJ-2QBINXR7.js} +1 -1
- package/dist/embed/{embed-page-sequenceDiagram-WL72ISMW-LV6Q4SHW.js → embed-page-sequenceDiagram-WL72ISMW-3MGTESG4.js} +1 -1
- package/dist/embed/{embed-page-stateDiagram-FKZM4ZOC-U4NHQDNP.js → embed-page-stateDiagram-FKZM4ZOC-W6VPIMB5.js} +1 -1
- package/dist/embed/embed-page-stateDiagram-v2-4FDKWEC3-QYJ47QBY.js +1 -0
- package/dist/embed/{embed-page-tsx-PNW7N5T4.js → embed-page-tsx-LGDDERM2.js} +1 -1
- package/dist/embed/{embed-page-xychartDiagram-PRI3JC2R-VZSIXBFK.js → embed-page-xychartDiagram-PRI3JC2R-7XKT4R7V.js} +1 -1
- package/dist/embed/embed-page-zh-D3IC6BGA.js +1 -0
- package/dist/embed/{embed-page-zh-SSMSF5LE.js → embed-page-zh-EQ5X2M32.js} +1 -1
- package/dist/embed/embed-page-zh-G75LDQU4.js +1 -0
- package/dist/embed/embed-page-zh-GXE63PQ7.js +1 -0
- package/dist/embed/{embed-page-zh-ACZNJ4X4.js → embed-page-zh-IB3422D4.js} +1 -1
- package/dist/embed/embed-page-zh-KJXKOUIY.js +1 -0
- package/dist/embed/{embed-page-zh-NKHP3CFU.js → embed-page-zh-N2STRBUG.js} +1 -1
- package/dist/embed/{embed-page-zh-XEYNKCTC.js → embed-page-zh-PRMID3YQ.js} +1 -1
- package/dist/embed/embed-page-zh-RMGMUSDH.js +1 -0
- package/dist/embed/embed-page-zh-RMSS6PXD.js +1 -0
- package/dist/embed/embed-page.js +1 -1
- package/dist/index.d.mts +1513 -252
- package/dist/index.d.ts +1513 -252
- package/dist/index.js +17568 -5813
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31824 -22070
- package/dist/index.mjs.map +1 -1
- package/dist/{open-MDC4VCR7.mjs → open-NTH7AGD3.mjs} +41 -78
- package/dist/open-NTH7AGD3.mjs.map +1 -0
- package/dist/{open-exposure-OX5NKWDG.mjs → open-exposure-AGNG4MC3.mjs} +13 -3
- package/dist/{open-exposure-OX5NKWDG.mjs.map → open-exposure-AGNG4MC3.mjs.map} +1 -1
- package/dist/{open-files-FS5AN6Q6.mjs → open-files-V3R2BFTV.mjs} +4 -4
- package/package.json +9 -8
- package/dist/a2a-standard-3OZFXDCW.mjs +0 -1413
- package/dist/a2a-standard-3OZFXDCW.mjs.map +0 -1
- package/dist/chunk-K32XEQLH.mjs +0 -28
- package/dist/chunk-K32XEQLH.mjs.map +0 -1
- package/dist/chunk-QUT2DS4R.mjs.map +0 -1
- package/dist/chunk-YKTXW6F6.mjs.map +0 -1
- package/dist/embed/embed-page-AgentDetailSideApp-JYVZKSUU.js +0 -1
- package/dist/embed/embed-page-AgentGovernanceWorkbench-SD2EHW2B.js +0 -1
- package/dist/embed/embed-page-MessageGraph-GLQXKAVC.js +0 -6
- package/dist/embed/embed-page-ReactChart-74MHCE2K.js +0 -1
- package/dist/embed/embed-page-SkillFlow-NDAK3GAD.js +0 -1
- package/dist/embed/embed-page-TeamWorkspace-DUB3L6V6.js +0 -748
- package/dist/embed/embed-page-TeamWorkspace-YQFWXDLI.css +0 -1
- package/dist/embed/embed-page-all-NEJVJ4G7.js +0 -1
- package/dist/embed/embed-page-aspnet-SRK67AH2.js +0 -1
- package/dist/embed/embed-page-chunk-2PFKLMTR.js +0 -21
- package/dist/embed/embed-page-chunk-DWIBLGBW.js +0 -2
- package/dist/embed/embed-page-chunk-ESQ5V77Q.js +0 -1
- package/dist/embed/embed-page-chunk-MHGVD4EV.js +0 -525
- package/dist/embed/embed-page-chunk-P477ZTUO.js +0 -7009
- package/dist/embed/embed-page-classDiagram-2ON5EDUG-VYHHMIKZ.js +0 -1
- package/dist/embed/embed-page-classDiagram-v2-WZHVMYZB-5AISLXUF.js +0 -1
- package/dist/embed/embed-page-cshtml-ZY7LJSTH.js +0 -1
- package/dist/embed/embed-page-en-2WLCQIB6.js +0 -1
- package/dist/embed/embed-page-en-7WXWAWGG.js +0 -1
- package/dist/embed/embed-page-en-ICVL5QAC.js +0 -1
- package/dist/embed/embed-page-en-OLZHSUC3.js +0 -1
- package/dist/embed/embed-page-infoDiagram-WHAUD3N6-FQMKXERB.js +0 -2
- package/dist/embed/embed-page-stateDiagram-v2-4FDKWEC3-G3EWQCOW.js +0 -1
- package/dist/embed/embed-page-zh-EYLHUD5G.js +0 -1
- package/dist/embed/embed-page-zh-FPYCTKQK.js +0 -1
- package/dist/embed/embed-page-zh-OPUVZZSQ.js +0 -1
- package/dist/mcpTransportRegistry-VXUVJKBC.mjs +0 -14
- package/dist/mcpTransportRegistry-VXUVJKBC.mjs.map +0 -1
- package/dist/open-MDC4VCR7.mjs.map +0 -1
- /package/dist/embed/{embed-page-chunk-34FVRFPW.js → embed-page-chunk-UH47IEAR.js} +0 -0
- /package/dist/{open-files-FS5AN6Q6.mjs.map → open-files-V3R2BFTV.mjs.map} +0 -0
|
@@ -0,0 +1,3416 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildAgentCardForAssistant
|
|
3
|
+
} from "./chunk-J6B2VFJ7.mjs";
|
|
4
|
+
import {
|
|
5
|
+
formatFileRefsSection,
|
|
6
|
+
getRunCancelRouter,
|
|
7
|
+
resolveRunTaskMaxActivePerTenant,
|
|
8
|
+
saveProjectFile,
|
|
9
|
+
wakeRunTaskQueue
|
|
10
|
+
} from "./chunk-2SR4F3ZD.mjs";
|
|
11
|
+
import {
|
|
12
|
+
A2AAuthError
|
|
13
|
+
} from "./chunk-GPHOYR3X.mjs";
|
|
14
|
+
import "./chunk-NFEGQTCC.mjs";
|
|
15
|
+
|
|
16
|
+
// src/routes/a2a-standard.ts
|
|
17
|
+
import { v4 as uuidv42 } from "uuid";
|
|
18
|
+
import {
|
|
19
|
+
A2AError as A2AError3,
|
|
20
|
+
DefaultExecutionEventBus as DefaultExecutionEventBus2,
|
|
21
|
+
DefaultRequestHandler,
|
|
22
|
+
JsonRpcTransportHandler,
|
|
23
|
+
RequestContext
|
|
24
|
+
} from "@a2a-js/sdk/server";
|
|
25
|
+
import {
|
|
26
|
+
agentInstanceManager as agentInstanceManager2,
|
|
27
|
+
ChunkCursorExpiredError,
|
|
28
|
+
chunkBufferStreamKey,
|
|
29
|
+
createTaskLifecycleService,
|
|
30
|
+
getChunkBuffer,
|
|
31
|
+
getStoreLattice
|
|
32
|
+
} from "@axiom-lattice/core";
|
|
33
|
+
import { MessageChunkTypes as MessageChunkTypes3 } from "@axiom-lattice/protocols";
|
|
34
|
+
|
|
35
|
+
// src/services/a2a/A2APushNotificationStore.ts
|
|
36
|
+
import { decrypt, encrypt } from "@axiom-lattice/core";
|
|
37
|
+
|
|
38
|
+
// src/services/a2a/A2APushNotificationUrl.ts
|
|
39
|
+
import { lookup } from "dns/promises";
|
|
40
|
+
import { A2AError } from "@a2a-js/sdk/server";
|
|
41
|
+
import { assertSafeUpstreamUrl } from "@axiom-lattice/core";
|
|
42
|
+
var defaultA2APushHostResolver = async (hostname) => {
|
|
43
|
+
const records = await lookup(hostname, { all: true });
|
|
44
|
+
return records.map((record) => ({
|
|
45
|
+
address: record.address,
|
|
46
|
+
family: record.family
|
|
47
|
+
}));
|
|
48
|
+
};
|
|
49
|
+
function normalizeHost(value) {
|
|
50
|
+
return value.trim().toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "").split("%")[0] ?? "";
|
|
51
|
+
}
|
|
52
|
+
function ipv4Parts(hostname) {
|
|
53
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const parts = match.slice(1).map(Number);
|
|
56
|
+
return parts.every((part) => part >= 0 && part <= 255) ? parts : null;
|
|
57
|
+
}
|
|
58
|
+
function isReservedIpv4(hostname) {
|
|
59
|
+
const parts = ipv4Parts(hostname);
|
|
60
|
+
if (!parts) return false;
|
|
61
|
+
const [a, b, c] = parts;
|
|
62
|
+
if (a === 0 || a === 10 || a === 127) return true;
|
|
63
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
64
|
+
if (a === 169 && b === 254) return true;
|
|
65
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
66
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
67
|
+
if (a === 192 && b === 0 && c === 2) return true;
|
|
68
|
+
if (a === 192 && b === 88 && c === 99) return true;
|
|
69
|
+
if (a === 192 && b === 168) return true;
|
|
70
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
71
|
+
if (a === 198 && b === 51 && c === 100) return true;
|
|
72
|
+
if (a === 203 && b === 0 && c === 113) return true;
|
|
73
|
+
return a >= 224;
|
|
74
|
+
}
|
|
75
|
+
function isReservedIpv6(hostname) {
|
|
76
|
+
const host = normalizeHost(hostname);
|
|
77
|
+
if (!host.includes(":")) return false;
|
|
78
|
+
if (host.startsWith("::")) return true;
|
|
79
|
+
if (host.startsWith("fc") || host.startsWith("fd")) return true;
|
|
80
|
+
if (/^fe[89ab]/.test(host)) return true;
|
|
81
|
+
if (/^fec[0-9a-f]/.test(host)) return true;
|
|
82
|
+
if (host.startsWith("ff")) return true;
|
|
83
|
+
if (host.startsWith("100:")) return true;
|
|
84
|
+
if (host.startsWith("2001:db8")) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function isBlockedPushAddress(address) {
|
|
88
|
+
const host = normalizeHost(address);
|
|
89
|
+
if (ipv4Parts(host)) return isReservedIpv4(host);
|
|
90
|
+
if (!host.includes(":")) return true;
|
|
91
|
+
return isReservedIpv6(host);
|
|
92
|
+
}
|
|
93
|
+
function isBlockedHostname(hostname) {
|
|
94
|
+
const host = normalizeHost(hostname);
|
|
95
|
+
if (host === "localhost" || host === "localhost.localdomain" || host.endsWith(".localhost") || host === "ip6-localhost" || host === "ip6-loopback") {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (host === "metadata" || host === "metadata.google.internal" || host === "metadata.google.com" || host === "instance-data" || host === "instance-data.ec2.internal") {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
return isReservedIpv4(host) || isReservedIpv6(host);
|
|
102
|
+
}
|
|
103
|
+
function toHostAddress(value) {
|
|
104
|
+
if (typeof value === "string") {
|
|
105
|
+
return { address: value, family: value.includes(":") ? 6 : 4 };
|
|
106
|
+
}
|
|
107
|
+
if (typeof value !== "object" || value === null) return null;
|
|
108
|
+
const record = value;
|
|
109
|
+
if (typeof record.address !== "string") return null;
|
|
110
|
+
return {
|
|
111
|
+
address: record.address,
|
|
112
|
+
family: typeof record.family === "number" ? record.family : record.address.includes(":") ? 6 : 4
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async function assertSafePushNotificationUrl(raw, resolveHost = defaultA2APushHostResolver) {
|
|
116
|
+
let url;
|
|
117
|
+
try {
|
|
118
|
+
({ url } = await assertSafeUpstreamUrl(raw));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw A2AError.invalidParams(
|
|
121
|
+
error instanceof Error ? `Push notification URL is not allowed: ${error.message}` : "Push notification URL is not allowed"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (url.protocol !== "https:") {
|
|
125
|
+
throw A2AError.invalidParams("Push notification URL must use HTTPS");
|
|
126
|
+
}
|
|
127
|
+
const hostname = normalizeHost(url.hostname);
|
|
128
|
+
if (isBlockedHostname(hostname)) {
|
|
129
|
+
throw A2AError.invalidParams(
|
|
130
|
+
"Push notification URL must not target a private, loopback, link-local, or metadata host"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
let addresses;
|
|
134
|
+
try {
|
|
135
|
+
addresses = await resolveHost(hostname);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw A2AError.invalidParams(
|
|
138
|
+
error instanceof Error ? `Push notification URL could not be resolved: ${error.message}` : "Push notification URL could not be resolved"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (addresses.length === 0) {
|
|
142
|
+
throw A2AError.invalidParams("Push notification URL did not resolve");
|
|
143
|
+
}
|
|
144
|
+
const normalized = [];
|
|
145
|
+
for (const record of addresses) {
|
|
146
|
+
const resolved = toHostAddress(record);
|
|
147
|
+
if (!resolved) {
|
|
148
|
+
throw A2AError.invalidParams(
|
|
149
|
+
"Push notification URL resolved to an invalid address"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const address = normalizeHost(resolved.address);
|
|
153
|
+
if (isBlockedPushAddress(address)) {
|
|
154
|
+
throw A2AError.invalidParams(
|
|
155
|
+
"Push notification URL resolves to a private, loopback, link-local, or reserved address"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
normalized.push({ address, family: resolved.family });
|
|
159
|
+
}
|
|
160
|
+
return { url, addresses: normalized };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/services/a2a/A2APushNotificationStore.ts
|
|
164
|
+
var NAMESPACE_ROOT = "a2a-push";
|
|
165
|
+
var PAGE_SIZE = 100;
|
|
166
|
+
function isRecord(value) {
|
|
167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
168
|
+
}
|
|
169
|
+
function normalizeConfigId(taskId, value) {
|
|
170
|
+
if (typeof value !== "string") return taskId;
|
|
171
|
+
return value.trim() || taskId;
|
|
172
|
+
}
|
|
173
|
+
var A2APushNotificationStore = class {
|
|
174
|
+
constructor(options) {
|
|
175
|
+
this.store = options.store;
|
|
176
|
+
this.scope = options.scope;
|
|
177
|
+
this.resolveHost = options.resolveHost ?? defaultA2APushHostResolver;
|
|
178
|
+
}
|
|
179
|
+
namespace() {
|
|
180
|
+
return [NAMESPACE_ROOT, this.scope.tenantId, this.scope.assistantId];
|
|
181
|
+
}
|
|
182
|
+
key(taskId, configId) {
|
|
183
|
+
return `${taskId}:${configId}`;
|
|
184
|
+
}
|
|
185
|
+
async save(taskId, pushNotificationConfig) {
|
|
186
|
+
await assertSafePushNotificationUrl(
|
|
187
|
+
pushNotificationConfig.url,
|
|
188
|
+
this.resolveHost
|
|
189
|
+
);
|
|
190
|
+
const configId = normalizeConfigId(taskId, pushNotificationConfig.id);
|
|
191
|
+
const value = {
|
|
192
|
+
taskId,
|
|
193
|
+
configId,
|
|
194
|
+
url: pushNotificationConfig.url
|
|
195
|
+
};
|
|
196
|
+
const token = pushNotificationConfig.token;
|
|
197
|
+
if (typeof token === "string" && token.length > 0) {
|
|
198
|
+
value.tokenEnc = encrypt(token);
|
|
199
|
+
}
|
|
200
|
+
const authentication = pushNotificationConfig.authentication;
|
|
201
|
+
if (authentication) {
|
|
202
|
+
const schemes = Array.isArray(authentication.schemes) ? authentication.schemes.filter((scheme) => typeof scheme === "string") : [];
|
|
203
|
+
const storedAuth = { schemes };
|
|
204
|
+
if (typeof authentication.credentials === "string" && authentication.credentials.length > 0) {
|
|
205
|
+
storedAuth.credentialsEnc = encrypt(authentication.credentials);
|
|
206
|
+
}
|
|
207
|
+
value.authentication = storedAuth;
|
|
208
|
+
}
|
|
209
|
+
await this.store.put(this.scope.tenantId, {
|
|
210
|
+
namespace: this.namespace(),
|
|
211
|
+
key: this.key(taskId, configId),
|
|
212
|
+
value
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async load(taskId) {
|
|
216
|
+
const prefix = `${taskId}:`;
|
|
217
|
+
const items = await this.searchTask(taskId);
|
|
218
|
+
const configs = [];
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
if (!item.key.startsWith(prefix)) continue;
|
|
221
|
+
try {
|
|
222
|
+
configs.push(await this.parse(item, taskId));
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.warn(
|
|
225
|
+
`[a2a] skipping invalid push notification config "${item.key}"`,
|
|
226
|
+
error
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return configs.sort((left, right) => (left.id ?? "").localeCompare(right.id ?? ""));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Read every config for a task. The generic store limit is only a page size;
|
|
234
|
+
* this walks offsets so a task with more than 1000 configs is never truncated.
|
|
235
|
+
*/
|
|
236
|
+
async searchTask(taskId) {
|
|
237
|
+
const items = [];
|
|
238
|
+
let offset = 0;
|
|
239
|
+
for (; ; ) {
|
|
240
|
+
const page = await this.store.search(this.scope.tenantId, {
|
|
241
|
+
namespacePrefix: this.namespace(),
|
|
242
|
+
filter: { taskId },
|
|
243
|
+
limit: PAGE_SIZE,
|
|
244
|
+
offset
|
|
245
|
+
});
|
|
246
|
+
items.push(...page);
|
|
247
|
+
if (page.length < PAGE_SIZE) return items;
|
|
248
|
+
offset += page.length;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async parse(item, taskId) {
|
|
252
|
+
const value = item.value;
|
|
253
|
+
if (typeof value.url !== "string") {
|
|
254
|
+
throw new Error("stored config has no URL");
|
|
255
|
+
}
|
|
256
|
+
await assertSafePushNotificationUrl(value.url, this.resolveHost);
|
|
257
|
+
const configId = normalizeConfigId(
|
|
258
|
+
taskId,
|
|
259
|
+
typeof value.configId === "string" ? value.configId : item.key.slice(taskId.length + 1)
|
|
260
|
+
);
|
|
261
|
+
const config = { id: configId, url: value.url };
|
|
262
|
+
if (typeof value.tokenEnc === "string" && value.tokenEnc.length > 0) {
|
|
263
|
+
config.token = decrypt(value.tokenEnc);
|
|
264
|
+
}
|
|
265
|
+
if (isRecord(value.authentication)) {
|
|
266
|
+
const schemes = Array.isArray(value.authentication.schemes) ? value.authentication.schemes.filter(
|
|
267
|
+
(scheme) => typeof scheme === "string"
|
|
268
|
+
) : [];
|
|
269
|
+
const authentication = {
|
|
270
|
+
schemes
|
|
271
|
+
};
|
|
272
|
+
if (typeof value.authentication.credentialsEnc === "string" && value.authentication.credentialsEnc.length > 0) {
|
|
273
|
+
authentication.credentials = decrypt(value.authentication.credentialsEnc);
|
|
274
|
+
}
|
|
275
|
+
config.authentication = authentication;
|
|
276
|
+
}
|
|
277
|
+
return config;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Delete one config. The SDK always supplies a config id; when it is omitted
|
|
281
|
+
* the default config is `${taskId}:${taskId}` (not every config for the task).
|
|
282
|
+
*/
|
|
283
|
+
async delete(taskId, configId) {
|
|
284
|
+
await this.store.delete(
|
|
285
|
+
this.scope.tenantId,
|
|
286
|
+
this.namespace(),
|
|
287
|
+
this.key(taskId, normalizeConfigId(taskId, configId))
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// src/services/a2a/A2APushNotificationSender.ts
|
|
293
|
+
import { request as httpsRequest } from "https";
|
|
294
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
295
|
+
var TOKEN_HEADER = "X-A2A-Notification-Token";
|
|
296
|
+
function withTimeout(promise, timeoutMs) {
|
|
297
|
+
return new Promise((resolve, reject) => {
|
|
298
|
+
const timer = setTimeout(() => {
|
|
299
|
+
reject(new Error("Push notification request timed out"));
|
|
300
|
+
}, timeoutMs);
|
|
301
|
+
promise.then(
|
|
302
|
+
(value) => {
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
resolve(value);
|
|
305
|
+
},
|
|
306
|
+
(error) => {
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
reject(error);
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function createPinnedLookup(address, family) {
|
|
314
|
+
return (_hostname, options, callback) => {
|
|
315
|
+
if (options?.all) {
|
|
316
|
+
callback(null, [{ address, family }]);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
callback(null, address, family);
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function createPinnedHttpsRequester() {
|
|
323
|
+
return ({ url, addresses, headers, body, timeoutMs }) => new Promise((resolve, reject) => {
|
|
324
|
+
const pinned = addresses[0];
|
|
325
|
+
if (!pinned) {
|
|
326
|
+
reject(new Error("No validated push notification address"));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const parsed = new URL(url);
|
|
330
|
+
const family = pinned.includes(":") ? 6 : 4;
|
|
331
|
+
const request = httpsRequest(parsed, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
agent: false,
|
|
334
|
+
headers: {
|
|
335
|
+
...headers,
|
|
336
|
+
"Content-Length": Buffer.byteLength(body)
|
|
337
|
+
},
|
|
338
|
+
lookup: createPinnedLookup(pinned, family),
|
|
339
|
+
servername: parsed.hostname
|
|
340
|
+
}, (response) => {
|
|
341
|
+
response.resume();
|
|
342
|
+
resolve({ statusCode: response.statusCode ?? 0 });
|
|
343
|
+
});
|
|
344
|
+
const timeout = setTimeout(() => {
|
|
345
|
+
request.destroy(new Error("Push notification request timed out"));
|
|
346
|
+
}, timeoutMs);
|
|
347
|
+
request.on("close", () => clearTimeout(timeout));
|
|
348
|
+
request.on("error", (error) => {
|
|
349
|
+
clearTimeout(timeout);
|
|
350
|
+
reject(error);
|
|
351
|
+
});
|
|
352
|
+
request.end(body);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
var A2APushNotificationSender = class {
|
|
356
|
+
constructor(options) {
|
|
357
|
+
this.store = options.store;
|
|
358
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
359
|
+
this.request = options.request ?? createPinnedHttpsRequester();
|
|
360
|
+
this.resolveHost = options.resolveHost ?? defaultA2APushHostResolver;
|
|
361
|
+
this.logger = options.logger;
|
|
362
|
+
}
|
|
363
|
+
async send(task) {
|
|
364
|
+
let configs;
|
|
365
|
+
try {
|
|
366
|
+
configs = await this.store.load(task.id);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
this.logger?.error?.(
|
|
369
|
+
`[a2a] failed to load push notification configs for task ${task.id}`,
|
|
370
|
+
error
|
|
371
|
+
);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!configs || configs.length === 0) return;
|
|
375
|
+
await Promise.all(configs.map(async (config) => {
|
|
376
|
+
await this.deliver(task, config);
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
async deliver(task, config) {
|
|
380
|
+
let target;
|
|
381
|
+
try {
|
|
382
|
+
target = await assertSafePushNotificationUrl(config.url, this.resolveHost);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
this.logger?.warn?.(
|
|
385
|
+
`[a2a] skipping unsafe push notification URL for task ${task.id}`,
|
|
386
|
+
error
|
|
387
|
+
);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const headers = {
|
|
392
|
+
"Content-Type": "application/json"
|
|
393
|
+
};
|
|
394
|
+
if (config.token) {
|
|
395
|
+
headers[TOKEN_HEADER] = config.token;
|
|
396
|
+
}
|
|
397
|
+
const authentication = config.authentication;
|
|
398
|
+
if (authentication?.credentials) {
|
|
399
|
+
const schemes = authentication.schemes.map((scheme) => scheme.toLowerCase());
|
|
400
|
+
if (schemes.includes("bearer")) {
|
|
401
|
+
headers.Authorization = `Bearer ${authentication.credentials}`;
|
|
402
|
+
} else if (schemes.includes("basic")) {
|
|
403
|
+
headers.Authorization = `Basic ${authentication.credentials}`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const response = await withTimeout(this.request({
|
|
407
|
+
url: target.url.toString(),
|
|
408
|
+
addresses: target.addresses.map((record) => record.address),
|
|
409
|
+
headers,
|
|
410
|
+
body: JSON.stringify(task),
|
|
411
|
+
timeoutMs: this.timeoutMs
|
|
412
|
+
}), this.timeoutMs);
|
|
413
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Push notification endpoint returned HTTP ${response.statusCode}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.logger?.error?.(
|
|
420
|
+
`[a2a] push notification failed for task ${task.id} to ${config.url}`,
|
|
421
|
+
error
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/services/a2a/A2ATaskStoreAdapter.ts
|
|
428
|
+
import { createHash } from "crypto";
|
|
429
|
+
import { isDeepStrictEqual } from "util";
|
|
430
|
+
import {
|
|
431
|
+
createTaskRunBridge
|
|
432
|
+
} from "@axiom-lattice/core";
|
|
433
|
+
var TASK_STATUS_TO_A2A_STATE = {
|
|
434
|
+
pending: "submitted",
|
|
435
|
+
in_progress: "working",
|
|
436
|
+
review: "input-required",
|
|
437
|
+
interrupted: "input-required",
|
|
438
|
+
completed: "completed",
|
|
439
|
+
failed: "failed",
|
|
440
|
+
cancelled: "canceled"
|
|
441
|
+
};
|
|
442
|
+
var TITLE_MAX_LENGTH = 80;
|
|
443
|
+
var A2ATaskScopeConflictError = class extends Error {
|
|
444
|
+
constructor(taskId) {
|
|
445
|
+
super(`A2A task ${taskId} belongs to another scope`);
|
|
446
|
+
this.code = "A2A_TASK_SCOPE_CONFLICT";
|
|
447
|
+
this.name = "A2ATaskScopeConflictError";
|
|
448
|
+
this.taskId = taskId;
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
function mapTaskStatusToA2AState(status) {
|
|
452
|
+
return TASK_STATUS_TO_A2A_STATE[status];
|
|
453
|
+
}
|
|
454
|
+
function asString(value) {
|
|
455
|
+
return typeof value === "string" ? value : void 0;
|
|
456
|
+
}
|
|
457
|
+
function asRecord(value) {
|
|
458
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
459
|
+
}
|
|
460
|
+
function asStringArray(value) {
|
|
461
|
+
if (!Array.isArray(value)) return void 0;
|
|
462
|
+
const strings = value.filter((item) => typeof item === "string");
|
|
463
|
+
return strings.length > 0 ? strings : void 0;
|
|
464
|
+
}
|
|
465
|
+
function extractText(parts) {
|
|
466
|
+
return parts.filter((part) => part.kind === "text").map((part) => part.text).join("\n");
|
|
467
|
+
}
|
|
468
|
+
function truncate(text, maxLength) {
|
|
469
|
+
if (text.length <= maxLength) return text;
|
|
470
|
+
return `${text.slice(0, maxLength)}\u2026`;
|
|
471
|
+
}
|
|
472
|
+
function extractTitleAndDescription(task) {
|
|
473
|
+
const firstUserMessage = task.history?.find((message) => message.role === "user");
|
|
474
|
+
const text = firstUserMessage ? extractText(firstUserMessage.parts) : "";
|
|
475
|
+
return {
|
|
476
|
+
title: truncate(text, TITLE_MAX_LENGTH),
|
|
477
|
+
description: text ? [
|
|
478
|
+
"## Objective",
|
|
479
|
+
"",
|
|
480
|
+
text,
|
|
481
|
+
"",
|
|
482
|
+
"## Acceptance Criteria",
|
|
483
|
+
"",
|
|
484
|
+
"Complete the requested work and return the result through A2A."
|
|
485
|
+
].join("\n") : ""
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function isFileRef(value) {
|
|
489
|
+
return typeof value === "object" && value !== null && typeof value.uri === "string";
|
|
490
|
+
}
|
|
491
|
+
function artifactFileRefs(artifacts) {
|
|
492
|
+
const refs = [];
|
|
493
|
+
for (const artifact of artifacts ?? []) {
|
|
494
|
+
for (const part of artifact.parts) {
|
|
495
|
+
if (part.kind !== "file") continue;
|
|
496
|
+
const file = part.file;
|
|
497
|
+
if (!("uri" in file)) continue;
|
|
498
|
+
refs.push({
|
|
499
|
+
uri: file.uri,
|
|
500
|
+
...file.name ? { name: file.name } : {},
|
|
501
|
+
...file.mimeType ? { mimeType: file.mimeType } : {},
|
|
502
|
+
addedBy: "agent"
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return refs;
|
|
507
|
+
}
|
|
508
|
+
function extractFiles(task) {
|
|
509
|
+
const inputFiles = task.metadata?.inputFiles;
|
|
510
|
+
const inputRefs = Array.isArray(inputFiles) ? inputFiles.filter(isFileRef) : [];
|
|
511
|
+
return [...inputRefs, ...artifactFileRefs(task.artifacts)];
|
|
512
|
+
}
|
|
513
|
+
function extractArtifactText(artifacts) {
|
|
514
|
+
return (artifacts ?? []).map((artifact) => extractText(artifact.parts)).filter((text) => text.length > 0).join("\n");
|
|
515
|
+
}
|
|
516
|
+
function extractFinalTurnAgentText(history) {
|
|
517
|
+
const messages = history ?? [];
|
|
518
|
+
let lastUserIndex = -1;
|
|
519
|
+
for (let i = 0; i < messages.length; i += 1) {
|
|
520
|
+
if (messages[i].role === "user") lastUserIndex = i;
|
|
521
|
+
}
|
|
522
|
+
return messages.slice(lastUserIndex + 1).filter((message) => message.role === "agent").map((message) => extractText(message.parts)).filter((text) => text.length > 0).join("");
|
|
523
|
+
}
|
|
524
|
+
function isFailureState(state) {
|
|
525
|
+
return state === "failed" || state === "rejected";
|
|
526
|
+
}
|
|
527
|
+
function extractFailureReason(task) {
|
|
528
|
+
if (!isFailureState(task.status.state) || !task.status.message) return void 0;
|
|
529
|
+
return extractText(task.status.message.parts);
|
|
530
|
+
}
|
|
531
|
+
function observationEventKey(task, result, failureReason) {
|
|
532
|
+
const digest = createHash("sha256").update(JSON.stringify({
|
|
533
|
+
state: task.status.state,
|
|
534
|
+
timestamp: task.status.timestamp,
|
|
535
|
+
message: task.status.message,
|
|
536
|
+
result,
|
|
537
|
+
failureReason
|
|
538
|
+
})).digest("hex");
|
|
539
|
+
return `a2a-status-observation:${task.id}:${digest}`;
|
|
540
|
+
}
|
|
541
|
+
function snapshotEventKey(task) {
|
|
542
|
+
const digest = createHash("sha256").update(JSON.stringify(task)).digest("hex");
|
|
543
|
+
return `a2a-task-snapshot:${task.id}:${digest}`;
|
|
544
|
+
}
|
|
545
|
+
function isGovernedObservation(state) {
|
|
546
|
+
return state === "completed" || state === "failed" || state === "rejected" || state === "input-required" || state === "canceled";
|
|
547
|
+
}
|
|
548
|
+
function isA2ATaskSnapshot(value) {
|
|
549
|
+
return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.status === "object" && value.status !== null;
|
|
550
|
+
}
|
|
551
|
+
function snapshotSequence(value) {
|
|
552
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
553
|
+
}
|
|
554
|
+
var A2ATaskStoreAdapter = class {
|
|
555
|
+
constructor(options) {
|
|
556
|
+
this.snapshotSequences = /* @__PURE__ */ new Map();
|
|
557
|
+
this.taskStore = options.taskStore;
|
|
558
|
+
this.workItemStore = options.workItemStore;
|
|
559
|
+
this.lifecycle = options.lifecycle;
|
|
560
|
+
this.scope = options.scope;
|
|
561
|
+
this.runBridge = createTaskRunBridge({
|
|
562
|
+
lifecycle: this.lifecycle,
|
|
563
|
+
taskStore: this.taskStore,
|
|
564
|
+
workItemStore: this.workItemStore
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Persist an SDK task by projecting it onto a `TaskItem`, creating it when
|
|
569
|
+
* the id is unseen or updating it otherwise. A full snapshot of the SDK task
|
|
570
|
+
* is stored under `context.a2aTask` so `load` can restore it.
|
|
571
|
+
*/
|
|
572
|
+
async save(task) {
|
|
573
|
+
let lastError;
|
|
574
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
575
|
+
try {
|
|
576
|
+
return await this.saveOnce(task);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
lastError = error;
|
|
579
|
+
if (!isA2AConcurrentWrite(error) || attempt === 2) throw error;
|
|
580
|
+
await new Promise((resolve) => setTimeout(resolve, 10 * (attempt + 1)));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
throw lastError;
|
|
584
|
+
}
|
|
585
|
+
async saveOnce(task) {
|
|
586
|
+
const { tenantId } = this.scope;
|
|
587
|
+
const { title, description } = extractTitleAndDescription(task);
|
|
588
|
+
const files = extractFiles(task);
|
|
589
|
+
const result = task.status.state === "completed" ? extractArtifactText(task.artifacts) || extractFinalTurnAgentText(task.history) : void 0;
|
|
590
|
+
const failureReason = extractFailureReason(task);
|
|
591
|
+
const a2aRunId = asString(task.metadata?.a2aRunId) ?? asString(task.metadata?.runId);
|
|
592
|
+
const context = {
|
|
593
|
+
a2aContextId: task.contextId,
|
|
594
|
+
...a2aRunId ? { a2aRunId } : {},
|
|
595
|
+
threadId: asString(task.metadata?.threadId),
|
|
596
|
+
a2aTask: task
|
|
597
|
+
};
|
|
598
|
+
let existing = await this.taskStore.getById(tenantId, task.id);
|
|
599
|
+
if (existing) {
|
|
600
|
+
this.requireScope(existing, task.id);
|
|
601
|
+
await this.recordSnapshot(task, existing);
|
|
602
|
+
existing = await this.persistRunLink(existing, context, a2aRunId, task);
|
|
603
|
+
} else {
|
|
604
|
+
try {
|
|
605
|
+
existing = await this.taskStore.create({
|
|
606
|
+
id: task.id,
|
|
607
|
+
tenantId,
|
|
608
|
+
ownerType: "agent",
|
|
609
|
+
ownerId: this.scope.assistantId,
|
|
610
|
+
projectId: this.scope.projectId,
|
|
611
|
+
...this.scope.workspaceId ? { workspaceId: this.scope.workspaceId } : {},
|
|
612
|
+
title,
|
|
613
|
+
description,
|
|
614
|
+
sourceId: "a2a",
|
|
615
|
+
status: "pending",
|
|
616
|
+
priority: "medium",
|
|
617
|
+
dependencies: asStringArray(task.metadata?.dependencies),
|
|
618
|
+
files,
|
|
619
|
+
context
|
|
620
|
+
});
|
|
621
|
+
} catch (error) {
|
|
622
|
+
existing = await this.taskStore.getById(tenantId, task.id);
|
|
623
|
+
if (!existing) throw error;
|
|
624
|
+
this.requireScope(existing, task.id);
|
|
625
|
+
}
|
|
626
|
+
await this.recordSnapshot(task, existing);
|
|
627
|
+
}
|
|
628
|
+
const actor = `a2a:${this.scope.assistantId}`;
|
|
629
|
+
const threadId = asString(task.metadata?.threadId);
|
|
630
|
+
if (task.status.state === "working") {
|
|
631
|
+
existing = await this.refresh(existing);
|
|
632
|
+
const resumesInterruption = existing.status === "interrupted" && task.metadata?.a2aResume === true;
|
|
633
|
+
if (existing.status === "pending" || resumesInterruption) {
|
|
634
|
+
await this.requireLifecycleSuccess("start", task.id, await this.runBridge.beginRun({
|
|
635
|
+
tenantId,
|
|
636
|
+
task: existing,
|
|
637
|
+
actor,
|
|
638
|
+
...threadId ? { threadId } : {}
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (!isGovernedObservation(task.status.state)) return;
|
|
644
|
+
existing = await this.refresh(existing);
|
|
645
|
+
await this.recordObservation(task, existing, result, failureReason);
|
|
646
|
+
if (["completed", "failed", "cancelled"].includes(existing.status)) return;
|
|
647
|
+
if (task.status.state === "input-required" && this.isExactInterruptionReplay(task, existing)) return;
|
|
648
|
+
if (task.status.state === "completed" && existing.status === "interrupted") {
|
|
649
|
+
const resumed = await this.lifecycle.resumeInterruption({
|
|
650
|
+
tenantId,
|
|
651
|
+
taskId: task.id,
|
|
652
|
+
actor,
|
|
653
|
+
...threadId ? { threadId } : {}
|
|
654
|
+
});
|
|
655
|
+
await this.requireLifecycleSuccess("resume", task.id, resumed);
|
|
656
|
+
existing = await this.refresh(existing);
|
|
657
|
+
}
|
|
658
|
+
if (existing.status === "pending") {
|
|
659
|
+
const started = await this.requireLifecycleSuccess("start", task.id, await this.runBridge.beginRun({
|
|
660
|
+
tenantId,
|
|
661
|
+
task: existing,
|
|
662
|
+
actor,
|
|
663
|
+
...threadId ? { threadId } : {}
|
|
664
|
+
}));
|
|
665
|
+
existing = started.task;
|
|
666
|
+
}
|
|
667
|
+
await this.applyObservation(task, existing, result, failureReason);
|
|
668
|
+
}
|
|
669
|
+
/** Re-read the canonical task immediately before a lifecycle write. */
|
|
670
|
+
async refresh(fallback) {
|
|
671
|
+
const current = await this.taskStore.getById(this.scope.tenantId, fallback.id);
|
|
672
|
+
if (!current) return fallback;
|
|
673
|
+
this.requireScope(current, fallback.id);
|
|
674
|
+
return current;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Persist the A2A↔run link on an existing TaskItem without replacing
|
|
678
|
+
* service-owned context (interruption audit, lifecycle fields, snapshots).
|
|
679
|
+
*/
|
|
680
|
+
async persistRunLink(item, context, a2aRunId, task) {
|
|
681
|
+
if (a2aRunId === void 0) return item;
|
|
682
|
+
const current = item.context ?? {};
|
|
683
|
+
if (current.a2aRunId === a2aRunId) return item;
|
|
684
|
+
const next = {
|
|
685
|
+
...current,
|
|
686
|
+
a2aContextId: context.a2aContextId,
|
|
687
|
+
threadId: context.threadId,
|
|
688
|
+
a2aRunId
|
|
689
|
+
};
|
|
690
|
+
const updated = await this.taskStore.update(this.scope.tenantId, task.id, { context: next });
|
|
691
|
+
return updated ?? item;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Complete a run result through the lifecycle, resuming an interrupted
|
|
695
|
+
* TaskItem first when the run succeeded after HITL.
|
|
696
|
+
*
|
|
697
|
+
* @throws When the canonical task cannot transition (for example a review
|
|
698
|
+
* interruption that still requires explicit approval).
|
|
699
|
+
*/
|
|
700
|
+
async completeRun(task, result) {
|
|
701
|
+
const { tenantId } = this.scope;
|
|
702
|
+
const actor = `a2a:${this.scope.assistantId}`;
|
|
703
|
+
const threadId = asString(task.metadata?.threadId);
|
|
704
|
+
let item = await this.taskStore.getById(tenantId, task.id);
|
|
705
|
+
if (!item) {
|
|
706
|
+
throw new Error(`A2A completed run cannot reconcile missing task ${task.id}`);
|
|
707
|
+
}
|
|
708
|
+
if (item.status === "interrupted") {
|
|
709
|
+
const resumed = await this.lifecycle.resumeInterruption({
|
|
710
|
+
tenantId,
|
|
711
|
+
taskId: task.id,
|
|
712
|
+
actor,
|
|
713
|
+
...threadId ? { threadId } : {}
|
|
714
|
+
});
|
|
715
|
+
if (!resumed.success) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`A2A completed run cannot resume interruption: ${resumed.code}: ${resumed.error}`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
item = await this.taskStore.getById(tenantId, task.id);
|
|
721
|
+
if (!item) throw new Error(`A2A task ${task.id} disappeared after resume`);
|
|
722
|
+
}
|
|
723
|
+
if (item.status === "pending") {
|
|
724
|
+
const started = await this.runBridge.beginRun({
|
|
725
|
+
tenantId,
|
|
726
|
+
task: item,
|
|
727
|
+
actor,
|
|
728
|
+
...threadId ? { threadId } : {}
|
|
729
|
+
});
|
|
730
|
+
if (!started.success) {
|
|
731
|
+
throw new Error(
|
|
732
|
+
`A2A completed run cannot start task: ${started.code}: ${started.error}`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
item = started.task;
|
|
736
|
+
}
|
|
737
|
+
if (item.status !== "in_progress" && item.status !== "completed") {
|
|
738
|
+
throw new Error(
|
|
739
|
+
`A2A completed run cannot complete canonical ${item.status} task ${task.id}`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
const resultArtifact = {
|
|
743
|
+
artifactId: `${task.id}:result`,
|
|
744
|
+
name: "result",
|
|
745
|
+
parts: [{ kind: "text", text: result.trim() || "(no output)" }]
|
|
746
|
+
};
|
|
747
|
+
const completed = {
|
|
748
|
+
...task,
|
|
749
|
+
artifacts: [
|
|
750
|
+
...(task.artifacts ?? []).filter(
|
|
751
|
+
(artifact) => artifact.artifactId !== resultArtifact.artifactId && artifact.name !== "result"
|
|
752
|
+
),
|
|
753
|
+
resultArtifact
|
|
754
|
+
],
|
|
755
|
+
status: { state: "completed", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
756
|
+
};
|
|
757
|
+
await this.save(completed);
|
|
758
|
+
const loaded = await this.load(task.id);
|
|
759
|
+
if (!loaded || loaded.status.state !== "completed") {
|
|
760
|
+
throw new Error(`A2A task ${task.id} did not persist as completed`);
|
|
761
|
+
}
|
|
762
|
+
return loaded;
|
|
763
|
+
}
|
|
764
|
+
async recordSnapshot(task, item) {
|
|
765
|
+
const incomingSequence = snapshotSequence(task.metadata?.snapshotSequence) ?? 0;
|
|
766
|
+
const currentSequence = this.snapshotSequences.get(task.id) ?? 0;
|
|
767
|
+
const sequence = Math.max(incomingSequence, currentSequence) + 1;
|
|
768
|
+
this.snapshotSequences.set(task.id, sequence);
|
|
769
|
+
const sequencedTask = {
|
|
770
|
+
...task,
|
|
771
|
+
metadata: { ...task.metadata ?? {}, snapshotSequence: sequence }
|
|
772
|
+
};
|
|
773
|
+
const eventKey = snapshotEventKey(sequencedTask);
|
|
774
|
+
const workspaceId = item.workspaceId ?? this.scope.workspaceId;
|
|
775
|
+
await this.workItemStore.createIfAbsentByEventKey({
|
|
776
|
+
tenantId: this.scope.tenantId,
|
|
777
|
+
taskId: task.id,
|
|
778
|
+
projectId: this.scope.projectId,
|
|
779
|
+
action: "activity",
|
|
780
|
+
actor: `a2a:${this.scope.assistantId}`,
|
|
781
|
+
summary: "A2A task snapshot",
|
|
782
|
+
detail: { type: "a2a_task_snapshot", snapshotSequence: sequence, eventKey, task: sequencedTask },
|
|
783
|
+
eventKey,
|
|
784
|
+
...asString(task.metadata?.threadId) ? { threadId: asString(task.metadata?.threadId) } : {},
|
|
785
|
+
...workspaceId ? { workspaceId } : {}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
async recordObservation(task, item, result, failureReason) {
|
|
789
|
+
const workspaceId = item.workspaceId ?? this.scope.workspaceId;
|
|
790
|
+
await this.workItemStore.createIfAbsentByEventKey({
|
|
791
|
+
tenantId: this.scope.tenantId,
|
|
792
|
+
taskId: task.id,
|
|
793
|
+
projectId: this.scope.projectId,
|
|
794
|
+
action: "activity",
|
|
795
|
+
actor: `a2a:${this.scope.assistantId}`,
|
|
796
|
+
summary: `A2A observed ${task.status.state}`,
|
|
797
|
+
detail: {
|
|
798
|
+
type: "a2a_status_observation",
|
|
799
|
+
state: task.status.state,
|
|
800
|
+
...result === void 0 ? {} : { result },
|
|
801
|
+
...failureReason === void 0 ? {} : { failureReason },
|
|
802
|
+
...task.status.message ? { message: extractText(task.status.message.parts) } : {}
|
|
803
|
+
},
|
|
804
|
+
eventKey: observationEventKey(task, result, failureReason),
|
|
805
|
+
...asString(task.metadata?.threadId) ? { threadId: asString(task.metadata?.threadId) } : {},
|
|
806
|
+
...workspaceId ? { workspaceId } : {}
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
async applyObservation(task, item, result, failureReason) {
|
|
810
|
+
const actor = `a2a:${this.scope.assistantId}`;
|
|
811
|
+
item = await this.refresh(item);
|
|
812
|
+
const threadId = asString(task.metadata?.threadId);
|
|
813
|
+
if (task.status.state === "canceled") {
|
|
814
|
+
await this.requireLifecycleSuccess("cancel", task.id, await this.runBridge.settleRun({
|
|
815
|
+
tenantId: this.scope.tenantId,
|
|
816
|
+
task: item,
|
|
817
|
+
actor,
|
|
818
|
+
...threadId ? { threadId } : {},
|
|
819
|
+
outcome: { kind: "cancelled", summary: "A2A observed canceled" }
|
|
820
|
+
}));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (isFailureState(task.status.state)) {
|
|
824
|
+
await this.requireLifecycleSuccess("fail", task.id, await this.runBridge.settleRun({
|
|
825
|
+
tenantId: this.scope.tenantId,
|
|
826
|
+
task: item,
|
|
827
|
+
actor,
|
|
828
|
+
...threadId ? { threadId } : {},
|
|
829
|
+
outcome: {
|
|
830
|
+
kind: "failed",
|
|
831
|
+
lifecycleHandled: false,
|
|
832
|
+
error: failureReason?.trim() || `A2A observed ${task.status.state}`
|
|
833
|
+
}
|
|
834
|
+
}));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (task.status.state === "input-required") {
|
|
838
|
+
const observed = asRecord(task.metadata?.interruption);
|
|
839
|
+
const observedType = asString(observed?.type);
|
|
840
|
+
const interruptionType = observedType === "review_required" || observedType === "user_decision" ? "user_decision" : "missing_input";
|
|
841
|
+
const summary = asString(observed?.summary) || (task.status.message ? extractText(task.status.message.parts).trim() : "") || "A2A requires input";
|
|
842
|
+
await this.requireLifecycleSuccess("interrupt", task.id, await this.lifecycle.interruptTask({
|
|
843
|
+
tenantId: this.scope.tenantId,
|
|
844
|
+
taskId: task.id,
|
|
845
|
+
type: interruptionType,
|
|
846
|
+
summary,
|
|
847
|
+
actor,
|
|
848
|
+
...threadId ? { threadId } : {}
|
|
849
|
+
}));
|
|
850
|
+
if (observedType === "review_required" && observed) {
|
|
851
|
+
await this.preserveObservedInterruption(task.id, observed);
|
|
852
|
+
}
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
await this.requireLifecycleSuccess("complete", task.id, await this.lifecycle.completeAgentTask({
|
|
856
|
+
tenantId: this.scope.tenantId,
|
|
857
|
+
taskId: task.id,
|
|
858
|
+
result: result?.trim() || "(no output)",
|
|
859
|
+
beliefImpact: [],
|
|
860
|
+
actor,
|
|
861
|
+
...threadId ? { threadId } : {}
|
|
862
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Preserve a review interruption's canonical type/metadata after the generic
|
|
866
|
+
* lifecycle interrupt (which only models missing_input/user_decision).
|
|
867
|
+
*/
|
|
868
|
+
async preserveObservedInterruption(taskId, observed) {
|
|
869
|
+
const item = await this.taskStore.getById(this.scope.tenantId, taskId);
|
|
870
|
+
if (!item) return;
|
|
871
|
+
const context = item.context ?? {};
|
|
872
|
+
const current = asRecord(context.interruption) ?? {};
|
|
873
|
+
await this.taskStore.update(this.scope.tenantId, taskId, {
|
|
874
|
+
context: {
|
|
875
|
+
...context,
|
|
876
|
+
interruption: { ...current, ...observed, type: "review_required" }
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
async requireLifecycleSuccess(operation, taskId, result) {
|
|
881
|
+
if (result.success) {
|
|
882
|
+
if (result.warnings?.length) {
|
|
883
|
+
console.warn({ event: "a2a:lifecycle:warnings", operation, taskId, warnings: result.warnings });
|
|
884
|
+
}
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
throw new Error(`A2A lifecycle ${operation} failed: ${result.code}: ${result.error}`);
|
|
888
|
+
}
|
|
889
|
+
isExactInterruptionReplay(task, item) {
|
|
890
|
+
const observed = task.metadata?.interruption;
|
|
891
|
+
const canonical = item.context?.interruption;
|
|
892
|
+
if (item.status !== "interrupted" || !canonical || typeof canonical !== "object") return false;
|
|
893
|
+
if (observed && typeof observed === "object") return isDeepStrictEqual(observed, canonical);
|
|
894
|
+
const observedSummary = task.status.message ? extractText(task.status.message.parts).trim() : "";
|
|
895
|
+
return observedSummary.length > 0 && canonical.summary === observedSummary;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Restore an SDK task from the stored snapshot, overlaying the current
|
|
899
|
+
* `TaskItem.status` onto `task.status.state`. Returns `undefined` when the
|
|
900
|
+
* task is unknown to the tenant or has no A2A snapshot.
|
|
901
|
+
*/
|
|
902
|
+
async load(taskId) {
|
|
903
|
+
const item = await this.taskStore.getById(this.scope.tenantId, taskId);
|
|
904
|
+
if (!item || !this.isInScope(item)) return void 0;
|
|
905
|
+
const pageSize = 100;
|
|
906
|
+
let selected;
|
|
907
|
+
for (let offset = 0; ; offset += pageSize) {
|
|
908
|
+
const activities = await this.workItemStore.list({
|
|
909
|
+
tenantId: this.scope.tenantId,
|
|
910
|
+
taskId,
|
|
911
|
+
action: "activity",
|
|
912
|
+
order: "desc",
|
|
913
|
+
limit: pageSize,
|
|
914
|
+
offset
|
|
915
|
+
});
|
|
916
|
+
for (const activity of activities) {
|
|
917
|
+
if (activity.detail?.type !== "a2a_task_snapshot" || !isA2ATaskSnapshot(activity.detail.task)) continue;
|
|
918
|
+
const sequence = snapshotSequence(activity.detail.snapshotSequence) ?? snapshotSequence(activity.detail.task.metadata?.snapshotSequence);
|
|
919
|
+
const candidate = {
|
|
920
|
+
task: activity.detail.task,
|
|
921
|
+
sequence,
|
|
922
|
+
eventKey: asString(activity.detail.eventKey) ?? activity.eventKey ?? snapshotEventKey(activity.detail.task),
|
|
923
|
+
statusTimestamp: Date.parse(activity.detail.task.status.timestamp ?? ""),
|
|
924
|
+
createdAt: activity.createdAt,
|
|
925
|
+
id: activity.id
|
|
926
|
+
};
|
|
927
|
+
if (!selected || this.isLaterSnapshot(candidate, selected)) selected = candidate;
|
|
928
|
+
}
|
|
929
|
+
if (activities.length < pageSize) break;
|
|
930
|
+
}
|
|
931
|
+
const snapshot = selected?.task ?? item.context?.a2aTask;
|
|
932
|
+
if (!isA2ATaskSnapshot(snapshot)) return void 0;
|
|
933
|
+
const restoredSequence = selected?.sequence ?? snapshotSequence(snapshot.metadata?.snapshotSequence);
|
|
934
|
+
if (restoredSequence !== void 0) {
|
|
935
|
+
this.snapshotSequences.set(taskId, Math.max(this.snapshotSequences.get(taskId) ?? 0, restoredSequence));
|
|
936
|
+
}
|
|
937
|
+
const state = mapTaskStatusToA2AState(item.status);
|
|
938
|
+
const interruption = item.context?.interruption;
|
|
939
|
+
const contextRunId = asString(item.context?.a2aRunId);
|
|
940
|
+
const contextThreadId = asString(item.context?.threadId);
|
|
941
|
+
const metadata = {
|
|
942
|
+
...snapshot.metadata ?? {},
|
|
943
|
+
...contextRunId === void 0 ? {} : { a2aRunId: contextRunId },
|
|
944
|
+
...contextThreadId === void 0 ? {} : { threadId: contextThreadId },
|
|
945
|
+
...interruption && typeof interruption === "object" ? { interruption } : {}
|
|
946
|
+
};
|
|
947
|
+
const nonResultArtifacts = (snapshot.artifacts ?? []).filter((artifact) => artifact.name !== "result");
|
|
948
|
+
const artifacts = item.status === "completed" && item.result?.trim() ? [...nonResultArtifacts, {
|
|
949
|
+
artifactId: `${item.id}:result`,
|
|
950
|
+
name: "result",
|
|
951
|
+
parts: [{ kind: "text", text: item.result }]
|
|
952
|
+
}] : nonResultArtifacts;
|
|
953
|
+
const message = state === "failed" && item.failureReason?.trim() ? {
|
|
954
|
+
kind: "message",
|
|
955
|
+
messageId: `${item.id}:failure`,
|
|
956
|
+
role: "agent",
|
|
957
|
+
parts: [{ kind: "text", text: item.failureReason }]
|
|
958
|
+
} : interruption && typeof interruption === "object" && typeof interruption.summary === "string" ? {
|
|
959
|
+
kind: "message",
|
|
960
|
+
messageId: `${item.id}:interruption`,
|
|
961
|
+
role: "agent",
|
|
962
|
+
parts: [{ kind: "text", text: interruption.summary }]
|
|
963
|
+
} : snapshot.status.message;
|
|
964
|
+
return {
|
|
965
|
+
...snapshot,
|
|
966
|
+
metadata,
|
|
967
|
+
...artifacts.length > 0 ? { artifacts } : { artifacts: void 0 },
|
|
968
|
+
status: {
|
|
969
|
+
...snapshot.status,
|
|
970
|
+
state,
|
|
971
|
+
...message ? { message } : {}
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
isInScope(item) {
|
|
976
|
+
return item.tenantId === this.scope.tenantId && item.projectId === this.scope.projectId && item.ownerType === "agent" && item.ownerId === this.scope.assistantId && item.sourceId === "a2a";
|
|
977
|
+
}
|
|
978
|
+
requireScope(item, taskId) {
|
|
979
|
+
if (!this.isInScope(item)) throw new A2ATaskScopeConflictError(taskId);
|
|
980
|
+
}
|
|
981
|
+
isLaterSnapshot(candidate, selected) {
|
|
982
|
+
if (candidate.sequence !== void 0 || selected.sequence !== void 0) {
|
|
983
|
+
if (candidate.sequence === void 0) return false;
|
|
984
|
+
if (selected.sequence === void 0) return true;
|
|
985
|
+
if (candidate.sequence !== selected.sequence) return candidate.sequence > selected.sequence;
|
|
986
|
+
const candidateTimestamp = Number.isNaN(candidate.statusTimestamp) ? Number.NEGATIVE_INFINITY : candidate.statusTimestamp;
|
|
987
|
+
const selectedTimestamp = Number.isNaN(selected.statusTimestamp) ? Number.NEGATIVE_INFINITY : selected.statusTimestamp;
|
|
988
|
+
if (candidateTimestamp !== selectedTimestamp) return candidateTimestamp > selectedTimestamp;
|
|
989
|
+
return candidate.eventKey > selected.eventKey;
|
|
990
|
+
}
|
|
991
|
+
return candidate.createdAt.getTime() > selected.createdAt.getTime() || candidate.createdAt.getTime() === selected.createdAt.getTime() && candidate.id > selected.id;
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
function isA2AConcurrentWrite(error) {
|
|
995
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
996
|
+
return message.includes("TASK_STATUS_CONFLICT") || message.includes("TASK_STORE_WRITE_FAILED") || message.includes("Task changed concurrently");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/services/a2a/AxiomAgentExecutor.ts
|
|
1000
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1001
|
+
import { agentInstanceManager, isOfficialInterruptId } from "@axiom-lattice/core";
|
|
1002
|
+
import { MessageChunkTypes as MessageChunkTypes2 } from "@axiom-lattice/protocols";
|
|
1003
|
+
import { A2AError as A2AError2 } from "@a2a-js/sdk/server";
|
|
1004
|
+
|
|
1005
|
+
// src/services/a2a/a2aFileRefs.ts
|
|
1006
|
+
function filePartsToRefs(parts, addedBy) {
|
|
1007
|
+
const refs = [];
|
|
1008
|
+
for (const part of parts) {
|
|
1009
|
+
if (part.kind !== "file") continue;
|
|
1010
|
+
const file = part.file;
|
|
1011
|
+
if ("bytes" in file) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
"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"
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
refs.push({
|
|
1017
|
+
uri: file.uri,
|
|
1018
|
+
...file.name ? { name: file.name } : {},
|
|
1019
|
+
...file.mimeType ? { mimeType: file.mimeType } : {},
|
|
1020
|
+
addedBy
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return refs;
|
|
1024
|
+
}
|
|
1025
|
+
async function filePartsToRefsWithIngest(parts, addedBy, ingestor, dedupeKeyBase, projectId) {
|
|
1026
|
+
if (!ingestor) {
|
|
1027
|
+
return filePartsToRefs(parts, addedBy);
|
|
1028
|
+
}
|
|
1029
|
+
const refs = [];
|
|
1030
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
1031
|
+
const part = parts[index];
|
|
1032
|
+
if (part.kind !== "file") continue;
|
|
1033
|
+
const dedupeKey = `${dedupeKeyBase}:${index}`;
|
|
1034
|
+
refs.push(await ingestor.ingest(part, dedupeKey, projectId, addedBy));
|
|
1035
|
+
}
|
|
1036
|
+
return refs;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/services/a2a/A2ARunService.ts
|
|
1040
|
+
import { randomUUID } from "crypto";
|
|
1041
|
+
import {
|
|
1042
|
+
extractAgentRunInputMessageIds
|
|
1043
|
+
} from "@axiom-lattice/core";
|
|
1044
|
+
import { MessageChunkTypes } from "@axiom-lattice/protocols";
|
|
1045
|
+
var TERMINAL_STREAM_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
1046
|
+
MessageChunkTypes.MESSAGE_COMPLETED,
|
|
1047
|
+
MessageChunkTypes.MESSAGE_FAILED,
|
|
1048
|
+
MessageChunkTypes.INTERRUPT
|
|
1049
|
+
]);
|
|
1050
|
+
async function* stopAtTerminalChunks(source) {
|
|
1051
|
+
for await (const chunk of source) {
|
|
1052
|
+
yield chunk;
|
|
1053
|
+
if (TERMINAL_STREAM_CHUNK_TYPES.has(chunk.type)) return;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
var A2ARunNotFoundError = class extends Error {
|
|
1057
|
+
constructor(runId) {
|
|
1058
|
+
super(`A2A run not found: ${runId}`);
|
|
1059
|
+
this.runId = runId;
|
|
1060
|
+
/** Stable error code for API/protocol mapping. */
|
|
1061
|
+
this.code = "A2A_RUN_NOT_FOUND";
|
|
1062
|
+
this.name = "A2ARunNotFoundError";
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
var TERMINAL_STATUSES = /* @__PURE__ */ new Set([
|
|
1066
|
+
"success",
|
|
1067
|
+
"error",
|
|
1068
|
+
"interrupted",
|
|
1069
|
+
"cancelled",
|
|
1070
|
+
"timeout"
|
|
1071
|
+
]);
|
|
1072
|
+
function isTerminal(status) {
|
|
1073
|
+
return TERMINAL_STATUSES.has(status);
|
|
1074
|
+
}
|
|
1075
|
+
function mapStatus(status) {
|
|
1076
|
+
switch (status) {
|
|
1077
|
+
case "pending":
|
|
1078
|
+
case "running":
|
|
1079
|
+
case "success":
|
|
1080
|
+
case "error":
|
|
1081
|
+
case "interrupted":
|
|
1082
|
+
case "timeout":
|
|
1083
|
+
return status;
|
|
1084
|
+
case "cancelled":
|
|
1085
|
+
return "interrupted";
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function messageIdFromRun(run) {
|
|
1089
|
+
if (typeof run.input?.id === "string" && run.input.id.length > 0) {
|
|
1090
|
+
return run.input.id;
|
|
1091
|
+
}
|
|
1092
|
+
if (run.inputMessageIds.length > 0) return run.inputMessageIds[0];
|
|
1093
|
+
return run.runId;
|
|
1094
|
+
}
|
|
1095
|
+
var A2ARunService = class {
|
|
1096
|
+
constructor(deps) {
|
|
1097
|
+
this.ledger = deps.ledger;
|
|
1098
|
+
this.chunkSource = deps.chunkSource;
|
|
1099
|
+
this.wake = deps.wake ?? wakeRunTaskQueue;
|
|
1100
|
+
this.getLatestCursor = deps.getLatestCursor;
|
|
1101
|
+
this.getRunCancelRouter = deps.getRunCancelRouter ?? (() => getRunCancelRouter());
|
|
1102
|
+
this.idFactory = deps.idFactory ?? randomUUID;
|
|
1103
|
+
this.now = deps.now ?? Date.now;
|
|
1104
|
+
this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1105
|
+
this.pollIntervalMs = deps.pollIntervalMs ?? 200;
|
|
1106
|
+
this.joinTimeoutMs = deps.joinTimeoutMs ?? 3e4;
|
|
1107
|
+
this.cancelTimeoutMs = deps.cancelTimeoutMs ?? 1e4;
|
|
1108
|
+
this.maxActiveRunsForTenant = deps.maxActiveRunsForTenant;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Creates exactly one pending `run_api` row for an A2A request.
|
|
1112
|
+
*
|
|
1113
|
+
* @param input Tenant/project/agent scope, A2A ids, and message or command.
|
|
1114
|
+
* @returns The run id, chunk-stream anchor/cursor, and pending status.
|
|
1115
|
+
* @remarks The stream cursor is captured before the row is committed so
|
|
1116
|
+
* already-emitted chunks cannot be lost. The queue wake is best-effort and
|
|
1117
|
+
* never awaited: a hung wake must not delay the protocol response.
|
|
1118
|
+
*/
|
|
1119
|
+
async create(input) {
|
|
1120
|
+
const streamCursor = await this.captureLatestCursor(
|
|
1121
|
+
input.threadId,
|
|
1122
|
+
input.assistantId
|
|
1123
|
+
);
|
|
1124
|
+
const runId = this.idFactory();
|
|
1125
|
+
const hasCommand = input.command !== void 0;
|
|
1126
|
+
const messageId = hasCommand ? runId : input.message.id?.trim() || this.idFactory();
|
|
1127
|
+
const runInput = hasCommand ? null : { id: messageId, message: input.message.content };
|
|
1128
|
+
await this.ledger.createPendingRun({
|
|
1129
|
+
runId,
|
|
1130
|
+
tenantId: input.tenantId,
|
|
1131
|
+
...input.workspaceId === void 0 ? {} : { workspaceId: input.workspaceId },
|
|
1132
|
+
projectId: input.projectId,
|
|
1133
|
+
assistantId: input.assistantId,
|
|
1134
|
+
threadId: input.threadId,
|
|
1135
|
+
input: runInput,
|
|
1136
|
+
command: input.command ?? null,
|
|
1137
|
+
config: input.config ?? null,
|
|
1138
|
+
metadata: {
|
|
1139
|
+
...input.metadata ?? {},
|
|
1140
|
+
a2aTaskId: input.a2aTaskId,
|
|
1141
|
+
a2aContextId: input.threadId
|
|
1142
|
+
},
|
|
1143
|
+
inputMessageIds: runInput ? extractAgentRunInputMessageIds(runInput) : [],
|
|
1144
|
+
...this.maxActiveRunsForTenant === void 0 ? {} : { maxActiveRunsForTenant: this.maxActiveRunsForTenant }
|
|
1145
|
+
});
|
|
1146
|
+
this.wakeSafely();
|
|
1147
|
+
return {
|
|
1148
|
+
runId,
|
|
1149
|
+
messageId,
|
|
1150
|
+
...streamCursor === void 0 ? {} : { streamCursor },
|
|
1151
|
+
status: "pending",
|
|
1152
|
+
terminal: false
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
/**
|
|
1156
|
+
* Waits for a `run_api` run to reach a terminal lifecycle state.
|
|
1157
|
+
*
|
|
1158
|
+
* @param timeoutMs Blocking budget (default {@link joinTimeoutMs}).
|
|
1159
|
+
* @returns The terminal handle, or a non-terminal `timeout` handle when the
|
|
1160
|
+
* budget ends.
|
|
1161
|
+
* @throws {A2ARunNotFoundError} When the run is absent, legacy-sourced, or out
|
|
1162
|
+
* of scope.
|
|
1163
|
+
*/
|
|
1164
|
+
async join(tenantId, threadId, assistantId, runId, timeoutMs) {
|
|
1165
|
+
const budget = timeoutMs ?? this.joinTimeoutMs;
|
|
1166
|
+
const deadline = this.now() + Math.max(0, budget);
|
|
1167
|
+
for (; ; ) {
|
|
1168
|
+
const run = await this.readScoped(tenantId, threadId, assistantId, runId);
|
|
1169
|
+
if (!run) throw new A2ARunNotFoundError(runId);
|
|
1170
|
+
if (isTerminal(run.status)) return this.toHandle(run);
|
|
1171
|
+
const remaining = deadline - this.now();
|
|
1172
|
+
if (remaining <= 0) {
|
|
1173
|
+
return { ...this.toHandle(run), status: "timeout", terminal: false };
|
|
1174
|
+
}
|
|
1175
|
+
await this.sleep(Math.max(1, Math.min(this.pollIntervalMs, remaining)));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Requests cancellation of one exact `run_api` run and waits (bounded) for
|
|
1180
|
+
* terminal state.
|
|
1181
|
+
*
|
|
1182
|
+
* @throws {A2ARunNotFoundError} When the run is absent, legacy-sourced, or out
|
|
1183
|
+
* of scope.
|
|
1184
|
+
*/
|
|
1185
|
+
async cancel(tenantId, threadId, assistantId, runId) {
|
|
1186
|
+
const run = await this.readScoped(tenantId, threadId, assistantId, runId);
|
|
1187
|
+
if (!run) throw new A2ARunNotFoundError(runId);
|
|
1188
|
+
if (isTerminal(run.status)) return;
|
|
1189
|
+
const router = this.getRunCancelRouter();
|
|
1190
|
+
let result;
|
|
1191
|
+
if (router) {
|
|
1192
|
+
result = await router.requestCancelById({
|
|
1193
|
+
tenantId,
|
|
1194
|
+
assistantId,
|
|
1195
|
+
threadId,
|
|
1196
|
+
runId,
|
|
1197
|
+
reason: "a2a.cancel"
|
|
1198
|
+
});
|
|
1199
|
+
} else {
|
|
1200
|
+
result = await this.ledger.requestCancel(tenantId, runId);
|
|
1201
|
+
}
|
|
1202
|
+
if (result === "not_found") throw new A2ARunNotFoundError(runId);
|
|
1203
|
+
if (result === "terminal") return;
|
|
1204
|
+
await this.waitForTerminal(tenantId, threadId, assistantId, runId);
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Attaches to the chunk stream for a run's message id.
|
|
1208
|
+
*
|
|
1209
|
+
* @param cursor Thread-stream position from the run handle. When supplied and
|
|
1210
|
+
* supported, the cursor-backed thread stream is used instead of message-id
|
|
1211
|
+
* anchoring (required for command runs).
|
|
1212
|
+
* @param scope Optional exact `run_api` scope guard, validated on first read.
|
|
1213
|
+
*/
|
|
1214
|
+
stream(threadId, assistantId, messageId, cursor, scope) {
|
|
1215
|
+
const useCursorStream = typeof cursor === "string" && cursor.trim() !== "" && this.chunkSource.streamThread !== void 0;
|
|
1216
|
+
const createSource = () => useCursorStream ? this.chunkSource.streamThread(threadId, assistantId, cursor) : this.chunkSource.stream(threadId, assistantId, messageId);
|
|
1217
|
+
if (!scope) return createSource();
|
|
1218
|
+
return this.guardStream(
|
|
1219
|
+
createSource,
|
|
1220
|
+
threadId,
|
|
1221
|
+
assistantId,
|
|
1222
|
+
scope
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Reads the authoritative `run_api` ledger record.
|
|
1227
|
+
*
|
|
1228
|
+
* @param threadId Optional scope guard; legacy runs are always rejected.
|
|
1229
|
+
* @param assistantId Optional scope guard.
|
|
1230
|
+
*/
|
|
1231
|
+
async getRun(tenantId, runId, threadId, assistantId) {
|
|
1232
|
+
const run = await this.ledger.getRun(tenantId, runId);
|
|
1233
|
+
if (!run || run.source !== "run_api") return void 0;
|
|
1234
|
+
if (run.tenantId !== tenantId) return void 0;
|
|
1235
|
+
if (threadId !== void 0 && run.threadId !== threadId) return void 0;
|
|
1236
|
+
if (assistantId !== void 0 && run.assistantId !== assistantId) {
|
|
1237
|
+
return void 0;
|
|
1238
|
+
}
|
|
1239
|
+
return run;
|
|
1240
|
+
}
|
|
1241
|
+
async waitForTerminal(tenantId, threadId, assistantId, runId) {
|
|
1242
|
+
const deadline = this.now() + this.cancelTimeoutMs;
|
|
1243
|
+
for (; ; ) {
|
|
1244
|
+
const run = await this.readScoped(tenantId, threadId, assistantId, runId);
|
|
1245
|
+
if (!run || isTerminal(run.status)) return;
|
|
1246
|
+
const remaining = deadline - this.now();
|
|
1247
|
+
if (remaining <= 0) return;
|
|
1248
|
+
await this.sleep(Math.max(1, Math.min(this.pollIntervalMs, remaining)));
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
async readScoped(tenantId, threadId, assistantId, runId) {
|
|
1252
|
+
const run = await this.ledger.getRun(tenantId, runId);
|
|
1253
|
+
if (!run || run.source !== "run_api" || run.tenantId !== tenantId || run.threadId !== threadId || run.assistantId !== assistantId) {
|
|
1254
|
+
return void 0;
|
|
1255
|
+
}
|
|
1256
|
+
return run;
|
|
1257
|
+
}
|
|
1258
|
+
toHandle(run) {
|
|
1259
|
+
return {
|
|
1260
|
+
runId: run.runId,
|
|
1261
|
+
messageId: messageIdFromRun(run),
|
|
1262
|
+
status: mapStatus(run.status),
|
|
1263
|
+
terminal: isTerminal(run.status)
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async captureLatestCursor(threadId, assistantId) {
|
|
1267
|
+
if (!this.getLatestCursor) return void 0;
|
|
1268
|
+
try {
|
|
1269
|
+
const cursor = await this.getLatestCursor(threadId, assistantId);
|
|
1270
|
+
if (typeof cursor !== "string" || cursor.trim() === "") return void 0;
|
|
1271
|
+
return cursor;
|
|
1272
|
+
} catch {
|
|
1273
|
+
return void 0;
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Lazily verifies the exact `run_api` scope before opening the chunk source.
|
|
1278
|
+
* Validation is deferred so `stream()` remains synchronous and an unused
|
|
1279
|
+
* stream never touches the ledger.
|
|
1280
|
+
*/
|
|
1281
|
+
guardStream(createSource, threadId, assistantId, scope) {
|
|
1282
|
+
return {
|
|
1283
|
+
[Symbol.asyncIterator]: () => {
|
|
1284
|
+
let validation;
|
|
1285
|
+
let inner;
|
|
1286
|
+
const validate = () => {
|
|
1287
|
+
validation ?? (validation = (async () => {
|
|
1288
|
+
const run = await this.readScoped(
|
|
1289
|
+
scope.tenantId,
|
|
1290
|
+
threadId,
|
|
1291
|
+
assistantId,
|
|
1292
|
+
scope.runId
|
|
1293
|
+
);
|
|
1294
|
+
if (!run) throw new A2ARunNotFoundError(scope.runId);
|
|
1295
|
+
})());
|
|
1296
|
+
return validation;
|
|
1297
|
+
};
|
|
1298
|
+
const source = () => {
|
|
1299
|
+
inner ?? (inner = createSource()[Symbol.asyncIterator]());
|
|
1300
|
+
return inner;
|
|
1301
|
+
};
|
|
1302
|
+
const settleValidation = async () => {
|
|
1303
|
+
if (!validation) return;
|
|
1304
|
+
try {
|
|
1305
|
+
await validation;
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
return {
|
|
1310
|
+
next: async () => {
|
|
1311
|
+
await validate();
|
|
1312
|
+
return source().next();
|
|
1313
|
+
},
|
|
1314
|
+
return: async (value) => {
|
|
1315
|
+
await settleValidation();
|
|
1316
|
+
if (!inner) return { done: true, value };
|
|
1317
|
+
return inner.return ? inner.return(value) : { done: true, value };
|
|
1318
|
+
},
|
|
1319
|
+
throw: async (error) => {
|
|
1320
|
+
await settleValidation();
|
|
1321
|
+
if (!inner) throw error;
|
|
1322
|
+
return inner.throw ? inner.throw(error) : Promise.reject(error);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
wakeSafely() {
|
|
1329
|
+
try {
|
|
1330
|
+
void this.wake().catch(() => void 0);
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
// src/services/a2a/AxiomAgentExecutor.ts
|
|
1337
|
+
var A2ABlockingDeadlineError = class extends Error {
|
|
1338
|
+
constructor() {
|
|
1339
|
+
super("A2A blocking execution deadline expired");
|
|
1340
|
+
this.name = "A2ABlockingDeadlineError";
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
var TERMINAL_A2A_STATES = /* @__PURE__ */ new Set([
|
|
1344
|
+
"completed",
|
|
1345
|
+
"failed",
|
|
1346
|
+
"canceled",
|
|
1347
|
+
"rejected"
|
|
1348
|
+
]);
|
|
1349
|
+
function isoNow() {
|
|
1350
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1351
|
+
}
|
|
1352
|
+
function asString2(value) {
|
|
1353
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1354
|
+
}
|
|
1355
|
+
function asRecord2(value) {
|
|
1356
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1357
|
+
}
|
|
1358
|
+
function extractText2(parts) {
|
|
1359
|
+
return parts.filter((part) => part.kind === "text").map((part) => part.text).join("\n");
|
|
1360
|
+
}
|
|
1361
|
+
function dataParts(parts) {
|
|
1362
|
+
return parts.filter(
|
|
1363
|
+
(part) => part.kind === "data"
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
function soleOfficialInterruptId(task) {
|
|
1367
|
+
const ids = (task?.artifacts ?? []).filter((artifact) => artifact.name === "interrupt").flatMap((artifact) => artifact.parts).filter((part) => part.kind === "data").map((part) => asString2(part.data.id)).filter((id) => id !== void 0 && isOfficialInterruptId(id));
|
|
1368
|
+
return ids.length === 1 ? ids[0] : void 0;
|
|
1369
|
+
}
|
|
1370
|
+
function agentMessage(text) {
|
|
1371
|
+
return {
|
|
1372
|
+
kind: "message",
|
|
1373
|
+
messageId: uuidv4(),
|
|
1374
|
+
role: "agent",
|
|
1375
|
+
parts: [{ kind: "text", text }]
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function assistantHasGovernedTask(assistant) {
|
|
1379
|
+
const graph = assistant.graphDefinition;
|
|
1380
|
+
const middleware = graph && typeof graph === "object" ? graph.middleware : void 0;
|
|
1381
|
+
const taskMiddleware = Array.isArray(middleware) ? middleware.find((entry) => !!entry && typeof entry === "object" && entry.type === "task" && entry.enabled === true) : void 0;
|
|
1382
|
+
const allowedTools = taskMiddleware?.allowedTools;
|
|
1383
|
+
return !!taskMiddleware && (allowedTools === void 0 || Array.isArray(allowedTools) && (allowedTools.length === 0 || allowedTools.includes("manage_task")));
|
|
1384
|
+
}
|
|
1385
|
+
var AxiomAgentExecutor = class {
|
|
1386
|
+
constructor(options) {
|
|
1387
|
+
this.scope = options.scope;
|
|
1388
|
+
this.assistant = options.assistant;
|
|
1389
|
+
this.taskStore = options.taskStore;
|
|
1390
|
+
this.runService = options.runService;
|
|
1391
|
+
this.ingestor = options.ingestor;
|
|
1392
|
+
this.blocking = options.blocking ?? true;
|
|
1393
|
+
this.getInterrupts = options.getInterrupts ?? ((threadId) => agentInstanceManager.getAgent(this.agentParams(threadId)).getInterrupts());
|
|
1394
|
+
this.joinTimeoutMs = options.joinTimeoutMs ?? 3e4;
|
|
1395
|
+
this.overallTimeoutMs = this.blocking ? options.overallTimeoutMs ?? 10 * 6e4 : Number.POSITIVE_INFINITY;
|
|
1396
|
+
}
|
|
1397
|
+
/** Execute one A2A request through the durable run service. */
|
|
1398
|
+
async execute(ctx, bus) {
|
|
1399
|
+
const existing = ctx.task;
|
|
1400
|
+
const threadId = asString2(existing?.metadata?.threadId) ?? ctx.contextId;
|
|
1401
|
+
const dependencies = ctx.referenceTasks?.map((task) => task.id) ?? [];
|
|
1402
|
+
const resume = this.isResume(existing);
|
|
1403
|
+
const text = extractText2(ctx.userMessage.parts);
|
|
1404
|
+
const governed = this.hasGovernedTaskCapability();
|
|
1405
|
+
const resumeCanonicalTask = resume && !governed;
|
|
1406
|
+
if (existing && !resume) {
|
|
1407
|
+
throw A2AError2.invalidParams(
|
|
1408
|
+
`Task ${existing.id} is not waiting for input and cannot be resumed`
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (!this.taskStore) {
|
|
1412
|
+
const initialTask2 = this.taskFor(ctx, threadId, [], dependencies, {
|
|
1413
|
+
handle: void 0,
|
|
1414
|
+
resume: false
|
|
1415
|
+
});
|
|
1416
|
+
bus.publish(initialTask2);
|
|
1417
|
+
await this.failTask(ctx, bus, initialTask2, new Error("A2A execution requires a canonical task store"));
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (existing && TERMINAL_A2A_STATES.has(existing.status.state)) {
|
|
1421
|
+
throw A2AError2.invalidParams(
|
|
1422
|
+
`Task ${existing.id} is terminal (${existing.status.state}) and cannot be modified`
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
const interruption = asRecord2(existing?.metadata?.interruption);
|
|
1426
|
+
const reviewInterruption = interruption?.type === "review_required";
|
|
1427
|
+
const command = resume ? this.resumeCommand(
|
|
1428
|
+
ctx.userMessage.parts,
|
|
1429
|
+
text,
|
|
1430
|
+
reviewInterruption,
|
|
1431
|
+
soleOfficialInterruptId(existing)
|
|
1432
|
+
) : void 0;
|
|
1433
|
+
if (reviewInterruption && !command) {
|
|
1434
|
+
bus.publish(agentMessage("Review response must be exactly 'approve', 'reject', or 'reject: <note>'."));
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
if (resume && !command) {
|
|
1438
|
+
throw A2AError2.invalidParams(`Task ${ctx.taskId} resume requires text or one DataPart`);
|
|
1439
|
+
}
|
|
1440
|
+
let inputFiles;
|
|
1441
|
+
try {
|
|
1442
|
+
inputFiles = await filePartsToRefsWithIngest(
|
|
1443
|
+
ctx.userMessage.parts,
|
|
1444
|
+
"user",
|
|
1445
|
+
this.ingestor,
|
|
1446
|
+
ctx.taskId,
|
|
1447
|
+
this.scope.projectId
|
|
1448
|
+
);
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
const initialTask2 = this.taskFor(ctx, threadId, [], dependencies, {
|
|
1451
|
+
handle: void 0,
|
|
1452
|
+
resume: false
|
|
1453
|
+
});
|
|
1454
|
+
await this.taskStore.save(initialTask2);
|
|
1455
|
+
bus.publish(initialTask2);
|
|
1456
|
+
await this.failTask(ctx, bus, initialTask2, err);
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
const requestText = this.requestText(ctx.taskId, text, inputFiles, governed, resume);
|
|
1460
|
+
const runInput = {
|
|
1461
|
+
tenantId: this.scope.tenantId,
|
|
1462
|
+
projectId: this.scope.projectId,
|
|
1463
|
+
...this.scope.workspaceId ? { workspaceId: this.scope.workspaceId } : {},
|
|
1464
|
+
assistantId: this.scope.assistantId,
|
|
1465
|
+
threadId,
|
|
1466
|
+
a2aTaskId: ctx.taskId,
|
|
1467
|
+
message: { id: ctx.userMessage.messageId, role: "user", content: requestText },
|
|
1468
|
+
...command ? { command } : {},
|
|
1469
|
+
config: {
|
|
1470
|
+
taskId: ctx.taskId,
|
|
1471
|
+
execution: { taskId: ctx.taskId }
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
let handle;
|
|
1475
|
+
try {
|
|
1476
|
+
handle = await this.runService.create(runInput);
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
const initialTask2 = this.taskFor(ctx, threadId, inputFiles, dependencies, {
|
|
1479
|
+
handle: void 0,
|
|
1480
|
+
resume: false
|
|
1481
|
+
});
|
|
1482
|
+
await this.taskStore.save(initialTask2);
|
|
1483
|
+
bus.publish(initialTask2);
|
|
1484
|
+
await this.failTask(ctx, bus, initialTask2, err);
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
const projection = {
|
|
1488
|
+
ctx,
|
|
1489
|
+
...existing ? { existing } : {},
|
|
1490
|
+
threadId,
|
|
1491
|
+
inputFiles,
|
|
1492
|
+
dependencies,
|
|
1493
|
+
handle,
|
|
1494
|
+
governed,
|
|
1495
|
+
resume: resumeCanonicalTask
|
|
1496
|
+
};
|
|
1497
|
+
const initialTask = this.taskFor(ctx, threadId, inputFiles, dependencies, {
|
|
1498
|
+
handle,
|
|
1499
|
+
resume: resumeCanonicalTask
|
|
1500
|
+
});
|
|
1501
|
+
await this.taskStore.save(initialTask);
|
|
1502
|
+
bus.publish(initialTask);
|
|
1503
|
+
if (!this.blocking) {
|
|
1504
|
+
void this.projectRun(bus, projection).catch((err) => {
|
|
1505
|
+
console.warn({
|
|
1506
|
+
event: "a2a:run:background_projection_failed",
|
|
1507
|
+
taskId: ctx.taskId,
|
|
1508
|
+
runId: handle.runId,
|
|
1509
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1510
|
+
});
|
|
1511
|
+
});
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
await this.projectRun(bus, projection);
|
|
1515
|
+
}
|
|
1516
|
+
async projectRun(bus, projection) {
|
|
1517
|
+
const { ctx, threadId, handle } = projection;
|
|
1518
|
+
let outputText = "";
|
|
1519
|
+
let streamError;
|
|
1520
|
+
let dropChunks = false;
|
|
1521
|
+
const iterator = this.runService.stream(
|
|
1522
|
+
threadId,
|
|
1523
|
+
this.scope.assistantId,
|
|
1524
|
+
handle.messageId,
|
|
1525
|
+
handle.streamCursor,
|
|
1526
|
+
{ tenantId: this.scope.tenantId, runId: handle.runId }
|
|
1527
|
+
)[Symbol.asyncIterator]();
|
|
1528
|
+
const consume = (async () => {
|
|
1529
|
+
try {
|
|
1530
|
+
for (; ; ) {
|
|
1531
|
+
const next = await iterator.next();
|
|
1532
|
+
if (next.done) return;
|
|
1533
|
+
if (dropChunks) return;
|
|
1534
|
+
const chunk = next.value;
|
|
1535
|
+
const chunkText = chunk.data?.content ?? "";
|
|
1536
|
+
if (chunk.type === MessageChunkTypes2.AI && chunkText) {
|
|
1537
|
+
outputText += chunkText;
|
|
1538
|
+
}
|
|
1539
|
+
if ((chunk.type === MessageChunkTypes2.AI || chunk.type === MessageChunkTypes2.TOOL) && chunkText) {
|
|
1540
|
+
bus.publish({
|
|
1541
|
+
kind: "status-update",
|
|
1542
|
+
taskId: ctx.taskId,
|
|
1543
|
+
contextId: ctx.contextId,
|
|
1544
|
+
status: {
|
|
1545
|
+
state: "working",
|
|
1546
|
+
message: agentMessage(chunkText),
|
|
1547
|
+
timestamp: isoNow()
|
|
1548
|
+
},
|
|
1549
|
+
final: false
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
streamError = err;
|
|
1555
|
+
}
|
|
1556
|
+
})();
|
|
1557
|
+
let terminal;
|
|
1558
|
+
try {
|
|
1559
|
+
terminal = await this.waitForTerminal(handle.runId, threadId);
|
|
1560
|
+
} catch (err) {
|
|
1561
|
+
await iterator.return?.();
|
|
1562
|
+
await consume.catch(() => void 0);
|
|
1563
|
+
const baseTask = this.taskFor(ctx, threadId, projection.inputFiles, projection.dependencies, projection);
|
|
1564
|
+
if (err instanceof A2ABlockingDeadlineError) {
|
|
1565
|
+
try {
|
|
1566
|
+
await this.runService.cancel(
|
|
1567
|
+
this.scope.tenantId,
|
|
1568
|
+
threadId,
|
|
1569
|
+
this.scope.assistantId,
|
|
1570
|
+
handle.runId
|
|
1571
|
+
);
|
|
1572
|
+
} catch (cancelError) {
|
|
1573
|
+
console.warn({
|
|
1574
|
+
event: "a2a:timeout:cancel_failed",
|
|
1575
|
+
taskId: ctx.taskId,
|
|
1576
|
+
runId: handle.runId,
|
|
1577
|
+
error: cancelError instanceof Error ? cancelError.message : String(cancelError)
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
await this.failTask(ctx, bus, baseTask, new Error("A2A run timed out"));
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
await this.failTask(ctx, bus, baseTask, err);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const run = await this.runService.getRun(
|
|
1587
|
+
this.scope.tenantId,
|
|
1588
|
+
handle.runId,
|
|
1589
|
+
threadId,
|
|
1590
|
+
this.scope.assistantId
|
|
1591
|
+
).catch(() => void 0);
|
|
1592
|
+
const runRecord = asRecord2(run);
|
|
1593
|
+
const abortStream = runRecord?.cancelRequested === true || terminal.status === "timeout";
|
|
1594
|
+
if (abortStream) {
|
|
1595
|
+
dropChunks = true;
|
|
1596
|
+
await iterator.return?.();
|
|
1597
|
+
}
|
|
1598
|
+
await consume.catch(() => void 0);
|
|
1599
|
+
if (runRecord?.cancelRequested === true) {
|
|
1600
|
+
await this.finishCanceled(bus, projection);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
switch (terminal.status) {
|
|
1604
|
+
case "success":
|
|
1605
|
+
await this.finishCompleted(bus, projection, outputText);
|
|
1606
|
+
return;
|
|
1607
|
+
case "interrupted":
|
|
1608
|
+
await this.finishInputRequired(bus, projection);
|
|
1609
|
+
return;
|
|
1610
|
+
case "error": {
|
|
1611
|
+
const canonical = this.taskStore ? await this.taskStore.load(ctx.taskId) : void 0;
|
|
1612
|
+
const canonicalMessage = canonical?.status.state === "failed" && canonical.status.message ? extractText2(canonical.status.message.parts).trim() : "";
|
|
1613
|
+
const message = canonicalMessage || asString2(runRecord?.error) || (streamError instanceof Error ? streamError.message : void 0) || "A2A run failed";
|
|
1614
|
+
await this.failTask(
|
|
1615
|
+
ctx,
|
|
1616
|
+
bus,
|
|
1617
|
+
this.taskFor(ctx, threadId, projection.inputFiles, projection.dependencies, projection),
|
|
1618
|
+
new Error(message)
|
|
1619
|
+
);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
case "timeout":
|
|
1623
|
+
await this.failTask(
|
|
1624
|
+
ctx,
|
|
1625
|
+
bus,
|
|
1626
|
+
this.taskFor(ctx, threadId, projection.inputFiles, projection.dependencies, projection),
|
|
1627
|
+
new Error("A2A run timed out")
|
|
1628
|
+
);
|
|
1629
|
+
return;
|
|
1630
|
+
default:
|
|
1631
|
+
await this.failTask(
|
|
1632
|
+
ctx,
|
|
1633
|
+
bus,
|
|
1634
|
+
this.taskFor(ctx, threadId, projection.inputFiles, projection.dependencies, projection),
|
|
1635
|
+
new Error(`A2A run ended in unexpected state ${terminal.status}`)
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
async waitForTerminal(runId, threadId) {
|
|
1640
|
+
const deadline = Date.now() + Math.max(0, this.overallTimeoutMs);
|
|
1641
|
+
for (; ; ) {
|
|
1642
|
+
const remaining = deadline - Date.now();
|
|
1643
|
+
if (remaining <= 0) {
|
|
1644
|
+
throw new A2ABlockingDeadlineError();
|
|
1645
|
+
}
|
|
1646
|
+
const handle = await this.runService.join(
|
|
1647
|
+
this.scope.tenantId,
|
|
1648
|
+
threadId,
|
|
1649
|
+
this.scope.assistantId,
|
|
1650
|
+
runId,
|
|
1651
|
+
Math.min(this.joinTimeoutMs, remaining)
|
|
1652
|
+
);
|
|
1653
|
+
if (handle.terminal) return handle;
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
async finishCompleted(bus, projection, outputText) {
|
|
1657
|
+
const { ctx, threadId, existing } = projection;
|
|
1658
|
+
const canonical = this.taskStore ? await this.taskStore.load(ctx.taskId) : void 0;
|
|
1659
|
+
const canonicalState = canonical?.status.state;
|
|
1660
|
+
if (canonicalState === "failed" || canonicalState === "rejected" || canonicalState === "canceled") {
|
|
1661
|
+
bus.publish({
|
|
1662
|
+
kind: "status-update",
|
|
1663
|
+
taskId: ctx.taskId,
|
|
1664
|
+
contextId: ctx.contextId,
|
|
1665
|
+
status: {
|
|
1666
|
+
state: canonicalState === "canceled" ? "canceled" : "failed",
|
|
1667
|
+
...canonical?.status.message ? { message: canonical.status.message } : {},
|
|
1668
|
+
timestamp: isoNow()
|
|
1669
|
+
},
|
|
1670
|
+
final: true
|
|
1671
|
+
});
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const canonicalResult = canonical?.artifacts?.find((artifact) => artifact.name === "result");
|
|
1675
|
+
const canonicalText = canonicalResult ? extractText2(canonicalResult.parts).trim() : "";
|
|
1676
|
+
const resultText = canonicalText || outputText;
|
|
1677
|
+
const resultArtifact = {
|
|
1678
|
+
artifactId: uuidv4(),
|
|
1679
|
+
name: "result",
|
|
1680
|
+
parts: [{ kind: "text", text: resultText }]
|
|
1681
|
+
};
|
|
1682
|
+
const completed = {
|
|
1683
|
+
...this.taskFor(ctx, threadId, projection.inputFiles, projection.dependencies, projection),
|
|
1684
|
+
artifacts: [...existing?.artifacts ?? [], resultArtifact],
|
|
1685
|
+
status: { state: "completed", timestamp: isoNow() }
|
|
1686
|
+
};
|
|
1687
|
+
if (this.taskStore) await this.taskStore.save(completed);
|
|
1688
|
+
bus.publish({
|
|
1689
|
+
kind: "artifact-update",
|
|
1690
|
+
taskId: ctx.taskId,
|
|
1691
|
+
contextId: ctx.contextId,
|
|
1692
|
+
artifact: resultArtifact
|
|
1693
|
+
});
|
|
1694
|
+
bus.publish({
|
|
1695
|
+
kind: "status-update",
|
|
1696
|
+
taskId: ctx.taskId,
|
|
1697
|
+
contextId: ctx.contextId,
|
|
1698
|
+
status: { state: "completed", timestamp: isoNow() },
|
|
1699
|
+
final: true
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
async finishInputRequired(bus, projection) {
|
|
1703
|
+
const { ctx, threadId } = projection;
|
|
1704
|
+
let interrupts = [];
|
|
1705
|
+
try {
|
|
1706
|
+
interrupts = await this.getInterrupts(threadId) ?? [];
|
|
1707
|
+
} catch (err) {
|
|
1708
|
+
console.warn({
|
|
1709
|
+
event: "a2a:interrupts:read_failed",
|
|
1710
|
+
taskId: ctx.taskId,
|
|
1711
|
+
threadId,
|
|
1712
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
const canonical = this.taskStore ? await this.taskStore.load(ctx.taskId) : void 0;
|
|
1716
|
+
const summary = canonical?.status.message ? extractText2(canonical.status.message.parts).trim() : "";
|
|
1717
|
+
const interruptArtifact = interrupts.length === 0 ? void 0 : {
|
|
1718
|
+
artifactId: `${ctx.taskId}:interrupt`,
|
|
1719
|
+
name: "interrupt",
|
|
1720
|
+
parts: interrupts.map((interrupt) => {
|
|
1721
|
+
const payload = asRecord2(interrupt.payload);
|
|
1722
|
+
const schema = asRecord2(payload?.response_schema) ?? payload?.response_schema;
|
|
1723
|
+
return {
|
|
1724
|
+
kind: "data",
|
|
1725
|
+
data: {
|
|
1726
|
+
id: interrupt.interruptId,
|
|
1727
|
+
value: interrupt.payload,
|
|
1728
|
+
...schema === void 0 ? {} : { response_schema: schema }
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
})
|
|
1732
|
+
};
|
|
1733
|
+
const baseTask = this.taskFor(
|
|
1734
|
+
ctx,
|
|
1735
|
+
threadId,
|
|
1736
|
+
projection.inputFiles,
|
|
1737
|
+
projection.dependencies,
|
|
1738
|
+
projection
|
|
1739
|
+
);
|
|
1740
|
+
const pending = {
|
|
1741
|
+
...baseTask,
|
|
1742
|
+
...interruptArtifact ? {
|
|
1743
|
+
artifacts: [
|
|
1744
|
+
...(baseTask.artifacts ?? []).filter(
|
|
1745
|
+
(artifact) => artifact.artifactId !== interruptArtifact.artifactId
|
|
1746
|
+
),
|
|
1747
|
+
interruptArtifact
|
|
1748
|
+
]
|
|
1749
|
+
} : {},
|
|
1750
|
+
status: {
|
|
1751
|
+
state: "input-required",
|
|
1752
|
+
...summary ? { message: agentMessage(summary) } : {},
|
|
1753
|
+
timestamp: isoNow()
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
if (this.taskStore) await this.taskStore.save(pending);
|
|
1757
|
+
if (interruptArtifact) {
|
|
1758
|
+
bus.publish({
|
|
1759
|
+
kind: "artifact-update",
|
|
1760
|
+
taskId: ctx.taskId,
|
|
1761
|
+
contextId: ctx.contextId,
|
|
1762
|
+
artifact: interruptArtifact
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
bus.publish({
|
|
1766
|
+
kind: "status-update",
|
|
1767
|
+
taskId: ctx.taskId,
|
|
1768
|
+
contextId: ctx.contextId,
|
|
1769
|
+
status: {
|
|
1770
|
+
state: "input-required",
|
|
1771
|
+
...summary ? { message: agentMessage(summary) } : {},
|
|
1772
|
+
timestamp: isoNow()
|
|
1773
|
+
},
|
|
1774
|
+
final: true
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
async finishCanceled(bus, projection) {
|
|
1778
|
+
await this.persistCanceled(projection.ctx.taskId, projection.existing, projection);
|
|
1779
|
+
bus.publish({
|
|
1780
|
+
kind: "status-update",
|
|
1781
|
+
taskId: projection.ctx.taskId,
|
|
1782
|
+
contextId: projection.ctx.contextId,
|
|
1783
|
+
status: { state: "canceled", timestamp: isoNow() },
|
|
1784
|
+
final: true
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
async failTask(ctx, bus, baseTask, err) {
|
|
1788
|
+
const failedTask = {
|
|
1789
|
+
...baseTask,
|
|
1790
|
+
status: {
|
|
1791
|
+
state: "failed",
|
|
1792
|
+
message: agentMessage(err instanceof Error ? err.message : String(err)),
|
|
1793
|
+
timestamp: isoNow()
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
if (this.taskStore) {
|
|
1797
|
+
try {
|
|
1798
|
+
await this.taskStore.save(failedTask);
|
|
1799
|
+
} catch (saveError) {
|
|
1800
|
+
console.warn({
|
|
1801
|
+
event: "a2a:settle:save_failed",
|
|
1802
|
+
taskId: ctx.taskId,
|
|
1803
|
+
error: saveError instanceof Error ? saveError.message : String(saveError)
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
bus.publish({
|
|
1808
|
+
kind: "status-update",
|
|
1809
|
+
taskId: ctx.taskId,
|
|
1810
|
+
contextId: ctx.contextId,
|
|
1811
|
+
status: failedTask.status,
|
|
1812
|
+
final: true
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
taskFor(ctx, threadId, inputFiles, dependencies, projection) {
|
|
1816
|
+
const existing = ctx.task;
|
|
1817
|
+
const metadata = {
|
|
1818
|
+
...existing?.metadata ?? {},
|
|
1819
|
+
threadId,
|
|
1820
|
+
dependencies,
|
|
1821
|
+
inputFiles,
|
|
1822
|
+
...projection?.handle ? { a2aRunId: projection.handle.runId } : {},
|
|
1823
|
+
...projection?.handle?.messageId !== void 0 ? { a2aMessageId: projection.handle.messageId } : {},
|
|
1824
|
+
...projection?.resume ? { a2aResume: true } : {}
|
|
1825
|
+
};
|
|
1826
|
+
if (projection?.handle) {
|
|
1827
|
+
if (projection.handle.streamCursor !== void 0) {
|
|
1828
|
+
metadata.a2aStreamCursor = projection.handle.streamCursor;
|
|
1829
|
+
} else {
|
|
1830
|
+
delete metadata.a2aStreamCursor;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return {
|
|
1834
|
+
kind: "task",
|
|
1835
|
+
id: ctx.taskId,
|
|
1836
|
+
contextId: ctx.contextId,
|
|
1837
|
+
...existing?.history ? { history: existing.history } : {},
|
|
1838
|
+
...existing?.artifacts ? { artifacts: existing.artifacts } : {},
|
|
1839
|
+
status: { state: "working", timestamp: isoNow() },
|
|
1840
|
+
metadata
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
requestText(taskId, text, inputFiles, governed, resume) {
|
|
1844
|
+
const requestText = inputFiles.length > 0 ? `${text}
|
|
1845
|
+
|
|
1846
|
+
${formatFileRefsSection(inputFiles)}` : text;
|
|
1847
|
+
if (!governed || resume) return requestText;
|
|
1848
|
+
return [
|
|
1849
|
+
`A2A governed task instruction: get and update the existing TaskItem ${taskId}; do not create a replacement task.`,
|
|
1850
|
+
"Before doing the requested work, initialize its canonical description through manage_task with ## Objective, ## Acceptance Criteria, and ## Belief State sections.",
|
|
1851
|
+
"Keep lifecycle status truthful. Change status only through manage_task set_status; complete with a nonblank result and structured beliefImpact, and use interrupted, failed, or cancelled when appropriate. Use manage_task update for content fields only.",
|
|
1852
|
+
"",
|
|
1853
|
+
requestText
|
|
1854
|
+
].join("\n");
|
|
1855
|
+
}
|
|
1856
|
+
isResume(task) {
|
|
1857
|
+
return task?.status.state === "input-required";
|
|
1858
|
+
}
|
|
1859
|
+
resumeCommand(parts, text, reviewInterruption, soleInterruptId) {
|
|
1860
|
+
if (reviewInterruption) return this.reviewCommand(text);
|
|
1861
|
+
const data = dataParts(parts);
|
|
1862
|
+
if (data.length > 0) {
|
|
1863
|
+
const officialIds = data.map((part) => {
|
|
1864
|
+
const id = asString2(part.data.id);
|
|
1865
|
+
return id !== void 0 && isOfficialInterruptId(id) ? id : void 0;
|
|
1866
|
+
});
|
|
1867
|
+
if (data.length > 1 && officialIds.every((id) => id !== void 0)) {
|
|
1868
|
+
const resume = {};
|
|
1869
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
1870
|
+
resume[officialIds[index]] = data[index].data.value;
|
|
1871
|
+
}
|
|
1872
|
+
return { resume };
|
|
1873
|
+
}
|
|
1874
|
+
const sole = data[0];
|
|
1875
|
+
if (sole && "value" in sole.data) {
|
|
1876
|
+
if (data.length === 1 && officialIds[0] !== void 0) {
|
|
1877
|
+
return { resume: { [officialIds[0]]: sole.data.value } };
|
|
1878
|
+
}
|
|
1879
|
+
return { resume: sole.data.value };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (text.trim().length > 0) {
|
|
1883
|
+
return {
|
|
1884
|
+
resume: soleInterruptId === void 0 ? text : { [soleInterruptId]: text }
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
return void 0;
|
|
1888
|
+
}
|
|
1889
|
+
reviewCommand(text) {
|
|
1890
|
+
const trimmed = text.trim();
|
|
1891
|
+
const normalized = trimmed.toLowerCase();
|
|
1892
|
+
if (normalized === "approve") return { resume: { action: "approve" } };
|
|
1893
|
+
if (normalized === "reject") {
|
|
1894
|
+
return { resume: { action: "reject", data: { note: "Changes requested." } } };
|
|
1895
|
+
}
|
|
1896
|
+
const prefixedNote = /^reject:\s*(.+)$/is.exec(trimmed)?.[1]?.trim();
|
|
1897
|
+
if (prefixedNote) {
|
|
1898
|
+
return { resume: { action: "reject", data: { note: prefixedNote } } };
|
|
1899
|
+
}
|
|
1900
|
+
return void 0;
|
|
1901
|
+
}
|
|
1902
|
+
agentParams(threadId) {
|
|
1903
|
+
return {
|
|
1904
|
+
assistant_id: this.scope.assistantId,
|
|
1905
|
+
thread_id: threadId,
|
|
1906
|
+
tenant_id: this.scope.tenantId,
|
|
1907
|
+
project_id: this.scope.projectId,
|
|
1908
|
+
...this.scope.workspaceId ? { workspace_id: this.scope.workspaceId } : {}
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
hasGovernedTaskCapability() {
|
|
1912
|
+
return assistantHasGovernedTask(this.assistant);
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Cancel the exact durable run persisted on the A2A task, then persist the
|
|
1916
|
+
* terminal canceled state. No agent instance is aborted directly.
|
|
1917
|
+
*/
|
|
1918
|
+
async cancelTask(taskId, bus) {
|
|
1919
|
+
const task = this.taskStore ? await this.taskStore.load(taskId) : void 0;
|
|
1920
|
+
const threadId = asString2(task?.metadata?.threadId);
|
|
1921
|
+
const runId = asString2(task?.metadata?.a2aRunId) ?? asString2(task?.metadata?.runId);
|
|
1922
|
+
if (threadId && runId) {
|
|
1923
|
+
try {
|
|
1924
|
+
await this.runService.cancel(
|
|
1925
|
+
this.scope.tenantId,
|
|
1926
|
+
threadId,
|
|
1927
|
+
this.scope.assistantId,
|
|
1928
|
+
runId
|
|
1929
|
+
);
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
if (!(err instanceof A2ARunNotFoundError)) {
|
|
1932
|
+
console.warn({
|
|
1933
|
+
event: "a2a:cancel:failed",
|
|
1934
|
+
taskId,
|
|
1935
|
+
threadId,
|
|
1936
|
+
runId,
|
|
1937
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
await this.persistCanceled(taskId, task);
|
|
1943
|
+
bus.publish({
|
|
1944
|
+
kind: "status-update",
|
|
1945
|
+
taskId,
|
|
1946
|
+
contextId: task?.contextId ?? "",
|
|
1947
|
+
status: { state: "canceled", timestamp: isoNow() },
|
|
1948
|
+
final: true
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async persistCanceled(taskId, task, projection) {
|
|
1952
|
+
const canceledTask = {
|
|
1953
|
+
...task ?? {
|
|
1954
|
+
kind: "task",
|
|
1955
|
+
id: taskId,
|
|
1956
|
+
contextId: "",
|
|
1957
|
+
status: { state: "working" }
|
|
1958
|
+
},
|
|
1959
|
+
...projection ? {
|
|
1960
|
+
metadata: {
|
|
1961
|
+
...task?.metadata ?? {},
|
|
1962
|
+
threadId: projection.threadId,
|
|
1963
|
+
dependencies: projection.dependencies,
|
|
1964
|
+
inputFiles: projection.inputFiles,
|
|
1965
|
+
...projection.handle ? { a2aRunId: projection.handle.runId } : {}
|
|
1966
|
+
}
|
|
1967
|
+
} : {},
|
|
1968
|
+
status: { state: "canceled", timestamp: isoNow() }
|
|
1969
|
+
};
|
|
1970
|
+
if (this.taskStore) {
|
|
1971
|
+
try {
|
|
1972
|
+
await this.taskStore.save(canceledTask);
|
|
1973
|
+
} catch (saveError) {
|
|
1974
|
+
console.warn({
|
|
1975
|
+
event: "a2a:settle:save_failed",
|
|
1976
|
+
taskId,
|
|
1977
|
+
error: saveError instanceof Error ? saveError.message : String(saveError)
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return canceledTask;
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
// src/services/a2a/A2AFileIngestor.ts
|
|
1986
|
+
var DEFAULT_MAX_BYTES = 100 * 1024 * 1024;
|
|
1987
|
+
var EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1988
|
+
".exe",
|
|
1989
|
+
".sh",
|
|
1990
|
+
".bat",
|
|
1991
|
+
".cmd",
|
|
1992
|
+
".com",
|
|
1993
|
+
".msi",
|
|
1994
|
+
".js",
|
|
1995
|
+
".mjs",
|
|
1996
|
+
".cjs",
|
|
1997
|
+
".ps1",
|
|
1998
|
+
".vbs",
|
|
1999
|
+
".dll",
|
|
2000
|
+
".so",
|
|
2001
|
+
".dylib"
|
|
2002
|
+
]);
|
|
2003
|
+
var EXECUTABLE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
2004
|
+
"application/x-msdownload",
|
|
2005
|
+
"application/x-msdos-program",
|
|
2006
|
+
"application/x-executable",
|
|
2007
|
+
"application/x-sh",
|
|
2008
|
+
"application/x-bat",
|
|
2009
|
+
"application/x-shellscript",
|
|
2010
|
+
"text/x-shellscript",
|
|
2011
|
+
"application/javascript",
|
|
2012
|
+
"application/x-javascript",
|
|
2013
|
+
"text/javascript"
|
|
2014
|
+
]);
|
|
2015
|
+
function decodeBase64(value) {
|
|
2016
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
2017
|
+
}
|
|
2018
|
+
function maxEncodedLengthFor(maxBytes) {
|
|
2019
|
+
return Math.ceil(maxBytes * 4 / 3) + 4;
|
|
2020
|
+
}
|
|
2021
|
+
function sanitizePathSegment(segment, fallback) {
|
|
2022
|
+
const cleaned = segment.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\.{2,}/g, ".").replace(/^[.-]+|[.-]+$/g, "");
|
|
2023
|
+
return cleaned.length > 0 ? cleaned : fallback;
|
|
2024
|
+
}
|
|
2025
|
+
function storageLocationFor(dedupeKey, name) {
|
|
2026
|
+
const separatorIndex = dedupeKey.lastIndexOf(":");
|
|
2027
|
+
const keyBase = separatorIndex >= 0 ? dedupeKey.slice(0, separatorIndex) : dedupeKey;
|
|
2028
|
+
const keyIndex = separatorIndex >= 0 ? dedupeKey.slice(separatorIndex + 1) : "0";
|
|
2029
|
+
return {
|
|
2030
|
+
path: `a2a/${sanitizePathSegment(keyBase, "task")}`,
|
|
2031
|
+
name: `${sanitizePathSegment(keyIndex, "0")}-${sanitizePathSegment(name, "file")}`
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
function extensionOf(name) {
|
|
2035
|
+
return name.includes(".") ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
|
|
2036
|
+
}
|
|
2037
|
+
function assertAllowedFileType(name, mimeType) {
|
|
2038
|
+
if (EXECUTABLE_EXTENSIONS.has(extensionOf(name))) {
|
|
2039
|
+
throw new Error(`Refusing to ingest executable file "${name}"`);
|
|
2040
|
+
}
|
|
2041
|
+
if (mimeType && EXECUTABLE_MIME_TYPES.has(mimeType.toLowerCase())) {
|
|
2042
|
+
throw new Error(`Refusing to ingest executable file "${name}"`);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
var A2AFileIngestor = class {
|
|
2046
|
+
constructor(options) {
|
|
2047
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
2048
|
+
this.saveProjectFile = options.saveProjectFile;
|
|
2049
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Project a single file part onto a {@link TaskFileRef}, ingesting bytes when
|
|
2053
|
+
* present. Idempotent per `dedupeKey`: a repeated key returns the cached URI
|
|
2054
|
+
* without re-writing storage, and even across ingestor instances the key
|
|
2055
|
+
* maps to a deterministic storage path (`a2a/<taskId>/<partIndex>-<name>`),
|
|
2056
|
+
* so request retries overwrite the same object.
|
|
2057
|
+
*
|
|
2058
|
+
* NOTE: the key is based on `taskId` + part index rather than the internal
|
|
2059
|
+
* message id because the executor must ingest files *before* `addMessage`
|
|
2060
|
+
* returns a messageId. A client retrying with a different taskId for the
|
|
2061
|
+
* same message writes a second copy; that is accepted and documented.
|
|
2062
|
+
*/
|
|
2063
|
+
async ingest(part, dedupeKey, projectId, addedBy) {
|
|
2064
|
+
const file = part.file;
|
|
2065
|
+
if ("uri" in file) {
|
|
2066
|
+
return {
|
|
2067
|
+
uri: file.uri,
|
|
2068
|
+
...file.name ? { name: file.name } : {},
|
|
2069
|
+
...file.mimeType ? { mimeType: file.mimeType } : {},
|
|
2070
|
+
addedBy
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
const name = file.name ?? "file";
|
|
2074
|
+
assertAllowedFileType(name, file.mimeType);
|
|
2075
|
+
if (file.bytes.length > maxEncodedLengthFor(this.maxBytes)) {
|
|
2076
|
+
throw new Error(
|
|
2077
|
+
`A2A file part exceeds the ${this.maxBytes} byte ingest limit`
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
const bytes = decodeBase64(file.bytes);
|
|
2081
|
+
if (bytes.length > this.maxBytes) {
|
|
2082
|
+
throw new Error(
|
|
2083
|
+
`A2A file part exceeds the ${this.maxBytes} byte ingest limit`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
const cached = this.cache.get(dedupeKey);
|
|
2087
|
+
if (cached !== void 0) {
|
|
2088
|
+
return {
|
|
2089
|
+
uri: cached,
|
|
2090
|
+
...file.name ? { name: file.name } : {},
|
|
2091
|
+
...file.mimeType ? { mimeType: file.mimeType } : {},
|
|
2092
|
+
addedBy
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
const location = storageLocationFor(dedupeKey, name);
|
|
2096
|
+
const { uri } = await this.saveProjectFile({
|
|
2097
|
+
projectId,
|
|
2098
|
+
name: location.name,
|
|
2099
|
+
path: location.path,
|
|
2100
|
+
bytes,
|
|
2101
|
+
...file.mimeType ? { mimeType: file.mimeType } : {}
|
|
2102
|
+
});
|
|
2103
|
+
this.cache.set(dedupeKey, uri);
|
|
2104
|
+
return {
|
|
2105
|
+
uri,
|
|
2106
|
+
...file.name ? { name: file.name } : {},
|
|
2107
|
+
...file.mimeType ? { mimeType: file.mimeType } : {},
|
|
2108
|
+
addedBy
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
// src/services/a2a/A2ARuntimeRegistry.ts
|
|
2114
|
+
import {
|
|
2115
|
+
DefaultExecutionEventBus
|
|
2116
|
+
} from "@a2a-js/sdk/server";
|
|
2117
|
+
var DEFAULT_MAX_SCOPES = 1e3;
|
|
2118
|
+
var DEFAULT_IDLE_TTL_MS = 15 * 60 * 1e3;
|
|
2119
|
+
var A2ARuntimeCapacityError = class extends Error {
|
|
2120
|
+
constructor(maxScopes) {
|
|
2121
|
+
super(`A2A runtime scope capacity of ${maxScopes} is exhausted`);
|
|
2122
|
+
this.name = "A2ARuntimeCapacityError";
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
2125
|
+
var ScopedExecutionEventBusManager = class {
|
|
2126
|
+
constructor(touch) {
|
|
2127
|
+
this.buses = /* @__PURE__ */ new Map();
|
|
2128
|
+
this.touch = touch;
|
|
2129
|
+
}
|
|
2130
|
+
createOrGetByTaskId(taskId) {
|
|
2131
|
+
this.touch();
|
|
2132
|
+
const existing = this.buses.get(taskId);
|
|
2133
|
+
if (existing) return existing;
|
|
2134
|
+
const bus = new DefaultExecutionEventBus();
|
|
2135
|
+
this.buses.set(taskId, bus);
|
|
2136
|
+
return bus;
|
|
2137
|
+
}
|
|
2138
|
+
getByTaskId(taskId) {
|
|
2139
|
+
this.touch();
|
|
2140
|
+
return this.buses.get(taskId);
|
|
2141
|
+
}
|
|
2142
|
+
cleanupByTaskId(taskId) {
|
|
2143
|
+
this.touch();
|
|
2144
|
+
const bus = this.buses.get(taskId);
|
|
2145
|
+
bus?.removeAllListeners();
|
|
2146
|
+
this.buses.delete(taskId);
|
|
2147
|
+
}
|
|
2148
|
+
get activeBusCount() {
|
|
2149
|
+
return this.buses.size;
|
|
2150
|
+
}
|
|
2151
|
+
close() {
|
|
2152
|
+
for (const bus of this.buses.values()) {
|
|
2153
|
+
bus.removeAllListeners();
|
|
2154
|
+
}
|
|
2155
|
+
this.buses.clear();
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
var A2ARuntimeRegistry = class {
|
|
2159
|
+
constructor(options = {}) {
|
|
2160
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
2161
|
+
this.closed = false;
|
|
2162
|
+
this.maxScopes = options.maxScopes ?? DEFAULT_MAX_SCOPES;
|
|
2163
|
+
this.idleTtlMs = options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS;
|
|
2164
|
+
this.now = options.now ?? Date.now;
|
|
2165
|
+
}
|
|
2166
|
+
/**
|
|
2167
|
+
* Acquire and pin the runtime manager for an authorization scope.
|
|
2168
|
+
*
|
|
2169
|
+
* @param scope Exact tenant, project, and assistant authorization scope.
|
|
2170
|
+
* @returns A lease whose manager is shared only within that scope.
|
|
2171
|
+
*/
|
|
2172
|
+
acquire(scope) {
|
|
2173
|
+
if (this.closed) throw new Error("A2A runtime registry is closed");
|
|
2174
|
+
const now = this.now();
|
|
2175
|
+
this.pruneExpired(now);
|
|
2176
|
+
const key = JSON.stringify([
|
|
2177
|
+
scope.tenantId,
|
|
2178
|
+
scope.projectId,
|
|
2179
|
+
scope.assistantId
|
|
2180
|
+
]);
|
|
2181
|
+
const existing = this.entries.get(key);
|
|
2182
|
+
const entry = existing ?? this.createEntry(key, now);
|
|
2183
|
+
entry.lastAccessedAt = now;
|
|
2184
|
+
entry.leaseCount += 1;
|
|
2185
|
+
let released = false;
|
|
2186
|
+
return {
|
|
2187
|
+
manager: entry.manager,
|
|
2188
|
+
release: () => {
|
|
2189
|
+
if (released) return;
|
|
2190
|
+
released = true;
|
|
2191
|
+
entry.leaseCount -= 1;
|
|
2192
|
+
entry.lastAccessedAt = this.now();
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
close() {
|
|
2197
|
+
if (this.closed) return;
|
|
2198
|
+
this.closed = true;
|
|
2199
|
+
for (const entry of this.entries.values()) {
|
|
2200
|
+
entry.manager.close();
|
|
2201
|
+
}
|
|
2202
|
+
this.entries.clear();
|
|
2203
|
+
}
|
|
2204
|
+
pruneExpired(now) {
|
|
2205
|
+
for (const [key, entry] of this.entries) {
|
|
2206
|
+
if (!this.isActive(entry) && now - entry.lastAccessedAt > this.idleTtlMs) {
|
|
2207
|
+
entry.manager.close();
|
|
2208
|
+
this.entries.delete(key);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
makeCapacity() {
|
|
2213
|
+
if (this.entries.size < this.maxScopes) return;
|
|
2214
|
+
let oldest;
|
|
2215
|
+
for (const candidate of this.entries) {
|
|
2216
|
+
if (this.isActive(candidate[1])) continue;
|
|
2217
|
+
if (!oldest || candidate[1].lastAccessedAt < oldest[1].lastAccessedAt) {
|
|
2218
|
+
oldest = candidate;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
if (!oldest) throw new A2ARuntimeCapacityError(this.maxScopes);
|
|
2222
|
+
oldest[1].manager.close();
|
|
2223
|
+
this.entries.delete(oldest[0]);
|
|
2224
|
+
}
|
|
2225
|
+
createEntry(key, now) {
|
|
2226
|
+
this.makeCapacity();
|
|
2227
|
+
const entry = {
|
|
2228
|
+
lastAccessedAt: now,
|
|
2229
|
+
leaseCount: 0,
|
|
2230
|
+
manager: new ScopedExecutionEventBusManager(() => {
|
|
2231
|
+
entry.lastAccessedAt = this.now();
|
|
2232
|
+
})
|
|
2233
|
+
};
|
|
2234
|
+
this.entries.set(key, entry);
|
|
2235
|
+
return entry;
|
|
2236
|
+
}
|
|
2237
|
+
isActive(entry) {
|
|
2238
|
+
return entry.leaseCount > 0 || entry.manager.activeBusCount > 0;
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
|
|
2242
|
+
// src/routes/a2a-standard.ts
|
|
2243
|
+
var TERMINAL_A2A_STATES2 = /* @__PURE__ */ new Set([
|
|
2244
|
+
"completed",
|
|
2245
|
+
"failed",
|
|
2246
|
+
"canceled",
|
|
2247
|
+
"rejected"
|
|
2248
|
+
]);
|
|
2249
|
+
function agentParamsFor(scope, threadId, assistantId) {
|
|
2250
|
+
return {
|
|
2251
|
+
assistant_id: assistantId,
|
|
2252
|
+
thread_id: threadId,
|
|
2253
|
+
tenant_id: scope.tenantId,
|
|
2254
|
+
project_id: scope.projectId,
|
|
2255
|
+
...scope.workspaceId ? { workspace_id: scope.workspaceId } : {}
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function chunksFromThreadStream(buffer, key, cursor) {
|
|
2259
|
+
return (async function* () {
|
|
2260
|
+
for await (const record of buffer.streamThread(
|
|
2261
|
+
key,
|
|
2262
|
+
cursor === void 0 ? {} : { cursor }
|
|
2263
|
+
)) {
|
|
2264
|
+
if (record.kind === "end") return;
|
|
2265
|
+
if (record.kind !== "chunk") continue;
|
|
2266
|
+
yield record.chunk;
|
|
2267
|
+
if (record.chunk.type === MessageChunkTypes3.MESSAGE_COMPLETED || record.chunk.type === MessageChunkTypes3.MESSAGE_FAILED || record.chunk.type === MessageChunkTypes3.INTERRUPT) {
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
})();
|
|
2272
|
+
}
|
|
2273
|
+
function runServiceChunkSource(scope) {
|
|
2274
|
+
return {
|
|
2275
|
+
stream: (threadId, assistantId, messageId) => {
|
|
2276
|
+
const buffer = getChunkBuffer("default");
|
|
2277
|
+
if (buffer) {
|
|
2278
|
+
return stopAtTerminalChunks(
|
|
2279
|
+
chunksFromThreadStream(
|
|
2280
|
+
buffer,
|
|
2281
|
+
chunkBufferStreamKey(scope.tenantId, assistantId, threadId)
|
|
2282
|
+
)
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
return stopAtTerminalChunks(
|
|
2286
|
+
agentInstanceManager2.getAgent(agentParamsFor(scope, threadId, assistantId)).chunkStream(
|
|
2287
|
+
messageId,
|
|
2288
|
+
[MessageChunkTypes3.MESSAGE_COMPLETED, MessageChunkTypes3.MESSAGE_FAILED]
|
|
2289
|
+
)
|
|
2290
|
+
);
|
|
2291
|
+
},
|
|
2292
|
+
streamThread: (threadId, assistantId, cursor) => {
|
|
2293
|
+
const buffer = getChunkBuffer("default");
|
|
2294
|
+
if (!buffer) return (async function* () {
|
|
2295
|
+
})();
|
|
2296
|
+
return chunksFromThreadStream(
|
|
2297
|
+
buffer,
|
|
2298
|
+
chunkBufferStreamKey(scope.tenantId, assistantId, threadId),
|
|
2299
|
+
cursor
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
}
|
|
2304
|
+
function messageText(content) {
|
|
2305
|
+
if (typeof content === "string") return content;
|
|
2306
|
+
if (!Array.isArray(content)) return "";
|
|
2307
|
+
return content.map((part) => {
|
|
2308
|
+
if (typeof part === "string") return part;
|
|
2309
|
+
if (typeof part !== "object" || part === null) return "";
|
|
2310
|
+
const record = part;
|
|
2311
|
+
if (typeof record.text === "string") return record.text;
|
|
2312
|
+
if (record.type === "text" && typeof record.value === "string") {
|
|
2313
|
+
return record.value;
|
|
2314
|
+
}
|
|
2315
|
+
return "";
|
|
2316
|
+
}).join("");
|
|
2317
|
+
}
|
|
2318
|
+
function normalizeCheckpointMessages(messages) {
|
|
2319
|
+
if (!Array.isArray(messages)) return [];
|
|
2320
|
+
const normalized = [];
|
|
2321
|
+
messages.forEach((message, index) => {
|
|
2322
|
+
if (typeof message !== "object" || message === null) return;
|
|
2323
|
+
const record = message;
|
|
2324
|
+
const type = typeof record.getType === "function" ? record.getType() : asString3(record.type) ?? asString3(record.role);
|
|
2325
|
+
const role = type === "human" || type === "user" ? "user" : type === "ai" || type === "assistant" ? "agent" : void 0;
|
|
2326
|
+
if (!role) return;
|
|
2327
|
+
const text = messageText(record.content);
|
|
2328
|
+
if (!text) return;
|
|
2329
|
+
normalized.push({
|
|
2330
|
+
kind: "message",
|
|
2331
|
+
messageId: asString3(record.id) ?? `a2a-checkpoint:${index}`,
|
|
2332
|
+
role,
|
|
2333
|
+
parts: [{ kind: "text", text }]
|
|
2334
|
+
});
|
|
2335
|
+
});
|
|
2336
|
+
return normalized;
|
|
2337
|
+
}
|
|
2338
|
+
function defaultStateResolver() {
|
|
2339
|
+
return {
|
|
2340
|
+
getCurrentMessages: async (threadId, assistantId, scope) => {
|
|
2341
|
+
const agent = agentInstanceManager2.getAgent(
|
|
2342
|
+
agentParamsFor(scope, threadId, assistantId)
|
|
2343
|
+
);
|
|
2344
|
+
const state = await agent.getCurrentState();
|
|
2345
|
+
return normalizeCheckpointMessages(state?.values?.messages);
|
|
2346
|
+
},
|
|
2347
|
+
getInterrupts: async (threadId, assistantId, scope) => await agentInstanceManager2.getAgent(agentParamsFor(scope, threadId, assistantId)).getInterrupts() ?? []
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
function blockingFor(body) {
|
|
2351
|
+
const params = typeof body === "object" && body !== null ? body.params : void 0;
|
|
2352
|
+
const configuration = typeof params === "object" && params !== null ? params.configuration : void 0;
|
|
2353
|
+
const blocking = typeof configuration === "object" && configuration !== null ? configuration.blocking : void 0;
|
|
2354
|
+
return blocking !== false;
|
|
2355
|
+
}
|
|
2356
|
+
async function resumeValidationError(taskStore, body) {
|
|
2357
|
+
const method = typeof body === "object" && body !== null ? body.method : void 0;
|
|
2358
|
+
if (method !== "message/send" && method !== "message/stream") return void 0;
|
|
2359
|
+
const params = body.params;
|
|
2360
|
+
const message = typeof params === "object" && params !== null ? params.message : void 0;
|
|
2361
|
+
const taskId = typeof message === "object" && message !== null ? message.taskId : void 0;
|
|
2362
|
+
if (typeof taskId !== "string" || taskId.length === 0) return void 0;
|
|
2363
|
+
const task = await taskStore.load(taskId);
|
|
2364
|
+
if (!task) return void 0;
|
|
2365
|
+
if (task.status.state === "input-required") return void 0;
|
|
2366
|
+
if (TERMINAL_A2A_STATES2.has(task.status.state)) return void 0;
|
|
2367
|
+
return `Task ${taskId} is not waiting for input and cannot be resumed`;
|
|
2368
|
+
}
|
|
2369
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
2370
|
+
"success",
|
|
2371
|
+
"error",
|
|
2372
|
+
"interrupted",
|
|
2373
|
+
"cancelled",
|
|
2374
|
+
"timeout"
|
|
2375
|
+
]);
|
|
2376
|
+
var RESUBSCRIBE_SETTLE_TIMEOUT_MS = 1e4;
|
|
2377
|
+
function asString3(value) {
|
|
2378
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2379
|
+
}
|
|
2380
|
+
function requestMethod(body) {
|
|
2381
|
+
return typeof body === "object" && body !== null ? asString3(body.method) : void 0;
|
|
2382
|
+
}
|
|
2383
|
+
function requestTaskId(body) {
|
|
2384
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
2385
|
+
const record = body;
|
|
2386
|
+
const params = typeof record.params === "object" && record.params !== null ? record.params : void 0;
|
|
2387
|
+
if (!params) return void 0;
|
|
2388
|
+
if (record.method === "tasks/get" || record.method === "tasks/cancel" || record.method === "tasks/resubscribe") {
|
|
2389
|
+
return asString3(params.id);
|
|
2390
|
+
}
|
|
2391
|
+
if (record.method === "message/send" || record.method === "message/stream") {
|
|
2392
|
+
const message = typeof params.message === "object" && params.message !== null ? params.message : void 0;
|
|
2393
|
+
return asString3(message?.taskId);
|
|
2394
|
+
}
|
|
2395
|
+
return void 0;
|
|
2396
|
+
}
|
|
2397
|
+
function isTerminalA2AState(state) {
|
|
2398
|
+
return TERMINAL_A2A_STATES2.has(state);
|
|
2399
|
+
}
|
|
2400
|
+
function agentMessage2(text) {
|
|
2401
|
+
return {
|
|
2402
|
+
kind: "message",
|
|
2403
|
+
messageId: `a2a-run-projection:${Date.now()}`,
|
|
2404
|
+
role: "agent",
|
|
2405
|
+
parts: [{ kind: "text", text }]
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
function projectRunState(run) {
|
|
2409
|
+
if (run.cancelRequested === true || run.status === "cancelled") {
|
|
2410
|
+
return { state: "canceled" };
|
|
2411
|
+
}
|
|
2412
|
+
switch (run.status) {
|
|
2413
|
+
case "success":
|
|
2414
|
+
return { state: "completed" };
|
|
2415
|
+
case "interrupted":
|
|
2416
|
+
return { state: "input-required" };
|
|
2417
|
+
case "error":
|
|
2418
|
+
case "timeout":
|
|
2419
|
+
return {
|
|
2420
|
+
state: "failed",
|
|
2421
|
+
...asString3(run.error) ? { message: agentMessage2(asString3(run.error)) } : {}
|
|
2422
|
+
};
|
|
2423
|
+
default:
|
|
2424
|
+
return void 0;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
function resultTextFromTask(task) {
|
|
2428
|
+
const artifact = task.artifacts?.find((item) => item.name === "result");
|
|
2429
|
+
if (!artifact) return "";
|
|
2430
|
+
return artifact.parts.filter((part) => part.kind === "text").map((part) => part.text).join("");
|
|
2431
|
+
}
|
|
2432
|
+
function resultArtifactFor(task, result) {
|
|
2433
|
+
return {
|
|
2434
|
+
artifactId: `${task.id}:result`,
|
|
2435
|
+
name: "result",
|
|
2436
|
+
parts: [{ kind: "text", text: result.trim() || "(no output)" }]
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
function withResult(task, result, runId, threadId) {
|
|
2440
|
+
const artifact = resultArtifactFor(task, result);
|
|
2441
|
+
return {
|
|
2442
|
+
...task,
|
|
2443
|
+
metadata: { ...task.metadata ?? {}, a2aRunId: runId, threadId },
|
|
2444
|
+
artifacts: [
|
|
2445
|
+
...(task.artifacts ?? []).filter(
|
|
2446
|
+
(item) => item.artifactId !== artifact.artifactId && item.name !== "result"
|
|
2447
|
+
),
|
|
2448
|
+
artifact
|
|
2449
|
+
],
|
|
2450
|
+
status: { state: "completed", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
function deriveInterruption(task, interrupts) {
|
|
2454
|
+
const existing = task.metadata?.interruption;
|
|
2455
|
+
if (existing && typeof existing === "object") {
|
|
2456
|
+
return existing;
|
|
2457
|
+
}
|
|
2458
|
+
if (interrupts.length === 0) return void 0;
|
|
2459
|
+
const review = interrupts.some((interrupt) => interrupt.type === "task_review");
|
|
2460
|
+
const clarify = interrupts.some((interrupt) => interrupt.type === "clarify");
|
|
2461
|
+
const summary = task.status.message ? task.status.message.parts.filter((part) => part.kind === "text").map((part) => part.text).join("").trim() : "";
|
|
2462
|
+
return {
|
|
2463
|
+
type: review ? "review_required" : clarify ? "missing_input" : "user_decision",
|
|
2464
|
+
summary: summary || "A2A requires input"
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
function interruptArtifactFor(task, interrupts) {
|
|
2468
|
+
if (interrupts.length === 0) return void 0;
|
|
2469
|
+
return {
|
|
2470
|
+
artifactId: `${task.id}:interrupt`,
|
|
2471
|
+
name: "interrupt",
|
|
2472
|
+
parts: interrupts.map((interrupt) => {
|
|
2473
|
+
const payload = interrupt.payload;
|
|
2474
|
+
const record = typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : void 0;
|
|
2475
|
+
const schema = record?.response_schema;
|
|
2476
|
+
return {
|
|
2477
|
+
kind: "data",
|
|
2478
|
+
data: {
|
|
2479
|
+
id: interrupt.interruptId,
|
|
2480
|
+
value: payload,
|
|
2481
|
+
...schema === void 0 ? {} : { response_schema: schema }
|
|
2482
|
+
}
|
|
2483
|
+
};
|
|
2484
|
+
})
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
async function reconcileTaskFromRun(taskStore, runService, stateResolver, scope, body) {
|
|
2488
|
+
const taskId = requestTaskId(body);
|
|
2489
|
+
if (!taskId) return void 0;
|
|
2490
|
+
let task;
|
|
2491
|
+
try {
|
|
2492
|
+
task = await taskStore.load(taskId);
|
|
2493
|
+
} catch {
|
|
2494
|
+
return void 0;
|
|
2495
|
+
}
|
|
2496
|
+
if (!task) return void 0;
|
|
2497
|
+
if (isTerminalA2AState(task.status.state)) {
|
|
2498
|
+
if (task.status.state !== "completed") return task;
|
|
2499
|
+
}
|
|
2500
|
+
const runId = asString3(task.metadata?.a2aRunId);
|
|
2501
|
+
if (!runId) return task;
|
|
2502
|
+
const threadId = asString3(task.metadata?.threadId) ?? task.contextId;
|
|
2503
|
+
const run = await runService.getRun(
|
|
2504
|
+
scope.tenantId,
|
|
2505
|
+
runId,
|
|
2506
|
+
threadId,
|
|
2507
|
+
scope.assistantId
|
|
2508
|
+
).catch(() => void 0);
|
|
2509
|
+
if (!run || !TERMINAL_RUN_STATUSES.has(run.status)) return task;
|
|
2510
|
+
const projection = projectRunState(run);
|
|
2511
|
+
if (!projection) return task;
|
|
2512
|
+
if (projection.state === "completed") {
|
|
2513
|
+
const existingResult = resultTextFromTask(task).trim();
|
|
2514
|
+
const missingResult = existingResult === "" || existingResult.toLowerCase() === "(no output)";
|
|
2515
|
+
let result = existingResult;
|
|
2516
|
+
if (missingResult) {
|
|
2517
|
+
try {
|
|
2518
|
+
const messages = await stateResolver.getCurrentMessages(
|
|
2519
|
+
threadId,
|
|
2520
|
+
scope.assistantId,
|
|
2521
|
+
scope
|
|
2522
|
+
);
|
|
2523
|
+
const finalAgent = [...messages].reverse().find((message) => message.role === "agent");
|
|
2524
|
+
const text = finalAgent ? finalAgent.parts.filter((part) => part.kind === "text").map((part) => part.text).join("").trim() : "";
|
|
2525
|
+
if (text) result = text;
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
if (task.status.state === "completed" && !missingResult) return task;
|
|
2530
|
+
const completedTask = withResult(task, result || "(no output)", runId, threadId);
|
|
2531
|
+
if (taskStore.completeRun) {
|
|
2532
|
+
return taskStore.completeRun(completedTask, result || "(no output)");
|
|
2533
|
+
}
|
|
2534
|
+
await taskStore.save(completedTask);
|
|
2535
|
+
return await taskStore.load(taskId) ?? completedTask;
|
|
2536
|
+
}
|
|
2537
|
+
if (projection.state === "input-required") {
|
|
2538
|
+
let interrupts = [];
|
|
2539
|
+
try {
|
|
2540
|
+
const resolved = await stateResolver.getInterrupts(
|
|
2541
|
+
threadId,
|
|
2542
|
+
scope.assistantId,
|
|
2543
|
+
scope
|
|
2544
|
+
);
|
|
2545
|
+
interrupts = Array.isArray(resolved) ? resolved : [];
|
|
2546
|
+
} catch {
|
|
2547
|
+
}
|
|
2548
|
+
const interruption = deriveInterruption(task, interrupts);
|
|
2549
|
+
const artifact = interruptArtifactFor(task, interrupts);
|
|
2550
|
+
const next2 = {
|
|
2551
|
+
...task,
|
|
2552
|
+
metadata: {
|
|
2553
|
+
...task.metadata ?? {},
|
|
2554
|
+
a2aRunId: runId,
|
|
2555
|
+
threadId,
|
|
2556
|
+
...interruption ? { interruption } : {}
|
|
2557
|
+
},
|
|
2558
|
+
...artifact ? {
|
|
2559
|
+
artifacts: [
|
|
2560
|
+
...(task.artifacts ?? []).filter(
|
|
2561
|
+
(item) => item.artifactId !== artifact.artifactId
|
|
2562
|
+
),
|
|
2563
|
+
artifact
|
|
2564
|
+
]
|
|
2565
|
+
} : {},
|
|
2566
|
+
status: {
|
|
2567
|
+
state: "input-required",
|
|
2568
|
+
...task.status.message ? { message: task.status.message } : {},
|
|
2569
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2570
|
+
}
|
|
2571
|
+
};
|
|
2572
|
+
await taskStore.save(next2);
|
|
2573
|
+
return await taskStore.load(taskId) ?? next2;
|
|
2574
|
+
}
|
|
2575
|
+
if (task.status.state === projection.state) return task;
|
|
2576
|
+
const next = {
|
|
2577
|
+
...task,
|
|
2578
|
+
metadata: { ...task.metadata ?? {}, a2aRunId: runId, threadId },
|
|
2579
|
+
status: {
|
|
2580
|
+
state: projection.state,
|
|
2581
|
+
...projection.message ? { message: projection.message } : {},
|
|
2582
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2583
|
+
}
|
|
2584
|
+
};
|
|
2585
|
+
await taskStore.save(next);
|
|
2586
|
+
return await taskStore.load(taskId) ?? next;
|
|
2587
|
+
}
|
|
2588
|
+
async function cancelTaskFromRun(taskStore, runService, scope, body) {
|
|
2589
|
+
if (requestMethod(body) !== "tasks/cancel") return void 0;
|
|
2590
|
+
const taskId = requestTaskId(body);
|
|
2591
|
+
if (!taskId) return void 0;
|
|
2592
|
+
const task = await taskStore.load(taskId);
|
|
2593
|
+
if (!task || isTerminalA2AState(task.status.state)) return void 0;
|
|
2594
|
+
const runId = asString3(task.metadata?.a2aRunId);
|
|
2595
|
+
if (!runId) return void 0;
|
|
2596
|
+
const threadId = asString3(task.metadata?.threadId) ?? task.contextId;
|
|
2597
|
+
const before = await runService.getRun(
|
|
2598
|
+
scope.tenantId,
|
|
2599
|
+
runId,
|
|
2600
|
+
threadId,
|
|
2601
|
+
scope.assistantId
|
|
2602
|
+
);
|
|
2603
|
+
if (!before) throw A2AError3.taskNotCancelable(taskId);
|
|
2604
|
+
if (TERMINAL_RUN_STATUSES.has(before.status)) {
|
|
2605
|
+
if (before.status !== "interrupted" && before.cancelRequested !== true) {
|
|
2606
|
+
throw A2AError3.taskNotCancelable(taskId);
|
|
2607
|
+
}
|
|
2608
|
+
} else {
|
|
2609
|
+
await runService.cancel(scope.tenantId, threadId, scope.assistantId, runId);
|
|
2610
|
+
const after = await runService.getRun(
|
|
2611
|
+
scope.tenantId,
|
|
2612
|
+
runId,
|
|
2613
|
+
threadId,
|
|
2614
|
+
scope.assistantId
|
|
2615
|
+
);
|
|
2616
|
+
if (!after) throw A2AError3.taskNotCancelable(taskId);
|
|
2617
|
+
if (!TERMINAL_RUN_STATUSES.has(after.status)) {
|
|
2618
|
+
throw new Error(`Run ${runId} did not reach a terminal state after cancellation`);
|
|
2619
|
+
}
|
|
2620
|
+
if (after.status !== "interrupted" && after.status !== "cancelled" && after.cancelRequested !== true) {
|
|
2621
|
+
throw A2AError3.taskNotCancelable(taskId);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const canceled = {
|
|
2625
|
+
...task,
|
|
2626
|
+
metadata: { ...task.metadata ?? {}, a2aRunId: runId, threadId },
|
|
2627
|
+
status: { state: "canceled", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
2628
|
+
};
|
|
2629
|
+
await taskStore.save(canceled);
|
|
2630
|
+
return await taskStore.load(taskId) ?? canceled;
|
|
2631
|
+
}
|
|
2632
|
+
function getHeader(value) {
|
|
2633
|
+
if (Array.isArray(value)) return value[0];
|
|
2634
|
+
return value;
|
|
2635
|
+
}
|
|
2636
|
+
function getSessionUser(request) {
|
|
2637
|
+
return request.user;
|
|
2638
|
+
}
|
|
2639
|
+
function buildBaseUrl(request) {
|
|
2640
|
+
const protocol = getHeader(request.headers["x-forwarded-proto"]) ?? request.protocol;
|
|
2641
|
+
const forwardedHost = getHeader(request.headers["x-forwarded-host"]);
|
|
2642
|
+
const host = forwardedHost ? forwardedHost.split(",")[0].trim() : request.host;
|
|
2643
|
+
return `${protocol}://${host}`;
|
|
2644
|
+
}
|
|
2645
|
+
function makeFileIngestor(tenantId, maxFileBytes) {
|
|
2646
|
+
return new A2AFileIngestor({
|
|
2647
|
+
saveProjectFile: (params) => saveProjectFile({ tenantId, ...params }),
|
|
2648
|
+
...maxFileBytes !== void 0 ? { maxBytes: maxFileBytes } : {}
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
async function resolveWorkspaceId(getProjectStore, tenantId, projectId) {
|
|
2652
|
+
if (!projectId) return void 0;
|
|
2653
|
+
try {
|
|
2654
|
+
const project = await getProjectStore().getProjectById(tenantId, projectId);
|
|
2655
|
+
return project?.workspaceId;
|
|
2656
|
+
} catch {
|
|
2657
|
+
return void 0;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
function extractRequestId(body) {
|
|
2661
|
+
if (typeof body === "object" && body !== null && "id" in body) {
|
|
2662
|
+
const id = body.id;
|
|
2663
|
+
if (typeof id === "string" || typeof id === "number" || id === null) {
|
|
2664
|
+
return id;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
return null;
|
|
2668
|
+
}
|
|
2669
|
+
function authErrorToRpcCode(statusCode) {
|
|
2670
|
+
switch (statusCode) {
|
|
2671
|
+
case 401:
|
|
2672
|
+
return -32001;
|
|
2673
|
+
case 403:
|
|
2674
|
+
return -32002;
|
|
2675
|
+
case 404:
|
|
2676
|
+
return -32003;
|
|
2677
|
+
default:
|
|
2678
|
+
return -32603;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
function jsonRpcError(id, code, message) {
|
|
2682
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
2683
|
+
}
|
|
2684
|
+
var DEFAULT_A2A_TASKS_PAGE_SIZE = 50;
|
|
2685
|
+
var MAX_A2A_TASKS_PAGE_SIZE = 200;
|
|
2686
|
+
var TASKS_LIST_STATUSES = {
|
|
2687
|
+
submitted: ["pending"],
|
|
2688
|
+
working: ["in_progress"],
|
|
2689
|
+
"input-required": ["interrupted", "review"],
|
|
2690
|
+
completed: ["completed"],
|
|
2691
|
+
failed: ["failed"],
|
|
2692
|
+
canceled: ["cancelled"],
|
|
2693
|
+
rejected: ["failed"],
|
|
2694
|
+
pending: ["pending"],
|
|
2695
|
+
in_progress: ["in_progress"],
|
|
2696
|
+
review: ["review"],
|
|
2697
|
+
interrupted: ["interrupted"],
|
|
2698
|
+
cancelled: ["cancelled"]
|
|
2699
|
+
};
|
|
2700
|
+
function asRecord3(value) {
|
|
2701
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
2702
|
+
}
|
|
2703
|
+
function encodePageToken(offset) {
|
|
2704
|
+
return Buffer.from(String(offset), "utf8").toString("base64url");
|
|
2705
|
+
}
|
|
2706
|
+
function decodePageToken(token) {
|
|
2707
|
+
if (!/^[A-Za-z0-9_-]+$/.test(token)) return void 0;
|
|
2708
|
+
const text = Buffer.from(token, "base64url").toString("utf8");
|
|
2709
|
+
if (!/^\d+$/.test(text)) return void 0;
|
|
2710
|
+
const offset = Number(text);
|
|
2711
|
+
return Number.isSafeInteger(offset) && offset >= 0 ? offset : void 0;
|
|
2712
|
+
}
|
|
2713
|
+
function parseTasksListQuery(body) {
|
|
2714
|
+
const record = asRecord3(body);
|
|
2715
|
+
const rawParams = record?.params;
|
|
2716
|
+
if (rawParams !== void 0 && rawParams !== null && asRecord3(rawParams) === void 0) {
|
|
2717
|
+
return { ok: false, message: "params must be an object" };
|
|
2718
|
+
}
|
|
2719
|
+
const params = asRecord3(rawParams) ?? {};
|
|
2720
|
+
let pageSize = DEFAULT_A2A_TASKS_PAGE_SIZE;
|
|
2721
|
+
if (params.pageSize !== void 0) {
|
|
2722
|
+
const value = params.pageSize;
|
|
2723
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_A2A_TASKS_PAGE_SIZE) {
|
|
2724
|
+
return {
|
|
2725
|
+
ok: false,
|
|
2726
|
+
message: `pageSize must be an integer between 1 and ${MAX_A2A_TASKS_PAGE_SIZE}`
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
pageSize = value;
|
|
2730
|
+
}
|
|
2731
|
+
let offset = 0;
|
|
2732
|
+
if (params.pageToken !== void 0) {
|
|
2733
|
+
const token = params.pageToken;
|
|
2734
|
+
const decoded = typeof token === "string" ? decodePageToken(token) : void 0;
|
|
2735
|
+
if (decoded === void 0) {
|
|
2736
|
+
return { ok: false, message: "pageToken is invalid" };
|
|
2737
|
+
}
|
|
2738
|
+
offset = decoded;
|
|
2739
|
+
}
|
|
2740
|
+
let statuses;
|
|
2741
|
+
if (params.status !== void 0) {
|
|
2742
|
+
const value = params.status;
|
|
2743
|
+
if (typeof value !== "string" || !Object.prototype.hasOwnProperty.call(TASKS_LIST_STATUSES, value)) {
|
|
2744
|
+
return { ok: false, message: "status is invalid" };
|
|
2745
|
+
}
|
|
2746
|
+
statuses = TASKS_LIST_STATUSES[value];
|
|
2747
|
+
}
|
|
2748
|
+
return {
|
|
2749
|
+
ok: true,
|
|
2750
|
+
query: { pageSize, offset, ...statuses ? { statuses } : {} }
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
function isA2ATaskSnapshot2(value, id) {
|
|
2754
|
+
return typeof value === "object" && value !== null && value.id === id && typeof value.status === "object" && value.status !== null;
|
|
2755
|
+
}
|
|
2756
|
+
function isCanonicalA2ATaskItem(item, scope) {
|
|
2757
|
+
return item.tenantId === scope.tenantId && item.ownerType === "agent" && item.ownerId === scope.assistantId && item.projectId === scope.projectId && item.sourceId === "a2a";
|
|
2758
|
+
}
|
|
2759
|
+
async function listCanonicalA2ATasks(internalTasks, taskStore, scope, query) {
|
|
2760
|
+
const statuses = query.statuses;
|
|
2761
|
+
const storeStatus = statuses?.length === 1 ? statuses[0] : void 0;
|
|
2762
|
+
const fetchSize = query.pageSize + 1;
|
|
2763
|
+
const tasks = [];
|
|
2764
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2765
|
+
let cursor = query.offset;
|
|
2766
|
+
for (; ; ) {
|
|
2767
|
+
const rows = await internalTasks.list({
|
|
2768
|
+
tenantId: scope.tenantId,
|
|
2769
|
+
ownerType: "agent",
|
|
2770
|
+
ownerId: scope.assistantId,
|
|
2771
|
+
projectId: scope.projectId,
|
|
2772
|
+
sourceId: "a2a",
|
|
2773
|
+
...storeStatus ? { status: storeStatus } : {},
|
|
2774
|
+
limit: fetchSize,
|
|
2775
|
+
offset: cursor
|
|
2776
|
+
});
|
|
2777
|
+
if (rows.length === 0) break;
|
|
2778
|
+
let nextPageToken;
|
|
2779
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
2780
|
+
const item = rows[index];
|
|
2781
|
+
if (!isCanonicalA2ATaskItem(item, scope)) continue;
|
|
2782
|
+
if (statuses && !statuses.includes(item.status)) continue;
|
|
2783
|
+
const restored = await taskStore.load(item.id).catch(() => void 0);
|
|
2784
|
+
if (!restored || !isA2ATaskSnapshot2(restored, item.id)) continue;
|
|
2785
|
+
if (seen.has(restored.id)) continue;
|
|
2786
|
+
if (tasks.length < query.pageSize) {
|
|
2787
|
+
seen.add(restored.id);
|
|
2788
|
+
tasks.push(restored);
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
nextPageToken = encodePageToken(cursor + index);
|
|
2792
|
+
break;
|
|
2793
|
+
}
|
|
2794
|
+
if (nextPageToken !== void 0) {
|
|
2795
|
+
return { tasks, nextPageToken };
|
|
2796
|
+
}
|
|
2797
|
+
cursor += rows.length;
|
|
2798
|
+
if (rows.length < fetchSize) break;
|
|
2799
|
+
}
|
|
2800
|
+
return { tasks };
|
|
2801
|
+
}
|
|
2802
|
+
function isAsyncGenerator(value) {
|
|
2803
|
+
return typeof value === "object" && value !== null && typeof value[Symbol.asyncIterator] === "function";
|
|
2804
|
+
}
|
|
2805
|
+
var SSE_HEADERS = {
|
|
2806
|
+
"Content-Type": "text/event-stream",
|
|
2807
|
+
"Cache-Control": "no-cache",
|
|
2808
|
+
Connection: "keep-alive",
|
|
2809
|
+
"Access-Control-Allow-Origin": "*",
|
|
2810
|
+
"A2A-Version": "0.3"
|
|
2811
|
+
};
|
|
2812
|
+
function registerA2AStandardRoutes(app, deps) {
|
|
2813
|
+
const authService = deps.authService;
|
|
2814
|
+
const resolveTasks = deps.getTaskStoreLattice ?? (() => getStoreLattice("default", "task").store);
|
|
2815
|
+
const resolveTaskWorkItems = deps.getTaskWorkItemStore ?? (() => getStoreLattice("default", "taskWorkItem").store);
|
|
2816
|
+
const getAssistantStore = deps.getAssistantStore ?? (() => getStoreLattice("default", "assistant").store);
|
|
2817
|
+
const getProjectStore = deps.getProjectStore ?? (() => getStoreLattice("default", "project").store);
|
|
2818
|
+
const resolveRunLedger = deps.getAgentRunLedger ?? (() => getStoreLattice("default", "agentRun").store);
|
|
2819
|
+
const resolveStore = deps.getStore ?? (() => getStoreLattice("default", "store").store);
|
|
2820
|
+
const hasPushStore = () => {
|
|
2821
|
+
try {
|
|
2822
|
+
return Boolean(resolveStore());
|
|
2823
|
+
} catch {
|
|
2824
|
+
return false;
|
|
2825
|
+
}
|
|
2826
|
+
};
|
|
2827
|
+
const createPushStore = (scope) => {
|
|
2828
|
+
const store = resolveStore();
|
|
2829
|
+
if (!store) return void 0;
|
|
2830
|
+
return new A2APushNotificationStore({
|
|
2831
|
+
store,
|
|
2832
|
+
scope: {
|
|
2833
|
+
tenantId: scope.tenantId,
|
|
2834
|
+
assistantId: scope.assistantId
|
|
2835
|
+
},
|
|
2836
|
+
...deps.resolvePushHost ? { resolveHost: deps.resolvePushHost } : {}
|
|
2837
|
+
});
|
|
2838
|
+
};
|
|
2839
|
+
const unavailableRunLedger = () => ({
|
|
2840
|
+
createPendingRun: async () => {
|
|
2841
|
+
throw new Error("A2A run ledger is not configured");
|
|
2842
|
+
},
|
|
2843
|
+
getRun: async () => void 0,
|
|
2844
|
+
requestCancel: async () => "not_found"
|
|
2845
|
+
});
|
|
2846
|
+
const runServiceFor = (scope) => {
|
|
2847
|
+
if (deps.createRunService) return deps.createRunService(scope);
|
|
2848
|
+
let ledger;
|
|
2849
|
+
try {
|
|
2850
|
+
ledger = resolveRunLedger();
|
|
2851
|
+
} catch {
|
|
2852
|
+
ledger = unavailableRunLedger();
|
|
2853
|
+
}
|
|
2854
|
+
const maxActiveRunsForTenant = resolveRunTaskMaxActivePerTenant();
|
|
2855
|
+
return new A2ARunService({
|
|
2856
|
+
ledger,
|
|
2857
|
+
chunkSource: runServiceChunkSource(scope),
|
|
2858
|
+
...maxActiveRunsForTenant === void 0 ? {} : { maxActiveRunsForTenant },
|
|
2859
|
+
getLatestCursor: async (threadId, assistantId) => {
|
|
2860
|
+
const buffer = getChunkBuffer("default");
|
|
2861
|
+
if (!buffer) return void 0;
|
|
2862
|
+
const key = chunkBufferStreamKey(scope.tenantId, assistantId, threadId);
|
|
2863
|
+
const status = await buffer.getThreadStatus(key);
|
|
2864
|
+
if (status !== "active") return void 0;
|
|
2865
|
+
return buffer.latestCursor(key);
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2868
|
+
};
|
|
2869
|
+
const stateResolver = deps.stateResolver ?? defaultStateResolver();
|
|
2870
|
+
const runtimeRegistry = deps.runtimeRegistry ?? new A2ARuntimeRegistry();
|
|
2871
|
+
const taskStoreFor = (scope, internalTasks, workItems) => new A2ATaskStoreAdapter({
|
|
2872
|
+
taskStore: internalTasks,
|
|
2873
|
+
workItemStore: workItems,
|
|
2874
|
+
lifecycle: createTaskLifecycleService({
|
|
2875
|
+
taskStore: internalTasks,
|
|
2876
|
+
workItemStore: workItems
|
|
2877
|
+
}),
|
|
2878
|
+
scope
|
|
2879
|
+
});
|
|
2880
|
+
app.addHook("onClose", async () => {
|
|
2881
|
+
runtimeRegistry.close();
|
|
2882
|
+
});
|
|
2883
|
+
const handleTaskResubscribe = async (request, reply, requestId, scope, taskStore) => {
|
|
2884
|
+
reply.header("A2A-Version", "0.3");
|
|
2885
|
+
const body = asRecord3(request.body);
|
|
2886
|
+
const params = asRecord3(body?.params);
|
|
2887
|
+
const taskId = asString3(params?.id);
|
|
2888
|
+
if (!taskId) {
|
|
2889
|
+
reply.send(jsonRpcError(requestId, -32602, "Invalid params: id is required"));
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2892
|
+
let task;
|
|
2893
|
+
try {
|
|
2894
|
+
task = await taskStore.load(taskId);
|
|
2895
|
+
} catch (err) {
|
|
2896
|
+
request.log.error({ err }, "a2a:jsonrpc:resubscribe");
|
|
2897
|
+
reply.status(500).send(jsonRpcError(requestId, -32603, "Internal error"));
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
2900
|
+
if (!task) {
|
|
2901
|
+
reply.send({
|
|
2902
|
+
jsonrpc: "2.0",
|
|
2903
|
+
id: requestId,
|
|
2904
|
+
error: A2AError3.taskNotFound(taskId).toJSONRPCError()
|
|
2905
|
+
});
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
let runtimeLease;
|
|
2909
|
+
try {
|
|
2910
|
+
runtimeLease = runtimeRegistry.acquire(scope);
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
if (err instanceof A2ARuntimeCapacityError) {
|
|
2913
|
+
request.log.error({ err, scope }, "a2a:jsonrpc:runtime_capacity");
|
|
2914
|
+
reply.status(503).send(jsonRpcError(requestId, -32603, "A2A runtime capacity exhausted"));
|
|
2915
|
+
return;
|
|
2916
|
+
}
|
|
2917
|
+
throw err;
|
|
2918
|
+
}
|
|
2919
|
+
reply.hijack();
|
|
2920
|
+
reply.raw.writeHead(200, SSE_HEADERS);
|
|
2921
|
+
const writeFrame = async (frame, event) => {
|
|
2922
|
+
if (reply.raw.writableEnded || reply.raw.destroyed) return;
|
|
2923
|
+
const data = `${event ? `event: ${event}
|
|
2924
|
+
` : ""}data: ${JSON.stringify(frame)}
|
|
2925
|
+
|
|
2926
|
+
`;
|
|
2927
|
+
if (reply.raw.write(data)) return;
|
|
2928
|
+
await new Promise((resolve) => {
|
|
2929
|
+
const done = () => {
|
|
2930
|
+
reply.raw.off("drain", done);
|
|
2931
|
+
reply.raw.off("close", done);
|
|
2932
|
+
resolve();
|
|
2933
|
+
};
|
|
2934
|
+
reply.raw.once("drain", done);
|
|
2935
|
+
reply.raw.once("close", done);
|
|
2936
|
+
});
|
|
2937
|
+
};
|
|
2938
|
+
const resultFrame = (result) => ({
|
|
2939
|
+
jsonrpc: "2.0",
|
|
2940
|
+
id: requestId,
|
|
2941
|
+
result
|
|
2942
|
+
});
|
|
2943
|
+
let disconnected = false;
|
|
2944
|
+
let iterator;
|
|
2945
|
+
const closeIterator = async () => {
|
|
2946
|
+
if (!iterator) return;
|
|
2947
|
+
const current = iterator;
|
|
2948
|
+
iterator = void 0;
|
|
2949
|
+
try {
|
|
2950
|
+
await current.return?.();
|
|
2951
|
+
} catch {
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
const onClose = () => {
|
|
2955
|
+
disconnected = true;
|
|
2956
|
+
void closeIterator();
|
|
2957
|
+
};
|
|
2958
|
+
reply.raw.once("close", onClose);
|
|
2959
|
+
try {
|
|
2960
|
+
await writeFrame(resultFrame(task));
|
|
2961
|
+
if (disconnected || isTerminalA2AState(task.status.state)) return;
|
|
2962
|
+
const runId = asString3(task.metadata?.a2aRunId);
|
|
2963
|
+
if (!runId) return;
|
|
2964
|
+
const threadId = asString3(task.metadata?.threadId) ?? task.contextId;
|
|
2965
|
+
const messageId = asString3(task.metadata?.a2aMessageId);
|
|
2966
|
+
const cursor = asString3(task.metadata?.a2aStreamCursor);
|
|
2967
|
+
const runService = runServiceFor(scope);
|
|
2968
|
+
const reconcileCanonicalTask = async () => {
|
|
2969
|
+
const reconciled = await reconcileTaskFromRun(
|
|
2970
|
+
taskStore,
|
|
2971
|
+
runService,
|
|
2972
|
+
stateResolver,
|
|
2973
|
+
scope,
|
|
2974
|
+
request.body
|
|
2975
|
+
).catch(() => void 0);
|
|
2976
|
+
return reconciled ?? taskStore.load(taskId).catch(() => void 0);
|
|
2977
|
+
};
|
|
2978
|
+
const run = await runService.getRun(scope.tenantId, runId, threadId, scope.assistantId).catch(() => void 0);
|
|
2979
|
+
if (!run) return;
|
|
2980
|
+
if (TERMINAL_RUN_STATUSES.has(run.status)) {
|
|
2981
|
+
const canonical2 = await reconcileCanonicalTask();
|
|
2982
|
+
if (canonical2 && !disconnected) await writeFrame(resultFrame(canonical2));
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
if (!messageId) return;
|
|
2986
|
+
const source = runService.stream(
|
|
2987
|
+
threadId,
|
|
2988
|
+
scope.assistantId,
|
|
2989
|
+
messageId,
|
|
2990
|
+
cursor,
|
|
2991
|
+
{ tenantId: scope.tenantId, runId }
|
|
2992
|
+
);
|
|
2993
|
+
iterator = source[Symbol.asyncIterator]();
|
|
2994
|
+
try {
|
|
2995
|
+
for (; ; ) {
|
|
2996
|
+
if (disconnected) return;
|
|
2997
|
+
const next = await iterator.next();
|
|
2998
|
+
if (next.done) break;
|
|
2999
|
+
const chunk = next.value;
|
|
3000
|
+
const text = chunk.data?.content ?? "";
|
|
3001
|
+
if (chunk.type === MessageChunkTypes3.AI && text) {
|
|
3002
|
+
await writeFrame(resultFrame({
|
|
3003
|
+
kind: "status-update",
|
|
3004
|
+
taskId: task.id,
|
|
3005
|
+
contextId: task.contextId,
|
|
3006
|
+
status: {
|
|
3007
|
+
state: "working",
|
|
3008
|
+
message: agentMessage2(text),
|
|
3009
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3010
|
+
},
|
|
3011
|
+
final: false
|
|
3012
|
+
}));
|
|
3013
|
+
}
|
|
3014
|
+
if (chunk.type === MessageChunkTypes3.MESSAGE_COMPLETED || chunk.type === MessageChunkTypes3.MESSAGE_FAILED || chunk.type === MessageChunkTypes3.INTERRUPT) {
|
|
3015
|
+
break;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
} finally {
|
|
3019
|
+
await closeIterator();
|
|
3020
|
+
}
|
|
3021
|
+
if (disconnected) return;
|
|
3022
|
+
await runService.join(
|
|
3023
|
+
scope.tenantId,
|
|
3024
|
+
threadId,
|
|
3025
|
+
scope.assistantId,
|
|
3026
|
+
runId,
|
|
3027
|
+
RESUBSCRIBE_SETTLE_TIMEOUT_MS
|
|
3028
|
+
).catch(() => void 0);
|
|
3029
|
+
if (disconnected) return;
|
|
3030
|
+
const canonical = await reconcileCanonicalTask();
|
|
3031
|
+
if (canonical) await writeFrame(resultFrame(canonical));
|
|
3032
|
+
} catch (err) {
|
|
3033
|
+
if (err instanceof ChunkCursorExpiredError) {
|
|
3034
|
+
await writeFrame({
|
|
3035
|
+
jsonrpc: "2.0",
|
|
3036
|
+
id: requestId,
|
|
3037
|
+
error: {
|
|
3038
|
+
code: -32603,
|
|
3039
|
+
message: "CURSOR_EXPIRED",
|
|
3040
|
+
data: { code: "CURSOR_EXPIRED" }
|
|
3041
|
+
}
|
|
3042
|
+
}, "error");
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
3045
|
+
request.log.error({ err }, "a2a:jsonrpc:resubscribe:stream");
|
|
3046
|
+
await writeFrame({
|
|
3047
|
+
jsonrpc: "2.0",
|
|
3048
|
+
id: requestId,
|
|
3049
|
+
error: A2AError3.internalError(
|
|
3050
|
+
err instanceof Error ? err.message : "Streaming error."
|
|
3051
|
+
).toJSONRPCError()
|
|
3052
|
+
}, "error");
|
|
3053
|
+
} finally {
|
|
3054
|
+
reply.raw.off("close", onClose);
|
|
3055
|
+
await closeIterator();
|
|
3056
|
+
runtimeLease.release();
|
|
3057
|
+
if (!reply.raw.writableEnded) reply.raw.end();
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
3060
|
+
const handleAgentCard = async (request, reply) => {
|
|
3061
|
+
const auth = await authService.authenticate(
|
|
3062
|
+
getHeader(request.headers.authorization) ?? getHeader(request.headers["x-api-key"])
|
|
3063
|
+
);
|
|
3064
|
+
if (!auth.authenticated) {
|
|
3065
|
+
reply.status(401).send({ error: "Unauthorized" });
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
let assistant;
|
|
3069
|
+
try {
|
|
3070
|
+
assistant = await authService.assertAgentAccess(
|
|
3071
|
+
auth,
|
|
3072
|
+
request.params.assistantId
|
|
3073
|
+
);
|
|
3074
|
+
} catch (err) {
|
|
3075
|
+
if (err instanceof A2AAuthError) {
|
|
3076
|
+
reply.status(err.statusCode).send({ error: err.message });
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
request.log.error({ err }, "a2a:agent-card:auth");
|
|
3080
|
+
reply.status(500).send({ error: "Internal error" });
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
reply.header("A2A-Version", "0.3");
|
|
3084
|
+
const officialAlias = request.url.split("?")[0].startsWith("/a2a/");
|
|
3085
|
+
reply.status(200).send(
|
|
3086
|
+
buildAgentCardForAssistant({
|
|
3087
|
+
assistant,
|
|
3088
|
+
baseUrl: buildBaseUrl(request),
|
|
3089
|
+
pushNotifications: hasPushStore(),
|
|
3090
|
+
...officialAlias ? { jsonRpcPath: `/a2a/${assistant.id}` } : {}
|
|
3091
|
+
})
|
|
3092
|
+
);
|
|
3093
|
+
};
|
|
3094
|
+
app.get(
|
|
3095
|
+
"/api/a2a/agents/:assistantId/.well-known/agent-card.json",
|
|
3096
|
+
handleAgentCard
|
|
3097
|
+
);
|
|
3098
|
+
app.get(
|
|
3099
|
+
"/a2a/:assistantId/.well-known/agent-card.json",
|
|
3100
|
+
handleAgentCard
|
|
3101
|
+
);
|
|
3102
|
+
const handleJsonRpc = async (request, reply) => {
|
|
3103
|
+
const requestId = extractRequestId(request.body);
|
|
3104
|
+
const auth = await authService.authenticate(
|
|
3105
|
+
getHeader(request.headers.authorization) ?? getHeader(request.headers["x-api-key"])
|
|
3106
|
+
);
|
|
3107
|
+
if (!auth.authenticated) {
|
|
3108
|
+
reply.status(401).send(jsonRpcError(requestId, -32001, "Unauthorized"));
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
let assistant;
|
|
3112
|
+
try {
|
|
3113
|
+
assistant = await authService.assertAgentAccess(
|
|
3114
|
+
auth,
|
|
3115
|
+
request.params.assistantId
|
|
3116
|
+
);
|
|
3117
|
+
} catch (err) {
|
|
3118
|
+
if (err instanceof A2AAuthError) {
|
|
3119
|
+
reply.status(err.statusCode).send(
|
|
3120
|
+
jsonRpcError(
|
|
3121
|
+
requestId,
|
|
3122
|
+
authErrorToRpcCode(err.statusCode),
|
|
3123
|
+
err.message
|
|
3124
|
+
)
|
|
3125
|
+
);
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
request.log.error({ err }, "a2a:jsonrpc:auth");
|
|
3129
|
+
reply.status(500).send(jsonRpcError(requestId, -32603, "Internal error"));
|
|
3130
|
+
return;
|
|
3131
|
+
}
|
|
3132
|
+
const tenantId = auth.tenantId ?? "default";
|
|
3133
|
+
const scope = {
|
|
3134
|
+
tenantId,
|
|
3135
|
+
projectId: auth.projectId ?? "",
|
|
3136
|
+
workspaceId: await resolveWorkspaceId(getProjectStore, tenantId, auth.projectId),
|
|
3137
|
+
assistantId: assistant.id
|
|
3138
|
+
};
|
|
3139
|
+
const internalTasks = resolveTasks();
|
|
3140
|
+
const workItems = resolveTaskWorkItems();
|
|
3141
|
+
const taskStore = taskStoreFor(scope, internalTasks, workItems);
|
|
3142
|
+
const method = requestMethod(request.body);
|
|
3143
|
+
if (method === "tasks/list" || method === "agent/getExtendedAgentCard" || method === "agent/getAuthenticatedExtendedCard") {
|
|
3144
|
+
reply.header("A2A-Version", "0.3");
|
|
3145
|
+
if (method === "tasks/list") {
|
|
3146
|
+
const parsed = parseTasksListQuery(request.body);
|
|
3147
|
+
if (!parsed.ok) {
|
|
3148
|
+
reply.send(jsonRpcError(requestId, -32602, parsed.message));
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
try {
|
|
3152
|
+
const result2 = await listCanonicalA2ATasks(
|
|
3153
|
+
internalTasks,
|
|
3154
|
+
taskStore,
|
|
3155
|
+
scope,
|
|
3156
|
+
parsed.query
|
|
3157
|
+
);
|
|
3158
|
+
reply.send({ jsonrpc: "2.0", id: requestId, result: result2 });
|
|
3159
|
+
} catch (err) {
|
|
3160
|
+
request.log.error({ err }, "a2a:jsonrpc:tasks-list");
|
|
3161
|
+
reply.send(jsonRpcError(requestId, -32603, "Internal error"));
|
|
3162
|
+
}
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const extendedCard = buildAgentCardForAssistant({
|
|
3166
|
+
assistant,
|
|
3167
|
+
baseUrl: buildBaseUrl(request),
|
|
3168
|
+
pushNotifications: hasPushStore(),
|
|
3169
|
+
...request.url.split("?")[0].startsWith("/a2a/") ? { jsonRpcPath: `/a2a/${assistant.id}` } : {}
|
|
3170
|
+
});
|
|
3171
|
+
reply.send({ jsonrpc: "2.0", id: requestId, result: extendedCard });
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
if (method === "tasks/resubscribe") {
|
|
3175
|
+
await handleTaskResubscribe(request, reply, requestId, scope, taskStore);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
const ingestor = makeFileIngestor(scope.tenantId, deps.maxFileBytes);
|
|
3179
|
+
const runService = runServiceFor(scope);
|
|
3180
|
+
const executor = new AxiomAgentExecutor({
|
|
3181
|
+
scope,
|
|
3182
|
+
assistant,
|
|
3183
|
+
taskStore,
|
|
3184
|
+
ingestor,
|
|
3185
|
+
runService,
|
|
3186
|
+
blocking: blockingFor(request.body),
|
|
3187
|
+
getInterrupts: (threadId) => stateResolver.getInterrupts(threadId, assistant.id, scope)
|
|
3188
|
+
});
|
|
3189
|
+
try {
|
|
3190
|
+
await reconcileTaskFromRun(
|
|
3191
|
+
taskStore,
|
|
3192
|
+
runService,
|
|
3193
|
+
stateResolver,
|
|
3194
|
+
scope,
|
|
3195
|
+
request.body
|
|
3196
|
+
);
|
|
3197
|
+
const canceled = await cancelTaskFromRun(
|
|
3198
|
+
taskStore,
|
|
3199
|
+
runService,
|
|
3200
|
+
scope,
|
|
3201
|
+
request.body
|
|
3202
|
+
);
|
|
3203
|
+
if (canceled) {
|
|
3204
|
+
reply.header("A2A-Version", "0.3");
|
|
3205
|
+
reply.send({ jsonrpc: "2.0", id: requestId, result: canceled });
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
} catch (err) {
|
|
3209
|
+
if (err instanceof A2AError3) {
|
|
3210
|
+
reply.send(jsonRpcError(requestId, err.code, err.message));
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
request.log.error({ err }, "a2a:jsonrpc:reconcile");
|
|
3214
|
+
reply.status(500).send(jsonRpcError(requestId, -32603, "Internal error"));
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
const resumeError = await resumeValidationError(taskStore, request.body).catch(() => void 0);
|
|
3218
|
+
if (resumeError !== void 0) {
|
|
3219
|
+
reply.send(jsonRpcError(requestId, -32602, resumeError));
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
let pushStore;
|
|
3223
|
+
try {
|
|
3224
|
+
pushStore = createPushStore(scope);
|
|
3225
|
+
} catch (err) {
|
|
3226
|
+
request.log.warn({ err }, "a2a:jsonrpc:push-store-unavailable");
|
|
3227
|
+
}
|
|
3228
|
+
const card = buildAgentCardForAssistant({
|
|
3229
|
+
assistant,
|
|
3230
|
+
baseUrl: buildBaseUrl(request),
|
|
3231
|
+
pushNotifications: pushStore !== void 0,
|
|
3232
|
+
...request.url.split("?")[0].startsWith("/a2a/") ? { jsonRpcPath: `/a2a/${assistant.id}` } : {}
|
|
3233
|
+
});
|
|
3234
|
+
let runtimeLease;
|
|
3235
|
+
try {
|
|
3236
|
+
runtimeLease = runtimeRegistry.acquire(scope);
|
|
3237
|
+
} catch (err) {
|
|
3238
|
+
if (err instanceof A2ARuntimeCapacityError) {
|
|
3239
|
+
request.log.error({ err, scope }, "a2a:jsonrpc:runtime_capacity");
|
|
3240
|
+
reply.status(503).send(jsonRpcError(requestId, -32603, "A2A runtime capacity exhausted"));
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
throw err;
|
|
3244
|
+
}
|
|
3245
|
+
let result;
|
|
3246
|
+
try {
|
|
3247
|
+
const pushSender = pushStore ? deps.getPushNotificationSender?.(pushStore) ?? new A2APushNotificationSender({
|
|
3248
|
+
store: pushStore,
|
|
3249
|
+
...deps.resolvePushHost ? { resolveHost: deps.resolvePushHost } : {}
|
|
3250
|
+
}) : void 0;
|
|
3251
|
+
const handler = new DefaultRequestHandler(
|
|
3252
|
+
card,
|
|
3253
|
+
taskStore,
|
|
3254
|
+
executor,
|
|
3255
|
+
runtimeLease.manager,
|
|
3256
|
+
pushStore,
|
|
3257
|
+
pushSender
|
|
3258
|
+
);
|
|
3259
|
+
const transport = new JsonRpcTransportHandler(handler);
|
|
3260
|
+
reply.header("A2A-Version", "0.3");
|
|
3261
|
+
result = await transport.handle(request.body);
|
|
3262
|
+
} catch (err) {
|
|
3263
|
+
runtimeLease.release();
|
|
3264
|
+
if (err instanceof A2AError3) {
|
|
3265
|
+
reply.send(jsonRpcError(requestId, err.code, err.message));
|
|
3266
|
+
return;
|
|
3267
|
+
}
|
|
3268
|
+
request.log.error({ err }, "a2a:jsonrpc:handler");
|
|
3269
|
+
reply.status(500).send(jsonRpcError(requestId, -32603, "Internal error"));
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
if (isAsyncGenerator(result)) {
|
|
3273
|
+
reply.hijack();
|
|
3274
|
+
reply.raw.writeHead(200, SSE_HEADERS);
|
|
3275
|
+
try {
|
|
3276
|
+
for await (const evt of result) {
|
|
3277
|
+
reply.raw.write(`data: ${JSON.stringify(evt)}
|
|
3278
|
+
|
|
3279
|
+
`);
|
|
3280
|
+
}
|
|
3281
|
+
} catch (err) {
|
|
3282
|
+
request.log.error({ err }, "a2a:jsonrpc:stream");
|
|
3283
|
+
const errorResponse = {
|
|
3284
|
+
jsonrpc: "2.0",
|
|
3285
|
+
id: requestId,
|
|
3286
|
+
error: A2AError3.internalError(
|
|
3287
|
+
err instanceof Error ? err.message : "Streaming error."
|
|
3288
|
+
).toJSONRPCError()
|
|
3289
|
+
};
|
|
3290
|
+
reply.raw.write(`event: error
|
|
3291
|
+
data: ${JSON.stringify(errorResponse)}
|
|
3292
|
+
|
|
3293
|
+
`);
|
|
3294
|
+
} finally {
|
|
3295
|
+
runtimeLease.release();
|
|
3296
|
+
reply.raw.end();
|
|
3297
|
+
}
|
|
3298
|
+
return;
|
|
3299
|
+
}
|
|
3300
|
+
runtimeLease.release();
|
|
3301
|
+
reply.send(result);
|
|
3302
|
+
};
|
|
3303
|
+
app.post(
|
|
3304
|
+
"/api/a2a/agents/:assistantId/jsonrpc",
|
|
3305
|
+
handleJsonRpc
|
|
3306
|
+
);
|
|
3307
|
+
app.post(
|
|
3308
|
+
"/a2a/:assistantId",
|
|
3309
|
+
handleJsonRpc
|
|
3310
|
+
);
|
|
3311
|
+
app.post(
|
|
3312
|
+
"/api/a2a/agents/:assistantId/test-call",
|
|
3313
|
+
async (request, reply) => {
|
|
3314
|
+
const user = getSessionUser(request);
|
|
3315
|
+
if (!user?.tenantId) {
|
|
3316
|
+
reply.status(401).send({ success: false, error: "Unauthorized" });
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
const tenantId = user.tenantId;
|
|
3320
|
+
const body = request.body ?? {};
|
|
3321
|
+
const { projectId, message } = body;
|
|
3322
|
+
if (!projectId) {
|
|
3323
|
+
reply.status(400).send({ error: "projectId is required" });
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
if (!message || !message.trim()) {
|
|
3327
|
+
reply.status(400).send({ error: "message is required" });
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
const assistant = await getAssistantStore().getAssistantById(
|
|
3331
|
+
tenantId,
|
|
3332
|
+
request.params.assistantId
|
|
3333
|
+
);
|
|
3334
|
+
if (!assistant) {
|
|
3335
|
+
reply.status(404).send({ error: "Assistant not found" });
|
|
3336
|
+
return;
|
|
3337
|
+
}
|
|
3338
|
+
const project = await getProjectStore().getProjectById(
|
|
3339
|
+
tenantId,
|
|
3340
|
+
projectId
|
|
3341
|
+
);
|
|
3342
|
+
if (!project || project.tenantId !== tenantId) {
|
|
3343
|
+
reply.status(403).send({ error: "Project not found" });
|
|
3344
|
+
return;
|
|
3345
|
+
}
|
|
3346
|
+
const scope = {
|
|
3347
|
+
tenantId,
|
|
3348
|
+
projectId,
|
|
3349
|
+
workspaceId: project.workspaceId,
|
|
3350
|
+
assistantId: assistant.id
|
|
3351
|
+
};
|
|
3352
|
+
const ingestor = makeFileIngestor(scope.tenantId, deps.maxFileBytes);
|
|
3353
|
+
const internalTasks = resolveTasks();
|
|
3354
|
+
const workItems = resolveTaskWorkItems();
|
|
3355
|
+
const taskStore = taskStoreFor(scope, internalTasks, workItems);
|
|
3356
|
+
const executor = new AxiomAgentExecutor({
|
|
3357
|
+
scope,
|
|
3358
|
+
assistant,
|
|
3359
|
+
taskStore,
|
|
3360
|
+
ingestor,
|
|
3361
|
+
runService: runServiceFor(scope),
|
|
3362
|
+
blocking: true,
|
|
3363
|
+
getInterrupts: (threadId) => stateResolver.getInterrupts(threadId, assistant.id, scope)
|
|
3364
|
+
});
|
|
3365
|
+
const taskId = uuidv42();
|
|
3366
|
+
const contextId = uuidv42();
|
|
3367
|
+
const userMessage = {
|
|
3368
|
+
kind: "message",
|
|
3369
|
+
messageId: uuidv42(),
|
|
3370
|
+
role: "user",
|
|
3371
|
+
parts: [{ kind: "text", text: message }]
|
|
3372
|
+
};
|
|
3373
|
+
const requestContext = new RequestContext(userMessage, taskId, contextId);
|
|
3374
|
+
const bus = new DefaultExecutionEventBus2();
|
|
3375
|
+
reply.hijack();
|
|
3376
|
+
reply.raw.writeHead(200, SSE_HEADERS);
|
|
3377
|
+
const sendEvent = (event, data) => {
|
|
3378
|
+
reply.raw.write(`event: ${event}
|
|
3379
|
+
data: ${JSON.stringify(data)}
|
|
3380
|
+
|
|
3381
|
+
`);
|
|
3382
|
+
};
|
|
3383
|
+
bus.on("event", (event) => {
|
|
3384
|
+
sendEvent(event.kind, event);
|
|
3385
|
+
});
|
|
3386
|
+
try {
|
|
3387
|
+
await executor.execute(requestContext, bus);
|
|
3388
|
+
} catch (err) {
|
|
3389
|
+
request.log.error({ err }, "a2a:test-call:execution");
|
|
3390
|
+
try {
|
|
3391
|
+
await taskStore.save({
|
|
3392
|
+
kind: "task",
|
|
3393
|
+
id: taskId,
|
|
3394
|
+
contextId,
|
|
3395
|
+
status: { state: "failed", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3396
|
+
});
|
|
3397
|
+
} catch (saveError) {
|
|
3398
|
+
request.log.error({ err: saveError }, "a2a:test-call:settle");
|
|
3399
|
+
}
|
|
3400
|
+
sendEvent("status-update", {
|
|
3401
|
+
kind: "status-update",
|
|
3402
|
+
taskId,
|
|
3403
|
+
contextId,
|
|
3404
|
+
status: { state: "failed", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3405
|
+
final: true
|
|
3406
|
+
});
|
|
3407
|
+
} finally {
|
|
3408
|
+
reply.raw.end();
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
);
|
|
3412
|
+
}
|
|
3413
|
+
export {
|
|
3414
|
+
registerA2AStandardRoutes
|
|
3415
|
+
};
|
|
3416
|
+
//# sourceMappingURL=a2a-standard-DP53K7RC.mjs.map
|