The harness, as a chat

August 21, 2026 · View on GitHub

A web page you talk to. Same harness, same stages, same artifacts as the CLI. This is a second renderer over the event stream the stages already emit, not a second implementation.

There is no separate front end to build or start. The page is one static file this server hands out on /, and it talks to the same server's JSON endpoints. No node, no npm, no build step.

Setting it up

git clone https://github.com/future-agi/agent-learning-kit
cd agent-learning-kit

uv sync --extra livekit --group dev

--extra livekit is required: the voice run path imports it. The UI's own fastapi and uvicorn come in with --group dev, and are also available as --extra harness-ui if you would rather not pull the dev tooling.

You also need the claude command on your PATH (npm install -g @anthropic-ai/claude-code). The harness talks to the model through the Claude Agent SDK, which runs that binary underneath; without it every stage fails immediately.

Credentials go through Vertex AI, not a plain Anthropic key: config.provider_env sets CLAUDE_CODE_USE_VERTEX=1, so ANTHROPIC_API_KEY on its own will not work. Copy the template and fill in your service account:

cp oss/simulation-acceptance/.env.example .env.acceptance
# GOOGLE_APPLICATION_CREDENTIALS=/absolute/path/to/your-service-account.json
# GOOGLE_CLOUD_PROJECT=your-gcp-project-id

.env.acceptance is git-ignored and points at a private key. Never commit it or paste its contents anywhere.

Then load it and pick the model, in each new terminal:

set -a; . ./.env.acceptance; set +a
export CLOUD_ML_REGION=global
export ALK_HARNESS_MODEL=claude-sonnet-4-6

It prints the model and which credentials it found before starting, so a run never begins on something you did not intend. Use Sonnet or better. Haiku has misread an agent's modality, and modality decides how every later test is run.

Running it

.venv/bin/python harness-ui/server.py

Open http://localhost:8777, press + new, and say what you want tested:

i want to test my voice ordering agent. the code is at /absolute/path/to/the/agent

One message is enough to begin. Reception takes the path out of the sentence and hands straight over to reading the agent. From there it is a conversation:

now build the environment for it
write me 5 scenarios: a plain order, one for something you do not have, one where the
customer changes their mind, one that pushes against a rule, and one with quantity

Questions in between are answered without spending a stage, so "can it handle quantity?" is a fair thing to ask mid-flight.

To stop the server, Ctrl-C. Checking whether it is still up with lsof -ti:8777 will mislead you: that matches a browser's leftover sockets. Use lsof -nP -iTCP:8777 -sTCP:LISTEN.

Restart the server after changing anything under src/fi/alk/harness/. A long-lived process does not pick up code or skills on its own.

What you can do in it

  • Start, reopen or delete a conversation from the picker. Everything about one conversation lives in its own folder under artifacts/sessions/<id>/, so reopening it restores the chat and every artifact. A blank slate is rm -rf artifacts/sessions/* artifacts/.open-session.
  • Talk. "build the world", "write 5 hard scenarios", "make that one harder", "add a mango smoothie to the menu". Each reply shows the work underneath it: which tool ran, what it answered, what it refused.
  • Move between stages by clicking the roadmap. Stages are not a wizard: going back to correct a contract after the world is built is the ordinary case. A stage whose input does not exist yet says why it cannot be opened.
  • Read the four tabs. Contract, Environment, Scenarios and Runs are what is on disk. Each scenario shows its instruction, what it changes, its reference solution, its checks, and three gate lights; its files open inline.
  • Run the scenarios. The conversation between the simulated customer and the agent streams into the chat as it happens, then a verdict lands with the checks.

What to expect while it works

The build stage is the long one: roughly 30 turns and about ten minutes for a five-tool agent. The Environment tab stays empty until it finishes, because the world is held in memory until save_world writes it. The chat is where the progress is: schema, seeds, then one line per handler as each is defined and smoke-called.

The two files

FileWhat it is
server.pyFastAPI. Holds one Conversation open, streams its events as server-sent events, and serves the artifacts as JSON.
static/index.htmlThe whole interface: markup, styling and rendering in one file.

To restyle it, edit the <style> block and refresh. To change what a pane shows, edit loadTab.

The endpoints

Anything that can read server-sent events can be a front end for this. If the platform team builds its own, it talks to these and nothing on the harness side changes.

EndpointDoes
POST /api/saySend a message; streams text, tool, result, artifact, done events
POST /api/runRun scenarios; streams exchange and result_card events
POST /api/stopStop the stage that is running, mid-turn
GET /api/statusStage, agent, model, spend, which artifacts exist, which stages can be opened
GET/POST /api/sessions · POST /api/sessions/open · DELETE /api/sessions/{id}List, start, reopen and delete conversations
GET /api/historyThe stored conversation, for restoring the page
POST /api/stageOpen a named stage, which is what the roadmap does
GET /api/contract · /world · /subgoals · /scenarios · /runsThe tabs
GET /api/scenario-fileOne file out of a scenario's folder

Every stream ends with a status event, which is what the header and tabs refresh from.

GET /api/scenarios re-runs all three gates on every request rather than reporting what was true when they were written. They are milliseconds of pure code, and a scenario shown as validated when the world has since changed underneath it is worse than one shown as unknown.

Known limits

  • One conversation per server. This is one operator talking to one harness. The open session is server-wide, so a second tab left on another conversation can move it between turns; it cannot happen mid-turn, because switching and deleting are both refused while a stage runs.
  • A second request while one is working gets a 409 rather than interleaving.
  • The run stage is not the focus yet. Environment and scenario generation are.