Skip to main content

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

ModuleFilePurpose
Typessrc/types/index.tsAll shared type definitions
Tool Registrysrc/agent/registry.tsRegister, query, and resolve tools
Permission Systemsrc/agent/permissions.tsTrust tiers, safety pipeline, approval handlers
Session Managersrc/agent/session.tsCreate, persist, and resume sessions
Workflow Managersrc/agent/workflow.tsStateful multi-step workflows with idempotency
Budget Trackersrc/agent/budget.tsToken and turn budget with pre-check enforcement
Event Streamsrc/agent/stream.tsTyped streaming events with lifecycle management
System Loggersrc/agent/logger.tsStructured JSONL system event logging
Audit Trailsrc/agent/audit.tsPermission decision tracking and querying
Compactionsrc/agent/compaction.tsTranscript summarization to manage context size
Context Assemblersrc/agent/context.tsProvenance-aware context assembly with trust scoring
Agent Typessrc/agent/agents.tsConstrained agent roles with allowed/denied tools
Memorysrc/agent/memory.tsPersistent + session memory with provenance and relevance
Cost Trackersrc/agent/cost.tsClaude API cost accounting and reporting
Heartbeatsrc/agent/heartbeat.tsHealth status monitoring with ring-buffer history
Wikisrc/agent/wiki.tsLLM-maintained knowledge base with frontmatter-driven pages
Skillssrc/agent/skills.tsSkill loader, registry, and tool bridge
Hookssrc/agent/hooks.tsCross-cutting before/after hook pipeline
Agent Loopsrc/agent/loop.tsCore turn cycle: Claude API + tool execution
Coordinatorsrc/agent/coordinator.tsMulti-agent orchestration: worker + managed agents
Doctorsrc/agent/doctor.tsSystem health checks
Verificationsrc/agent/verify.tsInvariant tests for agent safety properties
Bootsrc/agent/boot.ts11-stage initialization pipeline
Utilitiessrc/agent/utils.tsShared pattern matching, formatting, helpers
Tool Definitionssrc/tools/definitions.ts34 registered tool metadata definitions
Tool Implementationssrc/tools/shell.tsShell command executors for tools
Memory Toolssrc/tools/memory-tools.tsmemory-store / memory-recall implementations
Wiki Toolssrc/tools/wiki-tools.ts5 wiki-* tool implementations (list, read, write, search, lint)
Worker Agent Toolsrc/tools/worker-agent.tsspawn-worker implementation
Managed Agent Toolsrc/tools/managed-agent.tsspawn-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

  1. Add user message to session
  2. Start streaming (message_start event)
  3. Budget pre-check (halt if exhausted)
  4. Assemble tool pool from registry
  5. Call Claude API with tools
  6. 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
  7. Repeat tool loop up to maxToolRounds times
  8. Record usage, persist session
  9. Stop streaming (message_stop event 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)

CategoryTools
VPSvps-list, vps-add, vps-new, vps-provision, vps-destroy
Docsdocs-build, docs-deploy
Git/PRpr-create, pr-merge
Qualitylint-sh, lint-md, lint
Agentagent-test, agent-typecheck, agent-doctor, agent-verify
Memorymemory-store, memory-recall
Wikiwiki-list, wiki-read, wiki-write, wiki-search, wiki-lint
Webweb-search
Skillsskill-coderabbit, skill-deploy, skill-new-doc, skill-status, skill-vps
Coordinationspawn-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

FieldTypeDescription
namestringUnique identifier
descriptionstringWhat the tool does
source'built-in' | 'plugin' | 'skill'Trust tier
requiredPermissionsPermission[]What permissions are needed
inputSchemaJSONSchemaInput validation schema
sideEffectProfile'read-only' | 'mutating' | 'destructive'Risk classification
tagsstring[]Grouping tags for filtering
contextFilter(ctx) => booleanOptional — restricts availability by context

Permission System

A pipeline of checks runs before any tool executes. Each check can grant, deny, or defer.

Pipeline Order

  1. Deny list — blocked patterns always deny first
  2. Pre-approved — known-safe patterns skip further checks
  3. Read-only auto-approve — read-only tools pass automatically
  4. Destructive detection — destructive tools denied in non-interactive modes
  5. SSH safety — destructive SSH tools require explicit override
  6. Trust tier — built-in auto-approved, skills restricted in worker mode
  7. Fallback — approval handler decides (default: deny)

Trust Tiers

TierTrustBehavior
built-inHighestAlways available, auto-approved
pluginMediumCan be disabled, needs pipeline approval
skillLowestUser-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

TypeRoleCan UseCannot Use
exploreRead-only search and analysislint-*, agent-test, vps-listvps-destroy, pr-*, docs-deploy
planDesign without executionlint-*, vps-listvps-*, pr-*, docs-*, skill-*
verificationTest and verifylint-*, agent-*vps-destroy, pr-merge, docs-deploy
general-purposeFull capability*
deployBuild and publishdocs-*, agent-build, lint-*vps-destroy, pr-merge
coordinatorMulti-agent orchestration*

Coordinator (Multi-Agent)

The Coordinator enables Hollanov to delegate tasks to sub-agents. Two backends are supported:

  • Worker agents — child AgentLoop instances running in the same process with worker permission 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

NeedBackend
VPS tools (SSH, Hostinger, wiki, memory)Worker agent
Self-contained + needs isolationManaged agent
Self-contained + no isolation neededWorker agent (cheaper)

Worker Target Options

The spawn-worker tool accepts a target parameter:

TargetStatusWhere
localImplementedSame Node.js process on the VPS
sandboxFutureDeno Sandbox (Firecracker microVM)
<vps-alias>FutureRemote 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

SourceDescription
bundledBuilt-in skills, highest trust
userLoaded from SKILL.md files in a directory
mcpAuto-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: plannedexecutingcompleted (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.

EventFieldsWhen
message_startsessionId, turnBeginning of a turn
tool_selectedname, reasonAgent picks a tool
permission_checktool, decision, reasonPermission pipeline runs
message_deltacontentText fragment streamed
usage_updateusageToken counts updated
workflow_stepstepId, statusWorkflow state change
message_stopreason, usageTurn ends
worker_spawnedchildSessionId, agentType, taskWorker agent created
worker_completedchildSessionId, success, tokensUsedWorker agent finished
managed_agent_spawnedsessionId, agentType, taskManaged agent session created
managed_agent_completedsessionId, successManaged 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:

  1. Create core services (registry, permissions, logger, memory, skills, hooks, etc.)
  2. Run doctor diagnostics (optional)
  3. Initialize or resume session
  4. Create budget tracker from session config
  5. Load persistent memory
  6. Register tools and load skills
  7. Wire event stream to logger
  8. Apply agent type constraints
  9. Create agent loop
  10. Create coordinator and register coordination tools
  11. 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

CommandDescription
just agent-runInteractive agent REPL
just agent-testRun all agent tests
just agent-test-watchRun tests in watch mode
just agent-typecheckTypeScript type checking
just agent-buildCompile to dist/
just agent-verifyRun 5 safety invariant checks
just agent-doctorSystem 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