Agent Architecture
Hollanov is the custom AI agent runtime powering the OpenClaw project. It's a TypeScript library that provides tool management, permissions, sessions, workflows, budget control, streaming, memory, skills, hooks, and a Claude API-powered agent loop. Hollanov handles Telegram; Ilya (the Hostinger OpenClaw gateway) handles WhatsApp.
Module Overview
| Module | File | Purpose |
|---|---|---|
| Types | src/types/index.ts | All shared type definitions |
| Tool Registry | src/agent/registry.ts | Register, query, and resolve tools |
| Permission System | src/agent/permissions.ts | Trust tiers, safety pipeline, approval handlers |
| Session Manager | src/agent/session.ts | Create, persist, and resume sessions |
| Workflow Manager | src/agent/workflow.ts | Stateful multi-step workflows with idempotency |
| Budget Tracker | src/agent/budget.ts | Token and turn budget with pre-check enforcement |
| Event Stream | src/agent/stream.ts | Typed streaming events with lifecycle management |
| System Logger | src/agent/logger.ts | Structured JSONL system event logging |
| Audit Trail | src/agent/audit.ts | Permission decision tracking and querying |
| Compaction | src/agent/compaction.ts | Transcript summarization to manage context size |
| Context Assembler | src/agent/context.ts | Provenance-aware context assembly with trust scoring |
| Agent Types | src/agent/agents.ts | Constrained agent roles with allowed/denied tools |
| Memory | src/agent/memory.ts | Persistent + session memory with provenance and relevance |
| Cost Tracker | src/agent/cost.ts | Claude API cost accounting and reporting |
| Heartbeat | src/agent/heartbeat.ts | Health status monitoring with ring-buffer history |
| Wiki | src/agent/wiki.ts | LLM-maintained knowledge base with frontmatter-driven pages |
| Skills | src/agent/skills.ts | Skill loader, registry, and tool bridge |
| Hooks | src/agent/hooks.ts | Cross-cutting before/after hook pipeline |
| Agent Loop | src/agent/loop.ts | Core turn cycle: Claude API + tool execution |
| Coordinator | src/agent/coordinator.ts | Multi-agent orchestration: worker + managed agents |
| Doctor | src/agent/doctor.ts | System health checks |
| Verification | src/agent/verify.ts | Invariant tests for agent safety properties |
| Boot | src/agent/boot.ts | 11-stage initialization pipeline |
| Utilities | src/agent/utils.ts | Shared pattern matching, formatting, helpers |
| Tool Definitions | src/tools/definitions.ts | 34 registered tool metadata definitions |
| Tool Implementations | src/tools/shell.ts | Shell command executors for tools |
| Memory Tools | src/tools/memory-tools.ts | memory-store / memory-recall implementations |
| Wiki Tools | src/tools/wiki-tools.ts | 5 wiki-* tool implementations (list, read, write, search, lint) |
| Worker Agent Tool | src/tools/worker-agent.ts | spawn-worker implementation |
| Managed Agent Tool | src/tools/managed-agent.ts | spawn-managed-agent implementation |
Agent Loop
The agent loop is the core engine. It handles user input, calls the Claude API, executes tools when requested, and manages the full turn lifecycle.
import { boot } from './src/agent/boot.js';
const runtime = await boot({ mode: 'interactive', registerTools: true });
// Single turn: user message in → assistant response out
const result = await runtime.loop.turn('List my VPS instances');
// result.response — the assistant's text
// result.toolsUsed — ['vps-list']
// result.stopReason — 'completed'
// result.usage — { input: 1539, output: 504, total: 2043 }
Turn Lifecycle
- Add user message to session
- Start streaming (
message_startevent) - Budget pre-check (halt if exhausted)
- Assemble tool pool from registry
- Call Claude API with tools
- For each tool_use in response:
- Permission check (deny list → pre-approved → read-only → destructive → trust tier → fallback)
- Execute tool implementation
- Return result to Claude
- Repeat tool loop up to
maxToolRoundstimes - Record usage, persist session
- Stop streaming (
message_stopevent with stop reason)
Multi-Round Tool Use
Claude can request multiple tools in sequence. The loop handles this automatically — each tool_use block triggers permission check + execution + result feedback, then Claude continues.
// The loop handles this internally:
// Claude: "I'll check the VPS status" → tool_use: vps-list
// Loop: permission check → execute just vps-list → return output
// Claude: "Here are your instances: ..."
Tool Registry
Every tool is defined as metadata before any implementation exists.
registry.register(
{
name: 'vps-list',
description: 'List all VPS instances',
source: 'built-in',
requiredPermissions: ['read'],
inputSchema: { type: 'object' },
sideEffectProfile: 'read-only',
tags: ['vps'],
},
async (input, ctx) => {
return { success: true, output: '...' };
},
);
// Query without executing
registry.listTools({ source: 'built-in' });
registry.listTools({ tags: ['vps'] });
registry.assemblePool(sessionContext, ['dangerous-*']);
Built-In Tools (31 registered)
| Category | Tools |
|---|---|
| VPS | vps-list, vps-add, vps-new, vps-provision, vps-destroy |
| Docs | docs-build, docs-deploy |
| Git/PR | pr-create, pr-merge |
| Quality | lint-sh, lint-md, lint |
| Agent | agent-test, agent-typecheck, agent-doctor, agent-verify |
| Memory | memory-store, memory-recall |
| Wiki | wiki-list, wiki-read, wiki-write, wiki-search, wiki-lint |
| Web | web-search |
| Skills | skill-coderabbit, skill-deploy, skill-new-doc, skill-status, skill-vps |
| Coordination | spawn-worker, spawn-managed-agent |
Tool implementations use shell executors that run just recipes or SSH commands:
import { justRecipe, sshCommand } from './src/tools/shell.js';
registry.register(vpsList, justRecipe('vps-list'));
registry.register(skillVps, sshCommand('openclaw-vps-1'));
Tool Definition Fields
| Field | Type | Description |
|---|---|---|
name | string | Unique identifier |
description | string | What the tool does |
source | 'built-in' | 'plugin' | 'skill' | Trust tier |
requiredPermissions | Permission[] | What permissions are needed |
inputSchema | JSONSchema | Input validation schema |
sideEffectProfile | 'read-only' | 'mutating' | 'destructive' | Risk classification |
tags | string[] | Grouping tags for filtering |
contextFilter | (ctx) => boolean | Optional — restricts availability by context |
Permission System
A pipeline of checks runs before any tool executes. Each check can grant, deny, or defer.
Pipeline Order
- Deny list — blocked patterns always deny first
- Pre-approved — known-safe patterns skip further checks
- Read-only auto-approve — read-only tools pass automatically
- Destructive detection — destructive tools denied in non-interactive modes
- SSH safety — destructive SSH tools require explicit override
- Trust tier — built-in auto-approved, skills restricted in worker mode
- Fallback — approval handler decides (default: deny)
Trust Tiers
| Tier | Trust | Behavior |
|---|---|---|
built-in | Highest | Always available, auto-approved |
plugin | Medium | Can be disabled, needs pipeline approval |
skill | Lowest | User-defined, denied in worker mode by default |
Runtime Command Safety
import { validateCommand, checkSshCommand, checkGitCommand } from './src/agent/permissions.js';
// validateCommand() combines both SSH and git checks in one call
validateCommand('rm -rf /'); // returns pattern source (blocked)
validateCommand('ls -la /var/www'); // returns null (safe)
validateCommand('git push --force'); // returns pattern source (blocked)
// Lower-level individual checks still available
checkSshCommand('rm -rf /'); // returns pattern source (blocked)
checkGitCommand('git push --force'); // returns pattern source (blocked)
UNSAFE_ARG_RE rejects arguments containing shell metacharacters including \n, \r, \t, #, and ~ in addition to the original set. resolveHost() rejects host values that start with - to prevent option injection.
Agent Type System
Constrained roles with allowed/denied tool patterns. Each type has a system prompt and output expectation.
import { AgentTypeRegistry } from './src/agent/agents.js';
const agents = new AgentTypeRegistry(); // loads 6 built-in types
// Get tool pool for a specific agent type
const pool = agents.assembleToolPool('explore', registry, ctx);
// explore: can read/lint/test, cannot edit/deploy/destroy
Built-In Agent Types
| Type | Role | Can Use | Cannot Use |
|---|---|---|---|
explore | Read-only search and analysis | lint-*, agent-test, vps-list | vps-destroy, pr-*, docs-deploy |
plan | Design without execution | lint-*, vps-list | vps-*, pr-*, docs-*, skill-* |
verification | Test and verify | lint-*, agent-* | vps-destroy, pr-merge, docs-deploy |
general-purpose | Full capability | * | — |
deploy | Build and publish | docs-*, agent-build, lint-* | vps-destroy, pr-merge |
coordinator | Multi-agent orchestration | * | — |
Coordinator (Multi-Agent)
The Coordinator enables Hollanov to delegate tasks to sub-agents. Two backends are supported:
- Worker agents — child
AgentLoopinstances running in the same process withworkerpermission mode - Managed agents — sessions on Anthropic's cloud infrastructure via the beta API
// Worker agent: runs on VPS with restricted tools
const result = await runtime.coordinator.spawnWorker({
agentType: 'explore', // determines tool pool
task: 'Search for all TODO comments in the codebase',
budgetTokens: 25_000, // carved from parent budget
});
// Managed agent: runs in Anthropic's cloud, fully isolated
runtime.coordinator.registerManagedAgent('research', {
agentId: 'agent-abc123', // from one-time setup
name: 'Research Agent',
model: 'claude-sonnet-4-6',
description: 'Web research',
});
const result = await runtime.coordinator.spawnManagedAgent({
agentType: 'research',
task: 'Research current best practices for TypeScript monorepos',
});
Worker vs Managed Decision
| Need | Backend |
|---|---|
| VPS tools (SSH, Hostinger, wiki, memory) | Worker agent |
| Self-contained + needs isolation | Managed agent |
| Self-contained + no isolation needed | Worker agent (cheaper) |
Worker Target Options
The spawn-worker tool accepts a target parameter:
| Target | Status | Where |
|---|---|---|
local | Implemented | Same Node.js process on the VPS |
sandbox | Future | Deno Sandbox (Firecracker microVM) |
<vps-alias> | Future | Remote VPS via SSH |
Memory System
Persistent memory with provenance tracking, relevance scoring, and age decay.
import { MemoryManager, SessionMemory } from './src/agent/memory.js';
// Persistent memory (disk-backed)
const memory = new MemoryManager();
await memory.loadAll();
await memory.store({
type: 'user', // user | feedback | project | reference
scope: 'personal', // personal | team | project
content: 'Ben prefers concise responses',
origin: 'user-stated', // user-stated | model-inferred | system-generated
description: 'Style preference',
tags: ['preferences'],
});
// Recall by relevance (accounts for age, access frequency, validation recency)
const results = memory.recall({ type: 'user', search: 'preferences', limit: 5 });
// Session memory (in-memory only, not persisted)
const sessionMem = new SessionMemory();
sessionMem.store({ content: 'User is debugging VPS', origin: 'model-inferred', description: 'topic' });
Relevance Scoring
- Age decay: half-life of 30 days
- Access boost: logarithmic boost from frequent access
- Validation penalty: memories not validated in 14+ days lose score
- Supersession penalty: superseded memories drop 0.5
Skills and Extensibility
Skills are self-contained modules with triggers, prompts, and tool requirements.
import { SkillRegistry } from './src/agent/skills.js';
const skills = new SkillRegistry();
// Load from .claude/skills/*/SKILL.md files
await skills.loadFromDirectory('.claude/skills');
// Bridge skills into the tool registry
skills.registerAsTools(toolRegistry);
// Find by trigger
const deploy = skills.findByTrigger('/deploy');
Skill Sources
| Source | Description |
|---|---|
bundled | Built-in skills, highest trust |
user | Loaded from SKILL.md files in a directory |
mcp | Auto-generated from MCP server capabilities |
Hooks Architecture
Cross-cutting before/after hooks on agent events. Hooks run in priority order and can transform data flowing through the pipeline.
import { HookPipeline } from './src/agent/hooks.js';
const hooks = new HookPipeline();
// Log every tool execution
hooks.after('tool:execute', (data) => {
console.log(`Tool ${data.tool.name}: ${data.result?.success}`);
});
// Transform input before tool runs
hooks.before('tool:execute', (data) => {
return { ...data, input: sanitize(data.input) };
});
// Wrap an operation with before + after
await hooks.wrap('tool:execute', data, ctx, async (d) => {
return await executeImpl(d);
});
Hook Events
tool:execute | permission:check | session:lifecycle | message:add | stream:event | turn:start | turn:end | boot:complete | compact:transcript
Session Persistence
Sessions capture everything needed to resume: messages, token usage, permission decisions, workflow state, and tool pool.
const sessions = new SessionManager();
const session = sessions.createSession({ maxTurns: 50 });
sessions.addMessage(session, { role: 'user', content: 'Deploy to production' });
await sessions.persist(session);
// Resume later
const resumed = await sessions.resume(session.id);
Workflow Manager
Models multi-step operations as explicit state machines with crash recovery.
const wm = new WorkflowManager();
const workflow = wm.createWorkflow('deploy', [
{ name: 'build', idempotencyKey: 'build-v1.2.3' },
{ name: 'test', idempotencyKey: 'test-v1.2.3' },
{ name: 'deploy', idempotencyKey: 'deploy-v1.2.3' },
]);
wm.start(workflow);
wm.completeStep(workflow, ['artifact-pushed'], { path: '/dist' });
// After crash: resume from last checkpoint
wm.resumeFromCheckpoint(workflow);
States: planned → executing → completed (with awaiting_approval, waiting_on_external, failed)
Budget Tracker
Pre-turn checks prevent overspending. Token reservations allow sub-agents to carve out capacity before running.
const decision = budget.check(estimatedTokens);
if (decision.decision === 'halt') {
// Stop with structured reason: 'max_budget_reached' or 'Token budget exhausted'
}
budget.record({ input: actualInput, output: actualOutput });
budget.recordTurn();
// Reservations — used by worker agents to carve out a token budget
budget.reserve(25_000); // reduce available capacity by 25K tokens
budget.release(25_000); // return unused reservation when worker finishes
Event Stream
Typed events that communicate system state in real time.
| Event | Fields | When |
|---|---|---|
message_start | sessionId, turn | Beginning of a turn |
tool_selected | name, reason | Agent picks a tool |
permission_check | tool, decision, reason | Permission pipeline runs |
message_delta | content | Text fragment streamed |
usage_update | usage | Token counts updated |
workflow_step | stepId, status | Workflow state change |
message_stop | reason, usage | Turn ends |
worker_spawned | childSessionId, agentType, task | Worker agent created |
worker_completed | childSessionId, success, tokensUsed | Worker agent finished |
managed_agent_spawned | sessionId, agentType, task | Managed agent session created |
managed_agent_completed | sessionId, success | Managed agent session finished |
Stop Reasons
completed | max_turns_reached | max_budget_reached | user_cancelled | error | timeout | permission_denied | workflow_blocked
Context Assembly
Provenance-aware: every fragment carries source, trust level, content type, and age.
const ctx = new ContextAssembler();
ctx.add('You are an infrastructure assistant', {
source: 'system',
trustLevel: 'verified',
contentType: 'instruction',
});
// Assemble within token budget, prioritized by type and trust
const fragments = ctx.assemble(4000);
Priority: instruction > evidence > summary > raw. Within each: verified > inferred > user-claimed > external.
Boot Sequence
11-stage initialization pipeline:
- Create core services (registry, permissions, logger, memory, skills, hooks, etc.)
- Run doctor diagnostics (optional)
- Initialize or resume session
- Create budget tracker from session config
- Load persistent memory
- Register tools and load skills
- Wire event stream to logger
- Apply agent type constraints
- Create agent loop
- Create coordinator and register coordination tools
- Return assembled
AgentRuntime
const runtime = await boot({
mode: 'interactive',
registerTools: true,
skillsDir: '.claude/skills',
runDoctorOnBoot: true,
loopConfig: { model: 'claude-sonnet-4-20250514' },
});
// runtime.loop.turn('Hello') — run a conversation turn
// runtime.registry — 34 tools registered
// runtime.memory — persistent memory loaded
// runtime.hooks — cross-cutting pipeline
System Logger
Structured JSONL logs at ~/.openclaw/logs/{date}.jsonl. Categories: init, registry, permission, execution, session, workflow, budget, coordination, error.
Audit Trail
Every permission decision is first-class data. Queryable by tool, decision, time range, and deciding check.
CLI Commands
| Command | Description |
|---|---|
just agent-run | Interactive agent REPL |
just agent-test | Run all agent tests |
just agent-test-watch | Run tests in watch mode |
just agent-typecheck | TypeScript type checking |
just agent-build | Compile to dist/ |
just agent-verify | Run 5 safety invariant checks |
just agent-doctor | System health diagnostics |
Testing
373 tests across 27 files using vitest:
tests/
registry.test.ts (19 tests) permissions.test.ts (34 tests)
session.test.ts (12 tests) workflow.test.ts (16 tests)
budget.test.ts (10 tests) stream.test.ts (14 tests)
logger.test.ts (10 tests) audit.test.ts (11 tests)
compaction.test.ts (6 tests) context.test.ts (10 tests)
agents.test.ts (7 tests) memory.test.ts (16 tests)
skills.test.ts (7 tests) hooks.test.ts (9 tests)
loop.test.ts (7 tests) tools.test.ts (5 tests)
boot.test.ts (2 tests) verify.test.ts (1 test)
cost.test.ts (9 tests) heartbeat.test.ts (27 tests)
wiki.test.ts (19 tests) coordinator.test.ts (9 tests)
Project Structure
src/
agent/
registry.ts permissions.ts session.ts
workflow.ts budget.ts stream.ts
logger.ts audit.ts compaction.ts
context.ts agents.ts memory.ts
skills.ts hooks.ts loop.ts
doctor.ts verify.ts boot.ts
cost.ts heartbeat.ts wiki.ts
coordinator.ts utils.ts
tools/
definitions.ts register.ts shell.ts
search.ts memory-tools.ts wiki-tools.ts
worker-agent.ts managed-agent.ts
cli/
agent.ts doctor.ts verify.ts
bot.ts telegram.ts whatsapp.ts
agents.ts cost.ts heartbeat.ts
memory.ts skills.ts wiki.ts
types/
index.ts
index.ts