Coding Agent API: Persistent OpenCode Sessions for Multi-Turn Automation
The first Coding Agent API release could only chain follow-ups after a sandbox died. Now the same run id stays warm: idle status, live messages, and SSE until you close the session.
When we shipped the Coding Agent API in Critique v5, the honest constraint was visible in the docs: follow-ups were new jobs that replayed prior output as text. That was the right MVP. It worked everywhere, it billed predictably, and it never pretended a dead sandbox was still alive.
It was also the wrong long-term shape for agents that think in conversations. If your internal bot fixes a migration, then wants a follow-up test, then wants a small doc tweak, you do not want three cold starts. You want one repository checkout, one OpenCode session, and a control plane that understands turns.
What the Coding Agent API is
The Coding Agent API is Critique’s HTTP surface for general repo work: clone a GitHub repository you already connected, run OpenCode headlessly inside an isolated E2B sandbox, return patch stats and assistant text, and optionally open a draft pull request. It is the same execution stack as Builder, packaged for machines instead of the browser UI.
That distinction matters for buyers. Critique’s review and Change Passport products are built to judge a proposed merge. The Coding Agent API is built to implement a task. You can use both in one company, but you should not confuse the trust model: review agents need evidence and policy; coding agents need sandboxes, tools, and a clear billing boundary.
Who persistent sessions are for
Persistent sessions reward multi-step automation. One-shot scripts can stay on chained fallbacks.
| Team | Typical job | Why persistent sessions help | |
|---|---|---|---|
| Platform engineering | Own an internal “fix bot” or codegen service | Ticket → code → tests → PR | Avoid re-cloning large monorepos on every message |
| Developer experience | Wire Critique into Backstage or a custom portal | Iterative refactors from product specs | Same run id in your UI maps to a real agent thread |
| Security / compliance automation | Remediate findings with human checkpoints | Findings batch → patch → verification turn | Session continuity keeps branch context intact |
| Single-shot CI scripts | Nightly chore with one prompt | Dependency bump | Chained fallback is fine; idle adds little value |
The API still authenticates with Critique API keys (crt_) scoped for Builder read/write. Billing can stay on managed Critique credits or shift model spend to your OpenRouter key per run. Persistent sessions do not change that contract; they change how many sandboxes you spin up per conversation.
How a turn works under the hood
On the first turn, Critique creates an E2B sandbox from the OpenCode template, clones your repository at the requested ref, bootstraps tooling, starts opencode serve on localhost inside the VM, and opens an OpenCode session. The model reads the task file, uses tools, writes a summary, and optionally publishes a branch.
For Coding Agent API runs, we no longer kill that sandbox immediately. We store session bindings (sandbox id, OpenCode base URL, session id) on the job and mark the run idle with a session expiry aligned to your sandbox timeout. When you queue a follow-up, QStash reconnects to the same sandbox, verifies OpenCode health, and POSTs your new prompt to /session/{id}/message.
If OpenCode is unhealthy or the session aged out, the messages route returns a conflict and you can still fall back to the older chained run behavior. That fallback is deliberate: we would rather spawn a fresh sandbox than silently corrupt repo state.
Start a run and wait for idle
Create a run with POST /api/v1/coding-agent/runs. Poll GET /api/v1/coding-agent/runs/{id} until status is idle and sessionActive is true. The examples below use managed billing and draft PR publish; adjust modelId, billing.mode, and publish for your account.
Example — create run and poll
Replace CRT_API_KEY and repository. Requires jq in the shell example.
#!/usr/bin/env bash
set -euo pipefail
export CRT_API_KEY="${CRT_API_KEY:?set CRT_API_KEY}"
export REPO="${REPO:-acme/web}"
RUN_ID="$(
curl -sS https://critique.sh/api/v1/coding-agent/runs \
-H "Authorization: Bearer ${CRT_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"repository\": \"${REPO}\",
\"prompt\": \"Add Stripe webhook signature verification and unit tests.\",
\"modelId\": \"anthropic/claude-sonnet-4.6\",
\"billing\": { \"mode\": \"managed\" },
\"publish\": { \"mode\": \"draft_pr\" },
\"validationMode\": \"tests\"
}" | jq -r '.run.id'
)"
echo "Run id: ${RUN_ID}"
# Optional: live OpenCode activity while the turn executes
curl -N "https://critique.sh/api/v1/coding-agent/runs/${RUN_ID}/stream" \
-H "Authorization: Bearer ${CRT_API_KEY}" &
STREAM_PID=$!
until STATUS="$(curl -sS \
"https://critique.sh/api/v1/coding-agent/runs/${RUN_ID}?events=1" \
-H "Authorization: Bearer ${CRT_API_KEY}" | jq -r '.run.status')"; do
sleep 4
done
kill "${STREAM_PID}" 2>/dev/null || true
echo "Status: ${STATUS}"
if [[ "${STATUS}" != "idle" ]]; then
echo "Run did not reach idle (persistent session not ready)." >&2
exit 1
fiFollow-ups, streaming, and teardown
Send the next instruction with POST /api/v1/coding-agent/runs/{id}/messages while the run is idle. Critique queues another turn on the same job id (HTTP 202). For observability, open GET /api/v1/coding-agent/runs/{id}/stream as Server-Sent Events: you receive builder.event rows as OpenCode activity lands, then a run.status event when the turn finishes.
When your workflow is done, POST { "endSession": true } on the messages route. That kills the sandbox and moves the run to completed so you are not billed for idle time you do not need.
Example — follow-up and close session
# Same run id — warm OpenCode session, no new sandbox
curl -sS -X POST "https://critique.sh/api/v1/coding-agent/runs/${RUN_ID}/messages" \
-H "Authorization: Bearer ${CRT_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"prompt": "Add a regression test for expired webhook signatures."
}'
# When automation is finished, release the sandbox
curl -sS -X POST "https://critique.sh/api/v1/coding-agent/runs/${RUN_ID}/messages" \
-H "Authorization: Bearer ${CRT_API_KEY}" \
-H "Content-Type: application/json" \
-d '{ "endSession": true }'What we did not try to solve yet
Persistent sessions are turn-based, not a WebSocket chat UI. You still queue work through QStash and poll or stream events; we have not exposed arbitrary mid-turn injection while OpenCode is running. Cross-region latency to the sandbox is unchanged. Very long idle periods will hit E2B timeout limits, which is why sessionExpiresAt exists and why endSession is part of the contract.
If you need review-gated merge discipline on the output, pair this API with Critique review runs and Change Passports on the PR the agent opens. The coding agent implements; the review agent argues. That separation is still the safest default for production teams.
Try the Coding Agent API
Read the REST reference, copy curl examples, and issue a crt_ key from Connections.
Open Coding Agent APIPlatform context
Marketplace, merge policy, Insights, and how warm sessions fit the v5 release.
Prompt in, sandbox out
Marketing page with managed vs OpenRouter billing examples.
Runs, messages, stream
Endpoint tables, scopes, and response fields including sessionActive.
Fix after review
Queue Composer 2.5 cloud agents from Critique review runs.
Separate jobs
Why merge-gate review should not be the same agent that wrote the patch.