The status page's walkthrough (M6.6b)
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:
1 parent
c1f3d9dafb
commit
a8e2023183
31 files changed
+2550
-15
No files matched your search
@@ -163,3 +163,47 @@ def test_roles_never_repeat_for_strict_templates() -> None:
|
||||
roles = _roles(OpenAIChatRenderer().render(events, tools=None, model="q"))
|
||||
for first, second in pairwise(roles):
|
||||
assert not (first == second and first in ("user", "assistant")), roles
|
||||
|
||||
|
||||
def test_each_event_maps_to_the_message_render_puts_it_in() -> None:
|
||||
# A turn's text and tool call fold into one message; a call with no
|
||||
# result gets a placeholder that came from no event; the system prompt
|
||||
# is message 0.
|
||||
events = stored(
|
||||
[
|
||||
UserMessage(text="go", source="m:1"),
|
||||
AssistantText(text="Looking.", model="m"),
|
||||
ToolCall(call_id="c1", tool="read_file", arguments={"path": "a"}),
|
||||
ToolResult(call_id="c1", result="text", success=True),
|
||||
ToolCall(call_id="c2", tool="finish", arguments={}),
|
||||
UserMessage(text="and then?", source="m:2"),
|
||||
]
|
||||
)
|
||||
renderer = OpenAIChatRenderer()
|
||||
body = renderer.render(events, tools=None, model="m", system="sys")
|
||||
indexes = renderer.message_indexes(events, system="sys")
|
||||
assert indexes == [1, 2, 2, 3, 4, 6]
|
||||
assert body["messages"][5]["content"] == "[no result recorded]"
|
||||
for event, index in zip(events, indexes, strict=True):
|
||||
message = body["messages"][index]
|
||||
if isinstance(event.event, ToolCall):
|
||||
ids = [c["id"] for c in message["tool_calls"]]
|
||||
assert event.event.call_id in ids
|
||||
elif isinstance(event.event, (UserMessage, AssistantText)):
|
||||
assert event.event.text in message["content"]
|
||||
assert renderer.message_indexes(events) == [i - 1 for i in indexes]
|
||||
|
||||
|
||||
def test_events_a_compaction_replaced_map_to_no_message() -> None:
|
||||
events = stored(
|
||||
[
|
||||
UserMessage(text="one", source="m:1"),
|
||||
AssistantText(text="two", model="m"),
|
||||
UserMessage(text="three", source="m:3"),
|
||||
Compaction(summary="one and two", model="m", replaces=[1, 2]),
|
||||
]
|
||||
)
|
||||
# The summary takes the place of what it replaced, and the user message
|
||||
# after it folds into the same user message.
|
||||
indexes = OpenAIChatRenderer().message_indexes(events)
|
||||
assert indexes == [None, None, 0, 0]
|
||||
Reference in new issue
Block a user