This file provides guidance to agents — Claude Code, Antigravity, Opencode, and anyone else — working with code in this repository. Treat it as the durable project charter.
Harmonograf is the observability + HCI companion to
goldfive for multi-agent
orchestration. Goldfive is optional for read-only observation:
harmonograf works for any agent that emits spans via
harmonograf_client.Client or attaches a HarmonografTelemetryPlugin to a
bare ADK App. Plans, tasks, drift, and the full intervention surface light
up when the run goes through goldfive.wrap(...). See
docs/standalone-observability.md for
the no-goldfive path and
docs/goldfive-integration.md for the
full-orchestration path.
When goldfive is in the picture: goldfive owns the plan, the task state machine, the drift taxonomy, the reporting tools, and the re-invocation loop. Harmonograf owns the session, the span timeline, the canonical storage, the frontend (Gantt / Graph / Trajectory / Notes / Settings), the intervention aggregator, and the control router that lets a human intervene on the same connection they observe on.
The line is load-bearing: orchestration changes belong in goldfive; observability and UI changes belong in harmonograf. If a change has to straddle both, the goldfive-side change lands first and harmonograf follows.
Phase A of the goldfive migration (issue #6, COMPLETED) moved plan/task/drift
types into goldfive's proto package. Harmonograf's TelemetryUp.goldfive_event
therefore references goldfive.v1.Event directly, and the generated pb stubs
import from goldfive.pb.goldfive.v1. Consequence: harmonograf_client
cannot be installed without goldfive present, but user code can (and
should) be goldfive-import-free in the standalone case. The
standalone-test CI job enforces this via
grep -i goldfive examples/standalone_observability/spans_only.py.
harmonograf/
server/harmonograf_server/ # gRPC server (asyncio, grpcio + sonora)
ingest.py # telemetry ingest pipeline
bus.py # per-session fan-out bus
control_router.py # frontend → agent control routing
interventions.py # intervention history aggregator (#69/#71)
rpc/ # RPC servicer impls (telemetry, control, frontend)
storage/ # SQLite + in-memory stores
pb/ # generated Python pb stubs (from proto/)
client/harmonograf_client/ # Python client library
client.py # top-level handle + identity/heartbeat
transport.py # buffered bidi gRPC transport (lazy Hello)
buffer.py # ring buffer + envelope kinds
sink.py # goldfive.EventSink adapter
observe.py # observe(runner) helper (issue #22)
telemetry_plugin.py # ADK BasePlugin for lifecycle spans (#74/#80)
_control_bridge.py # control-stream ↔ goldfive.ControlChannel bridge
pb/ # generated Python pb stubs
frontend/ # React + Vite + TypeScript
src/components/shell/views/ # ActivityView, GraphView, TrajectoryView, NotesView, SettingsView
src/components/Interventions/ # intervention timeline strip (#76)
src/components/Gantt/ # gantt chrome (minimap, legend, ctx badges)
src/gantt/ # canvas renderer, layout, drift/span kinds
src/state/ # Zustand stores (sessions, popover, ui, annotations)
src/rpc/ # connect-rpc client + sessions syncer
src/pb/ # generated TS pb stubs
proto/harmonograf/v1/ # harmonograf proto (imports goldfive/v1/*)
docs/ # durable, tree-based documentation
tests/
e2e/ # end-to-end tests
reference_agents/ # demo/reference ADK agents
examples/standalone_observability/ # non-goldfive examples
.agents/skills/ # structured skill bundles for AI coders
third_party/ # adk-python clone lives here (git-ignored)
Makefile # make install / demo / test / proto
Three components, and changes usually span more than one of them:
-
Visual frontend (
frontend/) — a six-view shell (Sessions, Activity, Graph, Trajectory, Notes, Settings). Activity is the Gantt: X-axis is time, Y-axis is one row per ADK agent (auto-registered from span attrs per harmonograf#74/#80), each block is an agent activity (span, tool call, transfer). Trajectory is the intervention-history ribbon (#69/#76). Plan, task, and drift state surfaces are rendered from goldfive events (PlanSubmitted,PlanRevised,TaskStarted,TaskCompleted,TaskFailed,DriftDetected,AgentInvocationStarted,DelegationObserved, …) delivered through the harmonograf server. -
Client library (
client/) — embedded inside agent processes. Public surfaces:Client— span transport, payload upload, control-handler registration, identity, heartbeat; lazy Hello (harmonograf#85) so no ghost session is created until the first real emit.HarmonografSink—goldfive.EventSinkthat translates goldfiveEventenvelopes intoTelemetryUp.goldfive_eventframes.HarmonografTelemetryPlugin— ADKBasePluginthat emits spans for lifecycle callbacks and stacks per-ADK-agent ids onto each span so the server can auto-register one Gantt row per agent in the wrapped tree. Dedups itself silently if installed twice (#68).observe(runner)— one-line helper that attaches the sink + optional control bridge to an existinggoldfive.Runner. See issue #22.
-
Server process (
server/) — terminates gRPC connections from every client and gRPC-Web connections from every frontend. Owns the canonical timeline (sessions, agents, spans, annotations, payloads) and the derived plan / task / drift index built from goldfive events. Aggregates the intervention history view across annotations + drift ring + plan-revision metadata (interventions.py,ListInterventionsRPC, #69/#71). Merges duplicate user-control interventions inside a 5-minute window (#81, #87). Routes control events from the frontend to the correct per-agent subscribers.
Key cross-cutting concerns to keep in mind when designing any piece:
- The data model for observability (session, agent, span, payload,
annotation, control event, intervention) is defined once in
proto/harmonograf/v1/*.protoand shared across all three components. - Plan, task, drift, and reporting-tool types are defined once in
proto/goldfive/v1/*.protoand imported into harmonograf's proto tree. Do not re-declare them in harmonograf. - The frontend is not read-only — interactions flow back through the server to clients, so the client library needs a bidirectional channel, not just telemetry egress.
- "Coordinating" implies the server mediates control (pause, resume, steer, cancel), not just display — design client APIs with that in mind rather than treating it purely as an observability tool.
- Sessions unify the ADK session id (what the user sees in
adk web) with harmonograf's session id. Goldfive events and spans route byctx.session.id/ per-eventsession_idrespectively, so one outer session gathers everything the user drove (harmonograf#63/#66).
Harmonograf's protocol carries goldfive events as a first-class variant:
proto/harmonograf/v1/telemetry.protoimportsgoldfive/v1/events.protoand declares agoldfive.v1.Event goldfive_eventfield insideTelemetryUp. Plan and task state ride that variant; the old harmonograf-nativetask_plan/task_status_updateenvelopes are retired.proto/harmonograf/v1/types.protoimports goldfive'sPlan,Task,TaskEdge,TaskStatus, andDriftKindrather than re-declaring them. Harmonograf-owned types (Session, Agent, Span, Payload, Annotation, Control*) stay local.client/harmonograf_client/sink.py(HarmonografSink) is the adapter:emit(event)pushes aGOLDFIVE_EVENTenvelope through the existing ring-buffered transport, which serialises it to aTelemetryUp(goldfive_event=...)frame.server/harmonograf_server/ingest.pydispatches onevent.payload(oneof) and updates the plan/task index, storage, and the session bus so frontend subscribers see deltas.
Orchestration semantics — session-state keys, reporting tool bodies, drift taxonomy, refine pipeline, invariant validator, parallel DAG walker — are goldfive concerns. When a question is "why does the agent state machine behave this way?", the answer lives in the goldfive repo. When a question is "why did the timeline render this span where it did?" or "why did this STEER dedupe with that drift?", the answer lives here.
- Span emission is harmonograf's job. ADK callbacks (BEFORE/AFTER
model/tool/run/agent, state_delta, transfer, escalate) are turned into
spans by
HarmonografTelemetryPlugin. Goldfive does not produce spans. - Per-ADK-agent attribution is harmonograf's job. The plugin stacks
per_agent_id = "{client_id}:{adk_name}"via before/after_agent callbacks and rides agent hints (name, parent, kind, branch, framework) on the first span each agent emits. The server's_ensure_routeauto-registers agents from those hints (harmonograf#74/#80). - Task state is goldfive's job. State transitions happen inside
goldfive's
DefaultSteererwhen a reporting tool is called or when an ADK event implies a transition. Harmonograf learns about those transitions only through goldfive events on the sink. - Control routing is harmonograf's job. The frontend → server → agent
path (pause, resume, steer, cancel, annotate) rides harmonograf's control
stream. Goldfive's
ControlChannelexposes hooks that let an orchestrated run react to delivered control events, but the wire is harmonograf's.ControlBridgevalidates STEER body (empty / >8 KiB UTF-8 rejected, ASCII control chars stripped) pre-forward; the server stampsSteerPayload.author+SteerPayload.annotation_idbefore delivery (harmonograf#72). - Session is harmonograf's job. Goldfive has a
run_id; harmonograf pairs it with asession_idat the storage layer. Under ADK, the outeradk-websession id is pinned ongoldfive.Session.id, so one goldfive run + its outer ADK chat lane collapse onto one harmonograf session (harmonograf#63/#66). - Intervention aggregation is harmonograf's job.
interventions.pyderives the chronological list from annotations + ingest drift ring +task_plans.revision_kind, with a 5-minute merge window for user-control kinds and a tight window for autonomous drift (#81/#87). TheListInterventions(session_id)unary RPC returns it; the frontend has a live deriver that mirrors the shape for in-flight deltas.
If you see these in docs or code, they are retired — do not bring them back:
HarmonografAgent,HarmonografRunner,attach_adk,make_adk_plugin,make_harmonograf_agent,make_harmonograf_runner— replaced bygoldfive.Runner+HarmonografSink+observe()._AdkState,PlannerHelper,LLMPlanner,PassthroughPlanner— replaced by goldfive's steerer/planner.state_protocol.pywith theharmonograf.*session-state keys — replaced by goldfive'sSessionContext.invariants.py,metrics.pyin the client — invariant checking is goldfive's responsibility.TaskPlan/UpdatedTaskStatusenvelopes inTelemetryUp— replaced bygoldfive_event.- Duplicate
Task/TaskEdge/TaskStatus/DriftKindmessages intypes.proto— imported from goldfive instead.
The full inventory of what moved, what stayed, and what was deleted is in docs/goldfive-migration-plan.md.
From the repo root:
make test # server + client + frontend (pnpm build + lint)
make server-test # just server/tests/ (pytest, asyncio)
make client-test # just client/tests/
make frontend-test # pnpm build + pnpm lint
make e2e # end-to-end tests under tests/e2e/The proto stubs are checked in; only regenerate them (make proto) when
you edit proto/harmonograf/v1/*.proto. make proto resolves goldfive's
proto tree via the installed package (editable-dep-aware; no hardcoded
path).
- Branch names follow
kind/short-descriptionorkind/harmonograf-NN-slugwhen tied to an issue. Examples:docs/top-level-refresh,fix/harmonograf-89-gantt-viewport. - Commits, branches, and PR titles do not use a Claude / AI co-author trailer. The user scrubs them.
- Prefer small PRs. When a change straddles server + client + frontend, split by layer when feasible; otherwise call out the three-way coupling in the PR body.
- PR CI gates: server pytest, client pytest, frontend
pnpm build && pnpm lint,standalone-test(provesexamples/standalone_observability/ spans_only.pyhas nogoldfiveimports). - Self-merge after CI green unless the user requests review.
Under docs/:
tour/— 15-minute tour, mental model, terminology map, front-door index.user-guide/— UI reference by region (Gantt, Graph, Trajectory, drawer, control actions, annotations, keyboard shortcuts, etc.).dev-guide/— setup, architecture, per-component internals, testing, debugging, protos, contribution workflow.protocol/— wire reference (telemetry, control, frontend-RPC, span lifecycle, payload flow, wire-ordering).design/— per-component design notes (data model, client, server, frontend, HCI model, information flow).adr/— Architecture Decision Records ("we decided X because Y").runbooks/— triage for common failure modes.internals/— annotated hot-path tours.- Top-level:
quickstart.md,overview.md,operator-quickstart.md,standalone-observability.md,goldfive-integration.md,goldfive-migration-plan.md(completed),reporting-tools.md(redirect to goldfive),milestones.md,index.md.
For architectural decisions reach for docs/adr/ first; it lists why a
given shape exists, which is usually the thing you need before a refactor.
Structured skill bundles live under .agents/skills/, each one a focused
"how to do X" recipe with explicit file touchpoints. Categories include:
- Proto edits:
hgraf-add-proto-field,hgraf-add-proto-message,hgraf-bump-proto-version. - Taxonomy/vocabulary additions:
hgraf-add-drift-kind,hgraf-add-span-kind,hgraf-add-control-kind,hgraf-add-annotation-kind,hgraf-add-capability. - Frontend UX:
hgraf-add-drawer-tab,hgraf-add-gantt-overlay,hgraf-add-renderer-overlay,hgraf-add-keyboard-shortcut,hgraf-update-frontend-component. - Ops / debugging:
hgraf-debug-frontend-state,hgraf-debug-task-stuck,hgraf-profile-callback-perf,hgraf-tune-heartbeat,hgraf-interpret-invariant-violations,hgraf-run-demo. - Workflow:
hgraf-review-pr,hgraf-spawn-agent-team,hgraf-write-e2e-scenario,hgraf-read-memory-bank.
When you're about to do one of these things, load the matching skill first — they encode the exact list of files to touch.