@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/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
- await adapter.sendReply(
6813
- message.replyTarget,
6814
- { text: ctx.result || "" },
6815
- installation
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);