Skip to content

Commit 0c0dea4

Browse files
ChibionosChibi Vikramclaude
authored
feat: add tool-calling suspend/resume sample (#442)
Co-authored-by: Chibi Vikram <chibivikram@gmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c914f29 commit 0c0dea4

16 files changed

Lines changed: 3746 additions & 0 deletions
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# Tool-Calling Suspend/Resume Sample
2+
3+
This sample demonstrates how agents can suspend execution at specific points, then resume seamlessly when external work completes. This is useful for long-running automations where you don't want to block the agent execution.
4+
5+
## Quick Start
6+
7+
Test the suspend/resume flow using the evaluation command:
8+
9+
```bash
10+
cd samples/tool-calling-suspend-resume
11+
12+
# Step 1: Run to suspend
13+
uv run uipath eval agent-simple evaluations/eval-sets/test_simple_no_auth.json
14+
15+
# Step 2: Resume with input override
16+
uv run uipath eval agent-simple evaluations/eval-sets/test_simple_no_auth.json --resume --input-overrides '{"query": "Test suspend with simple payload"}'
17+
```
18+
19+
## What is Suspend/Resume?
20+
21+
The suspend/resume pattern allows agents to:
22+
- **Suspend** execution at specific points (e.g., when waiting for external work)
23+
- **Persist** their state to disk
24+
- **Resume** execution later when the work completes
25+
- **Continue** seamlessly from where they left off
26+
27+
This is critical for:
28+
- Long-running RPA automations
29+
- Human-in-the-loop workflows
30+
- External API calls with async callbacks
31+
- Multi-step processes across systems
32+
33+
## How It Works
34+
35+
### The `interrupt()` Function
36+
37+
The key is LangGraph's `interrupt()` function:
38+
39+
```python
40+
async def suspend_node(state: State) -> State:
41+
logger.info("About to suspend execution...")
42+
43+
# 🔴 Execution SUSPENDS here!
44+
# State is saved to SQLite checkpoint
45+
resume_data = interrupt({
46+
"message": "Waiting for external completion",
47+
"query": state.query
48+
})
49+
50+
# 🟢 This code runs AFTER resume
51+
logger.info(f"Received resume data: {resume_data}")
52+
result = f"Completed with resume data: {resume_data}"
53+
return {"query": state.query, "result": result}
54+
```
55+
56+
### The Lifecycle
57+
58+
```
59+
┌─────────────────────────────────────────────────────────────────┐
60+
│ 1. SUSPEND PHASE │
61+
├─────────────────────────────────────────────────────────────────┤
62+
│ Agent executes → reaches interrupt() │
63+
│ ↓ │
64+
│ LangGraph suspends execution │
65+
│ ↓ │
66+
│ State saved to __uipath/state.db │
67+
│ ↓ │
68+
│ Returns SUSPENDED status │
69+
│ ↓ │
70+
│ Python process can safely exit │
71+
└─────────────────────────────────────────────────────────────────┘
72+
73+
... time passes, external work completes ...
74+
75+
┌─────────────────────────────────────────────────────────────────┐
76+
│ 2. RESUME PHASE │
77+
├─────────────────────────────────────────────────────────────────┤
78+
│ New Python process starts │
79+
│ ↓ │
80+
│ Loads state from __uipath/state.db │
81+
│ ↓ │
82+
│ Invokes with Command(resume=result_data) │
83+
│ ↓ │
84+
│ Execution continues from interrupt() │
85+
│ ↓ │
86+
│ Agent completes and returns final result │
87+
└─────────────────────────────────────────────────────────────────┘
88+
```
89+
90+
## Files in This Sample
91+
92+
### Core Files
93+
- **`graph_simple.py`** - Simple agent demonstrating suspend/resume with dict payload
94+
- **`agent-simple.py`** - Symlink to graph_simple.py (referenced by uipath.json)
95+
- **`uipath.json`** - Agent configuration
96+
- **`langgraph.json`** - Graph definition for agent-simple
97+
- **`pyproject.toml`** - Python dependencies
98+
99+
### Evaluation Files
100+
- **`evaluations/eval-sets/test_simple_no_auth.json`** - Test cases for suspend/resume
101+
- **`evaluations/evaluators/contains_evaluator.json`** - Evaluator checking completion
102+
103+
## Running the Sample
104+
105+
### Step 1: Suspend
106+
107+
Run the evaluation without `--resume`:
108+
109+
```bash
110+
uv run uipath eval agent-simple evaluations/eval-sets/test_simple_no_auth.json
111+
```
112+
113+
**What happens**:
114+
- Agent executes and calls `interrupt()` → suspends
115+
- State is saved to `__uipath/state.db`
116+
- Evaluation runtime detects SUSPENDED status
117+
- Process exits
118+
119+
**Expected output**:
120+
```
121+
EVAL RUNTIME: Resume mode: False
122+
🔴 EVAL RUNTIME: DETECTED SUSPENSION
123+
EVAL RUNTIME: Agent returned SUSPENDED status
124+
EVAL RUNTIME: Extracted trigger(s) from suspended execution
125+
✓ Basic suspend/resume with query - No evaluators
126+
```
127+
128+
### Step 2: Resume
129+
130+
Resume execution with the `--resume` flag and provide input override:
131+
132+
```bash
133+
uv run uipath eval agent-simple evaluations/eval-sets/test_simple_no_auth.json --resume --input-overrides '{"query": "Test suspend with simple payload"}'
134+
```
135+
136+
**What happens**:
137+
- Loads state from `__uipath/state.db`
138+
- Continues execution from `interrupt()`
139+
- Agent completes and returns result
140+
- Evaluators run on final output
141+
142+
**Expected output**:
143+
```
144+
EVAL RUNTIME: Resume mode: True
145+
🟢 AGENT NODE: Execution RESUMED after interrupt()
146+
AGENT NODE: Received resume data: ...
147+
✓ Basic suspend/resume with query - Completed with resume data
148+
```
149+
150+
## Key Components
151+
152+
### AsyncSqliteSaver
153+
154+
Persists checkpoints to SQLite:
155+
156+
```python
157+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
158+
159+
async def _create_graph():
160+
checkpointer = AsyncSqliteSaver.from_conn_string("__uipath/state.db")
161+
return builder.compile(checkpointer=checkpointer)
162+
```
163+
164+
### Thread ID
165+
166+
Critical for resume - must match the suspend invocation. The evaluation runtime handles this automatically.
167+
168+
### Command API
169+
170+
The runtime uses the Command API to provide resume data:
171+
172+
```python
173+
from langgraph.types import Command
174+
175+
# Resume with specific data
176+
result = await graph.ainvoke(
177+
Command(resume={"status": "completed", "output": "success"}),
178+
config=config
179+
)
180+
```
181+
182+
## Agent Implementation
183+
184+
The core agent (`graph_simple.py`) is simple:
185+
186+
```python
187+
from langgraph.graph import StateGraph
188+
from langgraph.types import interrupt
189+
from pydantic import BaseModel
190+
191+
class Input(BaseModel):
192+
query: str
193+
194+
class Output(BaseModel):
195+
result: str
196+
197+
class State(BaseModel):
198+
query: str
199+
result: str = ""
200+
201+
async def suspend_node(state: State) -> State:
202+
"""Node that suspends execution."""
203+
# Interrupt with simple dict (no RPA invocation needed)
204+
resume_data = interrupt({
205+
"message": "Waiting for external completion",
206+
"query": state.query
207+
})
208+
209+
# This code executes after resume
210+
result = f"Completed with resume data: {resume_data}"
211+
return {"query": state.query, "result": result}
212+
213+
# Build the graph
214+
builder = StateGraph(state_schema=State)
215+
builder.add_node("suspend_node", suspend_node)
216+
builder.add_edge(START, "suspend_node")
217+
builder.add_edge("suspend_node", END)
218+
219+
# Compile with AsyncSqliteSaver
220+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
221+
222+
async def _create_graph():
223+
checkpointer = AsyncSqliteSaver.from_conn_string("__uipath/state.db")
224+
return builder.compile(checkpointer=checkpointer)
225+
```
226+
227+
## Troubleshooting
228+
229+
### "No checkpoint found"
230+
- Make sure you ran the suspend step first
231+
- Check that `__uipath/state.db` exists
232+
- Clean state between runs if needed: `rm -rf __uipath/state.db`
233+
234+
### "Agent doesn't suspend"
235+
- Ensure you're using a checkpointer: `builder.compile(checkpointer=...)`
236+
- Check that `interrupt()` is actually called in your code
237+
- Look for SUSPENDED status in the output
238+
239+
### "Resume starts from beginning"
240+
- Use `--resume` flag when resuming
241+
- Verify the state file exists at `__uipath/state.db`
242+
243+
## Next Steps
244+
245+
1. **Run the suspend step**: `uv run uipath eval agent-simple evaluations/eval-sets/test_simple_no_auth.json`
246+
2. **Run the resume step**: `uv run uipath eval agent-simple evaluations/eval-sets/test_simple_no_auth.json --resume --input-overrides '{"query": "Test suspend with simple payload"}'`
247+
3. **Build your own**: Use `graph_simple.py` as a template for your suspend/resume workflows
248+
249+
## Resources
250+
251+
- [LangGraph Interrupts Documentation](https://langchain-ai.github.io/langgraph/how-tos/human_in_the_loop/breakpoints/)
252+
- [UiPath Python SDK](https://github.com/UiPath/uipath-python)
253+
- [AsyncSqliteSaver Reference](https://langchain-ai.github.io/langgraph/reference/checkpoints/)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
graph_basic.py
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
flowchart TB
2+
__start__(__start__)
3+
suspend_node(suspend_node)
4+
__end__(__end__)
5+
__start__ --> suspend_node
6+
suspend_node --> __end__
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
graph_simple.py
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"version": "2.0",
3+
"resources": [
4+
{
5+
"resource": "process",
6+
"key": "Add.Two.Number.API.Workflow.Tool.api.API.Workflow",
7+
"value": {
8+
"processKey": {
9+
"defaultValue": "Add.Two.Number.API.Workflow.Tool.api.API.Workflow",
10+
"isExpression": false,
11+
"displayName": "Process Key"
12+
},
13+
"releaseKey": {
14+
"defaultValue": "40A364E7-8B12-407E-BAB4-16F1E028AEA4",
15+
"isExpression": false,
16+
"displayName": "Release Key"
17+
}
18+
},
19+
"metadata": {
20+
"ActivityName": "InvokeProcess",
21+
"BindingsVersion": "2.2",
22+
"DisplayLabel": "Add.Two.Number.API.Workflow.Tool.api.API.Workflow"
23+
}
24+
}
25+
]
26+
}

0 commit comments

Comments
 (0)