The status page's walkthrough (M6.6b)
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 2m41s
CI / e2e (pull_request) Successful in 4m25s

A new package, nisse-ui: a read-only page for studying how the agents
work. For each work item it shows the steps (what people and the
orchestrator said, and each run), each run's turns, and each turn's
request read against the log it was made from:
- each message beside the events it came from, tagged unchanged,
  labelled, narrated, shortened (by how much) or a compaction, with what
  fitting dropped;
- what is new since the turn before, and where the request first
  differs (where prompt reuse had to stop), next to the server's count;
- the reply with its reasoning, and the raw JSON.

Runs going in circles are flagged by nisse_core.loop.circles (repeated
near-identical replies, one correction given again and again, repeated
apologies), a pure function M7.8 can reuse in Driver.run.

In nisse-core:
- render.py: message_indexes(), from the same code render() uses, so
  the page's matching can't drift from what was sent.
- calls.py: the record notes when recording began; the reader gains
  calls_for(item) and call(id).
- state/readonly.py: opens an agent's SQLite file without creating its
  -wal/-shm files (mode=ro alone does, once the writer has closed).

The page reads only through read-only connections, escapes everything,
and sends a Content Security Policy that allows no script. It listens on
127.0.0.1; until M6.6d puts it behind Authelia, reach it over an SSH
tunnel. Jinja2 is its one new dependency, for autoescaping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-25 03:36:50 +00:00
1 parent c1f3d9dafb
commit a8e2023183
31 files changed
+2550 -15

No files matched your search

+230
View File
@@ -0,0 +1,230 @@
"""The page's routes, as a plain function from a path to a response, so the
tests drive it without a socket (X6).
/ the sources: agents and kept runs
/s/<source>/ a source's work items
/s/<source>/item/<id> an item's walkthrough ("-": the agent-level log)
/s/<source>/call/<n> one turn, read against its log
/s/<source>/event/<n> one log event in full
"""
import json
import sqlite3
from dataclasses import dataclass, field
from importlib import resources
from typing import Any
from urllib.parse import quote, unquote, urlsplit
from jinja2 import Environment, PackageLoader, StrictUndefined
from nisse_core.models import calls
from nisse_ui import walk
from nisse_ui.sources import Source, StateReader, UnknownSchema, call_record
AGENT_LEVEL = "-" # the item id that stands for the agent-level log
# Everything shown is untrusted text (L11): escaped by the templates, and
# the browser runs no script at all, loads nothing from elsewhere, and
# sends no referrer.
HEADERS = {
"Content-Security-Policy": (
"default-src 'none'; style-src 'self'; img-src 'self'; base-uri 'none'; "
"form-action 'none'; frame-ancestors 'none'"
),
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
"Cache-Control": "no-store",
}
@dataclass(frozen=True)
class Response:
status: int
body: bytes
content_type: str = "text/html; charset=utf-8"
headers: dict[str, str] = field(default_factory=lambda: dict(HEADERS))
def _when(ts: Any) -> str:
return str(ts)[:19].replace("T", " ") if ts else "—"
def _num(n: Any) -> str:
return f"{n:,}" if isinstance(n, int) and not isinstance(n, bool) else "—"
def _pretty(text: Any) -> str:
"""JSON text indented, anything else as it is."""
if not isinstance(text, str):
return json.dumps(text, indent=2, ensure_ascii=False)
try:
return json.dumps(json.loads(text), indent=2, ensure_ascii=False)
except ValueError:
return text
def _clip(text: Any, limit: int = 300) -> str:
text = str(text or "")
return (
text
if len(text) <= limit
else text[:limit] + f" … [{len(text) - limit:,} more]"
)
def summary(kind: str, payload: dict[str, Any]) -> str:
"""One event as a line of text: what it said or did."""
match kind:
case "user_message" | "assistant_text" | "fact":
return str(payload.get("text", ""))
case "tool_call":
args = json.dumps(payload.get("arguments", {}), ensure_ascii=False)
return f"{payload.get('tool', '?')}({args})"
case "tool_result":
mark = "ok" if payload.get("success") else "FAILED"
return f"{mark}: {payload.get('result', '')}"
case "plan_submitted":
return str(payload.get("plan", ""))
case "gate_opened":
return f"{payload.get('gate', '')}: {payload.get('reason', '')}"
case "delegation_started":
return f"({payload.get('model', '')}) {payload.get('brief', '')}"
case "delegation_completed":
return f"[{payload.get('exit_status', '')}] {payload.get('handback', '')}"
case "compaction":
first, last = (payload.get("replaces") or ["?", "?"])[:2]
return f"replaces {first}–{last}: {payload.get('summary', '')}"
case "action_verdict":
return (
f"{payload.get('tool', '')}: {payload.get('effect', '')} → "
f"{payload.get('decision', '')} ({payload.get('reason', '')})"
)
case _:
return json.dumps(payload, ensure_ascii=False)
def _reply_line(reply: walk.Reply) -> str:
calls_made = "; ".join(f"{tool}({args})" for tool, args in reply.calls)
text = " ".join(reply.text.split())
return " · ".join(p for p in (text, calls_made) if p) or "(no reply)"
class App:
def __init__(self, sources: list[Source]) -> None:
self.sources = list(sources)
self.by_name = {s.name: s for s in sources}
env = Environment(
loader=PackageLoader("nisse_ui", "templates"),
autoescape=True, # always: every page shows untrusted text
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
env.filters.update(
u=lambda s: quote(str(s), safe=""),
when=_when,
num=_num,
pretty=_pretty,
clip=_clip,
reply_line=_reply_line,
)
env.globals.update(summary=summary, AGENT_LEVEL=AGENT_LEVEL)
self.env = env
def page(self, template: str, status: int = 200, **values: Any) -> Response:
html = self.env.get_template(template).render(**values)
return Response(status, html.encode())
def problem(self, status: int, title: str, detail: str) -> Response:
return self.page("problem.html", status, title=title, detail=detail)
def handle(self, raw_path: str) -> Response:
path = urlsplit(raw_path).path
parts = [unquote(p) for p in path.split("/")[1:]]
if parts in ([], [""]):
return self.home()
if parts == ["static", "style.css"]:
css = resources.files("nisse_ui").joinpath("static/style.css").read_bytes()
return Response(200, css, "text/css; charset=utf-8")
if len(parts) >= 2 and parts[0] == "s":
source = self.by_name.get(parts[1])
if source is None:
return self.problem(404, "No such source", parts[1])
rest = parts[2:]
try:
if rest in ([], [""]):
return self.items(source)
if len(rest) == 2 and rest[0] == "item":
item = None if rest[1] == AGENT_LEVEL else rest[1]
return self.item(source, item)
if (
len(rest) == 2
and rest[0] in ("call", "event")
and rest[1].isdigit()
):
if rest[0] == "call":
return self.call(source, int(rest[1]))
return self.event(source, int(rest[1]))
except UnknownSchema as exc:
return self.problem(
409, "A state store this page doesn't know", str(exc)
)
except calls.UnknownLayout as exc:
return self.problem(
409, "A call record this page doesn't know", str(exc)
)
except sqlite3.Error as exc:
return self.problem(503, "Couldn't read the store", str(exc))
return self.problem(404, "Not found", path)
# --- pages ---------------------------------------------------------------------
def home(self) -> Response:
rows = []
for source in self.sources:
row: dict[str, Any] = {"source": source, "count": None, "note": ""}
try:
with StateReader(source.state_db) as state:
row["count"] = state.count_items()
row["schema"] = state.schema_version
except (UnknownSchema, sqlite3.Error) as exc:
row["note"] = str(exc)
row["record"] = source.calls_db.is_file()
rows.append(row)
return self.page("sources.html", rows=rows)
def items(self, source: Source) -> Response:
with StateReader(source.state_db) as state:
return self.page(
"items.html",
source=source,
items=state.items(),
active=state.active_item(),
)
def item(self, source: Source, item_id: str | None) -> Response:
with StateReader(source.state_db) as state:
if item_id is not None and state.item(item_id) is None:
return self.problem(404, "No such work item", item_id)
try:
with call_record(source) as reader:
view = walk.walkthrough(state, reader, item_id)
except calls.UnknownLayout as exc:
view = walk.walkthrough(state, None, item_id, record_note=str(exc))
return self.page("item.html", source=source, view=view)
def call(self, source: Source, call_id: int) -> Response:
with StateReader(source.state_db) as state, call_record(source) as reader:
if reader is None:
return self.problem(404, "No call record", str(source.calls_db))
view = walk.turn(state, reader, call_id)
if view is None:
return self.problem(404, "No such call", str(call_id))
return self.page("call.html", source=source, view=view)
def event(self, source: Source, event_id: int) -> Response:
with StateReader(source.state_db) as state:
found = state.event(event_id)
if found is None:
return self.problem(404, "No such event", str(event_id))
return self.page("event.html", source=source, event=found)