- This project implements a stateful agent using LangGraph that can reason, call tools, handle errors safely and persist conversation state across runs (threads).
The system is design to demostrate:
- Controlled agent-tool loops
- Tool based computation
- Error handling and guardrails
- Checkpoint and replay
- Observability
-
Install dependencies
pip install -r requirements.txt -
Configure environment
- Create a new
.envfile in the project root and add yourOPENAI_API_KEY
- Create a new
-
Run the application
python app.py
This will start a CLI interface to interact with the agent.
| Command | Description |
|---|---|
/help |
Show available commands |
/new <thread_id> |
Switch conversation thread |
/thread |
Show current thread |
/state |
Show current persisted state |
/history |
Show checkpoint history |
/stream on/off |
Enable or disable streaming |
/test_math_chain |
Run math chain acceptance test |
/test_invalid_input |
Run invalid input test |
/test_loop_cap |
Run loop guard test |
/test_replay |
Run replay/resume test |
/exit |
Exit CLI |
The agent uses a shared state (GraphState) with reducers:
messages:add_messages→ accumulates conversationtool_calls:operator.add→ counts tool usageretries:operator.add→ tracks retry attemptserrors:operator.add→ stores structured errors
This allows:
- Persistent memory
- Traceability
- Accumulation of execution metadata
The system follows a controlled loop:
START → agent → tools → agent → ... → END
Additional routes:
tools→handle_tool_error→ validation failuretools→exit→ loop cap reachedagent→END→ no tool call needed
A safety mechanism prevents infinite loops:
MAX_TOOL_CALLS = 5- Computed per turn using message inspection
- Enforced after tool execution
If exceeded:
- The graph routes to
exit - A safe message is returned to the user
There are two layers of validation:
- Tool-level validation
- Input validation inside tools (
parse_number)
- Input validation inside tools (
- Graph-level handling
ToolNode(handle_tool_errors=True)- Routes to
handle_tool_error
- Output
- Structured error stored in state
- User-friendly message returned
The system uses:
InMemorySaveras checkpointerthread_idas session key
This enables:
- Persistence across runs
- Replay of conversations
- State inspection
Run:
/test_replay
Expected behavior:
- Message count increases across turns
- Checkpoint history grows
- State continuity is preserved
The system supports full execution tracing via:
/stream on
This shows:
- Node execution sequence
- Intermediate states
- Tool calls per step
Each run includes a summary:
- Nodes executed
- Tool usage
- Exit reason
The system includes four reproducible tests:
- Math Chain
/test_math_chain- Uses add and multiply
- Verifies correct result
- Validates tool usage
- Invalid Input
/test_invalid_input- Forces tool usage
- Triggers validation error
- Ensures safe error handling
- Loop Cap
/test_loop_cap- Forces repeated tool calls
- Validates guard condition
- Ensures safe exit
- Replay / Resume
/test_replay- Runs multiple turns in same thread
- Verifies state persistence
The default agent does not force tool usage, allowing natural handling of invalid inputs.
However, for specific tests (invalid_input, loop_cap), the system uses:
tool_choice="required"
This ensures:
- Deterministic traversal through tool logic
- Full coverage of validation and guard paths
project/
├── app.py # Entry point
├── cli.py # CLI interface
├── config/ # Settings
│ └── settings.py
├── graph/ # LangGraph components
│ ├── builder.py
│ ├── nodes.py
│ ├── routes.py
│ └── state.py
├── tools/ # Tools + validation
│ ├── math_tools.py
│ └── validators.py
├── observability/ # Streaming + summaries
│ ├── stream.py
│ └── summaries.py
├── tests_cli/ # Acceptance tests
│ ├── acceptance.py
│ └── replay.py
└── utils/ # Helpers
└── messages.py