Executing tool calls
Composio runs tool calls two ways: through a session, with its meta tools, or directly, with tools you fetch by slug for a user. Only the direct path takes modifiers.
| Session | Direct | |
|---|---|---|
| Tools | session.tools() | composio.tools.get(user_id, tools=[...]) |
| Execute | provider.handle_tool_calls(session=session, response=...) | provider.handle_tool_calls(user_id=..., response=..., modifiers=[...]) |
| The model sees | Meta tools that find and run app tools | Exactly the tools you listed |
tools = composio.tools.get("cust_7f3a", tools=["GITHUB_CREATE_AN_ISSUE", "GMAIL_FETCH_EMAILS"])
response = client.chat.completions.create(model="gpt-4.1-mini", messages=messages, tools=tools)
results = composio.provider.handle_tool_calls(user_id="cust_7f3a", response=response)handle_tool_calls takes either a user_id or a session, never both, and runs every tool call in the response's first choice, in order, so the first result answers the first tool call. Modifiers, your own functions that change a call's arguments or its result, are the subject of lesson 6. Passing them together with a session is refused before anything is sent:
from composio import Composio, after_execute
from openai.types.chat import ChatCompletion
composio = Composio(api_key="not-a-real-key", allow_tracking=False)
@after_execute(toolkits=["gmail"])
def unchanged(tool, toolkit, response):
return response
response = ChatCompletion(id="1", object="chat.completion", created=0, model="m", choices=[])
composio.provider.handle_tool_calls(session=object(), response=response, modifiers=[unchanged])Results and failures
from composio.core.models.tools import ToolExecutionResponse
print(list(ToolExecutionResponse.__annotations__))Each result is a dictionary with the app's data, an error message and successful. Give the model the error so it can try something else, but stop your loop on authentication problems, such as a user who revoked access, and ask the user to reconnect.
- Print the signature of
composio.tools.execute. What does it need thathandle_tool_callsdoes not? - Read
ToolVersionRequiredErrorincomposio/exceptions.py: when is it raised? - Print
inspect.signature(composio.provider.handle_tool_calls).
This is what real progress feels like.