diff --git a/apps/website/content/docs/chat/components/chat-debug.mdx b/apps/website/content/docs/chat/components/chat-debug.mdx
index ca357cbfd..879eb7d44 100644
--- a/apps/website/content/docs/chat/components/chat-debug.mdx
+++ b/apps/website/content/docs/chat/components/chat-debug.mdx
@@ -22,19 +22,25 @@ Before the first turn the Timeline tab shows its empty state: "No checkpoints ye
## How it is built
-Three files carry the example: the graph that produces the checkpoints, the provider that points Angular at it, and the component that mounts the panel. Open the Code tab to read them in place.
+The example is three files: the graph that produces the checkpoints, the provider that points Angular at it, and the component that mounts the panel. Open the Code tab to read them in place.
### A graph with several nodes per turn
-The backend is deliberately multi-step, because one node per turn produces one checkpoint and very little to look at. `generate` answers with the model, `process` appends a synthetic message derived from the answer, and `summarize` asks the model for a one-sentence summary of the conversation so far. The system prompt read by `generate` frames the assistant as an aviation helper; the graph binds no tools, so every answer comes from the model alone.
+The backend is deliberately multi-step, because one node per turn produces one checkpoint and very little to look at. `generate` answers with the model, `process` measures that answer and records the result, and `summarize` asks the model for a one-sentence summary of the conversation so far. The system prompt read by `generate` frames the assistant as an aviation helper; the graph binds no tools, so every answer comes from the model alone.
Each node returns a partial state update, and each of those updates becomes a checkpoint on the thread.
+### State the inspector can show
+
+`agent.state()` is the LangGraph values bag with `messages` projected out into the transcript, so a graph that carries nothing but its messages leaves the State tab printing an empty object. This graph widens `MessagesState` with the metrics `process` computes, which is what gives the second tab something to inspect.
+
+
+
### Wiring the nodes into a linear pipeline
-The nodes are registered on a `StateGraph` over `MessagesState` and chained: `generate` to `process` to `summarize` to `generate_title`, then to the end. `generate_title` is a background node that summarizes the first user message into a thread title; it returns an empty update, so it changes the message list not at all while still adding a step to the run.
+The nodes are registered on a `StateGraph` over `DebugState` and chained: `generate` to `process` to `summarize` to `generate_title`, then to the end. `generate_title` is a background node that summarizes the first user message into a thread title; it returns an empty update, so it changes the message list not at all while still adding a step to the run.
diff --git a/cockpit/chat/debug/angular/e2e/c-debug.spec.ts b/cockpit/chat/debug/angular/e2e/c-debug.spec.ts
index 69662d4ed..857e624c0 100644
--- a/cockpit/chat/debug/angular/e2e/c-debug.spec.ts
+++ b/cockpit/chat/debug/angular/e2e/c-debug.spec.ts
@@ -88,12 +88,11 @@ test('c-debug: the State tab swaps in the live state inspector', async ({ page }
// The tab owns the panel body — the timeline is torn down, not stacked.
await expect(page.locator('chat-debug-checkpoint-card')).toHaveCount(0);
// `agent.state()` is the LangGraph values bag with `messages` projected out
- // into the transcript, so on this MessagesState graph the inspector renders
- // an empty object today. Assert the shape the JsonPipe produces rather than
- // that exact literal: the claim is that the inspector is mounted and bound
- // to the agent, and a graph that carries state beyond its messages should
- // widen this tab's coverage, not fail it.
- await expect(stateTab.locator('chat-debug-state-inspector pre')).toHaveText(
- /^\{[\s\S]*\}$/,
- );
+ // into the transcript. This graph's DebugState also carries `analysis`, the
+ // metrics the `process` node computes, so the inspector has real run state
+ // to print — that is what makes this tab worth opening.
+ const inspector = stateTab.locator('chat-debug-state-inspector pre');
+ await expect(inspector).toContainText('analysis');
+ await expect(inspector).toContainText('characters');
+ await expect(inspector).toContainText('words');
});
diff --git a/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json b/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json
index be0972183..33d8cd90e 100644
--- a/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json
+++ b/cockpit/chat/debug/angular/e2e/fixtures/c-debug.json
@@ -2,30 +2,381 @@
"fixtures": [
{
"match": {
- "systemMessage": "Aviation Assistant",
- "userMessage": "What is a jet bridge?"
+ "userMessage": "What is a jet bridge?",
+ "model": "gpt-5-mini",
+ "turnIndex": 0,
+ "hasToolResult": false
},
"response": {
- "content": "A jet bridge is the enclosed, movable walkway that connects an airport gate to an aircraft door, so passengers board without crossing the ramp."
- }
- },
- {
- "match": {
- "systemMessage": "brief one-sentence summary",
- "userMessage": "What is a jet bridge?"
+ "content": "A jet bridge is an enclosed, movable connector that links an airport terminal gate directly to an aircraft door so passengers can board and disembark without going outdoors. It\u2019s sometimes called a jetway, airbridge, passenger boarding bridge (PBB) or boarding bridge.\n\nKey points\n- Construction: usually a telescoping, pivoting metal-and-glass tube mounted on wheels or a rotatable plaza so it can extend, retract, raise and lower to match different aircraft doors and heights.\n- Operation: a trained operator maneuvers the bridge from the terminal end or a control panel, aligns it with the aircraft door, and seals the interface with a flexible canopy or curtain.\n- Benefits: protects passengers from weather, improves security and accessibility (no stairs), speeds boarding and deplaning, and reduces ground handling complexity.\n- Variations: single bridges for most jets; dual or double bridges (one at upper deck, one at main deck) for very large aircraft like the A380; some gates lack jet bridges and use mobile stairs or buses to remote stands.\n- Limitations: not all airports or remote stands have them; very large or unusual door positions may require special bridges; improper docking can damage aircraft so precise operation and safety checks are required.\n\nIn short: a jet bridge is the enclosed walkway that provides a dry, secure, and accessible path between the terminal and an airplane."
},
- "response": {
- "content": "The traveler asked what a jet bridge is and received a short definition of the boarding walkway."
+ "recordedTimings": {
+ "ttftMs": 4731,
+ "interChunkDelaysMs": [
+ 14,
+ 0,
+ 16,
+ 0,
+ 15,
+ 0,
+ 18,
+ 0,
+ 15,
+ 0,
+ 29,
+ 0,
+ 15,
+ 0,
+ 15,
+ 0,
+ 18,
+ 0,
+ 16,
+ 0,
+ 17,
+ 0,
+ 28,
+ 0,
+ 10,
+ 0,
+ 14,
+ 0,
+ 15,
+ 0,
+ 15,
+ 0,
+ 17,
+ 0,
+ 14,
+ 0,
+ 17,
+ 0,
+ 19,
+ 0,
+ 16,
+ 0,
+ 16,
+ 0,
+ 17,
+ 0,
+ 34,
+ 0,
+ 4,
+ 0,
+ 13,
+ 0,
+ 15,
+ 0,
+ 4,
+ 0,
+ 33,
+ 0,
+ 25,
+ 0,
+ 11,
+ 0,
+ 13,
+ 0,
+ 29,
+ 0,
+ 14,
+ 0,
+ 18,
+ 0,
+ 16,
+ 0,
+ 20,
+ 0,
+ 14,
+ 1,
+ 13,
+ 0,
+ 16,
+ 0,
+ 14,
+ 0,
+ 27,
+ 0,
+ 8,
+ 0,
+ 16,
+ 0,
+ 18,
+ 0,
+ 16,
+ 0,
+ 21,
+ 0,
+ 9,
+ 0,
+ 29,
+ 0,
+ 5,
+ 0,
+ 15,
+ 0,
+ 19,
+ 0,
+ 17,
+ 0,
+ 15,
+ 0,
+ 23,
+ 0,
+ 16,
+ 0,
+ 23,
+ 0,
+ 16,
+ 0,
+ 15,
+ 0,
+ 65,
+ 0,
+ 35,
+ 0,
+ 5,
+ 0,
+ 41,
+ 0,
+ 6,
+ 0,
+ 5,
+ 0,
+ 7,
+ 0,
+ 7,
+ 0,
+ 1,
+ 0,
+ 5,
+ 0,
+ 11,
+ 0,
+ 17,
+ 0,
+ 17,
+ 0,
+ 16,
+ 0,
+ 97,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 1,
+ 0,
+ 10,
+ 0,
+ 5,
+ 0,
+ 7,
+ 0,
+ 18,
+ 0,
+ 7,
+ 0,
+ 31,
+ 0,
+ 13,
+ 0,
+ 14,
+ 0,
+ 17,
+ 0,
+ 18,
+ 0,
+ 19,
+ 0,
+ 17,
+ 0,
+ 21,
+ 0,
+ 16,
+ 0,
+ 18,
+ 0,
+ 34,
+ 0,
+ 16,
+ 0,
+ 1,
+ 0,
+ 13,
+ 0,
+ 18,
+ 0,
+ 14,
+ 0,
+ 17,
+ 0,
+ 21,
+ 0,
+ 21,
+ 0,
+ 23,
+ 0,
+ 13,
+ 0,
+ 3,
+ 0,
+ 19,
+ 0,
+ 34,
+ 0,
+ 44,
+ 0,
+ 4,
+ 0,
+ 8,
+ 0,
+ 5,
+ 0,
+ 15,
+ 0,
+ 16,
+ 0,
+ 13,
+ 0,
+ 18,
+ 0,
+ 18,
+ 0,
+ 36,
+ 0,
+ 11,
+ 0,
+ 40,
+ 0,
+ 17,
+ 0,
+ 32,
+ 0,
+ 6,
+ 0,
+ 3,
+ 0,
+ 5,
+ 0,
+ 3,
+ 0,
+ 5,
+ 0,
+ 21,
+ 0,
+ 21,
+ 0,
+ 11,
+ 0,
+ 20,
+ 0,
+ 13,
+ 0,
+ 15,
+ 0,
+ 16,
+ 0,
+ 18,
+ 0,
+ 16,
+ 0,
+ 17,
+ 0,
+ 17,
+ 0,
+ 14,
+ 0,
+ 17,
+ 0,
+ 18,
+ 0,
+ 39,
+ 0,
+ 13
+ ],
+ "totalDurationMs": 7131
+ },
+ "metadata": {
+ "systemHash": "b3d7aaf7"
}
},
{
"match": {
- "systemMessage": "In 3-5 words",
- "userMessage": "What is a jet bridge?"
+ "userMessage": "What is a jet bridge?",
+ "model": "gpt-5-mini",
+ "turnIndex": 2,
+ "hasToolResult": false
},
"response": {
- "content": "Jet bridge basics"
+ "content": "The user asked what a jet bridge is, and the assistant explained it\u2019s an enclosed, movable connector between an airport terminal gate and an aircraft (covering construction, operation, benefits, variations, and limitations) and then posted a brief processing note with character and word counts."
+ },
+ "recordedTimings": {
+ "ttftMs": 3377,
+ "interChunkDelaysMs": [
+ 14,
+ 0,
+ 20,
+ 0,
+ 31,
+ 0,
+ 74,
+ 0,
+ 25,
+ 0,
+ 52,
+ 0,
+ 1,
+ 0,
+ 14,
+ 0,
+ 11,
+ 0,
+ 106,
+ 0,
+ 107,
+ 0,
+ 40,
+ 0,
+ 5,
+ 0,
+ 9,
+ 0,
+ 4,
+ 0,
+ 11,
+ 0,
+ 3,
+ 0,
+ 0,
+ 4,
+ 0,
+ 7,
+ 0,
+ 9,
+ 0,
+ 5,
+ 0,
+ 5,
+ 1,
+ 2,
+ 0,
+ 8,
+ 0,
+ 6,
+ 0,
+ 5,
+ 0,
+ 72,
+ 0,
+ 9,
+ 0
+ ],
+ "totalDurationMs": 4037
+ },
+ "metadata": {
+ "systemHash": "05dfe622"
}
}
]
-}
+}
\ No newline at end of file
diff --git a/cockpit/chat/debug/python/src/graph.py b/cockpit/chat/debug/python/src/graph.py
index 82e866945..abe4a3cb5 100644
--- a/cockpit/chat/debug/python/src/graph.py
+++ b/cockpit/chat/debug/python/src/graph.py
@@ -66,6 +66,21 @@ async def generate_title(state: MessagesState, config) -> dict:
return {}
+# region debug-state
+class DebugState(MessagesState):
+ """MessagesState plus the metrics `process` computes.
+
+ The devtools State tab pretty-prints whatever the graph keeps in state.
+ A graph that only carries `messages` has nothing to show there, because
+ the transcript is rendered as the conversation rather than as state.
+ """
+
+ analysis: dict
+
+
+# endregion
+
+
def build_debug_graph():
"""
Constructs a multi-step graph with generate, process, and summarize
@@ -74,21 +89,25 @@ def build_debug_graph():
llm = ChatOpenAI(model="gpt-5-mini", streaming=True)
# region pipeline-nodes
- async def generate(state: MessagesState) -> dict:
+ async def generate(state: DebugState) -> dict:
system_prompt = (PROMPTS_DIR / "debug.md").read_text()
messages = [SystemMessage(content=system_prompt)] + state["messages"]
response = await llm.ainvoke(messages)
return {"messages": [response]}
- async def process(state: MessagesState) -> dict:
+ async def process(state: DebugState) -> dict:
last = state["messages"][-1].content
+ analysis = {
+ "characters": len(last),
+ "words": last.count(" ") + 1,
+ }
processed = AIMessage(
- content=f"[Processing] Analyzed {len(last)} characters. "
- f"Found {last.count(' ') + 1} words. Processing complete."
+ content=f"[Processing] Analyzed {analysis['characters']} characters. "
+ f"Found {analysis['words']} words. Processing complete."
)
- return {"messages": [processed]}
+ return {"messages": [processed], "analysis": analysis}
- async def summarize(state: MessagesState) -> dict:
+ async def summarize(state: DebugState) -> dict:
messages = [
SystemMessage(content="Provide a brief one-sentence summary of the conversation so far.")
] + state["messages"]
@@ -98,7 +117,7 @@ async def summarize(state: MessagesState) -> dict:
# endregion
# region graph-wiring
- graph = StateGraph(MessagesState)
+ graph = StateGraph(DebugState)
graph.add_node("generate", generate)
graph.add_node("process", process)
graph.add_node("summarize", summarize)