← Engineering agent harnesses

Part 8 of 18 · 6 min read

Tools as data, execution as a controlled boundary

Compiling tool definitions into handlers, validating backend contracts and keeping model arguments inside authenticated scope.

Arunank Sharan · Kuzushi Labs ·

In this article

A tool definition looks deceptively simple: a name, description and JSON schema. In a product, it also represents a promise about an API, a permission boundary, a timeout, a side effect and a result the model must interpret correctly.

The client platform stored tool definitions as versioned configuration, then compiled them into runtime handlers. This made the tool catalog editable and inspectable without pretending that executable behaviour was just prompt text.

Tool configuration compiles into a handler; calls pass through validation, policy, scope and bounded execution.

Four tool families

The implementation distinguishes declarative backend bindings, registered Python handlers, asynchronous workflows and client presentation tools.

FamilySuitable useRuntime behaviour
Backend bindingA bounded internal API read or writeCompile method, path and parameter mapping into a handler
Registered codeMulti-step domain logicCall a known implementation such as booking or issue flow
WorkflowWork longer than a conversational turnStart a workflow and return its identity/status
Client presentationCard, chart or quick replyProject a UI event rather than perform a domain mutation

Most simple API operations can be data-driven. Complicated workflows should not be forced into an increasingly obscure binding language just to keep the catalog “no code.” A small registered handler is often clearer than a declarative structure that has quietly become a programming language.

Compile definitions, do not evaluate arbitrary text

A published tool row contributes its name, description, parameter schema, type and policy metadata. A known compiler selects the appropriate handler type. It does not execute arbitrary Python pasted into an end-user message.

A standalone catalog-search example makes the binding contract concrete:

{
  "name": "example_catalog_search",
  "type": "backend_binding",
  "mutability": "read",
  "method": "GET",
  "path": "/example/catalog",
  "parameter_map": {"search": "q", "limit": "per_page"},
  "scope": ["account_id"]
}

The mapping is important. If the model supplies search but the API reads q, a successful HTTP response can still ignore the requested search. These are contract failures that a generic “tool call succeeded” metric will miss.

The executor pipeline

The executor resolves the tool, strips protected tenant/account keys from model arguments, validates arguments, checks policy, handles confirmation or approval, constructs trusted context and executes under bounded resources.

Tool errors are returned in-band so the model can recover or explain the failure. An invalid date should not crash the entire service. An unknown tool should not be dynamically discovered from the network merely because the model requested it.

Bound execution with a resource-specific concurrency limit and a deadline that includes queue wait. A handler can be fast once admitted while users spend most of their time waiting for a slot. Measure queue duration separately, reject work whose useful deadline has passed, and release capacity when a caller cancels. The performance chapter develops a worked capacity and deadline budget.

Tenant scope comes from the service

The model can propose a booking reference, but it cannot choose an account. Protected tenant/account arguments are removed and authenticated context is supplied separately. Domain-specific bindings and flows then enforce the relevant entity scope.

def scoped_args(model_args, trusted_scope):
    protected = {"tenant_id", "tenantId", "account_id", "accountId"}
    clean = {k: v for k, v in model_args.items() if k not in protected}
    return {**clean, **trusted_scope}

assert scoped_args(
    {"account_id": "other-account", "listing_id": "other-property"},
    {"account_id": "account-a", "listing_id": "property-a"},
)["listing_id"] == "property-a"

This example demonstrates overwrite semantics. It is not a substitute for backend authorization. The backend must verify that the trusted account may access the referenced entity. A parameter dictionary with a correct account_id does not make an unscoped database lookup safe.

Scope policy also depends on the operation. A host inventory search can range over the account’s properties. A guest retrieving access instructions should be pinned to an authorised stay. The same generic “overwrite every ID” rule cannot replace domain-specific authority.

Tool descriptions are decision surfaces

The description tells the model when to call the tool, which prerequisites it needs and what the result means. That makes description changes behaviour changes, even if the schema is unchanged.

For booking, a useful description can explain whether the tool handles missing dates, whether it validates calendar availability and whether a returned link means checkout rather than confirmation. For task creation, it should distinguish a proposal from a created record.

Descriptions should not become competing copies of business rules. If a deterministic flow owns the dispatch ladder, describe the flow’s contract. Do not also ask the model to independently recreate that ladder from six lower-level tools.

Validate the running contract

The operator portal includes a tool-contract surface. Its value is joining three facts: the published definition, the handler actually loaded in the running registry and the backend’s expected parameters.

That join can reveal a dead route, an ignored argument, a missing required parameter, a handler override or a tool absent from the agent allowlist. Each has a different correction. Looking only at a database row cannot tell you which function is executing.

A good contract report records both model-facing names and backend-facing names. It should also show which fields are filled from trusted scope and which must come from the model. This prevents a required parameter from being silently dropped during binding.

Parallel calls need semantic independence

The executor can gather multiple tool calls concurrently behind its semaphore. That improves independent reads. It does not make arbitrary writes safe to parallelise.

Searching properties and retrieving general rules may be independent. Creating a task and assigning a slot to that task are not. A workflow with mandatory order belongs in a composite handler, or must explicitly encode dependencies before execution.

The same issue appears with approvals: a batch containing an approval-gated write and an unrelated write may partially execute before the run pauses. A strict approval design should validate the entire batch before performing irreversible work. That is a stronger guarantee than detecting an approval marker after collecting results.

Return actionable errors

An error should identify what failed and what the model can safely do next. “Invalid request” gives little help. “Check-out must be after check-in; ask for corrected dates” supports recovery.

Errors must not expose credentials or another tenant’s data. Tool traces should preserve enough structured detail to diagnose the failure while redacting sensitive values. The guest-facing explanation can then stay concise.

The tool layer is where flexible language becomes application behaviour. Its quality depends on schema, scope, policy, backend semantics and evidence—not just whether the model selected the right function name.