Register a tool
Look up the exact name first with GET /v1/tools?name=<name>.
curl -sS --get "$PANTHEON_BASE_URL/v1/tools" \
-H "Authorization: Bearer $PANTHEON_API_KEY" \
--data-urlencode "name=echo_tool"Read the matches from data. If none exist, create the tool with POST /v1/tools.
Otherwise, use the matching tool's id to add a version.
Creating an existing name returns HTTP 409 idempotency_conflict. The error details carry tool_id.
Tool creation returns ToolResponse with these fields. Additional definition fields are omitted here.
{
"id": "tool_123",
"name": "echo_tool",
"display_name": "Echo tool",
"latest_version": 1,
"status": "active",
"created_at": "2026-09-08T12:00:00Z",
"updated_at": "2026-09-08T12:00:00Z",
"latest_definition": {
"id": "tool_definition_123",
"tool_id": "tool_123",
"name": "echo_tool",
"version": 1
}
}Deployments bind latest_definition.id through tool_definition_ids.
Tool options
Set side_effect and confirmation when creating a tool or version.
| Value | Meaning |
|---|---|
none | Accepted for side_effect. Default. |
workspace_write | Accepted for side_effect. |
external_write | Accepted for side_effect. |
never | Accepted for confirmation. Default. |
policy | Accepted for confirmation. |
Add a version
POST /v1/tools/{{id}}/versions accepts only these fields:
| Field | Meaning |
|---|---|
description | Accepted request field. |
executor | Accepted request field. |
input_schema | Accepted request field. |
output_schema | Accepted request field. |
side_effect | Accepted request field. |
confirmation | Accepted request field. |
Omit name and display_name. Both are rejected with "Extra inputs are not permitted".
Set TOOL_ID to the matching tool's id before running this request.
curl -sS -X POST "$PANTHEON_BASE_URL/v1/tools/$TOOL_ID/versions" \
-H "Authorization: Bearer $PANTHEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Echo a message.",
"executor": "application",
"input_schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"echoed": {
"type": "string"
}
},
"required": [
"echoed"
],
"additionalProperties": false
},
"side_effect": "none",
"confirmation": "never"
}'Handle a tool call
Track the tool
tool.started identifies the call.
Receive the pause
agent.custom_tool_use contains the application tool input.
Wait for idle
session.status_idle contains requires_action.
Resume
Send the result. Watch for tool.completed and run.completed.
Correlate the call
Use tool_call_id to update one UI item through the tool lifecycle.
Use execution_idempotency_key to execute the application tool once.
Use the durable SSE id as in_reply_to_event_id.
event: agent.custom_tool_use
id: evt_custom_tool_123
data: {"id":"evt_custom_tool_123","sequence":3,"type":"agent.custom_tool_use","workspace_id":"workspace_123","session_id":"session_123","run_id":"0123456789abcdef0123456789abcdef","created_at":"2026-09-03T12:00:02Z","content":{"tool_call_id":"call_123","pending_input_id":"pending_123","execution_idempotency_key":"evt_custom_tool_123","name":"echo_tool","input":{"message":"hello"}},"data":{"tool_call_id":"call_123","pending_input_id":"pending_123","execution_idempotency_key":"evt_custom_tool_123","name":"echo_tool"}}Resume the run
Reuse the same Idempotency-Key when retrying this result.
curl -sS -X POST "$PANTHEON_BASE_URL/v1/sessions/$SESSION_ID/runs/$RUN_ID/resume" \
-H "Authorization: Bearer $PANTHEON_API_KEY" \
-H "Idempotency-Key: resume-evt-custom-tool-123" \
-H "Content-Type: application/json" \
-d '{
"events": [{
"type": "user.custom_tool_result",
"in_reply_to_event_id": "evt_custom_tool_123",
"content": {"echoed": "hello"}
}]
}'A waiting run stays resumable for 30 minutes by default.
Per-operation result schemas
Register an output_schema with top-level oneOf or anyOf branches.
Each branch must be an object schema. Nested combinators are not supported.
Give each operation a distinct operation value with const and its own required fields.
This example accepts separate create and delete results:
{
"oneOf": [
{
"title": "Create result",
"type": "object",
"properties": {
"operation": {
"const": "create"
},
"id": {
"type": "string"
},
"title": {
"type": "string"
}
},
"required": [
"operation",
"id",
"title"
],
"additionalProperties": false
},
{
"title": "Delete result",
"type": "object",
"properties": {
"operation": {
"const": "delete"
},
"id": {
"type": "string"
},
"deleted": {
"type": "boolean"
}
},
"required": [
"operation",
"id",
"deleted"
],
"additionalProperties": false
}
]
}A create result contains operation, id, and title.
A delete result contains operation, id, and deleted.
Send the matching object as the result's content. Publish a new deployment after changing a tool version.
When no branch matches, resume returns HTTP 422 with tool_result_invalid.
Each error.details entry includes a path JSON Pointer and a reason.
The reason names the closest branch, its index, and the failed rule.
For example, a missing title points to /events/0/content/title and identifies Create result at oneOf[0].
The run remains waiting. Correct the result and retry before its deadline.
Decline a call
Return a structured tool error. The error code must use uppercase letters, digits, or underscores.
{
"events": [{
"type": "user.custom_tool_result",
"in_reply_to_event_id": "evt_custom_tool_123",
"error": {
"code": "USER_CANCELLED",
"message": "The user declined this action.",
"retryable": false
}
}]
}Reconnect
Save the last durable SSE id. Reconnect with Last-Event-ID.
If live startup fails, the stream stays open with durable events and X-Pantheon-Live-Events: unavailable. Otherwise that header is live. If you receive stream.resync after a mid-stream live failure, reconnect with that id, with backoff if resyncs repeat within seconds of connecting.
Pantheon replays persisted events before it resumes live output.
Pitfalls
- Tool creation returns idempotency_conflict: The name already exists. Read tool_id from error details, then add a version.
- Confirmation always is rejected: This release does not accept always. Gate the call in your application.
- Resume returns event_expired: The wait deadline passed. Start a new run.
- Resume returns event_already_resolved: Treat the original result as accepted. Do not execute the tool again.
- Resume returns tool_result_invalid: Match the tool's output schema. Keep each operation's shape unambiguous.
- A reconnect misses live tokens: Ephemeral deltas are not replayed. Rebuild final text from durable message events.