Install
$ agentstack add skill-patterai-skills-add-tools-and-handoffs ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Add tools and handoffs to a Patter agent
Patter agents have two built-in tools (transfer_call, end_call) plus unlimited custom tools. Custom tools can be handler-based (your Python/TS function runs in-process) or webhook-based (the agent POSTs JSON arguments to your URL). Both shapes use the same Tool model. Output guardrails block or rephrase agent responses before they reach TTS.
Built-in tools (always available)
Every agent automatically has:
transfer_call(target: str)— bridge to another phone number. Caller
hears the new party, your Patter session ends.
end_call()— hang up. Use for "goodbye" flows or abusive callers.
You don't have to register these. The LLM picks them when the system prompt authorizes it.
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt=(
"You are Mia, the receptionist for Acme. "
"If the caller asks for a human, call `transfer_call` with target='+14155559999'. "
"If the caller wants to hang up, call `end_call`."
),
first_message="Hi, how can I help?",
)
Custom tools — handler form
The @tool decorator (Python) and defineTool (TypeScript) auto-extract the JSON schema from your function signature and docstring. Best for simple synchronous lookups.
Python
from getpatter import Patter, Twilio, OpenAIRealtime2, tool
@tool
async def lookup_customer(phone: str) -> dict:
"""Look up customer details by phone number.
Args:
phone: E.164 phone number, e.g. +14155551234.
Returns:
Customer name, tier, and last order date.
"""
# your CRM call here
return {"name": "Jane Doe", "tier": "gold", "last_order": "2026-04-12"}
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="You are a customer-service agent. Use `lookup_customer` when you need account info.",
first_message="Hi, can I help with your account?",
tools=[lookup_customer],
)
TypeScript
import { Patter, Twilio, OpenAIRealtime2, defineTool } from "getpatter";
const lookupCustomer = defineTool({
name: "lookup_customer",
description: "Look up customer details by phone number.",
parameters: {
type: "object",
properties: {
phone: { type: "string", description: "E.164 phone number, e.g. +14155551234." },
},
required: ["phone"],
},
handler: async ({ phone }) => {
// your CRM call here
return { name: "Jane Doe", tier: "gold", lastOrder: "2026-04-12" };
},
});
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
const agent = phone.agent({
engine: new OpenAIRealtime2(),
systemPrompt: "You are a customer-service agent. Use lookup_customer when you need account info.",
firstMessage: "Hi, can I help with your account?",
tools: [lookupCustomer],
});
Custom tools — webhook form
Better for cross-language deployments or when the tool lives in a separate service. Patter POSTs the tool args as JSON to your URL and uses the JSON response as the tool result.
from getpatter import Tool
crm_lookup = Tool(
name="lookup_customer",
description="Look up customer details by phone number.",
parameters={
"type": "object",
"properties": {"phone": {"type": "string"}},
"required": ["phone"],
},
webhook_url="https://api.acme.com/crm/lookup",
)
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="…",
tools=[crm_lookup],
)
The endpoint receives POST with body {"phone": "+14155551234"} and must respond with JSON. Authentication: add your own header via reverse proxy, or use Patter's Tool(webhook_headers={...}) (omitted here for brevity).
Reassurance messages (Realtime mode)
In Realtime mode, tool calls happen during the call — the LLM has to decide whether to keep talking while the tool runs. Set reassurance to play a "hold on" message while the handler executes:
@tool(reassurance="Let me check that for you, one moment.")
async def lookup_customer(phone: str) -> dict:
await asyncio.sleep(2) # imagine a slow CRM
return {"name": "Jane"}
Without it, there's silence while the tool runs — which sounds broken to the caller.
Output guardrails
Block specific phrases or run a custom check before the agent's response reaches the TTS.
from getpatter import guardrail, Patter, Twilio, OpenAIRealtime2
@guardrail
def no_competitors(text: str) -> bool:
"""Block any response that mentions a competitor."""
return "patter" not in text.lower() or "alternative" not in text.lower()
phone = Patter(carrier=Twilio(), phone_number="+15550001234")
agent = phone.agent(
engine=OpenAIRealtime2(),
system_prompt="You are a sales agent for Acme.",
first_message="Hi, how can I help?",
guardrails=[no_competitors],
)
When a guardrail blocks, the agent speaks the guardrail's replacement text instead (default: "I'm sorry, I can't respond to that.").
For a simple keyword list:
from getpatter import Guardrail
agent = phone.agent(
...,
guardrails=[Guardrail(
name="no-pii",
blocked_terms=["social security", "credit card"],
replacement="I can't help with sensitive information. Let me transfer you.",
)],
)
Pipeline hooks (advanced)
For more control than guardrails, pipeline hooks intercept every leg of the pipeline (only available in Pipeline mode):
| Hook | Fires when | Use case | |---|---|---| | before_send_to_stt | Audio in | Echo cancellation, noise gate. | | after_transcribe | After STT | PII redaction, command parsing. | | before_llm | Before LLM | RAG injection, context rewrites. | | after_llm | After LLM | Output filter, persona enforcement. | | before_synthesize | Before TTS | Sentence chunking, voice-tag insertion. | | after_synthesize | After TTS | Audio post-processing. |
Hooks are async functions returning None (no change) or the modified value. See .
Gotchas
transfer_callrequirestargetin E.164. Tell the agent in the
system prompt: "Use transfer_call with the target number +1...."
- Webhook tools time out at 30 s. For longer ops, return a job ID and
expose a second tool to poll.
- JSON Schema strict mode: pass
strict=True(Python) /
strict: true (TS) to force OpenAI's strict-mode schema validation. Catches bad LLM args at the boundary.
- Guardrails fire post-LLM, pre-TTS — the caller never hears the
filtered text. The agent's memory still has the original though.
- Reassurance only works in Realtime mode. In Pipeline mode the LLM
generates the reassurance text itself; ConvAI mode doesn't expose this.
Common errors
| Symptom | Fix | |---|---| | LLM never calls the tool | System prompt isn't authorizing it. Add "Use the X tool when ..." explicitly. | | ValueError: parameters must be a JSON Schema object | You passed parameters={"phone": "string"} instead of {"type": "object", "properties": {...}}. | | Tool returns None and LLM gets stuck | Always return something — a status string, a dict, an empty {}. Never None. | | Transfer rings out | The target number isn't a Twilio-owned or verified caller ID (Twilio trial). |
Related skills
- [
build-voice-agent](../build-voice-agent/) — define the agent the tools attach to. - [
inspect-calls-and-metrics](../inspect-calls-and-metrics/) — see tool-call latency in the dashboard.
References
- Patter Tools doc: ·
- Patter Guardrails doc: ·
- Pipeline hooks:
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: PatterAI
- Source: PatterAI/skills
- License: MIT
- Homepage: https://www.skills.sh/patterai/skills
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.