How to Implement `user.custom_tool_result` for Tool Call Responses in Anthropic Managed Agents
Emit a user.custom_tool_result event containing the tool output and the original custom_tool_use_id to return custom tool execution results back to the managed agent's event stream.
The anthropics/cwc-workshops repository demonstrates the complete lifecycle of custom tool integration in Claude-managed agents. When a model invokes a custom tool via agent.custom_tool_use, your host code must execute the corresponding Python function and bridge the result back through the user.custom_tool_result event type. This pattern ensures deterministic request-response pairing and maintains transcript continuity for model reasoning.
Understanding the user.custom_tool_result Event Loop
The event loop in ship-your-first-managed-agent/provided.py implements a three-stage handshake for custom tool execution. This mechanism separates sandboxed tool calls (handled internally) from custom tools that require host-side Python execution.
Detecting Custom Tool Requests
When the event stream returns an agent.custom_tool_use event, the system distinguishes it from standard sandbox tools. The event contains a unique identifier, tool name, and input arguments that your handler must process.
In ship-your-first-managed-agent/provided.py (lines 72-73), the detection logic branches based on event type:
elif ev.type == "agent.custom_tool_use":
# Custom tool detected - requires host-side execution
tool_boxes[ev.id] = box # Store placeholder for async result matching
The code stores a placeholder "box" in a tool_boxes dictionary using the event's id as the key. This temporary storage enables the system to match asynchronous responses when they arrive.
Executing Handlers and Emitting Results
Once detected, the host invokes your custom handle_tool function and wraps the return value in a properly structured user.custom_tool_result event. The critical requirement is preserving the custom_tool_use_id to link the response to the original request.
From ship-your-first-managed-agent/provided.py (lines 84-92):
result = handle_tool(ev.name, ev.input)
events = [{
"type": "user.custom_tool_result",
"custom_tool_use_id": ev.id,
"content": result,
}]
client.beta.events.create(agent_id=agent.id, events=events)
Key implementation details:
- The
handle_toolfunction executes your local Python implementation - The
custom_tool_use_idmust exactly match the originating event'sid - The
contentfield accepts a string containing the tool output - Events are submitted via the Anthropic client's
beta.events.createmethod
Consuming Results in the Event Stream
When the user.custom_tool_result event returns to the event loop, the system retrieves the stored placeholder and injects the content into the conversation transcript as an assistant message.
From ship-your-first-managed-agent/provided.py (lines 200-209):
elif ev.type == "user.custom_tool_result":
box = tool_boxes.pop(ev.custom_tool_use_id, None)
if box:
transcript.append({"role": "assistant", "content": ev.content})
This insertion allows the model to reason about the tool output as part of the ongoing dialogue, effectively treating the custom tool result as if the model had generated the text itself.
Implementing Custom Tool Handlers
Custom tools are defined in ship-your-first-managed-agent/agent.py by extending the handle_tool function. The following example adds a weather lookup tool:
# ship-your-first-managed-agent/agent.py
def handle_tool(name: str, args: dict) -> str:
if name == "weather":
city = args.get("city", "San Francisco")
# Production implementations would call an external API
return f"The weather in {city} is sunny, 72°F."
return f"unknown tool {name}"
The existing event loop in provided.py automatically routes the returned string through user.custom_tool_result. No additional modifications are required to the event emission logic when adding new tools—only the handler implementation needs extension.
Complete Implementation Example
The following script demonstrates the full round-trip from tool request to result consumption:
from anthropic import Anthropic
# Assume client and AGENT_ID are initialized
# 1. Model requests custom tool execution
client.beta.events.create(
agent_id=AGENT_ID,
events=[{
"type": "agent.custom_tool_use",
"id": "tool-123",
"name": "weather",
"input": {"city": "Berlin"},
}],
)
# 2. Host receives the event, executes handle_tool("weather", {"city": "Berlin"})
# and automatically emits user.custom_tool_result via provided.py logic
# 3. Subsequent event stream reads include the result:
# {"type": "user.custom_tool_result", "custom_tool_use_id": "tool-123", "content": "..."}
Manual Event Emission for Edge Cases
While provided.py handles automatic emission in standard workflows, you may need to manually emit results when operating outside the built-in loop. Maintain the exact schema structure:
result = "Tool execution output here"
client.beta.events.create(
agent_id=AGENT_ID,
events=[{
"type": "user.custom_tool_result",
"custom_tool_use_id": "tool-123", # Must match original request ID
"content": result,
}],
)
Critical requirements for manual emission:
- The
custom_tool_use_idmust match the correspondingagent.custom_tool_useevent'sid - The
contentfield must be a string - The event type must be exactly
user.custom_tool_result
Summary
user.custom_tool_resultbridges custom tool execution back to the managed agent's conversation stream- Deterministic pairing relies on matching
custom_tool_use_idvalues between request and response events - Implementation flow: Detect
agent.custom_tool_use→ Executehandle_tool→ Emituser.custom_tool_result→ Consume result in transcript - Source files: Core logic resides in
ship-your-first-managed-agent/provided.py, while tool definitions belong inship-your-first-managed-agent/agent.py - Extensibility: Adding tools requires only extending the
handle_toolfunction without modifying event loop mechanics
Frequently Asked Questions
What happens if the custom_tool_use_id doesn't match?
If the custom_tool_use_id in your user.custom_tool_result event does not match any pending tool ID stored in tool_boxes, the event loop cannot associate the result with the original request. According to the provided.py implementation (lines 200-209), the code attempts to pop from tool_boxes with a default of None, and if no box is found, the result is silently ignored rather than inserted into the transcript.
Can custom tools return structured data like JSON?
Yes, but you must serialize structured data to a string before emitting the user.custom_tool_result event. The content field accepts string values only. Parse the JSON in your handle_tool function, then return json.dumps(result) to ensure valid transmission. The model will receive the JSON string and can parse it contextually within the conversation.
How does this differ from regular sandbox tool_use events?
Standard agent.tool_use events execute within Anthropic's managed sandbox environment and return results automatically without host intervention. In contrast, agent.custom_tool_use events require your host code to execute Python functions locally and explicitly emit user.custom_tool_result events to return data. The custom tool pattern exists specifically for operations that require local system access or proprietary business logic.
Where is the handle_tool function defined?
The handle_tool function is defined in ship-your-first-managed-agent/agent.py (and ship-your-first-managed-agent/agent_complete.py for the full example). This function acts as the dispatch layer that maps tool names to your Python implementations. The provided.py file imports and calls this function when processing agent.custom_tool_use events, but the actual tool logic resides in your agent implementation file.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →