Sunil Pai
spai@cloudflare.com
90d · built 2026-05-28
90-day totals
- Commits
- 264
- Grow
- 40.1
- Maintenance
- 33.1
- Fixes
- 13.2
- Total ETV
- 86.3
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 18 %
- By Growth share
- Top 22 %
30-day trajectory
Last 30 days vs. the 30 days before. Up arrows on Growth and ETV mean improvement; up arrow on Fixes share means more time on fixes (worse).
Daily performance
Daily ETV, stacked by Growth, Maintenance and Fixes.
Work-mix over time
Share of Growth / Maintenance / Fixes over a rolling 7-day window. Reads as 'where is effort flowing right now'.
Bug flow over time
Monthly bug flow attributed to this developer. The left bar (red) is bug impact this dev authored that was addressed in the given month — combining bugs others fixed for them and bugs they fixed themselves. The right bar is fixes they personally shipped that month, split between self-fixes (overlap with the red bar) and fixes done for someone else. X-axis is fix-time, not introduction-time — the Navigara API attributes bugs backward to the author at the moment the fix lands.
- Self-fix share
- 65%
- Bugs you introduced
- 29.4
- Bugs you fixed
- 20.3
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
| Repo | Commits | ETV |
|---|---|---|
| agents | 200 | 83.0 |
| cloudflare-docs | 13 | 2.7 |
| workers-sdk | 1 | 0.7 |
Most impactful commits
Top 20 by ETV in the 90-day window.
- 3.4ETVfeat(observability): replace console.log with diagnostics_channel, add typed subscribe helper and ai-chat events (#1024) * Use diagnostics_channel for agent observability Replace console.log-based observability with Node's diagnostics_channel API. Events are now published to named channels (agents:state, agents:rpc, agents:message, agents:schedule, agents:lifecycle, agents:workflow, agents:mcp) via a new channels map and a getChannel(type) helper that routes event types to the appropriate channel. The genericObservability.emit implementation now publishes events to diagnostics_channel instead of printing them, and the previous local-mode console-printing logic and getCurrentAgent usage were removed. A changeset was added documenting the change and noting that messages are forwarded to Tail Workers in production. * Use diagnostics_channel and add typed subscribe Replace console.log-based observability with Node's diagnostics_channel API and update docs/changeset. Events are now published to named channels (agents:state, agents:rpc, agents:message, agents:schedule, agents:lifecycle, agents:workflow, agents:mcp) instead of unconditionally logging to stdout. Add a ChannelEventMap type and a typed subscribe(channel, callback) helper that returns an unsubscribe function. Changes also document Tail Worker integration where published events are forwarded to production tailing. * Observability: diagnostics_channel & typed events Replace console.log observability with Node diagnostics_channel and stricter typed events. Breaking changes to agents/observability types: BaseEvent no longer includes id or displayMessage and payloads are now strict types; Observability.emit signature removed the optional ctx parameter (emit(event: ObservabilityEvent): void). Exported ObservabilityEvent type and refined per-channel unions (including new error event types such as rpc:error, schedule:error, queue:error). Add Agent._emit helper to auto-generate timestamps and replace ~20 inline emit blocks (removed nanoid usage and per-call ids). Update MCP observability emissions to drop legacy fields. Docs updated to describe named channels, typed subscribe helper, Tail Worker integration, and event reference. Tests and example agents cleaned up to stop overriding observability. Bump agents package to minor. If you implement a custom Observability, update your emit signature and narrow on event.type before accessing payload fields.github.com-cloudflare-agents · e9ae0701 · 2026-02-28
- 3.3ETVfeat: add Workspace class — persistent virtual filesystem for Agents (#1069) * feat: add workspace mixin + refactor mixin patterns to use typeof Agent * Add just-bash and refine fiber typings Add just-bash as a dependency in packages/agents and refine the fiber mixin typings. Introduce a FiberAgentClass generic constructor return type so consumers extending the mixin retain FiberMethods on `this`, broaden spawnFiber's methodName to string, export FiberMethods for external use, and add several internal tracking properties to the interface. Update a test to pass a string methodName accordingly. * Add Workspace class and tests Introduce a new Workspace class that provides durable hybrid file storage (SQLite inline + optional R2 for large files) and optional bash execution. Replaces the previous withWorkspace mixin pattern with a class-based API (usage: new Workspace(this, { namespace, r2, r2Prefix, inlineThreshold, bashLimits })). The Workspace includes namespace validation, per-host registration to avoid duplicate namespaces, a scoped SQL helper, lazy table initialization, and R2 key prefixing. Add TestWorkspaceAgent exposing workspace operations, a comprehensive vitest suite (workspace.test.ts) covering file I/O, dirs, rm, listing, path normalization and bash integration, and wire the agent into the test worker and wrangler config. Add just-bash as a dependency in package.json and include it in the test runtime config. Update changelog (.changeset) to document the new Workspace class. * Add Workspace class; rename idPrefix -> r2Prefix Introduce a new Workspace class providing durable file storage for Agents with a hybrid SQLite+R2 backend and optional just-bash execution (usage: new Workspace(this, { r2, r2Prefix })). Update package exports, tests, and workspace implementation to use the new API (idPrefix renamed to r2Prefix). Also update package metadata and regenerate package-lock.json to reflect dependency changes and added example entries. * Add experimental Workspace and workspace-chat demo Introduce an experimental Workspace virtual filesystem (hybrid SQLite+R2) under agents/experimental/workspace with BashSession, persistent cwd/env sessions, streaming I/O, symlinks, change events, and diagnostics/observability hooks. Add design documentation (design/workspace.md) and register it in design indices. Add a workspace-chat example (frontend, server, configs, vite/wrangler, types) demonstrating AIChatAgent integration and tools (read/write/list/mkdir/bash/glob). Expose the workspace API under the package exports as ./experimental/workspace, add observability helpers and tests updates, and make a small example model comment tweak in the OpenAI SDK sample. * Persist user ID and use for agent name Add a STORAGE_KEY and getUserId() helper that retrieves a persisted UUID from localStorage (or generates one with crypto.randomUUID() and stores it). Fall back to "default" when window is undefined. Use the returned ID as the agent name when initializing the WorkspaceChatAgent so the client retains a stable identifier across sessions. * chore: changeset bump to patch * Delete fix-preserve-server-messages.md * Add turndown stub and Vite alias Introduce a minimal TurndownService stub (remove and turndown methods) at experimental/workspace-chat/src/turndown-stub.ts and update vite.config.ts to import node:path and alias 'turndown' to the stub. This avoids pulling in the real turndown package for the workspace-chat experiment during bundling.github.com-cloudflare-agents · b5238de6 · 2026-03-05
- 3.2ETVfeat: worker-bundler package — runtime app bundler with asset handling (#1079) * Add worker-bundler package and playground Introduce a new packages/worker-bundler package (bundler, config, resolver, installer, transformer, types, utils, build script and tests) and an experimental Worker Bundler Playground app under experimental/worker-bundler-playground. The playground includes a React/Vite UI, a Durable Object AI agent (WorkerPlayground) that uses @cloudflare/ai-chat and @cloudflare/worker-bundler to generate, bundle and load Workers at runtime, plus tooling (vite.config, wrangler.jsonc, tsconfig, styles, README). The agent exposes callable tools to generate and test Workers and persists source files to a Workspace. This change wires up the new package in the repo and adds example/demo app to exercise the bundler end-to-end. * Add createApp app bundler and playground support Introduce full-stack app bundling and preview support: add createApp API and app bundler (packages/worker-bundler/src/app.ts) to build server Workers + client bundles + static assets, asset manifest/storage handling, and optional Durable Object wrapper. Update build script to prebundle asset runtime code and ignore generated file. Extend playground UI and agent to use "app" terminology, support assets, preview iframe proxy, tabs (preview/source/test), and new callable tools (generateApp/testApp). Update wrangler config, README with createApp/docs and examples, and add tests and mime/asset-handler sources. These changes enable generating, bundling, persisting, and previewing full-stack apps in the playground. * Mark worker-bundler experimental and adjust builds Add an experimental warning utility and surface it in the worker-bundler API, plus standardize build configs across packages. Changes include: add src/experimental.ts with showExperimentalWarning(), call it from createApp and createWorker, and add an experimental note to the worker-bundler README; add a ts-expect-error in the playground server for the experimental API; and update build scripts for agents, ai-chat, codemode, and hono-agents to move skipNodeModulesBundle/external into a deps object using neverBundle. These updates warn users about the package's unstable API and unify dependency bundling behavior.github.com-cloudflare-agents · 91d3ebab · 2026-03-07
- 3.1ETVEnable alarm-backed APIs in sub-agents (#1418) * Add sub-agent alarm recovery support Made-with: Cursor * Tighten facet cleanup bookkeeping Made-with: Cursor * Hide internal schedule storage fields Made-with: Cursor * Stabilize destroy cleanup schema test Made-with: Cursorgithub.com-cloudflare-agents · 8de0ce39 · 2026-04-30
- 2.5ETVfeat: experimental sub-agent API and examples (#1060) * Add experimental sub-agent API and examples Introduce an experimental sub-agent system and integrate it into example gadgets. Adds a new RFC (design/rfc-sub-agents.md) describing sub-agents, plus an implementation (packages/agents/src/experimental/sub-agent.ts) with a withSubAgents mixin and typed RPC stubs. Refactors Agents package (index/build/scripts/tests) and adds tests for sub-agents. Update example apps to use sub-agents: experimental/gadgets-chat now uses SubAgent facets, a StreamRelay RPC target, and a client-side AgentChatTransport that supports streaming, cancel, and resume; other gadgets and servers updated to the new API. Misc: docs/README and design listings updated and various package metadata/build changes to wire everything together. * docs: migrate gadgets READMEs to sub-agents Replace experimental Durable Object "facet" docs with the new sub-agent pattern across four READMEs. Updated terminology, diagrams and TypeScript examples to use SubAgent / withSubAgents, added Key Pattern snippets, streaming protocol details (chat), and Related links. Files changed: experimental/gadgets-chat/README.md, experimental/gadgets-gatekeeper/README.md, experimental/gadgets-sandbox/README.md, experimental/gadgets-subagents/README.md. Primarily documentation updates to reflect API/architecture changes (no functional code changes).github.com-cloudflare-agents · 054e65d1 · 2026-03-04
- 2.3ETVAdd retained streaming agent tools (#1421) * Add agent tool orchestration Introduce first-class agent tools for running chat-capable Think sub-agents from a parent agent. This adds the parent run registry, event replay, cleanup, cancellation wiring, the AI SDK `agentTool` wrapper, React event aggregation, and the Think child adapter needed to stream retained child timelines through the parent connection. Rewrite the agents-as-tools example to consume the public APIs instead of the old helper-event prototype, and refresh docs, READMEs, design notes, tests, and release metadata so the feature is discoverable as the supported agent tools surface. Made-with: Cursor * Support AIChatAgent agent tools Extend the agent-tool child adapter contract to AIChatAgent so existing chat agents can run as retained, streaming tools with durable inspection, replay, and cancellation. Also update the shared live-tail transport for Durable Object RPC byte streams and document the headless client-tool limitation for follow-up work. Made-with: Cursor * Harden agent tool edge cases Persist structured agent-tool outputs, make AIChatAgent stream errors terminal, and expand cancellation/idempotency coverage so retained runs behave consistently across retries and replays. Refresh the docs and schema-version tests to reflect AIChatAgent support and the new parent registry column. Made-with: Cursor * Harden agent tool cancellation cleanup Clean up parent abort listeners after completed agent-tool runs and avoid acquiring stream readers when forwarding starts from an already-aborted signal. Add regression coverage for both edge cases so future cancellation changes preserve the resource cleanup behavior. Made-with: Cursor * Use polling helper for root keepAlive ref count Add expectRootKeepAliveRefCount helper that polls agent.getRootKeepAliveRefCount (up to 20 attempts with a short delay) and use it in sub-agent tests instead of ad-hoc setTimeout waits. This replaces fragile fixed delays with a deterministic polling assert to reduce test flakiness in packages/agents/src/tests/sub-agent.test.ts. * Skip malformed agent tool stream frames Drop malformed or shape-invalid NDJSON frames during agent-tool stream forwarding so a corrupted display chunk does not fail an otherwise completed child run. Add regression coverage for the byte-stream forwarding path. Made-with: Cursor * Test and fix agent-tool in-memory cleanup Add a unit test (packages/think/src/tests/agent-tools.test.ts) that verifies in-memory agent-tool bookkeeping is cleared after a run completes. Extend ThinkTestAgent with helpers to seed a last-error for a run and to inspect map sizes (seedAgentToolLastErrorForTest, getAgentToolCleanupMapSizesForTest). Fix cleanup logic in think.ts to remove entries from _agentToolLastErrors and _agentToolPreTurnAssistantIds when an agent-tool run is torn down to avoid retained in-memory state. * Add types for agent tool test utilities Introduce AgentToolInspection and ThinkAgentToolTestStub types and tighten test helpers' signatures. freshAgent now returns a Promise<ThinkAgentToolTestStub> (with a cast from getAgentByName) and waitForAgentToolRun accepts the stub and returns AgentToolInspection. These changes improve TypeScript safety for agent tool tests and make available explicit method shapes used in the tests (inspectAgentToolRun, seedAgentToolLastErrorForTest, startAgentToolRun, getAgentToolCleanupMapSizesForTest).github.com-cloudflare-agents · 1b65ff55 · 2026-04-30
- 2.3ETVUpgrade to Vitest 4.1, Vite 8, and @cloudflare/vitest-pool-workers 0.13 (#1138) * Add decorator transform & bump deps Add a vite decorator-transform plugin and wire it into many example/experimental vite.configs, migrate tests for Vitest (new env.d.ts files and numerous test updates), and add a changeset describing MCP schema conversion (replace dynamic import("ai") with z.fromJSONSchema and remove ensureJsonSchema). Also bump multiple example/experimental dependencies (kumo, tailwindcss, nanoid, viem, jose, postal-mime, cronstrue, etc.), update package.json/package-lock, add new scripts and patch files, and remove an old vitest-browser-react patch. * Update vitest.config.ts * Require Zod v4 and warm up workers in tests Bump peer dependency range to require Zod ^4.0.0 across packages and update the changeset to reflect Zod v4 and MCP tool schema conversion (replace dynamic import with z.fromJSONSchema(), remove ensureJsonSchema()). Add Vitest test setup that warms up the Cloudflare worker module graph (beforeAll exports.default.fetch) and retains a short afterAll delay to avoid noisy Durable Object close-handler logs. Add a new setup file and enable setupFiles for the think package, and increase a flaky resumable-streaming test delay from 200ms to 1000ms to reduce CI timeouts.github.com-cloudflare-agents · 36e2020d · 2026-03-20
- 2.2ETVFix sub-agent WebSocket forwarding (#1443) * Fix sub-agent WebSocket forwarding Keep sub-agent browser WebSockets owned by the parent Agent and resume chat streams without duplicating assistant text blocks. Co-authored-by: Cursor <cursoragent@cursor.com> * Cover sub-agent WebSocket edge cases Persist child virtual connection metadata, preserve child connection flags across RPC forwarding, and make replay resume hydration recover safely when the replay targets a different assistant. Co-authored-by: Cursor <cursoragent@cursor.com> * Cast connection via unknown before assigning tags Cast connection to unknown before asserting { tags: string[] } when assigning the computed tags array. This adjusts the TypeScript type assertion to satisfy stricter type-checking (avoiding a direct incompatible cast) while preserving runtime behavior of setting connection.tags to [connection.id, ...childTags]. No functional change intended. * Replay stored chunks for late stream ACKs Co-authored-by: Cursor <cursoragent@cursor.com> * Attach resume listener before ACK Co-authored-by: Cursor <cursoragent@cursor.com> * Harden sub-agent streaming edge cases Co-authored-by: Cursor <cursoragent@cursor.com> * Harden sub-agent resume edge cases Keep child WebSocket connection behavior closer to top-level agents and make completed stream replay stricter after late ACKs. Co-authored-by: Cursor <cursoragent@cursor.com> * Sync useAgent identity before ready resolves Ensure the mutable agent fields reflect the identity frame before resolving ready, closing a small React render race. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>github.com-cloudflare-agents · e7d225b7 · 2026-05-01
- 2.1ETVsub-agent routing: RFC + implementation (phases 1–3) (#1355)github.com-cloudflare-agents · df2023fb · 2026-04-22
- 2.1ETVfeat(ai-chat): add onChatResponse hook + client streaming indicators (#1228) * Add server-driven messages and onResponse hook Introduce server-driven messaging support: add a protected AIChatAgent.onResponse hook and exported ResponseResult type so agents can react after a turn completes. Expose client-side flags isServerStreaming and isStreaming in useAgentChat to indicate server-initiated streams and provide a unified streaming indicator. Update docs and examples to demonstrate scheduled proactive messages, queue processing, webhooks, and chained reasoning. Add end-to-end tests for server-initiated streams and onResponse behavior, and adjust example agent/server code to showcase broadcasting and proactive message scheduling. Also fix stream error propagation so non-abort reader errors surface correctly and partial messages are not persisted. A changelog changeset and minor dependency lockfile update were included. * Rename onResponse to onChatResponse Rename the lifecycle hook from onResponse to onChatResponse across the codebase (docs, examples, tests, e2e specs, and worker/test agents). Update AIChatAgent internals to use a _pendingResponseResults array (instead of a single _pendingResponseResult) and adjust the drain loop and re-entrancy guard to process queued results and call onChatResponse. Update type/docs/comments and error logging to reference the new hook name. These changes prevent loss of multiple completed-turn results and align naming in documentation and tests. * Rename ResponseResult to ChatResponseResult Rename the exported type ResponseResult to ChatResponseResult and update all usages across the codebase. This includes renaming internal state and helpers (e.g. _pendingResponseResults -> _pendingChatResponseResults), public test helpers (getResponseResults -> getChatResponseResults, clearResponseResults -> clearChatResponseResults), and updating docs, examples, e2e worker code, and unit tests to use the new type and identifiers. Adjusted AIChatAgent drain loop and hook signatures/comments to reflect the new name. * Clear pending responses on stop; update test timing When stopping auto-continuation, clear the _pendingChatResponseResults array to avoid leaving stale response data. Adjusted timings in custom-body-continuation.test.ts to stabilize async behavior: increased simulated request delay (delayMs 300 -> 1000) and corresponding small waits (50 -> 100 ms) so continuations are reliably captured during the test.github.com-cloudflare-agents · 53f27b16 · 2026-03-29
- 1.8ETVAdd Chat SDK messenger example with managed fiber durability (#1563) * Add Chat SDK messenger example Demonstrates Chat SDK ingress on Agents with subagent-backed state and Think-owned conversation replies. Co-authored-by: Cursor <cursoragent@cursor.com> * Stream Chat SDK messenger replies Adds Think chat streaming with RPC-safe cancellation so messenger delivery failures can stop the corresponding sub-agent turn. Co-authored-by: Cursor <cursoragent@cursor.com> * Add managed fiber jobs Introduce managed fiber jobs on top of runFiber so agents can durably accept idempotent background work, inspect retained status, cancel running jobs, explicitly resolve interrupted jobs, and record recovery policy decisions. This adds the cf_agents_fibers ledger, schema v8 migration, status/list/delete/resolve APIs, cooperative cancellation signals, and waitForCompletion support that waits on terminal ledger state instead of only the callback promise. Tighten crash recovery semantics for managed work by reconciling stale run rows, recovering ledger-only pending/running rows, skipping recovery for already-terminal fibers, settling setup failures, and letting onFiberRecovered return a FiberRecoveryResult to move interrupted fibers to completed, error, aborted, or intentionally interrupted. The implementation also tracks active managed executions and terminal waiters so duplicate requests can join in-memory work when possible while post-restart retries drive the same recovery path. Use the new managed fiber API in the Chat SDK messenger example for AI replies. Telegram messages now get a stable per-message idempotency boundary, completion waiting preserves Chat SDK per-thread visible reply serialization, and recovery policy is explicit: accepted replies are replayed while mid-stream interruptions post a concise apology and settle the retained job. Expand coverage across unit, sub-agent, schema, and real eviction tests. The E2E harness now starts wrangler dev with persisted SQLite state, kills it mid-managed-fiber, restarts it, and verifies interrupted retention, recovery-result settlement, duplicate waitForCompletion retries after restart, and sub-agent managed fiber recovery through the parent alarm. Document the new durable job surface in the Agent and durable execution docs, including waitForCompletion, cancellation behavior, retained terminal records, explicit recovery outcomes, and how this differs from Think message admission. Co-authored-by: Cursor <cursoragent@cursor.com> * Polish managed fiber cleanup API Rename the public managed-fiber terminal timestamp from completedAt to settledAt, and rename the cleanup filter from completedBefore to settledBefore. These names better describe terminal rows across completed, error, aborted, and interrupted states while keeping the existing SQLite completed_at column internal. Make default deleteFibers() cleanup preserve interrupted rows. Interrupted managed fibers often need inspection or explicit application-level resolution, so callers must now opt in to deleting them by passing status: "interrupted". Clarify FiberContext.snapshot documentation so it does not imply callbacks are automatically re-entered with recovered snapshots; recovery snapshots are delivered through onFiberRecovered(). Add a regression test that default cleanup deletes completed rows while preserving interrupted rows, then verifies explicit interrupted cleanup still works. Co-authored-by: Cursor <cursoragent@cursor.com> * Document managed fiber adoption patterns Add practical guidance for using managed fibers around webhook-style application jobs, including retained cleanup with settledBefore, interrupted recovery, resolveFiber, and waitForCompletion behavior. Clarify the boundary between Think submissions and managed fibers across the Think docs, package README, server-driven messaging docs, webhook docs, and examples so users can distinguish durable Think turn admission from app-owned side-effect jobs. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix PR install after main package bumps Use the workspace dependency for the Chat SDK messenger example's Think package so npm ci can resolve the merged branch after main's version-package release. Always run npm ci in the shared GitHub install action while relying on setup-node's npm package cache, avoiding stale node_modules cache hits that can mask lockfile drift. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix managed fiber review issues Correct the malformed Think changeset frontmatter so Changesets can parse the release metadata. Ensure waitForCompletion waits for a terminal managed fiber status even when duplicate calls race with an already-running recovery pass, and cover the race with a regression test. Also document and test the Chat SDK state adapter's list-level TTL behavior. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>github.com-cloudflare-agents · 32cde406 · 2026-05-19
- 1.7ETVAdd durable Think submissions API (#1511) * Add durable Think submissions API Introduce submitMessages() for Think so RPC callers can durably accept a programmatic chat turn without waiting for model execution to finish. The new API persists a submission row before execution, supports idempotent retries through idempotencyKey, exposes inspection/list/cancel/delete helpers, and emits submission lifecycle observability events. The implementation uses cf_think_submissions as a SQLite-backed ledger and an idempotent scheduled drain as the wakeup mechanism. Submissions move through a small pending/running/terminal state machine, append messages to Session only after being claimed, and use messages_applied_at as the replay boundary so hibernation recovery never duplicates already-applied messages. Pending submissions are synchronously skipped during turn reset, terminal states are protected with conditional updates, and recovered chat continuations stay running until they reach a terminal outcome. Add focused coverage for fast acceptance, idempotent retries, FIFO draining, cancellation, reset races, startup recovery, chat recovery, malformed durable rows, cleanup filters, and recovered-continuation cancellation. Also add user docs, a contributor-facing design doc, a changeset, observability tests, and a dedicated think-submissions example that demonstrates durable submission, retry, status inspection, cancellation, and cleanup flows. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix submission stream error retention Only durable submissions need to capture stream errors in the programmatic stream error map so their terminal row can be marked as error after _streamResult returns. Other callers such as saveMessages, WebSocket chat turns, continuations, and auto-continuation already handle stream errors through the normal response hook path and should not leave request-scoped entries behind for the isolate lifetime. Scope _programmaticStreamErrors writes behind an explicit capture option used by _runSubmission, and add regression coverage that a non-submission stream failure does not retain an entry. Also align the think-submissions example with the examples directory convention by using public/favicon.ico from the standard example favicon instead of a custom favicon.svg. Co-authored-by: Cursor <cursoragent@cursor.com> * Preserve aborted submission status after stream errors A captured programmatic stream error should only turn an otherwise completed submission into an error. If the underlying programmatic turn reports aborted or skipped, those explicit terminal outcomes must win even when abort/reset also surfaced as a stream iterator error. Factor submission final status selection into a helper, clear error_message for non-error terminal states, and add regression coverage for completed+error, aborted+error, and skipped+error precedence. Co-authored-by: Cursor <cursoragent@cursor.com> * Make submission recovery staleness configurable Keep the stale-evidence safety net for recovered durable submissions, but expose the recovery freshness window as a protected static setting that Think subclasses can tune for legitimate long-running turns. The default remains 15 minutes, preserving the existing behavior for normal agents while avoiding a hardcoded limit for providers or workloads that can validly run longer before a Durable Object restart. Update the recovery check to read submissionRecoveryStaleMs from the concrete subclass, document the override point in the durable submissions design doc, and add coverage proving that an older recoverable fiber remains running when a subclass extends the stale window. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>github.com-cloudflare-agents · bf3860c2 · 2026-05-12
- 1.6ETVfeat: inference buffer — durable response buffer for long-running inference calls (#1262) * Bump deps & add inference-buffer experiment Upgrade numerous dependencies across examples and packages (notable bumps: @cloudflare/kumo -> ^1.17.0, @cloudflare/vite-plugin -> ^1.31.0, @cloudflare/workers-types -> ^4.20260405.1, wrangler -> ^4.80.0, ai -> ^6.0.146, workers-ai-provider -> ^3.1.10, plus several x402/viem/hono/react-router/openai version updates). Add a new experimental/inference-buffer module (README, types, implementation, tests, and wrangler config). Improve experimental/forever-chat by adding INFERENCE_BUFFER to env types, adding test tooling (vitest), server/client changes (including a client toggle to use the buffer), SSE parsers and replay-model files. Also rename the vitest-browser-react patch file to the 2.2.0 variant and update package-lock/root package metadata. * Update programmatic-turns.test.tsgithub.com-cloudflare-agents · 8a3a84e1 · 2026-04-05
- 1.5ETVthink: align lifecycle hook contexts with AI SDK; fix tool-call hook timing and extension dispatch (#1340) * think: align lifecycle hook contexts with AI SDK; fix tool-call hook timing and extension dispatch Resolves #1339. Lifecycle hook context types are now derived from the AI SDK so users get full typed access instead of `unknown`: - `StepContext<TOOLS>` = `StepResult<TOOLS>` — `reasoning`, `sources`, `files`, `providerMetadata` (cache tokens), `request`/`response`, `warnings`, full `LanguageModelUsage` (incl. `cachedInputTokens`, `reasoningTokens`). - `ChunkContext<TOOLS>` = AI SDK's `StreamTextOnChunkCallback` event (discriminated `TextStreamPart`). - `ToolCallContext<TOOLS>` spreads `TypedToolCall<TOOLS>` plus `stepNumber`, `messages`, `abortSignal`. - `ToolCallResultContext<TOOLS>` spreads `TypedToolCall<TOOLS>` plus `durationMs`, `messages`, `stepNumber`, and a discriminated `success`/`output`/`error` outcome. The relevant AI SDK types (`StepResult`, `TextStreamPart`, `TypedToolCall`, `TypedToolResult`) are re-exported from the package. Tool-call hook timing is now correct: `beforeToolCall` fires before `execute` (Think wraps every tool's `execute`), and `afterToolCall` fires after via `experimental_onToolCallFinish` with accurate `durationMs`. Previously both fired post-execution from `onStepFinish` data, and the wrapper was reading `tc.args` / `tr.result` (the AI SDK uses `input` / `output`) so subclass hooks always saw `{}` and `undefined`. `ToolCallDecision` is now functional. Returning `{ action: "block", reason }`, `{ action: "substitute", output }`, or `{ action: "allow", input }` from `beforeToolCall` actually intercepts execution. Extension dispatch for `beforeToolCall` / `afterToolCall` / `onStepFinish` / `onChunk`. The `ExtensionManifest.hooks` array claimed support for these but Think only ever dispatched `beforeTurn`. All five hooks now dispatch with JSON-safe snapshots. Extension hook handlers also receive `(snapshot, host)` symmetric with tool `execute`; previously only tool executes got the host bridge. Test mocks updated to the AI SDK v3 LanguageModel spec (`finishReason: { unified, raw }`, structured `usage` with `{ inputTokens: { total, ... }, outputTokens: { total, text, reasoning } }`, explicit `tool-call` chunks). The previous mocks were quietly relying on fallbacks in the wrapper that masked these shape mismatches. Breaking renames per AI SDK conventions: `ToolCallContext.args` -> `input`, `ToolCallResultContext.args` -> `input`, `ToolCallResultContext.result` -> `output`. `afterToolCall` is now a discriminated union — read `output` only when `ctx.success === true`, and `error` when `ctx.success === false`. Equivalent renames on `ToolCallDecision`. 247/247 tests passing. Docs and the assistant example are updated. Made-with: Cursor * ai-chat: waitForIdle/waitUntilStable also drain pending enqueues `waitForIdle` and `waitUntilStable` previously only awaited `_turnQueue.waitForIdle()`. They missed submits that had passed `_getSubmitConcurrencyDecision` (so already bumped `_latestOverlappingSubmitSequence` and incremented `_pendingEnqueueCount`) but hadn't yet reached `_runExclusiveChatTurn` because they were mid-`persistMessages`. Anything calling these helpers (tests, recovery code, callers waiting for quiescence) could race the in-flight submit and observe the agent as "idle" when it really wasn't. Both helpers now poll on `_pendingEnqueueCount === 0` after the queue drains, with a brief setTimeout yield between checks so in-flight handlers can reach the enqueue call. Fixes the long-standing flake in `merge concatenates overlapping queued user messages into one follow-up turn`. Also bumps that test's stream durations (10×100ms → 15×150ms) to give the WS dispatch enough headroom under CI load to bump `_latestOverlappingSubmitSequence` *before* the first turn finishes — the supersession check on the second submit needs to see the latest counter value, and `waitForOverlappingSubmits` only helps if the third submit's handler has actually run. Made-with: Cursor * think: fix Promise<AsyncIterable> tool returns being passed through unwrapped `_wrapToolsWithDecision` previously did: const result = originalExecute(finalInput, options); if (Symbol.asyncIterator in (result as object)) { ...collapse... } For an `async function execute(...) { return makeAsyncIterable(); }` (the common shape if you build the iterator inside the async body), `originalExecute(...)` returns `Promise<AsyncIterable>` — and `Symbol.asyncIterator in Promise` is always false. The collapse logic was skipped. The wrapper is async, so it auto-unwrapped one Promise layer and effectively returned `Promise<AsyncIterable>`. The AI SDK's `executeTool` then `await`s that, gets the AsyncIterable, and yields `{ type: "final", output: AsyncIterable }` — exactly the broken case the wrapper's own comment warned against. Fix: await `originalExecute(finalInput, options)` before inspecting, so the symbol check sees the resolved value (an AsyncIterable in any of: direct return, `async function*`, or `async () => makeIter()`). Verified by adding two regression tests: - `collapses Promise<AsyncIterable> returns to the last yielded value` (the buggy case — fails on the pre-fix code with the iterator serializing to `{}`) - `collapses sync AsyncIterable returns to the last yielded value` (the case that already worked — belt-and-suspenders) Updated docs and JSDoc to spell out that all three iterator-return shapes are handled identically. Spotted by code review on #1340. Made-with: Cursorgithub.com-cloudflare-agents · 3cbe7766 · 2026-04-18
- 1.4ETVfeat(think): lifecycle hooks, dynamic context, extension manifest (#1278) * feat(think): own the inference loop — lifecycle hooks, remove onChatMessage Phase 1 of the Think extension system redesign. Think now owns the streamText call end-to-end, enabling lifecycle hooks at every stage of the agentic loop. Users who need full custom inference extend Agent directly instead of overriding onChatMessage. BREAKING CHANGES: - Remove onChatMessage() — Think owns the streamText call internally via private _runInferenceLoop(TurnInput). All 4 entry paths (WebSocket, chat(), saveMessages, auto-continuation) converge on it. - Remove assembleContext() — absorbed by beforeTurn hook. Think assembles context internally; beforeTurn receives the result in TurnContext and can override any part via TurnConfig. - Remove getMaxSteps() method — replaced by maxSteps property (default 10). Per-turn override via TurnConfig.maxSteps. - Deprecate sanitizeMessageForPersistence() — will move to session configuration in a future release. - Deprecate ChatMessageOptions — aliased to TurnInput for migration. NEW LIFECYCLE HOOKS: - beforeTurn(ctx: TurnContext) → TurnConfig | void Fires before streamText. Inspect assembled system prompt, messages, tools, model. Return overrides: model, system, messages, tools, activeTools, toolChoice, maxSteps, providerOptions. - beforeToolCall(ctx: ToolCallContext) → ToolCallDecision | void Fires when model produces a tool call. Currently observation-only (fires via onStepFinish data post-execution). ToolCallDecision is a discriminated union: allow | block | substitute. Block/substitute not yet functional — AI SDK doesn't expose pre-execution interception in Workers runtime. Types are in place for future implementation. - afterToolCall(ctx: ToolCallResultContext) → void Fires after tool execution with tool name, args, and result. - onStepFinish(ctx: StepContext) → void Fires after each step (initial, continue, tool-result) with step type, text, tool calls, tool results, finish reason, usage. - onChunk(ctx: ChunkContext) → void Fires per streaming chunk. High-frequency, observational only. NEW CONFIGURATION: - maxSteps property (replaces getMaxSteps method) - getExtensions() stub for Phase 2 extension declaration - MCP tools auto-merged into tool set (no manual merging needed) - waitForMcpConnections moved inside inference loop NEW EXPORTED TYPES: TurnInput, TurnContext, TurnConfig, ToolCallContext, ToolCallDecision, ToolCallResultContext, StepContext, ChunkContext, ExtensionConfig DESIGN DECISIONS: - _runInferenceLoop is private — hooks always fire, no bypass possible - ToolCallDecision is a discriminated union (allow/block/substitute) with clear, non-overlapping semantics per action - chat() now wraps _runInferenceLoop in agentContext.run() for consistency with the WebSocket path - _transformInferenceResult is a protected test seam for error injection (replaces the old onChatMessage stream-wrapping pattern) TEST CHANGES: - Migrated 6 test agents that overrode onChatMessage or getMaxSteps - TestAssistantAgentAgent: replaced fake stream with mock model - ThinkTestAgent: error injection via _transformInferenceResult, added beforeTurn/onStepFinish/onChunk instrumentation - ThinkProgrammaticTestAgent: captures via beforeTurn instead of onChatMessage - ThinkRecoveryTestAgent/ThinkNonRecoveryTestAgent: count via beforeTurn instead of onChatMessage - LoopToolTestAgent: added tool call hook instrumentation - ThinkToolsTestAgent: switched to tool-calling mock model - Renamed _onChatMessageCallCount → _turnCallCount - Updated stale test descriptions referencing removed methods - Added 11 new hook tests (hooks.test.ts): beforeTurn context, multi-turn, convergence across entry paths, onStepFinish, onChunk, maxSteps property - 191 tests pass across 9 test files Made-with: Cursor * feat(think,agents): dynamic context blocks + expanded extension manifest Phase 2 of the Think extension system redesign. Extensions can now declare context blocks in their manifests, and Session supports dynamic add/remove of context blocks after initialization. SESSION CHANGES (packages/agents): - ContextBlocks.addBlock(config) — register a new context block after init. Triggers load() if blocks haven't been loaded yet. Initializes provider, loads content, adds to both configs array and blocks Map. - ContextBlocks.removeBlock(label) — remove a block. Cleans up from configs, blocks, and loaded skills tracking. Skill unload callbacks are NOT fired (appropriate for full extension removal). Caller must call refreshSystemPrompt() to rebuild the prompt. - Session.addContext(label, options?) — public API wrapping addBlock. Auto-wires AgentContextProvider (SQLite-backed) when no provider is given. Requires builder-constructed sessions (Session.create). - Session.removeContext(label) — public API wrapping removeBlock. EXTENSION MANIFEST (packages/think): - ExtensionManifest.context — array of context block declarations with label, description, type (readonly/writable/skill/searchable), and maxTokens. Labels are namespaced as {extName}_{label}. Type is declared but not yet enforced (all blocks use SQLite storage until bridge providers are implemented in Phase 4). - ExtensionManifest.hooks — lifecycle hooks the extension provides. - ExtensionInfo.contextLabels — namespaced labels in list() output. BRIDGE PROVIDERS (packages/think — Phase 4 infrastructure): - ExtensionContextBridge, ExtensionWritableBridge, ExtensionSkillBridge adapt extension Worker RPC into Session provider interfaces. Uses protected base fields so children don't duplicate state. Not wired yet — current blocks use AgentContextProvider directly. - createBridgeProvider(label, type, entrypoint) factory function. EXTENSION LIFECYCLE IN THINK: - _initializeExtensions() — creates ExtensionManager from extensionLoader property, loads static extensions from getExtensions(), restores dynamic extensions from DO storage, registers extension context blocks in Session via addContext() (SQLite-backed, not bridge-delegated), wires onUnload callback. - extensionLoader property — set to env.LOADER to enable extensions. - extensionManager field — public, auto-created when extensionLoader is set. Use for dynamic load()/unload() at runtime. - Extension tools auto-merged in _runInferenceLoop. - ExtensionManager.unload() fires onUnload callback which removes context blocks from Session and refreshes the system prompt. - ExtensionManager.onUnload(cb) — register cleanup callback. - ExtensionManager.getContextLabels() — namespaced labels. - ExtensionManager.getManifest(name) — get manifest by name. HIBERNATION RESTORATION: onStart() ordering: 1. Workspace initialization 2. configureSession() (builder phase) 3. ExtensionManager created (if extensionLoader set) 4. getExtensions() loaded (static extensions) 5. restore() (dynamic extensions from DO storage) 6. Extension context blocks registered in Session (mutation phase) 7. Protocol handlers 8. User's onStart() TESTS: 5 new tests for dynamic context: - addContext registers a new block - addContext block appears in system prompt after refresh - removeContext removes the block - removeContext returns false for non-existent block - removed block disappears from system prompt after refresh 195 total tests pass across 8 files. Made-with: Cursor * docs: update Think README, assistant example, and changeset README: - Replace "Override points" table with "Configuration" + "Lifecycle hooks" tables reflecting the new API (no onChatMessage, assembleContext, getMaxSteps) - Add beforeTurn example with TurnConfig documentation - Add "Dynamic context blocks" section showing addContext/removeContext - Update MCP section to note auto-merging - Update production features list with lifecycle hooks Assistant example: - Replace getMaxSteps() with maxSteps property - Remove manual MCP tool merging in getTools() (auto-merged now) - Add beforeTurn and onChatResponse hooks to demonstrate the lifecycle - Import new types (TurnContext, TurnConfig, ChatResponseResult) Changeset: - Patch for @cloudflare/think and agents - Documents breaking changes (removed onChatMessage, assembleContext, getMaxSteps) and new features (hooks, maxSteps property, MCP auto-merge, dynamic context blocks, expanded manifest) Made-with: Cursor * test(think): expand Phase 2 test coverage for dynamic context Add 5 more tests for dynamic context blocks: - dynamic block is writable by default (AgentContextProvider) - dynamic block content can be written via setContextBlock - session tools include set_context after adding writable block - addContext coexists with configureSession blocks (both in prompt) - dynamic block visible in chat turn tools (negative: ThinkTestAgent has no context blocks, so set_context should not appear) Also add test helpers to ThinkSessionTestAgent: - getSessionToolNames() — returns tool names from session.tools() - getContextBlockDetails() — returns writable/isSkill for a block 200 total tests pass across 8 files. Made-with: Cursorgithub.com-cloudflare-agents · 8c7caabb · 2026-04-09
- 1.4ETVFollow-up cleanups for recent chat and voice PRs (#1497) * fix(voice): harden Workers AI STT turn handling Follow up on PR #1458 by preserving Flux turn transcripts across lifecycle events and using model-detected speech start for low-latency barge-in. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(chat): harden stream resume negotiation close races Follow-up to PR #1463: route stream-resume negotiation sends through close-safe helpers so WebSocket close races do not crash resume handling in think and ai-chat. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(voice): parse raw NDJSON text streams Follow-up to PR #1462: make the voice text stream parser honor its documented NDJSON support while preserving SSE parsing for AI text streams. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(agents): defer recovered agent-tool finish hooks (#1476) Co-authored-by: Cursor <cursoragent@cursor.com> * test(voice): cover useVoiceAgent enabled lifecycle (#1478) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ai-chat): close resumed streams on disconnect (#1487) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(voice): invalidate playback on client interrupt (#1458) Co-authored-by: Cursor <cursoragent@cursor.com> * fix(voice): invalidate playback when ending calls (#1458) Co-authored-by: Cursor <cursoragent@cursor.com> * Run deferred finish hooks after successful startup Ensure recovered agent-tool finish hooks are only executed after a successful user onStart. Await _runDeferredAgentToolFinishHooks inside the onStart flow so deferred finishes are skipped when startup fails. Add a test and helper (reconcileCompletedChildWithFailedStartupForTest) to verify finish hooks are not run on failed startup and to cover lifecycle ordering and event emission. --------- Co-authored-by: Cursor <cursoragent@cursor.com>github.com-cloudflare-agents · f5df6385 · 2026-05-11
- 1.4ETVStop provider tool-call replays from regressing tool part state (#1404) (#1412)github.com-cloudflare-agents · 8fb7c032 · 2026-04-29
- 1.3ETVrefactor(tests): migrate to SELF.fetch and remove unnecessary @callable decorators (#1091) * Use SELF and deterministic DO helpers in tests Refactor agent tests to use SELF.fetch and durable-object helpers for deterministic behavior. Replace createExecutionContext + worker.fetch with SELF.fetch across many tests and simplify connectWS to return just the WebSocket. Use runInDurableObject and runDurableObjectAlarm to read internal instance state and trigger alarms instead of polling with setTimeout, and add helper functions (e.g. in keep-alive tests) to query DO internals. Removed several test-only @callable methods from test agent classes and moved their logic into test helpers that access instance fields directly. These changes reduce flakiness and make tests more deterministic and faster. * Use SELF.fetch in ai-chat tests Replace createExecutionContext/worker.fetch usage with SELF.fetch across ai-chat tests. Updated imports to pull SELF (and env where needed), removed ExecutionContext handling and ctx.waitUntil calls, and simplified connectChatWS to return only the WebSocket. Changes touch test files under packages/ai-chat/src/tests to align with the Cloudflare test harness and simplify websocket test setup. * Remove @callable decorators and simplify tests Drop unused @callable decorators and related imports from test helper agents, and simplify WebSocket test setup to use SELF.fetch. Updated files remove the `callable` import and many `@callable()` annotations in test agents (assistant-agent*, assistant-session, assistant-tools, assistant-agent-loop). Tests now import SELF from cloudflare:test and call SELF.fetch directly (removing createExecutionContext/worker.fetch and the ctx return), and connectWS/freshAgent helpers no longer return the execution context. These changes tidy tests and decouple them from the previous worker/context plumbing.github.com-cloudflare-agents · a20b77d3 · 2026-03-10
- 1.2ETVFix saveMessages cancellation race with external AbortSignal (#1411) * Fix saveMessages cancellation race with external AbortSignal Resolves the race in `Think.saveMessages` and `AIChatAgent.saveMessages` where callers had no way to safely cancel an in-flight programmatic turn without reaching into private `_aborts` state. `saveMessages` and `continueLastTurn` now accept `options.signal`, the signal is bridged into the per-turn `AbortRegistry` controller via a new `AbortRegistry.linkExternal()`, and `SaveMessagesResult.status` reports `"aborted"` when the external signal fires. Adds protected `abortRequest()` / `abortAllRequests()` helpers so subclasses no longer need bracket-access workarounds. Updates the `agents-as-tools` example to use the new contract, expands unit + integration coverage (registry, Think, AIChatAgent, helper stream), and documents the API plus its DO RPC and hibernation limitations. See cloudflare/agents#1406. Made-with: Cursor * ai-chat: hoist detachExternal try/finally above runFiber Fixes a listener leak in `_runProgrammaticChatTurn` where `linkExternal` was called before the `runFiber` boundary while the `try/finally` that calls `detachExternal()` lived inside `programmaticBody`. If `runFiber` itself threw — e.g. a SQLite error inserting the fiber row, or `keepAlive()` failing — the body never ran, so the external-signal listener was never removed and the registry entry was never cleaned up. Long-lived parent signals driving many helper turns would accumulate listeners across failures. The fix mirrors the structure already used by `Think.saveMessages` and `Think.continueLastTurn`: the `try/finally` now wraps both the `runFiber` and direct-call branches, so cleanup runs regardless of where the throw originates. `AIChatAgent.continueLastTurn` was already structurally safe (linkExternal runs *inside* the runFiber boundary) and is unchanged. Adds a regression test that monkey-patches `runFiber` to throw synchronously and asserts the abort registry drains and no listener remains on the external signal. Made-with: Cursor * ai-chat tests: tighten typing on runFiber-failure regression seam Use `Parameters<...>` of the bound `addEventListener`/`removeEventListener` overloads (instead of redeclaring the signature) and reference the non-polymorphic `RecoverySlowStreamAgent["runFiber"]` so `typeof this` doesn't leak into the cast. Behavior is unchanged. Made-with: Cursorgithub.com-cloudflare-agents · 2fa68bea · 2026-04-28
- 1.2ETVai-chat: align with Think + multi-ai-chat example on the sub-agent routing primitive (#1353) * ai-chat: align with think + maintenance RFC + multi-ai-chat example Mechanical alignments between `@cloudflare/ai-chat` and `@cloudflare/think`, paired with a stance RFC and a reference example for multi-session chat. ## Code changes - `AIChatAgent` gains a `Props` generic to match the Think change we just shipped: `AIChatAgent<Env, State, Props>` extending `Agent<Env, State, Props>`. `this.ctx.props` is typed now. - `ChatResponseResult`, `ChatRecoveryContext`, `ChatRecoveryOptions`, `SaveMessagesResult`, and `MessageConcurrency` move into `agents/chat/lifecycle.ts`. Both `@cloudflare/ai-chat` and `@cloudflare/think` import from `agents/chat` and re-export. No behavior change; one place to edit when the shapes evolve. - `AIChatAgent` drops the `UIMessage as ChatMessage` import alias and uses `UIMessage` everywhere. The `ChatMessage` type is no longer exported from `@cloudflare/ai-chat`. Internal `message-reconciler` also drops its local alias. - `AIChatAgent.messages` becomes a getter over a protected `_messages` backing field. Prevents `this.messages = [...]` reassignment from subclasses. The returned array type stays mutable for AI SDK compat (`convertToModelMessages(this.messages)` works unchanged); signatures on the `reconcileMessages` helpers and the `OutgoingMessage` wire type accept `readonly UIMessage[]` where they only read. ## Docs - `design/rfc-ai-chat-maintenance.md` captures the stance: `AIChatAgent` stays first-class and fully supported while `Think` stabilizes. New features land in `agents/chat` where both benefit. Deferred structural work (hoisting protocol handling, promoting `agents/chat` to a public toolkit, `onChatMessage` signature revision) is listed with rationale. ## Example - `examples/multi-ai-chat/` — a hand-rolled preview of the `Chats` pattern from `rfc-think-multi-session.md`, using `AIChatAgent` children. An `Inbox` parent DO owns the chat list + per-user shared memory; per-chat `AIChatAgent` DOs run in parallel. Client wires up via `useAgent` + `useAgentChat` directly, so when the `Chats` base class lands, the migration is ~10 lines. Made-with: Cursor * example: evolve multi-ai-chat onto the sub-agent routing primitive Rebuilds the example on top of the sub-agent routing primitive that landed in #1355. The original commit on this branch was written before that primitive existed and used two top-level DO bindings (`Inbox` + `Chat`) with direct namespace RPC between them. Now that the routing primitive is merged, the example can — and should — demonstrate it. ## Server (`src/server.ts`) - `Chat` becomes a **facet** of `Inbox`. No top-level binding; no namespace lookup for the child. `Inbox.createChat` calls `this.subAgent(Chat, id)` to spawn the facet and register it in the parent's sub-agent registry. `deleteChat` calls `this.deleteSubAgent(Chat, id)`. - `Inbox.onBeforeSubAgent` implements a strict-registry gate using `hasSubAgent`. A chat becomes reachable only after `createChat` has spawned it; unknown ids get a 404 before any facet is woken. - `Chat` reaches its parent via `this.parentPath[0]` — the root-first ancestor chain the framework populates at facet-init time. No hardcoded user id inside the chat. - Worker entry collapses to a one-line `routeAgentRequest` call: `/agents/inbox/{user}/sub/chat/{chatId}` is handled natively. ## Client (`src/client.tsx`) - `ActiveChat` connects via `useAgent({ agent: "Inbox", name: DEMO_USER, sub: [{ agent: "Chat", name: chatId }] })` — the hook builds the nested `/sub/chat/{chatId}` URL; everything downstream (identity, state sync, `useAgentChat`) works unchanged. The sidebar connection stays as a plain `useAgent({ agent: "Inbox", ... })`. ## Config - `wrangler.jsonc` drops the `Chat` top-level binding but keeps `Chat` in `new_sqlite_classes` so the runtime can still construct it as a facet. - `env.d.ts` drops the `Chat: DurableObjectNamespace<...>` entry for the same reason. ## Docs - README rewritten to describe the actual mechanics (URLs, hook gate, parentPath) rather than a forward-looking "Chats pattern sketch". Adds a link to the now-landed sub-agent routing RFC. - Changeset updated to note the example exercises the routing primitive end-to-end. The `Chats` base class from `rfc-think-multi-session.md` will collapse `Inbox`'s chat bookkeeping (create / delete / list / `onBeforeSubAgent` gate) into framework defaults. When that lands, this example's `Inbox` becomes ~10 lines. Made-with: Cursor * agents: make keepAlive()/identity-warning facet-safe Two regressions surfaced by running the multi-ai-chat example: **1. `keepAlive()` threw inside a facet, breaking streaming chats.** `AIChatAgent._reply` wraps the streaming turn in `keepAliveWhile(...)` to guarantee the DO finishes committing the final message even if the client disconnects mid-stream. That path crashed every turn inside a Chat facet with: Error: keepAlive() is not supported in sub-agents. The original guard assumed "facets delegate lifecycle to the parent" but that left a real hole: a facet's `_reply` can't just give up keepalive bookkeeping because the parent doesn't know about it. workerd doesn't support independent alarms on facets yet ("alarms are not yet implemented for SQLite-backed Durable Objects" when you try), so the fix can't be "add an alarm on the facet". Instead, make `keepAlive()` a **soft no-op** in facets: return an inert disposer, don't throw. Facets piggyback on the parent isolate — active Promise chains, WebSockets, and the parent's own alarm all keep the shared isolate alive; the defensive keepalive is redundant in that context. Documented in the JSDoc with a pointer at "call `keepAlive()` on the parent via RPC if you really need it". **2. `sendIdentityOnConnect` mis-warned for facet instances.** The warning fires when the instance name isn't visible in the URL — but it checks the request URL the DO itself sees, which for a facet has been rewritten by `_cf_forwardToFacet` to strip `/sub/{class}/{name}`. The CLIENT always put the name in the URL (that's literally how sub-agent routing works). Suppress the warning for facets; the concern doesn't apply. Tests: - `keepAlive() works inside a sub-agent` (no throw, returns a working disposer) - `keepAliveWhile() runs to completion inside a sub-agent` — same call shape as AIChatAgent._reply, pins the multi-ai-chat regression - The old "keepAlive throws in facets" assertion is flipped to assert it succeeds. Made-with: Cursor * agents: let facets broadcast to their own WebSocket clients **User-visible bug**: In `examples/multi-ai-chat`, the assistant's streaming reply didn't appear in the chat UI until the user refreshed the page. The sidebar "last message preview" updated in real time (it goes through `recordChatTurn` RPC to the parent Inbox), but the streaming chunks never reached the browser over the WebSocket. On refresh, `/get-messages` fetched the persisted turn from the facet's SQLite and it showed up — so data was being written; only live broadcast was silent. **Root cause**: two guards in `Agent` — an early-return in `_broadcastProtocol` and an override on `broadcast` itself — that no-op'd whenever `_isFacet` was true. The comments explained the concern: > Facets share the parent DO's WebSocket registry: getConnections() > returns parent-owned sockets, so iterating from a facet throws > "Cannot perform I/O on behalf of a different Durable Object". > Sub-agents are RPC-only and have no WS clients of their own. That was accurate for the pre-routing world where facets existed only as RPC targets reachable by the parent. Sub-agent routing (#1355) changed the model: clients now connect directly to facets via `/agents/{parent}/{name}/sub/{class}/{name}`, and those WebSockets are upgraded on — and owned by — the facet's isolate. `getConnections()` inside the facet returns the facet's own sockets. The "cross-DO I/O" concern no longer applies. The consequence was that every `this.broadcast(...)` call on a facet silently did nothing. That includes: - `AIChatAgent._broadcastChatMessage` — streaming chunks to the client during a chat turn. **This is the one that broke the demo.** - `setState()` → `_broadcastProtocol` → `CF_AGENT_STATE` — state sync to connected clients from a facet. - `broadcastMcpServers` — MCP server updates. - Any user-defined broadcast from subclass code. **Fix**: remove both guards. `this.broadcast(...)` and `this._broadcastProtocol(...)` now iterate the facet's own connections — same behavior as a top-level DO. Regression test (spike suite): a facet is connected to directly, then invokes `this.broadcast(...)` from `onMessage`. The client receives the broadcast. Before this fix the broadcast was silently dropped; now it round-trips. Other `_isFacet` guards are unchanged: - `schedule()` / `cancelSchedule()` / `keepAlive()` still special-case facets — workerd doesn't support alarms on SQLite-backed facets today. The previous commit documents `keepAlive`'s soft-no-op semantics. - `destroy()`'s `deleteAlarm` skip for facets stays (facets never set alarms, so there's nothing to clear). Fixes the "chat UI doesn't update until refresh" symptom in `examples/multi-ai-chat`. Made-with: Cursor * example(multi-ai-chat): render reasoning + tool parts, add shared-memory tools Three small tools on the `Chat` agent to make the demo actually agentic: - `rememberFact(fact)` — persists a fact to the parent Inbox's shared memory (`inbox.setSharedMemory`). Every sibling chat picks up the fact on the next turn. Demonstrates cross-DO RPC from inside a tool `execute` that runs in a facet. - `recallMemory()` — reads the current shared memory. - `getCurrentTime()` — returns the server's ISO time. Included mostly to give the model a tool to pick when the user just wants small talk about the clock. The model now runs in a multi-step agentic loop (`stopWhen: stepCountIs(5)`) so it can call a tool, observe the output, and respond in the same turn. Client rendering overhaul: - Drop the "join all text parts into one string" renderer. - Render `UIMessage.parts` in order: text → bubble, `reasoning` → dimmed "Thinking" block, tool parts → panel with state badge (Running/Done/Error), input JSON, output JSON, and errorText. - Streaming cursor only appears on the trailing text part of the last assistant message. - Ignore `step-start`, `source-*`, `file` — the `examples/ai-chat` has a fuller treatment if needed. README points people at things to try: _"Remember I prefer TypeScript"_ exercises `rememberFact`, and _"What time is it?"_ exercises `getCurrentTime`. Saving memory via the sidebar still works for the no-tool-call case. Made-with: Cursor * agents: parentAgent() helper + multi-ai-chat review polish Five small follow-ups from a self-review pass on the PR. All tests pass (1325/1325 in agents); all 75 projects typecheck. **1. `parentAgent<T>(namespace)` on the Agent base class.** Every facet-based app was about to hand-roll a `getParent()` helper that reads `this.parentPath[0]` and opens a stub via `getAgentByName`. Codify it on the base class — pass the parent's namespace binding, get back a typed `DurableObjectStub<T>` with the right instance name resolved for you: class Chat extends AIChatAgent<Env> { private getInbox() { return this.parentAgent(this.env.Inbox); } } Throws a clear error when called on a top-level (non-facet) agent. Tests: `resolves the parent stub from within a facet`, `throws a clear error when called on a non-facet`. **2. `examples/multi-ai-chat`: `listSubAgents(Chat)` as the source of truth.** Previously the example maintained a parallel `inbox_chats` table alongside `cf_agents_sub_agents` — both tracked "this chat exists", and a crash between the two writes could leave them out of sync. Now: the sub-agent registry is authoritative for existence, and a thin `chat_meta` table holds app-owned decoration (title, preview, updated_at). `_refreshState` joins `listSubAgents(Chat)` against `chat_meta` to build the sidebar. A chat with a missing meta row just gets a default title. **3. Drop the redundant `className !== "Chat"` check in `onBeforeSubAgent`.** `Agent.fetch` filters URLs via `knownClasses: Object.keys(ctx.exports)` before the hook runs, so by the time `onBeforeSubAgent` fires the class is guaranteed to be in exports. The subsequent `hasSubAgent` check acts as the real gate. **4. `Chat.getInbox()` now delegates to `this.parentAgent(...)`.** Two hardcoded ancestor-shape assertions collapse into the framework helper. **5. Client `AnyToolPart` type cleanup.** Drop the hand-rolled intersection type. `ToolPart` now takes `Parameters<typeof getToolName>[0]` — the same narrowed union `isToolUIPart` returns — and reads optional fields via `"x" in part` checks instead of re-widening. Type-safe with no casts. **6. Trim `keepAlive()` docstring.** The previous text pointed users at `getAgentByName(parent).keepAlive()` as an escape hatch. In practice nobody needs it — the soft no-op is sufficient because facets share the parent's isolate and the active Promise chain plus open WebSockets already keep the machine alive for the duration of real work. Made-with: Cursor * agents: parentAgent(Cls) — class-ref API with runtime safety The `parentAgent(namespace)` signature from the previous commit had a silent-corruption footgun: passing the wrong binding resolved a stub for a different DO against the recorded parent name. If the target class happened to share method names with the recorded parent, calls would succeed silently against the wrong data. Change the API to take a class reference (symmetric with `subAgent(Cls, name)` on the parent side), plus two runtime guards: 1. `cls.name === parentPath[0].className` — catches the wrong-class mistake directly. Error names both the passed and the recorded class so the diagnostic is actionable. 2. `env[cls.name]` exists — catches the "binding name ≠ class name" case with a suggestion to use `getAgentByName(env.X, this.parentPath[0].name)` directly. Usage collapses from await this.parentAgent(this.env.Inbox as DurableObjectNamespace<Inbox>) to await this.parentAgent(Inbox) Symmetric with `this.subAgent(Chat, id)`. JSDoc now also documents how to reach grandparents (iterate `this.parentPath`; there's no framework helper for further ancestors — the one-hop case is 95% of usage). Example `multi-ai-chat`: - `Chat.getInbox()` uses the new form: `this.parentAgent(Inbox)`. - `Inbox.onBeforeSubAgent` now returns a class-agnostic `"${className} "${name}" not found"` body (previously said "Chat not found" for anything, stale after we dropped the className-equality guard). Tests: - Existing `resolves the parent stub from within a facet` test now exercises the class-ref form (casts dropped). - New `throws when the passed class doesn't match the recorded parent class` test verifies the class-mismatch guard. Asserts both class names appear in the error body. Made-with: Cursor * ai-chat: restore ChatMessage/messages compat + align docs/RFCs Implements the review decisions directly: 1. **`messages` stays a public field.** Revert the getter + `_messages` backing field experiment in `AIChatAgent`. The compatibility cost was real, the benefit was thin, and existing subclasses may legitimately assign `this.messages = [...]` or mutate it directly. Internals now write `this.messages` again. 2. **`ChatMessage` stays exported.** Internally the codebase still standardizes on `UIMessage`, but the package now keeps `export type ChatMessage = UIMessage` so existing user imports from `@cloudflare/ai-chat` do not break. 3. **Docs / README / changeset sweep.** - `packages/ai-chat/README.md` - API header updated to `AIChatAgent<Env, State, Props>` - `messages` described as public + mutable for compatibility - exports table includes `ChatMessage` - `docs/chat-agents.md` - `ChatRecoveryContext.messages` → `UIMessage[]` - stale `this.messages = []` example → `await this.saveMessages([])` - top-level `README.md` - adds Sub-agents feature row - includes `examples/multi-ai-chat` in the examples tour - `packages/agents/README.md` - adds a new Sub-agents section (`subAgent`, `onBeforeSubAgent`, `useAgent({ sub })`, `parentAgent`) - `packages/agents/AGENTS.md` - refreshes the source layout (`sub-routing.ts`, `chat/`) - adds `agents/chat` export, but explicitly frames it as a sibling-package support layer rather than a broad user-facing surface - updates the stale `src/index.ts` line count and test-suite list - `design/AGENTS.md` - adds missing entries for `rfc-think-multi-session.md` and the AIChatAgent stance RFC - `.changeset/ai-chat-cleanups.md` - reflects the actual compatibility decisions (`ChatMessage` kept, `messages` stays mutable, `parentAgent(Inbox)` in the example) 4. **Rewrite the AIChatAgent RFC around the real stance.** `design/rfc-ai-chat-maintenance.md` is now: - retitled to remove "maintenance" - marked `Status: accepted` - explicit that `AIChatAgent` is first-class, production-ready, and continuing to get features - corrected to say `messages` stays mutable and `ChatMessage` stays exported - reframed `agents/chat` as primarily a sibling-package shared toolkit today (published, versioned, but not yet over-marketed) 5. **Update RFCs for shipped reality.** - `rfc-think-multi-session.md` now reflects the shipped `parentAgent(Cls)` helper instead of the old generic / manual `parentPath` lookup text. - `rfc-sub-agent-routing.md` now reflects `className`, `parentAgent(Cls)`, current `listSubAgents` return shape, and the post-launch facet semantics (facet broadcasts, keepAlive no-op). Checks: - `npm run check` — all 75 projects typecheck successfully - `packages/ai-chat` workers tests — 414/414 passing - `packages/agents` workers tests — 1005/1005 passing (7 skipped) Note: full workspace browser projects still require Playwright browsers installed locally; they were not runnable in this environment. Made-with: Cursor * docs(ai-chat): use ChatMessage as the public type language Normalizes the user-facing wording around AIChatAgent: - is the public message type name in docs / README / changeset / RFCs - is described simply as the public field users already know, without compatibility framing - removes the lingering / language from user-facing AIChat docs This matches the actual public stance: we never shipped a breaking change to , and we don't need to narrate the public API as an apology for a change that never landed. Also updates the chat API design doc so the analysis uses the same public terminology () instead of oscillating between and . Made-with: Cursor * agents: fix parentAgent root-vs-direct + example polish Four independent review fixes: 1. parentAgent root-vs-direct-parent bug (real, silent-corruption footgun). parentPath is root-first, so the direct parent is the LAST entry, not the first. The previous implementation did `const [parent] = this._parentPath` which destructures the first element — fine for one-level chains (Root -> Chat), but for any deeper chain (Root -> Outer -> Inner) `parentPath[0]` is the root and not the spawning parent. `parentAgent(Outer)` from Inner would then either throw a confusingly wrong class-match error, or — if the caller passed `Root` to silence the error — quietly resolve a stub to the wrong DO. Fix: use `this._parentPath[this._parentPath.length - 1]`. Update the JSDoc and the diagnostic error messages to reference `parentPath.at(-1)`. Regression test added: a doubly-nested Inner facet calling `parentAgent(TestSubAgentParent)` must throw with the real direct parent `OuterSubAgent` named in the error. 2. `_cf_initAsFacet` JSDoc claimed setting `_isFacet` early was needed so broadcasts would be suppressed during the first `onStart()`. That guard was removed in `e5827d54` ("let facets broadcast to their own WebSocket clients"). The note has been rewritten to reflect the actual remaining reason (schedule guards still branch on `_isFacet`, not broadcasts). 3. Example violated the "no `dark:` Tailwind variants" rule in `examples/AGENTS.md`. Replaced `bg-red-50 dark:bg-red-950/20` / `text-red-600 dark:text-red-400` with the Kumo semantic tokens (`bg-kumo-danger-tint`, `text-kumo-danger`). 4. Example was missing the required `public/favicon.ico`. Copied from `examples/assistant/public/favicon.ico`. Also updated the server header comment in `examples/multi-ai-chat/src/server.ts` and the `rfc-sub-agent-routing.md` note about "the last entry of parentPath" so the public docs match the implementation. Made-with: Cursor * docs: add sub-agents reference + correct long-running guide This fills the biggest documentation gap around sub-agents / facets: there was no single user-facing page that explained the shipped primitive end-to-end. Users had to piece it together from the routing RFC, the Think-specific `chat()` docs, the long-running-agents guide, and the multi-ai-chat example. ## New: `docs/sub-agents.md` A dedicated user-facing reference page covering the primitive as it works today: - what a sub-agent / facet is - when to use it vs a top-level DO - `subAgent`, `deleteSubAgent`, `abortSubAgent` - `onBeforeSubAgent` - `hasSubAgent`, `listSubAgents` - `parentPath`, `selfPath`, `parentAgent(Cls)` - `useAgent({ sub: [...] })` - `routeSubAgentRequest`, `getSubAgentByName` - lifecycle / routing flow - current limitations (no independent alarms on facets **yet**) - link to the multi-ai-chat example ## Fix: `docs/long-running-agents.md` The existing "Delegating to sub-agents" section said: > Sub-agents are independent Durable Objects. They have their own > state, their own schedules, and their own lifecycle. That is not true today. Facets have their own state and lifecycle, but *not* their own alarms. `schedule()` / `scheduleEvery()` are unsupported on facets at the moment. The text now says so explicitly, notes that support is coming soon, and points readers at the new sub-agents page for the full routing / client / parent-lookup story. ## Navigation - `docs/index.md` now links to `./sub-agents.md` under Core Concepts. - `docs/think/sub-agents.md` now makes its scope explicit: it covers Think's `chat()` RPC method and programmatic turns, while the generic framework primitive lives in `../sub-agents.md`. ## Design docs - Add `design/sub-agent-routing.md` as the living design doc for the shipped primitive (the RFC remains the historical decision record). - Register it in `design/AGENTS.md` and `design/README.md`. - Fix one confusing example in `design/rfc-sub-agent-routing.md` where the array order in the `parentPath` example contradicted the comment (`root -> direct parent`). Made-with: Cursor * Add favicon to multi-ai-chat example Insert a <link rel="icon" href="/favicon.ico" /> tag into the head of examples/multi-ai-chat/index.html so the page displays the site favicon and improves UX.github.com-cloudflare-agents · f834c814 · 2026-04-22