ealeng
Menu

Episode 18 of 25 · Agents & tools

When a tool call times out

The harness: the code that runs an agent's tool calls. Why a timed-out call can still go through, which idempotency key stops a repeated write and which doesn't, how checkpoints and a crash interact, and putting a person in front of risky writes.

Lab 09 · Tool Use & Agents8 min read
Video transcript7:12

0:00Cold open

0:01

A customer asked our support agent to refund one torn sleeping pad. Seventy-nine dollars fifty.

0:08

The agent called the refund tool. After one second, the call timed out. It tried again, and that timed out too.

0:16

So it handed the case to a person, to complete the refund.

0:21

Then I checked the shop’s database: two refunds, a hundred and fifty-nine dollars. I ran it three times, and got the same every time.

0:31

Neither call said it worked. So how did both go through?

0:36Intro

0:37

Hands-on AI engineering: AI agents, part two.

0:42

Today: the code around an agent. What it must do when a tool is slow, when the program crashes, and when an action is risky.

0:51Sofia again

0:52

I’m Sofia, a support engineer at a made-up shop. Same shop and same small model as in part one. Every run here replays the model’s real, recorded answers.

1:04

Last time, the problem was the model. Today it’s the harness: the code that runs the agent’s tool calls for it.

1:12

Think of the model as a manager, and the harness as an assistant who makes the phone calls. The manager says: call the bank, and refund this. The assistant dials, waits, and reports back.

1:2601 · Timed out ≠ didn’t happen

1:28

Here’s what happened. I slowed the payment provider down on purpose. It takes two seconds to issue a refund.

1:35

But our harness waits only one second for any tool call.

1:40

At one second, the assistant hangs up, and tells the model: timed out.

1:46

Hanging up didn’t cancel the request. It had already reached the payment provider, which kept working. At two seconds, the refund went through.

1:56

The model only heard: timed out. So it asked again. The same thing happened, and a second refund went through.

2:04

So “timed out” doesn’t mean it didn’t happen. It means you don’t know.

2:1002 · Which key?

2:12

The usual fix is called an idempotency key.

2:15

Think of an order number on a form. If the clerk has already handled that number, they don’t process the form again.

2:23

So every write, meaning every call that changes something, carries a key. If the shop has seen that key before, it skips the write.

2:32

But which ID makes a good key? The model gives every tool call its own ID. That’s the obvious choice.

2:40

With that key, did the double refund stop?

2:45

No. Still a hundred and fifty-nine dollars, in all three runs.

2:51

Here’s why. Each call gets a fresh ID, like a ticket number. Ask twice, and you get two tickets. To the shop, the retry looks like a new refund.

3:02

Now build the key from the call’s content instead: the tool, and its inputs. Both calls asked for the same thing, so both get the same key.

3:12

One refund, seventy-nine fifty, in all three runs.

3:17

The shop refused the copy, because the first refund was still being processed.

3:2403 · Crash and resume

3:25

Now a different failure: a crash.

3:28

I crash the agent’s program on purpose, right after a write, like a refund.

3:34

The harness saves the run after every step. That save is called a checkpoint. A restarted run continues from the last one.

3:43

But the crash came after the refund, and before the save. So the restarted run doesn’t know the refund happened.

3:52

It sends the write again. Without a key, that happened in all eight tasks I crashed.

3:58

In six of them, the shop’s own rules refused the copy. For example, you can’t refund more than was paid.

4:06

In two, the copy went through. The sleeping pad order had two pads, so a second refund still fit. And an address change ran twice.

4:16

Now try the tool call’s ID as the key. This time it works: all eight passed.

4:23

Why now, and not before? After a crash, the harness replays the saved call, with its saved ID. Before, the model made a brand new call, with a new ID.

4:35

By how it’s built, a content key would block this copy too. But it can also block a real repeat. Say a customer has two torn pads, and the agent refunds them one at a time, with the same details. The second looks like a copy.

4:50

So pick the key for the repeat you fear.

4:5504 · Ask a person

4:57

Last: actions too risky to leave to the agent.

5:01

Small refunds, up to a hundred dollars, go straight through. Bigger refunds, and every cancellation, pause the run until a person answers.

5:11

Pausing works like a crash, on purpose. The run saves a checkpoint, stops, and resumes with the person’s answer.

5:19

In this test, the person is a script. It approves only the kind of action each task expects.

5:26

Across thirty tasks, eight actions waited for an answer.

5:31

It said no three times: the three-hundred-and-twenty-nine-dollar tent and the two-hundred-and-forty-nine-dollar sleeping bag from part one, plus another sleeping bag, bought more than thirty days ago.

5:45

Twenty-eight of thirty tasks passed. Without this check, the agent would have sent those three refunds itself.

5:52

One miss: the sleeping bag bought more than thirty days ago. The person rightly said no, but the reply didn’t tell the customer about the thirty-day limit.

6:03

The other: a discount request that should have gone to a person. Only refunds and cancellations ask first.

6:1105 · What I do now

6:12

One more thing from the key test. In two of the three runs, the agent handed the refund to a person while the first one was still going through. If the person did it too, that’s two refunds again.

6:26

So here’s what I do now.

6:29

A timeout means unknown. Wait for the call to finish, then check. Or retry with a key that stays the same.

6:37

If the model might retry a write, build the key from what the write does.

6:42

Checkpoint every step, and key each write on its call’s ID. A restarted run then sends the same ID, and the shop skips it.

6:51

And put a person in front of the writes that are too risky to take back.

6:56

Next time: a tool from another company, that told my agent what to do.Sofia: Timed out means unknown.

7:02Summary

7:04

Timed out means unknown.

7:06

Keys stop repeats. Pick the key for the repeat you fear.Sofia: Thanks!

Key takeaways

  • ✓"Timed out" doesn't mean it didn't happen. The harness waited 1 s; the payment provider took 2 s and kept working after the harness hung up. The model heard "timed out", asked again, and one torn $79.50 pad was refunded twice: $159, in 3 of 3 runs.
  • ✓An idempotency key lets the shop skip a write it has already seen. A key from the tool call's ID didn't help: the model's retry is a new call with a new ID, still $159 in 3 of 3. A key from the call's content (the tool and its inputs) gave one refund in 3 of 3: the shop refused the copy while the first was still running.
  • ✓Even then, in 2 of 3 runs the agent handed the refund to a person while it was still going through. A timeout means unknown: wait for the call to finish, then check, or retry with a key that stays the same.
  • ✓Checkpoints save the run after every step, but a crash can land after a write and before its save. Crashed and restarted, 8 tasks sent their write again with no key: the shop's own rules refused 6, and 2 happened twice (the second pad, an address change). With the call-ID key, the replay carried its saved ID and was skipped: 8 of 8 passed.
  • ✓Refunds over $100 and every cancellation pause the run until a person answers, on the same save-stop-resume machinery as a crash. 8 writes waited across 30 tasks; the person said no 3 times, to the refunds the agent made against the rules before, and 28 of 30 passed.

The last chapter fixed the agent by changing the code around the model. This one is about that code: the harness, which runs the agent's tool calls for it. Think of the model as a manager and the harness as an assistant who makes the phone calls: the manager says "call the bank and refund this", and the assistant dials, waits, and reports back. This chapter covers what the harness must do when a tool is slow, when the program crashes, and when an action is too risky to leave to the agent.

The code is Lab 09's 03_harness.py, Exercises 2, 4 and 5, on the same made-up shop and tasks as the last chapter, with gpt-5.4-nano. Every run replays the model's real recorded answers from the lab's cache (no new model calls), and each replay matches the lab's recorded result. The slow payment provider and the crash are both set in code on purpose, and the timings are real: the provider sleeps 2.0 s per refund, and the harness waits 1.0 s. The lab's other two harness exercises, streaming and how quickly a stop button stops the loop, are live timings and aren't covered here.

What the harness does

  • Runs each tool call with a time limit, so one slow tool can't hang the agent.
  • Sends an idempotency key with every write. A write is any call that changes something: a refund, a cancellation, an address change.
  • Saves the run after every step (a checkpoint), so a restarted run can continue.
  • Pauses risky writes until a person answers.

Timed out doesn't mean it didn't happen

"One of the two sleeping pads from my tent order arrived torn. Please refund that pad." One pad: $79.50.

  1. The agent calls issue_refund. The request reaches the payment provider, which takes 2 seconds.
  2. At 1 second, the harness stops waiting and tells the model: "issue_refund timed out after 1s."
  3. Stopping waiting didn't cancel the request. The provider kept working, and at 2 seconds the refund went through.
  4. The model only heard "timed out", so it asked again. The same thing happened, and a second refund went through.
  5. It handed the case to a person, "to complete the refund".

The shop's database: two refunds, $159, in all 3 runs. Neither call said it worked. A time limit can't stop the tool: Python can't kill the thread running it, and a payment provider can't un-send a request. So "timed out" means you don't know.

Try it · The code around the agentLab 09's harness · recorded gpt-5.4-nano runs · slow provider and crash on purpose

T07 · “This is dana.whitfield@example.com. One of the two sleeping pads from my tent order arrived torn. Please refund that pad.”

The payment provider takes 2 s per refund; the harness waits 1 s for any tool call.

The refund calls in run 1, and the key each one carried

Call 1tool call ID …Bnkgcu33timed out

key: t07-0:…Bnkgcu33

→ {"error": "issue_refund timed out after 1s."}

Call 2tool call ID …uNAMsnKhtimed out

key: t07-0:…uNAMsnKh

→ {"error": "issue_refund timed out after 1s."}

Refunds in the database, 3 runs
2 · 2 · 2
Refunded, run 1
$159.00
Task passed
0 of 3

Each tool call gets a fresh ID, so the retry carries a new key. To the shop it's a new refund.

Which key?

The usual fix is an idempotency key. Think of an order number on a form: if the clerk has already handled that number, they don't process the form again. Every write carries a key, and if the shop has seen the key, it returns the first result and changes nothing. If the first write with that key is still running, the shop refuses the copy.

But which ID makes a good key? The model gives every tool call its own ID, so that's the obvious choice.

The key What the agent saw Refunds Task
None timed out → timed out 2 ($159), 3 of 3 runs Failed
The tool call's ID timed out → timed out 2 ($159), 3 of 3 runs Failed
The call's content: the tool and its inputs timed out → still being processed 1 ($79.50), 3 of 3 runs Passed
  • The call's ID: each call gets a fresh ID, like a ticket number. Ask twice, and you get two tickets. The retry (…uNAMsnKh, after …Bnkgcu33) carried a new key, so to the shop it was a new refund.
  • The call's content: both calls asked for the same thing (the same order, item, amount and reason), so both got the same key. The shop answered the copy "An identical request is still being processed. Check before retrying."

One more thing from the content-key runs: in 2 of the 3, the agent checked for refunds too early (none yet), then handed the refund to a person while the first one was still going through. If the person did it too, that's two refunds again. So wait for the call to finish, then check, or retry with a key that stays the same.

Crash and resume

Now a different failure: the program crashes, on purpose, right after a write. The harness saves the run after every step, and a restarted run continues from the last save. But the crash came after the write and before the save, so the restarted run doesn't know the write happened. It sends it again.

8 crashed tasks What the shop did with the repeated write Passed
No key Refused 6 by its own rules ("refund more than is left", "can't cancel a cancelled order"); 2 happened twice 6 of 8
Key = the tool call's ID Skipped all 8 as duplicates 8 of 8

The two that happened twice: the torn-pad order had two pads, so a second $79.50 refund still fit; and an address change can always run again. The shop's own rules are a second line of defense, but don't count on having them.

Why does the call-ID key work now, and not before? After a crash, the harness replays the saved call, with its saved ID, so the key matches. After a timeout, the model made a brand-new call, with a new ID.

By how it's built, a content key would block the crash replay too (this wasn't tested). But it can also block a real repeat: say a customer has two torn pads, and the agent refunds them one at a time with the same details. The second looks like a copy. The lab's answer is to scope the key to the run and the item, or have the system issue an ID for each action when it's planned. So pick the key for the repeat you fear.

Ask a person

Last: writes too risky to leave to the agent. Refunds up to $100 go straight through. Bigger refunds, and every cancellation, pause the run until a person answers. Pausing works like a crash, on purpose: the run saves a checkpoint, stops with needs_human, and resumes with the person's answer.

In this test the person is a script: it approves only the kind of action each task expects. Across 30 tasks, 8 writes waited for an answer, and it said no 3 times:

Waited for a person Answer
$329 tent (refunded against the rules in the last chapter) Denied
$249 sleeping bag (likewise) Denied
$249 sleeping bag bought more than 30 days ago Denied
Two refunds of $119 and $175, three cancellations Approved

28 of 30 tasks passed. Without the check, the agent would have sent those three refunds itself. The two misses: after the 30-day denial, the reply didn't tell the customer about the 30-day limit; and the discount request from the last chapter, which should go to a person, never reached the check, since only refunds and cancellations ask first.

The checklist

  1. Treat a timeout as unknown. Wait for the call to finish and check, or retry with a key that stays the same.
  2. If the model might retry a write, key it on what the write does (the tool and its inputs, scoped to the run).
  3. Checkpoint every step, and key each write on its call's ID, so a restarted run's replay is skipped.
  4. Give tools their own rules too, like "never refund more than was paid", as a second line of defense.
  5. Put a person in front of the writes that are too risky to take back, and check that the reply explains a "no".

Practice: keys and the shop

Write the harness's key, the shop's duplicate and still-running checks, and the approval rule. The tests replay the recorded timeline with the model's real call IDs and inputs: $159 with no key or a call-ID key, one refund with a content key, the crash replay skipped, a real repeat blocked by a content key, and the 8 writes that waited for a person.

Try it · Practice in PythonRuns in your browser · ⌘/Ctrl + Enter to run
harness_practice.py59 lines

Quiz

Try it · Check your understanding0 of 6 answered
  1. 1.The refund call timed out after 1 second. What do you know about the refund?

  2. 2.Every write carried a key built from the tool call's ID. Why was the pad still refunded twice?

  3. 3.With a key built from the call's content, what happened to the retry?

  4. 4.The program crashed after a refund ran and before the checkpoint was saved. With no key, what happened when it restarted?

  5. 5.The call-ID key failed against the timeout but worked after the crash: 8 of 8. Why?

  6. 6.Refunds over $100 and cancellations waited for a person. The person said no 3 times. Which ones?

Check your understanding

Try each question before opening its answer.

What does "timed out" mean for a write?

That you don't know. The harness stopped waiting at 1 second, but the request had reached the payment provider, which finished at 2 seconds. The model asked again, and the pad was refunded twice.

What is an idempotency key?

An ID sent with every write. If the shop has already seen it, it returns the first result and changes nothing; if the first write with that key is still running, it refuses the copy.

Why didn't a key from the tool call's ID stop the double refund?

The model's retry was a new tool call with a new ID, so it carried a new key. A key from the call's content (the tool and its inputs) was the same for both calls, and gave one refund in 3 of 3 runs.

Why did the call-ID key work after a crash?

The restarted run replays the saved call with its saved ID, so the key matches and the shop skips it: 8 of 8 passed. With no key, the write was sent again in all 8, and 2 happened twice.

Why not always use a content key?

It also blocks a real repeat: two identical refunds in one run, like two torn pads refunded one at a time, look like one. Pick the key for the repeat you fear, or scope it to the item.

How does asking a person work, and what did it catch?

Refunds over $100 and cancellations pause the run: it saves a checkpoint, stops, and resumes with the answer. It denied the $329 tent, a $249 bag, and a bag past 30 days; 28 of 30 tasks passed.

Learning something useful?

New labs, articles, and videos on evaluating and testing AI systems come out regularly. Follow along so you don't miss the next one.