A simple orchestration engine for OpenCode
OpenLoop is a zero-dependency, local-first orchestration engine specifically built for OpenCode. It allows you to define multiple AI agents, chain them in iterative loops via opencode run, and manage their execution state until a specific termination condition is met.
OpenLoop solves the "premature convergence" problem of single-agent systems by enforcing strict state isolation, externalized state management, and deterministic loop control, turning OpenCode from an interactive coding assistant into a robust, autonomous workflow engine.
OpenCode is incredible at interactive, single-turn coding tasks. However, when tasked with complex, multi-step workflows (e.g., "write a full test suite, audit it, and iterate until coverage is optimal"), a single OpenCode session will typically hallucinate completion after the first or second attempt. This happens due to context pollution and the inherent LLM bias to mark tasks as "done" prematurely.
OpenLoop solves this by decoupling the orchestration from the execution.
Instead of one agent doing everything in a single, bloated context window, OpenLoop acts as a deterministic state machine. It spins up isolated, headless OpenCode instances via opencode run, injects the current workflow state into their prompts, parses their structured output to update the state, and decides whether to loop back or terminate.
The classic use case, included as a working example in this repository, is the Author/Auditor pattern:
- Amala (Author) writes a comprehensive
pytestsuite. - Vera (Auditor) reviews the test output, checks coverage gaps, and audits the code.
- If Vera finds gaps, she updates the state with specific feedback, and OpenLoop loops back to Amala.
- If Vera is satisfied, she updates the state to
is_complete: true, and OpenLoop terminates the loop.
Because each agent runs in a fresh OpenCode context with only the relevant state injected, they never suffer from context degradation.
OpenLoop is built on three core pillars: Agent Definitions, Externalized State, and Deterministic Execution.
Agents are defined as simple Markdown files with YAML frontmatter. This keeps the system prompt human-readable while providing the orchestrator with necessary metadata.
---
name: vera
role: auditor
expected_output_format: xml_tag
---
# Role
You are VERA, a strict QA Auditor.
# Current State Context
The orchestrator will inject the current state below.
# Instructions
1. Review the code and test coverage.
2. Update the state based on your findings.
3. You MUST output your state update inside a `<state_update>` XML tag at the very end of your response.
Example:
<state_update>
{
"next_phase": "amala_fix",
"feedback": "Missing edge case in auth.py line 42",
"is_complete": false
}
</state_update>The orchestrator holds the "Single Source of Truth" in a Python dataclass.
- Injection: Before an agent runs, the current state is serialized to JSON and injected into the
opencode runprompt. - Extraction: After OpenCode finishes, OpenLoop parses the stdout for a structured state update (e.g., inside
<state_update>tags). - Transition: The orchestrator merges the update into the master state and evaluates the termination conditions.
A workflow in OpenLoop consists of three distinct phases:
- Preparation Phase (Optional): Runs once at the very beginning. Supports one or multiple agents executed in sequence. Useful for scaffolding, gathering context, or setting up the initial state.
- Loop Phase: Iterates through a defined sequence of agents.
- Supports any number of agents in the loop sequence.
- Checks
max_loopsto prevent infinite execution. - Evaluates the
end_state_condition(e.g.,is_complete == Trueorpayload.get('coverage', 0) >= 80) after each agent.
- Finalization Phase (Optional): Runs once at the end. Supports one or multiple agents executed in sequence. You can configure whether it runs only on successful completion (
finalize_on_abort: false), or also if the loop was aborted due to hittingmax_loops(finalize_on_abort: true).
Every agent invocation follows the same six-step cycle:
┌──────────────────────────────────────┐
│ ExecutionEngine │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │Workflow │────►│ serialize to │ │
│ │State │ │ JSON │ │
│ └───┬──────┘ └──────┬────────┘ │
│ │ │ │
│ │ merge() append to prompt │
│ │ │ │
│ ┌───▼──────┐ ┌──────▼────────┐ │
│ │ parse() │◄────│ opencode run │ │
│ │<state_ │ │ (LLM sub- │ │
│ │ update> │ │ process) │ │
│ └──────────┘ └───────────────┘ │
│ │
│ eval(end_state_condition) ──► loop │
│ or │
│ stop │
└──────────────────────────────────────┘
- Serialize — The engine serializes the current
WorkflowStateto a JSON string viastate.to_json(). - Inject — The JSON is appended to the agent's system prompt under a
# Current Stateheading (see_build_prompt()incore/engine.py). - Execute — The full prompt is sent to
opencode run, which spawns a headless LLM subprocess in a fresh, isolated context. - Parse — After the subprocess returns,
StateParser.extract_state_update()(incore/parser.py) scans stdout for a<state_update>XML tag (preferred) or a JSON code block (fallback). - Merge — The parsed key-value pairs are merged into the shared
WorkflowStateviastate.merge(). Only keys present in the update are changed; thepayloaddict is merged deeply. - Evaluate — The
end_state_conditionis evaluated. If true, the loop terminates. Otherwise, the next agent in the sequence starts at step 1 with the updated state.
The same cycle applies in all three phases. During the loop phase, the iteration counter is incremented before each full pass through the agent sequence.
Detailed reference documentation is available in the docs/ directory:
| Document | Description |
|---|---|
| Agent Definitions | Agent file format, frontmatter fields, system prompt conventions, best practices |
| Workflows | Workflow JSON schema, slot configuration, end_state_condition syntax |
| CLI | Complete CLI reference, flags, exit codes, examples |
| Configuration | openloop.json schema (JSONC with // and /* */ comments), field descriptions, search order, override priority |
| State | WorkflowState API, merge behavior, agent output format |
| GUI | Tkinter GUI layout, zones, toolbar actions |
- Built for OpenCode: Deeply integrated with the
opencode runheadless command. - Zero External Dependencies: Built entirely on the Python 3.13+ Standard Library. No
pip installrequired. Just clone and run. - Context Isolation: Agents run in fresh, isolated OpenCode contexts, preventing the "context pollution" that plagues single-agent workflows.
- Deterministic Loop Control: Hard limits (
max_loops) and strict state evaluation (including arbitrary Python expressions against the state payload) ensure the system never hangs in an infinite loop. - Flexible Agent Chaining: Define any number of agents per phase (preparation, loop, finalization). Supports complex multi-agent pipelines.
- Multiple Agents per Phase: Preparation and Finalization phases support one or multiple agents executed in sequence.
- Init Script & Workdir: Run a setup script (e.g., activate conda environment) and set the working directory before each agent invocation – configurable globally, per workflow, or via CLI.
- OpenCode Defaults: Set global defaults for model, agent (
build/plan), variant, and pure mode for everyopencode runinvocation – configurable inopenloop.json, per workflow, or via--opencode-defaultsCLI flag. - CLI & GUI Modes: Use the visual Tkinter builder for interactive workflow design, or run headless via
--clifor CI/CD integration. - Ready-to-Use Examples: Ships with fully fleshed-out
amala.md(author),vera.md(auditor), andproteus.md(analyst) agents, plus a workingtest_generationworkflow.
openloop/
├── openloop.py # Entry point (starts Tkinter GUI or CLI)
├── openloop.example.json # Example configuration with documented fields
├── openloop.json # Local configuration (gitignored; searched in CWD, falls back to openloop directory)
├── requirements.txt # Empty (Zero dependencies)
├── core/
│ ├── config.py # Configuration loader
│ ├── state.py # WorkflowState dataclass & serialization
│ ├── parser.py # State extraction from OpenCode stdout
│ ├── agent.py # AgentDefinition loader (Markdown + YAML)
│ ├── runner.py # Subprocess wrapper for `opencode run`
│ └── engine.py # The execution loop and state machine
├── ui/
│ └── app.py # Tkinter GUI for workflow configuration
├── agents/ # Default directory for agent definitions
│ ├── amala.md # Example: The Test Author
│ ├── vera.md # Example: The QA Auditor
│ └── proteus.md # Example: The Change Analyst
├── workflows/ # Default directory for saved workflow configs
│ └── test_generation.json # Example: A ready-to-run Amala/Vera workflow
└── tests/
└── integration.py # Integration tests
git clone https://github.com/yourusername/openloop.git
cd openloopCopy the example configuration and adjust it to your needs:
cp openloop.example.json openloop.jsonEnsure opencode is installed and available in your $PATH.
opencode --versionSince there are no dependencies, you can start the GUI immediately:
python openloop.pyThe GUI will show the available agents in the left panel. From there you can build a workflow, load the example, or configure a new one.
- Launch the GUI:
python openloop.py - Click Load Workflow and select
workflows/test_generation.json. - The workflow builder populates with Amala and Vera in the Loop zone.
- Click Start Execution to run. The log panel shows live output as the engine iterates between authoring and auditing.
- Click Stop Execution to abort between iterations.
You can also run headless (requires opencode in PATH):
python openloop.py --cli --workflow workflows/test_generation.jsonAdditional CLI options:
| Flag | Description |
|---|---|---|
| --cli | Run in headless CLI mode |
| --workflow <path> | Path to the workflow JSON file |
| --workdir <path> | Override the working directory |
| --init-script <cmd> | Override the init script/command |
| --opencode-defaults <json> | JSON string overriding opencode defaults for all agents (e.g., '{"model":"gpt-4o","agent":"plan"}') |
| --config <path> | Path to configuration file (default: openloop.json in CWD, falls back to openloop.json next to openloop.py) |
- Core Architecture & State Management
- Agent Loader & Subprocess Runner
- Execution Engine with Loop Control
- Tkinter GUI for Workflow Building
- Example Agents (Amala/Vera/Proteus) & Example Workflow
- CLI mode with
--cli,--workdir,--init-scriptflags - Advanced state evaluation (arbitrary Python expressions against the state payload)
- Multiple agents in preparation and finalization phases
- Workdir & init_script support (global, per-workflow, and CLI override)
- GUI preview pane for agent content
- Integration tests
- Global OpenCode defaults (
opencode_defaultsin config/workflow/CLI) (Issue #23) - Sub-workflow support in slots (Epic #18)
- Script hooks as third element category in slots (Epic #19)
- Dedicated documentation in
docs/(Issue #20)
This project is developed iteratively using GitHub Issues. If you want to contribute, please check the open issues, pick one, and follow the standard fork-and-PR workflow.
Note: All code, comments, and documentation must be written in English.