The xAI agentic tool loop — server-side tools, client-side functions, state, max_turns
Status: DOCUMENTED + LIVE_VERIFIED (2026-09-19, grok-4.3: Responses function_call → function_call_output via previous_response_id; server tools web_search / x_search / code_interpreter / mcp / file_search inside single requests; Chat Completions function round trip).
Sources: https://docs.x.ai/developers/tools/overview · https://docs.x.ai/developers/tools/advanced-usage · https://docs.x.ai/developers/tools/tool-usage-details · https://docs.x.ai/developers/tools/streaming-and-sync · https://docs.x.ai/developers/tools/function-calling Last verified: 2026-09-19
1. Who executes what (Responses API)
output[] item type |
Executed by | You send back |
|---|---|---|
function_call |
you | {type:"function_call_output", call_id, output} |
shell_call (tool shell, env local) |
you | shell_call_output {call_id, output:[{stdout, stderr, outcome}]} |
web_search_call, custom_tool_call (x_search sub-tools), code_interpreter_call, file_search_call, mcp_call, image_generation_call, tool_search_call/output |
xAI (server side, inside the same request) | nothing |
Stop when a response contains no client-side calls (normally it ends with a message). There is no approval step for MCP (require_approval ignored).
2. Loop with previous_response_id (verified)
resp = client.responses.create(model="grok-4.3", input="…", tools=tools)
while True:
outs = [{"type":"function_call_output","call_id":i.call_id,"output":run(i)} for i in resp.output if i.type == "function_call"]
if not outs: break
resp = client.responses.create(model="grok-4.3", tools=tools, input=outs, previous_response_id=resp.id)Server keeps reasoning, server-tool calls and their outputs. Follow-ups may use different tools/params.
3. Loop with encrypted state (ZDR / store:false)
Request include:["reasoning.encrypted_content"], keep an input_list, input_list.extend(resp.output) then append your function_call_output items and resend everything (store:false allowed). Verified: replayed reasoning + message items → 200.
4. Turns, limits, billing
max_turnsbounds assistant/server-tool turns within one request; a client-side call ends the request, and the follow-up request gets a fresh budget. Default = server cap. Recommended 1–2 quick, 3–5 balanced, 10+ deep.- One turn may run several tools in parallel;
parallel_tool_calls:falselimits to one call. - Billing: only successful server-tool executions count (
usage.server_side_tool_usage_details.*_calls,num_server_side_tools_used) — failed attempts (e.g.file_search_call status:"failed") are free; tokens for every intermediate step are billed (input_tokensis cumulative across steps, heavily cached: 7 467 input / 704 cached on a one-search request). Prices: web_search $5/1k, x_search $5/1k (→ per-post/profile pricing from 2026-09-21), code_interpreter $5/1k, file_search $2.50/1k, attachment search $10/1k, MCP tokens only. - Rejected-before-generation policy violations on Responses cost $0.05.
5. Observability
- Streaming recommended: watch
response.output_item.addedfor tool items,response.reasoning_summary_text.deltafor thinking,response.function_call_arguments.donefor client calls. - Citations:
output_text.annotations[](url_citation) + inline[[N]](url)(disable withinclude:["no_inline_citations"]); collections citationscollections://…. - xai-sdk:
get_tool_call_type(tool_call)→client_side_tool | web_search_tool | x_search_tool | code_execution_tool | collections_search_tool | mcp_tool;response.server_side_tool_usagemap (e.g.{'SERVER_SIDE_TOOL_X_SEARCH': 3}),response.tool_calls(all attempts incl. failed).
6. Chat Completions variant
Only client functions: tools:[{type:"function", function:{…}}] → choices[0].message.tool_calls (finish_reason:"tool_calls") → append the assistant message + {"role":"tool","tool_call_id":id,"content":…} → resend full history (stateless). Server-side tools are not available there (422 / 410).
Shared runnable loop: examples/xai/tools/function-calling/ (py/ts/sh) and examples/xai/responses/tool_loop.py.