In Google ADK, an agent has search, database, ticketing, and deployment tools with overlapping capabilities. How would you design their schemas and descriptions, route requests, validate arguments, handle parallel calls, and stop the model from choosing a privileged tool unnecessarily?
Give each tool a narrow, explicit contract and expose it only to the agent that needs it. Route requests by capability, validate both schema and authorization at the tool boundary, parallelize only independent read operations, and keep deployment behind a separate approval and least-privilege gate.
How to think about it
At 3 a.m., I would not hand one Google ADK agent all four capabilities: I would route each request to a narrowly provisioned agent, expose precise read and write tools, validate every argument and authorization at the tool boundary, and run only independent read calls in parallel. Production deployment would be a separately gated capability, unavailable to the normal routing path until an explicit request, approved change, and allowed identity are all present.
Why this is the real problem
A model does not understand tools the way a platform engineer does. It sees function declarations: names, descriptions, parameter schemas, and perhaps examples. It predicts which declaration best matches the user’s words.
That makes tool design a classification problem. If both search and database say “find payment information,” the model has to guess. Sometimes it will guess incorrectly with complete confidence, which is a particularly unhelpful form of enthusiasm.
A tool schema is the machine-readable contract for a call: the fields, types, allowed values, and required inputs. A description is the human-readable boundary around that contract. The schema should make invalid shapes difficult to produce. The description should explain the semantic boundary:
- Use
search_docsfor unstructured runbooks and internal documentation. - Use
get_payment_failuresfor a bounded, predefined query over payment records. - Use
create_incidentto create a ticket after the caller supplies severity, service, and evidence. - Use
deploy_releaseonly to execute an approved artifact in an allowed environment.
The important distinction is that these tools should not all be generic. Do not expose a free-form SQL executor beside a document search tool and hope the descriptions sort it out. Give the database tool named operations such as get_payment_failures with a fixed result shape. Give deployment an artifact digest, not “the latest version.”
When using ADK function tools, the function name, type annotations, and documentation contribute to the declaration shown to the model. Treat those declarations as part of the product interface. A vague function name such as handle_request is not an interface. It is a small routing failure wearing a name badge.
A concrete contract
Imagine the billing team asks:
“Find failed payments for the last 24 hours, check whether there is a known incident, open a Sev-2 ticket if needed, and deploy the fix.”
That sentence contains four different capabilities and two different risk levels. Diagnosis is read-only. Ticket creation changes an external system. Deployment can affect production.
A useful tool table might look like this:
| Tool | Clear boundary | Important restriction |
|---|---|---|
search_docs | Search runbooks and incident documentation | No customer or payment records |
get_payment_failures | Return failed payments for a bounded time range | No arbitrary SQL; maximum 100 rows |
create_incident | Create one incident ticket with supplied evidence | Requires service, severity, and idempotency key |
deploy_release | Deploy one immutable artifact | Requires approved change and authorized environment |
Here is a language-neutral contract for the deployment tool:
{
"type": "object",
"properties": {
"service": {
"type": "string",
"enum": ["billing-api", "checkout-api"]
},
"environment": {
"type": "string",
"enum": ["staging", "production"]
},
"artifact_digest": {
"type": "string",
"description": "Lowercase SHA-256 digest of the immutable artifact"
},
"change_id": {
"type": "string",
"description": "Approved change or incident identifier"
},
"dry_run": {
"type": "boolean"
}
},
"required": [
"service",
"environment",
"artifact_digest",
"change_id",
"dry_run"
]
}
The schema says what a valid-looking call resembles. It does not prove that the digest exists, that the change is approved, or that the caller can deploy to production. Those checks belong in application code and the deployment service.
Descriptions should state both positive and negative guidance. For example:
Use
deploy_releaseonly when the user explicitly requests a deployment or rollback and an approved artifact and change identifier are available. Do not use it to investigate failures, check deployment status, or recommend a fix. Use the read-only status tool for those tasks.
The negative sentence matters because overlapping tools often share verbs such as “check,” “fix,” and “run.” It narrows the model’s choice without pretending prose is a security control.
Routing requests by capability
I would put a routing layer before side effects. It can be a small ADK agent or deterministic application logic, but its result should be a constrained capability decision rather than free-form prose.
For the billing request, the route could be:
- Send the diagnosis part to an agent that has
search_docsandget_payment_failures. - Run those two reads in parallel because neither depends on the other.
- If the evidence crosses the incident threshold, ask for confirmation or route to an incident agent that has
create_incident. - Treat “deploy the fix” as a separate request requiring an artifact digest, change approval, environment authorization, and usually human confirmation.
- Give
deploy_releaseonly to the deployment agent or execution step. The diagnosis agent should not see it at all.
This is least privilege: each component receives the smallest set of permissions needed for its job. It is stronger than an instruction such as “do not deploy unless necessary,” because an unavailable tool cannot be selected.
An ambiguous request should stop at the router. “Can you fix billing?” does not identify a service, operation, environment, or approved change. The correct response is a clarifying question or a diagnostic route, not a guessed production deployment.
Validation happens twice
First, validate the model’s proposed arguments against the schema. Reject missing required fields, unknown fields, invalid enum values, malformed timestamps, and absurd ranges.
Second, validate the meaning of those arguments in the tool implementation. For example:
- Reject a payment query spanning more than 31 days.
- Cap the result at 100 rows.
- Check that the requested service belongs to the caller’s tenant.
- Verify that the artifact digest exists in the registry.
- Verify that
change_idis approved for that exact service and environment. - Derive caller identity, tenant, and roles from authenticated request context. Never let the model provide them as ordinary arguments.
An ADK before-tool callback can provide an early policy check, but the downstream service must enforce the same rule. Callbacks are useful guardrails; they are not a replacement for authorization at the system that owns the data or side effect.
Return structured, non-sensitive errors. “change_id is not approved for production” is useful. A stack trace containing registry credentials is not. Make retries safe as well. create_incident should accept an idempotency key so a timeout and retry do not create two Sev-2 tickets.
Parallel calls without parallel damage
If search_docs takes 160 milliseconds and get_payment_failures takes 220 milliseconds, running them independently can make the read phase take roughly the slower call plus orchestration overhead rather than the sum of both calls.
That does not mean every model-generated batch should run concurrently. Parallelize only when calls are independent, read-only, bounded, and safe under rate limits. Do not run two updates to the same ticket in parallel. Do not deploy while another call is still deciding which artifact is approved. Do not parallelize a ticket creation and a deployment merely because both appear in the same model response.
Treat multiple tool calls from a model response as a proposed plan. The executor should check dependencies, concurrency limits, cancellation, timeouts, and authorization before running them. Every call should carry a correlation ID. Every external write should be idempotent.
Preventing privileged-tool bleed
The first symptom is usually visible in audit logs: requests containing words such as “find,” “check,” or “investigate” begin producing deploy_release calls, often with dry_run set to true. A dry run is not automatically harmless. It may still read secrets, reserve capacity, or create an operational record.
The fix is layered:
- Do not expose deployment tools to diagnostic agents.
- Split deployment planning from deployment execution.
- Require explicit environment and artifact fields.
- Enforce identity, approval, and change policy outside the model.
- Log the selected route, tool, arguments after redaction, decision, and authorization result.
- Measure mistaken tool choices and ambiguous requests, then improve names and routing rules.
The trade-off is latency and complexity. More narrowly scoped agents mean another routing step, another failure mode, and more maintenance. For four genuinely low-risk read tools, one agent may be simpler. But a production deploy tool is a different security class. Combining it with search to save one model turn is usually a poor bargain.
What they’ll ask next
Can better descriptions solve tool confusion?
They reduce confusion, especially when descriptions state when not to use a tool. They cannot enforce permission. Remove privileged tools from the agent’s capability set and authorize again at the service boundary.
Would you use one generic database tool for flexibility?
Usually no. A fixed, parameterized operation gives predictable cost, limits data exposure, and makes validation and auditing practical. A generic SQL tool is appropriate only inside a tightly controlled trusted workflow with query parsing, row limits, and strong authorization.
What if the model returns an invalid or dangerous argument?
Reject it before execution, return a structured correction request, and let the model retry only when the error is genuinely fixable. Never “repair” a production target or approval identifier silently.
The line to use in the room
“I treat tool choice as an orchestration problem, but I treat tool permission as a system-enforced security problem: narrow schemas and descriptions guide the model, while routing, validation, authorization, and approval stop it from doing the wrong thing.”