Dove gira il codice decide di cosa sei responsabile
Il tool use è un contratto: tu dichiari quali operazioni esistono e che forma hanno input e output, Claude decide quando chiamarle, e il modello non esegue niente. La distinzione che conta è dove gira il codice. I client tool — i tuoi, e quelli con schema Anthropic come bash, text_editor e memory — girano nella tua applicazione: la risposta arriva con stop_reason uguale a tool_use e uno o più blocchi tool_use, tu esegui e rimandi un tool_result. I server tool come web_search, web_fetch e code_execution girano sull’infrastruttura Anthropic, e per quelli non costruisci mai un tool_result.
Il loop lato client è un while su stop_reason: finché vale tool_use esegui i tool e continui la conversazione, e si esce su end_turn, max_tokens, stop_sequence o refusal. Quando esiste uno schema Anthropic equivalente al tool che stavi per scrivere, conviene: quegli schemi sono addestrati dentro il modello, quindi le chiamate sono più affidabili.
Un caso ibrido morde spesso: se nello stesso gruppo di chiamate parallele Claude invoca un tuo client tool e un server tool, il turno torna a te con un server_tool_use privo di risultato, e la risposta deve contenere solo blocchi tool_result, altrimenti arriva un 400.
La descrizione è il codice che il modello legge
Un tool definito da te ha name, description e input_schema, più input_examples opzionale. La documentazione è categorica sulla descrizione: «This is by far the most important factor in tool performance.» E dà una misura: «Aim for at least 3–4 sentences for each tool description, more if the tool is complex.» Scrivi che cosa fa, quando va usato e quando no, che cosa significa ogni parametro e che cosa non restituisce.
{
"name": "crm_search_contacts",
"description": "Searches contacts in the CRM by name, email domain or company. Use it before creating a contact, to avoid duplicates. Returns at most 25 matches with a stable contact slug, full name and owner; it does not return deal history or notes. Call crm_get_contact with a slug for the full record.",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Free-text search over name, email and company" }
},
"required": ["query"]
}
}
Sul set nel suo insieme valgono tre regole. «Consolidate related operations into fewer tools»: meglio un tool con un parametro action che la terna create_pr, review_pr, merge_pr. Namespacing nei nomi quando i tool coprono servizi diversi, tipo github_list_prs e slack_send_message. E «Design tool responses to return only high-signal information»: identificatori semantici e stabili, e solo i campi che servono al passo successivo. Se lo schema ha oggetti annidati o formati sensibili aggiungi input_examples, e se ti serve la conformità garantita metti strict: true.
Il ritorno: risultato, errore, ordine dei blocchi
Il tool_result porta tool_use_id, un content opzionale e un is_error opzionale. Due vincoli di formato mandano in 400 chi li ignora: i blocchi di risultato devono seguire immediatamente i rispettivi tool_use, e «In the user message containing tool results, the tool_result blocks must come FIRST in the content array. Any text must come AFTER all tool results.»
Sugli errori, il fallimento è informazione, non rumore. Torna is_error a true con un messaggio che dica che cosa è andato storto e che cosa provare: «Write instructive error messages.» Un "failed" secco costringe il modello a indovinare; "Rate limit exceeded. Retry after 60 seconds." gli dice come recuperare. Su una chiamata invalida Claude riprova due o tre volte con correzioni prima di arrendersi: se succede spesso, il problema è la tua description, non il modello.
Il risultato è anche superficie di attacco. I tool result trasportano contenuti che non controlli, dalle pagine web agli upload, e la documentazione è esplicita: «Treat that content as untrusted: an attacker who can influence it may embed instructions that try to redirect Claude (indirect prompt injection).» Tienili dentro i blocchi tool_result, mai nel system prompt o in un blocco di testo utente.
Dispatch e approvazione
Nell’harness il dispatch si fa sul campo name del blocco tool_use, e le risposte si appaiano per tool_use_id: con le chiamate parallele quel campo è l’unico aggancio corretto, non l’ordine. Per i membri dei toolset computer use e browser use si smista su name e toolset_name insieme, e il risultato deve riportare lo stesso toolset_name. Se preferisci una chiamata per turno, usa tool_choice di tipo auto con disable_parallel_tool_use.
Poi c’è chi autorizza. Nei Managed Agents le policy sono due, always_allow e always_ask, con default diversi: il toolset dell’agente parte da always_allow, le toolset MCP da always_ask, perché «This ensures that new tools added to an MCP server do not execute in your application without approval.» Con always_ask la sessione si ferma con uno stop_reason di tipo requires_action, e tu rispondi con un evento user.tool_confirmation per ogni blocco, allow o deny, con un deny_message che il modello legge.
Nell’Agent SDK il gancio equivalente è il callback canUseTool, e qui c’è l’errore classico: «Auto-approved tools never reach canUseTool.» Una regola allow con nome nudo, oppure le modalità acceptEdits e bypassPermissions, approvano prima che il callback venga interrogato, quindi i controlli che ci hai messo dentro non girano. Per una verifica che deve valere su ogni chiamata usa un hook PreToolUse: gli hook girano prima di tutto il resto, e un loro rifiuto vale anche in bypassPermissions.