External Tool Execution allows you to mark tools as requiring external execution, causing the agent to pause and wait for results before continuing. This is essential for tools that call external services, require human approval, or execute in different systems.Documentation Index
Fetch the complete documentation index at: https://docs.upsonic.ai/llms.txt
Use this file to discover all available pages before exploring further.
Quick Start
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
# Mark tool as requiring external execution
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
"""Execute the external tool and return result."""
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def main():
agent = Agent("anthropic/claude-sonnet-4-6", name="email_agent")
task = Task(
description="Send an email to test@example.com with subject 'Hello' and body 'Test message'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
# Process external tool requirements
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
# Resume agent with results
result = await agent.continue_run_async(run_id=output.run_id, return_output=True)
print(result.output)
asyncio.run(main())
Core Concepts
Defining External Tools
Use the@tool(external_execution=True) decorator to mark tools that require external execution:
from upsonic.tools import tool
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""
Send an email - requires external execution.
Args:
to: Email recipient
subject: Email subject
body: Email body content
Returns:
Confirmation message
"""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
@tool(external_execution=True)
def execute_database_query(query: str) -> str:
"""Execute a database query - requires external execution."""
# In a real implementation, this would execute the query
return f"Query executed: {query} | Results: 10 rows returned"
@tool(external_execution=True)
def call_external_api(endpoint: str, payload: dict = None) -> dict:
"""Call an external API - requires external execution."""
# In a real implementation, this would call the API
return {"status": "success", "data": {"message": f"API called at {endpoint}"}}
External Tool Executor
Create a function to handle external tool execution:def execute_tool_externally(requirement) -> str:
"""Execute an external tool based on the requirement."""
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
elif tool_name == "execute_database_query":
return execute_database_query(**tool_args)
elif tool_name == "call_external_api":
result = call_external_api(**tool_args)
return str(result) if isinstance(result, dict) else result
else:
raise ValueError(f"Unknown tool: {tool_name}")
Processing Requirements
When the agent pauses for external tools, access the requirements:from upsonic import Agent, Task
agent = Agent("anthropic/claude-sonnet-4-6")
task = Task("Your task description", tools=[send_email])
output = await agent.do_async(task, return_output=True)
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
# Access tool details
tool_name = requirement.tool_execution.tool_name
tool_args = requirement.tool_execution.tool_args
tool_call_id = requirement.tool_execution.tool_call_id
# Execute externally and set result
result = execute_my_tool(tool_name, tool_args)
requirement.tool_execution.result = result
Continuation Methods
Resume with run_id (Same Agent)
The simplest approach - use run_id with the same agent:
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_with_run_id_same_agent():
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent")
task = Task(
description="Send an email to test@example.com with subject 'Hello' and body 'Test message'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
result = await agent.continue_run_async(run_id=output.run_id, return_output=True)
return result
asyncio.run(external_with_run_id_same_agent())
Resume with task (Same Agent)
Use task object for in-memory context continuation:
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_with_task_same_agent():
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent")
task = Task(
description="Send an email to test@example.com with subject 'Hello' and body 'Test message'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
result = await agent.continue_run_async(task=task, return_output=True)
return result
asyncio.run(external_with_task_same_agent())
Resume with run_id (New Agent - Cross-Process)
For cross-process resumption with persistent storage:
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
from upsonic.db.database import SqliteDatabase
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_with_run_id_new_agent():
db = SqliteDatabase(db_file="external.db", session_id="session_1", user_id="user_1")
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=db)
task = Task(
description="Send an email to test@example.com with subject 'Hello' and body 'Test message'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
run_id = output.run_id
# Execute tools externally
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
# Create NEW agent to resume (simulates different process)
new_agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=db)
result = await new_agent.continue_run_async(
run_id=run_id,
requirements=output.requirements, # Pass requirements with results
return_output=True
)
return result
asyncio.run(external_with_run_id_new_agent())
Resume with task (New Agent - Cross-Process)
New agent using task for continuation:
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
from upsonic.db.database import SqliteDatabase
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_with_task_new_agent():
db = SqliteDatabase(db_file="external.db", session_id="session_1", user_id="user_1")
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=db)
task = Task(
description="Send an email to test@example.com with subject 'Hello' and body 'Test message'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
# Create NEW agent with task
new_agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=db)
result = await new_agent.continue_run_async(
task=task,
requirements=output.requirements,
return_output=True
)
return result
asyncio.run(external_with_task_new_agent())
Multiple External Tools
Loop-Based Handling with run_id
Handle multiple external tools with a loop:
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
@tool(external_execution=True)
def execute_database_query(query: str) -> str:
"""Execute a database query - requires external execution."""
# In a real implementation, this would execute the query
return f"Query executed: {query} | Results: 10 rows returned"
@tool(external_execution=True)
def call_external_api(endpoint: str, payload: dict = None) -> dict:
"""Call an external API - requires external execution."""
# In a real implementation, this would call the API
return {"status": "success", "data": {"message": f"API called at {endpoint}"}}
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
elif tool_name == "execute_database_query":
return execute_database_query(**tool_args)
elif tool_name == "call_external_api":
result = call_external_api(**tool_args)
return str(result) if isinstance(result, dict) else result
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_multiple_tools_loop():
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent")
task = Task(
description=(
"First, send an email to admin@example.com with subject 'Report' and body 'Monthly report'. "
"Then query the database with 'SELECT * FROM users'. "
"Finally, call the external API at https://api.example.com/data."
),
tools=[send_email, execute_database_query, call_external_api]
)
output = await agent.do_async(task, return_output=True)
# Loop until all tools are processed
while output.active_requirements:
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
output = await agent.continue_run_async(
run_id=output.run_id,
return_output=True
)
return output
asyncio.run(external_multiple_tools_loop())
Loop-Based Handling with task
Same pattern using task:
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
@tool(external_execution=True)
def execute_database_query(query: str) -> str:
"""Execute a database query - requires external execution."""
# In a real implementation, this would execute the query
return f"Query executed: {query} | Results: 10 rows returned"
@tool(external_execution=True)
def call_external_api(endpoint: str, payload: dict = None) -> dict:
"""Call an external API - requires external execution."""
# In a real implementation, this would call the API
return {"status": "success", "data": {"message": f"API called at {endpoint}"}}
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
elif tool_name == "execute_database_query":
return execute_database_query(**tool_args)
elif tool_name == "call_external_api":
result = call_external_api(**tool_args)
return str(result) if isinstance(result, dict) else result
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_multiple_tools_loop_task():
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent")
task = Task(
description=(
"First, send an email to admin@example.com with subject 'Report' and body 'Monthly report'. "
"Then query the database with 'SELECT * FROM users'. "
"Finally, call the external API at https://api.example.com/data."
),
tools=[send_email, execute_database_query, call_external_api]
)
output = await agent.do_async(task, return_output=True)
while output.active_requirements:
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
output = await agent.continue_run_async(
task=task,
return_output=True
)
return output
asyncio.run(external_multiple_tools_loop_task())
Using Executor Callback for Multiple Tools
Let the executor handle all subsequent tools automatically:import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
@tool(external_execution=True)
def execute_database_query(query: str) -> str:
"""Execute a database query - requires external execution."""
# In a real implementation, this would execute the query
return f"Query executed: {query} | Results: 10 rows returned"
@tool(external_execution=True)
def call_external_api(endpoint: str, payload: dict = None) -> dict:
"""Call an external API - requires external execution."""
# In a real implementation, this would call the API
return {"status": "success", "data": {"message": f"API called at {endpoint}"}}
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
elif tool_name == "execute_database_query":
return execute_database_query(**tool_args)
elif tool_name == "call_external_api":
result = call_external_api(**tool_args)
return str(result) if isinstance(result, dict) else result
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_multiple_tools_with_executor():
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent")
task = Task(
description=(
"First, send an email to admin@example.com with subject 'Report' and body 'Monthly report'. "
"Then query the database with 'SELECT * FROM users'. "
"Finally, call the external API at https://api.example.com/data."
),
tools=[send_email, execute_database_query, call_external_api]
)
output = await agent.do_async(task, return_output=True)
# Handle first batch of requirements
for requirement in output.active_requirements:
if requirement.is_external_tool_execution:
result = execute_tool_externally(requirement)
requirement.tool_execution.result = result
# Executor handles all subsequent tools automatically
result = await agent.continue_run_async(
run_id=output.run_id,
return_output=True,
external_tool_executor=execute_tool_externally
)
return result
asyncio.run(external_multiple_tools_with_executor())
Cross-Process External Tool Handling
Complete pattern for handling external tools across process restarts:With run_id
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
from upsonic.db.database import SqliteDatabase
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_cross_process():
db = SqliteDatabase(db_file="external.db", session_id="session_1", user_id="user_1")
# STEP 1: Initial run pauses for external tool
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=db)
task = Task(
description="Send an email to test@example.com with subject 'Test' and body 'Hello'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
run_id = output.run_id
if output.is_paused and output.active_requirements:
print(f"Run {run_id} paused for external tools:")
for req in output.active_requirements:
if req.tool_execution:
print(f" - Tool: {req.tool_execution.tool_name}")
print(f" Call ID: {req.tool_execution.tool_call_id}")
print(f" Args: {req.tool_execution.tool_args}")
# STEP 2: Execute tools and set results
print("\nExecuting external tools...")
for req in output.active_requirements:
if req.is_external_tool_execution and not req.is_resolved:
tool_result = execute_tool_externally(req)
req.tool_execution.result = tool_result
print(f" Set result for {req.tool_execution.tool_name}: {tool_result}")
# STEP 3: New agent resumes (simulates different process)
print(f"\nCreating new agent to resume run {run_id}...")
new_db = SqliteDatabase(db_file="external.db", session_id="session_1", user_id="user_1")
new_agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=new_db)
result = await new_agent.continue_run_async(
run_id=run_id,
requirements=output.requirements,
return_output=True
)
print(f"Final result: {result.output}")
return result
asyncio.run(external_cross_process())
With task
import asyncio
from upsonic import Agent, Task
from upsonic.tools import tool
from upsonic.db.database import SqliteDatabase
@tool(external_execution=True)
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email - requires external execution."""
# In a real implementation, this would call an email service
return f"Email sent successfully to {to} with subject '{subject}'"
def execute_tool_externally(requirement) -> str:
tool_exec = requirement.tool_execution
tool_name = tool_exec.tool_name
tool_args = tool_exec.tool_args
if tool_name == "send_email":
return send_email(**tool_args)
else:
raise ValueError(f"Unknown tool: {tool_name}")
async def external_cross_process_task():
db = SqliteDatabase(db_file="external.db", session_id="session_1", user_id="user_1")
# STEP 1: Initial run
agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=db)
task = Task(
description="Send an email to test@example.com with subject 'Test' and body 'Hello'.",
tools=[send_email]
)
output = await agent.do_async(task, return_output=True)
run_id = output.run_id
if output.is_paused and output.active_requirements:
print(f"Run {run_id} paused for external tools")
# STEP 2: Execute tools
for req in output.active_requirements:
if req.is_external_tool_execution and not req.is_resolved:
tool_result = execute_tool_externally(req)
req.tool_execution.result = tool_result
# STEP 3: New agent uses task for continuation
new_db = SqliteDatabase(db_file="external.db", session_id="session_1", user_id="user_1")
new_agent = Agent("anthropic/claude-sonnet-4-6", name="external_tool_agent", db=new_db)
result = await new_agent.continue_run_async(
task=task,
requirements=output.requirements,
return_output=True
)
return result
asyncio.run(external_cross_process_task())
Important Notes
- Direct Call Mode Only: HITL continuation only supports direct call mode. Streaming is not supported.
- Requirements Parameter: When using a new agent, pass
requirements=output.requirementsto inject the results into the loaded state. - Persistent Storage: For cross-process scenarios, always use persistent storage like
SqliteDatabase. - is_resolved Check: Always check
requirement.is_resolvedbefore processing to avoid re-executing completed tools.

