@axiom-lattice/gateway 2.1.88 → 2.1.90
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/.turbo/turbo-build.log +14 -12
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +22 -0
- package/dist/a2a-ERG5RMUW.mjs +567 -0
- package/dist/a2a-ERG5RMUW.mjs.map +1 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +879 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +294 -16
- package/dist/index.mjs.map +1 -1
- package/jest.config.js +1 -1
- package/package.json +6 -6
- package/src/__tests__/a2a.test.ts +346 -0
- package/src/index.ts +19 -0
- package/src/router/MessageRouter.ts +169 -17
- package/src/routes/a2a-bridge.ts +266 -0
- package/src/routes/a2a.ts +779 -0
- package/src/routes/index.ts +5 -0
package/dist/index.mjs
CHANGED
|
@@ -6409,8 +6409,183 @@ function registerChannelBindingRoutes(app2) {
|
|
|
6409
6409
|
app2.delete("/api/channel-bindings/:id", deleteBinding);
|
|
6410
6410
|
}
|
|
6411
6411
|
|
|
6412
|
+
// src/routes/a2a-bridge.ts
|
|
6413
|
+
import { v4 as v42 } from "uuid";
|
|
6414
|
+
var log = {
|
|
6415
|
+
info(msg, ctx) {
|
|
6416
|
+
console.log(JSON.stringify({ level: "info", msg, ...ctx }));
|
|
6417
|
+
},
|
|
6418
|
+
warn(msg, ctx) {
|
|
6419
|
+
console.warn(JSON.stringify({ level: "warn", msg, ...ctx }));
|
|
6420
|
+
},
|
|
6421
|
+
error(msg, ctx) {
|
|
6422
|
+
console.error(JSON.stringify({ level: "error", msg, ...ctx }));
|
|
6423
|
+
}
|
|
6424
|
+
};
|
|
6425
|
+
var connections = /* @__PURE__ */ new Map();
|
|
6426
|
+
var pending = /* @__PURE__ */ new Map();
|
|
6427
|
+
function getAgentIdFromPath(path3) {
|
|
6428
|
+
const match = path3.match(/^\/api\/a2a\/bridge\/proxy\/([^/]+)/);
|
|
6429
|
+
return match ? match[1] : null;
|
|
6430
|
+
}
|
|
6431
|
+
function handleBridgeConnection(socket, _request) {
|
|
6432
|
+
let agentId = null;
|
|
6433
|
+
log.info("Bridge WebSocket connected");
|
|
6434
|
+
socket.on("message", (raw) => {
|
|
6435
|
+
let msg;
|
|
6436
|
+
try {
|
|
6437
|
+
msg = JSON.parse(raw.toString());
|
|
6438
|
+
} catch {
|
|
6439
|
+
log.warn("Bridge invalid message");
|
|
6440
|
+
return;
|
|
6441
|
+
}
|
|
6442
|
+
switch (msg.type) {
|
|
6443
|
+
case "agent:register": {
|
|
6444
|
+
agentId = msg.agentId;
|
|
6445
|
+
if (!agentId) {
|
|
6446
|
+
socket.send(JSON.stringify({ type: "error", message: "agentId required" }));
|
|
6447
|
+
socket.close();
|
|
6448
|
+
return;
|
|
6449
|
+
}
|
|
6450
|
+
connections.set(agentId, {
|
|
6451
|
+
ws: socket,
|
|
6452
|
+
agentId,
|
|
6453
|
+
agentCard: msg.agentCard ?? {},
|
|
6454
|
+
connectedAt: Date.now()
|
|
6455
|
+
});
|
|
6456
|
+
log.info("Agent registered via bridge", { agentId });
|
|
6457
|
+
socket.send(JSON.stringify({ type: "agent:registered", agentId }));
|
|
6458
|
+
break;
|
|
6459
|
+
}
|
|
6460
|
+
case "http:response": {
|
|
6461
|
+
const requestId = msg.requestId;
|
|
6462
|
+
const p = pending.get(requestId);
|
|
6463
|
+
if (p) {
|
|
6464
|
+
clearTimeout(p.timer);
|
|
6465
|
+
pending.delete(requestId);
|
|
6466
|
+
p.resolve({
|
|
6467
|
+
status: msg.status ?? 200,
|
|
6468
|
+
headers: msg.headers ?? {},
|
|
6469
|
+
body: msg.body ?? ""
|
|
6470
|
+
});
|
|
6471
|
+
}
|
|
6472
|
+
break;
|
|
6473
|
+
}
|
|
6474
|
+
case "http:error": {
|
|
6475
|
+
const requestId = msg.requestId;
|
|
6476
|
+
const p = pending.get(requestId);
|
|
6477
|
+
if (p) {
|
|
6478
|
+
clearTimeout(p.timer);
|
|
6479
|
+
pending.delete(requestId);
|
|
6480
|
+
p.reject(new Error(msg.message ?? "Agent error"));
|
|
6481
|
+
}
|
|
6482
|
+
break;
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
});
|
|
6486
|
+
socket.on("close", () => {
|
|
6487
|
+
if (agentId) {
|
|
6488
|
+
connections.delete(agentId);
|
|
6489
|
+
log.info("Bridge WebSocket disconnected", { agentId });
|
|
6490
|
+
for (const [reqId, p] of pending) {
|
|
6491
|
+
if (p.agentId === agentId) {
|
|
6492
|
+
clearTimeout(p.timer);
|
|
6493
|
+
p.reject(new Error("Agent disconnected"));
|
|
6494
|
+
pending.delete(reqId);
|
|
6495
|
+
}
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
});
|
|
6499
|
+
socket.on("error", (err) => {
|
|
6500
|
+
log.error("Bridge WebSocket error", { agentId, error: err.message });
|
|
6501
|
+
});
|
|
6502
|
+
}
|
|
6503
|
+
async function handleBridgeProxy(request, reply) {
|
|
6504
|
+
const agentId = getAgentIdFromPath(request.url);
|
|
6505
|
+
if (!agentId) {
|
|
6506
|
+
reply.status(400).send({ error: "Agent ID not found in path" });
|
|
6507
|
+
return;
|
|
6508
|
+
}
|
|
6509
|
+
const conn = connections.get(agentId);
|
|
6510
|
+
if (!conn) {
|
|
6511
|
+
reply.status(503).send({
|
|
6512
|
+
error: "Agent not connected",
|
|
6513
|
+
message: `Agent "${agentId}" is not connected via WebSocket bridge`
|
|
6514
|
+
});
|
|
6515
|
+
return;
|
|
6516
|
+
}
|
|
6517
|
+
const requestId = v42();
|
|
6518
|
+
const subPath = request.url.replace(`/api/a2a/bridge/proxy/${agentId}`, "") || "/";
|
|
6519
|
+
let body;
|
|
6520
|
+
if (request.method === "POST" || request.method === "PUT") {
|
|
6521
|
+
body = typeof request.body === "string" ? request.body : JSON.stringify(request.body ?? {});
|
|
6522
|
+
}
|
|
6523
|
+
const bridgeRequest = {
|
|
6524
|
+
type: "http:request",
|
|
6525
|
+
requestId,
|
|
6526
|
+
method: request.method,
|
|
6527
|
+
path: subPath,
|
|
6528
|
+
headers: {
|
|
6529
|
+
"content-type": request.headers["content-type"] ?? "application/json",
|
|
6530
|
+
"accept": request.headers["accept"] ?? "*/*"
|
|
6531
|
+
},
|
|
6532
|
+
body: body ?? ""
|
|
6533
|
+
};
|
|
6534
|
+
const timeout = 3e5;
|
|
6535
|
+
try {
|
|
6536
|
+
const response = await new Promise((resolve, reject) => {
|
|
6537
|
+
const timer = setTimeout(() => {
|
|
6538
|
+
pending.delete(requestId);
|
|
6539
|
+
reject(new Error(`Bridge request timeout after ${timeout}ms`));
|
|
6540
|
+
}, timeout);
|
|
6541
|
+
pending.set(requestId, { resolve, reject, timer, agentId });
|
|
6542
|
+
try {
|
|
6543
|
+
if (conn.ws.readyState !== conn.ws.OPEN) {
|
|
6544
|
+
pending.delete(requestId);
|
|
6545
|
+
reject(new Error("WebSocket not open"));
|
|
6546
|
+
return;
|
|
6547
|
+
}
|
|
6548
|
+
conn.ws.send(JSON.stringify(bridgeRequest));
|
|
6549
|
+
} catch (err) {
|
|
6550
|
+
clearTimeout(timer);
|
|
6551
|
+
pending.delete(requestId);
|
|
6552
|
+
reject(err);
|
|
6553
|
+
}
|
|
6554
|
+
});
|
|
6555
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
6556
|
+
if (!["content-length", "transfer-encoding", "connection"].includes(key.toLowerCase())) {
|
|
6557
|
+
reply.header(key, value);
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6560
|
+
reply.status(response.status);
|
|
6561
|
+
try {
|
|
6562
|
+
const parsed = JSON.parse(response.body);
|
|
6563
|
+
reply.send(parsed);
|
|
6564
|
+
} catch {
|
|
6565
|
+
reply.send(response.body);
|
|
6566
|
+
}
|
|
6567
|
+
} catch (err) {
|
|
6568
|
+
log.error("Bridge proxy error", { agentId, error: err.message });
|
|
6569
|
+
reply.status(502).send({
|
|
6570
|
+
error: "Bridge proxy error",
|
|
6571
|
+
message: err.message
|
|
6572
|
+
});
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
function registerA2ABridgeRoutes(app2) {
|
|
6576
|
+
app2.get("/api/a2a/bridge", { websocket: true }, (socket, req) => {
|
|
6577
|
+
handleBridgeConnection(socket, req);
|
|
6578
|
+
});
|
|
6579
|
+
app2.route({
|
|
6580
|
+
method: ["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
6581
|
+
url: "/api/a2a/bridge/proxy/*",
|
|
6582
|
+
handler: handleBridgeProxy
|
|
6583
|
+
});
|
|
6584
|
+
}
|
|
6585
|
+
|
|
6412
6586
|
// src/routes/index.ts
|
|
6413
6587
|
var registerLatticeRoutes = (app2, channelDeps) => {
|
|
6588
|
+
registerA2ABridgeRoutes(app2);
|
|
6414
6589
|
app2.post("/api/runs", createRun);
|
|
6415
6590
|
app2.post("/api/resume_stream", resumeStream);
|
|
6416
6591
|
app2.post("/api/assistants/:assistantId/threads/:threadId/abort", abortRun);
|
|
@@ -6637,14 +6812,39 @@ var BindingNotFoundError = class extends Error {
|
|
|
6637
6812
|
};
|
|
6638
6813
|
var MessageRouter = class {
|
|
6639
6814
|
constructor(config) {
|
|
6815
|
+
/**
|
|
6816
|
+
* Tracks reply subscriptions per thread+channel to avoid duplicate
|
|
6817
|
+
* `subscribeOnce` registrations and ensure proper cleanup.
|
|
6818
|
+
*
|
|
6819
|
+
* Key format: `{threadId}:{adapterChannel}:reply`
|
|
6820
|
+
*/
|
|
6821
|
+
this._replySubs = /* @__PURE__ */ new Map();
|
|
6640
6822
|
this.middlewares = [...config.middlewares];
|
|
6641
6823
|
this.bindingRegistry = config.bindingRegistry;
|
|
6642
6824
|
this.adapterRegistry = config.adapterRegistry;
|
|
6643
6825
|
this.installationStore = config.installationStore;
|
|
6644
6826
|
}
|
|
6827
|
+
/**
|
|
6828
|
+
* Register an additional middleware at the end of the chain.
|
|
6829
|
+
*
|
|
6830
|
+
* @param middleware - A {@link MessageMiddleware} function
|
|
6831
|
+
*/
|
|
6645
6832
|
use(middleware) {
|
|
6646
6833
|
this.middlewares.push(middleware);
|
|
6647
6834
|
}
|
|
6835
|
+
/**
|
|
6836
|
+
* Dispatch an inbound channel message to the bound agent.
|
|
6837
|
+
*
|
|
6838
|
+
* Full pipeline: middleware chain → binding resolution → thread lifecycle
|
|
6839
|
+
* → agent.addMessage() → (async) reply via {@link ChannelAdapter.sendReply}.
|
|
6840
|
+
*
|
|
6841
|
+
* If the message has a {@link InboundMessage.replyTarget}, the router subscribes
|
|
6842
|
+
* to the agent's `reply:ready` event before enqueuing the message, and sends
|
|
6843
|
+
* the AI response back to the channel when it arrives.
|
|
6844
|
+
*
|
|
6845
|
+
* @param message - Normalized inbound message from a channel adapter
|
|
6846
|
+
* @returns {@link DispatchResult} with success status, bindingId, and threadId
|
|
6847
|
+
*/
|
|
6648
6848
|
async dispatch(message) {
|
|
6649
6849
|
const ctx = {
|
|
6650
6850
|
inboundMessage: message,
|
|
@@ -6791,32 +6991,96 @@ var MessageRouter = class {
|
|
|
6791
6991
|
workspace_id: ctx.binding.workspaceId || "",
|
|
6792
6992
|
project_id: ctx.binding.projectId || ""
|
|
6793
6993
|
});
|
|
6794
|
-
const addResult = await agent.addMessage({
|
|
6795
|
-
input: { message: message.content.text },
|
|
6796
|
-
custom_run_config: message.content.metadata || {}
|
|
6797
|
-
});
|
|
6798
|
-
console.log({
|
|
6799
|
-
event: "dispatch:complete",
|
|
6800
|
-
agentId: ctx.binding.agentId,
|
|
6801
|
-
threadId,
|
|
6802
|
-
messageId: addResult?.messageId,
|
|
6803
|
-
result: JSON.stringify(addResult)
|
|
6804
|
-
}, "Agent dispatch complete \u2014 messageId = " + (addResult?.messageId || "N/A"));
|
|
6805
6994
|
if (message.replyTarget) {
|
|
6995
|
+
const replySubKey = `${threadId}:${message.replyTarget.adapterChannel}:reply`;
|
|
6806
6996
|
const adapter = this.adapterRegistry.get(message.replyTarget.adapterChannel);
|
|
6807
6997
|
if (adapter) {
|
|
6808
6998
|
const installation = await this.installationStore.getInstallationById(
|
|
6809
6999
|
message.channelInstallationId
|
|
6810
7000
|
);
|
|
6811
7001
|
if (installation) {
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
7002
|
+
const existing = this._replySubs.get(replySubKey);
|
|
7003
|
+
if (!existing || existing.count === 0) {
|
|
7004
|
+
const timer = setTimeout(() => {
|
|
7005
|
+
const entry = this._replySubs.get(replySubKey);
|
|
7006
|
+
if (entry) {
|
|
7007
|
+
this._replySubs.delete(replySubKey);
|
|
7008
|
+
console.warn({
|
|
7009
|
+
event: "dispatch:reply:timeout",
|
|
7010
|
+
threadId,
|
|
7011
|
+
channel: message.replyTarget.adapterChannel
|
|
7012
|
+
}, "Reply subscription timed out \u2014 no reply:ready fired within 1h");
|
|
7013
|
+
}
|
|
7014
|
+
}, 36e5);
|
|
7015
|
+
this._replySubs.set(replySubKey, { count: 1, timer });
|
|
7016
|
+
console.log({
|
|
7017
|
+
event: "dispatch:reply:subscribed",
|
|
7018
|
+
threadId,
|
|
7019
|
+
channel: message.replyTarget.adapterChannel,
|
|
7020
|
+
senderId: message.sender.id
|
|
7021
|
+
}, "Subscribed to reply:ready for thread");
|
|
7022
|
+
agent.subscribeOnce("reply:ready", (data) => {
|
|
7023
|
+
const messages = data.state?.values?.messages;
|
|
7024
|
+
const lastAI = messages?.filter((m) => m.type === "ai" || m.getType?.() === "ai").pop();
|
|
7025
|
+
const replyText = lastAI?.content ?? "";
|
|
7026
|
+
const entry = this._replySubs.get(replySubKey);
|
|
7027
|
+
if (entry) {
|
|
7028
|
+
entry.count--;
|
|
7029
|
+
if (entry.count <= 0) {
|
|
7030
|
+
clearTimeout(entry.timer);
|
|
7031
|
+
this._replySubs.delete(replySubKey);
|
|
7032
|
+
}
|
|
7033
|
+
}
|
|
7034
|
+
if (replyText) {
|
|
7035
|
+
console.log({
|
|
7036
|
+
event: "dispatch:reply:sending",
|
|
7037
|
+
threadId,
|
|
7038
|
+
channel: message.replyTarget.adapterChannel,
|
|
7039
|
+
replyLength: replyText.length
|
|
7040
|
+
}, "Sending channel reply");
|
|
7041
|
+
adapter.sendReply(message.replyTarget, { text: replyText }, installation).then(() => {
|
|
7042
|
+
console.log({
|
|
7043
|
+
event: "dispatch:reply:sent",
|
|
7044
|
+
threadId,
|
|
7045
|
+
channel: message.replyTarget.adapterChannel
|
|
7046
|
+
}, "Channel reply sent successfully");
|
|
7047
|
+
}).catch((err) => console.error({
|
|
7048
|
+
event: "dispatch:reply:failed",
|
|
7049
|
+
threadId,
|
|
7050
|
+
channel: message.replyTarget.adapterChannel,
|
|
7051
|
+
error: err instanceof Error ? err.message : String(err)
|
|
7052
|
+
}, "Failed to send channel reply"));
|
|
7053
|
+
} else {
|
|
7054
|
+
console.warn({
|
|
7055
|
+
event: "dispatch:reply:empty",
|
|
7056
|
+
threadId,
|
|
7057
|
+
channel: message.replyTarget.adapterChannel
|
|
7058
|
+
}, "Agent produced no text output \u2014 skipping reply");
|
|
7059
|
+
}
|
|
7060
|
+
});
|
|
7061
|
+
} else {
|
|
7062
|
+
existing.count++;
|
|
7063
|
+
console.log({
|
|
7064
|
+
event: "dispatch:reply:incremented",
|
|
7065
|
+
threadId,
|
|
7066
|
+
channel: message.replyTarget.adapterChannel,
|
|
7067
|
+
count: existing.count
|
|
7068
|
+
}, "Incremented reply counter for thread (already subscribed)");
|
|
7069
|
+
}
|
|
6817
7070
|
}
|
|
6818
7071
|
}
|
|
6819
7072
|
}
|
|
7073
|
+
const addResult = await agent.addMessage({
|
|
7074
|
+
input: { message: message.content.text },
|
|
7075
|
+
custom_run_config: message.replyTarget ? { ...message.content.metadata || {}, _replyTarget: message.replyTarget } : message.content.metadata || {}
|
|
7076
|
+
});
|
|
7077
|
+
console.log({
|
|
7078
|
+
event: "dispatch:complete",
|
|
7079
|
+
agentId: ctx.binding.agentId,
|
|
7080
|
+
threadId,
|
|
7081
|
+
messageId: addResult?.messageId,
|
|
7082
|
+
result: JSON.stringify(addResult)
|
|
7083
|
+
}, "Agent dispatch complete \u2014 messageId = " + (addResult?.messageId || "N/A"));
|
|
6820
7084
|
});
|
|
6821
7085
|
return {
|
|
6822
7086
|
success: true,
|
|
@@ -7357,6 +7621,12 @@ app.addHook("preHandler", async (request, reply) => {
|
|
|
7357
7621
|
error: "Unauthorized - Missing or invalid token"
|
|
7358
7622
|
});
|
|
7359
7623
|
});
|
|
7624
|
+
app.addHook("onRequest", (request, reply, done) => {
|
|
7625
|
+
if (!request.headers["x-project-id"]) {
|
|
7626
|
+
request.headers["x-project-id"] = "default";
|
|
7627
|
+
}
|
|
7628
|
+
done();
|
|
7629
|
+
});
|
|
7360
7630
|
app.addHook("onRequest", (request, reply, done) => {
|
|
7361
7631
|
const context = {
|
|
7362
7632
|
"x-tenant-id": getHeaderValue(request.headers["x-tenant-id"]),
|
|
@@ -7464,6 +7734,14 @@ var start = async (config) => {
|
|
|
7464
7734
|
installationStore
|
|
7465
7735
|
});
|
|
7466
7736
|
channelDeps = { router, installationStore };
|
|
7737
|
+
try {
|
|
7738
|
+
const a2aKeyStore = getStoreLattice16("default", "a2aApiKey").store;
|
|
7739
|
+
const a2a = await import("./a2a-ERG5RMUW.mjs");
|
|
7740
|
+
a2a.setA2AKeyStore(a2aKeyStore);
|
|
7741
|
+
await a2a.refreshStoreKeyMap();
|
|
7742
|
+
logger3.info("A2A key store initialized");
|
|
7743
|
+
} catch {
|
|
7744
|
+
}
|
|
7467
7745
|
} catch {
|
|
7468
7746
|
}
|
|
7469
7747
|
registerLatticeRoutes(app, channelDeps);
|