@axiom-lattice/core
v2.1.79
Published
Core library for agent-based applications
Readme
@axiom-lattice/core
Core agent framework providing lattice managers for models, tools, agents, memory, stores, and more.
Store Configuration
configureStores
Unified store registration that replaces the manual new → initialize → remove → register boilerplate.
Handles StoreLatticeManager, ScheduleLatticeManager, and MemoryLatticeManager through a single call.
import { configureStores } from "@axiom-lattice/core";
import { createPgStoreConfig } from "@axiom-lattice/pg-stores";
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
// One connection string creates all PG store instances
const stores = createPgStoreConfig(process.env.DATABASE_URL);
// One call registers all stores across three managers
await configureStores({
...stores,
checkpoint: PostgresSaver.fromConnString(process.env.DATABASE_URL),
}, { autoDisposeStores: true });Supported store keys
| Key | Registered via |
|-----|---------------|
| thread, assistant, workspace, project, database, metrics, mcp, user, tenant, userTenantLink, threadMessageQueue, workflowTracking, eval, channelInstallation, channelBinding, skill | StoreLatticeManager |
| schedule | ScheduleLatticeManager (auto-configured as POSTGRES) |
| checkpoint | MemoryLatticeManager (accepts any CheckpointSaver) |
Behavior
For each store entry:
- Calls
store.initialize()if the method exists (skipped if it requires arguments) - Removes any existing
"default"registration - Registers the new store under
"default" - Tracks
store.disposefor cleanup
Options
interface ConfigureStoresOptions {
autoDisposeStores?: boolean; // Register SIGINT/SIGTERM handlers for cleanup
customStores?: Record<string, object>; // Register custom store types
}Returns a dispose function for manual cleanup regardless of autoDisposeStores.
Custom stores
Register custom store implementations that follow the same initialize() / dispose() pattern:
interface MyCustomStore {
getData(): Promise<string>;
}
class PostgreSQLMyCustomStore implements MyCustomStore {
async initialize() { /* run migrations */ }
async dispose() { /* close pool */ }
async getData() { return "from pg"; }
}await configureStores({
thread: new PostgreSQLThreadStore(opts),
}, {
customStores: {
myCustom: new PostgreSQLMyCustomStore({ poolConfig }),
},
});
// Consume via StoreLatticeManager
const { store } = getStoreLattice("default", "myCustom");
const customStore = store as MyCustomStore;Consumer pattern
Components that need stores read them directly from StoreLatticeManager — no setConfigStore required:
// SqlDatabaseManager reads database configs lazily from the store lattice
const db = await sqlDatabaseManager.getDatabase(tenantId, "main-db");
// MetricsServerManager loads configs on first access
const client = await metricsServerManager.getClient(tenantId, "prometheus");
const servers = await metricsServerManager.getServerKeys(tenantId);Other Lattice Managers
Model Lattice
import { registerModelLattice } from "@axiom-lattice/core";
registerModelLattice("default", {
model: "gpt-4o",
provider: "openai",
streaming: true,
apiKeyEnvName: "OPENAI_API_KEY",
});ChunkBuffer
import { InMemoryChunkBuffer, registerChunkBuffer } from "@axiom-lattice/core";
const buffer = new InMemoryChunkBuffer({ ttl: 30 * 60 * 1000 });
registerChunkBuffer("default", buffer);Directory Structure
src/store_lattice/— Store lattice manager + configureStoressrc/model_lattice/— LLM provider abstractionssrc/tool_lattice/— Tool implementations (SQL, metrics, etc.)src/agent_lattice/— Agent definitions and builderssrc/memory_lattice/— Context/memory managementsrc/schedule_lattice/— Scheduled task managementsrc/sandbox_lattice/— Code execution sandbox providers
Custom Middleware Registry
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.
Quick start
import { createMiddleware, CustomMiddlewareRegistry } from "@axiom-lattice/core";
CustomMiddlewareRegistry.register("my-logger", (config) =>
createMiddleware({
name: "MyLogger",
wrapModelCall: async (request, handler) => {
console.log(`[${config.logLevel}] Model call`);
return handler(request);
},
})
);Database config format
In your agent's graphDefinition.middleware array, add a "custom" type entry:
{
"id": "mw-001",
"type": "custom",
"name": "Audit Logger",
"description": "Logs model calls for audit",
"enabled": true,
"config": {
"key": "my-logger",
"logLevel": "debug"
}
}The config.key must match the key passed to register(). All other config fields are forwarded to your factory function.
Full example — permission-check middleware
import { createMiddleware, CustomMiddlewareRegistry } from "@axiom-lattice/core";
CustomMiddlewareRegistry.register("permission-check", (config) =>
createMiddleware({
name: "PermissionCheck",
wrapToolCall: async (request, handler) => {
const toolName = request.toolCall?.name;
if (toolName && !config.allowedTools.includes(toolName)) {
return { content: `Tool "${toolName}" is not permitted.` };
}
return handler(request);
},
})
);Database config:
{
"id": "perm-001",
"type": "custom",
"name": "Tool Permissions",
"description": "Restrict tool access",
"enabled": true,
"config": {
"key": "permission-check",
"allowedTools": ["read_file", "write_file"]
}
}API reference
| Method | Signature | Description |
|--------|-----------|-------------|
| register | (key: string, factory: (config: Record<string, any>) => AgentMiddleware \| Promise<AgentMiddleware>) => void | Register a factory by key |
| unregister | (key: string) => boolean | Remove a registration |
| get | (key: string) => AgentMiddlewareFactory \| undefined | Look up a factory |
| has | (key: string) => boolean | Check if key is registered |
| list | () => string[] | Get all registered keys |
Important
- Register factories before building agents (typically at app startup)
- Duplicate keys overwrite previous registrations
- Unregistered keys are skipped with a console warning at build time
- Factory functions receive the raw
configobject (minuskey) — validate it yourself
External Channel Integration
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.
What core provides
| Component | Location | Purpose |
|-----------|----------|---------|
| InMemoryChannelInstallationStore | store_lattice/ | In-memory ChannelInstallationStore implementation |
| InMemoryBindingStore | store_lattice/ | In-memory BindingRegistry implementation |
| BindingRegistryHolder | bindings_lattice/ | Global accessor for the active BindingRegistry |
| manage_binding tool | tool_lattice/manage_binding/ | Agent tool for CRUD on sender-to-agent bindings |
Architecture overview
External Platform (Lark, Slack, Email)
→ Gateway HTTP route (packages/gateway)
→ ChannelAdapter.receive(rawPayload) → InboundMessage
→ MessageRouter.dispatch() (packages/gateway)
→ BindingRegistry.resolve() ←── core
→ Thread creation / reuse ←── core
→ Agent.addMessage() ←── core
→ ChannelAdapter.sendReply() (packages/gateway)Agents remain channel-agnostic — they only see standard thread/message execution.
Setup for custom channel development
1. Register channel stores
import { configureStores } from "@axiom-lattice/core";
import { PostgreSQLChannelInstallationStore, PostgreSQLBindingStore } from "@axiom-lattice/pg-stores";
await configureStores({
channelInstallation: new PostgreSQLChannelInstallationStore({ pool }),
channelBinding: new PostgreSQLBindingStore({ pool }),
// ... other stores
});2. Set the global BindingRegistry
import { setBindingRegistry } from "@axiom-lattice/core";
const bindingStore = getStoreLattice("default", "channelBinding").store;
setBindingRegistry(bindingStore);3. Implement ChannelAdapter (in gateway)
import type { ChannelAdapter, InboundMessage, OutboundMessage, ReplyTarget } from "@axiom-lattice/protocols";
import { z } from "zod";
const slackConfigSchema = z.object({
botToken: z.string(),
signingSecret: z.string(),
});
export const slackAdapter: ChannelAdapter = {
channel: "slack",
configSchema: slackConfigSchema,
async receive(rawPayload, installation): Promise<InboundMessage | null> {
// 1. Parse Slack-specific event
// 2. Return normalized InboundMessage or null to ignore
return {
channel: "slack",
channelInstallationId: installation.id,
tenantId: installation.tenantId,
sender: { id: userId, displayName: userName },
content: { text: messageText },
replyTarget: {
adapterChannel: "slack",
channelInstallationId: installation.id,
rawTarget: { channelId, threadTs },
},
};
},
async sendReply(replyTarget, message, installation): Promise<void> {
// Use Slack API to send reply
// replyTarget.rawTarget contains channel-specific context
},
};4. Register routes and adapter (in gateway)
// packages/gateway/src/channels/registry.ts
adapterRegistry.register(slackAdapter);
// packages/gateway/src/channels/routes.ts
app.post("/api/channels/slack/installations/:id/events", async (req, reply) => {
const installation = await installationStore.getInstallationById(req.params.id);
const message = await slackAdapter.receive(req.body, installation);
if (message) await messageRouter.dispatch(message);
});Binding resolution flow
When MessageRouter.dispatch() receives an InboundMessage:
- Calls
BindingRegistry.resolve({ channel, senderId, channelInstallationId, tenantId }) - If no binding found:
- If
installation.rejectWhenNoBinding→ throwBindingNotFoundError - If
installation.fallbackAgentId→ create temporary fallback binding
- If
- If binding disabled → reject
- Thread resolution (based on
binding.threadMode):"fixed"→ reusebinding.threadId"per_conversation"→ always create new thread
- Execute agent via
agent.addMessage()
Agent-side binding management
Agents can manage bindings dynamically via the manage_binding tool:
// Agent can call this tool to:
// - list_installations: List available channel installations
// - create: Bind a sender to an agent (channel, senderId, agentId)
// - update: Change agent or threadMode for a binding
// - delete: Remove a binding
// - list: List all bindings with optional filtersKey types (from @axiom-lattice/protocols)
interface ChannelAdapter<TConfig = unknown> {
readonly channel: string;
readonly configSchema: z.ZodSchema<TConfig>;
receive(rawPayload: unknown, installation: ChannelInstallation): Promise<InboundMessage | null>;
sendReply(replyTarget: ReplyTarget, message: OutboundMessage, installation: ChannelInstallation): Promise<void>;
}
interface Binding {
id: string;
channel: string;
channelInstallationId: string;
tenantId: string;
senderId: string;
agentId: string;
threadMode: "fixed" | "per_conversation";
enabled: boolean;
}Reference implementation
See packages/gateway/src/channels/lark/ for a complete production channel adapter including:
- Webhook verification and decryption
- Event parsing and normalization
- Thread mapping (
user/group/hybridmodes) - Reply delivery via official SDK
