fetch.
Before you begin
Make sure that you have the following:- A Secret that holds the API key of your model provider. The examples read it as
env.ANTHROPIC_API_KEY. For more information, see Vars and Secrets. - A working knowledge of Durable Objects, including SQLite storage, alarms, and WebSockets. For more information, see Durable Objects.
How the loop works
The design follows these rules:- One object for each session. Your Worker addresses a session with
env.AGENTS.getByName(sessionId). - Only the
alarmhandler runs steps. RPC methods and thefetchhandler change the status and set the alarm, but they never call the model. Alarms of one object don’t overlap, so two steps can’t run at the same time. - One unit of work for each alarm. A unit is one model call, or the tool calls of one model response. Each unit must finish within the 300-second limit for one handler call, and one
fetchhas a 120-second limit. - Save before you continue. The object saves the model response and its tool calls before it runs tools. It saves each tool result before the next model call. After a crash, the alarm runs again and continues from the saved state.
- A generation number for each run. The object increments
generationon start and on cancel. After eachawait, it compares the number. If the number changed, then a user cancelled or restarted the run during the call, and the object discards the result.
Create the agent
1
Configure the binding and vars
Add a Durable Object binding for the Replace
Agent class and the model settings to the wrangler.json file:wrangler.json
MODEL_NAME with the model ID from your provider.Keep the API key in a Secret, not in vars. The callAnthropic function in a later step also reads an optional LLM_BASE_URL var, which lets you point the App at a mock server for tests.2
Define the schema
The object stores messages and tool calls in two tables:The run state lives in
index.js
ctx.storage.kv under the following keys:status: one ofidle,running,waiting_approval,paused,done,failed, orcancelled.generation: the run counter.steps: the number of model calls in this run.attempts: the number of failed attempts for the current unit.error: the reason for afailedordonestatus, if any.
3
Add the control methods
The control methods change the status and set or delete the alarm. They don’t call the model:
index.js
4
Run one unit in the alarm handler
Each alarm runs one unit. If tool calls are waiting, then the alarm runs them. Otherwise, it calls the model. After a successful unit, the alarm schedules the next one at once:The
index.js
alarm handler does its own retries. It catches model errors, sets the alarm with backoff, and sets the status to failed after MAX_ATTEMPTS. Don’t let the alarm handler throw for expected errors.5
Call the model and save the response
The The A 429 or 5xx response is retryable, and the
callModel method checks the step limit, calls the model, and saves the response and its tool calls in one transaction:index.js
callAnthropic function is a module-level function. It sets a timeout on the request:index.js
alarm handler retries it with backoff, obeying the Retry-After header. Any other 4xx response doesn’t improve with a retry, so the function marks it as fatal, and the run fails at once.To show text while the model writes it, call the model with "stream": true, and send each delta to the sockets. Save only the complete message.6
Run tools and save each result
The A crash between a tool call and its saved result can cause the tool to run a second time. This method handles both kinds of tool:
runTools method runs the pending tool calls of one model response, saves each result, and then adds the results to the conversation:index.js
- Idempotent tools run again after a crash. Give each external side effect an idempotency key. The method passes
${sessionId}:${toolCallId}askey. - Tools that must run at most one time get the
runningstatus, and the method callsawait this.ctx.storage.sync()before the side effect. After a restart, the method doesn’t run arunningcall again. It saves the call as an error and gives that result to the model.
MAX_TOOL_OUTPUT characters before it saves the output.7
Add the helper methods
Add the helper methods to the
Agent class:index.js
8
Define the tools
Define each tool twice: as a JSON Schema in Follow these rules for tools:
TOOLS, which the model receives, and as an implementation in TOOL_IMPL, which the object runs. Also define SYSTEM_PROMPT. The following example has one tool:index.js
- Validate the input before you use it.
- For each tool that calls the network, set a timeout with
AbortSignal.timeout()and a limit on the response size. Don’t let a URL tool call internal addresses. - To run code that the model writes, use a Dynamic Worker with
globalOutbound: null. For more information, see Dynamic Workers.
9
Connect clients
Clients send commands through your Worker and receive events over a WebSocket. The Worker routes each request to the object of the session:In the The object sends each saved event to all sockets through the
index.js
Agent class, accept WebSockets with this.ctx.acceptWebSocket, and send the status and the history on connect. A plain HTTP GET returns the same snapshot:index.js
broadcast method. A socket closes when the object moves to a different node, for example at a deploy. The client must reconnect and load the history again after a close.10
Optional: Require approval for a tool
For a tool that needs approval, save its tool call with the
waiting_approval status, set the run status to waiting_approval, and don’t set the alarm. Add an approve(toolCallId) RPC method that sets the tool call to pending, sets the run status to running, and sets the alarm.Keep the history within the model context
The history grows at each step. Before each model call, check that the history fits the context of the model. Use one of the following approaches:- Send the first user message and the last messages, up to a fixed count.
- Keep a summary row, and send the summary in place of old messages.
tool_use block without its tool_result block, because the API refuses the request.
Also limit the number of steps, the tokens, and the size of each tool result. The example sets MAX_STEPS, max_tokens, and MAX_TOOL_OUTPUT.
Test the loop
Test the loop on the dev server with a mock model server instead of the real provider:- Write a local mock server for the
/v1/messagespath. Return atool_useresponse first, and a text response second. - In the
varskey of thewrangler.jsonfile, setLLM_BASE_URLto the URL of the mock server. Remove the var before you deploy. - Start a run, restart the dev server in the middle of the run, and check that the loop continues.
- Stop the dev server with
kill -9during a tool call, send a preview request to start it again, and check that the tool doesn’t run a second time. - Check that the next step starts less than one second after the previous step ends.
- Cancel a run during a model call, and check that the object discards the result.