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.QlawChatretains the liveGraphSession, theReActV2agent, and itsdspy.Historyacross calls. The optional clienthistorylist 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 currentGraphState).dspy.Toolinfers name, description, and schema from the function, andReActV2executes 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_lensandadd_nodeorchestrate, but every wiring change goes throughGraphState.apply()(deterministic expansion). A tool error is captured by ReActV2 as a tool result, so the agent can recover. - One
dspy.ReActV2per 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:
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
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 for the SSE envelope.