Skip to content

Commit 542c22b

Browse files
Click a node and it answers: the live graph grows an inspector (0.1.4)
Every node in the live view is now clickable: a click pins a card with the node's status, executions, tokens (live "so far" spend while running), recorded cost, duration, share of the run's tokens, each execution's window on the timeline, the full error text, and every edge in and out with its kind. The pin survives snapshot patches and clears when a replan drops the node; Escape, the close button or the graph background dismisses it. The exposure boundary did not move: state_delta contents still never reach a live byte — the panel renders only fields the viewmodel already shipped. Also ships the incident demo video (docs/media/grapharc-incident-demo.mp4 and .gif — one question in, a qwen3:8b-planned nine-node graph out, live in the browser) and a short clip of the inspector. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a7c781a commit 542c22b

14 files changed

Lines changed: 295 additions & 12 deletions

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ and "used to be true" — the two things a reader most needs kept apart.
77

88
Entries are newest-last within a release, matching the order they were written.
99

10-
## Unreleased
10+
## 0.1.4
1111

1212
- a run **stopped for overspending reported spending nothing**. Tokens were attributed from `end` events, and a node the budget interrupts emits `error` instead — so `grapharc metrics` answered `tokens: 0` for a run whose own enforcement message named the figure that stopped it (`max_tokens reached (51/5)`). The audit trail lost precisely the number the stop was about, and per-node attribution dropped the most expensive node in the run. Every `error` event is now stamped with what its node spent, exactly as `end` is, and both `summarize` and the cost report count it; sub-events inside a node remain a breakdown of its total rather than an addition, so the disjointness that kept `ends + orphans` from double-counting is unchanged, and `RunCost.tokens == RunMetrics.tokens` still holds.
1313
- the `.env` credential loader **walked up parent directories to `/`**, while the config layer next door refuses exactly that on principle — so the file that *spends money* was discovered more eagerly than the one that *constrains* a run. A run started in a scratch subdirectory picked up an `OPENROUTER_API_KEY` from any ancestor: a `.env` in `$HOME` billed every user's experiment on a shared box to that key, a demo checked out under a client project quietly used the client's key, and since `redact()` is the only thing that ever prints a key, nothing in normal operation said *which file paid*. The rationale `cli/config.py` wrote down for `grapharc.toml` — "a run must never be silently governed by a file in a directory you didn't know about" — applies with more force to the file that pays than to the file that restrains, so `find_env_file` now reads the start directory (default: the working directory) and no ancestor of it. **This is a behaviour change:** anyone relying on a parent-directory `.env` must move it into the directory they run from, `export` the variable, or pass `env_file=` naming the file. Neither escape hatch moved — a real environment variable still beats any file, and an explicit `env_file=` still reads a file anywhere on disk — and no "search boundary" was added in place of the walk, because stopping at a git root is still an upward search.
@@ -23,6 +23,7 @@ Entries are newest-last within a release, matching the order they were written.
2323
- **the CLI never joined the runs it starts to the live view that draws them.** `plan`'s default trace went to a tempdir no server serves, and no command printed a URL. Defaults compose now: traces land under `.grapharc/runs/<stamp>/`, `serve --live-root` writes a discovery marker (`.grapharc/live-server.json` — URL, root, pid, never the token; removed on clean shutdown), and `plan`/`go` end with a `watch :` line — the exact page URL when a marker names a server that answers one loopback connect, the command that would start one otherwise. The goal now rides the loop's topology and approval events (operator-supplied text, deliberately shown — the second state field after `termination_reason`), so the page can say what a graph is *for*; a parked run shows its proposed nodes in violet with a copy-ready `grapharc approve <dir>` banner.
2424
- **`plan` planned nothing and executed everything** — the name lied. The verbs are split now: `grapharc plan` proposes, the gate admits, and the run STOPS with the admitted plan saved to `plan.json` next to its trace (exit 0, `stopped: planned`); `grapharc go` executes the newest saved plan (`go <run-dir>` for a specific one), replaying the stored proposal through the full governed loop so admission judges it again on the way in — a hand-edited plan.json is a new proposal, not a pre-approved one; `plan --go` (and `go "a goal"`) does both in one run. Looking at a plan and then typing `go` *is* the approval; `--approve` remains for parking one-shot runs mid-flight. Registry resolution is now one visible chain shared by both commands: flag/config first, else a `registry.py` in the directory (yours wins), else the built-in general-purpose kinds — with `--default` forcing the built-ins past everything.
2525
- **new commands for the first hour**: bare `grapharc` orients instead of erroring (exit 0); `grapharc start` is the guided tour; `grapharc init` scaffolds a commented `registry.py` (whose first free run reproduces refuse-then-admit), a `grapharc.toml`, and `.grapharc/runs/` — refusing to overwrite either authored file, with no `--force`; `grapharc go` is plan with doing-defaults (the stdlib tool-using registry, `--model` required); `--registry path/to/file.py:attr` loads a registry file directly; `--workspace` confines the stdlib kinds' tools to a directory (refused when a registry cannot take one — never silently un-confined); `--model-arg KEY=VALUE` reaches the backend constructor. The stdlib planner is now *told* the deterministic completion rule (end with `summarize`) instead of discovering it by burning rounds.
26+
- the live graph's nodes are **clickable now**. A click pins an inspector card to the node with everything the snapshot already carries about it: status, executions, tokens (a running node shows its live "so far" spend), recorded cost, duration, its share of the whole run's tokens, each execution's window on the run's timeline, the full error text, and every edge in and out with its kind. The pin survives snapshot patches — a running node's spend ticks in place — and clears itself when a replan produces a topology without that node; Escape, the × or a click on the graph background closes it. The exposure boundary did not move: `state_delta` contents still never reach a live byte — the panel renders only fields the viewmodel already shipped, and the test pinning that sentence is unchanged.
2627

2728
## 0.1.3
2829

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ A planner *proposes* a subgraph, a deterministic checker *admits* it — or refu
2121
- **The worst-case cost is known before a graph runs, and the exact per-node bill after — even when it fails.** Admission prices the worst case against what is *left* of the budget; at runtime each node's spend lands on its own trace event, error and cancellation included. `recorded_cost_usd` is never an estimate.
2222
- **One append-only JSONL trace file is the whole truth.** `replay`, `diff`, `metrics`, `viz`, cost attribution, OTel export and the live browser view all read the same file — the dashboard cannot disagree with the audit trail, because they are the same record.
2323

24-
**Website:** [codegraphcontext.github.io/GraphARC](https://codegraphcontext.github.io/GraphARC/) · **Status:** early days (`0.1.3`) — the API is not stable yet. Known limits are listed in [Status and limits](#status-and-limits); closed ones are in [CHANGELOG.md](CHANGELOG.md).
24+
**Website:** [codegraphcontext.github.io/GraphARC](https://codegraphcontext.github.io/GraphARC/) · **Status:** early days (`0.1.4`) — the API is not stable yet. Known limits are listed in [Status and limits](#status-and-limits); closed ones are in [CHANGELOG.md](CHANGELOG.md).
2525

2626
![One English question is decomposed by a local model into a nine-node graph — four parallel evidence pulls fanning out of START, a correlate join, a hypothesis fork, and a final report — shown live in the browser: the proposed graph waits grey for human approval, then each node turns amber while it runs and green when it is done.](docs/media/grapharc-decompose.gif)
2727

docs/cookbook/01-basics.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ projection instead and says which fields it dropped.
1414
`tests/test_cookbook_basics.py` reproduces every recipe here and asserts these
1515
exact strings, so the page cannot rot quietly.
1616

17-
Verified against `grapharc 0.1.3`, Python 3.14.6, `langgraph 1.2.9`,
17+
Verified against `grapharc 0.1.4`, Python 3.14.6, `langgraph 1.2.9`,
1818
`langchain-core 1.5.1`, `pydantic 2.13.4`.
1919

2020
Each snippet is a complete file. Save it and run it; nothing carries over between
@@ -41,7 +41,7 @@ uv run grapharc --version
4141
Output:
4242

4343
```
44-
grapharc 0.1.3
44+
grapharc 0.1.4
4545
```
4646

4747
Everything below uses only the base install — no API key, no network, no optional

docs/cookbook/06-serving-and-ops.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,7 +1062,7 @@ with TestClient(app) as client:
10621062
```
10631063

10641064
```
1065-
health : {'status': 'ok', 'version': '0.1.3', 'graphs': ['qa']}
1065+
health : {'status': 'ok', 'version': '0.1.4', 'graphs': ['qa']}
10661066
created: 201 queued
10671067
status : succeeded
10681068
answer : Budgets cap iterations, tokens and time.
@@ -1259,7 +1259,7 @@ graphs : qa
12591259
ctrl-c to stop
12601260

12611261
$ curl -s localhost:8124/healthz
1262-
{"status":"ok","version":"0.1.3","graphs":["qa"]}
1262+
{"status":"ok","version":"0.1.4","graphs":["qa"]}
12631263

12641264
$ curl -s -X POST localhost:8124/sessions -H 'content-type: application/json' \
12651265
-d '{"graph":"qa","input":{"question":"how do budgets work?"}}'
3.19 MB
Loading
890 KB
Binary file not shown.
433 KB
Binary file not shown.

grapharc/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from grapharc.runtime.graph import GraphARC, WritePermissionError
1010
from grapharc.runtime.state import GraphARCState
1111

12-
__version__ = "0.1.3"
12+
__version__ = "0.1.4"
1313

1414
__all__ = [
1515
"GraphARC",

grapharc/server/static/view.css

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,16 @@ svg { display: block; margin: 0 auto; }
188188
50% { opacity: 0.55; }
189189
}
190190

191+
.node { cursor: pointer; }
192+
.node:hover rect { stroke-width: 2.5; }
193+
.node.sel rect { stroke-width: 2.5; }
194+
.node.sel .halo {
195+
opacity: 0.45;
196+
stroke: var(--g-border-hi);
197+
animation: none;
198+
}
199+
.node.sel.st-running .halo { stroke: var(--g-status-running); }
200+
191201
.term circle { fill: var(--g-surface); stroke: var(--g-text-3); stroke-width: 1.5; }
192202
.term text {
193203
font-size: 9px;
@@ -228,6 +238,115 @@ svg { display: block; margin: 0 auto; }
228238
letter-spacing: 0.04em;
229239
}
230240

241+
/* --- node inspector ------------------------------------------------------- */
242+
243+
#inspector {
244+
position: fixed;
245+
top: 4.5rem;
246+
right: 1.25rem;
247+
width: 310px;
248+
max-height: calc(100vh - 6rem);
249+
overflow: auto;
250+
background: var(--g-card);
251+
border: 1px solid var(--g-border-hi);
252+
border-radius: 12px;
253+
padding: 0.8rem 1rem 1rem;
254+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
255+
z-index: 5;
256+
}
257+
#inspector[hidden] { display: none; }
258+
#inspector .ihead {
259+
display: flex;
260+
align-items: baseline;
261+
gap: 0.5rem;
262+
}
263+
#inspector .ihead b {
264+
flex: 1;
265+
font-size: 1rem;
266+
overflow-wrap: anywhere;
267+
}
268+
#i-close {
269+
font: inherit;
270+
font-size: 1rem;
271+
line-height: 1;
272+
background: none;
273+
color: var(--g-text-2);
274+
border: none;
275+
padding: 0.1rem 0.3rem;
276+
cursor: pointer;
277+
border-radius: 6px;
278+
}
279+
#i-close:hover { color: var(--g-text); background: var(--g-surface); }
280+
#i-status {
281+
font-size: 0.8rem;
282+
font-weight: 600;
283+
margin: 0.1rem 0 0.4rem;
284+
}
285+
#i-status.st-pending, #i-status.st-proposed { color: var(--g-text-2); }
286+
#i-status.st-proposed { color: var(--g-status-approval); }
287+
#i-status.st-running { color: var(--g-status-running); }
288+
#i-status.st-done { color: var(--g-status-done); }
289+
#i-status.st-errored { color: var(--g-status-errored); }
290+
#i-props {
291+
display: grid;
292+
grid-template-columns: auto 1fr;
293+
gap: 0.15rem 0.9rem;
294+
margin: 0 0 0.4rem;
295+
font-size: 0.85rem;
296+
}
297+
#i-props dt {
298+
color: var(--g-text-2);
299+
text-transform: uppercase;
300+
font-size: 0.68rem;
301+
letter-spacing: 0.04em;
302+
align-self: baseline;
303+
padding-top: 0.15rem;
304+
}
305+
#i-props dd {
306+
margin: 0;
307+
font-family: var(--g-font-mono);
308+
font-variant-numeric: tabular-nums;
309+
overflow-wrap: anywhere;
310+
}
311+
#i-error {
312+
margin: 0.3rem 0 0.5rem;
313+
color: var(--g-status-errored);
314+
font-size: 0.82rem;
315+
overflow-wrap: anywhere;
316+
}
317+
#i-error[hidden] { display: none; }
318+
#inspector h3 {
319+
font-size: 0.68rem;
320+
text-transform: uppercase;
321+
letter-spacing: 0.04em;
322+
color: var(--g-text-2);
323+
margin: 0.7rem 0 0.25rem;
324+
font-weight: 600;
325+
}
326+
#inspector ul {
327+
list-style: none;
328+
margin: 0;
329+
padding: 0;
330+
font-size: 0.8rem;
331+
font-family: var(--g-font-mono);
332+
font-variant-numeric: tabular-nums;
333+
}
334+
#inspector ul li { padding: 0.1rem 0; color: var(--g-text-2); }
335+
#inspector ul li .ok { color: var(--g-status-done); }
336+
#inspector ul li .bad { color: var(--g-status-errored); }
337+
#inspector ul li .open { color: var(--g-status-running); }
338+
#inspector ul li .peer { color: var(--g-text); }
339+
340+
@media (max-width: 1000px) {
341+
#inspector {
342+
position: static;
343+
width: auto;
344+
max-height: none;
345+
margin-top: 1rem;
346+
box-shadow: none;
347+
}
348+
}
349+
231350
/* --- approval banner ------------------------------------------------------ */
232351

233352
#approvalbar {

grapharc/server/static/view.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@
2828
<div class="tile" id="t-events" hidden><b></b><span>events</span></div>
2929
</div>
3030
<div id="panel"><div class="empty">waiting for the run to start…</div></div>
31+
<aside id="inspector" hidden>
32+
<div class="ihead">
33+
<b id="i-name"></b>
34+
<button id="i-close" type="button" aria-label="close">×</button>
35+
</div>
36+
<div id="i-status"></div>
37+
<dl id="i-props"></dl>
38+
<p id="i-error" hidden></p>
39+
<div id="i-spans"></div>
40+
<div id="i-edges"></div>
41+
</aside>
3142
<div id="playback" hidden>
3243
<button id="play" type="button">▶ replay</button>
3344
<select id="speed">

0 commit comments

Comments
 (0)