@yansirplus/backend-cloudflare-do
v0.5.16
Published
<!-- generated by scripts/generate-docs.mjs; edit docs/surface.json and docs/packages/backend-cloudflare-do.md -->
Readme
@agent-os/backend-cloudflare-do
Purpose
Cloudflare Durable Object backend for agentOS: app facade, DO storage, transactions, alarms, SSE streaming, dispatch delivery, and Cloudflare binding materialization.
Public API Status
Backend package. It is the only substrate package that imports cloudflare:workers.
Invariant
Cloudflare-specific APIs stay in this backend. Shared kernel/runtime packages must not import Durable Object state, Worker bindings, or alarm APIs.
Application code registers app-owned durable triggers only through the facade
configuration. defineAgentDO({ triggers }) is the Cloudflare DO construction
surface for trigger registration. Pure triggers are registered as an array.
Backend-bound triggers use triggers(ctx) where ctx exposes env, scope,
and DO-local sql; they may close over ctx.sql for app-owned provider
idempotency or custom tables. Materialized current state should use
defineAgentDO({ projections }) instead of trigger-local projection writes. The
backend builds registries, validates duplicate kinds, and uses those same
registries for submit, drain, stream, and projection operations. Apps must not
import runtime-core, due-work, SQL helpers, inserted-event helpers, or
backend state internals.
agent.enqueueTrigger({ triggerKind, payload, at }) is the app-facing trigger
submit primitive. It is a public projection of the backend durable-trigger
commit path: registry lookup happens before any ledger, due-work, or alarm
write.
defineAgentDO({ domains, tools }) boot-validates external tool execution
domains. Deterministic tools need no domain declaration. External tools must
reference a declared domain; duplicate declarations or host domains without explicit
environment allowlists fail during lowering, before submit.
agent.cancelTrigger({ triggerKind, intentEventId, reason }) is the app-facing
best-effort cancellation primitive. It is registry-closed: unknown trigger kind
fails before ledger, due-work, or alarm mutation. Triggers declared with
cancellation: "ignored" return ignored before reading or mutating due-work.
Cooperative pending rows are terminally cancelled in one transaction. Running
rows record a durable cancel request and abort the active in-isolate
AbortController when present; if the acquire already crossed a
non-cancellable external boundary, normal commit may still win. Expired claims
redrive automatically, and a redrive that sees a durable cancel request starts
with AcquireCtx.signal.aborted === true.
The backend adds claim/cancel columns to due_work, but the payload remains the
intent pointer { intentEventId }. due_work is a mechanical buffer, not an
audit source. Cancellation visibility comes from trigger-owned
commitCancelled facts.
Production drain is alarm-owned. Deterministic drain helpers are package-local test fixtures only; production app routes must not call them and no public package subpath exports them.
Durable Object RPC named-stub helpers are exported from
@agent-os/backend-cloudflare-do/do-rpc. Keep them out of the root backend
barrel so browser/consumer protocol types can import the helper without pulling
in the Durable Object implementation graph.
Backend primitives stop at append-only ledger order, trigger identity, tx-local
ledger reads, and materialized projection tables. Custom
product SQL tables and provider idempotency key mappings stay in application
code until at least two independent apps or adapters prove the same shape should
become substrate. Factories must be idempotent in resource acquisition: Durable
Object eviction can cause triggers(ctx) to run again, and the backend does not
promise a stable factory call count.
defineAgentDO({ projections }) registers materialized projection
declarations. Every ledger insert applies matching projection reducers in the
same Durable Object transaction. Reducer failure rolls back the ledger insert.
projectionStatus reports version mismatch as needs_rebuild; projectionRebuild
is explicit and replays rows from the ledger. There is no hidden auto-repair.
defineAgentDO({ streams }) registers attached stream handlers. attachStream
returns WebSocket for mode: "bidi" and SSE for mode: "output_only";
cancelStream({ streamRef, reason }) is the explicit output-only cancel
surface. WebSocket/SSE disconnect only detaches transport. It does not write a
cancelled fact. v1 streams do not support reconnect/resume or hibernation; an
active WebSocket pins the Durable Object alive and costs DO uptime. Long
workspace sessions need a later durable stream log or hibernation substrate.
LLM routes are dispatched through the Effect AI transport. Submit provider-call timeouts pass runtime cancellation through the HTTP transport and still write one terminal ledger abort fact. Provider-side cancellation and billing semantics remain provider-specific; the ledger terminal fact is the substrate-owned bookkeeping boundary.
stuckTriggers(now) is observability over expired claims. It is not a repair
entry point; ordinary drain redrives expired claims by claiming the same due row
with acquireMode: "redrive".
Cross-DO strong consistency is not provided by this backend. Apps choose a single DO scope for strong transaction boundaries, or model cross-scope work as saga/compensation facts until a future coordination substrate exists.
Minimal Usage
Create a DO class from one app-facing facade config. bindings is the only
material declaration surface; LLM routes reference symbolic ids.
import { credential, defineAgentDO, endpoint, openAIChat } from "@agent-os/backend-cloudflare-do";
import { defineTool } from "@agent-os/kernel/tools";
import { Schema } from "effect";
const lookup = defineTool({
name: "lookup",
description: "Look up a symbolic key.",
args: Schema.Struct({ key: Schema.String }),
authority: "read",
admit: () => ({ ok: true }),
execute: ({ key }) => ({ value: key }),
});
export const AgentDO = defineAgentDO<Env>({
bindings: [
endpoint("llm").from((env) => env.LLM_ENDPOINT),
credential("llm-key").from((env) => env.LLM_KEY),
],
llms: {
default: openAIChat({
model: "gpt-4.1-mini",
endpoint: "llm",
credential: "llm-key",
}),
},
tools: [lookup],
});App triggers use runtime trigger types and the same facade registration path as built-in triggers. Pure triggers pass as arrays:
import { defineAgentDO } from "@agent-os/backend-cloudflare-do";
import type { AnyDurableTrigger } from "@agent-os/runtime";
const appTriggers = [imageScanTrigger] satisfies ReadonlyArray<AnyDurableTrigger>;
export const AgentDO = defineAgentDO<Env>({
bindings: [],
triggers: appTriggers,
});Materialized projections use runtime declarations and the facade registration path:
import { defineProjection } from "@agent-os/runtime";
const runs = defineProjection({
kind: "run.workflow",
version: 1,
eventKinds: ["run.requested", "run.completed"],
identity,
state,
identityKey,
identify,
initial,
reduce,
});
export const AgentDO = defineAgentDO<Env>({
bindings: [],
projections: [runs],
});Omit llms for event-only facades. They keep emit, schedule, dispatch,
on, bindings, and extensions, but do not expose submit. Full
SubmitSpec remains on the low-level createAgentDurableObject API.
External consumer apps should install the published internal npm packages, not
source workspace package paths. Use the public package entrypoints only; backend
source subpaths remain outside the contract. Published packages include
dist JavaScript and declaration files for NodeNext and Bundler consumers.
Workspace jobs use Cloudflare host helpers instead of product-owned Sandbox
glue. createCloudflareSandboxWorkspaceEnvResolver({ binding, transport, cwd,
scopePrefix, cleanup, shellFileOperationTimeoutMs }) owns the Cloudflare
Sandbox binding/client composition, validates the DO namespace and client shape,
pins normalizeId: true, disables implicit default sessions, and returns one
WorkspaceEnv lease per scope/runId. Products choose the binding and declare
cleanup policy; they do not configure sandbox sessions/transports or mint
workspace refs. Shell-derived file operation timeout policy comes from
@agent-os/workspace-env-cloudflare, and the resolver only forwards the single
declared override.
Consumers that want the complete Cloudflare workspace-job bridge can install
installCloudflareWorkspaceJobProfile({ workspaceResolver, readProjection }).
The profile only composes existing helpers: workspace resolver, workspace-op
provider install, AG-UI SSE response helpers, and workspace-job HTTP response.
Its default response reader expects the runtime-owned sanitized observability
projection, so consumer responses do not expose raw submitRunId or runtime
numeric run ids.
AG-UI SSE responses are also host composition. @agent-os/ag-ui projects
ledger events into AG-UI frames, @agent-os/sse-http stays a generic SSE
transport, and createCloudflareLedgerAgUiSseResponse combines them for
Cloudflare callers. It accepts either an already chunked ledger SSE stream or a
Web Response; products do not need local ledger-response chunk adapters.
Verification
cd packages/backends/cloudflare-do
bun run test