CodeRigor

LESSON

A feedback loop with calls, steps and cancellation

Feed tool results back to a mock model, stop on a final answer, and enforce distinct call and model-step budgets with cancellation before effects.

A feedback loop is more than a list of tool calls. The next model turn must be able to observe what the previous tool returned and decide whether to finish, correct an argument or request another action. Our model is a deterministic callback over copied history, so those decisions can be tested without a provider account.

Keep two budgets. A model-step budget counts every callback invocation, including a failed or malformed response. A call budget counts accepted tool attempts, including ones whose dispatcher result is denied or invalid_arguments. Counting failures prevents a model from spending unlimited attempts on requests that never succeed.

Observe before deciding again
start / 3
Each accepted call is followed by its result before the next model turn; final ends the loop.
Observe before deciding again
Each accepted call is followed by its result before the next model turn; final ends the loop.

A final response is allowed after the last permitted tool call, provided a model step remains. Check final before rejecting a further call on budget grounds. Otherwise a useful tool result could arrive at the cap and the application would refuse the model’s valid conclusion.

Reserve each call ID before dispatch. Reusing it within this invocation stops with repeated_call rather than executing again. This is a small in-memory replay guard, not a durable exactly-once guarantee. Real effects across retries or process restarts need an explicit idempotency and persistence design.

Python
calls = 1
max_calls = 1
response = {'kind': 'final', 'text': 'Finished'}
print(response['kind'] == 'final')
print(calls >= max_calls)

Cancellation is checked immediately before and after obtaining a model response. That second check matters because the request may have been cancelled while the model was working. Once observed, cancellation wins before a tool runs, and no unpaired call is added to history.

Pass a copy of history to the model. A test callback that mutates its input must not rewrite previously recorded calls or results. Return a copied transcript too. These ownership choices make the trace trustworthy as a record of this simulated run, while natural-language answer quality still needs a separate evaluation.

Cancellation gets a second gate
start / 3
A request can be cancelled while the model runs; the fresh response must pass a new cancellation check before effects.
Cancellation gets a second gate
A request can be cancelled while the model runs; the fresh response must pass a new cancellation check before effects.
Python
history = [{'kind': 'call', 'id': 'c1'}, {'kind': 'tool_result', 'id': 'c1'}]
seen = {message['id'] for message in history if message['kind'] == 'call'}
print('c1' in seen, len(history))

Test a tool-to-final exchange, a new call beyond budget, a repeated ID, cancellation triggered by the model callback, malformed output and a model exception. Assert status, counts, transcript shape and handler effects together. The budgets bound this synchronous simulation’s turns and accepted calls; they do not establish provider token costs, latency or safe execution of arbitrary real-world tools.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Watch the tool-and-feedback discussion. Our exercises use a deterministic teaching protocol, not a vendor SDK.