The operator view: how to start watching a running goldfive rollout, how to steer it live from the UI, and how to interpret the intervention cards the Trajectory view shows. Every command below is copy-pasteable from the repository root.
For deeper architectural context see docs/design/03-server.md and docs/design/02-client-library.md.
The path from clone to live demo, with the network bindings each step opens up:
flowchart LR
Clone[clone repo<br/>+ third_party/adk-python] --> Install[make install<br/>uv sync · pnpm install]
Install --> Server[make server-run<br/>:7531 gRPC · :7532 gRPC-Web]
Install --> Frontend[make frontend-dev<br/>Vite → :7532]
Server --> Demo[make demo-presentation<br/>agent → :7531]
Frontend --> UI([browser at Vite URL])
Demo --> UI
- Python 3.11+ with
uvonPATH - Node 20+ with
pnpm git(for cloning the adk-python dependency intothird_party/)
The root pyproject.toml installs Google's adk-python as an editable path dependency, so you need a local checkout of it before running make install:
git clone https://github.com/google/adk-python.git third_party/adk-python
make installmake install runs uv sync for server/ and client/ and pnpm install --frozen-lockfile for frontend/. third_party/ is git-ignored in this repo — ADK is a read-only vendored dependency that never ships with harmonograf.
make server-runThe server-run target invokes:
cd server && uv run python -m harmonograf_server --store sqlite --data-dir <repo>/data
Defaults you get from that invocation:
| Flag | Default | Notes |
|---|---|---|
--host |
127.0.0.1 |
Loopback only. Binding non-loopback emits a warning; v0 has no TLS. |
--port |
7531 |
Native gRPC (agent clients). |
--web-port |
7532 |
gRPC-Web via sonora + hypercorn (browser frontend). |
--store |
sqlite |
Overridden by the Makefile target; use --store memory for ephemeral runs. |
--data-dir |
~/.harmonograf/data |
Makefile target overrides this to <repo>/data. |
--log-level |
INFO |
Also accepts DEBUG, WARNING, ERROR. |
--log-format |
text |
Switch to json for log shippers. |
To run the server directly with different flags:
cd server && uv run python -m harmonograf_server \
--store sqlite --data-dir ~/.harmonograf/data \
--log-level DEBUG --log-format jsonIn a second terminal:
make frontend-devThis runs pnpm dev under frontend/ (Vite). The UI defaults to talking to the gRPC-Web listener at http://127.0.0.1:7532. To point it elsewhere, set VITE_HARMONOGRAF_API before starting Vite:
cd frontend && VITE_HARMONOGRAF_API=http://127.0.0.1:7532 pnpm devWith the server running, drive one real ADK invocation into Harmonograf:
make demo-presentationThat expands to:
HARMONOGRAF_SERVER=127.0.0.1:7531 uv run --extra e2e python -m presentation_agent.run_harmonograf \
--topic "Python programming" --server 127.0.0.1:7531
Override the topic or server address on the command line:
make demo-presentation TOPIC="Rust memory model" HARMONOGRAF_SERVER=127.0.0.1:7531The demo prints [harmonograf] session_id=<id> as soon as the server assigns one — open the frontend and that session should materialize on the Gantt view while the coordinator → research → web_developer → reviewer → (debugger) pipeline runs.
The presentation_agent coordinator drives five agents:
- coordinator_agent — orchestrates the flow and dispatches to each sub-agent via AgentTool.
- research_agent — gathers facts and notes about the user's topic.
- web_developer_agent — generates the presentation HTML/CSS/JS and saves them via
write_webpage. - reviewer_agent — reads the generated files with
read_presentation_filesand returns a structured critique (issues + severity). - debugger_agent — invoked only when
write_webpagefails or the reviewer flags critical issues; patches files in place withpatch_file.
presentation_agent defaults to gemini-2.5-flash (which routes through ADK's native Google models path and needs GOOGLE_API_KEY or ADC). To point it at a local OpenAI-compatible server (Ollama, vLLM, llama.cpp, LM Studio, anything that speaks /v1/chat/completions), set USER_MODEL_NAME to a LiteLLM provider-style identifier and export OPENAI_API_BASE:
export USER_MODEL_NAME="openai/qwen3.5:122b"
export OPENAI_API_BASE="http://localhost:8080/v1"
# OPENAI_API_KEY is optional for local endpoints; the demo target defaults
# it to "dummy" if unset so LiteLLM stops complaining.
make demotests/reference_agents/presentation_agent/agent.py detects provider-style strings (anything with a / before any :) and wraps them in google.adk.models.lite_llm.LiteLlm. Plain gemini-* names keep the native path and don't pull LiteLLM in. The demo / demo-presentation Makefile targets install LiteLLM via uv run --extra demo … — no extra steps required.
Both endpoints live on the gRPC-Web port (7532 by default) and are always unauthenticated so orchestrators can probe without credentials:
curl -sf http://127.0.0.1:7532/healthz # -> "ok"
curl -sf http://127.0.0.1:7532/readyz # -> "ready" (200) or "not ready" (503)/healthz returns 200 as long as the process is serving. /readyz additionally calls store.ping() and returns 503 if the backing store is not reachable.
Auth is off by default. To require a shared secret on every RPC (native gRPC and gRPC-Web), start the server with --auth-token:
cd server && uv run python -m harmonograf_server \
--store sqlite --data-dir ~/.harmonograf/data \
--auth-token "s3cret"Clients then pass the same token:
from harmonograf_client import Client
client = Client(
name="my-agent",
server_addr="127.0.0.1:7531",
framework="ADK",
token="s3cret",
)For the presentation_agent demo, set the token on the command line (the sample currently does not thread a token through — use an unauthenticated dev server, or run the demo script directly and add token=... to the Client(...) call in tests/reference_agents/presentation_agent/run_harmonograf.py).
/healthz and /readyz remain open regardless of --auth-token.
Logs go to stderr. --log-format text (the default) is the human-readable formatter; --log-format json emits one JSON record per line with a stable {ts, level, logger, msg} shape plus any extra={} fields.
Retention is opt-in. By default, terminal (COMPLETED / ABORTED) sessions are kept forever. To sweep them:
cd server && uv run python -m harmonograf_server \
--store sqlite --data-dir ~/.harmonograf/data \
--retention-hours 24 \
--retention-interval-seconds 300--retention-hours 0(default) disables the sweeper entirely.--retention-hours Ndeletes terminal sessions whoseended_at(orcreated_atif unset) is older thanNhours.--retention-interval-secondscontrols how often the sweeper wakes (default300). Live sessions are never touched.
Periodic metrics snapshots (sessions / spans / ingest rate / active streams) are emitted every --metrics-interval-seconds (default 30); set to 0 to disable.
Once the server, frontend, and an agent are up:
- The Sessions picker auto-selects the newest live session. Post
lazy-Hello (harmonograf#85) there is one row per ADK session you drive
— no ghost rows per app-build. The session id pinned on the row is
whatever
adk web/ your orchestrator passed through asgoldfive.Session.id(harmonograf#66). - The Activity (Gantt) view renders one row per ADK agent in the
wrapped tree. Rows auto-register on the first span each agent emits —
coordinator, specialists, and any
AgentTool/ sequential / parallel / loop wrappers (harmonograf#74 / #80). - The Trajectory view (↪ in the nav rail) is the operator's primary handle — one horizontal ribbon with one marker per intervention event.
Every intervention path lives in one of three places:
- Inspector drawer → Control tab. Pick any span in Activity, open the
drawer, and use the Control tab to send PAUSE, RESUME, STEER (free-form
text), or CANCEL. The client's
ControlBridgevalidates the STEER body (empty / over 8 KiB UTF-8 rejected, ASCII control chars stripped) before forwarding; the server stampsauthor+annotation_id(harmonograf#72). - Keyboard shortcut (Space). Toggles global PAUSE / RESUME in the Gantt view.
- Annotation with deliver-to-agent. Posting an annotation on a span / task with the "deliver to agent" box checked synthesizes a STEER targeting the annotated actor.
Every successful STEER / CANCEL / PAUSE you issue becomes one marker in Trajectory. If the same target sees a second user-control intervention within 5 minutes, its card merges with the previous one so the timeline doesn't spray out duplicates (harmonograf#81 / #87 closing prior #75 / #73 / #86).
Clicking a marker in Trajectory opens the intervention popover. Each card carries:
- Source —
user(you, via the UI) vs. autonomous (goldfive's steerer). Colour and marker glyph differ by source. - Kind — STEER, CANCEL, PAUSE, RESUME for user-control;
tool_error,agent_refusal,new_work_discovered,task_failed_recoverable, … for autonomous drift. Glyphs differ by kind. - Severity — info / warn / critical, rendered as an outer ring on the marker.
- Body — the STEER text or drift detail.
- Author — who issued the intervention (usually
operatorfor user control). - Outcome — if the intervention was immediately followed (within ~5 s)
by a plan revision or cascade-cancel, the card links to it
(
plan_revised:rN,cascade_cancel:N_tasks). Seeserver/harmonograf_server/interventions.pyfor the join logic.
If markers look clustered or the timestamps feel off, see harmonograf#87 — convertAnnotation now stores session-relative ms, which is the shape the density clusterer expects.
- In-memory vs. sqlite.
make server-runuses--store sqliteagainst<repo>/data.--store memoryis fine for tests but loses everything on restart. - sonora CORS shim. The server monkey-patches
sonora.asgiat startup to work around a bytes/str comparison bug in sonora's preflight path (seeserver/harmonograf_server/_sonora_shim.py, commit5c00817). AnINFOline is logged on startup confirming the shim is active. If you upgrade sonora and the preflight path is fixed upstream, the shim can be removed. - No TLS. The gRPC and gRPC-Web listeners are plaintext. Keep them on loopback;
--hostvalues other than127.0.0.1log a warning. - Frontend does not currently send a bearer token. If you start the server with
--auth-token, the UI will be rejected with 401 until frontend auth lands. Run the UI against an unauthenticated dev server for now. - Single-process, single-node. There is no clustering, no replication, no horizontal scale. One server process owns the canonical timeline.
- Gantt viewport on completed sessions currently opens past the last span rather than framed on it; tracked as harmonograf#89 (in flight).