Why architecture matters here

Tool quality is agent quality, more than prompt quality is. Measured failure modes of production agents cluster at the tool boundary: wrong tool selected (description ambiguity), right tool with malformed intent (schema too permissive — the string that should have been an enum), results that poison the context (a 200KB JSON dump the model must drag through every subsequent turn), and errors the model cannot act on (a stack trace where 'retry after 30s' was the actionable truth). Every one of those is a design defect in the tool layer, fixable without touching a prompt — which is why mature teams treat the tool catalog as the primary engineering surface and the instruction as secondary tuning.

The declaration-is-a-prompt principle has concrete consequences. Descriptions are read by the model at every turn: they must say when to use the tool, when not to, and what the arguments mean in the domain's terms — 'order_id: the 8-digit numeric ID from the order confirmation, not the shipment tracking number' prevents a real class of wrong-argument calls. Parameter schemas are enforcement: enums close open sets, required-vs-optional shapes the model's defaults, and constrained types (via structured-output machinery underneath) make whole hallucination categories ungenerable. And the number of tools is a routing budget: selection accuracy degrades as the catalog grows, which is why toolsets, sub-agents with narrow catalogs, and per-context tool filtering are architecture rather than tidiness.

Finally, tools are the security boundary. The model is untrusted input's playground — prompt injection rides in on every fetched document — and the tool layer is where ambition meets permission: per-tool credentials scoped minimally, user-consent flows for sensitive actions, argument validation in callbacks, and result sanitization for content from the outside world. The agent's real capability envelope is exactly the union of what its tools permit; designing that envelope deliberately is the difference between an incident and a log line.

Advertisement

The architecture: every piece explained

Top row: the function-tool pipeline. A typed function — def lookup_order(order_id: str, include_items: bool = False) -> dict with a docstring — becomes a declaration: ADK extracts name, description (the docstring), and a JSON-schema of parameters from the signature; return-type hints inform the result contract. At each LLM turn, the agent's toolset is rendered into the request; tool selection is the model's move — it emits function calls (possibly several in parallel where independent) — and execution dispatches them: async functions awaited on the event loop, sync functions shunted to executors, results returned as function responses that re-enter the context for the model's next step. Errors are results too: a raised exception becomes an error payload, and the difference between a useful one ('inventory service timeout; safe to retry') and a stack trace is whether the model recovers or flails.

Middle row: context and the integration ladder. ToolContext is the implementation's side channel: read/write session state (scoped keys), load/save artifacts (the big-payload escape hatch), access the invocation's auth info, and signal actions — all invisible to the model's schema. Built-in tools plug platform capabilities (grounded search, code execution) with their own execution paths. MCP toolsets connect to Model Context Protocol servers — stdio subprocesses locally, HTTP remotely — and surface each server tool as a native ADK tool, schema and all; connection lifecycle and (for remote) OAuth are handled at the toolset boundary, and one agent can mount several servers with per-toolset tool filters. OpenAPI toolsets compile a REST spec into one tool per operation — instant breadth, which is exactly why they need filtering (mount the six operations the agent needs, not all 214) and description curation (specs written for developers read poorly as prompts).

Bottom rows: time and trust. Long-running tools break the call-and-wait shape: the tool returns a pending marker immediately (with an operation handle in state), the agent tells the user work is underway, and a later event — human approval, job completion webhook — resumes the session with the real result; the pattern that keeps approvals and batch jobs from holding a turn hostage. Tool auth operates per tool: service credentials injected from secret managers at call time, user-delegated OAuth flows (the tool triggers consent, the token binds to this user's session), and the callback lattice enforcing which arguments this user may use. The ops strip is the daily discipline: result-size budgets (summarize or artifact anything past ~4KB), documented error contracts per tool, and eval cases covering every tool's happy path and its two most likely failures.

ADK tools — the agent's hands: functions, toolsets, MCP, OpenAPIdeclaration is the contract the model readsFunction toolstyped Python/Java funcsDeclarationsschema from signature+docstringTool selectionmodel picks by descriptionExecutionasync, parallel callsToolContextstate, auth, artifactsBuilt-in toolssearch, code execMCP toolsetsexternal servers mountedOpenAPI toolsetsREST specs → toolsLong-running toolspause + resume patternTool authper-tool credentials + user consentOps — result-size discipline + error contracts + tool eval coveragecontext inexposemountgeneratepauseextendsecureoperateoperate
ADK tools: typed functions become model-readable declarations; toolsets mount MCP servers and OpenAPI specs; ToolContext carries state and auth.
Advertisement

End-to-end flow

Build the tool layer for a customer-operations agent, end to end. The catalog: four function tools (lookup_order, update_shipping_address, issue_refund, get_policy), one MCP toolset (the company's ticketing system exposed by an internal MCP server), and an OpenAPI toolset filtered to three operations of the logistics provider's API. Total surface: 9 tools — deliberately under the routing-accuracy knee measured in evals.

A conversation exercises the machinery. 'My order 45BX-9 arrived at the wrong address — fix it and make sure the replacement ships express.' The model calls lookup_order("45BX-9"); validation in before_tool catches the format (the schema said 8-digit numeric; the description told the model where users find it; the model re-asks and the user supplies 45810923). The lookup returns via ToolContext-trimmed payload — the implementation fetched 30KB from the order service, stored the full document as an artifact, and returned a 900-byte summary with the artifact reference; the context stays lean for the rest of the session. Next the model calls two tools in parallelget_policy("address_change") and the logistics toolset's get_shipment_status — independent lookups the runtime executes concurrently, halving the turn's tool latency.

The address change is sensitive: update_shipping_address is configured with user-delegated auth against the order system. First use in this session triggers the consent flow — the user approves scoped access in a popup; the token binds to their session; the tool executes as them, and the order system's audit log shows the user's identity, not a service account. The express-replacement request routes through the ticketing MCP toolset's create_ticket — but shipping upgrades above policy require human signoff, so the tool is long-running: it returns pending with an approval handle, the agent says 'I've requested the express upgrade — you'll get a confirmation within the hour', and when the supervisor approves in their queue, the callback resumes the session, the model composes the confirmation, and the transcript shows one coherent conversation spanning ninety minutes of wall-clock and three systems. Every hop — validation, artifact trimming, parallel calls, consent, pause/resume — was tool-layer architecture doing its specific job.