Skip to main content
Laravel, thinking fast.
Chapter 6 · Slow Is the Default

Telling the User What Is Happening

Julian Beaujardin

A minute of silence needs filling with something better than three animated dots, and what you can honestly say is shaped by how the toolset runs.

When tools execute on the provider's side, the tool call and its result arrive together — so you only learn a step happened after it finished:

// What the turn has just done, for the waiting indicator. The toolset runs
// server-side, so the use and the result arrive together and we only ever
// learn about a step once it has finished — hence "did", not "doing".
$state['activity'] = self::lastStep($toolUseBlocks) ?? ($state['activity'] ?? '');

Past tense, therefore: checked that address, not checking that address. Claiming the present tense would be a small lie, and it desynchronises from reality the moment a step takes longer than the poll interval.

The second detail is subtler and bites everyone once:

// Returned as a bare key, never a sentence: the job runs on the queue with no
// request locale, so translating here would pin every visitor to the app
// default. The widget resolves it.

A queued job has no request context. No session, no locale, no authenticated user unless you passed one. Translate a string inside the job and every user gets whatever the app default is, regardless of the language they are typing in. Emit a key; let the layer that has a locale turn it into words.

Finally, degrade to silence rather than to jargon. The step names are matched against a known list, and an unrecognised one returns null — so a rename upstream shows nothing rather than printing an internal tool name at a customer.

What This Chapter Argued

  • Ninety seconds is the normal case, not the bad case. Design for it rather than hoping.
  • A model turn that times out costs the whole turn. You pay for work the closed connection threw away.
  • Put the turn on a queue and let the front end poll a published state.
  • Set tries = 1. Retrying a turn that has spent money and called tools is not resilience.
  • Nest three clocks: per-call timeout inside your own wall-clock deadline inside the job timeout — with a margin so the job always reaches its final write.
  • Derive the per-call timeout from what remains, and refuse to start a call there is no time to finish.
  • Publish after every iteration, with an explicit done flag as the contract with the client.
  • Checkpoint, and write a real failed() that recovers partial state, sets done, and clears its own keys.
  • Make stopping cooperative and check it before the call, so at most one round trip is wasted — and treat stopped as its own state, not an error.
  • A queued job has no locale. Emit keys, resolve them where the request context lives.
  • Say what just happened, in the past tense, and degrade to silence rather than showing internal names.