BYOKchat Blog

How AI Tool Calling Works: From Model Decision to Tool Result

Understand the full AI tool-calling loop: schemas, model-generated arguments, validation, execution, results, retries, parallel calls, and failure recovery.

· 8 min read

On this page
  1. A tool definition is a contract
  2. Tool calling is not ordinary structured output
  3. The model sees descriptions, not implementation code
  4. Tool selection is probabilistic
  5. Arguments are model output
  6. Streaming tool arguments need accumulation
  7. Every call needs an identity
  8. Authorization happens after selection
  9. Tool execution should return data, not prose theater
  10. Tool errors are part of the conversation loop
  11. Read-only and mutating tools have different retry rules
  12. Parallel tool calls require real concurrency semantics
  13. Multi-round tool use is an agent loop
  14. Context grows during tool loops
  15. Provider schemas should stop at the adapter boundary
  16. Tool calling is not a security sandbox
  17. Test the loop as a state machine
  18. A compact implementation checklist
  19. Where BYOKchat fits
  20. Further reading

AI tool calling lets a model request an operation your application knows how to perform. The important word is request: the model normally does not execute your function, query your database, send the email, or call your internal API itself. It produces a structured tool call, and your application decides what happens next.

That distinction is the foundation of a reliable tool system.

A production loop looks roughly like this:

Diagram illustrating the surrounding section

The model proposes. The application validates and executes. The model then interprets the result.

A tool definition is a contract

A tool usually has three important parts:

  • a stable name;
  • a description that explains when it should be used;
  • a schema describing accepted arguments.

For example:

{
  "name": "get_weather",
  "description": "Get current weather for a city",
  "parameters": {
    "type": "object",
    "properties": {
      "city": { "type": "string" },
      "units": {
        "type": "string",
        "enum": ["metric", "imperial"]
      }
    },
    "required": ["city", "units"],
    "additionalProperties": false
  }
}

The schema helps the model generate arguments and gives the application something deterministic to validate. It does not prove that an argument is safe or semantically correct.

{"city":"Paris","units":"metric"} may satisfy the schema. So might an argument that targets the wrong customer, workspace, file, or account in a more sensitive tool.

Schema validation is necessary, not sufficient.

Tool calling is not ordinary structured output

Tool calling and structured response output both involve schemas, but they solve different problems.

Structured output says:

Return the answer in this machine-readable shape.

Tool calling says:

Ask the application to perform this capability, then continue with the result.

If you want a model to classify an invoice into JSON, structured output may be enough. If you want it to retrieve the invoice from a private system first, that retrieval is a tool call.

The two can also be combined: tools gather evidence, then the final response follows a JSON schema.

The model sees descriptions, not implementation code

The model normally receives the public tool contract, not your Swift, TypeScript, Python, SQL, or backend implementation.

That creates an important security boundary:

model-visible contract

proposed call

application policy

private implementation

Your implementation can enforce constraints the model never controls:

  • authenticated user identity;
  • workspace boundaries;
  • rate limits;
  • allowed paths;
  • authorization roles;
  • network allowlists;
  • transaction rules;
  • confirmation requirements.

Do not encode critical authorization only in a tool description such as “never delete production data.” Enforce it in code.

Tool selection is probabilistic

When several tools are available, the model decides whether and how to use them according to the provider’s tool-calling semantics and your request configuration.

That decision is influenced by:

  • tool names;
  • descriptions;
  • argument schemas;
  • system/developer instructions;
  • conversation context;
  • model capabilities;
  • provider-specific tool-choice controls.

Descriptions should therefore be concrete and non-overlapping.

Bad:

search — searches things
lookup — looks things up
find — finds information

Better:

search_product_catalog — full-text search over products
get_product_by_id — retrieve one product by canonical ID
search_support_articles — search customer-support documentation

Ambiguous tool sets make routing harder for both models and humans.

Arguments are model output

A generated tool call might contain:

{
  "customer_id": "cus_42",
  "amount": 500
}

Treat those fields with the same skepticism you would apply to any untrusted input.

Validate at several layers:

  1. Syntax — can the arguments be decoded?
  2. Schema — do types, required fields, enums, and bounds match?
  3. Business rules — is this operation valid?
  4. Authorization — may this user/model session act on the target?
  5. Safety policy — does it require confirmation or denial?

A schema cannot tell you whether cus_42 belongs to the authenticated organization.

Streaming tool arguments need accumulation

Providers may stream function arguments incrementally. A fragment such as:

{"city":"Ho Chi

is not valid JSON yet.

Another fragment may complete it later. The client should associate fragments with the correct call ID, accumulate them, wait for the provider’s completion boundary, verify the surrounding response was not truncated, then parse and validate.

Do not execute because a partial buffer happens to become parseable early.

See How AI Streaming Works for the transport and event-state details.

Every call needs an identity

A tool-call ID is more than UI metadata. It lets the application correlate:

model request → approval → execution → result → continuation

For concurrent calls, IDs prevent fragments and results from being mixed together.

For durable agent workflows, you may also need an application execution ID distinct from the provider’s call ID. Provider IDs describe protocol objects; your own ID can survive retries, persistence, provider switches, or application relaunches.

Authorization happens after selection

A model choosing a tool does not authorize it.

Suppose the model requests:

delete_issue(repo="example", issue=142)

The application can still:

  • deny it;
  • ask the user;
  • allow it once;
  • apply a stored policy;
  • rewrite nothing and require a corrected request;
  • refuse because the target is outside the current workspace.

This is especially important for MCP and other external tool systems where the tool implementation may be operated outside the client.

See MCP Security Checklist for the broader trust model.

Tool execution should return data, not prose theater

A tool result should communicate the operation’s actual outcome clearly enough for the model and application to reason about it.

Prefer structured results where useful:

{
  "status": "success",
  "issue_id": 142,
  "state": "closed"
}

rather than vague output such as:

Done! Everything worked great.

Structured results improve error handling, testing, observability, and portability between models.

The result is still untrusted model input when it contains external data. A webpage, issue body, document, or email returned by a tool can contain prompt injection.

Tool errors are part of the conversation loop

A tool can fail because:

  • arguments are invalid;
  • authorization is denied;
  • the user rejects approval;
  • a backend is unavailable;
  • the requested object no longer exists;
  • a timeout occurs;
  • the result is too large;
  • the operation succeeds but the response is lost.

Do not flatten all of these into "tool failed".

A useful internal error model might include:

type ToolFailure = {
  category: "invalid_arguments" | "denied" | "not_found" | "rate_limited" | "transient" | "unknown_outcome";
  retryable: boolean;
  safeToReplay: boolean;
  messageForModel: string;
};

The model may be able to repair invalid arguments. It should not automatically replay a payment whose outcome is unknown.

Read-only and mutating tools have different retry rules

Consider:

search_docs(query="caching")

versus:

send_email(to="customer@example.com", body="...")

If the network drops after the first call, retrying is usually low risk. If it drops after the second call, the email may already have been sent.

Mutating tools benefit from:

  • idempotency keys;
  • durable operation IDs;
  • server-side deduplication;
  • transactional APIs;
  • explicit unknown-outcome states;
  • human confirmation before uncertain replay.

This is the same replay boundary that makes provider fallback difficult after tools execute. See Reliable AI Provider Fallback and Model Routing.

Parallel tool calls require real concurrency semantics

Some models can request multiple calls in one turn:

get_weather(city="Tokyo")
get_weather(city="Paris")
get_weather(city="Hanoi")

These calls are independent and can often execute concurrently.

But if a later operation requires the result of an earlier one, it cannot be expressed as a true same-turn dependency merely by appearing beside it:

create_project(...)
add_issue(project_id=???...)

The model needs another round after create_project returns the new identifier, or the application needs a higher-level tool that performs the transaction itself. A provider emitting multiple calls in one turn does not create a data-dependency chain between them.

Even independent calls can have conflicting side effects, so concurrency also complicates approval UI, cancellation, result ordering, and rate limits.

Multi-round tool use is an agent loop

A single user turn may involve several rounds:

Diagram illustrating the surrounding section

Without limits, that loop can run indefinitely.

Set explicit budgets such as:

  • maximum model rounds;
  • maximum tool calls;
  • wall-clock deadline;
  • token/cost budget;
  • per-tool rate limits;
  • cancellation propagation.

A loop terminating because it hit a budget should be distinguishable from successful completion.

Context grows during tool loops

Each round may add:

  • assistant tool-call objects;
  • tool arguments;
  • tool results;
  • model text;
  • provider reasoning state.

Large search results can consume context quickly. Do not blindly insert megabytes of tool output into the next model request.

Strategies include:

  • pagination;
  • result limits;
  • selecting relevant fields;
  • references/handles to durable data;
  • application-side summarization where semantics permit it;
  • provider-native retrieval mechanisms.

Context management is orchestration, not merely token counting.

Provider schemas should stop at the adapter boundary

OpenAI, Anthropic, Gemini, and OpenAI-compatible providers represent tools and streamed calls differently.

A multi-provider application benefits from a neutral internal model:

type ToolCall = {
  id: string;
  name: string;
  argumentsJSON: string;
  status: "building" | "ready" | "executing" | "completed" | "failed";
};

Provider adapters translate between native events and this model.

Do not force every provider into the exact wire representation of one API. Normalize application concepts, while preserving provider-specific state when continuation requires it.

Tool calling is not a security sandbox

A model following a JSON schema does not make an operation safe.

Hard boundaries belong outside the model:

model proposes
policy decides
runtime validates
sandbox constrains
backend authorizes

For a filesystem tool, enforce a workspace root. For an HTTP tool, enforce network policy. For a database tool, use an account with appropriate permissions. For destructive actions, require meaningful approval where appropriate.

The safest prompt injection is the one that cannot cross a deterministic boundary even if the model obeys it.

Test the loop as a state machine

Useful tests include:

  • model returns no tool call;
  • one valid call;
  • malformed arguments;
  • schema-valid but unauthorized arguments;
  • user denies approval;
  • tool times out;
  • tool succeeds but response is lost;
  • multiple parallel calls;
  • one of several calls fails;
  • cancellation during execution;
  • model repeatedly requests the same tool;
  • result contains hostile prompt-injection text;
  • context budget is exhausted mid-loop;
  • provider stream ends before arguments are complete.

Deterministic fake providers and fake tools are extremely useful here because real models do not reliably reproduce edge cases on demand.

A compact implementation checklist

A reliable tool system should:

  • expose clear, narrow tool contracts;
  • treat model arguments as untrusted input;
  • validate schema and business authorization separately;
  • keep tool-call IDs stable through execution;
  • wait for complete streamed arguments;
  • gate sensitive calls with deterministic policy;
  • distinguish retryable failure from unknown outcome;
  • protect mutating operations from duplicate execution;
  • bound multi-round loops;
  • control tool-result context growth;
  • preserve provider-specific continuation state when necessary;
  • log metadata without leaking sensitive payloads;
  • test failures and cancellation, not only happy paths.

Where BYOKchat fits

BYOKchat can keep provider-native tool semantics at the adapter boundary while representing calls, approvals, results, and conversation state consistently in shared code. MCP adds another tool source, but the same central rule remains: the model can propose an action; execution authority belongs to the client and tool runtime.

Further reading

Keep reading