Skip to content

feat(cloudflare): run each conversation as one Agents SDK state machine - #736

Closed
mattzcarey wants to merge 2 commits into
mainfrom
feat/cloudflare-coordinator-on-state-machine
Closed

mattzcarey wants to merge 2 commits into
mainfrom
feat/cloudflare-coordinator-on-state-machine

Conversation

@mattzcarey

Copy link
Copy Markdown
Collaborator

Continues discussion #709 and supersedes #700: the Cloudflare coordinator now runs each conversation as one durable state machine on the Agents SDK engine, instead of a drive run plus one attempt run per submission.

Depends on the unreleased Agents SDK engine in cloudflare/agents#2301 (which is stacked on #2274). Draft until an agents release carries it; locally it was built against that branch through a workspace link: override that is not committed.

What changes

  • One run per conversation, flue:conversation@v1, at a fixed address. Its idle phase parks on the run's mailbox — holding no alarm — until admission or an abort sends it a wake, then claims the next runnable head from the submission ledger. Its turn phase processes that submission to settlement and returns to idle, where the next queued head is claimed in the same invocation. The submission ledger stays the record of what was accepted and how it settled (it is shared with the Node target); the machine is its executor.
  • Interruption is a replayed turn. A turn re-dispatched without the in-isolate claim memory reconciles the running row from the canonical stream, exactly as the attempt run did — same reconcileInterruptedSubmission, same classification.
  • A hung turn is abandoned, not fatal. The SDK's per-transition watchdog runs the machine's onCancel, which aborts and orphans the attempt's controller (so a zombie's appends are refused) and returns the machine to idle, where the ledger settles the submission through the timeout classification. Live turns heartbeat the watchdog until the submission's own timeout, which aborts their work from inside.
  • Aborts are honoured in-isolate at once and durably through the mailbox.
  • The generated agent class declares the machine on taskDefinitions; the coordinator loses ensureDrive, ensureAttemptRun, hasLiveAttemptRun and the per-attempt run ids (~600 lines net).

Verified

pnpm check:types (109 tasks), the runtime and vite builds, and the runtime unit tests pass. Live on examples/cloudflare against the engine build: a prompt settles completed; two prompts queued back-to-back settle with the second joined at the turn boundary; an abort mid-response settles aborted and a follow-up prompt completes; killing the dev server mid-response and restarting it replays the turn, which continues from the durable partial stream and settles completed. The machine's row parks waiting on its mailbox with no wake job while the conversation is quiet.

Docs: the "Durable agent execution" section of the Cloudflare target guide and "Cloudflare recovery" in the durability guide describe the new model.

Replace the hand-rolled Cloudflare execution layer — the zero-delay drain
plus 30s heartbeat on schedule(), the in-isolate supervisor chain,
detached runFiber attempt fibers with onFiberRecovered, and live deadline
enforcement with a force-settle grace — with two Agents SDK Task
definitions declared on the generated class:

- flue:drive@v1: one bounded reconcile pass (materialize, finalize,
  recover interrupted rows, claim runnable heads) that starts an attempt
  run per claim and completes; every former arm-the-drain boundary now
  ensures this run and joins it when live.
- flue:attempt@v1: one submission processed to settlement in the handler
  body, with the flue abort controller linked to step.signal and the
  submission's durability timeout as the run deadline. The SDK owns the
  claim backstop, the replay after an interruption (the body reconciles
  from evidence first), and settling over a signal-deaf attempt.

The submission ledger, canonical stream, admission paths, and recovery
classification are unchanged. Requires the agents release that carries
cloudflare/agents#2274 (step.signal, run deadline, onError context).
The Cloudflare coordinator's drive and per-submission attempt runs
become one durable state machine per conversation, flue:conversation@v1,
at a fixed address. Its idle phase parks on the run's mailbox — holding
no alarm — until admission or an abort sends it a wake, then claims the
next runnable head from the submission ledger; its turn phase processes
that submission to settlement and returns to idle, where the next head
is claimed in the same invocation. The ledger stays the record of what
was accepted and how it settled; the machine is its executor.

Interruption is a replayed turn: a turn re-dispatched without the
in-isolate claim memory reconciles the running row from the canonical
stream, the same way the attempt run did. A hung turn trips the SDK's
transition watchdog, whose cancel transition abandons the attempt and
returns the machine to idle. Live turns heartbeat the watchdog until the
submission's own timeout, which aborts their work from inside. Aborts
are honoured in-isolate at once and durably through the mailbox.
@astrobot-houston

Copy link
Copy Markdown

Thanks for the contribution! We're closing this PR and moving the conversation to the discussion: #737

We've moved to a model where bugs and feature proposals are discussed in issues/discussions before code review, so the community can help prioritize and shape the work. Your branch is linked from the new thread so the implementation isn't lost — please join us there to continue the conversation.

— astrobot 🤖

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants