@backendkit-labs/agent-core
The foundation engine for the BackendKit Labs agent framework. Provides everything needed to build, run, and compose AI agents: typed tool definitions, provider adapters, session memory, MCP client, workflow orchestration, and advanced multi-layer memory.
Status:
core— Required by the enterprise use case. Breaking changes go through full review before merging.
Table of contents
- Installation
- Quick start
- defineTool — typed tools with Zod
- AgentEngine — the execution core
- createBaseEngine — high-level factory
- Context compaction (token budget)
- Guardrails
- Providers
- Transports
- SessionMemory
- MCP client
- Workflow engine
- Advanced memory system
- Observability
- Skills and slash commands
- Checkpoints
- Workspaces
- Autonomous agents and triggers
- Delegation bus
- Type reference
Installation
npm install @backendkit-labs/agent-core
Requires Node.js ≥ 18 and TypeScript ≥ 5.0.
Quick start
The fastest path to a running agent:
import {
AgentEngine,
AgentRegistry,
ToolRegistry,
ProviderRegistry,
defineTool,
z,
StdoutTransport,
} from '@backendkit-labs/agent-core';
// 1. Define a tool
const weatherTool = defineTool({
name: 'get_weather',
description: 'Returns current weather for a city',
input: z.object({
city: z.string().describe('City name'),
}),
execute: async ({ city }) => `Weather in ${city}: sunny, 22°C`,
});
// 2. Register tools and agents
const tools = new ToolRegistry();
tools.register(weatherTool);
const agents = new AgentRegistry();
agents.register({
id: 'general',
name: 'General Assistant',
systemPrompt: 'You are a helpful assistant. Use available tools when relevant.',
tools: ['get_weather'],
});
// 3. Register an LLM provider
const providers = new ProviderRegistry();
providers.register('my-provider', myLLMProvider);
// 4. Run
const engine = new AgentEngine({
model: { provider: 'my-provider', id: 'gpt-4o' },
agents,
tools,
providers,
defaultProvider: 'my-provider',
defaultAgentId: 'general',
transport: new StdoutTransport(),
maxIterations: 10,
iterationMode: 'auto',
});
await engine.run("What's the weather in London?");
defineTool — typed tools with Zod
defineTool wraps a function with a Zod input schema. The engine validates inputs before calling execute, and the TypeScript types flow through automatically.
Basic tool
import { defineTool, z } from '@backendkit-labs/agent-core';
const greetTool = defineTool({
name: 'greet',
description: 'Greet a user by name',
input: z.object({ name: z.string() }),
execute: async ({ name }) => `Hello, ${name}!`,
});
Optional parameters and defaults
const searchTool = defineTool({
name: 'search_products',
description: 'Search the product catalog',
input: z.object({
query: z.string().describe('Search term'),
category: z.string().optional().describe('Filter by category'),
limit: z.number().min(1).max(50).default(10),
sort: z.enum(['price_asc', 'price_desc', 'relevance']).default('relevance'),
}),
execute: async ({ query, category, limit, sort }) => {
const results = await db.products.search({ query, category, limit, sort });
return results.map(p => `${p.name} — $${p.price}`).join('\n');
},
});
Structured / nested input
const createOrderTool = defineTool({
name: 'create_order',
description: 'Create a new customer order',
input: z.object({
customer: z.object({
id: z.string(),
email: z.string().email(),
}),
items: z.array(z.object({
productId: z.string(),
qty: z.number().int().positive(),
})).min(1),
shippingAddress: z.string(),
notes: z.string().optional(),
}),
execute: async ({ customer, items, shippingAddress, notes }) => {
const order = await orders.create({ customer, items, shippingAddress, notes });
return `Order ${order.id} created for ${items.length} item(s).`;
},
});
Returning JSON
Return a JSON string so the LLM can reason about structured data:
const analyzeCodeTool = defineTool({
name: 'analyze_code',
description: 'Analyze code and return structured issues',
input: z.object({
code: z.string(),
language: z.string(),
}),
execute: async ({ code, language }) => {
const issues = await analyzer.run(code, language);
return JSON.stringify({
issueCount: issues.length,
critical: issues.filter(i => i.severity === 'critical'),
warnings: issues.filter(i => i.severity === 'warning'),
});
},
});
Tool that throws (error signaling)
When a tool throws, the engine catches the error and tells the LLM, which can retry with corrected arguments or apologize to the user:
const lookupUserTool = defineTool({
name: 'lookup_user',
description: 'Look up a user by email',
input: z.object({ email: z.string().email() }),
execute: async ({ email }) => {
const user = await db.users.findByEmail(email);
if (!user) throw new Error(`No user found with email ${email}`);
return JSON.stringify(user);
},
});
AgentEngine — the execution core
AgentEngine is the stateful agent runner. It owns the conversation loop, tool dispatch, multi-agent delegation, and iteration control.
Constructor options
new AgentEngine({
// Required
model: { provider: 'openai', id: 'gpt-4o' },
agents,
tools,
providers,
defaultProvider: 'openai',
defaultAgentId: 'general',
// I/O
transport: new StdoutTransport(),
sessionId: 'session-xyz',
// Iteration control
maxIterations: 50,
iterationMode: 'auto', // 'auto' | 'step-by-step' | 'interactive' (default)
// Memory
sessionMemory: mySessionMemory,
memorySystem: myMemorySystem,
historyPath: '/path/to/history.json',
// MCP servers (connected lazily on first run)
mcpServers: [
{ name: 'github', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'] }
],
// Observability
observability: obsManager,
auditLog: '/path/to/audit.ndjson',
// Context injected into the system prompt
projectContext: 'You are working in project X...',
})
Running the engine
// Basic run — returns when done
const result = await engine.run('Summarize the README.md file');
console.log(result.final); // final assistant response
// Run with a specific agent
const result = await engine.run('Review this code for security issues', {
agentId: 'security-reviewer',
});
// Stream events to a UI
const events: AgentEvent[] = [];
await engine.run('Analyze the database schema', {
onEvent: (event) => {
if (event.type === 'chunk') process.stdout.write(event.content);
events.push(event);
},
});
Iteration modes
| Mode | Behavior |
|---|---|
'auto' | Runs until complete or maxIterations reached |
'step-by-step' | Calls onStep after every iteration; return false to stop |
'interactive' | Calls onIterationLimit when cap is reached; return true to continue. Without that callback the run stops at the cap |
Tool approval is independent of the mode: whenever onToolApproval is set, the engine asks before write_file, edit_file, run_command and package installs.
// Approval — user approves destructive tool calls
const engine = new AgentEngine({
...opts,
onToolApproval: async (toolName, agentId, argsPreview) => {
console.log(`[${agentId}] → ${toolName}:`, argsPreview);
const answer = await prompt('Approve? (y/n/all) ');
if (answer === 'y') return 'approve';
if (answer === 'all') return 'approve_all';
return 'reject';
},
});
// Step-by-step — inspect each iteration
const engine = new AgentEngine({
...opts,
iterationMode: 'step-by-step',
onStep: async (stats) => {
console.log(`Iteration ${stats.count}/${stats.max} — agent: ${stats.agentId}`);
return true; // false to stop
},
});
Multi-turn sessions
The engine retains conversation history between run() calls on the same instance:
await engine.run('My name is Alice.');
await engine.run("What's my name?"); // → "Your name is Alice."
createBaseEngine — high-level factory
createBaseEngine assembles all internals from a flat options object so you don't have to instantiate registries manually.
import { createBaseEngine } from '@backendkit-labs/agent-core';
const engine = createBaseEngine({
providers: { openai: myOpenAIProvider },
defaultProvider: 'openai',
agents: myAgentList, // AgentProfile[]
tools: myToolList, // ToolDefinition[]
defaultAgent: 'general',
transport: new StdoutTransport(),
maxIterations: 100,
workingDir: process.cwd(),
});
await engine.run('Hello!');
With MCP servers and orchestration
const engine = createBaseEngine({
providers: { anthropic: myAnthropicProvider },
defaultProvider: 'anthropic',
agents: myAgents,
tools: myTools,
defaultAgent: 'general',
mcpServers: [
{
name: 'github',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-github'],
env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN! },
},
{
name: 'postgres',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-postgres', process.env.DATABASE_URL!],
},
],
orchestration: true, // enables intent detection + domain routing + QA review
});
Context compaction (token budget)
maxContextMessages caps the history by message count and discards what it cuts. With context the
history is kept under a token budget instead, and old turns are summarized rather than lost:
const engine = createBaseEngine({
// ...
context: {
maxTokens: 60_000, // budget for the history (not the system prompt)
summarizer: { provider: 'deepseek', model: 'deepseek-chat' }, // cheap model for summaries (default: the default provider)
// strategy: 'summarize' | 'drop' (default 'summarize'; a failing summarizer falls back to 'drop')
// keepRecentTurns: 4 (last user turns are never touched)
// toolResultMaxChars: 8000 (older tool results are cut head+tail)
// threshold: 0.85 (compact above 85% of maxTokens, down to 70%)
// estimateTokens: (msg) => number (default ~4 chars per token, calibrated with real provider usage)
},
});
Before every LLM call the engine (1) truncates bulky old tool results, (2) folds the oldest turns into one
<conversation_summary> block merged into the first kept user turn, extending the previous summary instead
of nesting a new one, and (3) as a last resort drops them. Cuts always fall on a user-turn boundary, so
tool_call / tool result pairs are never orphaned. It emits compacting events and a [Context] system line;
summarizer usage is reported as metrics events like any other call, so CostTracker accounts for it.
Compaction is best-effort: any failure is reported and the run continues. Can be combined with maxContextMessages.
Guardrails
Checks on what enters and leaves the agent loop, applied to the main agent and every sub-agent:
| Phase | Text inspected | When a guardrail blocks |
|---|---|---|
input | the user message | the run ends with an error event; the model is never called |
tool_call | the JSON arguments of a tool call (before approval) | the tool does not run; the model is told why and the arguments are scrubbed from the history |
tool_result | what a tool returned (before the model reads it) | the result is replaced by a notice |
output | the model's answer | the answer is replaced by a notice in the history |
import { createBaseEngine, secretLeakGuardrail, piiGuardrail, promptInjectionGuardrail } from '@backendkit-labs/agent-core';
const engine = createBaseEngine({
// ...
guardrails: [secretLeakGuardrail(), piiGuardrail(), promptInjectionGuardrail()],
guardrailOutputMode: 'after', // or 'buffer'
});
Ready-made: piiGuardrail (email, phone [heuristic], card [Luhn], IBAN [mod 97], US SSN), secretLeakGuardrail
(literal AgentProfile.secrets of every agent plus common key formats; always blocks them in tool arguments),
promptInjectionGuardrail (heuristic: wraps suspicious tool results as <untrusted_content>; not a guarantee),
maxLengthGuardrail, denyListGuardrail, llmJudgeGuardrail (one extra LLM call per check).
Write your own: { name, phases, check(ctx) => { action: 'allow' } | { action: 'redact', content, reason } | { action: 'block', reason }, failMode?, dryRun? }.
redact feeds the next guardrail; the first block wins. A guardrail that throws blocks (failMode: 'closed', default)
unless you set failMode: 'open'. dryRun: true only reports, which is the way to tune false positives.
guardrailOutputMode: with after (default) tokens stream as they arrive; if an output guardrail changes the answer, the
stored history is corrected and a guardrail event carries the replacement for the UI. With buffer tokens are held
until the answer is checked: nothing unchecked is ever shown, but there is no streaming.
Every redaction, block and error emits a { type: 'guardrail', name, phase, action, reason, ... } event, is written to the
auditLog as guardrail:<phase>:<name>, and is counted in MetricsSnapshot.guardrails.
Tool cancellation and time limits
Every tool call receives ctx.signal, an AbortSignal that fires when the engine is aborted (engine.abort(), or a budget stop with abortOnBudget) or when the call's time limit passes. Hand it to fetch, child processes and anything else cancellable:
const fetchPage = defineTool({
name: 'fetch_page',
description: 'Fetch a URL',
input: z.object({ url: z.string().url() }),
timeoutMs: 15_000, // this tool's limit; overrides the engine default
execute: async ({ url }, ctx) => (await fetch(url, { signal: ctx.signal })).text(),
});
const engine = createBaseEngine({
// ...
toolTimeoutMs: 60_000, // default limit for tools without their own (default: none)
toolTimeoutGraceMs: 2000, // wait for a tool to wind down after its signal fires
});
When a limit passes, the signal is aborted and, after the grace period, the model receives Error: TOOL_TIMEOUT: tool "fetch_page" exceeded 15000ms (tool_result.outcome = 'timeout'); a result that arrives later is discarded. After engine.abort() a running tool reports Error: aborted (outcome = 'aborted'). Time spent waiting for approval does not count. A tool with side effects that times out may still have completed, so make such tools idempotent.
Providers
Providers wrap LLM APIs and implement the LLMProvider interface. ProviderRegistry holds named providers that the engine selects per-agent.
import { ProviderRegistry } from '@backendkit-labs/agent-core';
const providers = new ProviderRegistry();
// Register multiple providers — engine selects per-agent or falls back to defaultProvider
providers.register('openai', openAIProvider);
providers.register('anthropic', anthropicProvider);
providers.register('local', ollamaProvider);
Custom LLM provider
Implement LLMProvider to add any API:
import type { LLMProvider, LLMMessage, LLMStreamCallbacks, ToolDefinition } from '@backendkit-labs/agent-core';
class MyProvider implements LLMProvider {
async chat(
messages: LLMMessage[],
tools: ToolDefinition[],
callbacks: LLMStreamCallbacks,
): Promise<void> {
const stream = await myAPI.stream({ messages, tools });
for await (const chunk of stream) {
if (chunk.type === 'text') callbacks.onChunk(chunk.text);
if (chunk.type === 'tool_use') callbacks.onToolCall({ id: chunk.id, name: chunk.name, arguments: chunk.input });
}
callbacks.onDone();
}
}
providers.register('my-api', new MyProvider());
Transports
Transports receive AgentEvent objects and deliver them to consumers.
StdoutTransport
Prints events as formatted text — good for CLI tools:
import { StdoutTransport } from '@backendkit-labs/agent-core';
const engine = new AgentEngine({ ...opts, transport: new StdoutTransport() });
CallbackTransport
Routes events to your own function — use this for server integrations or streaming UIs:
import { CallbackTransport } from '@backendkit-labs/agent-core';
const transport = new CallbackTransport((event) => {
switch (event.type) {
case 'chunk': process.stdout.write(event.content); break;
case 'tool_call': console.log(`[tool] ${event.name}`); break;
case 'agent_switch': console.log(`→ ${event.to}`); break;
case 'done': console.log('\n[done]'); break;
}
});
AgentEvent types
| Type | Key fields | Description |
|---|---|---|
'chunk' | content: string | Incremental LLM token |
'tool_call' | name, args, result | Tool called and returned |
'agent_switch' | from, to | Multi-agent delegation |
'iteration' | count, max | Iteration counter |
'done' | final: string | Run completed |
'error' | message: string | Recoverable error |
SessionMemory
SessionMemory stores conversation messages for the current session. AgentEngine manages it automatically; use it directly for custom initialization or testing.
import { SessionMemory } from '@backendkit-labs/agent-core';
const memory = new SessionMemory();
// Seed with prior context
memory.addMessage({ role: 'user', content: 'My name is Alice.' });
memory.addMessage({ role: 'assistant', content: 'Nice to meet you, Alice!' });
const engine = new AgentEngine({ ...opts, sessionMemory: memory });
await engine.run("What's my name?"); // → "Your name is Alice."
MCP client
MCPClientManager connects to external MCP (Model Context Protocol) servers, discovers their tools, and makes them available to the engine. Supports stdio, Streamable HTTP, and SSE transports.
Stdio (most common)
import { MCPClientManager } from '@backendkit-labs/agent-core';
const mcp = new MCPClientManager();
await mcp.connectStdio({
name: 'filesystem',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/workspace'],
env: {},
});
const tools = mcp.getTools(); // ToolDefinition[] — discovered from the server
HTTP transport
await mcp.connectStreamableHTTP({
name: 'remote-server',
baseUrl: 'https://mcp.example.com',
apiKey: process.env.MCP_API_KEY,
});
SSE transport (legacy servers)
await mcp.connectSSE({
name: 'legacy-server',
url: 'http://localhost:8080/sse',
});
Multiple servers via engine options
Passing mcpServers to createBaseEngine or AgentEngine is the recommended path — the engine manages the lifecycle, retries, and disconnect detection:
const engine = createBaseEngine({
...opts,
mcpServers: [
{ name: 'github', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'], env: { GITHUB_TOKEN: '...' } },
{ name: 'slack', command: 'npx', args: ['-y', '@modelcontextprotocol/server-slack'], env: { SLACK_TOKEN: '...' } },
{ name: 'postgres', command: 'npx', args: ['-y', '@modelcontextprotocol/server-postgres'], env: { DATABASE_URL: '...' } },
],
});
Disconnect detection (health monitoring)
The client detects server crashes via client.onclose and marks them 'failed', preventing stale 'connected' state:
// The engine's MCPRegistrar wires this automatically.
// For manual use:
mcp.setDisconnectHandler((serverName) => {
console.warn(`MCP server "${serverName}" disconnected`);
// Re-attempt connection on next engine.run() call
});
Workflow engine
Build declarative multi-agent workflows with WorkflowBuilder. Steps can run sequentially, in parallel, or conditionally.
Sequential workflow
import { workflow, agent } from '@backendkit-labs/agent-core';
const reviewPipeline = workflow('code-review')
.step('fetch', agent('general', 'List all TypeScript files in src/'))
.step('analyze', agent('security-reviewer', 'Review the files for security issues'))
.step('report', agent('general', 'Summarize the findings into a report'));
const result = await engine.runWorkflow(reviewPipeline);
console.log(result.steps.report.output);
Context passing between steps
Steps reference previous outputs with {{stepName.output}}:
const dataPipeline = workflow('etl')
.step('ingest', agent('data-agent', 'Read /data/sales.csv'))
.step('transform', agent('data-agent', 'Clean {{ingest.output}}: fix nulls, normalize dates'))
.step('analyze', agent('analyst', 'Find the top 5 trends in: {{transform.output}}'));
Parallel steps
const auditWorkflow = workflow('full-audit')
.parallel([
agent('frontend-agent', 'Audit React components for accessibility'),
agent('backend-agent', 'Audit API endpoints for security'),
agent('database-agent', 'Audit database schema for missing indexes'),
])
.step('merge', agent('general', 'Synthesize all three audit reports'));
Conditional steps
const smartReview = workflow('smart-review')
.step('classify', agent('general', 'Classify this PR: feature, bug fix, or refactor. Reply with just the type.'))
.when('{{classify.output}} includes "bug"',
agent('qa-engineer', 'Focus on regression risk and edge cases')
)
.otherwise(
agent('general', 'Perform a standard code quality review')
);
WorkflowRunner (direct usage)
import { WorkflowRunner } from '@backendkit-labs/agent-core';
const runner = new WorkflowRunner(engine);
const result = await runner.run(myWorkflow, {
initialContext: { projectName: 'my-app', version: '2.0' },
onStepStart: (name) => console.log(`▶ ${name}`),
onStepDone: (name, out) => console.log(`✓ ${name}: ${out.slice(0, 80)}`),
});
// result.steps — Map<stepName, { output, durationMs }>
// result.total — total wall time in ms
// result.failed — boolean
Advanced memory system
Beyond session history, agents can use a three-layer persistent memory. Each layer is independently configurable and backed by a pluggable store.
| Layer | Purpose | Use case |
|---|---|---|
EpisodicMemory | What happened — events and interactions | "Last week we refactored auth" |
SemanticMemory | What is known — facts and domain knowledge | "Alice owns the auth module" |
ProceduralMemory | How to do it — step-by-step patterns | "Deploy: build → push → apply" |
Creating a full memory system
import { createMemorySystem } from '@backendkit-labs/agent-core';
const memory = createMemorySystem({
episodic: {
provider: 'json',
path: '~/.bk-agent/projects/my-project/memory/episodes.json',
},
semantic: {
provider: 'json',
path: '~/.bk-agent/projects/my-project/memory/knowledge.json',
},
procedural: {
provider: 'json',
path: '~/.bk-agent/projects/my-project/memory/patterns.json',
},
retention: {
maxEpisodes: 500, // prunes by relevance score when exceeded (v0.22.3+)
summarizeAfter: 50,
// Optional: compress old episodes with an LLM summary
summarizeFn: async (episodes) => {
const list = episodes.map((e, i) => `${i + 1}. ${e.content}`).join('\n');
return await llm.summarize(list);
},
},
});
// Pass to engine
const engine = new AgentEngine({ ...opts, memorySystem: memory });
Using each layer directly
const { episodic, semantic, procedural } = memory;
// --- Episodic ---
await episodic.add({
content: 'User asked to refactor the auth module',
tags: ['refactor', 'auth'],
timestamp: Date.now(),
});
const recentAuth = await episodic.search('auth', { limit: 5 });
// [{ id, content, tags, timestamp }, ...]
// --- Semantic ---
await semantic.add({
key: 'auth-module-owner',
value: 'Alice (Backend Team)',
tags: ['ownership', 'auth'],
});
const owner = await semantic.get('auth-module-owner');
// 'Alice (Backend Team)'
const authKnowledge = await semantic.search('auth', { limit: 10 });
// --- Procedural ---
await procedural.add({
name: 'deploy-to-staging',
steps: ['npm run build', 'docker build .', 'kubectl apply -f k8s/staging/'],
tags: ['deployment'],
});
const deploySteps = await procedural.get('deploy-to-staging');
Forgetting curve (v0.22.3+)
When maxEpisodes is set and the limit is exceeded, EpisodicMemory prunes by relevance score rather than raw age:
relevanceScore = recency(0.6) × 1/(1 + daysSince × 0.1)
+ utility(0.4) × log(1 + recallCount)
Episodes that are recalled frequently survive longer than episodes that sit idle. recall() automatically increments recallCount on every matched record — no extra calls needed.
// Episode recalled 4× over 30 days: score ~0.43
// Episode that sat idle for 30 days: score ~0.15
// → idle episode is pruned first when the store is full
The recallCount and lastRecalledAt fields are persisted in JsonFileStore and survive restarts.
In-memory store (testing)
import { InMemoryStore, EpisodicMemory } from '@backendkit-labs/agent-core';
const episodic = new EpisodicMemory(new InMemoryStore());
await episodic.add({ content: 'test event', tags: [], timestamp: Date.now() });
JSON file store (production)
import { JsonFileStore, SemanticMemory } from '@backendkit-labs/agent-core';
const semantic = new SemanticMemory(new JsonFileStore('/app/data/knowledge.json'));
MemoryStoreBackend interface
All backends implement:
interface MemoryStoreBackend {
insert(record: StoredRecord): void;
upsert(id: string, record: StoredRecord): void;
getAll(): StoredRecord[];
deleteOldest(keepNewest: number): void;
deleteByIds(ids: string[]): void; // v0.22.3+: used by relevance-based pruning
count(): number;
}
deleteByIds is used internally by EpisodicMemory pruning. Use it in custom backends to support the forgetting curve.
Memory tools (agents writing their own memory)
buildMemoryTools() creates tool definitions that let agents read and write all three memory layers:
import { buildMemoryTools } from '@backendkit-labs/agent-core';
const memTools = buildMemoryTools(memory);
// Returns: remember_episode, recall_episodes, store_knowledge, recall_knowledge,
// store_pattern, recall_pattern
memTools.forEach(t => tools.register(t));
Observability
ObservabilityManager ties together cost tracking, metrics collection, and distributed tracing.
Enable observability on the engine
import { ObservabilityManager } from '@backendkit-labs/agent-core';
const obs = new ObservabilityManager({
costTracking: true,
metricsEnabled: true,
tracing: true,
});
const engine = new AgentEngine({ ...opts, observability: obs });
Cost tracking
import { CostTracker, MODEL_PRICING } from '@backendkit-labs/agent-core';
const tracker = new CostTracker();
// MODEL_PRICING[model] = { input: $/1M tokens, output: $/1M tokens }
console.log(MODEL_PRICING['gpt-4o']); // { input: 2.50, output: 10.00 }
console.log(MODEL_PRICING['claude-sonnet-4-6']); // { input: 3.00, output: 15.00 }
tracker.record({ model: 'gpt-4o', inputTokens: 1500, outputTokens: 200 });
console.log(`Cost: $${tracker.getTotalCostUSD().toFixed(6)}`); // $0.005750
Metrics snapshot
import { MetricsCollector } from '@backendkit-labs/agent-core';
const metrics = new MetricsCollector();
// After some runs:
const snap = metrics.getSnapshot();
// {
// sessionsActive: 3,
// runsTotal: 47,
// runsSucceeded: 44,
// runsFailed: 3,
// avgDurationMs: 1823,
// }
Custom span exporter
Export traces to Jaeger, Honeycomb, Datadog, or any OpenTelemetry backend:
import { Tracer } from '@backendkit-labs/agent-core';
const tracer = new Tracer({
onSpan: (span) => {
honeycomb.sendEvent({
'span.name': span.name,
'duration_ms': span.durationMs,
'agent.id': span.agentId,
'tool.calls': span.toolCallCount,
});
},
});
Audit log (NDJSON)
Every tool call can be logged to a newline-delimited JSON file:
const engine = new AgentEngine({
...opts,
auditLog: '/var/log/agent-audit.ndjson',
// Each line: { ts, agentId, toolName, args, result, durationMs }
});
Skills and slash commands
Skills are reusable prompt snippets that augment agent behavior. Slash commands let users invoke agent features directly.
Loading built-in skills
import { SkillLoader, createSkillManager } from '@backendkit-labs/agent-core';
const manager = createSkillManager('my-app');
// Install a skill pack from GitHub
await manager.install({
source: 'github:backendkit-labs/skills#main',
pack: 'backend',
});
const installed = manager.listInstalled();
// [{ id, name, pack, version }, ...]
Inline skill definition
import { SkillRegistry, SkillActivator } from '@backendkit-labs/agent-core';
const registry = new SkillRegistry();
registry.register({
id: 'pr-review',
name: 'PR Review',
description: 'Perform a thorough pull request review',
prompt: `
Review this pull request with the following criteria:
1. Correctness — does the code do what the PR description claims?
2. Security — any new attack surface?
3. Performance — any obvious regressions?
4. Tests — are changes covered?
Output a concise review with LGTM or CHANGES REQUESTED at the end.
`,
tags: ['github', 'review'],
});
const activator = new SkillActivator(registry);
const systemContext = activator.buildContext(['pr-review']);
// Returns the skill prompt injected into the system context
Slash commands
Built-in commands: /help, /clear, /status, /skills, /agents, /workspace, /checkpoint.
import { SlashCommandRegistry, registerBuiltinCommands } from '@backendkit-labs/agent-core';
const cmds = new SlashCommandRegistry();
registerBuiltinCommands(cmds);
// Custom command
cmds.register({
name: 'deploy',
description: 'Trigger a deployment to an environment',
handler: async (ctx) => {
const env = ctx.args[0] ?? 'staging';
await ci.triggerDeploy(env);
return `Deployment to ${env} triggered. Watch: https://ci.company.com/jobs/latest`;
},
});
// Handle user input
const input = '/deploy production';
if (input.startsWith('/')) {
const response = await cmds.handle(input, agentContext);
console.log(response);
}
Checkpoints
Save and restore session state for recovery and branching.
Save a checkpoint
import { createCheckpoint, listCheckpoints, readCheckpoint } from '@backendkit-labs/agent-core';
// After a successful milestone
await createCheckpoint({
sessionId: 'session-abc',
messages: engine.getMessages(),
projectDir: '/app/.bk-agent/projects/my-project',
description: 'After auth module refactor — all tests pass',
});
List and restore
// List all checkpoints
const summaries = await listCheckpoints('/app/.bk-agent/projects/my-project');
// [{ id, timestamp, description, messageCount }, ...]
// Restore a specific checkpoint
const checkpoint = await readCheckpoint(summaries[0].id, '/app/.bk-agent/projects/my-project');
const restoredMemory = new SessionMemory();
restoredMemory.loadMessages(checkpoint.messages);
const engine = new AgentEngine({ ...opts, sessionMemory: restoredMemory });
await engine.run('Continue from where we left off');
Compact a long session
import { compactSession } from '@backendkit-labs/agent-core';
// Compress a 200-message conversation to a short summary
const compacted = await compactSession(
engine.getMessages(),
async (messages) => await llm.summarize(messages),
);
// Replace memory with compacted version to reduce context size
engine.replaceMessages(compacted);
Workspaces
Manage multiple projects and let agents switch context between them.
import {
loadWorkspaces, createWorkspace, addProject,
setActiveProject, buildWorkspaceContext,
} from '@backendkit-labs/agent-core';
// One-time setup
const config = await loadWorkspaces('my-app');
await createWorkspace('my-app', 'work');
await addProject('my-app', 'work', {
name: 'api-server',
path: '/home/user/projects/api-server',
});
await addProject('my-app', 'work', {
name: 'frontend',
path: '/home/user/projects/frontend',
});
// Switch active project
await setActiveProject('my-app', 'work', 'frontend');
// Build context string injected into the engine's system prompt
const ctx = buildWorkspaceContext(await loadWorkspaces('my-app'));
// "Active project: frontend (/home/user/projects/frontend)\nWorkspace: work\n..."
Autonomous agents and triggers
Create fire-and-forget agents that run on schedules or in response to events.
Scheduled agent (cron)
import { createAutonomousAgent } from '@backendkit-labs/agent-core';
const standupAgent = createAutonomousAgent({
engine: myEngine,
schedule: '0 9 * * 1-5', // cron: weekdays at 9 AM
buildPrompt: () => 'Generate a daily standup summary from recent git commits and open PRs.',
onResult: (output) => slack.postMessage('#standup', output),
onError: (err) => logger.error('[standup-agent]', err),
});
await standupAgent.start();
// Stops the cron job
await standupAgent.stop();
Event-driven agent
import { EventBus } from '@backendkit-labs/agent-core';
const eventBus = new EventBus();
// Subscribe to an event
eventBus.on('deployment.failed', async (payload) => {
const analysis = await engine.run(
`Investigate this deployment failure:\n${JSON.stringify(payload, null, 2)}`
);
await pagerduty.alert({ summary: analysis.final });
});
// Publish from your CI/CD system
await eventBus.emit('deployment.failed', {
service: 'payment-api',
version: '2.1.4',
errorLog: lastError,
});
TriggerManager (low-level)
import { TriggerManager } from '@backendkit-labs/agent-core';
const triggers = new TriggerManager();
// Cron trigger
triggers.addSchedule({
id: 'daily-report',
schedule: '0 8 * * *',
handler: async () => engine.run('Generate daily report'),
});
// Webhook trigger
triggers.addWebhook({
id: 'github-pr',
path: '/webhooks/github',
handler: async (body) => engine.run(`Review PR: ${body.pull_request.title}`),
});
await triggers.start();
Delegation bus
DelegationBus enables multi-agent collaboration — orchestrators delegate sub-tasks to specialized agents and collect results.
import { DelegationBus } from '@backendkit-labs/agent-core';
const bus = new DelegationBus();
// Register specialized agents
bus.register('security-agent', async (req) => ({
output: (await securityEngine.run(req.prompt)).final,
agentId: 'security-agent',
}));
bus.register('data-agent', async (req) => ({
output: (await dataEngine.run(req.prompt)).final,
agentId: 'data-agent',
}));
// Delegate from orchestrator
const secResult = await bus.delegate({
targetAgentId: 'security-agent',
prompt: 'Audit this SQL query for injection risks: SELECT * FROM users WHERE id=' + input,
context: currentMessages,
});
console.log(secResult.output);
Type reference
Key types exported from @backendkit-labs/agent-core:
// Engine
AgentEngineOptions // Full constructor options
BaseEngineOptions // Options for createBaseEngine
// Agents
AgentProfile // { id, name, systemPrompt, tools, provider? }
// Tools
ToolDefinition // { name, description, inputSchema, execute }
TypedToolOptions // Options for defineTool
ExecutionContext // Runtime context passed to tool.execute
// LLM
LLMProvider // Interface for LLM adapters
LLMMessage // { role, content }
LLMToolCall // { id, name, arguments }
LLMStreamCallbacks // { onChunk, onToolCall, onDone, onError }
ModelConfig // { provider, id }
// Memory
SessionMemoryData // Serialized session memory
MemorySystemConfig // createMemorySystem options
MemoryStoreBackend // 'json' | 'memory'
Episode // Episodic memory entry
Knowledge // Semantic memory entry
Pattern // Procedural memory entry
// MCP
MCPServerConfig // { name, command, args, env?, baseUrl?, url? }
// Workflows
WorkflowDef // Compiled workflow definition
WorkflowResult // { steps, total, failed }
WorkflowRunOptions // WorkflowRunner.run() options
// Iteration
IterationMode // 'auto' | 'step-by-step' | 'interactive'
IterationStats // { count, max, agentId }
ToolApprovalDecision // 'approve' | 'approve_all' | 'reject'
// Observability
ObservabilityConfig // { costTracking, metricsEnabled, tracing }
MetricsSnapshot // Current counters snapshot
Span // Tracing span
SpanExporter // Interface for custom exporters
// Events
AgentEvent // Union of all transport event types