zentis
v1.1.24
Published
A high-level agentic framework for Model Context Protocol (MCP) with memory and LLM integration.
Maintainers
Readme
Zentis
A high-level agentic framework for Model Context Protocol (MCP) with built-in memory management and LLM integration.
Features
- 🚀 MCP Client: Connect to multiple MCP servers in parallel (SSE or HTTP).
- 🧠 Smart Memory: Persistent history with Session ID support and automatic technical message skipping.
- 🗄️ Storage Adapters: Support for SQLite and PostgreSQL (with SSL) with composite keys.
- 🤖 Agentic Reasoning: Multi-turn tool calling with Regex-based Smart Routing or LLM-based Planning.
- 🛠️ Universal Compatibility: Built-in normalization for Gemini, Groq, and OpenRouter.
- 🛡️ Recursion Guard: Prevents infinite tool-calling loops by tracking and blocking redundant calls with identical arguments.
- ⚡ Batch Tool Execution: Execute multiple tools in parallel within a single reasoning turn for high-performance workflows.
- �📋 Planning Phase: Optional dedicated LLM turn to pre-select tool sequences, improving accuracy for complex tasks.
- 🎨 Smarter UI Rendering: Register custom UI components (Table, Charts, etc.) that automatically resolve large datasets from hidden data references.
Installation
npm install zentisNote: If using SQLite or Postgres, install the respective drivers:
npm install better-sqlite3 # for SQLite
npm install pg # for PostgreSQLUI Component Integration (React)
Zentis allows you to register UI components that the LLM can trigger. These components automatically handle large datasets by swapping background "Data References" for actual props before the response is returned.
1. Register Components (Node.js/Agent side)
Zentis uses a central ZentisUI registry to define which components the LLM is allowed to use. By default, Table, Chart, and Graph are pre-registered.
import { ZentisAgent, ZentisUI } from 'zentis';
const ui = new ZentisUI();
// Register a custom component
ui.register({
name: 'WeatherCard',
description: 'Display current weather and forecast.',
props: {
location: { type: 'string', description: 'City name', required: true },
temperature: { type: 'number', description: 'Current temp in Celsius' },
isRainy: { type: 'boolean', description: 'Whether it is raining' },
forecast: {
type: 'data_reference',
description: 'Reference to the forecast data list from a tool'
},
filters: {
type: 'object',
description: 'Dynamic filters to apply to the data'
}
}
});
const agent = new ZentisAgent({ ui, ... });Property Types
string,number,boolean,array,object: Standard JSON types.data_reference: Crucial for performance. This tells the LLM to provide a Result ID (e.g.,res_1_get_weather) instead of the raw data. Zentis will automatically swap this ID for the actual data before returning the response to your frontend.
2. Render in React
import React from 'react';
import { AgentResponse } from 'zentis';
// Example UI Component Map
const ComponentMap = {
Table: ({ title, data, filters }: any) => {
// Apply dynamic filters generated by the LLM
const filteredData = React.useMemo(() => {
if (!filters) return data;
return data.filter((row: any) =>
Object.entries(filters).every(([key, val]) => String(row[key]) === String(val))
);
}, [data, filters]);
return (
<div>
<h3>{title}</h3>
<table className="min-w-full border">
<thead>
<tr className="bg-gray-100">
{Object.keys(filteredData[0] || {}).map(k => <th key={k} className="border p-2">{k}</th>)}
</tr>
</thead>
<tbody>
{filteredData.map((row: any, i: number) => (
<tr key={i}>
{Object.values(row).map((v: any, j) => <td key={j} className="border p-2">{String(v)}</td>)}
</tr>
))}
</tbody>
</table>
</div>
);
},
Chart: (props: any) => <MyChartLibrary {...props} />
};
export const ZentisChat = () => {
const [response, setResponse] = React.useState<AgentResponse | null>(null);
const handleQuery = async (text: string) => {
// res.components[0].props.data is already populated with the full dataset
const res = await agent.query(text);
setResponse(res);
};
return (
<div className="p-4">
{/* 1. Render the Conversational Text */}
<p className="mb-4">{response?.text}</p>
{/* 2. Render Auto-populated UI Components */}
{response?.components.map((comp, i) => {
const Renderer = ComponentMap[comp.name];
return Renderer ? <Renderer key={i} {...comp.props} /> : null;
})}
</div>
);
};Quick Start
import { ZentisAgent, ZentisMcpClient } from 'zentis';
// 1. Initialize Agent with LLM, Storage, and MCP configs
const agent = new ZentisAgent({
llm: {
apiKey: process.env.ZEN_API_KEY || process.env.GEMINI_API_KEY,
baseURL: process.env.ZEN_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta/openai/',
model: process.env.ZEN_MODEL || 'gemini-3-flash-preview'
},
storage: {
type: 'sqlite',
connectionString: process.env.DB_CONNECTION_URL || 'zentis.db',
userId: 'user_123',
sessionId: 'session_abc' // Optional: Scope history to a specific session
},
mcp: {
name: 'primary',
url: 'http://localhost:8001/sse'
},
tool_router: true, // Enable internal tool routing to avoid prompt bloat
maxHistoryMessages: 10 // Set a custom sliding window for history
});
// 2. Query the agent with real-time lifecycle hooks
// ALWAYS call waitReady() before querying to ensure all MCP servers are connected
await agent.waitReady();
const response = await agent.query("Check the status of the front yard camera", {
onStep: (step) => {
console.log(`[${step.type}] ${step.message}`);
if (step.data) console.log('Data:', step.data);
},
onAction: (action) => {
console.log(`Executing ${action.tool} on ${action.server}`);
},
extraArgs: {
API_auth: 'your-frontend-token-here'
}
});
// Optional: Override model for a specific query
const flashResponse = await agent.query("Quick summary", { model: "llama-3.1-8b-instant" });Core Concepts
1. Multi-Server Connectivity
Zentis uses a singleton ZentisMcpClient to manage connections. You can connect to multiple servers in parallel, each with its own transport and authentication settings.
const client = ZentisMcpClient.getInstance();
await client.connectMany([
{
name: 'analytics',
url: 'https://api.analytics.com/mcp',
options: {
transportType: 'sse',
headers: { 'Authorization': process.env.MCP_TOKEN }
}
},
{
name: 'local-tools',
url: 'http://localhost:8080/mcp',
options: { transportType: 'http' }
}
]);
// Tools from ALL servers are automatically available to the agent
const tools = await agent.listAvailableTools();2. Autonomous Reasoning
Zentis implements a multi-turn reasoning loop with optional perception and planning layers. You can control how the agent perceives and plans its actions:
const agent = new ZentisAgent({
// ...
tool_router: true, // Perception: keyword-based tool filtering (saves tokens)
planner: true // Planning: dedicated LLM turn to pre-select tools
});- Smart Tool Routing: If
tool_routeris enabled, Zentis uses regex heuristics to inject only the most relevant tools into the prompt, preventing "tool noise" and saving context window. - Batch & Recursive Execution: Zentis supports executing multiple tools in a single turn. If an LLM emits multiple
[CALL:...]tags, Zentis runs them all and returns the results as separate entries, enabling complex tasks like "Turn off all 5 smart lights" to happen in one step. - Experimental Planner: When
planneris true, Zentis runs a hidden initial LLM turn to identify the exact sequence of tools needed. This significantly improves accuracy for complex, multi-step goals. - Observability: Use the
onStepcallback for real-time lifecycle tracking.
Recursion Guard: To ensure stability, Zentis tracks every tool call made in a reasoning loop. If the agent attempts to call the exact same tool with the exact same arguments twice, Zentis will block the call and notify the agent to prevent an infinite loop.
Tip: Zentis automatically sanitizes tool call IDs and patches schemas to ensure compatibility with strict providers like Gemini/Google.
Tip: Use the
onActionoption to listen for tool executions in real-time.
3. Smart Memory & Custom Personas (Zero-Identity)
Zentis avoids "library bloat" by not injecting any hardcoded identity, instructions, or UI help. This Zero-Identity Architecture gives you full control. Use the "notes stack" to define your agent's persona and rules.
// Define Identity
await agent.note("You are a helpful Security Assistant named Sentinel.");
await agent.note("You have access to the Sherlock MCP server.");
// Define Operational Rules
await agent.note("If multiple cameras are found, always list their status in a [UI:Table].");
await agent.note("ALWAYS respond in a professional tone.");
// The agent's core 'system prompt' is built from these notes.
const currentInstructions = agent.getNotes();Multi-Turn Planning
You can use notes to guide the planning phase as well:
await agent.note("Before using any tools, explain your plan to the user briefly.");4. Extra Arguments (Sensitive Tokens)
Zentis allows you to pass custom arguments (like auth tokens or session IDs) directly to your tools without exposing them to the LLM. These are passed via the extraArgs option in the query method.
const response = await agent.query("Get my cameras", {
extraArgs: {
API_auth: "eyJhbGciOi...", // Hidden from LLM
nodeId: 101
}
});How it works:
- Schema Scrubbing: Zentis removes
API_authandnodeIdfrom the tool definition before sending it to the LLM. The LLM doesn't even know these parameters exist. - Automatic Injection: When the LLM calls the tool, Zentis automatically injects your
extraArgsinto the arguments before execution. - Security: Prevents the LLM from hallucinating or attempting to manipulate sensitive session/auth parameters.
- Strict Overriding:
extraArgsalways take precedence. - Flexibility: You can name the keys anything and set their types to any valid JSON value.
Memory & Storage Backends
Zentis supports multiple storage adapters. Configure them via the storage option in the ZentisAgent constructor. All backends use a composite key (userId + sessionId) to ensure strict session isolation across parallel conversations.
| Type | Environment | Isolation | Key Features |
| :--- | :--- | :--- | :--- |
| sqlite | Node.js | Composite PK | Fast, file-based persistence |
| postgres | Node.js | Composite PK | Scalable, SSL-ready storage |
Examples
SQLite (Node.js)
const agent = new ZentisAgent({
storage: {
type: 'sqlite',
connectionString: './data.db', // default: 'zentis.db'
userId: 'user_789'
}
});PostgreSQL (Node.js)
// Option 1: Connection String
const agent = new ZentisAgent({
storage: {
type: 'postgres',
connectionString: 'postgresql://user:pass@localhost:5432/db',
ssl: { rejectUnauthorized: false },
userId: 'user_999'
}
});
// Option 2: Existing PG Pool Instance
import { Pool } from 'pg';
const myPool = new Pool({ ... });
const agent = new ZentisAgent({
storage: {
type: 'postgres',
pool: myPool, // Pass your own pre-configured pool
userId: 'user_999'
}
});UI Components (Browser Integration)
Agents can "trigger" UI components in the frontend. Zentis automatically parses these into a structured components array.
Syntax
The agent should respond with: [UI:ComponentName]{"props": "here"}[/UI].
Usage
const response = await agent.query("Show me the map of London");
// response.text -> "Here is the map you requested:"
// response.components -> [{ name: "Map", props: { lat: 51.5, lng: -0.12 } }]Frontend Integration (React Example)
You can easily map Zentis components to your own UI library:
const ChatMessage = ({ response }) => {
return (
<div>
<p>{response.text}</p>
{response.components.map((comp, i) => {
switch(comp.name) {
case 'Map': return <MyMap key={i} {...comp.props} />;
case 'VideoPlayer': return <MyVideo key={i} {...comp.props} />;
case 'Table': return <MyTable key={i} {...comp.props} />;
default: return null;
}
})}
</div>
);
};UI Actions (Web API)
Zentis allows the agent to interact with the frontend by triggering specific actions like highlighting, clicking, or focusing elements.
Syntax
[ACTION:Type]{"targetId": "element-id", "metadata": {}}[/ACTION]
Example
const response = await agent.query("Highlight the submit button");
// response.actions -> [{ type: "highlight", targetId: "submit-btn" }]Supported Actions
- highlight: Focus attention on a specific element.
- click: Trigger a programmatic click.
- focus: Set focus to an input field.
- scroll: Scroll an element into view.
- custom: Pass arbitrary events to your frontend.
Token & Tool Optimization
Zentis is built for production efficiency.
1. Smart History Optimization
To keep storage clean and token usage low, Zentis only saves final responses and user queries to permanent storage. Intermediate tool calls and raw results are kept in memory only for the duration of the current reasoning loop.
Supported Default Components
- DetectionGallery:
{ "title": string, "data": any[] } - VideoPlayer:
{ "url": string, "title": string, "className": string } - Map:
{ "lat": number, "lng": number, "zoom": number, "className": string } - Chart:
{ "type": "bar" | "line", "data": any[], "className": string } - Table:
{ "headers": string[], "rows": any[][], "title": string, "className": string }
License
ISC
