> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qredence.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat and tools

> Tier 3 of Fleet Reasoner: the multi-turn Qlaw chat agent — dspy.ReActV2 with lenses-as-tools, stateful sessions, and graph mutation that still goes through the pure apply() boundary.

Tier 3 is the Qlaw chat ("God's Eye View") plus its shared tool set. Both the chat agent and the Enrich lens run on **`dspy.ReActV2`** — DSPy 3.3.0's native tool-calling agent: parallel tool calls, a reserved `submit` tool, multi-turn replay of prior calls, and prompt-cache reuse.

## Design

* **Stateful `dspy.History`.** `QlawChat` retains the live `GraphSession`, the `ReActV2` agent, and its `dspy.History` across calls. The optional client `history` list is accepted only to bootstrap a fresh instance for HTTP compatibility. Later turns use the instance-owned history directly.
* **Persistent tools.** Tools are built once as closures over the chat's `GraphSession` (a mutable holder for the current `GraphState`). `dspy.Tool` infers name, description, and schema from the function, and `ReActV2` executes plain callables directly, so the agent never has to serialize the whole graph into a tool argument.
* **The invariant holds.** The LM still never mutates the graph. `run_lens` and `add_node` orchestrate, but every wiring change goes through `GraphState.apply()` (deterministic expansion). A tool error is captured by ReActV2 as a tool result, so the agent can recover.
* **One `dspy.ReActV2` per chat session.** `reset()` creates a new agent and clears history. Calls are serialized because the session and tool-bound graph are mutable. Zero-shot is the supported mode.

## Tools

`make_chat_tools(session, lenses)` in `qlaw/chat.py`:

| Tool                                                     | Effect                                                                                                                                                  |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run_lens(node_id, lens)`                                | Runs a lens on the node, applies its `NodeBatch` via `GraphState.apply()`. Returns what was added — or the `explain` summary or `enrich` intel summary. |
| `add_node(label, type, description, parent_id="active")` | Attaches a `ChatNode` (any `NodeType`) under `active`, `root`, or a specific node id.                                                                   |
| `inspect_node(node_id)`                                  | Read-only view: label, type, description, intel, parent, children.                                                                                      |
| `graph_stats()`                                          | Read-only node counts per type.                                                                                                                         |

`ChatNode` (in `qlaw/graph.py`) is the chat's own child DTO. Unlike the lens output contracts (`SubNode`, `Concept`, ...), its `type` spans the full `NodeType` taxonomy. It participates in `NodeBatch.children`, so `apply()` needs no special-casing.

`qlaw/tools.py` keeps only graph-agnostic tools. `search` remains a `NotImplementedError` stub — inject a real implementation via DI: `GroundingEnricher(search_tool=...)`.

## Mapping from the current app

| Current flow (`App.tsx` / `chatSession`)                               | Fleet Reasoner                                                                                        |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `systemInstruction` "You are Qlaw's Co-Pilot…" plus tools `[addNode]`. | `ChatAnswer` docstring plus `ReActV2` tools (`run_lens`, `add_node`, `inspect_node`, `graph_stats`).  |
| `[Current Graph Context]` node summary injected per message.           | `graph_summary` input field, built by `GraphState.summary()`.                                         |
| `addNode` function call → `manualAddNode`.                             | `add_node` tool → `GraphState.apply()` inside the agent run. The serve layer returns the final graph. |

## Session storage on the server

`ChatSessionStore` is a bounded, per-`session_id`, independently locked map of `QlawChat` instances. The browser stores one opaque id per tab in `sessionStorage`; the server maps it to a live agent. Missing or expired entries start a fresh conversation rather than sharing another client's history.

## Streaming to the client

`dspy.streamify(chat, status_message_provider=ChatStatusProvider())` wraps the agent and returns an async generator of events, ending with the final `dspy.Prediction`. ReActV2 emits its final answer as `submit` tool-call arguments, so token-level streaming of `answer` does not apply. `ChatStatusProvider` surfaces tool activity as `status` events, and the complete answer arrives in the `done` frame.

The `done` frame carries the final graph. Chat tools expand the graph during the agent run, so the client must adopt `done.graph` — the request graph may be stale by the time the answer arrives.

See [API and streaming](/fleet-reasoner/api) for the SSE envelope.
