Commit 0f80fac
authored
fix: make agent runs durable across worker restarts (#176)
## Summary
- Move the semantic agent loop out of a Worker-isolate promise and into
one deterministic Cloudflare Workflow per run.
- Checkpoint every model turn, tool invocation, transcript publication,
terminal transition, and cleanup so isolate eviction or deployment
resumes completed work.
- Reacquire provider credentials and sandbox capabilities inside each
active step, keeping plaintext secrets out of durable state.
- Reconcile the web client with the persisted terminal transcript when a
browser misses the end of a stream.
## What's Included
### Durable execution
- Add a model-only Mastra turn that exposes tool schemas without
executing them.
- Reconstruct each selected tool through Mastra's cross-process
execution API in a separate Workflow step.
- Replace lease epochs and the volatile stream driver with validated
JSON Workflow state.
### Idempotent publication and lifecycle
- Give Workflow events deterministic keys and atomically receipt
transcript fragments in Durable Object SQLite.
- Recover ambiguous admission, reconcile retained Workflow status, and
convert exhausted or externally terminated runs into explicit retryable
failures.
- Terminate the owning Workflow before cancellation or deletion and keep
cleanup independently retryable.
### Client convergence
- Poll the lightweight thread record only while its run is active and
the tab is visible.
- Stop a stale stream and refresh persisted messages and chat lists when
the authoritative run pointer clears.
## Architecture
```mermaid
sequenceDiagram
participant G as Gateway
participant D as AgentRun Durable Object
participant W as Cloudflare Workflow
participant M as Mastra
participant S as Daytona / external tools
G->>D: admit immutable run payload
D->>W: create deterministic run instance
W->>M: checkpoint one model-only turn
M-->>W: text and tool calls
W->>D: publish turn with idempotency key
loop each tool call
W->>M: reconstruct registered tool
M->>S: execute with step-scoped credentials
S-->>M: serializable result
W->>D: publish result with idempotency key
end
W->>D: commit terminal status and transcript
W->>S: retryable cleanup
```
## Decisions Made
| Decision | Choice | Alternatives considered | Reasoning |
|---|---|---|---|
| Execution owner | Cloudflare Workflow | In-isolate promise; Mastra
in-process durable agent | The existing promise disappears with isolate
eviction. Mastra's built-in durable agent is documented for
in-process/simple deployments and defaults to a hard step ceiling, while
Cloudflare already owns durable execution here. |
| Tool boundary | Model-only turn plus one Workflow step per tool |
Stream the whole Mastra loop in one step | Completed tool calls become
durable checkpoints and tools are reconstructed through Mastra's
intended cross-process API. |
| Transcript delivery | Deterministic event keys plus atomic SQLite
receipts | Best-effort append | A retried publication cannot duplicate
visible assistant or tool parts. |
| Failed retained Workflow | Visible retryable terminal failure |
Restart from the beginning | Blind restart can repeat external side
effects. |
| Secrets | Resolve inside each step | Serialize request context |
Plaintext BYOK credentials and capabilities never enter Workflow
storage. |
## Edge Cases Handled
| Scenario | Handling |
|---|---|
| Worker or Durable Object eviction | Workflow replays completed steps
and continues from the next checkpoint. |
| Ambiguous Workflow creation response | Alarm recovery reuses the
deterministic instance and validates the immutable input hash. |
| Repeated Workflow callback | Durable Object verifies instance
identity; transcript receipts suppress duplicates. |
| Transient tool failure | Tool error is returned to the model so it can
recover semantically. |
| Workflow API temporarily unavailable | Reconciliation is re-armed
without prematurely failing the run. |
| Workflow reports unknown | Three spaced observations are required
before declaring interruption. |
| User cancellation or deletion | Active queued/running/waiting/paused
Workflow is terminated before terminal state is committed. |
| Browser misses terminal stream chunks | Active-run polling stops the
stale stream and replaces transient state with the persisted transcript.
|
## How to Review
1. Start with `agent-run-workflow.ts` and
`agent-run-workflow-runtime.ts` for the checkpointed loop.
2. Review `durable-agent-step.ts` for the Mastra model/tool separation.
3. Review `agent-run-workflow-controller.ts` and `agent-run-output.ts`
for admission, reconciliation, and idempotency.
4. Finish with the web lifecycle hooks that converge a disconnected
client.
## Verification
- [x] `pnpm lint`
- [x] `pnpm typecheck`
- [x] `pnpm turbo build --force`
- [x] `pnpm deadcode`
- [x] `pnpm architecture:check`
- [x] `pnpm turbo skills:build`
- [x] Full documented Docker stack starts under Node 24.18.0
- [x] Gateway liveness returns HTTP 200
- [x] Web root returns HTTP 200 after cold compilation
- [x] Startup logs contain no runtime errors
Production browser acceptance will run after deployment so it exercises
the exact merged Worker and web release.1 parent b33340f commit 0f80fac
36 files changed
Lines changed: 1923 additions & 2295 deletions
File tree
- apps
- agent-worker
- src/durable-objects
- web
- src/components
- chat
- projects
- packages
- agent-core
- src
- mastra
- agents
- types/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | | - | |
28 | | - | |
| 27 | + | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
93 | | - | |
94 | | - | |
95 | | - | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| |||
110 | 110 | | |
111 | 111 | | |
112 | 112 | | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
120 | 119 | | |
121 | 120 | | |
122 | 121 | | |
| |||
240 | 239 | | |
241 | 240 | | |
242 | 241 | | |
243 | | - | |
244 | | - | |
245 | | - | |
| 242 | + | |
| 243 | + | |
246 | 244 | | |
247 | 245 | | |
248 | 246 | | |
| |||
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | 19 | | |
24 | 20 | | |
25 | 21 | | |
26 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
| |||
Lines changed: 8 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
145 | 145 | | |
146 | 146 | | |
147 | 147 | | |
148 | | - | |
| 148 | + | |
149 | 149 | | |
150 | 150 | | |
151 | 151 | | |
| |||
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
222 | | - | |
| 222 | + | |
223 | 223 | | |
224 | 224 | | |
225 | | - | |
| 225 | + | |
226 | 226 | | |
227 | 227 | | |
228 | 228 | | |
| |||
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
246 | | - | |
| 246 | + | |
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | 252 | | |
253 | | - | |
| 253 | + | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
| |||
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
277 | | - | |
| 277 | + | |
278 | 278 | | |
279 | 279 | | |
280 | 280 | | |
| |||
299 | 299 | | |
300 | 300 | | |
301 | 301 | | |
302 | | - | |
| 302 | + | |
303 | 303 | | |
304 | 304 | | |
305 | 305 | | |
| |||
350 | 350 | | |
351 | 351 | | |
352 | 352 | | |
353 | | - | |
| 353 | + | |
354 | 354 | | |
355 | 355 | | |
356 | 356 | | |
| |||
0 commit comments