Local service for turning broad research or engineering goals into low-noise Codex-assisted orchestration loops.
The intended entrypoint is a goal brief, not a hand-written JSON file. For example:
Find and reproduce three high-quality fraud-detection baselines published or updated after 2024.
Produce runnable reproduction notes, metrics, and a final comparison report.
The orchestrator owns the lifecycle. Codex-powered turns provide model intelligence in three short-lived roles:
- Planner turns the broad goal into a task graph and next task plan.
- Worker applies short-lived code, configuration, adaptation, or documentation changes.
- Judge evaluates evidence, task results, artifacts, and recommends the next decision.
Long-running work stays outside Codex. Searches, downloads, dataset preparation, baseline reproduction, training, benchmarks, and report generation are TaskRuns owned by tmux, subprocesses, schedulers, or external systems. Once a long TaskRun is safely owned outside Codex, the orchestrator records state and forces an operational pause so no Codex turn sits around monitoring logs.
Hard boundary: Codex roles cannot create or own loops, cannot instantiate real TaskRuns, and cannot recursively start new loops to keep control. Planner, Worker, and Judge may write their required artifacts, but task creation and lifecycle transitions are executed only by the Orchestrator and Policy Engine. SQLite state is authoritative.
Production Codex turns are intended to run through the Codex SDK. Skills, codex exec, hooks, and thread automations can inspire operational-pause behavior or serve as temporary wake/debug surfaces, but they are not the Orchestrator control plane.
For research engineers and automation platform builders who need to delegate cross-day coding, research, reproduction, and benchmark goals to Codex without keeping a model turn alive, Codex Agent Loop Orchestrator is a local orchestration service that turns broad goals into auditable task graphs, runs Codex only for planning/work/judgment, and hands long work to external owners with explicit wake paths.
Unlike ad hoc tmux scripts, notebook notes, or long-running codex exec sessions, CALO keeps lifecycle authority outside the model: Planner can suggest, Worker can change files, Judge can recommend, but Orchestrator and PolicyEngine decide what actually runs.
- Local FastAPI service with Web UI at
/ui/ - CLI for creating, starting, stepping, pausing, resuming, cancelling, and inspecting loops
- Planner / Worker / Judge role separation
- Task graph and TaskRun orchestration model
- Sync and async execution modes
- SQLite state store
- Idempotent callback handling
- Optional HMAC-signed webhook callbacks
- Git commits for source changes
- Audit artifacts under
.codex/agent-loop/<loop_id>/ - Offline deterministic runner for testing
- Codex SDK runner target for real Planner / Worker / Judge turns
Clone and install in editable mode:
git clone https://github.com/2019wakeup/codex-agent-loop-orchestrator.git
cd codex-agent-loop-orchestrator
python -m venv .venv
. .venv/bin/activate
pip install -e '.[dev]'Verify the install:
calo --help
pytest -qExpected test result:
33 passed
The product flow should look like this:
- User submits a goal brief, repo path, constraints, resource budget, and approval preferences.
- Orchestrator compiles that into a goal contract.
- Planner produces a task graph, such as literature search, repo discovery, dataset download, environment setup, baseline reproduction, benchmark, and final report.
- Policy Engine validates the task graph against permissions, budget, and human gates.
- Orchestrator creates TaskRuns for approved tasks.
- Short work can run immediately through Worker or local commands.
- Long work is handed to an external owner. After that handoff, Codex must stop monitoring and the loop enters operational pause.
- Webhooks, status files, watchdogs, schedulers, or user actions wake the orchestrator later.
- Judge reviews compact evidence and recommends continue, fix, stop, rollback, or ask for review.
- Orchestrator decides and records the next state.
The JSON contract is the durable internal control plane for this flow. It is still exposed in the current MVP and for automation, but it should not remain the main human entrypoint.
CALO is intended to replace brittle research automation patterns:
- Leaving Codex blocked on a long Bash command.
- Manually watching tmux logs and pasting summaries back into a chat.
- Hand-maintaining scattered status notes in notebooks or Markdown.
- Letting a model decide to retry, clean data, or spawn follow-up loops without a hard external policy layer.
- Treating one training command as the whole product when the real job is a task chain: discover, select, download, adapt, run, evaluate, report.
The current repository is a working local MVP, but not the final product experience:
- It proves loop state, policy decisions, callback idempotency, sync/async demos, Git audit commits, and a local Web UI.
- It supports goal-first creation from CLI, API, and Web UI.
- It persists TaskGraph and TaskRun records and exposes artifact previews in the API and Web UI.
- It still exposes
examples/loop_contract.jsonas an advanced automation and test-fixture entry. - It has a deterministic local runner and a
codex-clicompatibility bridge. - It does not yet implement the production Codex SDK adapter or scheduler-specific TaskRun adapters.
Use the current MVP to validate the orchestration spine. Use the PRD as the source of truth for the product direction.
Run a complete local loop using the deterministic offline runner. This demo is intentionally narrow and metric-based; it proves the loop machinery, not the final broad-goal UX:
calo demo --workspace /tmp/calo-demo-workspace --target 0.70 --max-turns 3Expected shape:
status=LoopStatus.COMPLETED
turns=2
best_score=0.7
artifacts=/tmp/calo-demo-workspace/.codex/agent-loop/demo_loop
Start the local service for that workspace:
calo serve --workspace /tmp/calo-demo-workspace --host 127.0.0.1 --port 8000Open:
http://127.0.0.1:8000/ui/
The Web UI shows loop-level status, current phase, next action, turn progress, metric progress, last run, last decision, and a human-readable timeline.
--workspace is where the orchestrator stores its SQLite database:
<workspace>/.calo/state.sqlite3
repo_path is defined inside the loop contract and points to the repository being changed by the loop. For simple local use, the workspace and repo path can be the same directory.
Each loop writes audit files under:
<repo_path>/.codex/agent-loop/<loop_id>/
Important files:
contract.jsontask_graph/turn_<n>.jsonstate.jsonplan/turn_<n>.jsonhandoff/turn_<n>.mdjudge/turn_<n>.jsonevidence/turn_<n>.jsonruns/run_<n>.jsonreports/final_report.md
Codex roles write task graph suggestions, plans, code changes, handoffs, and judge reports. They do not control the loop.
The loop is controlled by:
LoopController -> PolicyEngine -> StateStore
This prevents a Codex turn from recursively creating new loops, instantiating TaskRuns on its own, force-completing a loop, or bypassing lifecycle policy.
The current MVP now supports a goal-first CLI and API path. This is the recommended human entrypoint:
calo goal \
--objective "Raise fake score from a plain goal brief" \
--workspace /tmp/calo-goal-loop \
--loop-id goal_loop \
--target 0.6 \
--max-turns 2
calo start goal_loop --workspace /tmp/calo-goal-loopThe goal command compiles a durable contract, stores it under .codex/agent-loop/<loop_id>/contract.json, and tells you the exact start command.
The contract JSON path remains available as an advanced/API entrypoint and test fixture:
{
"loop_id": "example_loop",
"objective": "Raise fake score to 0.70",
"repo_path": "/tmp/calo-example-loop",
"target_metric": "score",
"target_value": 0.7,
"execution_mode": "sync",
"iteration_limits": {
"max_turns": 3,
"patience": 3,
"min_delta": 0.001
},
"commands": {
"validation": "python -m py_compile target_app.py",
"train": "python fake_train.py --callback-file {callback_file} --run-id {run_id} --turn-id {turn_id}"
}
}Important fields:
loop_id: stable identifier used by CLI, API, UI, and artifacts.objective: what Planner and Worker should optimize.repo_path: repository to modify and audit.target_metric/target_value: metric used by Judge and PolicyEngine.execution_mode:syncorasync.iteration_limits.max_turns: hard stop.iteration_limits.patience: stop after repeated low-improvement turns.commands.validation: quick command before training.commands.train: long-running command. It receives{callback_file},{run_id}, and{turn_id}placeholders.
Available default human entrypoints:
calo goal: accepts a broad objective, repo, constraints, budget, and approval gates, then generates the contract.- API
POST /api/v1/goals: accepts a goal brief and creates a loop through the Orchestrator.
Available inspection surfaces:
- Web UI task graph, TaskRun, and artifact browser panels.
- API
GET /api/v1/loops/{loop_id}/tasksandGET /api/v1/loops/{loop_id}/artifacts.
Now:
- Keep goal-first CLI/API/Web creation stable while preserving contract JSON for tests and automation.
- Tighten the dashboard around phase, next action, owner, wake path, callback readiness, task graph, TaskRuns, artifacts, and readable event summaries.
- Preserve deterministic local runner for acceptance tests.
Next:
- Harden
calo goal,POST /api/v1/goals, and Web goal creation for real user repositories. - Replace training-specific naming in callbacks and events with generic TaskRun language while preserving backward compatibility.
- Implement Codex SDK Runner as the production model-backed path.
- Add scheduler-specific TaskRun adapters for tmux/systemd/Slurm/Ray/Kubernetes.
- Add policy tests that reject recursive loop creation, unapproved TaskRun creation, over-budget tasks, and long polling.
Later:
- Add a richer final report viewer.
- Add multi-loop queueing, PR integration, and team audit controls.
Sync mode runs a turn and immediately runs the training command in the same CLI process. This is best for smoke tests and short jobs.
Create the loop:
calo create --config examples/loop_contract.json --workspace /tmp/calo-example-loopRun until complete:
calo start example_loop --workspace /tmp/calo-example-loopInspect state:
calo status example_loop --workspace /tmp/calo-example-loop
calo events example_loop --workspace /tmp/calo-example-loopOpen the Web UI:
calo serve --workspace /tmp/calo-example-loop --host 127.0.0.1 --port 8000Then visit:
http://127.0.0.1:8000/ui/
Async mode launches training as a background process and returns with the loop in waiting_callback. This is an operational pause: the manifest must name an external owner and wake path before the orchestrator releases Codex control.
Create the loop:
calo create --config examples/async_loop_contract.json --workspace /tmp/calo-async-example-loopRun one orchestrator turn and launch training:
calo step async_example_loop --workspace /tmp/calo-async-example-loopThe loop should now be waiting:
"status": "waiting_callback"
"last_decision": "operational_pause"
Collect the callback file after training writes it:
calo collect-callback async_example_loop --workspace /tmp/calo-async-example-loopRepeat step and collect-callback until the loop reaches completed, or use the Web UI to watch progress.
The run manifest records:
owner: who holds the long-running TaskRun, for examplelocal_subprocesswake_path: the callback file or webhook path that will wake the loopcodex_control:released, meaning Codex is not monitoring logs
Start the service:
calo serve --workspace /tmp/calo-example-loop --host 127.0.0.1 --port 8000Open:
http://127.0.0.1:8000/ui/
The dashboard lets you create and operate local loops:
- Create a loop from a goal brief without hand-writing contract JSON
- Choose the repository from a dropdown instead of typing raw paths
- Configure target metric, patience, min delta, review gates, runner, and model from advanced settings
- Keep validation and long-work adapter commands tucked under Adapter commands for custom integrations
- Start a loop until the next terminal or waiting state
- Step exactly one orchestrator turn
- Collect an async callback after the wake path is written
- Pause and resume orchestrator-owned work
- Cancel orchestration while explicitly preserving external TaskRun ownership
The dashboard tracks:
- Loop status
- Current phase and next expected action
- Current turn and max turns
- Turn progress
- Best metric and target value
- Metric progress
- Last run ID
- Last decision
- Elapsed loop time and estimated Codex token usage
- Human-readable loop timeline with expandable details
- Async TaskRun owner, wake path, run manifest, and Codex control state
- Callback readiness, run status, and run log path for async TaskRuns
- Persisted TaskGraph nodes and TaskRun records
- Artifact previews for contract, task graph, evidence, runs, judge reports, and final reports
Operational pause is already a release of Codex control, so waiting_callback loops are not pausable. Collect the callback when the wake path is ready, cancel orchestration if the loop should stop, or explicitly terminate an owned local subprocess TaskRun. Plain cancellation does not terminate an external TaskRun; the manifest and timeline record that the external owner still controls it.
Buttons call the same backend lifecycle endpoints as the CLI:
- Create loop
- Start
- Step
- Collect callback
- Pause
- Resume
- Cancel
- Terminate TaskRun
The UI does not own lifecycle state. It reads summaries from:
GET /api/v1/dashboard
GET /api/v1/loops/{loop_id}/summary
Some hosted GPU machines do not expose public HTTP/HTTPS ports. In that case, bind the service to localhost on the remote machine and access it through an SSH tunnel from your laptop.
On the remote machine, start CALO on port 6006:
calo serve --workspace /tmp/calo-example-loop --host 127.0.0.1 --port 6006On your local machine, open PowerShell on Windows or Terminal on macOS/Linux, then create the tunnel:
ssh -CNg -L <local_port>:127.0.0.1:<remote_service_port> <user>@<ssh_host> -p <ssh_port>For the 6006 service example, keep both forwarded ports as 6006 unless your local machine already uses that port:
ssh -CNg -L 6006:127.0.0.1:6006 <user>@<ssh_host> -p <ssh_port>If SSH asks yes/no, answer yes. Enter the machine password or key passphrase when prompted. The terminal usually shows no output after a successful connection.
Then open this URL locally:
http://127.0.0.1:6006/ui/
Notes:
- Do not commit machine passwords or temporary access credentials.
- If the local port is already in use, change both local sides consistently, for example
-L 16006:127.0.0.1:6006, then openhttp://127.0.0.1:16006/ui/. - If you see
Permission denied, re-enter the password manually; some terminals handle pasted passwords poorly.
calo goal --objective <goal brief> --workspace <workspace>
calo create --config <contract.json> --workspace <workspace>
calo start <loop_id> --workspace <workspace>
calo step <loop_id> --workspace <workspace>
calo collect-callback <loop_id> --workspace <workspace>
calo pause <loop_id> --workspace <workspace>
calo resume <loop_id> --workspace <workspace>
calo cancel <loop_id> --workspace <workspace>
calo terminate-run <loop_id> --workspace <workspace> --run-id <run_id>
calo status <loop_id> --workspace <workspace>
calo events <loop_id> --workspace <workspace>
calo list --workspace <workspace>
calo serve --workspace <workspace> --host 127.0.0.1 --port 8000Default:
calo start example_loop --workspace /tmp/calo-example-loop --runner localThe local runner is deterministic and offline. It is intended for tests, demos, and end-to-end validation without API credentials.
The PRD target is a real Codex SDK runner. The runner must let Orchestrator create, run, resume, sandbox, and audit Planner / Worker / Judge turns programmatically. The SDK runner, not a skill, is the production path for model-backed decisions.
The current MVP still exposes a codex-cli runner as a compatibility bridge:
calo start example_loop \
--workspace /tmp/calo-example-loop \
--runner codex-cliOptionally select a model:
calo start example_loop \
--workspace /tmp/calo-example-loop \
--runner codex-cli \
--model gpt-5.1-codexThe Codex CLI runner still obeys the same boundary:
- Planner writes plan artifacts.
- Worker changes repo files and writes handoff artifacts.
- Judge writes advisory reports.
- Orchestrator and PolicyEngine own lifecycle transitions.
Do not treat codex-cli as the final architecture. It is useful for smoke tests while the SDK adapter is being wired, but production orchestration should use Codex SDK calls and persist SDK thread metadata.
Goal-first API:
POST /api/v1/goalsAdvanced/API entry:
POST /api/v1/loopsLifecycle:
POST /api/v1/loops/{loop_id}/start
POST /api/v1/loops/{loop_id}/step
POST /api/v1/loops/{loop_id}/collect-callback
POST /api/v1/loops/{loop_id}/pause
POST /api/v1/loops/{loop_id}/resume
POST /api/v1/loops/{loop_id}/cancel
POST /api/v1/loops/{loop_id}/runs/{run_id}/terminateLifecycle endpoints accept optional runner and model query parameters for API symmetry. start, step, and collect-callback use them when invoking a runner; pause, resume, and cancel validate them for consistent Web behavior:
?runner=local
?runner=codex-cli&model=<model>
Read state:
GET /api/v1/loops
GET /api/v1/loops/{loop_id}
GET /api/v1/loops/{loop_id}/events
GET /api/v1/context
GET /api/v1/dashboard
GET /api/v1/loops/{loop_id}/summary
GET /api/v1/loops/{loop_id}/tasks
GET /api/v1/loops/{loop_id}/artifactsTaskRun callback:
POST /api/v1/loops/{loop_id}/runs/{run_id}/callbackCallbacks are idempotent by (loop_id, run_id).
To require HMAC verification, set webhook.secret in the contract. Then send:
X-Agent-Loop-Timestamp: <unix_seconds>
X-Agent-Loop-Signature: sha256=<hmac_sha256(secret, timestamp + "." + raw_body)>
The timestamp is checked against webhook.timestamp_tolerance_seconds.
Run all tests:
python -m pytest -qRun goal-first acceptance:
bash scripts/goal_acceptance_demo.sh /tmp/calo-goal-acceptanceRun sync acceptance:
bash scripts/acceptance_demo.sh /tmp/calo-sync-acceptanceRun async acceptance:
bash scripts/async_acceptance_demo.sh /tmp/calo-async-acceptanceRun Web acceptance:
bash scripts/web_acceptance_demo.sh /tmp/calo-web-acceptance 8766The async script proves:
- A loop can launch a background TaskRun and enter
waiting_callback. - A callback can be collected later.
- The loop can continue across turns.
- The final state reaches
completed. - A final report is written.
Install the package in the active environment:
pip install -e '.[dev]'Or run through Python:
python -m calo.cli --helpConfirm the workspace points to the same .calo/state.sqlite3 used when the loop was created:
calo list --workspace /tmp/calo-example-loopThen restart:
calo serve --workspace /tmp/calo-example-loopCheck whether the background TaskRun wrote the callback file:
ls <repo_path>/.codex/agent-loop/<loop_id>/runs/Then collect it:
calo collect-callback <loop_id> --workspace <workspace>Verify Codex CLI authentication and availability:
codex --help
codex exec --helpUse the local runner for smoke tests:
calo start <loop_id> --workspace <workspace> --runner localThis repository is usable as a local MVP:
- It can run complete sync demo loops.
- It can create runnable loops from plain goal briefs.
- It can run async callback demo loops.
- It has a local Web UI.
- It persists TaskGraph and TaskRun records.
- It exposes artifact previews through API and Web UI.
- It can explicitly terminate owned local subprocess TaskRuns.
- It has signed, idempotent callbacks.
- It has tests and acceptance scripts.
- It keeps lifecycle authority in the Orchestrator and PolicyEngine.
- It still needs scheduler-specific TaskRun adapters and the production Codex SDK Runner to match the full PRD.
See also: