Skip to content

Agent Architecture

Current execution reference. For design goals see DESIGN.md; for proposed changes see the consolidation plan. Historical rebuild plans record earlier decisions and may describe superseded paths.

Executable capabilities with functions and permissions. Each tool module provides one or more functions that agents can call.

Examples: shell (run, env), filesystem (read, write), git (commit, push), browser (navigate, click), docker (run_container), github (create_issue), google-workspace (send_email), websearch (search)

Location: src/tools/ — each extends BaseTool, registered in ToolRegistry

Domain knowledge sets that provide expertise to agents via system prompt injection. Skills contain principles, best practices, anti-patterns, and relevant frameworks.

Examples: software-architecture, test-automation, security-practices, financial-analysis, ai-engineering, design-principles

Location: DB table skills, seeded from src/db/seed-skills.ts, managed via SkillRegistry and /api/skills CRUD

Experts were pre-configured personas pairing a role with tools, skills, a prompt, critical rules, a deliverable template and success metrics. They are gone: every one of them named a role that already carried the same tools, so the expert row was a second, database-resident copy of the role — and a seed that had not run produced a specialist with no rules while nothing said so.

Where each part went:

Was on the expertLives now
role + toolsthe role itself (roles/<name>/config.ts, or a row on the Topics page)
critical rulesRoleMeta.criticalRules, appended to the role prompt
skillsskill↔role assignments (skill_topic_assignments, keyed by role name)
deliverable templatethe role’s prompt.md OUTPUT section
model preferencethe LANE the role resolves to (defaultTopic → canonicalTopic)

Runtime instances that execute tasks using an LLM tool loop. Each agent has a context (session, user, model, role) and iterates: call LLM → parse tool calls → execute tools → repeat.

Location: src/core/agent-worker.ts, managed by AgentManager

Depth-0 root of the swarm, and the agent the user is talking to. It runs as an ordinary role — general (ROOT_ROLE) — with the general toolset plus the spawn_child / collect_children / create_pipeline meta-tools, so it answers with its own tools and delegates sub-topics to specialist Agents when one is genuinely needed. Owns the final user-facing reply.

Until Phase 9 of the rebuild plan this was a dedicated root agent role holding meta-tools and profiles and nothing else, reached through a keyword classifier that decided per message whether to run it at all. That design added a routing hop and restricted direct tool work. Both the classifier branch and the tool-less role are gone; AgentContext.root is what identifies the root now, since the role string no longer does.

Location: src/core/agent/service.ts and root-runner.ts, role prompt at src/core/agent/roles/general/prompt.md, delegation mechanics at src/core/agent/delegation-prompt.md

Message Classification (Two-Layer Architecture)

Section titled “Message Classification (Two-Layer Architecture)”

The root agent uses two layers of classification with different vocabularies:

Layer A — src/core/router.ts (legacy, 15 topics): coding, research, architecture, chat, embedding, design, devops, security, data, ai, qa, finance, automation, pm, writing (+ ‘general’ fallback).

Layer B — src/core/agent/classifier.ts (live, 14 categories): coding, research, devops, security, review, qa, data, writing, architecture, design, finance, communication, automation, general.

Layer B is the active classification path. What it is NOT any more is a routing decision: it does not choose the specialist (Phase 2) and it does not choose whether an agent runs at all (Phase 9). It scopes memory retrieval, and in the lite tier only it reaches the model as a hint the request can override. TOPIC_TO_ROLE_ALIAS (swarm-tool.ts) still catches natural-language synonyms the model may use in spawn_child (e.g. development→coding, database→data). Divergences from Layer A: Layer B has distinct ‘communication’ category and lacks ‘chat’/‘embedding’/‘ai’/‘pm’ (handled via LLM fallback).

Delegation runs through a fixed 3-level tree: Root agent → Agent → Subagent. Depth is structural, not configurable.

KindDepthSpawned byCan spawn?Lifetime
Root agent0AgentService.handleMessage()Yes → AgentsPer session
Agent1Parent’s spawn_childYes → SubagentsEphemeral, per topic
Subagent2Parent’s spawn_childNo (leaf)Ephemeral, per subtopic

The LLM-facing tool is spawn_child. Agents also get the lane escalation tool (one use per Agent lifetime) to retry a task on a different configured lane when children hit budget or timeout. It no longer selects a database expert.

Location: src/core/swarm/ — spawner.ts, call-graph.ts, types.ts, errors.ts, escalate-tool.ts, swarm-tool.ts, orphan-reaper.ts, fan-out-budget.ts, node-repository.ts. DB table swarm_nodes. The full design lives in .octipus/swarm-design.md.

A per-user identity layer the root agent inherits at every turn — name, pronouns, tone, narration volume, free-form self-facts. Default is Octipus (the octopus-machine). Layered between SECURITY_PREAMBLE and the role prompt via the before-agent-start hook:

SECURITY_PREAMBLE (DESIGN.md rule #6 — untouched)
│
▼
PERSONA BLOCK (from personas/<preset>.yaml + per-user overrides)
│
▼
ROLE PROMPT (roles/general/prompt.md + delegation-prompt.md — untouched)
│
▼
memory block, recent history, classifier hint, …

The same persona applies to the two remaining non-agentic surfaces that also speak to the user — the voice propose-then-confirm turn and chat.interject — both of which still run directResponse, so those replies sound like Octipus too. Live swarm events (swarm.node_spawned, node_completed, budget_warning) get re-emitted as swarm.narration carrying persona-rendered text (“Octipus dispatches a research arm.”, “qa arm failed. Predictable.”) — channels subscribe independently.

Specialist children don’t inherit the persona — they stay role-defined. Persona is host-level only.

Six presets ship under personas/: octipus (default), terse-engineer, mentor, nautilus, concierge, verbose-academic. The category='assistant' row in the profiles table stores the user’s overrides (PROFILES.md). Edit via /persona ... slash commands or the web /persona page.

User Message
│
▼
Root agent (depth 0)
│
└─ Every message ──► the root agent runs: general tools + meta-tools
│
├─ spawn_child(role, topic, subtopic, taskBrief)
│ ──► Agent (depth 1) with the named role,
│ topic-bound model, intersected tools
│ │
│ └─ spawn_child / lane escalation
│ ──► Subagent (depth 2), hard leaf
│
├─ Multiple spawn_child calls with same parallelGroup
│ ──► Promise.all fan-out (cap 4/turn)
│
└─ create_pipeline(type) ──► Sequential stages
Stage 1 → Stage 2 → ... → Stage N
  1. spawn_child (single) — default for single-role delegation with structured output.
  2. spawn_child (multiple, parallel or sequential) — when the task has distinct sub-topics.
  3. create_pipeline — a root-only staged workflow for work needing explicit implementation, testing, review, and QA steps. Development templates can retry failed QA within configured bounds. The current delegation prompt prefers pipelines for development tasks with runnable checks. That is prompt guidance; a pipeline does not itself prove correctness. A single delegated task can also attach independent scorer checks. Pipelines can be started through POST /api/pipelines.

The legacy spawn_worker and spawn_team meta-tools are removed from the LLM-visible tool surface. The worker-spawner.ts internals still back pipeline stages (sequential handover, non-LLM) but the LLM no longer sees either primitive.

Every node has a hard budget envelope enforced pre-LLM-call inside AgentWorker.loop(). Breach throws a structured error (BudgetExceededError, ChildTimeoutError, CascadedCancellationError) which the spawner maps to ChildResult.status.

LevelTokens (cap)Wall-clock (cap)Fan-out (cap)
Root agent (0)200k10 min6
Agent (1)80k10 min4
Subagent (2)30k10 min0
  • Tokens cascade (pool-shared): child.tokens.cap = min(LEVEL_DEFAULT[depth], parent.remaining.tokens − 10% RESERVE).
  • Wall-clock does NOT cascade: each node gets its own LEVEL_DEFAULT wall cap. Parent’s clock excludes time spent awaiting children via AgentWorker.pausedMs.
  • Fan-out: per-node cap enforced synchronously by the spawner before concurrency/cache/budget math. Per-turn parallel cap (4) is a secondary guard in tool-executor.ts; overflow returns concurrency_limit.

Live defaults are configurable under swarm.levelDefaults; src/core/swarm/types.ts supplies fallback LEVEL_DEFAULT values and BUDGET_RESERVE_FRACTION. Budget checks do not guarantee exact billing or immediate cancellation of external work.

SwarmCallGraph per-root-session keeps a fingerprint set of (topicPath, normalized(taskBrief), inputArtifact refs). Duplicate spawns within a session return cancelled with a parentNotice so the parent LLM can synthesize against the in-flight result. Ancestor-chain collisions are rejected as a second guard. Fingerprints are released on child failure so the parent may respawn with a refined brief.

Parent abort propagates to children via an AbortSignal chain rooted on the Root agent. AgentManager.stop(id, { cascade: true }) walks the in-memory childrenByParent index synchronously and fires a background DB walk so zombie descendants flip to cancelled in swarm_nodes.

child.allowedToolIds = parent.allowedToolIds ∩ requiredToolIds. The root Root agent’s allowedToolIds is the union of all role toolIds — so children inherit their role’s full toolbox via intersection rather than an empty set. Children that need a tool the parent lacks escalate via request_user_approval.

Gateway events (src/core/gateway/protocol.ts): swarm.node_spawned, swarm.node_completed, swarm.budget_warning, swarm.call_graph_cycle_blocked. The authoritative list, with each type’s publishers and subscribers, is generated: architecture catalog. UI subscribes with the swarm.* pattern and renders a live tree per session. The replay buffer keeps swarm events for reconnection; /api/swarm/nodes?rootSessionId=… is the REST fallback for tree rehydration after the buffer ages out.

Three additions make delegation auditable, verifiable, and resumable: receipts (a deterministic side-effect audit of what each child’s tool calls actually did, on ChildResult.receipt), scorer gates (deterministic checks a parent attaches to spawn_child to verify the deliverable — a failure yields the first-class contract_failed status), and the swarm ledger (an append-only history of node transitions that lets a crash-interrupted swarm be replayed and reconciled on boot). Full reference: SWARM-RELIABILITY.md.

Pipeline stages can be typed as qa_validation. When a QA stage fails, the pipeline automatically retries the previous implementation stage with the QA feedback injected into context. This loop continues up to 3 retries (configurable via maxRetries on the pipeline step). After max retries are exhausted, the pipeline escalates to the user for manual approval.

Pipeline step fields for QA retry:

FieldTypePurpose
stageType"implementation" | "qa_validation" | "review"Classifies the stage for retry logic
maxRetriesnumberMaximum retry attempts before escalation (default: 3)
retryTargetStagestringWhich stage to retry on failure (typically the preceding implementation stage)

Acceptance criteria per plan item. The planning stage can give each plan item an acceptance list (plan__add_items), which is one checkable statement per entry. The implementer sees the list with the item. The QA stage has to return a criteria array in its verdict, marking each criterion met true or false with evidence. The audit gate then applies three rules:

  • A pass that leaves a criterion unreported, or marks one met with no evidence, is sent back to the auditor.
  • A pass that reports a criterion as not met is turned into a real failure and goes back to the builder.
  • Items without criteria are judged exactly as before.

Best attempt on exhaustion. Each settled QA verdict for an item is kept, with the workspace HEAD it was judged at. When retries run out and an earlier attempt did better than the last one (it passed, met more criteria, or had fewer issues), the escalation names that attempt and its commit. Nothing is rolled back automatically; that is the person’s decision.

When work passes between pipeline stages, a structured handoff document is automatically generated and forwarded to the next stage’s agent. Each handoff contains:

  • Completed work summary — what the previous stage accomplished
  • Key decisions — architectural or implementation choices made
  • Open questions — unresolved issues for the next stage to address
  • Artifacts produced — files created/modified, endpoints added, etc.
  • Role-aware instructions — context tailored to the receiving agent’s role

The full handoff chain is accumulated across all stages, so later stages have visibility into the entire pipeline history. This prevents context loss and reduces redundant work across sequential stages.

When the root agent (or an Agent) calls spawn_child, the named role supplies the system prompt, critical rules, tool allowlist and default lane. Skills are discovered from the role/topic assignment and injected as an index that the child can load on demand. There is no runtime database expert lookup or expert model preference in this path; a caller may name a model lane explicitly with topic, while the lane binding supplies the model.

Children resolve their model strictly (no parent-model inheritance, no hardcoded defaults):

  1. A configured lane executor, only when the parent supplied a plan.
  2. resolveModel({ topic: lane }) — the requester’s personal binding, then the install binding.
  3. If a tool-equipped child is bound to a model without tool support, try an eligible local Ollama tool-capable model.
  4. Fail loud — if no model resolves, the spawner throws with a message pointing the user at the Topics page. No default-model fallback for workers.

Children inherit topic bindings, not the parent’s model. If a research Agent spawns a security Subagent, the Subagent resolves the model bound to security, not the parent’s research model. ModelRegistry.getModelForTopic() is the single authoritative entry point. litellm-client.ts:embed() and visual/analyzer.ts resolve embedding/vision models the same way (or throw if unbound). Default fallback applies only to the root agent via selectForOrchestration().

ConceptWhat it isWhen createdLifetime
ToolExecutable module (shell, git…)App startupSingleton
SkillDomain knowledgeApp startupSingleton
RoleAgent configuration (prompt, rules, tools and default lane)Code or user-created rowPersistent
AgentRunning worker instance (depth 1)Per spawn_childRequest-scoped, tracked in swarm_nodes
SubagentLeaf worker (depth 2)Per spawn_child from an AgentRequest-scoped
SwarmFull tree for a sessionPer root messageSession-scoped, persisted in swarm_nodes
PipelineSequential stage chainPer-requestDB-tracked (pipelines table)
RoleToolsDefault SkillsUse Case
codingfilesystem, shell, git, github, knowledge, task_state, repo_registry, mcparchitecture, data-structures, db-design, api-designCode implementation
reviewfilesystem, shell, git, knowledge, task_state, visualarchitecture, testing, security, performanceCode review
researchwebsearch, knowledge, filesystem, profiles, artifacts, artifacts_toolbox, documents, task_state, repo_registry, skill-distill, mcptechnical-writingInvestigation
designbrowser, filesystemdesign-principles, design-frameworksUI/UX
devopsshell, docker, git, filesystem, mcpdevops, containers, cloud, networkingInfrastructure
securityshell, filesystem, browser, browser-ext, websearch, knowledge, task_state, mcpsecurity, networking, cloudSecurity analysis
datadata, shell, filesystem, knowledge, task_state, artifacts, artifacts_toolbox, documents, mcpdb-design, data-engineering, performanceData/DB work
aishell, filesystem, browser, browser-ext, websearch, knowledge, task_state, mcpai-engineering, ML, data-structuresAI/ML tasks
qabrowser, browser-ext, shell, docker, filesystem, knowledge, task_state, visual, artifacts, artifacts_toolboxtest-automation, performanceTesting
financebrowser, websearch, filesystemfinancial-analysisFinancial work
automationshell, docker, filesystem, scheduling, mcpautomation-patterns, devopsWorkflows
pmfilesystem, messaging, tasks, knowledge, github, atlassian, skill-distillproject-management, technical-writingProject mgmt
writingfilesystem, browser, websearch, knowledge, task_state, messaging, documentstechnical-writing, api-designDocumentation
communicationgoogle-workspace, microsoft365, messaging, scheduling, profiles, notes, email-processor, voice—Email/calendar
architecturefilesystem, shell, knowledge, task_state, websearch, mcp—System design
generalfilesystem, shell, browser-ext, websearch, messaging, knowledge, notes, tasks, task_state, scheduling, profiles, email-processor, artifacts, artifacts_toolbox, documents, skill-distill, mcp—Fallback

The root agent can also get tools no role lists: inline-ui (interactive answers) is added by root-runner when the client that sent the turn can render it, or the conversation is a room — see Interactive answers.

Some models (Qwen3, DeepSeek) emit <think>...</think> reasoning blocks that consume output tokens. The system handles this at multiple levels:

  • Model-level: “Disable Thinking” checkbox in the model Add/Edit dialog sets extraBody: { think: false } — prevents the model from generating reasoning tokens entirely (Ollama)
  • Agent workers (root agent and role workers): Strip think:false from extraBody so the model can reason before emitting tool calls. Empirically (2026-05-12 QA), Ollama with think:false produces malformed tool-call JSON that the Go-side parser rejects (“Value looks like object, but can’t find closing ’}’”); with thinking ON, the same models emit valid tool calls. The override applies to every role that uses tools.
  • LLM client safety net: <think> blocks are stripped from both sync and streaming responses before delivery, to reduce exposure of raw reasoning markup; provider formats can vary

Strategy: Keep thinking enabled for any agent that emits tool calls (root agent and role workers) — the cost in tokens is much smaller than the cost of a failed tool call + retry storm. Disable thinking only for the toolless surfaces (direct-response.ts — voice propose, chat.interject), where there are no tool calls to corrupt.

Steering messages allow users to inject corrections or guidance into a running agent session without interrupting the current tool execution.

  1. Client sends { type: 'steer', sessionId, content } over the WebSocket gateway
  2. The message is queued on the session
  3. After the current tool call completes, the queue is drained
  4. Steering content is injected as a system-level message before the next LLM call
  • Redirect an agent that is going down the wrong path (“focus on the API layer, not the UI”)
  • Add constraints mid-run (“don’t modify any test files”)
  • Provide clarification the agent needs without aborting and restarting

Steering messages are non-disruptive — they never interrupt a tool that is already executing. The agent sees them as additional context on its next LLM turn.

When a conversation grows large, the system compacts it using LLM-based summarization rather than simple message truncation.

  • An LLM call summarizes the conversation history into a condensed form
  • File operation metadata is preserved — which files were read, written, created, or deleted — so the agent retains awareness of filesystem state even after compaction
  • Tool call/result pairs are condensed but their essential outcomes are kept
  • The compacted summary replaces older messages while recent messages remain intact

This preserves more useful context per token than naive truncation, especially for long coding sessions where file state matters.

Concurrent agents (teams, parallel pipeline stages) may attempt to write to the same file simultaneously, causing race conditions or corrupted output.

  • A per-file write queue serializes all mutation operations (write, append, patch) targeting the same file path
  • Read operations are not queued — only writes acquire the lock
  • Each file path gets its own independent queue, so writes to different files proceed in parallel
  • The queue is transparent to tool implementations — serialization is handled at the tool executor level

Reasoning models (Qwen3, DeepSeek, o-series) benefit from thinking tokens but consume output capacity. The system auto-manages thinking budgets.

LevelThinking BudgetUse Case
off0 tokensCasual chat
low~1024 tokensSimple tool calls, classification
medium~4096 tokensMulti-step tool reasoning
high~8192+ tokensComplex planning, code generation
  • Auto-detection: The system detects reasoning-capable models and assigns a default budget based on the agent’s role
  • Scaling: Budget scales with task complexity — simple turns get off or low, agent workers get medium or high
  • Agent worker override: Workers always enable thinking even if the model config disables it, since complex tool-use benefits from reasoning
  • Thinking tokens are stripped from output before delivery to users

Tools auto-discover via discovery.ts from src/tools/ folders. No manual registration needed; each tool extends BaseTool with a unique toolId.

Create via the API (POST /api/skills) or add to SYSTEM_SKILLS in src/db/seed-skills.ts for system skills.

From the UI: the Topics page lists roles under the lane they run on — add one there with a name, a one-line description, a prompt and a tool list. It is stored in roles with is_system = false and joins the registry at boot (loadRolesFromDb); the create call also registers it immediately, so it is spawnable without a restart.

In code, for a role that ships with Octipus:

  1. Create folder src/core/agent/roles/<name>/
  2. Add config.ts with RoleMeta (role, description, toolIds, defaultTopic)
  3. Add prompt.md (role system prompt)
  4. Add the three static imports in roles/index.ts — a folder scan breaks in the bundle

Either way the role is CHOSEN by an agent naming it in spawn_child, off the one-line description rendered into the delegation menu. Nothing selects a role out of a lane; the arrow runs role → lane, via defaultTopic → canonicalTopic.

Agent outputs are automatically indexed into the knowledge base on completion, enabling future agents to retrieve past work. Most specialist roles have access to the knowledge tool for search and manual indexing.

See RAG Documentation for full details on setup, configuration, and architecture.

See Execution reliability audit for cancellation, shutdown, approval and restart-reconciliation checks, their regression tests, and remaining limits around external side effects and shared-database recovery.


Edit this page in the repository.