Skip to content

Commit 06061a4

Browse files
Chibi Vikramclaude
andcommitted
feat: add tool-calling suspend/resume sample
Add comprehensive sample demonstrating suspend/resume pattern for agents that need to pause execution for external work (RPA processes, HITL, etc.). **Sample Contents:** - graph.py: Agent with RPA process invocation using interrupt() - graph_simple.py: Simplified variant for testing without auth - demo_suspend_resume.py: Interactive demo showing full suspend/resume cycle - Comprehensive test files for validation - README with usage guide and architecture explanation - Evaluation sets for testing suspend/resume behavior **Key Features:** - Shows proper use of LangGraph's interrupt() for suspension - Demonstrates checkpoint persistence with SQLite - Includes both RPA invocation and simplified test variants - Comprehensive documentation and testing **Use Cases:** - Long-running RPA automations - Human-in-the-loop workflows - External API calls with async callbacks - Multi-step processes across systems 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent acde588 commit 06061a4

16 files changed

Lines changed: 1727 additions & 0 deletions
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Manual Testing Guide for Suspend/Resume
2+
3+
This guide shows how to manually test the suspend/resume functionality using CLI commands.
4+
5+
## Step 1: Initial Execution (Suspend Phase)
6+
7+
Run the agent - it will suspend at the `interrupt()` call:
8+
9+
```bash
10+
uv run uipath run agent-simple --input '{"query": "test manual suspend"}'
11+
```
12+
13+
Expected output:
14+
```
15+
Status: SUSPENDED
16+
Output: {
17+
'abc123...': {
18+
'message': 'Waiting for external completion',
19+
'query': 'test manual suspend'
20+
}
21+
}
22+
```
23+
24+
The key here is the **interrupt_id** (the long hash like `abc123...`). This is needed for resume.
25+
26+
## Step 2: Inspect What Was Saved
27+
28+
### Check the checkpoint:
29+
```bash
30+
uv run python -c "
31+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
32+
from graph_simple import builder
33+
import asyncio
34+
35+
async def check():
36+
async with AsyncSqliteSaver.from_conn_string('__uipath/state.db') as saver:
37+
graph = builder.compile(checkpointer=saver)
38+
state = await graph.aget_state({'configurable': {'thread_id': 'default'}})
39+
print('State values:', state.values)
40+
print('Next tasks:', state.next)
41+
42+
asyncio.run(check())
43+
"
44+
```
45+
46+
### Check triggers in database:
47+
```bash
48+
sqlite3 __uipath/state.db "SELECT runtime_id, interrupt_id FROM __uipath_resume_triggers"
49+
```
50+
51+
## Step 3: Resume Execution
52+
53+
### Option A: Using CLI Resume (If Available)
54+
55+
```bash
56+
# If the uipath CLI supports resume with data:
57+
uv run uipath resume agent-simple \
58+
--thread-id default \
59+
--resume-data '{"<interrupt_id>": "MY RESUME DATA"}'
60+
```
61+
62+
Replace `<interrupt_id>` with the actual interrupt ID from Step 1.
63+
64+
### Option B: Using Python Script (Recommended)
65+
66+
Create a resume script:
67+
68+
```bash
69+
cat > test_manual_resume.py << 'EOF'
70+
import asyncio
71+
from uipath.runtime import UiPathRuntimeContext, UiPathExecuteOptions
72+
from uipath_langchain.runtime.factory import UiPathLangGraphRuntimeFactory
73+
74+
async def main():
75+
# Prompt user for interrupt_id
76+
print("Enter the interrupt_id from the suspend output:")
77+
interrupt_id = input("> ").strip()
78+
79+
print("\nEnter the data you want to provide for resume:")
80+
resume_data_value = input("> ").strip()
81+
82+
# Create runtime
83+
ctx = UiPathRuntimeContext()
84+
factory = UiPathLangGraphRuntimeFactory(ctx)
85+
runtime = await factory.new_runtime(entrypoint="agent-simple", runtime_id="default")
86+
87+
# Resume with provided data
88+
resume_input = {interrupt_id: resume_data_value}
89+
options = UiPathExecuteOptions(resume=True)
90+
91+
print(f"\nResuming with data: {resume_input}")
92+
result = await runtime.execute(input=resume_input, options=options)
93+
94+
print(f"\n✅ Status: {result.status}")
95+
print(f"Output: {result.output}")
96+
97+
await factory.dispose()
98+
99+
if __name__ == "__main__":
100+
asyncio.run(main())
101+
EOF
102+
103+
uv run python test_manual_resume.py
104+
```
105+
106+
Example interaction:
107+
```
108+
Enter the interrupt_id from the suspend output:
109+
> abc123def456...
110+
111+
Enter the data you want to provide for resume:
112+
> Completed by manual testing
113+
114+
✅ Status: SUCCESSFUL
115+
Output: {'query': 'test manual suspend', 'result': 'Completed with resume data: Completed by manual testing'}
116+
```
117+
118+
## Step 4: Verify Final State
119+
120+
Check that the execution completed:
121+
122+
```bash
123+
uv run python -c "
124+
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
125+
from graph_simple import builder
126+
import asyncio
127+
128+
async def check():
129+
async with AsyncSqliteSaver.from_conn_string('__uipath/state.db') as saver:
130+
graph = builder.compile(checkpointer=saver)
131+
state = await graph.aget_state({'configurable': {'thread_id': 'default'}})
132+
print('Final state:', state.values)
133+
print('Next tasks:', state.next) # Should be empty
134+
135+
asyncio.run(check())
136+
"
137+
```
138+
139+
Expected output:
140+
```
141+
Final state: {'query': 'test manual suspend', 'result': 'Completed with resume data: Completed by manual testing'}
142+
Next tasks: ()
143+
```
144+
145+
## Full End-to-End Test Script
146+
147+
For convenience, here's a complete script that does both phases:
148+
149+
```bash
150+
cat > test_full_cycle.py << 'EOF'
151+
import asyncio
152+
from uipath.runtime import UiPathRuntimeContext, UiPathExecuteOptions
153+
from uipath_langchain.runtime.factory import UiPathLangGraphRuntimeFactory
154+
155+
async def main():
156+
ctx = UiPathRuntimeContext()
157+
factory = UiPathLangGraphRuntimeFactory(ctx)
158+
runtime = await factory.new_runtime(entrypoint="agent-simple", runtime_id="manual_test")
159+
160+
print("=" * 80)
161+
print("PHASE 1: Execute and Suspend")
162+
print("=" * 80)
163+
164+
result1 = await runtime.execute(input={"query": "test full cycle"})
165+
print(f"Status: {result1.status}")
166+
print(f"Interrupts: {result1.output}")
167+
168+
if result1.status.name != "SUSPENDED":
169+
print("ERROR: Expected SUSPENDED status")
170+
return
171+
172+
interrupt_id = list(result1.output.keys())[0]
173+
print(f"\n✓ Got interrupt_id: {interrupt_id[:16]}...")
174+
175+
print("\n" + "=" * 80)
176+
print("PHASE 2: Resume")
177+
print("=" * 80)
178+
179+
user_data = input("Enter data to provide for resume (or press Enter for default): ").strip()
180+
if not user_data:
181+
user_data = "Manual test completed"
182+
183+
resume_input = {interrupt_id: user_data}
184+
options = UiPathExecuteOptions(resume=True)
185+
result2 = await runtime.execute(input=resume_input, options=options)
186+
187+
print(f"\n✅ Status: {result2.status}")
188+
print(f"Final output: {result2.output}")
189+
190+
await factory.dispose()
191+
192+
if __name__ == "__main__":
193+
asyncio.run(main())
194+
EOF
195+
196+
uv run python test_full_cycle.py
197+
```
198+
199+
## Common Issues
200+
201+
### Issue: "No checkpoint found"
202+
- Make sure you're using the same `thread_id` / `runtime_id` for both suspend and resume
203+
- Default is `"default"` for `uipath run`
204+
205+
### Issue: "Field required" validation error
206+
- This was the bug we just fixed - make sure `graph_simple.py` returns a dict, not a State object
207+
208+
### Issue: "No triggers found in database"
209+
- Triggers might have been deleted by a previous failed resume attempt
210+
- Re-run the suspend phase (Step 1)
211+
212+
### Issue: Empty resume data
213+
- Make sure you're providing the correct interrupt_id from the suspend output
214+
- The interrupt_id is the key in the output dict from the suspend phase

0 commit comments

Comments
 (0)