# OpenCode → Omnius: Agentic Loop & Sub-Agent Delegation Comparison

> Exhaustive comparison between `https://github.com/anomalyco/opencode/tree/dev` and
> `packages/orchestrator/src/agenticRunner.ts` (omnius).
> Generated 2026-06-10.

---

## 1. Main Agent Loop Structure

### OpenCode — `src/session/prompt.ts:1190-1390`

- **`while(true)`** loop with **natural exit** conditions — breaks when `lastAssistant.finish` is not `"tool-calls"` AND no pending tool calls
- Each iteration: `filterCompacted` → `latest` (find last user/assistant/finished/tasks) → resolve tools → call LLM → decide next step
- No fixed turn cap — bounded by agent `steps` config (default varies by agent)
- Result triage: `"stop"`, `"compact"`, or `"continue"` — compaction is an **inline loop control** decision

### Omnius — `orchestrator/src/agenticRunner.ts:9311`

- **`for (let turn = 0; turn < turnCap; turn++)`** with explicit turn cap (default 60)
- Much more complex per-turn preamble: REG-35 DoVer checkpoints, REG-46 world-state, REG-58/60/61 stagnation, REG-44 stuck, REG-50 write-thrash, REG-53 edit-fail-thrash, REG-18 stagnation window, etc.
- Exit only via `task_complete` tool with 3 identical handler implementations (streaming/unhandled/batch paths)
- No natural "model ran out of things to do" exit — model must explicitly call `task_complete`

### Learnings

- **Replace fixed `for` loop with natural-exit `while(true)`** — let the model exhaust its intent naturally. Keep turn cap as a safety fuse only (e.g., `maxTurns`). The 3× duplicated `task_complete` handler is a code-smell; unify into one path.
- **Adopt OpenCode's `finish` reason pattern** — use the LLM's `stop_reason` / `finish_reason` to detect natural completion instead of requiring an explicit `task_complete` tool call for every exit.

---

## 2. Sub-Agent Delegation

### OpenCode — `src/tool/task.ts`

- **`sessions.create({ parentID, title, agent, permission })`** — creates a **persisted child session** in the DB with full lifecycle
- **Permission derivation** (`src/agent/subagent-permissions.ts:1-46`): forwards parent agent `edit` denies, parent session denies, defaults-deny `todowrite`/`task` unless subagent explicitly permits
- **Background mode** (`task.ts:background.start()`): returns immediately, result injected as synthetic message
- **`task_id` resumption**: passing a prior `task_id` continues the same subagent session with accumulated context
- **Agent types are declarative** (`src/agent/agent.ts`): `mode: "subagent" | "primary" | "all"` — subagents can only be spawned via `task` tool, never selected by user
- **Concurrency hint in prompt** (`src/tool/task.txt`): "Launch multiple agents concurrently whenever possible"

### Omnius — `execution/src/tools/agent-tool.ts`

- **Three delegation mechanisms**:
  1. `agent-tool.ts:329` — **worktree isolation**: spawns subprocess via `spawnSubprocess`
  2. `agent-tool.ts:351` — **background in-process**: returns task ID, spawns in-process agent
  3. `agent-tool.ts:390` — **foreground in-process**: blocks until complete
- **No persisted child session** — sub-agents share the parent session's message array
- **No permission derivation** — sub-agents get same permissions as parent (except optional `toolNames` filter)
- **No `task_id` resumption** — each spawn is fresh context
- **Also has legacy**: `full-sub-agent.ts` (separate OS process), `sub_agent` (routed through agent-tool.ts now)
- **Coordinator pattern** (`orchestrator/src/coordinator.ts:106`): `CoordinatorManager` enforces `maxConcurrentWorkers` (5) and `maxTotalWorkers` (20), but limited to coordinator mode

### Learnings

- **Implement child sessions with permission derivation** — this is the single biggest gap. Sub-agents currently unbounded — can do anything parent can. No state isolation.
- **Add `task_id` resumption** — deduplicate sub-agent invocations by allowing the model to continue a prior sub-agent's context. This avoids redundant exploration and enables iterative refinement.
- **Adopt declarative agent types with `mode`** — replace the programmatic `agent-types.ts` registry with a schema-driven system where `mode: "subagent"` agents can only be called via the `agent` tool (not activated directly by user), matching OpenCode's separation of concerns.
- **Permission forwarding**: parent agent's `edit`-deny rules (e.g., plan mode) must cascade to sub-agents automatically.

---

## 3. Tool Batching / Parallel Execution

### OpenCode — `src/session/processor.ts`

- **No explicit batching** — relies on LLM provider's native ability to emit multiple `tool-call` events in one stream
- **AI SDK `streamText()`** (`src/session/llm/ai-sdk.ts`) executes tools concurrently internally
- **Per-tool `Deferred<void>`** (`processor.ts:ensureToolCall`) — each tool call gets a deferred that resolves when result arrives
- **Cleanup**: `Effect.forEach(Object.values(ctx.toolcalls), ..., { concurrency: "unbounded" })` with 250ms timeout per call
- **Ordering**: events arrive in temporal order from the LLM stream; results are interleaved arbitrarily

### Omnius — `orchestrator/src/tool-batching.ts`

- **Explicit batching**: `partitionToolCalls()` groups concurrent-safe tools into parallel batches, serial tools each get single-item batches
- **`executeBatch()` with worker pool** (`tool-batching.ts:212-233`): `withConcurrencyLimit(fns, limit=8)` — N concurrent workers
- **Dual dispatch paths** (`agenticRunner.ts:15281-15705`):
  - Streaming: `StreamingToolExecutor` with `queue → finalize → waitAll → drainCompleted` + ordering guarantees
  - Non-streaming: `partitionToolCalls → executeBatch` with REG-24 fingerprint dedup
- **Streaming executor state machine** (`streaming-executor.ts:292`): `canExecute()` checks — concurrent-safe tools run in parallel, exclusive tools run alone (ordering: stops at first exclusive)
- **Duplicate detection** (`streaming-executor.ts:327-393`): `entryFingerprint()` / `findPriorEquivalent()` / `mirrorPriorEquivalent()` — identical tool calls within same stream share results

### Learnings

- **Simplify to single dispatch path** — the streaming/non-streaming bifurcation leads to code duplication (3 `task_complete` handlers, parallel dispatch logic). OpenCode's unified event-stream model avoids this entirely.
- **Adopt event-based tool tracking** — replace the `rawToolCalls` array + result-mapping with a per-call `Deferred`/`Promise` map. This eliminates the need for `drainCompleted()` ordering logic and the dual-path complexity.
- **OpenCode's simplicity is instructive** — it doesn't need `partitionToolCalls` or concurrency-safe classifications because the AI SDK handles it. Consider delegating concurrency to the backend layer.

---

## 4. Session / Runner State Management

### OpenCode — Three-Layer Architecture

1. **`Runner`** (`src/effect/runner.ts:1-220`): Per-session state machine with 4 states — `Idle`, `Running`, `Shell`, `ShellThenRun`. Run and shell are **mutually exclusive**. Shell has priority; runs queue as `ShellThenRun` and auto-start when shell finishes.
2. **`RunCoordinator`** (`packages/core/src/session/run-coordinator.ts:1-200`): Per-key drain coordination with coalescing. At most **1 active + 1 pending** per session. `run` (explicit) dominates `wake` (advisory). Interrupts suppress stale wakes.
3. **`SessionPrompt.runLoop`** (`src/session/prompt.ts`): High-level loop orchestration. `ensureRunning()` guarantees at most one loop iteration per session.

### Omnius — `orchestrator/src/agenticRunner.ts:1583-1600`

- **Single-class monolith**: `AgenticRunner` class with 200+ private properties, all in one file (~25,159 lines)
- **No state machine** for run/shell concurrency — the `run()` method is called directly
- **No drain coordination** — multiple calls to `run()` would stack, not coalesce
- **No explicit interruption model** — abort via `options.abortSignal` at `agenticRunner.ts:9330` (passthrough, not session-aware)

### Learnings

- **Decompose `AgenticRunner`** — split into layers: Runner (state machine), Orchestrator (loop control), Coordinator (drain + interruption). The monolith approach makes reasoning about concurrency impossible.
- **Add Run/Shell mutual exclusion** — when a shell command is running, agent loop should defer; when loop is running, shell should either fail or queue. OpenCode's `ShellThenRun` pattern is correct.
- **Add drain coalescing** — if the agent completes a turn and the session has new work, coalesce into one chain instead of stacking multiple `run()` calls.

---

## 5. Tool Definition & Registration

### OpenCode — `packages/core/src/tool/tool.ts`

- **`Tool.make(config)`**: Zod-schema-validated input/output, `toModelOutput` mapping, opaque branded `Definition<Input, Output>` type
- **`definition(name)` + `settle(call, context)`** stored in `WeakMap<AnyTool, Runtime>` — decoupled from AI SDK format
- **`withPermission(tool, name)`** — decorator pattern, produces a new tool object with an attached permission string
- **`ToolRegistry.materialize()`** (`packages/core/src/tool/registry.ts`): resolves full tool definitions for a given permission ruleset
- **`SessionTools.resolve()`** (`packages/opencode/src/session/tools.ts`): wraps tools as AI SDK `tool()` objects with permission checking, plugin hooks, truncation

### Omnius — `execution/src/index.ts`

- **`AgenticTool` interface**: `{ name, description, execute(args): Promise<string>, parameters?, isConcurrencySafe?, isReadOnly?, executeStream? }`
- **Flat catalog registration** — all tools exported from a single massive `index.ts` (~100+ tools)
- **No schema validation** — `execute` receives `Record<string, unknown>`, manual parsing inside each tool
- **No permission system** — tools are either available or not, no granular `allow`/`deny`/`ask` rules
- **No AI SDK compatibility layer** — tool definitions are constructed manually for each backend format

### Learnings

- **Add schema-validated tool definitions** — Zod schemas for input/output give free validation, documentation generation, and type safety. The current `Record<string, unknown>` pattern is error-prone.
- **Add a permission decorator system** — `withPermission` pattern allows reusable, composable permission rules on any tool without modifying the tool itself. This cascades naturally to sub-agent permissions.
- **Decouple tool definition from backend format** — like OpenCode's `WeakMap<AnyTool, Runtime>`, have a canonical internal format and adapters per backend (OpenAI, vLLM, Ollama).

---

## 6. Context Compaction / Overflow

### OpenCode — `src/session/prompt.ts` + `compaction` agent

- **Inline overflow detection**: `runLoop` checks `lastFinished` context size against budget; if exceeded, `compaction.create()` and `continue`
- **Dedicated compaction agent** (`src/agent/agent.ts`): `compaction` — hidden primary agent with all tools denied, runs `PROMPT_COMPACTION` via LLM
- **Message filtering**: `MessageV2.filterCompactedEffect(sessionID)` — compacted messages are filtered out before each loop iteration
- **Compact or break**: compaction returns `"stop"` or `"continue"` — if context is too tight even after compaction, the loop terminates naturally

### Omnius — `orchestrator/src/context-compressor.ts`

- **Two-phase compression**:
  1. Phase 1 (cheap): prune old tool results (no LLM call)
  2. Phase 2 (expensive): LLM-generated structured summaries with sections (Goal, Constraints, Progress, Key Decisions, Relevant Files, Next Steps, Critical Context)
- **`DefaultContextEngine`** (`orchestrator/src/contextEngine.ts`): simple budget-based pruning — drops oldest non-evidence messages
- **Context assembly** (`agenticRunner.ts:assembleContext()`): sections `c_instr, c_state, c_identity, c_know, c_retrieval, c_graph, c_plan, c_todos, c_lessons, c_workboard`
- **No compaction agent** — compression is performed in-process, not delegated to a dedicated agent call

### Learnings

- **Consider a dedicated compaction agent** — OpenCode's approach of delegating context compression to a separate LLM call with zero-tool agent avoids polluting the main agent's context with compression overhead and allows the compression to be a real summarization pass rather than a pruning pass.
- **Add post-compaction exit** — when compression has been applied but token budget is still exceeded, the loop should terminate gracefully (signal "context too tightly coupled to summarize") instead of entering a compaction loop.
- **Pre-compute budget before LLM call** — detect overflow before calling the LLM, not after. OpenCode does this in `runLoop` via the `overflow` task check.

---

## 7. Model Calling / Streaming

### OpenCode — `src/session/llm.ts`

- **Dual runtime architecture**: Native (`@opencode-ai/llm`) or AI SDK (`streamText`) fallback
- **Unified `LLMEvent` stream**: normalized event types — `text-start/delta/end`, `reasoning-start/delta/end`, `tool-input-start/delta/end`, `tool-call`, `tool-result`, `tool-error`, `step-start/finish`, `finish`
- **Tool call streaming**: `tool-input-start/delta/end` events let the processor track partial tool arguments before the call is finalized
- **`createStructuredOutputTool()`** (`prompt.ts`): injects a json_schema tool via `tool_choice: "required"` for structured outputs

### Omnius — `backend-vllm/src/VllmBackend.ts`, `OllamaBackend.ts`

- **Simple `chatCompletion({ messages, tools })`** — standard OpenAI-compatible API
- **Streaming path** (`agenticRunner.ts:23035-23392`): custom SSE accumulation via `streamingRequest()` — manually accumulates `tool_call_delta` chunks, repairs JSON, manages multi-tool ordering
- **No normalized event stream** — tool parsing is done after the entire response is received (non-streaming) or via ad-hoc accumulator logic (streaming)
- **No tool-call streaming** — partial tool arguments are not exposed to the executor (cannot start executing before args are fully received, unlike OpenCode's `tool-input-start`)

### Learnings

- **Normalize the event stream** — build an `LLMEvent` union type that all backends emit. This eliminates the streaming/non-streaming bifurcation and the two separate dispatch paths. Omnius already has `runEvents.ts` types but doesn't use them as a unified stream.
- **Adopt tool-input streaming** — expose partial tool argument events upstream so the streaming executor can start executing tools before all arguments are fully received (especially valuable for large writes/reads with many arguments).
- **Unify backend adapter interface** — currently each backend returns a different format. A normalized `Stream<LLMEvent>` return type would allow the entire dispatch pipeline to be backend-agnostic.

---

## 8. Adversary / Critic / Post-Turn Analysis

### OpenCode

- **No adversary system** — OpenCode does not have a dedicated post-turn meta-analysis layer. It relies on:
  - **Permission `ask` prompts** for user-in-the-loop decisions (`doom_loop` detection at `processor.ts` — checks last 3 identical tool calls)
  - **The model's own reasoning** to self-correct
  - **Compaction agent** for context-level correction
- **Error handling**: `tool-error` → `failToolCall()` → marks part as `"error"` + sets `ctx.blocked` for permission rejections

### Omnius — `orchestrator/src/agenticRunner.ts:20395-20758`

- **Extensive adversary system** with 4 detections:
  1. `adversaryObserve` false failure claim (line 20541)
  2. False success claim (line 20595)
  3. **Redundant action** — repeated same tool+args that already succeeded (line 20659)
  4. **Idle think** — runaway output without input (line 20743)
- **Redundant action signal** (`_adversaryRedundantSignals`): forwarded to `Critic.evaluate()` in `executeSingle` (line 12735-12738)
- **Escalation**: dedup `_adversaryRecentFlags` with system-role injection after 3 repeats
- **Adversary mode**: `"backseat"` (events only), `"skillcoach"` (injects critiques), `"both"` (default)
- **Also**: REG-24 fingerprint dedup, REG-47 backward-pass critic, REG-11 semantic shell failure detection

### Learnings

- **Omnius's adversary system is genuinely innovative** — OpenCode has no equivalent. The redundant action detection, false-failure-correction, and depth-gauge integration are unique differentiators.
- **But complexity is high** — the adversary adds 500+ lines of stateful logic across multiple methods. Consider whether some adversarial patterns could be simplified to OpenCode's permission `ask` pattern (user-in-the-loop) instead of autonomous critique generation, at least for lower-confidence detections.
- **Unify with Critic** — the adversary `_adversaryRedundantSignals` is consumed by `Critic.evaluate()` in `executeSingle`. This cross-cutting coupling makes it hard to understand the full redundant-action flow. Consider making the adversary emit typed events that the critic subscribes to.

---

## 9. Agent Type System

### OpenCode — `src/agent/agent.ts`

- **Declarative schema**: `Info = { name, description, mode: "subagent"|"primary"|"all", permission: Ruleset[], model, prompt, steps, color, ... }`
- **Built-in registration inside `layer`**: build (primary), plan (primary), general (subagent), explore (subagent), compaction/title/summary (hidden primaries)
- **User-defined**: read from user config (`cfg.agent`), merged into `agents` map
- **`generate()`**: creates new agents via LLM `generateObject` using `PROMPT_GENERATE`
- **Permission rulesets per agent**: `explore` denies everything except grep/glob/list/bash/webfetch/websearch/read; `plan` denies all edit tools
- **Hidden agents**: `compaction`, `title`, `summary` — `native: true, hidden: true`, not shown in agent picker, used internally

### Omnius — `orchestrator/src/agent-types.ts`

- **Programmatic registry**: `AGENT_TYPES = Record<string, AgentType>` with `registerAgentType(name, config)`
- **Built-in**: `general` (full access), `explore` (read-only), `plan` (read-only + planning), `coordinator` (orchestration + spawning)
- **Agent shape**: `{ allowedTools, disallowedTools, maxTurns, model, canSpawnAgents, description, systemPromptAddition }`
- **No `mode` field** — agents aren't classified as primary/subagent. Any agent can be activated by the user.
- **No user-defined agents** — no config-based agent creation
- **No agent generation** — no LLM-based agent creation

### Learnings

- **Add `mode` to agent types** — explicitly declare which agents are `"primary"` (user-selectable) vs `"subagent"` (task-tool-only) vs `"all"`. This enforces the architectural constraint that subagents can only be spawned via the `agent` tool.
- **Make agent definitions declarative/configurable** — allow users to define custom agents in config files. The `generate()` pattern (LLM creates an agent definition) is an interesting power feature but secondary.
- **Use permission rulesets per agent type** — instead of `allowedTools`/`disallowedTools` arrays, adopt OpenCode's `PermissionV1.Ruleset` pattern. This enables granular `allow`/`deny`/`ask` per tool/permission and natural subagent inheritance.

---

## 10. Permissions & Safety

### OpenCode — `packages/core/src/session/permission.ts`

- **Granular rulesets**: `PermissionV1.Ruleset = Array<{ permission: string, pattern: string, action: "allow" | "deny" | "ask" }>`
- **`ask` prompts**: user-in-the-loop permission checks (e.g., `doom_loop: ask` — after 3 identical tool calls, ask user if they want to allow/deny)
- **Parent→child forwarding** (`src/agent/subagent-permissions.ts`): parent agent denies + parent session denies + `external_directory` rules are forwarded to subagent sessions
- **Default deny for subagents**: `todowrite` and `task` are denied in subagents unless explicitly permitted

### Omnius — `orchestrator/src/agenticRunner.ts` + tool implementations

- **Constraint system** (`execution/src/workingNotes.ts`, `constraints`):
  - `formatSecurityNotice()`, `checkConstraints()`, `formatViolationWarning()`
  - Constraints checked in `executeSingle` before tool dispatch
- **Tool allow/disallow lists**: per agent type, simple string arrays
- **No granular permission rules** — no `ask` prompts, no `deny`/`allow`/`ask` ternary
- **No subagent permission derivation** — sub-agents (via `agent` tool) inherit parent's full permission set
- **No doom-loop detection** — the adversary has `idle_think` (consecutive short outputs) but no permission-based intervention

### Learnings

- **Implement granular permission rulesets** — replace flat allow/disallow arrays with structured `{ permission, pattern, action }` rules. This enables: (a) `ask` for sensitive operations, (b) pattern-based denials (e.g., `edit:*.env` → deny), (c) forwarding to subagents, (d) user-configurable permission overrides.
- **Add doom-loop detection with `ask`** — when the model repeats the same tool with same args 3+ times, prompt the user instead of silently injecting an adversary critique. The user can decide allow/deny/interrupt.
- **Constraint system is good, but integrate with permissions** — the `checkConstraints()` system is powerful but operates independently of the agent type system. Constraints should be part of the permission ruleset, not a separate pre-hook.

---

## Summary: Highest-Impact Improvements (Stack Ranked)

| Priority | Improvement                               | Why                                                                                                      |
| -------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| **P0**   | Child sessions with permission derivation | Sub-agents currently unbounded — can do anything parent can. No state isolation.                         |
| **P1**   | Declarative agent types with `mode`       | Enables architectural enforcement (primary vs subagent), user-configurable agents, LLM-generated agents  |
| **P2**   | Unified event-stream dispatch             | Eliminates streaming/non-streaming bifurcation + 3× duplicated `task_complete` handlers                  |
| **P3**   | Natural exit condition                    | Replace `task_complete`-required exit with LLM `stop_reason` detection; keep `task_complete` as optional |
| **P4**   | Runner state machine                      | Run/Shell mutual exclusion, drain coalescing, interruption model — prevents subtle concurrency bugs      |
| **P5**   | Tool definition schema validation         | Replace `Record<string, unknown>` with Zod schemas for free validation + documented schemas              |
| **P6**   | Granular permission rulesets              | `allow`/`deny`/`ask` ternary — cascade to subagents, user-in-the-loop for sensitive ops                  |
| **P7**   | Decompose AgenticRunner                   | Split into Runner + Orchestrator + Coordinator layers (currently 25K-line monolith)                      |
| **P8**   | Unify backend adapter interface           | Normalized `LLMEvent` stream type eliminates per-backend dispatch differences                            |
| **P9**   | Dedicated compaction agent                | Offload context compression to a zero-tool agent to avoid pollution in main context                      |
| **P10**  | `task_id` resumption for sub-agents       | Allow continuing prior sub-agent sessions (dedup exploration, iterative refinement)                      |
