@axiom-lattice/gateway 2.1.88 → 2.1.89

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @axiom-lattice/gateway@2.1.88 build /home/runner/work/agentic/agentic/packages/gateway
2
+ > @axiom-lattice/gateway@2.1.89 build /home/runner/work/agentic/agentic/packages/gateway
3
3
  > tsup src/index.ts --format cjs,esm --dts --clean --sourcemap
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -18,15 +18,15 @@
18
18
  You need to set the output format to "esm" for "import.meta" to work correctly.
19
19
 
20
20
 
21
- ESM dist/index.mjs 237.01 KB
21
+ CJS dist/index.js 246.20 KB
22
+ CJS dist/index.js.map 515.19 KB
23
+ CJS ⚡️ Build success in 415ms
24
+ ESM dist/index.mjs 241.30 KB
22
25
  ESM dist/sender-PX32VSHB.mjs 873.00 B
23
- ESM dist/index.mjs.map 504.85 KB
24
26
  ESM dist/sender-PX32VSHB.mjs.map 2.07 KB
25
- ESM ⚡️ Build success in 417ms
26
- CJS dist/index.js 241.90 KB
27
- CJS dist/index.js.map 506.42 KB
28
- CJS ⚡️ Build success in 418ms
27
+ ESM dist/index.mjs.map 513.63 KB
28
+ ESM ⚡️ Build success in 415ms
29
29
  DTS Build start
30
- DTS ⚡️ Build success in 13918ms
31
- DTS dist/index.d.ts 5.01 KB
32
- DTS dist/index.d.mts 5.01 KB
30
+ DTS ⚡️ Build success in 14563ms
31
+ DTS dist/index.d.ts 7.57 KB
32
+ DTS dist/index.d.mts 7.57 KB
package/AGENTS.md CHANGED
@@ -24,6 +24,12 @@ src/
24
24
  queue_service.ts # Task queue (memory/redis)
25
25
  agent_task_consumer.ts # Queue consumer
26
26
  sandbox_service.ts # Sandbox proxy
27
+ channels/ # External channel adapters (Lark, Slack, etc.)
28
+ lark/ # Lark (Feishu) adapter
29
+ registry.ts # ChannelAdapterRegistry
30
+ router/ # Message routing
31
+ MessageRouter.ts # Inbound message dispatch + channel reply
32
+ MessageContext.ts # Request-scoped context
27
33
  schemas/index.ts # Zod/Fastify schemas
28
34
  logger/Logger.ts # Logger lattice integration
29
35
  ```
@@ -40,6 +46,62 @@ src/
40
46
  - **DB configs**: `routes/index.ts`, `controllers/database-configs.ts`
41
47
  - **Queue setup**: `services/queue_service.ts`, `services/agent_task_consumer.ts`
42
48
  - **Logger lattice**: `logger/Logger.ts`, integration in `index.ts`
49
+ - **Channel message routing**: `router/MessageRouter.ts`, `channels/lark/`
50
+ - **Channel reply mechanism**: `router/MessageRouter.ts#dispatch` (see below)
51
+
52
+ ## CHANNEL MESSAGE REPLY FLOW
53
+
54
+ When an external channel (e.g. Lark) sends a message to a bound agent, the gateway
55
+ routes the message and sends the AI response back to the channel. The flow:
56
+
57
+ ```
58
+ 1. Channel webhook → ChannelAdapter.receive(rawPayload)
59
+ → InboundMessage with replyTarget set
60
+
61
+ 2. MessageRouter.dispatch(inboundMessage)
62
+ → resolve binding → create/reuse thread
63
+ → subscribe to Agent 'reply:ready' event (before addMessage)
64
+ → agent.addMessage({ custom_run_config: { _replyTarget: replyTarget } })
65
+
66
+ 3. Agent queue processes the message
67
+ → agentStreamExecutor() runs the agent
68
+ → emits 'reply:ready' { state, customRunConfig }
69
+
70
+ 4. reply:ready callback fires
71
+ → extracts last AI message from state.values.messages
72
+ → calls ChannelAdapter.sendReply(replyTarget, { text }, installation)
73
+ → reply appears in the external channel
74
+ ```
75
+
76
+ ### Reply deduplication per thread
77
+
78
+ `MessageRouter` uses a counter-based subscription (`_replySubs: Map<string, { count, timer }>`) to handle
79
+ multiple concurrent dispatches on the same thread:
80
+
81
+ - Each dispatch increments the counter, only the first registers the `subscribeOnce` listener.
82
+ - When `reply:ready` fires, the callback decrements the counter. The subscription is removed
83
+ only when the counter reaches 0.
84
+ - A 1-hour timeout cleans up stale subscriptions (e.g. agent aborted, sequence error).
85
+
86
+ ### Reply timing across QueueModes
87
+
88
+ | Mode | `reply:ready` emitted | Reply behavior |
89
+ |------|----------------------|----------------|
90
+ | STEER | After each execution | 1 reply per message |
91
+ | FOLLOWUP | After each execution | 1 reply per message |
92
+ | COLLECT | After the batch (all per-message events first) | 1 reply for the entire batch |
93
+
94
+ ### Key logs for debugging
95
+
96
+ | Event | Meaning |
97
+ |-------|---------|
98
+ | `dispatch:reply:subscribed` | First reply subscription for this thread |
99
+ | `dispatch:reply:incremented` | Additional dispatch joined existing subscription |
100
+ | `dispatch:reply:sending` | AI text extracted, about to call sendReply |
101
+ | `dispatch:reply:sent` | Channel API responded OK |
102
+ | `dispatch:reply:failed` | Channel API returned an error |
103
+ | `dispatch:reply:empty` | Agent produced no output, skipping reply |
104
+ | `dispatch:reply:timeout` | No reply:ready fired within 1h, subscription cleaned up |
43
105
 
44
106
  ## CONVENTIONS
45
107
  - Controllers export named handlers, consume from `services/`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @axiom-lattice/gateway
2
2
 
3
+ ## 2.1.89
4
+
5
+ ### Patch Changes
6
+
7
+ - 176bfe8: custom middleware, enhance messagerouter
8
+ - Updated dependencies [176bfe8]
9
+ - @axiom-lattice/core@2.1.77
10
+ - @axiom-lattice/agent-eval@2.1.71
11
+ - @axiom-lattice/pg-stores@1.0.68
12
+
3
13
  ## 2.1.88
4
14
 
5
15
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -62,19 +62,81 @@ interface MessageContext {
62
62
  }
63
63
  type MessageMiddleware = (ctx: MessageContext, next: () => Promise<void>) => Promise<void>;
64
64
 
65
+ /**
66
+ * Configuration for {@link MessageRouter}.
67
+ *
68
+ * @param middlewares - Ordered middleware chain executed before dispatch
69
+ * @param bindingRegistry - Resolves sender-to-agent bindings
70
+ * @param adapterRegistry - Registry of {@link ChannelAdapter} implementations
71
+ * @param installationStore - Persisted channel installation configs
72
+ */
65
73
  interface MessageRouterConfig {
66
74
  middlewares: MessageMiddleware[];
67
75
  bindingRegistry: BindingRegistry;
68
76
  adapterRegistry: ChannelAdapterRegistry;
69
77
  installationStore: ChannelInstallationStore;
70
78
  }
79
+ /**
80
+ * Core message router for external channel integration.
81
+ *
82
+ * Receives normalized {@link InboundMessage} objects from channel adapters,
83
+ * resolves the target agent via {@link BindingRegistry}, manages thread
84
+ * lifecycle, and sends AI responses back to the channel via
85
+ * `ChannelAdapter.sendReply`.
86
+ *
87
+ * ## Reply flow
88
+ *
89
+ * The router uses a counter-based deduplication scheme on the internal
90
+ * {@link Agent.reply:ready} event:
91
+ *
92
+ * 1. Before `agent.addMessage()`, it subscribes to `reply:ready` on the agent.
93
+ * 2. Multiple concurrent dispatches on the same thread increment a counter;
94
+ * only the first registers the `EventEmitter.once` listener.
95
+ * 3. When `reply:ready` fires, the callback extracts the last AI message from
96
+ * the LangGraph state and sends it via `ChannelAdapter.sendReply`.
97
+ * 4. The counter is decremented; the subscription is only removed when the
98
+ * counter reaches 0.
99
+ * 5. Stale subscriptions are cleaned up after a 1-hour timeout.
100
+ *
101
+ * The `replyTarget` is carried through by injecting `_replyTarget` into
102
+ * `custom_run_config` on {@link Agent.addMessage}, and retrieved from the
103
+ * `reply:ready` event payload.
104
+ *
105
+ * @see {@link ChannelAdapter.sendReply}
106
+ * @see {@link InboundMessage.replyTarget}
107
+ */
71
108
  declare class MessageRouter {
72
109
  private middlewares;
73
110
  private bindingRegistry;
74
111
  private adapterRegistry;
75
112
  private installationStore;
113
+ /**
114
+ * Tracks reply subscriptions per thread+channel to avoid duplicate
115
+ * `subscribeOnce` registrations and ensure proper cleanup.
116
+ *
117
+ * Key format: `{threadId}:{adapterChannel}:reply`
118
+ */
119
+ private _replySubs;
76
120
  constructor(config: MessageRouterConfig);
121
+ /**
122
+ * Register an additional middleware at the end of the chain.
123
+ *
124
+ * @param middleware - A {@link MessageMiddleware} function
125
+ */
77
126
  use(middleware: MessageMiddleware): void;
127
+ /**
128
+ * Dispatch an inbound channel message to the bound agent.
129
+ *
130
+ * Full pipeline: middleware chain → binding resolution → thread lifecycle
131
+ * → agent.addMessage() → (async) reply via {@link ChannelAdapter.sendReply}.
132
+ *
133
+ * If the message has a {@link InboundMessage.replyTarget}, the router subscribes
134
+ * to the agent's `reply:ready` event before enqueuing the message, and sends
135
+ * the AI response back to the channel when it arrives.
136
+ *
137
+ * @param message - Normalized inbound message from a channel adapter
138
+ * @returns {@link DispatchResult} with success status, bindingId, and threadId
139
+ */
78
140
  dispatch(message: InboundMessage): Promise<DispatchResult>;
79
141
  private runMiddlewares;
80
142
  }
package/dist/index.d.ts CHANGED
@@ -62,19 +62,81 @@ interface MessageContext {
62
62
  }
63
63
  type MessageMiddleware = (ctx: MessageContext, next: () => Promise<void>) => Promise<void>;
64
64
 
65
+ /**
66
+ * Configuration for {@link MessageRouter}.
67
+ *
68
+ * @param middlewares - Ordered middleware chain executed before dispatch
69
+ * @param bindingRegistry - Resolves sender-to-agent bindings
70
+ * @param adapterRegistry - Registry of {@link ChannelAdapter} implementations
71
+ * @param installationStore - Persisted channel installation configs
72
+ */
65
73
  interface MessageRouterConfig {
66
74
  middlewares: MessageMiddleware[];
67
75
  bindingRegistry: BindingRegistry;
68
76
  adapterRegistry: ChannelAdapterRegistry;
69
77
  installationStore: ChannelInstallationStore;
70
78
  }
79
+ /**
80
+ * Core message router for external channel integration.
81
+ *
82
+ * Receives normalized {@link InboundMessage} objects from channel adapters,
83
+ * resolves the target agent via {@link BindingRegistry}, manages thread
84
+ * lifecycle, and sends AI responses back to the channel via
85
+ * `ChannelAdapter.sendReply`.
86
+ *
87
+ * ## Reply flow
88
+ *
89
+ * The router uses a counter-based deduplication scheme on the internal
90
+ * {@link Agent.reply:ready} event:
91
+ *
92
+ * 1. Before `agent.addMessage()`, it subscribes to `reply:ready` on the agent.
93
+ * 2. Multiple concurrent dispatches on the same thread increment a counter;
94
+ * only the first registers the `EventEmitter.once` listener.
95
+ * 3. When `reply:ready` fires, the callback extracts the last AI message from
96
+ * the LangGraph state and sends it via `ChannelAdapter.sendReply`.
97
+ * 4. The counter is decremented; the subscription is only removed when the
98
+ * counter reaches 0.
99
+ * 5. Stale subscriptions are cleaned up after a 1-hour timeout.
100
+ *
101
+ * The `replyTarget` is carried through by injecting `_replyTarget` into
102
+ * `custom_run_config` on {@link Agent.addMessage}, and retrieved from the
103
+ * `reply:ready` event payload.
104
+ *
105
+ * @see {@link ChannelAdapter.sendReply}
106
+ * @see {@link InboundMessage.replyTarget}
107
+ */
71
108
  declare class MessageRouter {
72
109
  private middlewares;
73
110
  private bindingRegistry;
74
111
  private adapterRegistry;
75
112
  private installationStore;
113
+ /**
114
+ * Tracks reply subscriptions per thread+channel to avoid duplicate
115
+ * `subscribeOnce` registrations and ensure proper cleanup.
116
+ *
117
+ * Key format: `{threadId}:{adapterChannel}:reply`
118
+ */
119
+ private _replySubs;
76
120
  constructor(config: MessageRouterConfig);
121
+ /**
122
+ * Register an additional middleware at the end of the chain.
123
+ *
124
+ * @param middleware - A {@link MessageMiddleware} function
125
+ */
77
126
  use(middleware: MessageMiddleware): void;
127
+ /**
128
+ * Dispatch an inbound channel message to the bound agent.
129
+ *
130
+ * Full pipeline: middleware chain → binding resolution → thread lifecycle
131
+ * → agent.addMessage() → (async) reply via {@link ChannelAdapter.sendReply}.
132
+ *
133
+ * If the message has a {@link InboundMessage.replyTarget}, the router subscribes
134
+ * to the agent's `reply:ready` event before enqueuing the message, and sends
135
+ * the AI response back to the channel when it arrives.
136
+ *
137
+ * @param message - Normalized inbound message from a channel adapter
138
+ * @returns {@link DispatchResult} with success status, bindingId, and threadId
139
+ */
78
140
  dispatch(message: InboundMessage): Promise<DispatchResult>;
79
141
  private runMiddlewares;
80
142
  }
package/dist/index.js CHANGED
@@ -6689,14 +6689,39 @@ var BindingNotFoundError = class extends Error {
6689
6689
  };
6690
6690
  var MessageRouter = class {
6691
6691
  constructor(config) {
6692
+ /**
6693
+ * Tracks reply subscriptions per thread+channel to avoid duplicate
6694
+ * `subscribeOnce` registrations and ensure proper cleanup.
6695
+ *
6696
+ * Key format: `{threadId}:{adapterChannel}:reply`
6697
+ */
6698
+ this._replySubs = /* @__PURE__ */ new Map();
6692
6699
  this.middlewares = [...config.middlewares];
6693
6700
  this.bindingRegistry = config.bindingRegistry;
6694
6701
  this.adapterRegistry = config.adapterRegistry;
6695
6702
  this.installationStore = config.installationStore;
6696
6703
  }
6704
+ /**
6705
+ * Register an additional middleware at the end of the chain.
6706
+ *
6707
+ * @param middleware - A {@link MessageMiddleware} function
6708
+ */
6697
6709
  use(middleware) {
6698
6710
  this.middlewares.push(middleware);
6699
6711
  }
6712
+ /**
6713
+ * Dispatch an inbound channel message to the bound agent.
6714
+ *
6715
+ * Full pipeline: middleware chain → binding resolution → thread lifecycle
6716
+ * → agent.addMessage() → (async) reply via {@link ChannelAdapter.sendReply}.
6717
+ *
6718
+ * If the message has a {@link InboundMessage.replyTarget}, the router subscribes
6719
+ * to the agent's `reply:ready` event before enqueuing the message, and sends
6720
+ * the AI response back to the channel when it arrives.
6721
+ *
6722
+ * @param message - Normalized inbound message from a channel adapter
6723
+ * @returns {@link DispatchResult} with success status, bindingId, and threadId
6724
+ */
6700
6725
  async dispatch(message) {
6701
6726
  const ctx = {
6702
6727
  inboundMessage: message,
@@ -6843,32 +6868,96 @@ var MessageRouter = class {
6843
6868
  workspace_id: ctx.binding.workspaceId || "",
6844
6869
  project_id: ctx.binding.projectId || ""
6845
6870
  });
6846
- const addResult = await agent.addMessage({
6847
- input: { message: message.content.text },
6848
- custom_run_config: message.content.metadata || {}
6849
- });
6850
- console.log({
6851
- event: "dispatch:complete",
6852
- agentId: ctx.binding.agentId,
6853
- threadId,
6854
- messageId: addResult?.messageId,
6855
- result: JSON.stringify(addResult)
6856
- }, "Agent dispatch complete \u2014 messageId = " + (addResult?.messageId || "N/A"));
6857
6871
  if (message.replyTarget) {
6872
+ const replySubKey = `${threadId}:${message.replyTarget.adapterChannel}:reply`;
6858
6873
  const adapter = this.adapterRegistry.get(message.replyTarget.adapterChannel);
6859
6874
  if (adapter) {
6860
6875
  const installation = await this.installationStore.getInstallationById(
6861
6876
  message.channelInstallationId
6862
6877
  );
6863
6878
  if (installation) {
6864
- await adapter.sendReply(
6865
- message.replyTarget,
6866
- { text: ctx.result || "" },
6867
- installation
6868
- );
6879
+ const existing = this._replySubs.get(replySubKey);
6880
+ if (!existing || existing.count === 0) {
6881
+ const timer = setTimeout(() => {
6882
+ const entry = this._replySubs.get(replySubKey);
6883
+ if (entry) {
6884
+ this._replySubs.delete(replySubKey);
6885
+ console.warn({
6886
+ event: "dispatch:reply:timeout",
6887
+ threadId,
6888
+ channel: message.replyTarget.adapterChannel
6889
+ }, "Reply subscription timed out \u2014 no reply:ready fired within 1h");
6890
+ }
6891
+ }, 36e5);
6892
+ this._replySubs.set(replySubKey, { count: 1, timer });
6893
+ console.log({
6894
+ event: "dispatch:reply:subscribed",
6895
+ threadId,
6896
+ channel: message.replyTarget.adapterChannel,
6897
+ senderId: message.sender.id
6898
+ }, "Subscribed to reply:ready for thread");
6899
+ agent.subscribeOnce("reply:ready", (data) => {
6900
+ const messages = data.state?.values?.messages;
6901
+ const lastAI = messages?.filter((m) => m.type === "ai" || m.getType?.() === "ai").pop();
6902
+ const replyText = lastAI?.content ?? "";
6903
+ const entry = this._replySubs.get(replySubKey);
6904
+ if (entry) {
6905
+ entry.count--;
6906
+ if (entry.count <= 0) {
6907
+ clearTimeout(entry.timer);
6908
+ this._replySubs.delete(replySubKey);
6909
+ }
6910
+ }
6911
+ if (replyText) {
6912
+ console.log({
6913
+ event: "dispatch:reply:sending",
6914
+ threadId,
6915
+ channel: message.replyTarget.adapterChannel,
6916
+ replyLength: replyText.length
6917
+ }, "Sending channel reply");
6918
+ adapter.sendReply(message.replyTarget, { text: replyText }, installation).then(() => {
6919
+ console.log({
6920
+ event: "dispatch:reply:sent",
6921
+ threadId,
6922
+ channel: message.replyTarget.adapterChannel
6923
+ }, "Channel reply sent successfully");
6924
+ }).catch((err) => console.error({
6925
+ event: "dispatch:reply:failed",
6926
+ threadId,
6927
+ channel: message.replyTarget.adapterChannel,
6928
+ error: err instanceof Error ? err.message : String(err)
6929
+ }, "Failed to send channel reply"));
6930
+ } else {
6931
+ console.warn({
6932
+ event: "dispatch:reply:empty",
6933
+ threadId,
6934
+ channel: message.replyTarget.adapterChannel
6935
+ }, "Agent produced no text output \u2014 skipping reply");
6936
+ }
6937
+ });
6938
+ } else {
6939
+ existing.count++;
6940
+ console.log({
6941
+ event: "dispatch:reply:incremented",
6942
+ threadId,
6943
+ channel: message.replyTarget.adapterChannel,
6944
+ count: existing.count
6945
+ }, "Incremented reply counter for thread (already subscribed)");
6946
+ }
6869
6947
  }
6870
6948
  }
6871
6949
  }
6950
+ const addResult = await agent.addMessage({
6951
+ input: { message: message.content.text },
6952
+ custom_run_config: message.replyTarget ? { ...message.content.metadata || {}, _replyTarget: message.replyTarget } : message.content.metadata || {}
6953
+ });
6954
+ console.log({
6955
+ event: "dispatch:complete",
6956
+ agentId: ctx.binding.agentId,
6957
+ threadId,
6958
+ messageId: addResult?.messageId,
6959
+ result: JSON.stringify(addResult)
6960
+ }, "Agent dispatch complete \u2014 messageId = " + (addResult?.messageId || "N/A"));
6872
6961
  });
6873
6962
  return {
6874
6963
  success: true,