Skip to content
11 min readCritique

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.

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.

Fit by team shape

Persistent sessions reward multi-step automation. One-shot scripts can stay on chained fallbacks.

TeamTypical jobWhy persistent sessions help
Platform engineeringOwn an internal “fix bot” or codegen serviceTicket → code → tests → PRAvoid re-cloning large monorepos on every message
Developer experienceWire Critique into Backstage or a custom portalIterative refactors from product specsSame run id in your UI maps to a real agent thread
Security / compliance automationRemediate findings with human checkpointsFindings batch → patch → verification turnSession continuity keeps branch context intact
Single-shot CI scriptsNightly chore with one promptDependency bumpChained 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.

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.

Before (chained MVP)
Turn 1 completesSandbox killedTurn 2 = new job + pasted prior summary
Now (persistent)
Turn 1 completes → idleSandbox warmTurn 2 = message into same OpenCode session

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
fi

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 }'

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.

idle means the turn finished but the sandbox is warm for another message. completed means the session was closed (explicit endSession, expiry, or a non-persistent run).
Yes. Billing mode is stored on the run. Follow-ups inherit the same mode unless you override it in the messages payload.
The API responds with a conflict describing that the persistent session is unavailable. You can start a chained follow-up run that embeds bounded prior context, matching the original MVP behavior.
The Coding Agent API path does. Browser Builder jobs remain ephemeral unless we expose the same mode there later.

Try the Coding Agent API

Read the REST reference, copy curl examples, and issue a crt_ key from Connections.

Open Coding Agent API