Lab 06 — Wire a tool the model can call
Scaffold: 3/5. The model has a tool and a backend; you build the loop that runs the tool when it asks.
The scenario
In Lab 03 the model filled in a schema and stopped. Here it goes a step further:
given a get_delivery_status tool, it decides on its own to call the tool,
waits for a result, and answers from it. That decide-call-read-continue cycle is
exactly what a managed agent does under the hood. You build it by hand so the
mechanics are not a mystery.
The requirement
- “Where is my order GB-1001?” makes the model call the tool, read the status, and answer “out for delivery, arriving today by 2pm”.
- An unknown order id comes back with an honest “no order found”.
What’s provided
template.yaml— a Lambda allowed to call Bedrock.src/handler.py— the tool schema (get_delivery_status), the mock backend (_ORDERS),_execute_tool(), and the first Converse call. The gap is_resolve(), the tool-use loop.solution/handler.py— the reference answer.scripts/— deploy, test (two order questions plus one the tool cannot help with), teardown.
Your task
Implement _resolve(response, messages). While the model’s stopReason is
tool_use:
- Append the assistant message to
messages. - For each
toolUseblock, run_execute_tool(name, input)and build atoolResultthat references the block’stoolUseId. - Append those results as a
usermessage. - Call
converseagain and loop.
When the model stops asking for tools, return its final text. The protocol is strict about the shapes; the docstring spells them out.
Run it
# Prerequisite: Model access enabled for a tool-use-capable model, in your region.
./scripts/deploy.sh
./scripts/test.sh
./scripts/teardown.sh
What success looks like
Before you write the loop, the function raises NotImplementedError. After,
“where is my order GB-1001?” returns the delivery status, which only exists in
the backend the tool read, and GB-9999 returns a not-found. The third question
has nothing to do with orders, and the model answers it straight off, no tool
call, which is the other half of the lesson: having a tool does not mean using
it. Watch the flow: the model never saw _ORDERS, it asked for the tool and you
fed it the answer.
If it fails
ValidationExceptionabout message order or tool result — atoolResultmust be ausermessage that immediately follows the assistanttoolUse, and thetoolUseIdmust match. Append the assistant message before the result.- Infinite loop or timeout — you must reassign
responsefrom the newconversecall inside the loop, orstopReasonnever changes. AccessDeniedException— enable Model access; some models do not support tool use, so pick one that does.ValidationExceptionabout on-demand throughput / “isn’t supported” — the default model id only invokes directly in regions that offer it on demand (us-east-1is the safe one). Elsewhere, redeploy with the cross-region inference profile id:MODEL_ID=us.amazon.nova-lite-v1:0 ./scripts/deploy.sh.
Reveal the solution
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
What you just learned
- Tool use is a conversation, not a single call. The model asks
(
stopReason: tool_use), you run the tool, you send atoolResult, and it continues; a tool call can trigger another before the final answer. - The model never touches your backend. It emits a request; your code holds the permissions and does the work, which is why the tool’s Lambda (or here, the function itself) is where you enforce least privilege.
- A managed Bedrock agent automates this exact loop, plus planning and memory. Having built it by hand, an agent’s action group is no longer a black box.
- The
toolUseIdstitches request to result; keep it exact or the next turn is rejected.
Next
Lab 07 — Gate ingestion with a data-quality check. You move from serving to the pipeline: raw records land in S3, and you write the check that lets the clean ones through and quarantines the rest before they ever reach a model.