Agent Features
Agent Features
Instructions (System Prompt)
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
instructions: `You are a Python programming expert.
Rules:
- Always provide working code examples
- Use type hints
- Include docstrings
- Follow PEP 8 style guide`,
});
const response = await agent.run('How do I read a CSV file?');
// Agent will provide Python code with all the rules applied
Instruction Templates
Agent instructions support {{COMMAND}} and {{COMMAND:arg}} template placeholders that resolve automatically. Templates are processed in two phases:
- Static — resolved once when the agent is created (e.g.,
AGENT_ID,MODEL) - Dynamic — resolved fresh before every LLM call (e.g.,
DATE,TIME,RANDOM)
Built-in Templates
| Template | Phase | Description |
|---|---|---|
{{AGENT_ID}} |
static | Agent's ID/name |
{{AGENT_NAME}} |
static | Agent's display name |
{{MODEL}} |
static | Current model (e.g., gpt-4.1) |
{{VENDOR}} |
static | Current vendor (e.g., openai) |
{{USER_ID}} |
static | Current user ID |
{{DATE}} |
dynamic | Current date — default YYYY-MM-DD |
{{DATE:format}} |
dynamic | Formatted date (e.g., {{DATE:MM/DD/YYYY}}, {{DATE:DD.MM.YY}}) |
{{TIME}} |
dynamic | Current time — default HH:mm:ss |
{{TIME:format}} |
dynamic | Formatted time (e.g., {{TIME:hh:mm A}} → 02:30 PM) |
{{DATETIME}} |
dynamic | Combined — default YYYY-MM-DD HH:mm:ss |
{{DATETIME:format}} |
dynamic | Formatted (e.g., {{DATETIME:YYYY/MM/DD HH:mm}}) |
{{RANDOM}} |
dynamic | Random integer 1–100 |
{{RANDOM:min:max}} |
dynamic | Random integer in range (e.g., {{RANDOM:1:10}}) |
Format tokens: YYYY, YY, MM, DD, HH (24h), hh (12h), mm, ss, A (AM/PM), a (am/pm)
Basic Usage
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
name: 'research-assistant',
userId: 'user-42',
instructions: `You are {{AGENT_NAME}}, running on {{VENDOR}}/{{MODEL}}.
Today is {{DATE:MM/DD/YYYY}}. Current time: {{TIME:hh:mm A}}.
Assigned to user {{USER_ID}}.
Your session token is {{RANDOM:1000:9999}}.`,
});
// After creation, static templates are already resolved:
// "You are research-assistant, running on openai/gpt-4.1."
// Dynamic templates resolve on each LLM call:
// "Today is 04/12/2026. Current time: 02:30 PM."
Custom Handlers
Register your own template commands with TemplateEngine.register(). Handlers receive the arg string (after the colon) and a context object:
import { TemplateEngine } from 'oneringai';
// Simple static handler (resolved once at agent creation)
TemplateEngine.register('COMPANY', () => 'Acme Corp');
// Handler with argument
TemplateEngine.register('GREET', (style, ctx) => {
const name = (ctx.userName as string) ?? 'there';
return style === 'casual' ? `Hey ${name}!` : `Good day, ${name}.`;
});
// Usage: {{GREET:casual}} → "Hey Alice!"
// Async dynamic handler (resolved every LLM call)
TemplateEngine.register('DB_COUNT', async (collection) => {
const count = await db.collection(collection!).countDocuments();
return String(count);
}, { dynamic: true });
// Usage: {{DB_COUNT:users}} → "4210"
// i18n translation handler
const translations: Record<string, Record<string, string>> = { en: { hello: 'Hello' }, es: { hello: 'Hola' } };
TemplateEngine.register('T', (key, ctx) => {
const lang = (ctx.lang as string) ?? 'en';
return translations[lang]?.[key ?? ''] ?? `[missing: ${key}]`;
});
// Usage: {{T:hello}} → "Hola" (when ctx.lang = 'es')
Overriding Built-in Handlers
Client apps can override any built-in handler. For example, to make {{DATE}} respect the user's timezone:
import { TemplateEngine } from 'oneringai';
TemplateEngine.register('DATE', (fmt, ctx) => {
const tz = (ctx.timezone as string) ?? 'UTC';
const now = new Date();
if (!fmt) {
return now.toLocaleDateString('en-CA', { timeZone: tz });
}
// Custom format logic with timezone...
return now.toLocaleDateString('en-US', { timeZone: tz });
}, { dynamic: true });
Overrides work regardless of registration order — user registrations always win over built-ins.
Escaping Templates
When you need to pass {{...}} literally to the LLM (e.g., to explain the template syntax itself), use one of two escape mechanisms:
Triple braces — for inline escaping:
const instructions = `You are {{AGENT_ID}}.
When users ask about templates, tell them:
- {{{DATE}}} inserts the current date
- {{{RANDOM:min:max}}} generates a random number`;
// Resolves to:
// "You are my-agent."
// "- {{DATE}} inserts the current date"
// "- {{RANDOM:min:max}} generates a random number"
Raw blocks — for longer passages:
const instructions = `Today is {{DATE}}.
{{raw}}
## Template Reference
{{DATE}} - Current date
{{TIME}} - Current time
{{AGENT_ID}} - Agent identifier
{{RANDOM:min:max}} - Random number
{{/raw}}`;
// {{DATE}} outside the raw block resolves normally.
// Everything inside {{raw}}...{{/raw}} is preserved verbatim.
How It Works
Templates are processed in two phases integrated into the agent lifecycle:
-
Agent creation (
Agent.create()) —TemplateEngine.processSync()runs the static pass onconfig.instructions, resolvingAGENT_ID,MODEL,VENDOR,USER_ID, and any custom static handlers. -
Before each LLM call (
AgentContextNextGen.buildSystemMessage()) —TemplateEngine.process()runs the dynamic pass, resolvingDATE,TIME,DATETIME,RANDOM, and any custom dynamic handlers.
Unknown {{COMMANDS}} are left as-is — they won't cause errors and can coexist with other templating systems.
API Reference
// Static API signatures
// Register a handler (overrides existing, including built-ins)
TemplateEngine.register(name: string, handler: TemplateHandler, options?: { dynamic?: boolean }): void;
// Unregister a handler
TemplateEngine.unregister(name: string): void;
// Check if a handler exists
TemplateEngine.has(name: string): boolean;
// List all registered handler names
TemplateEngine.getRegisteredHandlers(): string[];
// Process templates (async — supports async handlers)
await TemplateEngine.process(text, context?, { phase?: 'static' | 'dynamic' | 'all' }): Promise<string>;
// Process templates (sync — throws if a matched handler returns a Promise)
TemplateEngine.processSync(text, context?, { phase?: 'static' | 'dynamic' | 'all' }): string;
// Handler signature
type TemplateHandler = (arg: string | undefined, context: TemplateContext) => string | Promise<string>;
// Context passed to handlers
interface TemplateContext {
agentId?: string;
agentName?: string;
model?: string;
vendor?: string;
userId?: string;
[key: string]: unknown; // custom fields
}
Control Methods
const agent = Agent.create({ connector: 'openai', model: 'gpt-4.1' });
// Pause execution
agent.pause();
// Resume execution
agent.resume();
// Cancel current execution
agent.cancel();
// Check status
if (agent.isRunning()) {
console.log('Agent is processing...');
}
if (agent.isPaused()) {
console.log('Agent is paused');
}
Metrics & Audit Trail
const agent = Agent.create({ connector: 'openai', model: 'gpt-4.1' });
await agent.run('Hello!');
// Get execution metrics
const metrics = agent.getMetrics();
console.log(metrics.iterationCount); // 1
console.log(metrics.totalTokens); // 150
console.log(metrics.totalDuration); // 1200 (ms)
// Get audit trail
const audit = agent.getAuditTrail();
audit.forEach(entry => {
console.log(`${entry.timestamp}: ${entry.type} - ${JSON.stringify(entry.details)}`);
});
Cleanup
const agent = Agent.create({ connector: 'openai', model: 'gpt-4.1' });
// Register cleanup callback
agent.onCleanup(() => {
console.log('Cleaning up resources...');
});
// Destroy agent
agent.destroy();
Multi-User Support (userId)
For multi-user systems, set userId once at agent creation and it automatically flows to all tool executions via ToolContext.userId — no need to manually thread it through every call:
const myTool: ToolFunction = {
definition: { type: 'function', function: {
name: 'current_user', description: 'Read the current host-selected user ID.',
parameters: { type: 'object', properties: {}, additionalProperties: false },
} },
execute: async (_args, context) => ({ userId: context?.userId }),
};
const agent = Agent.create({
connector: 'openai', model: 'gpt-4.1', userId: 'user-123', tools: [myTool],
});
// Existing setter; change only while idle and with compatible host-owned scope.
agent.userId = 'user-456';
// Also accessible via context
console.log(agent.context.userId); // 'user-456'
userId propagates identity; it is not authentication or a tenant boundary.
Use a fresh Agent/context for unrelated users and derive connector visibility,
memory/evidence scope, permissions, and storage from trusted host authorization.
Changing the setter does not erase an earlier conversation or transfer its scope.
What userId enables:
- Tool context — Every
tool.execute(args, context)receivescontext.userIdautomatically - Authenticated API calls — All ConnectorTools (generic API, GitHub, etc.) use userId for per-user OAuth tokens at execution time
- Connector registry —
context.connectorRegistryis scoped to the user when an access policy is set - Session metadata —
userIdis automatically included when saving sessions - Per-user storage — Multimedia tools organize output by userId when set
Setting userId at different levels:
// Option 1: At agent level (recommended)
const agent = Agent.create({ connector: 'openai', model: 'gpt-4.1', userId: 'user-123' });
// Option 2: At context level
const agentVariant2 = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
context: { model: 'gpt-4.1', userId: 'user-123' },
});
// Option 3: At runtime
agentVariant2.userId = 'user-123';
Auth Identities (identities)
Restrict an agent to specific connectors (and optionally specific accounts). Only listed identities produce tool sets and are accessible in sandbox execution:
import type { AuthIdentity } from 'oneringai';
const agent = Agent.create({
connector: 'openai',
model: 'gpt-4.1',
userId: 'user-123',
identities: [ // Only these connectors available to tools
{ connector: 'github' },
{ connector: 'slack' },
{ connector: 'microsoft', accountId: 'work' }, // Specific account
],
tools: [executeJavaScript],
});
// Tools only see github, slack, and microsoft (work account) — stripe, etc. are invisible
// This works with userId scoping: identities filter on top of access-policy view
// Change at runtime
agent.identities = [
{ connector: 'github' },
{ connector: 'slack' },
{ connector: 'stripe' },
];
// Remove restriction (all visible connectors available)
agent.identities = undefined;
How it composes with access policies:
- Access policy filters connectors by userId (if set)
identitiesfurther restricts to named connectors (and accounts)- Result: only connectors matching identities AND visible to the user
Available via ToolContext.connectorRegistry — tools that need connector access (like execute_javascript) read the pre-built, scoped registry directly from their execution context.