@axiom-lattice/core 2.1.76 → 2.1.78
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 +258 -0
- package/dist/index.d.mts +539 -30
- package/dist/index.d.ts +539 -30
- package/dist/index.js +678 -51
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +658 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -130,3 +130,261 @@ registerChunkBuffer("default", buffer);
|
|
|
130
130
|
- `src/memory_lattice/` — Context/memory management
|
|
131
131
|
- `src/schedule_lattice/` — Scheduled task management
|
|
132
132
|
- `src/sandbox_lattice/` — Code execution sandbox providers
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Custom Middleware Registry
|
|
137
|
+
|
|
138
|
+
Applications can write their own middleware and register it at runtime without modifying core source code. The registered middleware can then be enabled via agent config stored in the database.
|
|
139
|
+
|
|
140
|
+
### Quick start
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { createMiddleware, CustomMiddlewareRegistry } from "@axiom-lattice/core";
|
|
144
|
+
|
|
145
|
+
CustomMiddlewareRegistry.register("my-logger", (config) =>
|
|
146
|
+
createMiddleware({
|
|
147
|
+
name: "MyLogger",
|
|
148
|
+
wrapModelCall: async (request, handler) => {
|
|
149
|
+
console.log(`[${config.logLevel}] Model call`);
|
|
150
|
+
return handler(request);
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Database config format
|
|
157
|
+
|
|
158
|
+
In your agent's `graphDefinition.middleware` array, add a `"custom"` type entry:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"id": "mw-001",
|
|
163
|
+
"type": "custom",
|
|
164
|
+
"name": "Audit Logger",
|
|
165
|
+
"description": "Logs model calls for audit",
|
|
166
|
+
"enabled": true,
|
|
167
|
+
"config": {
|
|
168
|
+
"key": "my-logger",
|
|
169
|
+
"logLevel": "debug"
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `config.key` must match the key passed to `register()`. All other config fields are forwarded to your factory function.
|
|
175
|
+
|
|
176
|
+
### Full example — permission-check middleware
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { createMiddleware, CustomMiddlewareRegistry } from "@axiom-lattice/core";
|
|
180
|
+
|
|
181
|
+
CustomMiddlewareRegistry.register("permission-check", (config) =>
|
|
182
|
+
createMiddleware({
|
|
183
|
+
name: "PermissionCheck",
|
|
184
|
+
wrapToolCall: async (request, handler) => {
|
|
185
|
+
const toolName = request.toolCall?.name;
|
|
186
|
+
if (toolName && !config.allowedTools.includes(toolName)) {
|
|
187
|
+
return { content: `Tool "${toolName}" is not permitted.` };
|
|
188
|
+
}
|
|
189
|
+
return handler(request);
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Database config:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"id": "perm-001",
|
|
200
|
+
"type": "custom",
|
|
201
|
+
"name": "Tool Permissions",
|
|
202
|
+
"description": "Restrict tool access",
|
|
203
|
+
"enabled": true,
|
|
204
|
+
"config": {
|
|
205
|
+
"key": "permission-check",
|
|
206
|
+
"allowedTools": ["read_file", "write_file"]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### API reference
|
|
212
|
+
|
|
213
|
+
| Method | Signature | Description |
|
|
214
|
+
|--------|-----------|-------------|
|
|
215
|
+
| `register` | `(key: string, factory: (config: Record<string, any>) => AgentMiddleware \| Promise<AgentMiddleware>) => void` | Register a factory by key |
|
|
216
|
+
| `unregister` | `(key: string) => boolean` | Remove a registration |
|
|
217
|
+
| `get` | `(key: string) => AgentMiddlewareFactory \| undefined` | Look up a factory |
|
|
218
|
+
| `has` | `(key: string) => boolean` | Check if key is registered |
|
|
219
|
+
| `list` | `() => string[]` | Get all registered keys |
|
|
220
|
+
|
|
221
|
+
### Important
|
|
222
|
+
|
|
223
|
+
- Register factories **before** building agents (typically at app startup)
|
|
224
|
+
- Duplicate keys overwrite previous registrations
|
|
225
|
+
- Unregistered keys are skipped with a console warning at build time
|
|
226
|
+
- Factory functions receive the raw `config` object (minus `key`) — validate it yourself
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## External Channel Integration
|
|
231
|
+
|
|
232
|
+
core provides the storage infrastructure and binding system that external channel adapters (Lark, Slack, Email, etc.) depend on. The actual HTTP routes and message dispatch live in `packages/gateway`, but all persistence and sender resolution are handled here.
|
|
233
|
+
|
|
234
|
+
### What core provides
|
|
235
|
+
|
|
236
|
+
| Component | Location | Purpose |
|
|
237
|
+
|-----------|----------|---------|
|
|
238
|
+
| `InMemoryChannelInstallationStore` | `store_lattice/` | In-memory `ChannelInstallationStore` implementation |
|
|
239
|
+
| `InMemoryBindingStore` | `store_lattice/` | In-memory `BindingRegistry` implementation |
|
|
240
|
+
| `BindingRegistryHolder` | `bindings_lattice/` | Global accessor for the active `BindingRegistry` |
|
|
241
|
+
| `manage_binding` tool | `tool_lattice/manage_binding/` | Agent tool for CRUD on sender-to-agent bindings |
|
|
242
|
+
|
|
243
|
+
### Architecture overview
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
External Platform (Lark, Slack, Email)
|
|
247
|
+
→ Gateway HTTP route (packages/gateway)
|
|
248
|
+
→ ChannelAdapter.receive(rawPayload) → InboundMessage
|
|
249
|
+
→ MessageRouter.dispatch() (packages/gateway)
|
|
250
|
+
→ BindingRegistry.resolve() ←── core
|
|
251
|
+
→ Thread creation / reuse ←── core
|
|
252
|
+
→ Agent.addMessage() ←── core
|
|
253
|
+
→ ChannelAdapter.sendReply() (packages/gateway)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Agents remain **channel-agnostic** — they only see standard thread/message execution.
|
|
257
|
+
|
|
258
|
+
### Setup for custom channel development
|
|
259
|
+
|
|
260
|
+
#### 1. Register channel stores
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { configureStores } from "@axiom-lattice/core";
|
|
264
|
+
import { PostgreSQLChannelInstallationStore, PostgreSQLBindingStore } from "@axiom-lattice/pg-stores";
|
|
265
|
+
|
|
266
|
+
await configureStores({
|
|
267
|
+
channelInstallation: new PostgreSQLChannelInstallationStore({ pool }),
|
|
268
|
+
channelBinding: new PostgreSQLBindingStore({ pool }),
|
|
269
|
+
// ... other stores
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### 2. Set the global BindingRegistry
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { setBindingRegistry } from "@axiom-lattice/core";
|
|
277
|
+
|
|
278
|
+
const bindingStore = getStoreLattice("default", "channelBinding").store;
|
|
279
|
+
setBindingRegistry(bindingStore);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
#### 3. Implement ChannelAdapter (in gateway)
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import type { ChannelAdapter, InboundMessage, OutboundMessage, ReplyTarget } from "@axiom-lattice/protocols";
|
|
286
|
+
import { z } from "zod";
|
|
287
|
+
|
|
288
|
+
const slackConfigSchema = z.object({
|
|
289
|
+
botToken: z.string(),
|
|
290
|
+
signingSecret: z.string(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
export const slackAdapter: ChannelAdapter = {
|
|
294
|
+
channel: "slack",
|
|
295
|
+
configSchema: slackConfigSchema,
|
|
296
|
+
|
|
297
|
+
async receive(rawPayload, installation): Promise<InboundMessage | null> {
|
|
298
|
+
// 1. Parse Slack-specific event
|
|
299
|
+
// 2. Return normalized InboundMessage or null to ignore
|
|
300
|
+
return {
|
|
301
|
+
channel: "slack",
|
|
302
|
+
channelInstallationId: installation.id,
|
|
303
|
+
tenantId: installation.tenantId,
|
|
304
|
+
sender: { id: userId, displayName: userName },
|
|
305
|
+
content: { text: messageText },
|
|
306
|
+
replyTarget: {
|
|
307
|
+
adapterChannel: "slack",
|
|
308
|
+
channelInstallationId: installation.id,
|
|
309
|
+
rawTarget: { channelId, threadTs },
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async sendReply(replyTarget, message, installation): Promise<void> {
|
|
315
|
+
// Use Slack API to send reply
|
|
316
|
+
// replyTarget.rawTarget contains channel-specific context
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### 4. Register routes and adapter (in gateway)
|
|
322
|
+
|
|
323
|
+
```typescript
|
|
324
|
+
// packages/gateway/src/channels/registry.ts
|
|
325
|
+
adapterRegistry.register(slackAdapter);
|
|
326
|
+
|
|
327
|
+
// packages/gateway/src/channels/routes.ts
|
|
328
|
+
app.post("/api/channels/slack/installations/:id/events", async (req, reply) => {
|
|
329
|
+
const installation = await installationStore.getInstallationById(req.params.id);
|
|
330
|
+
const message = await slackAdapter.receive(req.body, installation);
|
|
331
|
+
if (message) await messageRouter.dispatch(message);
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Binding resolution flow
|
|
336
|
+
|
|
337
|
+
When `MessageRouter.dispatch()` receives an `InboundMessage`:
|
|
338
|
+
|
|
339
|
+
1. Calls `BindingRegistry.resolve({ channel, senderId, channelInstallationId, tenantId })`
|
|
340
|
+
2. If **no binding found**:
|
|
341
|
+
- If `installation.rejectWhenNoBinding` → throw `BindingNotFoundError`
|
|
342
|
+
- If `installation.fallbackAgentId` → create temporary fallback binding
|
|
343
|
+
3. If **binding disabled** → reject
|
|
344
|
+
4. **Thread resolution** (based on `binding.threadMode`):
|
|
345
|
+
- `"fixed"` → reuse `binding.threadId`
|
|
346
|
+
- `"per_conversation"` → always create new thread
|
|
347
|
+
5. Execute agent via `agent.addMessage()`
|
|
348
|
+
|
|
349
|
+
### Agent-side binding management
|
|
350
|
+
|
|
351
|
+
Agents can manage bindings dynamically via the `manage_binding` tool:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Agent can call this tool to:
|
|
355
|
+
// - list_installations: List available channel installations
|
|
356
|
+
// - create: Bind a sender to an agent (channel, senderId, agentId)
|
|
357
|
+
// - update: Change agent or threadMode for a binding
|
|
358
|
+
// - delete: Remove a binding
|
|
359
|
+
// - list: List all bindings with optional filters
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Key types (from @axiom-lattice/protocols)
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
interface ChannelAdapter<TConfig = unknown> {
|
|
366
|
+
readonly channel: string;
|
|
367
|
+
readonly configSchema: z.ZodSchema<TConfig>;
|
|
368
|
+
receive(rawPayload: unknown, installation: ChannelInstallation): Promise<InboundMessage | null>;
|
|
369
|
+
sendReply(replyTarget: ReplyTarget, message: OutboundMessage, installation: ChannelInstallation): Promise<void>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
interface Binding {
|
|
373
|
+
id: string;
|
|
374
|
+
channel: string;
|
|
375
|
+
channelInstallationId: string;
|
|
376
|
+
tenantId: string;
|
|
377
|
+
senderId: string;
|
|
378
|
+
agentId: string;
|
|
379
|
+
threadMode: "fixed" | "per_conversation";
|
|
380
|
+
enabled: boolean;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Reference implementation
|
|
385
|
+
|
|
386
|
+
See `packages/gateway/src/channels/lark/` for a complete production channel adapter including:
|
|
387
|
+
- Webhook verification and decryption
|
|
388
|
+
- Event parsing and normalization
|
|
389
|
+
- Thread mapping (`user` / `group` / `hybrid` modes)
|
|
390
|
+
- Reply delivery via official SDK
|