Tools let Tara call into your code. The model can't actually run anything — it returns a request to call a function. You execute it, return the result, and the model continues with the new information.
This guide walks through the full lifecycle. Endpoint reference is in agent-endpoint.md.
Tool use is only on
/api/v1/agent./api/v1/chathas no tool surface.
You Tara/API Your tool
| | |
|----req----->| |
| (msg + tools) |
| | |
|<--res-------| |
| stop_reason=tool_use |
| content=[..., tool_use{name,id,input}]
| | |
|--------- run locally ---------->|
| | |
|<------- result -----------------|
| | |
|----req----->| |
| (msg + assistant.last + tool_result)
| | |
|<--res-------| |
| stop_reason=end_turn |
| content=[text{...}] |
It's a loop. The model can call multiple tools per turn, and across many turns, before it's satisfied.
{
"name": "get_weather",
"description": "Get the current weather for a city. Returns temperature in °C and a one-word condition (sunny/cloudy/rainy/snowy).",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name in English, e.g. 'Dubai', 'New York'."
},
"units": {
"type": "string",
"enum": ["c", "f"],
"default": "c"
}
},
"required": ["city"]
}
}namemust match^[a-zA-Z0-9_-]{1,64}$.descriptionis what the model reads. Write it like the docstring of a function — clear, mentions edge cases, says what the tool returns.input_schemais JSON Schema. Stick to a Draft 7 subset:type,properties,required,enum,default, nestedobject/array. Avoid$refandoneOf— the model handles them poorly.
{
"id": "msg_abc123",
"role": "assistant",
"content": [
{ "type": "text", "text": "Let me check the weather for you." },
{
"type": "tool_use",
"id": "toolu_xyz789",
"name": "get_weather",
"input": { "city": "Dubai" }
}
],
"stop_reason": "tool_use",
"usage": { "input_tokens": 124, "output_tokens": 38, "total_tokens": 162 }
}Key things:
stop_reasonis"tool_use"(not"end_turn"). You're not done — call again.- The
tool_useblock has anid. Save it. You need it in the response. inputis the model's arguments, schema-validated againstinput_schema.- There may be multiple
tool_useblocks in one response (parallel tool calls).
After running your tool locally, build the next request:
{
"messages": [
{ "role": "user", "content": "What's the weather like in Dubai right now?" },
// The assistant's previous response, verbatim.
{
"role": "assistant",
"content": [
{ "type": "text", "text": "Let me check the weather for you." },
{
"type": "tool_use",
"id": "toolu_xyz789",
"name": "get_weather",
"input": { "city": "Dubai" }
}
]
},
// Your tool result. Role is "user" (not "tool" — this matches Anthropic).
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_xyz789",
"content": "{\"temp_c\": 37, \"condition\": \"sunny\"}"
}
]
}
],
"tools": [/* same tools as before */],
"max_tokens": 1024
}roleis"user". (Anthropic style — there is no"tool"role.)tool_use_idmust match theidfrom the assistant'stool_useblock.contentis a string (your serialized result). JSON-encode complex data.- Multiple results in one user message? Multiple
tool_resultblocks in the samecontentarray — one pertool_use_id. tool_resultcan include anis_error: trueflag to signal the tool failed.
{
"type": "tool_result",
"tool_use_id": "toolu_xyz789",
"content": "Could not reach weather provider: timeout.",
"is_error": true
}Python version (see examples/python/agent_loop.py for runnable code):
def run_agent(user_message, tools, tool_handlers, max_turns=10):
messages = [{"role": "user", "content": user_message}]
for _ in range(max_turns):
response = post_agent(messages=messages, tools=tools)
# Always append the assistant turn to history
messages.append({"role": "assistant", "content": response["content"]})
# If the model is done, return its text
if response["stop_reason"] == "end_turn":
return "".join(
block["text"] for block in response["content"]
if block["type"] == "text"
)
# Otherwise execute any tool_use blocks and append the results
if response["stop_reason"] == "tool_use":
tool_results = []
for block in response["content"]:
if block["type"] != "tool_use":
continue
try:
result = tool_handlers[block["name"]](**block["input"])
tool_results.append({
"type": "tool_result",
"tool_use_id": block["id"],
"content": json.dumps(result),
})
except Exception as e:
tool_results.append({
"type": "tool_result",
"tool_use_id": block["id"],
"content": f"Tool error: {e}",
"is_error": True,
})
messages.append({"role": "user", "content": tool_results})
continue
raise RuntimeError(f"Unexpected stop_reason: {response['stop_reason']}")
raise RuntimeError("Agent loop exceeded max_turns")// Auto: model decides (default)
{ "tool_choice": { "type": "auto" } }
// Any: force the model to call SOME tool
{ "tool_choice": { "type": "any" } }
// None: prohibit tool calls (useful for asking for a final summary)
{ "tool_choice": { "type": "none" } }
// Tool: force a specific one
{ "tool_choice": { "type": "tool", "name": "get_weather" } }The model can return multiple tool_use blocks in one turn. Run them in any order (in parallel, if you like) and return one tool_result per tool_use_id:
{
"role": "assistant",
"content": [
{ "type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": { "city": "Dubai" } },
{ "type": "tool_use", "id": "toolu_2", "name": "get_weather", "input": { "city": "Abu Dhabi" } }
]
}Your next message:
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_1", "content": "{\"temp_c\":37,\"condition\":\"sunny\"}" },
{ "type": "tool_result", "tool_use_id": "toolu_2", "content": "{\"temp_c\":39,\"condition\":\"sunny\"}" }
]
}- Cap your loop. Always
max_turns(we recommend 10). A buggy tool can spiral. - Validate inputs. The schema is enforced loosely. Treat tool inputs as untrusted — validate before executing.
- Return strings, not objects.
tool_result.contentmust be a string.json.dumps()your structured data. - Stick to one task per tool. Tools that do many things confuse the model. Prefer many narrow tools over one wide one.
- Idempotency. If your tool has side effects (sending email, charging a card), make sure repeated calls with the same args don't double-act. The model may retry on errors.
- Errors are normal. Return
is_error: truewith a human-readable message. The model will adapt — often by trying with different arguments. - Don't put secrets in
input_schema.description. It gets sent to the model and may surface intool_use.input.
- agent-endpoint.md — full request/response reference
- examples/python/agent_tools.py — minimal single-tool example
- examples/python/agent_loop.py — full multi-turn loop
- examples/typescript/agent.ts — typed agent loop
{ "messages": [ { "role": "user", "content": "What's the weather like in Dubai right now?" } ], "tools": [ { "name": "get_weather", "description": "Get the current weather for a city. Returns temperature in °C and condition.", "input_schema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ], "max_tokens": 1024 }