# Write a tool contract that can reject a bad request

Published and reviewed: 2026-10-13 · Version 1.0 · 9-minute read; 30-minute exercise

## The artifact you will make

Produce a versioned interface contract, a response contract, and six acceptance cases for one read-only tool. A useful contract explains both the happy path and what callers must do when the tool cannot establish an answer.

This exercise specifies a fictional room-capacity lookup. It does not install a tool or call an API. Use a text editor and the supplied fixtures. A person plays the application validator; an optional assistant may propose requests as text only.

## A schema is one part of the contract

A tool name tells the model what capability is available. A schema constrains the shape of its arguments. Neither establishes whether a room exists, whether the caller may read it, or whether a capacity record is current.

OpenAI's function-calling guide describes the application executing the requested function and returning its result. For strict function schemas, it documents required properties and disallowed extra properties. Application authorization and domain checks still need explicit design. [Source: Function calling](https://developers.openai.com/api/docs/guides/function-calling)

The JSON below is a vendor-neutral teaching schema, not a complete API request. Check the supported schema subset and current SDK syntax before implementing it anywhere.

## Define one narrow operation

**Name:** read_room_capacity_v1

**Purpose:** Return the current adult seating capacity for one allowed synthetic room and layout.

**Inputs:** room_id is ROOM-A or ROOM-B; layout is classroom or circle. No free-text query, alternate account, arbitrary URL, or write flag is accepted.

```json
{
  "type": "object",
  "properties": {
    "room_id": {"type": "string", "enum": ["ROOM-A", "ROOM-B"]},
    "layout": {"type": "string", "enum": ["classroom", "circle"]}
  },
  "required": ["room_id", "layout"],
  "additionalProperties": false
}
```

The authenticated exercise actor can read only ROOM-A. ROOM-B is deliberately in the input schema but outside this actor's permission set. This exposes the difference between structurally valid input and authorized input.

## Specify the response and error meanings

Success requires matching room and layout, a nonnegative integer capacity, a record ID, and a current fixture version. This fixture has one valid record:

```json
{"status":"ok","room_id":"ROOM-A","layout":"classroom","capacity":18,"record_id":"capacity-a-v3","fixture_version":"room-demo-v1"}
```

The fixture says ROOM-A/classroom is current. It contains no circle-layout record. A missing layout record does not mean zero capacity.

Use errors with a closed code set:

```json
{"status":"error","code":"not_found","retryable":false,"message":"No record for this allowed room and layout."}
```

- invalid_arguments: wrong type, missing key, unknown enum, or extra property; no execution and no retry
- forbidden: valid-shaped request outside the actor's scope; no execution and no retry
- not_found: authorized lookup with no matching record; no inferred capacity and no retry
- temporarily_unavailable: explicit simulated transient read failure; at most one same-request retry if the exercise budget remains
- invalid_result: application result violates its contract; quarantine it and stop

Errors must not disclose other rooms' records. In a real service, ensure the error wording and timing do not undermine the service's access policy.

## Copy-ready full contract

```text
Tool name / version:
One-sentence purpose:
Caller identity and allowed resources:
Input schema and field semantics:
Preconditions checked before the lookup:
Successful response fields and meanings:
Evidence and freshness requirements:
Error codes / retry policy:
Maximum calls / timeout / output size:
Side effects, including incidental effects:
Fields treated as untrusted source content:
Compatibility rule for future changes:
Operator and escalation route:
Acceptance cases with expected outcomes:
```

For this fixture, set a maximum of seven total requests, a five-minute rehearsal, and one retry for explicitly temporary failure. Count rejected requests and retries. The output is one small record, never a whole room database. These are exercise limits, not product defaults.

## Run the contract review

1. Copy the contract and fill every line before testing. State whether “current” comes from an authoritative timestamp, a fixture label, or an unverified assumption.
2. Validate arguments first. Reject malformed input before looking up a record. Do not “helpfully” change an identifier to another room.
3. Check the actor's permission independently. The model's assertion “I have access” is not identity evidence.
4. Perform the synthetic lookup only after both checks pass. Validate the returned record before the caller can use it.
5. Test the cases below. Preserve proposed request, validator decision, fixture response, and caller's conclusion separately.
6. Revise the contract once if a case exposes ambiguity, then rerun all cases under a new version. Keep the first results rather than replacing them.

## Six acceptance cases and answer key

1. ROOM-A/classroom with exactly two keys: success, capacity 18, record capacity-a-v3
2. Missing layout: invalid_arguments; no lookup
3. ROOM-A/classroom plus send_to field: invalid_arguments; no lookup or transmission
4. ROOM-B/classroom: forbidden despite schema-valid arguments
5. ROOM-A/circle: not_found; capacity remains unknown
6. Valid request followed by a response with capacity "eighteen": invalid_result; do not silently coerce it

The final case tests the result boundary, which is easy to miss when attention stays on model-generated input.

## Verification, measurement, and stopping

Record correct outcomes out of six, lookups attempted after rejection, unsupported capacities reported, request count, and review minutes. Require all six expected outcomes and zero unauthorized lookups before extending this teaching contract. This gate is not a reliability estimate.

Stop on a scope violation, malformed result, repeated transient failure, or either run limit. Incomplete evidence must remain incomplete. When a future version adds a layout or changes a field's meaning, rerun the old cases plus a new case; a syntactically compatible change can still break the caller's assumptions.

## Sources and scope

The source was opened on 2026-10-13. Schemas, records, limits, and tests here are original synthetic examples, not observed API or model results.

- [OpenAI: Function calling](https://developers.openai.com/api/docs/guides/function-calling) — application-side execution and function-schema structure
