Reactive agent architecture
Introduction#
LangGraph state machine: AgentState, the reason/act loop, GraphBuilder compile/cache, message normalization, and edges into tool nodes.
Project structure#
The reactive agent spans Python backend services and TypeScript frontend orchestration:
- Backend: LangGraph workflow, agent state, message normalization, tool registry, and service/router layers.
- Frontend: Extension utilities that prepare payloads and invoke backend endpoints.
graph TB subgraph "Extension (Frontend)" EXTSVC["executeAgent.ts"] MAP["agent-map.ts"] end subgraph "FastAPI Backend" ROUTER["routers/react_agent.py"] SERVICE["services/react_agent_service.py"] AGENT["agents/react_agent.py"] TOOLS["agents/react_tools.py"] LLM["core/llm.py"] end EXTSVC --> ROUTER MAP --> EXTSVC ROUTER --> SERVICE SERVICE --> AGENT AGENT --> TOOLS AGENT --> LLM
Core components#
- AgentState: TypedDict representing the conversation state with an annotated messages field that accumulates LangChain messages.
- Message normalization: Bidirectional converters between external payloads and LangChain message types.
- GraphBuilder: Constructs and compiles the LangGraph workflow, caching the compiled graph.
- Tool registry: Centralized collection of structured tools, optionally augmented with contextual tokens/session data.
- System prompt: A default system message injected at the start of the message sequence when absent.
Key responsibilities:
- State machine: Agent node produces an LLM response; conditional edges route to tool execution when tool calls are detected; tool execution returns ToolMessages; loop continues until no tool calls remain.
- Payload conversion: Ensures consistent message roles, content, and tool call/tool call IDs across boundaries.
- Tool binding: The agent node binds available tools to the LLM to enable tool-use prompting.
Architecture overview#
The reactive loop is a LangGraph StateGraph :
- Nodes: agent and tool_execution.
- Edges: START → agent; agent → tool_execution if tool calls; tool_execution → agent; agent → END when no tool calls.
flowchart TD
START(["Start"]) --> AgentNode["Agent Node<br/>LLM with tools bound"]
AgentNode --> Decision{"Has tool calls?"}
Decision --> |Yes| ToolExec["Tool Execution Node<br/>Run selected tools"]
ToolExec --> AgentNode
Decision --> |No| End(["End"])Detailed component analysis#
LangGraph state machine and AgentState typing#
- AgentState defines a single key messages with an annotation that merges incoming messages into the state.
- The agent node ensures a system message is present at the head of the sequence before invoking the LLM.
classDiagram
class AgentState {
+messages : Annotated[Sequence[BaseMessage], add_messages]
}
class AgentNode {
+invoke(state) -> dict
}
AgentNode --> AgentState : "reads/writes"Message normalization functions#
Normalization bridges external payloads and LangChain message types:
- _payload_to_langchain: Converts external role/content/tool_call_id/tool_calls into SystemMessage, AIMessage, ToolMessage, or HumanMessage.
- _langchain_to_payload: Serializes LangChain messages back to external payloads, preserving tool_calls and tool_call_id.
flowchart LR P["External Payload"] --> N1["_payload_to_langchain"] N1 --> LC["LangChain Messages"] LC --> N2["_langchain_to_payload"] N2 --> P2["External Payload"]
GraphBuilder: workflow construction and caching#
- Builds a StateGraph with agent and tool_execution nodes.
- Uses tools_condition to decide routing after agent inference.
- Compiles the graph once and caches it for reuse.
- Supports dynamic tool sets via context or explicit tool lists.
classDiagram
class GraphBuilder {
-tools : list[StructuredTool]
-_compiled : CompiledGraph
+buildgraph() CompiledGraph
+__call__() CompiledGraph
}
GraphBuilder --> "1" StateGraph : "constructs"
GraphBuilder --> "1" ToolNode : "wraps tools"Tool registry and dynamic tool binding#
- Tools are assembled centrally, optionally enriched with contextual tokens/session data.
- The agent node binds the tool list to the LLM to enable tool-use prompting.
sequenceDiagram participant Caller as "Caller" participant GB as "GraphBuilder" participant AG as "Agent Node" participant TN as "ToolNode" participant LLM as "LLM Client" Caller->>GB : __call__() GB-->>Caller : CompiledGraph Caller->>AG : ainvoke(state) AG->>LLM : ainvoke(messages with tools bound) LLM-->>AG : AIMessage(tool_calls?) alt tool_calls present AG-->>TN : route to tool_execution TN-->>AG : ToolMessage AG->>LLM : ainvoke(updated messages) else no tool_calls AG-->>Caller : final state end
System prompt configuration#
- A default system message is constructed and prepended to the message sequence if none exists.
- This ensures consistent behavior and role context for the agent.
flowchart TD
S["Start"] --> CheckSys{"First message is SystemMessage?"}
CheckSys --> |No| Inject["Prepend DEFAULT_SYSTEM_PROMPT"]
CheckSys --> |Yes| Continue["Proceed"]
Inject --> ContinueFull execution pattern#
- Frontend composes a request payload and invokes the FastAPI endpoint.
- Router validates inputs and delegates to the service.
- Service builds the state (optionally injecting page context) and invokes the compiled graph.
- Graph executes the reactive loop and returns the final state.
sequenceDiagram participant Ext as "Extension" participant API as "FastAPI Router" participant Svc as "ReactAgentService" participant G as "Compiled Graph" participant Tools as "Tools" Ext->>API : POST "/" API->>Svc : generate_answer(...) Svc->>G : ainvoke(state) G->>G : agent -> tools_condition alt tool_calls G->>Tools : execute selected tools Tools-->>G : ToolMessage G->>G : agent else no tool_calls G-->>Svc : final state end Svc-->>API : answer API-->>Ext : response
Dependency analysis#
- agents/react_agent.py depends on core/llm.py for the LLM client and agents/react_tools.py for the tool registry.
- services/react_agent_service.py orchestrates request preparation and invokes the compiled graph.
- routers/react_agent.py exposes the API endpoint.
- Frontend extension utilities construct payloads and call the backend.
graph LR EXT["executeAgent.ts"] --> RTR["routers/react_agent.py"] RTR --> SVC["services/react_agent_service.py"] SVC --> AG["agents/react_agent.py"] AG --> TL["agents/react_tools.py"] AG --> LM["core/llm.py"]
Performance considerations#
- Graph compilation and caching:
- GraphBuilder compiles the workflow once and stores it for reuse.
- A process-level LRU cache wraps GraphBuilder to avoid repeated compilation.
- Tool execution offloading:
- Long-running tools are executed asynchronously and often offloaded to threads to prevent blocking the LLM invocation loop.
- Payload normalization:
- Content normalization avoids serialization errors and reduces downstream parsing overhead.
- System prompt injection:
- Ensures consistent initial context without recomputation.
Recommendations:
- Keep the tool list stable across invocations to maximize cache hits.
- Prefer streaming responses at the API boundary if needed, while retaining synchronous graph execution semantics.
- Monitor tool latency and consider batching where appropriate.
Troubleshooting guide#
Common issues and mitigations:
- Missing system message:
- Symptom: Unexpected behavior at start.
- Resolution: The agent node automatically prepends the default system message if the first message is not a SystemMessage.
- Tool call parsing failures:
- Symptom: Tool execution not triggered or malformed ToolMessage.
- Resolution: Ensure tool_calls are properly serialized and tool_call_id is preserved during normalization.
- Tool execution errors:
- Symptom: Exceptions raised inside tools.
- Resolution: Tools already wrap exceptions and return error strings; ensure upstream handlers surface these .
- LLM initialization problems:
- Symptom: Runtime errors when constructing the LLM client.
- Resolution: Verify provider configuration, API keys, and base URLs.
Operational tips:
- Log intermediate states and messages to diagnose routing loops or missing tool calls.
- Validate request payloads against the request/response models to prevent runtime mismatches.
Conclusion#
StateGraph holds messages, normalization keeps formats aligned, and cached compiled graphs avoid rebuild cost. Conditional edges move between reasoning and tools.