Matt
90d · built 2026-07-24
90-day totals
- Commits
- 65
- Grow
- 21.9
- Maintenance
- 14.2
- Fixes
- 7.3
- Total ETV
- 43.5
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 98 %
- By Growth share
- Top 21 %
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).
↑+233.3 %
vs 12 prior
↑+9.6 pp
recent vs prior
↑+13.6 pp
recent vs prior
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'.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
| Repo | Commits | ETV |
|---|---|---|
| agents | 60 | 42.3 |
| cloudflare-docs | 4 | 1.2 |
Most impactful commits
Top 20 by ETV in the 90-day window.
- 7.0ETVfeat: Cloudflare-native AI tracing (agents/observability/ai) (#1860) * feat: initial pass of cloudflare-native ai tracing Co-authored-by: msmps <7691252+msmps@users.noreply.github.com> * feat: add ai sdk v7 telemetry support to ai-tracing Co-authored-by: msmps <7691252+msmps@users.noreply.github.com> * chore: align ai-tracing with repo conventions - match workspace devDependency versions (sherif) - oxfmt formatting, remove unused type imports (oxlint) - bundler moduleResolution with extensionless relative imports - build with tsdown like sibling packages (cloudflare:workers kept external) - explicit types field for TS 6 (no automatic @types inclusion) - start at version 0.0.0 with an initial-release changeset - update pnpm lockfile * refactor: fold ai tracing into agents observability exports Move the ai-tracing package into the agents package: the tracer core (createTracer, the cloudflare:workers-bound tracer, span types) is exported from agents/observability and the AI SDK v6/v7 adapters from the new agents/observability/ai entry. The cloudflare:workers 'tracing' export is accessed via the module namespace with a no-op fallback so runtimes that predate it degrade gracefully instead of failing at module-link time (the observability module loads with the main agents entry). The hand-rolled cloudflare:workers type shim is dropped in favor of @cloudflare/workers-types. Tests run in the agents workers pool. Co-authored-by: msmps <7691252+msmps@users.noreply.github.com> * feat: align tracing schema with OTel GenAI semconv, harden public surface Schema (per semconv research; nothing shipped, renames free): - span names follow the semconv formula with a 64-byte bare-op fallback: 'invoke_agent {agent}', 'chat {model}', 'execute_tool {tool}' — the stable query key is gen_ai.operation.name, never the span name - vendor keys move to cloudflare.agents.* (ai.* is the Vercel AI SDK's de-facto namespace); ai.tool.call_id becomes semconv gen_ai.tool.call.id - failures record otel.status_code: ERROR + error.type (the spec-defined status encoding for status-less backends) instead of a bare error boolean; cancellations record cloudflare.agents.canceled and are not errors - gen_ai.provider.name normalized to the semconv enum; gen_ai.request.stream emitted only when true; gen_ai.response.time_to_first_chunk and response id/model captured on the stream path Wrapper fixes surfaced by the trace-content audit: - AI SDK v6 signals aborts as in-band {type:'abort'} chunks and never rejects with AbortError — recognize them so aborted streams close as canceled instead of false successes - streaming tools (async-generator execute) keep their execute_tool span open until the iterable is consumed instead of finishing at ~0ms - tool spans carry gen_ai.tool.call.id from the execute options Public surface hardening (runtime will gain native OTel support later): - types renamed to avoid @opentelemetry/api collisions: AgentTracer, AgentSpan, TraceAttributes, TraceAttributeValue; startSpan renamed openSpan (OTel's startSpan means create-without-activating — a semantic inversion) - createTracer, SpanRuntime, SpanWriter, MaybePromise are private: SpanRuntime is the OTel-convergence seam and must stay free to change * feat: instrument think out of the box Zero new public surface. Think's streamText call routes through the always-on agents/observability/ai wrapper, so every turn emits an 'invoke_agent {agent class}' root span with 'chat {model}' and 'execute_tool {tool}' children in Workers Observability. - the admittedTurnContext ALS internally carries trigger/admission/channel/ continuation/generation; _turnTelemetry() injects agent identity and turn metadata into experimental_telemetry.metadata (caller values win; inert for the AI SDK's own telemetry unless enabled) - agents adapters (v6 + v7) project telemetry metadata onto root-span attributes: reserved keys -> cloudflare.agents.turn.*, userId -> user.id, other scalars -> cloudflare.agents.metadata.{key}, objects dropped - drain loops finalize the underlying model stream on early exit (in-stream error break, stall abort, user abort) via a WeakMap finalizer calling consumeStream — the SDK tees its base stream, so an abandoned tee branch would otherwise leave the operation span open forever - wrapModel skips middleware for gateway-style string model ids (the root span still carries the model) * fix: address external code review of the tracing wrapper Verified against the pinned ai@6.0.208 and fixed: - stream observation now unwraps the SDK's {part} baseStream envelope — previously real spans missed usage, finish reasons, errors, and aborts (only look-alike test fixtures passed); added real-SDK integration tests (actual streamText + MockLanguageModelV3) covering envelope unwrapping, in-band error/abort parts, tool call ids, and time-to-first-chunk - removed the eager result-getter 'safeguard': steps/totalUsage/finishReason getters call consumeStream(), so touching them started hidden stream consumption at wrap time; added a laziness regression test - untraced fast path: when an invocation is not traced the wrapper calls the original operation with the original params — no tool wrapping, no model middleware, no stream patching (AgentSpan gains readonly isTraced) - main agents entry no longer initializes tracing: diagnostics-channel events moved to observability/events.ts; the public barrel composes events+tracing - provider doStream now runs inside the chat span's activation so provider work nests under it; stream patching fails open on unknown result shapes - extractors read the public result shapes (inputTokenDetails/ outputTokenDetails, response.modelId, deprecated flat fields) and string gateway model ids - think: agents peer floor raised to >=0.18.0; the early-exit stream drain is idempotent (deleted before invocation) and rides ctx.waitUntil - v7 tool spans keyed by callId:toolCallId (concurrent id reuse); operation wrappers cached for stable identity; tracer attribute writes fail-safe; cloudflare.agents.operation.id renamed to .operation.name (values are names) * fix: address round-2 review findings - untraced calls no longer compute the span spec: roots open with only the semconv name (agent name via direct property reads) and empty attributes; the full spec — metadata enumeration, request fields, context allowlists — is computed after the isTraced check and written through an internal writeSpanAttributes seam, so caller getters/proxies are never enumerated on untraced calls - think drains the model stream only on early exits (break or throw), via a natural-exhaustion flag — consumeStream is not a no-op (it tees baseStream and traverses the buffered branch), so draining every call was per-inference overhead; a thrown exit (stall watchdog) still drains - the finalizer runs exactly once: the drain promise is created before ctx.waitUntil, so a missing/throwing waitUntil cannot start a second tee consumer - async-generator tool bodies are re-entered into the tool span's async context via AsyncLocalStorage.snapshot() on every pull, so spans created inside the body parent under execute_tool (verified in workerd) - extractors: provider response-metadata stream parts populate response id/model on chat spans; v7 reads public usage detail shapes (inputTokenDetails/outputTokenDetails + deprecated flat fields) and prefers the served response.modelId over the requested event.modelId * fix: forward early termination to streaming tool iterators The round-2 manual iterator.next() loop dropped for-await's automatic return() forwarding: a consumer breaking while the wrapper was suspended at yield closed the span but never ran the tool generator's own finally blocks. The wrapper now tracks exhaustion and, on early termination, forwards iterator.return() inside the tool span's context before finishing the span. Regression test: consumer breaks after the first yield; the tool generator's cleanup runs (and a span opened in that cleanup parents under execute_tool). * fix(observability): keep tracing adapter internal * refactor(observability): trim tracing surface * fix(observability): correct AI trace semantics * fix(observability): retain span name limit * docs(observability): remove repeated scope section * refactor(observability): scope AI tracing to SDK v6 * feat: wrap agent initialization in tracing span Group constructor-time setup (method wrapping, schema creation, MCP client manager initialization) under one stable agent_initialization span so the UI can collapse it instead of surfacing top-level clutter, and give init-specific trace behaviour a hook. The agent id attribute is read defensively: facets restore their name after construction and idFromString()/newUniqueId() DOs are named later via setName(), so an unreadable name leaves the attribute unset instead of failing construction. * feat(observability): restore AI SDK v7 telemetry integration Restore the v7 Telemetry adapter (createAISDKTelemetry) alongside the v6 wrapAISDK, conformed to the ai@7.0.22 GA Telemetry interface. The adapter's structural event/hook types remain independent of the "ai" package so it still compiles in this v6-installed repo. Re-adds the cloudflare.agents.call.id correlation attribute and the v7 docs sections removed when v7 was scoped out. - observability/ai/v7/{types,extract,telemetry}.ts - observability/ai/index.ts: re-export createAISDKTelemetry - genai/attributes.ts: restore Cloudflare.CallID - tests: ai-sdk-v7-telemetry.test.ts (structural, RecordingTracer) - docs + changeset: v7 usage via registerTelemetry / experimental_telemetry * feat(observability): opt-in span content capture Add an explicit, default-off opt-in for recording chat inputs/outputs and tool inputs/outputs on the AI SDK tracing spans. This content is potentially PII, so it is emitted only when a record flag resolves to true; the default projection remains content-free. - v6: `recordInputs`/`recordOutputs` on the `wrapAISDK` options, plus per-call `experimental_telemetry.recordInputs`/`recordOutputs` (authoritative, mirrors the AI SDK's own TelemetrySettings). Chat inputs and streamed/generated output and tool arguments/results are serialized onto the operation and execute_tool spans. - v7: `createAISDKTelemetry(options)` gains the same flags; content is read from the event fields the adapter already receives and emitted only when opted in. - Shared genai builders serialize each value to a JSON string attribute truncated to a safe byte cap with a marker; semconv-aligned keys (gen_ai.input.messages / gen_ai.output.messages / gen_ai.tool.call.arguments / gen_ai.tool.call.result). Never emitted on error/abort beyond the flag. - Think exposes a single `recordTraceContent` flag (off by default) that flows into the per-turn telemetry; agent-think opts in. Tests assert the default records no content attribute and the opt-in records the expected serialized (and truncated) value for v6, v7, and Think. Docs updated with the opt-in, flagged as PII-recording and off by default. * refactor(observability): match AI SDK recordInputs/recordOutputs in Think; cap content to span budget Think exposes recordInputs/recordOutputs fields (matching the AI SDK's own TelemetrySettings and the tracing adapter) instead of a single recordTraceContent flag; agent-think opts into both. Derive the content-attribute cap from workerd's 64 KiB MAX_SPAN_BYTES total-span budget (split across the up-to-two content attributes a span can carry, with headroom for scalar metadata) instead of a flat 4 KiB. * docs(observability): clarify system-role message + per-turn override + metadata budget notes * fix(observability): record message content on chat spans * feat(observability): trace tool approval lifecycle * fix(think): run durable submissions from alarm invocations * feat(observability): reference AI Gateway logs * fix(observability): restore opt-in GenAI payloads * feat(observability): group agent storage spans * fix(observability): conform stored messages to GenAI schemas Map AI SDK-native message fields to the OpenTelemetry GenAI role/parts contract, including canonical text, reasoning, tool-call, and tool-response parts. Embed normalized finish_reason in buffered and streamed outputs so Workers Observability can render Think traces.\n\nAdd dedicated genai_semantics coverage plus real AI SDK integration assertions for system history and streamed tool calls. --------- Co-authored-by: msmps <7691252+msmps@users.noreply.github.com> Co-authored-by: Thomas Ankcorn <tankcorn@cloudflare.com>github.com-cloudflare-agents · f5b1dd81 · 2026-07-21
- 4.6ETVfeat(agent-think): issue repro/fix agent on Workers + Containers (#1861) * feat(agent-think): issue repro/fix agent on Workers + Containers A Think agent that reproduces and fixes cloudflare/agents GitHub issues inside a container-backed @cloudflare/workspace VFS, triggered from an issue comment (@agent-think <instruction>) via a GitHub App webhook worker. Supersedes the CI-based /repro + /pr Actions from #1844 — same skills, but running as a persistent Worker + pre-warmed container instead of Actions runners. Architecture (patterns from aron/cloudflare-workspaces-prototype): - AgentThink WorkerEntrypoint: dispatch() RPC from the webhook worker; returns in ~1s (submitMessages only — container gh/git auth happens inside the durable turn via beforeTurn, so the caller's waitUntil cancellation window can never kill the run) - ThinkAgent DO owns the Workspace (SQLite VFS) + the durable turn; two exec backends: container (full Linux: gh/git/npm/node/wrangler) and just-bash isolate for cheap text ops - Sandbox DO hosts the Cloudflare Container; WarmPool DO keeps one pre-warmed and hands them out per session - live thread UI (Vite + React) at /thread/:session - skills (reproduce / open-pr) mounted read-only from R2; repros must ship a minimal Vite frontend so maintainers can click the deployed URL and watch the failing behavior in a UI The worker holds no GitHub App credentials — the webhook worker mints a short-lived installation token per dispatch. Note: requires the enable_abortsignal_rpc compat flag — the container backend's health probe passes an AbortSignal over cross-DO RPC. Verified end-to-end in prod on issue #1859: trigger comment to bot reply in 5s, 30-minute turn with real clone/install/deploy, structured repro report posted back on the issue. * docs(agent-think): HANDOFF — branch/PR landed, note skill-recipe verification * docs(agent-think): gh-app no longer posts an 'on it' comment (👀 only) * chore(agent-think): AGENTS.md, tidy root configs, standardise on .env - Replace HANDOFF.md (session-log style) with AGENTS.md: aims, how the system works, the rules we hold ourselves to, and the edge cases that cost real debugging time (abortsignal RPC flag, lossy tail, stale container reconcile, R2 skill seeding, Access, WARP builds). - Vitest configs move next to their suites (test/, tests-e2e/); root keeps only vite.config.ts (thread UI build). - Env files standardise on .env + .env_example (the e2e harness reads .env); drop .dev.vars.example and the stray root WARP pem copy. - Drop the vite alias workaround for agents/chat/react — the subpath export exists upstream now, plain resolution works. * chore(agent-think): prune unused deps, add author @cloudflare/worker-bundler, ws, @types/ws: imported nowhere. @cloudflare/workers-types: redundant — tsconfig consumes the wrangler-generated worker-configuration.d.ts runtime types only. (isomorphic-git and @platformatic/vfs stay: optional peers of @cloudflare/workspace whose main entry — which we bundle — imports both; git.diff runs on isomorphic-git.) * fix(agent-think): commit generated worker types for CI; address review - Commit worker-configuration.d.ts (wrangler types) and stop ignoring it — CI has no way to generate it, so the tsconfig types reference failed with TS2688 on a fresh checkout. - tsconfig extends agents/tsconfig (verbatimModuleSyntax et al.), with the types list overridden to the generated runtime file — keeping @cloudflare/workers-types alongside it would conflict. client.tsx now typechecks too (was outside the old include). - compatibility_date 2026-05-26 -> 2026-06-11 (repo standard), both configs; types regenerated against it. - write tool now takes the same per-file lock as edit: its stat-then-write mode preservation had the same interleaving window edit's read-modify-write guards against. Lock extracted to src/tools/fs/file-lock.ts. * docs(agent-think): advertise auto-created parent dirs in the write tool The store already mkdir -p's the parent on every write; telling the model saves it a container exec mkdir round-trip first. * feat(agent-think): GPT-5.5 via gateway catalog + command-center UI Model: openai/gpt-5.5 through the default AI Gateway's model catalog (Unified Billing over the AI binding — no provider key). The providers: [openai] plugin is required: workers-ai-provider refuses {provider}/{model} slugs without it (verified empirically: text, tool calls, and streaming all work with the plugin; raw env.AI.run works either way but Think needs an AI SDK LanguageModel). Command center: the root URL is now a dashboard run by a singleton CommandCenterAgent (synced-state registry of every thread + counters). ThinkAgent reports dispatch/tool/turn events fire-and-forget — observing must never break a run. The UI gains a ChatGPT-style left sidebar listing threads reverse-chronologically, live over agents state sync; /thread/:session renders inside the same shell. The old plain-text root banner is gone (root serves the SPA, with a worker fallback where asset-first routing is not emulated). * feat(agent-think): command-center repo cards + sidebar search Main screen leads with per-repo cards (name, github link, issue/status counts) per the wireframe; the sidebar gains a search filter and a ChatGPT-style Recents treatment. Sidebar persists across thread navigation (unchanged). * fix(agent-think): per-comment turn idempotency The repo#issue idempotency key silently swallowed re-mentions: once an issue's first turn completed, submitMessages returned the old submission (accepted:false) and nothing ran. The key now includes the triggering commentId (passed by gh-app); dev dispatches without one get a random key. Webhook redeliveries are already deduped in gh-app's KV before dispatch, so nothing is lost. * chore(agent-think): observable command-center reporting Log (never throw) when a lifecycle report fails, and emit one structured line per registry update — silent-success and silent-failure were indistinguishable in the logs. * fix(agent-think): hermetic assets fixture for the unit suite CI has no vite output (dist/client is not committed), so the root-route test read an empty body. The test config now points ASSETS at a committed fixture with the SPA root node. * fix(agent-think): HTTP snapshot fallback for the command center Cloudflare Access on the domain passes authenticated HTTP but eats WebSocket upgrades (zero WS ever reached the worker — the thread view only worked via useAgentChat's HTTP get-messages polling). Plain useAgent state sync has no such fallback, so the command center rendered empty. GET /api/command-center returns the registry snapshot; the client hydrates from it and polls while the WS is not connected. * feat(agent-think): issue title + requester avatar on thread rows ThreadMeta carries the GitHub issue title and who mentioned @agent-think (login + avatar). Activity rows and the sidebar show the title; the requester's avatar sits on each row with a hover tooltip ('login: instruction'). Both flow from the webhook payload through dispatch; old threads without the fields fall back to the instruction. * fix(agent-think): route /agents/* and /api/* worker-first — WS upgrades died at the assets router The assets layer forwards ordinary no-asset-match requests to the worker but not WebSocket upgrades, so every wss:// connect to /agents/* failed while plain HTTP worked — which is why the command center sat on the HTTP fallback and showed 'disconnected'. (Corrects the earlier Access diagnosis; Access passes authenticated WS fine.)github.com-cloudflare-agents · d1ce3cbd · 2026-07-03
- 4.1ETVfeat(codemode): connector model + durable runtime, snippets, and vite plugin (#1581) * feat(codemode): connector model with durable runtime, skills, and vite plugin Executor is the dumb code sandbox (DynamicWorkerExecutor, IframeSandboxExecutor). CodemodeRuntime is a DurableObject facet that wraps an executor and makes execution durable via abort-and-replay. Connectors — class-based service integrations (WorkerEntrypoint subclasses): CodemodeConnector, McpConnector, OpenApiConnector, ToolsetConnector Runtime — durable execution engine: - Every tool call recorded in a durable log (the replay spine) - Observations execute and record; approval-required actions abort the run - resumeCodemode() replays the log and runs the approved action - rejectCodemode() / rollbackCodemode() for HITL resolution - codemode.get/set persist scratchpad state across runs Model-facing tool: createProxyTool({ ctx, executor, connectors, skills }) → { code }. Sandbox SDK: codemode.search/describe/connectors/pending/run/get/set + connector globals. Skills: CodemodeSkillSource interface — pluggable reusable code patterns. Vite: @cloudflare/codemode/vite discovers *.codemode.ts, auto-exports connectors + runtime. Search: Executor-style ranked search with normalization/scoring. Connectors support revertAction() for rollback. * feat(codemode): runtime handle with pending(), spec+request OpenAPI surface - createCodemodeRuntime({ ctx, executor, connectors }) returns the runtime handle; runtime.tool() is the primary way to expose codemode to a model - add runtime.pending() (and pendingCodemode) so approval UIs can list actions awaiting approval, per the RFC runtime API - OpenApiConnector is now two overridable primitives: spec() returns the OpenAPI doc into the sandbox, request() performs an authenticated call; drop the search substring matcher and operationId dispatch - docs, README, changeset and PR body updated to snippets language and the runtime-first API * Merge origin/main into feat/codemode-executor-style-providers (pnpm migration) * feat(codemode): trim to the minimal API surface Sandbox SDK is now five methods: search, describe, step, save, run. Removed codemode.connectors()/pending()/fork()/get()/set()/snippets(): - pending() was dead code (a pause aborts the run, so there is never anything pending while model code is executing) - fork() is a host decision, not a model decision - connectors() duplicated the tool description and search/describe - get/set duplicated step (deterministic code recomputes on replay; nondeterministic work belongs in a step) - snippets() duplicated search Host runtime handle is now: tool, pending, approve, reject, rollback. - removed the resume() alias of approve() - removed runtime.fork() and the facet fork(): no concrete developer story yet; the replay log supports re-adding it later - removed the duplicate description option on createCodemodeRuntime (set it on runtime.tool({ description }) instead) - scratch state and parentId removed from ExecutionState The low-level proxy-tool functions (createProxyTool, resumeCodemode, rollbackCodemode, ...) are no longer exported: the runtime handle is the one public API, matching the RFC's one-way-of-doing-things thesis. Docs, changeset, PR body and the RFC wiki (v12) updated to match. * feat(codemode): one tools() record per connector, curated snippets, execution audit trail Connector authoring is now a single surface. A connector is three things: name(), instructions()?, and tools() — one record, one entry per tool, with each tool carrying its own description, schema, requiresApproval, execute, and optional revert. The old parallel string-keyed maps (loadDescriptors/annotations/executeTool/ revertAction) are now wire plumbing derived from the record, not something authors write. ToolsetConnector is deleted: AI SDK toolsets are shape-compatible and return from tools() directly. Derived connectors (MCP) are decorated via a single tool(name, t) hook. observation and approvalDescription annotations are gone (observation was behaviorally dead; the approval UI uses the tool's own description). setConnection two-phase init replaced by constructor injection in the example. Snippets are curated by the developer, not self-promoted by the model: codemode.save is removed from the sandbox; runtime.saveSnippet(name, { executionId? }) promotes any run's script, with runtime.snippets() and runtime.deleteSnippet(name) for management. runtime.executions() exposes the full run history (the audit trail) for developer UIs. The sandbox SDK is now four methods: search, describe, step, run. Also: docs/codemode overhauled around why/configure/use per page (search-and-describe.md folded into runtime.md), example dependency versions aligned with the workspace so sherif passes, RFC wiki updated to v15. * chore(codemode): refresh PR body with audience-split API summary * fix(codemode): address review findings - connector sandbox proxies guard non-string property access, matching the dispatcher proxy, so symbol lookups no longer produce bogus RPC calls - codemode.run executes snippets with the platform provider attached: snippets are saved execution code and may use codemode.step, which previously threw ReferenceError inside a snippet run - McpConnector throws on sanitized tool-name collisions instead of silently dropping tools; override toolName() to disambiguate - add connector base tests (describe derivation, execute/revert dispatch, tool() decoration hook, collision error) * fix(codemode): harden durable runtime — stateless, explicit executionId, resilient rollback Reworks the CodemodeRuntime durable-execution model for correctness under hibernation and concurrency, simplifies the OpenAPI connector, and adds an end-to-end test suite. Runtime architecture - Make CodemodeRuntime stateless across calls: no in-memory cursor or annotations. Every interaction is addressed by (executionId, seq), with seq allocated host-side, so a run survives eviction between any two tool calls. - Remove the global CURRENT_KEY "current execution" pointer and its helpers (#currentId, #current, #resolve). approve/reject/rollback/saveSnippet now require an explicit executionId, eliminating a class of races when multiple runs share one Durable Object. - Thread executionId through to every tool outcome: ProxyToolOutput now includes executionId on completed/paused/error so callers can follow up (e.g. saveSnippet) without guessing the newest run. Replay correctness - Add "executing" ToolLogEntryState: non-approval calls/steps are logged as executing by decide() and only promoted to "applied" once recordResult() stores the real value. A crash between the two re-executes instead of replaying undefined. - Detect replay divergence by hashing connector/method/args via a stable stringify (sorted keys, bigint-tagged). Divergence is recorded as a terminal error and surfaced as { status: "error" } rather than thrown across RPC. - Guard decide() on terminal/paused state: once a run is paused/error/ rolled_back, further decide() calls are inert and return a pause decision, so model code that swallows the pause sentinel cannot apply more side effects. Approvals & rollback - rollback() now reverts ALL applied reversible actions (any tool with a revert), not just approval-gated ones, in reverse order. requiresApproval (pause-before-do) and revert (undo-after-do) are orthogonal. - Make rollback resilient: each revert is wrapped in try/catch, all reverts are attempted, failures are aggregated and thrown, and the run is marked with the new "rolled_back" status when anything was undone. - listPending()/pending() aggregate pending actions across ALL paused runs when no executionId is given, fixing a racy single-run approvals view. - Document that reject() ends a paused run but does not undo applied actions. Execution retention - begin() accepts maxExecutions and prunes old terminal runs automatically; add explicit deleteExecution() and pruneExecutions() APIs. Connectors / DX - OpenApiConnector derives one typed tool per operation host-side (e.g. repoApi.get_repository) instead of making the model parse the raw spec; request() remains as an escape hatch. Adds module-level memoization of derived operations (WeakMap keyed by spec), deeper $ref resolution across allOf/oneOf/anyOf/additionalProperties, and collision warnings for operation names that clash or hit reserved names. - Pass connector bindings as RpcTarget evaluate() arguments instead of via worker env to fix DataCloneError; route pause via a control marker rather than throwing across the sandbox→host RPC boundary. - Switch DynamicWorkerExecutor to loader.load() for one-off dynamic workers (loader.get(random-id) gave no caching benefit). - Widen CodemodeConnector ctx to DurableObjectState | ExecutionContext so connectors inside a Durable Object no longer need to cast this.ctx. - revertAction() returns boolean to report whether a revert actually occurred. Tests, docs, cleanup - Add src/runtime-tests/ e2e suite (vitest-pool-workers) driving a real DO host: read-only, pause/approve, replay, reject, rollback (+rolled_back), divergence, step replay-safety, concurrent runs, retention, snippets, delete, pause-swallow guard, and pending aggregation. Wire vitest.runtime.config.ts into test script. - Rewrite examples/codemode-connectors with an approvals panel and snippet flow. - Update changeset and docs (runtime, approvals, connectors, index, READMEs). - Delete orphaned src/mcp-provider.ts and stray .pr-body.md / EXECUTOR_TODO.md. * feat(codemode): per-execution connector lifecycle + result shaping Adds the two codemode primitives needed for stateful connectors (e.g. reusable browser sessions) to ride on the durable runtime instead of reinventing session storage, plus a model-facing result transform. Both are additive. Per-execution resource lifecycle - A tool's execute(args, ctx) and revert(args, result, ctx) now receive a ToolExecuteContext carrying the run's executionId, stable across pause/resume, so a connector can lazily acquire/reconnect a resource keyed by that id. - CodemodeConnector.disposeExecution(executionId, status) is an optional override (default no-op) called when a run reaches a terminal state, so a connector can tear the resource down. It fires on each terminal transition (completed/error/rejected/rolled_back) and never on pause — a paused run may resume, so the resource must outlive a pause. Documented to be idempotent (a completed-then-rolled-back run disposes twice), to not rely on instance memory (keyed off durable storage), and to never throw (rejections ignored). - A stale/no-op reject() no longer triggers teardown: runtime.reject now returns whether it actually terminated the run, and dispose is gated on that, so a still-resumable run keeps its resources. rejected is now a first-class ExecutionStatus - reject() marks the execution "rejected" instead of masquerading as "error", so the audit trail distinguishes a user rejection from a failure, and ExecutionEndStatus is exactly the terminal subset of ExecutionStatus. Result shaping - createCodemodeRuntime accepts an optional transformResult that reshapes the model-facing result of a completed run (initial run and resume), after the raw value is recorded — so the audit trail keeps the full result while the model sees the shaped one. A throwing transform falls back to the raw result rather than failing a completed run. - New exported truncateResult/truncateResponse (token-aware, { maxChars?, maxTokens? }) as the default building blocks: small structured results pass through unchanged; oversized ones serialize to a bounded, marked string. Tests + docs - e2e: executionId threading, dispose on complete/reject/rollback, no dispose while paused, no dispose on a stale reject, transformResult on run + resume. - unit: truncateResponse/truncateResult. - Documented the lifecycle contract, result shaping, the rejected status, and the sequential-tool-call determinism constraint; updated the changeset. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): guard resume() to paused runs + make example methods callable Two correctness fixes surfaced in review, plus the docs/readme/changeset updates that go with them. 1. resume() no longer revives terminal runs CodemodeRuntime.resume() reset status to "running" unconditionally, so approve({ executionId }) on a completed/error/rejected/rolled_back run flipped it back to running and re-executed it — bypassing decide()'s terminal guard. The concrete hazards: a rejected action's log entry (state "reverted") fell through decide() to a fresh "pending" entry, re-offering the exact action the user rejected; and a rolled_back run re-applied the side effects rollback had just undone. resume() now changes nothing unless the run is "paused" (returns null otherwise). resumeCodemode() distinguishes missing vs. not-paused and returns a { status: "error", executionId, error } ProxyToolOutput instead of throwing — matching the divergence/pause paths, so the result crosses RPC cleanly and the agent loop is never broken by an exception. This is intentionally a safe no-op rather than a hard error: approve() is operator-initiated (never on the model's tool path), and a stale/racing approval UI hitting an already-finished run is an expected race, not a caller bug. 2. example server methods are now @callable() examples/codemode-connectors exposed pendingApprovals/approveExecution/ rejectExecution/rollbackExecution/executions/saveSnippet/snippets for the client's agent.call(), but none carried @callable(). The Agent RPC dispatcher rejects any method without callable metadata ("Method X is not callable"), so the entire approval/snippet UI threw at runtime. Added the import and the decorators. Tests: new e2e "refuses to approve a terminal run, never re-offering a rejected action" (reject a paused run, then assert approve returns status:"error", no new pending action, no leaked side effects, run stays rejected). 282 unit + 21 e2e + 33 browser pass; pnpm run check clean. Docs: approvals.md and the example README document approve() as a safe no-op on a non-paused run; README snippets fixed to show @callable() and the now-required saveSnippet executionId; changeset updated. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): listPending() only surfaces paused runs pendingOf() filters log entries by state === "pending" without regard to the execution's overall status, and the aggregate listPending() scanned every execution. A non-paused run can retain a stale "pending" entry — #diverge sets status to "error" but leaves the log untouched, so a resume that diverges before reaching the pending entry ends the run as "error" while that entry stays "pending". Those entries aren't actionable (approve() is a no-op on a non-paused run), so they must not clutter the approval queue. listPending() now considers only paused runs on both paths. The explicit executionId path is only ever called from runPass on a confirmed-paused run, so guarding it is safe and makes "pending = actionable approval on a paused run" the consistent contract. This matches the docs, which already said "all paused runs" — the code was the side out of sync. Regression: the divergence e2e now asserts that after the run ends "error" with a leftover pending entry, both pending() and pending(executionId) return []. 282 unit + 21 e2e + 33 browser pass; pnpm run check clean. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): reserve __connectors so a provider can't shadow RPC bindings RESERVED_NAMES guarded __dispatchers (one evaluate() parameter) but not __connectors (the other). A provider named __connectors passed validation and emitted `const __connectors = new Proxy(...)` into the same function scope as the `evaluate(__dispatchers = {}, __connectors = {})` parameter — clobbering the RPC bindings that every connector proxy reads from (`__connectors.<name>.callTool`), and in fact producing a SyntaxError (const redeclaring a parameter binding). The connector validation path already special-cased "__connectors"; the provider path didn't. Add __connectors to RESERVED_NAMES so both providers and connectors are checked against it, and drop the now-redundant special case in the connector loop. Regression: new executor test asserts a provider named __connectors is rejected as reserved. 283 unit + 21 e2e + 33 browser pass; pnpm run check clean. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): connector errors return a marker instead of rejecting across RPC The connector binding's comment promised "the RpcTarget method always resolves" (control signals are returned, not thrown, to avoid an unhandled rejection on the host), but the execute/record path didn't honor it: connector.executeTool, runtime.recordResult, and runtime.decide could all reject ConnectorCallTarget. A rejected promise returned from a DO/RPC method is tracked as an uncaught (in promise) on the host even though the sandbox awaits it — so any throwing connector tool (a failed API call, a bug) produced a misleading "unhandled rejection" host trace. Correctness was already fine (the sandbox try/catch ends the run as "error"), but the noise contradicted the design. Make the error path symmetric with pause: the whole binding body is wrapped so it always resolves — to a result, a { control: "pause" } marker, or a new { control: "error", message } marker. The sandbox connector proxy re-throws the error marker locally, so the run's own try/catch records it and the run ends "error" with the message exactly as before — just without a host-side rejection. Regression: ItemsConnector gains a boom tool that throws; a new e2e asserts the run ends "error" with the message and the suite completes with no unhandled rejection. 283 unit + 22 e2e + 33 browser pass; pnpm run check clean. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): log connector-call failures on the host before returning the marker Returning an error marker keeps the RPC call from rejecting (no misleading unhandled-rejection trace), but a genuine connector failure still deserves a host-side log with its stack for debugging. Add a console.error in the binding's catch with the connector/method and execution id. This restores the visibility the pre-marker throw had — minus the "uncaught (in promise)" framing — while the message continues to reach the model and the audit trail via the run's "error" outcome. Pause is unaffected (it isn't an error and isn't logged). 283 unit + 22 e2e + 33 browser pass; pnpm run check clean. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): mark approved action "executing" before running it (reject race) decide() returned { kind: "execute" } for a just-approved (pending) entry WITHOUT persisting, leaving the entry "pending" in storage for the entire tool-execution window. Between decide() returning and recordResult(), the DO is idle (the tool runs on the host worker), so a concurrent reject() — e.g. a second UI tab — could read "pending", mark it "reverted", and set status "rejected"; recordResult() then overwrote the entry back to "applied". Net result: the side effect ran even though the user rejected it, and the status was left inconsistent. The fresh-call path already guarded this window by persisting "executing" before returning; the pending→execute path skipped it. Now the pending→execute transition writes "executing" before returning, so a racing reject() sees "executing" and no-ops (reject only acts on "pending"). decide() also handles an existing "executing" entry explicitly — re-execute, never re-pause — so a crash mid-execution recovers without re-requesting approval for an already-approved action (which a naive "executing" flip would have caused via the requiresApproval branch on the fall-through). Either interleaving is now consistent: reject-before-decide ends the run before the action runs (decide sees status != running → pause); decide-before-reject runs and applies the action while reject no-ops. Regression: new e2e drives the facet directly (begin → decide → resume → decide → reject → recordResult) and asserts the approved action is "executing" at the decision boundary, the racing reject returns false and leaves the run "running", and the action records as "applied". 283 unit + 23 e2e + 33 browser pass; pnpm run check clean. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(codemode): normalize snippet code before embedding it in codemode.run A snippet stores the model's raw code (runtime.begin keeps it verbatim, and saveSnippet copies it). codemode.run embeds that raw text as an expression: `const snippet = (${snippet.code})`. Normal runs and replay survive fenced or statement-style code because they pass through normalizeCode (strip markdown fences, wrap non-expressions into an arrow), but the snippet wrapper bypassed that — so a snippet saved from ```ts-fenced output or a statement block (`const x = ...; return x;`) became a syntax error on re-run. Normalize snippet.code to a valid arrow expression before embedding it, the same transform the executor applies to a fresh run; runCode still normalizes the outer wrapper. The fix lives in the execution layer (proxy-tool) so the runtime facet stays pure storage and snippet.code remains the faithful raw model output. Regression: new e2e saves snippets from both fenced (```ts ... ```) and statement-block code, then re-runs each via codemode.run and asserts they complete with the right result. 283 unit + 24 e2e + 33 browser pass; pnpm run check clean. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(examples): richer codemode-connectors UI + dark-mode fix Render assistant/user text through Streamdown (markdown + highlighted fences), add collapsible tool cards showing the model's code, result, console logs, and errors, and add a collapsible reasoning-trace block. Fix the user message bubble, which used a non-theme-aware `text-black` on the accent background, switching to `bg-kumo-contrast` + `**:text-kumo-inverse` so it reads correctly in both light and dark mode. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Matt Carey <matt@cloudflare.com> Co-authored-by: Sunil Pai <spai@cloudflare.com> Co-authored-by: Cursor <cursoragent@cursor.com>github.com-cloudflare-agents · b2b67623 · 2026-06-10
- 4.1ETVfeat(experimental): Postgres session providers with Hyperdrive support (#1297) * feat(experimental): PlanetScale SessionProvider + async interface * fix(session): address PR #1297 review feedback Responses to @mattzcarey review on PR #1297. Covers all 13 comments: 1. Drop wrapPgClient boilerplate — Postgres providers now accept raw pg.Client directly via new providers/postgres-adapter.ts. The adapter normalises pg.Client-style (`query`) into the internal PostgresConnection shape (`execute`) and rewrites `?` placeholders to `$1, $2, …` so providers keep a driver-agnostic SQL dialect. 2. appendMessage parentId semantics — both PostgresSessionProvider and AgentSessionProvider were using `parentId ?? latestLeaf`, which collapsed undefined (auto-detect) with null (explicit root). Fixed to honour the documented contract: - undefined / omitted → auto-detect - explicit null → root with no parent SessionProvider JSDoc now documents this. New tests cover both cases for both providers. 3. extractText — renamed to extractSearchableText, added JSDoc explaining it feeds text_content for FTS while the full JSON stays in `content`. 4/6/8. Restored `enum` on label params across set_context, load_context, unload_context, search_context — schema-level enforcement instead of free-text description hints so smaller models can't hallucinate invalid labels. 5. set_context metadata shape — switched from flat `title` to nested `metadata: { title?, description? }`. Tool description now explains metadata is optional and useful for longer loadable entries (skills). setSkill() receives description ?? title so behaviour is preserved when only title is passed. 7. Dropped 'e.g. memory' example from search_context description — avoids seeding models with a non-existent block name. 9. Renamed Session.create(storageOrAgent) → Session.create(provider). 10. Async skill restore — _ensureReady() now kicks off restoration as a background _restorePromise; a new _ensureRestored() awaits it. Every async Session public method awaits _ensureRestored() before touching storage or skill state. unloadSkill / getLoadedSkillKeys are now async (internal callers only). Async SessionProviders (Postgres) now correctly rehydrate loaded-skill tracking after DO hibernation instead of silently dropping it. 11. Added JSDoc to _reclaimLoadedSkill explaining it reclaims context-window tokens by collapsing a load_context tool result to a short marker (kept the name per review feedback). 12. Clarified addContext JSDoc: it's a builder/host API, not an LLM tool; the LLM writes via set_context. 13. Added a comment on the Think._cachedMessages in-place patch explaining why it's not a full _syncMessages() call (in-flight streaming messages would be dropped — see commits 3f615a24, 6e76bd49). Example server.ts + docs/sessions.md updated for the new API and fixed the Devin-flagged premature client-caching bug (client is only assigned after connect() resolves). Tests: +4 in postgres-providers.test.ts (parentId null/undefined, raw pg.Client adapter for session/context/search providers), +2 in provider.test.ts (same parentId semantics for AgentSessionProvider). 161/161 session-related tests pass. * chore(session-planetscale): align kumo + ai dep versions with workspace Bump @cloudflare/kumo from ^1.18.0 to ^1.19.0 and ai from ^6.0.159 to ^6.0.168 so the session-planetscale example matches every other package in the monorepo. Makes `npm run check` (sherif) pass without multiple-dependency-versions errors. * fix(session): resolve postgres provider rebase fallout Align the new PlanetScale example with the rebased workspace dependencies and update Think async-session call sites/tests so the branch stays typecheck- and lint-clean on current main. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(session): harden postgres provider follow-ups Tighten the Postgres session provider for shared database usage by scoping message id conflicts to (session_id, id) and validating explicit parent ids against the current session before storing them. This keeps caller-provided message ids safe across sessions and preserves the SQLite provider's fallback-to-root behavior for invalid parents. Make generated keys for keyed context writes deterministic but collision-resistant when the model omits metadata.title. Title-based writes remain stable update keys, while content-derived keys now include a short hash so long shared prefixes and non-Latin content do not silently overwrite unrelated skill or search entries. Clean up the new PlanetScale example and docs for merge readiness: remove committed Cloudflare account/resource IDs, document the required Hyperdrive placeholder, use raw pg.Client in examples, initialize the client/session from onStart instead of request-created promises, update Session docs for the async API, document the Postgres composite message primary key, and add the relevant changeset for the new public providers and async session surface. Tests cover cross-session duplicate message ids, foreign-session parent fallback, and generated key collision cases. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(think): keep session message cache coherent Teach Session to notify internal listeners after message mutations so cache-owning framework code can mirror durable storage changes without widening the public API. Think now registers that hook during startup, treats `messages` as its live cached view, and routes writes through history helpers that sanitize and enforce row-size limits before delegating to Session. This avoids full storage rereads during active streaming turns, while still refreshing at safe boundaries for duplicate appends, branch writes, deletes, clears, and compaction overlays. It also makes direct `this.session.appendMessage()` calls from advanced Think subclasses update the live cache through the same observer path. Add regression coverage for duplicate message IDs, compaction-triggered refreshes, direct Session appends, subclass append helpers, `getMessages()` copy semantics, and host-injected messages. Update the Session docs and PlanetScale example README for async APIs, Postgres-backed search/storage wording, Durable Object persistence semantics, and mark the changeset as a minor bump because the async Session API is breaking for 0.x consumers. Co-authored-by: Cursor <cursoragent@cursor.com> * Render skill blocks and label keyed block kinds Treat empty skill blocks as renderable in ContextBlocks so the LLM can discover loadable skill collections (add !block.isSkill to the skip logic). Include a human-readable kind for keyed/writable blocks in the set_context description ("skill collection, keyed entries", "searchable, keyed entries", or "writable"). Add unit tests and small test providers (EmptySkillProvider, WritableSkillProvider, WritableSearchProvider) to verify empty skill block rendering and that tools().set_context lists keyed block kinds and metadata fields. * Normalize Postgres timestamps and patch cache Normalize created_at values returned from Postgres to ISO strings (handle Date objects and other types) in PostgresSessionProvider and add a unit test for this behavior. In Think, replace an upsert on session update events with a patch-only _patchCachedMessage implementation so updateMessage no longer inserts messages that are missing from the live cache; add a test helper and a test to ensure missing messages are not appended. These changes prevent Date objects from leaking into API fields and stop update events from creating unexpected cached entries. --------- Co-authored-by: Matt <matt@test.com> Co-authored-by: Sunil Pai <spai@cloudflare.com> Co-authored-by: Cursor <cursoragent@cursor.com>github.com-cloudflare-agents · d151e6d6 · 2026-05-19
- 2.5ETVfix(chat): reconcile errored reconnect streams (#1963)github.com-cloudflare-agents · 3ce98ff0 · 2026-07-21
- 1.6ETVfix(mcp): SSE resumability for McpAgent + correct keepalive for stateless WorkerTransport (#1583) (#1602) * fix(mcp): default-enable SSE resumability for McpAgent (#1583) McpAgent.serve() opens an SSE stream from the Worker to the client that the Cloudflare edge closes after ~5 minutes of inactivity. The existing workaround was to write keepalive pings, which would have prevented the Durable Object from hibernating and contradict the serverless model. This switches McpAgent to the spec-sanctioned recovery path instead: - New `DurableObjectEventStore` that backs the MCP SDK's `EventStore` interface with the agent's own DO storage. Bounded at 256 events per stream (configurable), namespaced under `__mcp_event__:` to avoid user-state collisions, and rehydrates its sequence counter from storage after hibernation so reconnects never issue duplicate IDs. - `McpAgent` exposes a `getEventStore()` hook (override to disable or swap) and `initTransport()` wires the result into `StreamableHTTPServerTransport`. Resumability is now on by default. - `StreamableHTTPServerTransport.handleGetRequest` now tags the reconnected connection as the standalone SSE stream *before* replay. Previously the early `return` after `replayEvents` meant a resumed stream received only the backlog and then sat idle — server-initiated notifications had no connection to land on. Per the 2025-03-26 spec the server replays missed messages on the disconnected stream *and* continues delivering subsequent messages on the same stream. Verified end-to-end against a deployed worker: idle GET still drops at ~270s (DO is free to hibernate), and a follow-up GET with Last-Event-ID returns 200 with no spurious `event: ping` frames. * fix(mcp): correct SSE keepalive on POST response streams (#1583) The MCP transports had a `event: ping\ndata: \n\n` keepalive frame that the SSE parser dispatched as a `MessageEvent` with `type="ping"` and empty data, firing any `addEventListener("ping", ...)` listener the client had registered. Per the WHATWG SSE spec the keepalive form is the comment frame `: ...\n\n`, which the parser drops outright ("If the line starts with a U+003A COLON character (:), ignore the line"). Beyond the frame format, the previous code armed a keepalive on every SSE response stream. That made sense for POST tool-call responses (scoped to a single request id, cannot be resumed), but for the standalone GET listen stream it forced the server to stay alive indefinitely instead of letting the edge close idle connections and relying on `Last-Event-ID` resumption \u2014 which `McpAgent` now defaults to via `DurableObjectEventStore`. This commit splits the policy by stream direction: - GET (standalone listen stream): never keepalive. Idle drops are recovered by clients reconnecting with `Last-Event-ID` against the configured `EventStore`. `McpAgent` ships one by default; `WorkerTransport` callers bring their own. - POST (tool response stream): always keepalive. The transport writes `: keepalive\n\n` every 25s so long-running tool calls survive the ~5min Cloudflare edge idle-stream watchdog. The two transport implementations (`utils.ts` for `McpAgent`, `worker-transport.ts` for `createMcpHandler`) now share the keepalive helper in `sse-keepalive.ts`. * refactor(mcp): share cleanup closure across GET stream remap (#1583) In the GET handler, the standalone SSE stream may be re-mapped under a different streamId after the eventStore replays missed events. Both the initial `streamMapping.set` and the remapped one previously inlined identical cleanup closures, which made it easy to drift if teardown later grew a new step (e.g. tearing down a keepalive). Hoist the cleanup into a single `const cleanup` closure used by both mappings. The closure reads `streamId` lazily so it stays correct after the remap rebinding. Behaviour unchanged; the invariant that every mapping shares the same teardown is now structural rather than copy-paste. Addresses devin-ai-integration review feedback on PR #1602. * chore(mcp): mark DurableObjectEventStore as internal (#1583) The default event store is an implementation detail of McpAgent.getEventStore() and not part of the public API. Drop the public re-export from agents/mcp, mark the class @internal, and remove the recommendation to construct it directly from the WorkerTransportOptions.eventStore JSDoc. Callers who want resumability on a WorkerTransport bring their own EventStore implementation. * fix(mcp): address Kate's review concerns on PR #1602 Three fixes following review on cloudflare/agents PR #1602: **1. POST stream resumption (transport.ts) — Kate concern 3.** The previous implementation stored POST events but couldn't actually resume them: events were keyed by the POST WS connection.id, and the send() routing looked up connections by their state.requestIds. When the POST WS died, both were lost; a reconnecting GET created a fresh WS with a fresh connection.id and no requestIds. Replay reached the new connection but in-flight tool messages never could. Fix: introduce a stable streamId on the connection state (seeded with connection.id for fresh POSTs), persist a streamId -> requestIds mapping in DO storage, and on resumed GET-with-Last-Event-ID restore those requestIds onto the new WS. Subsequent send() calls keyed by requestId now find the resumed connection. New McpAgent helpers (set/get/deleteStreamRequestIds) wrap the persistence so the transport doesn't reach into protected ctx.storage directly. **2. Bounded event-store storage (event-store.ts, index.ts) — Kate concern 1.** The cap was per-stream, so busy sessions with many short-lived POST streams under the cap could accumulate events forever in DO storage. Fix: clear streams cleanly on the final POST response (eventStore clearStream + deleteStreamRequestIds), and add a TTL safety net for the unhappy path. DurableObjectEventStore now wraps stored values with a write timestamp and exposes sweep(maxAgeMs). McpAgent schedules a recurring sweep via the existing Agent scheduler (default cron */5 minutes, default 1 hr TTL), gated on idempotent: true so duplicate schedules don't pile up across hibernation/restart. getEventStoreMaxAgeMs() and getEventStoreSweepCron() expose both knobs. **3. WorkerTransport GET keepalive when no eventStore (worker-transport.ts) — Kate concern 2.** Previous behaviour kept GET streams alive via setInterval. New code relied entirely on resumability, which is a regression for callers who don't configure an eventStore. Restore the policy: keepalive when no eventStore (preserve pre-fix behaviour), skip keepalive when one is configured (resumability is the recovery path). Keeps this PR a true patch with no behaviour regression. **4. Re-export DurableObjectEventStore + wire into elicitation example.** The elicitation example demonstrates the stateful createMcpHandler pattern (Agent-hosted WorkerTransport with persistent storage). It needs a concrete EventStore to opt into resumability without writing its own. Removed @internal, exported from agents/mcp, added `eventStore: new DurableObjectEventStore(this.ctx.storage)` to the example. **5. Fixed stale JSDoc on WorkerTransportOptions.eventStore** that claimed POST streams can't be resumed. They can. Tests: 434/434 pass. Added 4 new tests covering sweep behaviour (deletes old events, no-op on bad input, respects batchSize, restarts seq after wiping a stream). * tune(mcp): default event-store sweep to hourly with 24h TTL (#1583) Five-minute sweeps with a one-hour TTL were too aggressive \u2014 cron tick cost on a busy DO and a tight window for client reconnects. Move to hourly sweeps (cron `0 * * * *`) and a 24-hour event TTL so abandoned streams still get garbage-collected but clients have all day to reconnect with `Last-Event-ID`. Knobs (`getEventStoreSweepCron`, `getEventStoreMaxAgeMs`) unchanged \u2014 override either to tighten or relax. --------- Co-authored-by: Matt Carey <matt@cloudflare.com>github.com-cloudflare-agents · cfc75bc9 · 2026-05-28
- 1.2ETVfix(agent-think): harden runs and simplify workspace lifecycle (#1889) * fix(agent-think): surface terminal turn failures * fix(agent-think): configure operating prompt as context * fix(agent-think): scope lifecycle to active runs * fix(agent-think): keep observer reporting non-blocking * fix(agent-think): retain recovery trace identity * test(agent-think): align target and terminal contracts * fix(agent-think): preserve evicted media in durable vfs * refactor(agent-think): unify workspace filesystem * refactor(agent-think): simplify workspace and pool * chore(agent-think): drop full-sync preview dependencygithub.com-cloudflare-agents · a43df13b · 2026-07-08
- 1.1ETVfix(mcp): configure client elicitation handler (#1911) * fix(mcp): configure client elicitation handler * fix(mcp): use grouped elicitation handlers * fix(test): url-mode elicit request literal requires elicitationId * docs(mcp): note why configureElicitationHandler rebuilds the client Client.registerCapabilities is merge-only, so a rebuild is the only way to un-advertise a mode when handlers are cleared before connecting. * persist advertised MCP capabilities so restore precedes fiber recovery Handlers are functions and cannot survive hibernation, but the capabilities they advertise can: configuring handlers stamps them onto each stored server row (server_options.capabilities), and the manager consumes the stamp as a capability seed when recreating a known server. Restored connections re-advertise the same modes at the handshake before onStart reconfigures the handlers, so automatic MCP restore keeps its original position before fiber/chat recovery and recovered turns see MCP tools. The stamp is valid for one restore: any configure call re-stamps every row, so a deploy that stops configuring handlers stops advertising stale modes after a single wake. init() re-entry rebuilds the SDK client so reconnects pick up handler changes. Seeding is self-contained in createConnection, so storage restore, the Agent RPC restore, and addMcpServer re-adds all get it uniformly.github.com-cloudflare-agents · 0f47d61c · 2026-07-09
- 1.0ETVagent-think: Isolate and recover Workspace sync (#1939) * agent-think: Cover repository sync recovery Exercise a local dependency install through the production container path and keep a visible running window after the durable command result. The test proves recovery can finish without replaying the command. * agent-think: Isolate workspace durable state * Recover exact Durable Object storage resets * agent-think: observe durable tool completion in E2E * agent-think: Pin Workspace PR buildgithub.com-cloudflare-agents · 1f321314 · 2026-07-14
- 0.9ETVfix(mcp): avoid repeated tool schema materialization (#1959) * fix(mcp): avoid repeated tool schema materialization * fix(mcp): bound AI tool schema cache * docs(think): clarify MCP tool exposure pathsgithub.com-cloudflare-agents · a3cbed1d · 2026-07-21
- 0.9ETVfeat(codemode): add browser iframe executor (#1468) * feat(codemode): add browser iframe executor Add IframeSandboxExecutor, a browser-native executor that runs LLM-generated code in a sandboxed iframe using postMessage for tool dispatch. Available via @cloudflare/codemode/browser. Also adds createBrowserCodeTool(), a zero-dependency equivalent of createCodeTool that accepts JSON Schema tools (object or array) and returns a plain tool descriptor with inputSchema, outputSchema, and execute. No ai or zod peer deps required. The iframe uses sandbox="allow-scripts" with a restrictive CSP. Tool calls flow through postMessage; the iframe is torn down after each execution. dist/browser.js: 11.3 KB raw, 3.77 KB gzip. Zero new dependencies. Closes #1111 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(codemode): align browser executor behavior * test(codemode): cover browser executor provider paths * docs(codemode): add browser webmcp client example * docs(codemode): keep browser example client-tool agnostic * docs(codemode): align browser example with ai chat tools * test(codemode): update browser vitest provider config * example(codemode): run browser iframe executor * example(codemode): allow choosing executor * Revert "example(codemode): allow choosing executor" This reverts commit 57a40acc5a64a18ce2dcdaf580b38c9b8dd38ce6. * Revert "example(codemode): run browser iframe executor" This reverts commit 50ba67c9f1f6bd2244ecc21d7f00732ca490125a. * example(codemode): add browser iframe example * example(codemode-browser): polish UI * example(codemode-browser): match footer layout * changeset: add codemode browser patch * fix(codemode): pass repo checks * fix(lockfile): include rolldown wasm deps --------- Co-authored-by: Alex Nahas <alexmnahas@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>github.com-cloudflare-agents · 186a2a45 · 2026-05-06
- 0.9ETV[Agents] Add Code Mode documentation (#31627) * [Agents] Add Code Mode documentation * [Agents] Clarify when to use Code Mode * [Agents] Refine direct tool guidance * [Agents] Clarify tool invocation patterns * [Agents] Reorganize Code Mode MCP guidance * [Agents] Remove unpublished Code Mode redirects * [Agents] Separate tool interface from execution location * [Agents] Clarify Code Mode description placeholders * [Agents] Move Code Mode MCP concept * [Agents] Add OpenAPI Code Mode connector guide * [Agents] Improve Code Mode integration wording * [Agents] Tighten Code Mode wording * [Agents] Fix Code Mode changelog links * [Agents] Clarify openApiMcpServer description handling * [Agents] Fix Code Mode example correctness from review * [Agents] Clarify MCP connector setup prerequisites * [Agents] Use plain connector imports and 60s executor defaultgithub.com-cloudflare-cloudflare-docs · d12be741 · 2026-06-24
- 0.9ETVfix(mcp): harden connection recovery (#1924) Honor retry budgets when connectToServer returns a failed result, drain old-id connection work before stable-id migration, and close connections replaced by the legacy connect path.github.com-cloudflare-agents · c19d58ae · 2026-07-22
- 0.8ETVtest: cover recovery after forced Durable Object eviction (#1820) * test(agents-core): cover DO eviction with evictDurableObject * test(agents-mcp): cover DO eviction rehydration with evictDurableObject * test(agents-memory): cover Session DO eviction with evictDurableObject * test(think): cover DO eviction and chat-recovery rehydration with evictDurableObject * test(codemode): cover DO eviction with evictDurableObject and evictAllDurableObjects * test(ai-chat): cover DO eviction with evictDurableObject * test(voice): cover DO eviction with evictDurableObject for conversation history rehydration * test(shell): cover Workspace DO eviction with evictDurableObject/evictAllDurableObjects * test: tighten forced eviction recovery coveragegithub.com-cloudflare-agents · e9067603 · 2026-07-17
- 0.8ETVfeat(mcp): MCP conformance test suites + two OAuth client fixes (#1722) * feat(mcp): add MCP conformance test suites and fix OAuth transport reuse bugs Runs the official @modelcontextprotocol/conformance suite (as used by the MCP TypeScript SDK) against the MCP client (Agent + MCPClientManager), McpAgent, and createMcpHandler + WorkerTransport - all hosted in workerd via wrangler dev so the implementations are tested as they run in production. The client suite found two real OAuth bugs in MCPClientConnection, fixed here: - finishAuth ran on a freshly created transport, losing the resource metadata URL captured from the WWW-Authenticate header, so token exchange fell back to the default /token endpoint - reconnecting after a mid-session 401 (scope step-up, token revocation) failed permanently with 'Already connected to a transport' Closes #1721 * fix(conformance): run server scenarios sequentially and baseline server-sse-polling --suite runs scenarios in parallel against the worker, which makes the timing-sensitive server-sse-polling scenario record different results on fast vs slow machines (zero checks locally, SHOULD-level warnings on CI). Sequential single-scenario runs are deterministic. server-sse-polling is baselined: McpAgent's internal transport does not implement SEP-1699 priming events or the retry field (tracked in #1723), and the harness probes with protocol 2025-03-26 for which the pinned SDK deliberately suppresses priming on the WorkerTransport variant. * fix(conformance): tolerate slow CI runners in client suite sse-retry flaked on CI: connection retry backoff plus the SDK's 5s SSE retry interval can exceed the worker's 25s waitForReady and the conformance CLI's default 30s scenario timeout on cold runners. Raise both (50s / 90s). --------- Co-authored-by: Matt Carey <matt@cloudflare.com>github.com-cloudflare-agents · 9f8e14b0 · 2026-06-17
- 0.7ETVfix(mcp): replay every event after POST stream interruption (#1583 follow-up) (#1607) * fix(mcp): minimal alarm-driven cleanup for SSE event store (#1583 followup) Reworking the resumability cleanup after a series of review rounds. Net result: -233 lines from the previous followup, no functional regression on the actual fixes. The Important Fixes (unchanged from earlier rounds) 1. Drop clearStream-on-shouldClose. Final tool response stays in the event store so a client that lost the connection right as it was in flight can reconnect with Last-Event-ID and recover it. 2. Unconditional storage on the request-bound send() path, matching the SDK. Falls back to McpAgent.getStreamIdForRequestId (a reverse lookup against __mcp_stream_reqs__:*) when the originating WS has dropped, so events are still recorded for replay. 3. Resumed GET registers under the source streamId, matching the SDK exactly. Active POST resumption inherits requestIds; standalone resumption takes over the listen role; completed POST resumption is a one-shot replay channel. The Storage Bounding (simplified) Earlier rounds layered cap-based eviction + a complex per-event-or- per-stream timestamped sweep + a recurring cron. After working through it: - Per-stream cap removed. Real streams never approach 256 events, and truncating replay history silently was arguably worse than letting the stream grow. DO storage is bounded structurally anyway. - Sweep is alarm-driven, not cron. storeEvent fires onStoreEvent; McpAgent uses it to idempotent-schedule a single cleanup at now + maxAgeMs. The callback sweeps streams whose lastWriteAt is past cutoff, then either reschedules at the next-earliest expiry or doesn't reschedule at all. Quiescent DOs do no periodic work. - Sweep cost is O(active streams), not O(total events): a per-stream metadata key (__mcp_stream_evt_meta__) tracks lastWriteAt, the sweep scans only that index and bulk-deletes events for streams whose meta is expired. - Stream-reqs cleanup folded into the same sweep. When sweep deletes a stream, the agent also deletes the matching __mcp_stream_reqs__ entry. No separate sweepStreamReqs, no timestamp on reqs entries. - All the knobs removed: getEventStoreSweepCron, getEventStoreSweepMaxIterations, batchSize, the cursor-pagination question. Just getEventStoreMaxAgeMs (default 24h). DurableObjectEventStore is exported so callers embedding WorkerTransport inside an Agent (e.g. the elicitation example) can opt into resumability with new DurableObjectEventStore(this.ctx.storage). Tests: 455/455 MCP tests pass. npm run check exit 0 across all 88 projects. event-store.ts is now 198 lines (down from 323); McpAgent additions for cleanup are ~50 lines instead of ~200. * fix(mcp): clear stream on close + address clanker review Rip the alarm-driven sweep added in the previous push of this PR. Storage cost is now bounded by in-flight POST streams (cleared the moment their close frame is written) plus the standalone GET stream. - event-store: drop sweep(), drop the __mcp_stream_evt_meta__ index, drop the onStoreEvent option. Assert streamId contains no ':' to rule out prefix-scan collisions. Chunk multi-key deletes at the DO 128-key cap. Cap replayEventsAfter at 1000 events to bound memory. - mcp/index: drop _cf_armEventStoreCleanup, _cf_runEventStoreCleanup, and getEventStoreMaxAgeMs. getEventStore() is now plain 'new DurableObjectEventStore(this.ctx.storage)'. - transport: writeSSEEvent FIRST, then deleteStreamRequestIds + clearStream. Trade-off: if the WS pipe is enqueued but the client TCP dies before the bytes arrive, that one final message is lost. Every earlier event is still replayable while the stream is open. - transport: when a GET resumes an active POST stream, strip requestIds off any other connection still claiming them, so stale POST bridges can't win the routing race against the resumed GET. - transport: fan standalone notifications out to every _standaloneSse connection instead of last-writer-wins. Matches the spec. - tests: replace the completed-POST 'replay' test with a real mid-flight POST disconnect test using a new deferredGreet tool that emits a progress notification then sleeps. Drop the misnamed 'resumed GET is registered as standalone' test (it was actually a POST-id resume). Drop event-store sweep tests; add chunked-delete and replay-cap tests. * refactor(mcp): clean up transport.ts per review Four structural cleanups, no behaviour change. All 453 mcp tests still pass. - Add ClearableEventStore interface (extends EventStore with clearStream). Replaces an inline cast + optional chaining dance with a real named contract and proper 'in' narrowing. - Supersede stale POST bridges by closing them (conn.close(1000, 'Superseded by resumed stream')) instead of reaching in and mutating their state. Matches the SDK's last-writer-wins _streamMapping mental model. Removes a 'spooky action at a distance' pattern. - Split send() into sendStandalone() and sendForRequest(). The two paths share nothing but the storeEvent call and were fused for no good reason. Removes the disjoint shouldClose-cleanup-after-write block as a side effect. - Drop the now-unused 'eventStore' getter on the transport. Its only caller was _cf_runEventStoreCleanup which was deleted in the previous commit. * refactor(mcp): act on bonk review Three legit findings, two no-ops: - clearStream: replace 'list-all-then-chunk-delete' with a paginated list+delete loop bounded by DELETE_CHUNK. Removes the deleteChunked helper as a side effect \u2014 the chunk size is now enforced by the list limit itself, not a separate pass. Net simpler. - Clearer error message in sendForRequest: 'No active stream found' instead of 'No connection established', since the failure is a missing stream mapping, not a missing connection. - resumability test: assert the progress notification is NOT re-delivered on the resumed GET. The Last-Event-ID *was* the progress event, so replayEventsAfter must skip it. Not acting on: - O(n) full scan in getStreamIdForRequestId: working set is in-flight POST streams per DO, which is small. - clearStream/replayEventsAfter race: window is between two awaits in the same microtask chain and the consequence (replaying about-to-be- deleted events) is harmless. - Double storage read in disconnected-client path: only on the slow path; threading data through would complicate the API. - Standalone GET events accumulate: already documented; bounded by session lifetime by design. * refactor(mcp): act on bonk pass 2 Five small wins from the second-pass review: - Replace the 'clearStream' in this._eventStore duck-type check with a proper isClearableEventStore type guard. Same runtime check, better readability, narrows the type through the inferred call. - Add an ordering note on the deleteStreamRequestIds -> clearStream sequence: the tiny window where a concurrent GET resume sees no requestIds and starts replaying soon-to-be-deleted events is benign, but the comment saves future readers from re-deriving it. - getStreamIdForRequestId: add an O(n) cost comment and a defensive limit: 1000 on the list call so an abandoned-POST leak can't unbounded-load the scan. - Standalone fan-out: wrap each writeSSEEvent in try/catch so one dead WS in the middle of the loop can't block delivery to the remaining standalone connections. - Mid-flight resumability test: assert postBuf does NOT contain '"result"' at the point of cancel, so a timing regression turns into a clear failure rather than a false pass. - Comment on the supersede-by-close loop noting the last-writer-wins semantic for rapid back-to-back GET resumes on the same stream. Skipped: - Collapsing the double storage read on the disconnected path (getStreamIdForRequestId + getStreamRequestIds): would require widening the agent API to return both in one pass. Slow-path-only, working set is single-digit streams. Not worth the API change. * refactor(mcp): act on bonk pass 3 Six fixes: - Collapse the double storage read on the disconnected-client send path. getStreamIdForRequestId is now getStreamForRequestId and returns { streamId, requestIds } in one pass; sendForRequest no longer issues a second getStreamRequestIds for the same key. - Re-export ClearableEventStore from mcp/index.ts so embedders who want to implement their own clearable store can import the type. - Fix the replayEventsAfter off-by-one: use start: <key>+'\x00' so the list result strictly excludes lastEventId rather than including it and post-filtering. Effective cap is now exactly 1000, not 999. Drops the (key <= lastKey) guard as a side effect. - Tighten the replay-cap test from <=1000 to ==1000 \u2014 catches the off-by-one if anyone reverts. - Comment the no-live-connection branch in sendForRequest: the close frame is intentionally dropped when there's nowhere to write it. The client's reconnect with Last-Event-ID gets the final event (until cleanup runs immediately after) and the spec lets them treat it as final. - Log a warning if getStreamForRequestId hits its 1000-key scan cap. Hitting it means abandoned __mcp_stream_reqs__ entries are accumulating, which would silently turn into 'No active stream found' errors otherwise. * fix(mcp): repair botched merge of #1607 with main The merge of main into this branch left two fused copies of sendForRequest \u2014 main's collision-routing version and this PR's no-live-connection fallback version \u2014 plus dangling references to the old _requestResponseMap (renamed to _streamResponseIds in main). The file built but the logic was incoherent. This commit restructures sendForRequest so both intents coexist cleanly: 1. Pick the live connection using main's collision-safe rule: prefer the originating connection; fall back to the unique matching connection; otherwise null. 2. If multiple live connections claim the request id and none is the originating, route an Internal Error to each \u2014 main's #1639 protocol-safety behaviour. 3. If a single live connection owns the request, delegate to sendOnStream (main's canonical store + close-detect + cleanup path). 4. Otherwise fall back to this PR's getStreamForRequestId reverse lookup so the event is still stored for replay when the originating WS has dropped, and run cleanup on shouldClose. The close frame is intentionally dropped when there's no live connection \u2014 documented trade-off in send()'s docstring. No behaviour change for the live-connection path (that's main's sendOnStream verbatim). The dropped-WS replay path now exists again, which is what this PR was originally about. * refactor(mcp): collapse duplicated send path The previous commit's repair of the botched merge left sendOnStream and sendForRequest's no-live-connection fallback as two parallel implementations of the same logical function. They had already drifted in this branch (live path used a cast, fallback used the isClearableEventStore type guard), and a 'void eventId;' silencing betrayed the fact that the structure wasn't right. sendOnStream now takes (streamId, relatedIds, liveConnection, message, requestId) directly. The caller resolves where the message goes; sendOnStream handles store + close-detect + cleanup + write. writeSSEEvent is gated on liveConnection being non-null \u2014 same behaviour, expressed as one branch instead of two. sendForRequest is now four phases readable top-to-bottom: 1. find matching live connections 2. ambiguous-multi \u2192 error route to each 3. resolve streamId + relatedIds (from connection state, or persisted fallback if the WS dropped) 4. one sendOnStream call No behaviour change. 457/457 mcp tests still pass. Net -10 lines, two cleanup paths collapsed to one, type-guard drift fixed, 'void eventId;' smell removed. * fix(mcp): write SSE frame before clearStream (bonk #24) In the collapse-the-duplicate refactor (6b5990cc), the cleanup sequence in sendOnStream ended up running BEFORE writeSSEEvent \u2014 the exact ordering the docstring and changeset warn against, and the exact bug Sunil's clanker review and the original #1583 ticket are about. Failure mode: tool finishes, sendOnStream is called with a still-live liveConnection (e.g. resumed GET), storeEvent writes the final event, shouldClose=true triggers clearStream which wipes the entire stream INCLUDING the event we just stored, then writeSSEEvent emits a frame with an event id that no longer resolves. A client losing the WS pipe at that exact moment can reconnect with Last-Event-ID and find nothing to replay. Fix: write the SSE frame first, then run cleanup. Matches the docstring already in place. Also fixes bonk #25: changeset referenced McpAgent.getStreamIdForRequestId; renamed to getStreamForRequestId in 880c0130. * fix(mcp): sendOnStream relatedIds is readonly Connection state arrays are ImmutableArray<RequestId>; sendOnStream only reads via .every() so the parameter type can accept readonly RequestId[]. Caught by CI typecheck. * refactor(mcp): act on bonk pass + trim comment noise - Wrap supersede-by-close in try/catch so a dead WS can't abort the loop (bonk). - Trim verbose comments in sendOnStream + send dispatcher. No PR / ticket references in code. * fix(mcp): try/catch around writeSSEEvent so a dead WS doesn't orphan cleanup If connection.send throws inside writeSSEEvent (WS torn down between iteration and write), the exception would skip the cleanup block below and leave stream-reqs + stored events permanently orphaned. Also document standalone events accumulating for the DO lifetime in the event-store class docstring. * refactor(mcp): drop cargo-culted try/catch around close() Reverting the try/catch I added around the supersede-by-close loop. Closing a WS in the Workers runtime is fire-and-forget and doesn't throw — no other close() call in the codebase is wrapped (including the transport's own close() method). Added it reactively to a review question; the answer was 'no, it doesn't throw'. Kept the two writeSSEEvent wraps: those guard connection.send (which the codebase consistently wraps) — one protects the cleanup block from being skipped, the other keeps one dead WS from aborting standalone fan-out to the rest. * test(mcp): cover standalone fan-out + resume supersession Two behavioural changes in this PR had no direct coverage: - sendStandalone fans out to ALL _standaloneSse connections (was last-writer-wins). Two new tests: fan-out reaches every standalone stream and skips POST bridges; one throwing send doesn't block the rest of the loop. - handleGetRequest closes a stale POST connection when a GET resumes its stream. New test asserts the stale bridge is close()d with 1000/'Superseded by resumed stream', the resuming conn isn't closed, and it claims the persisted requestIds. Mutation-checked: commenting out the supersede close() fails the new test. 460/460 mcp tests pass. * fix(mcp): standalone send goes to ONE stream, not fanned out (spec) MCP 2025-06-18 'Multiple Connections': the server MUST send each JSON-RPC message on only one of the connected streams and MUST NOT broadcast the same message across multiple streams. The prior commit fanned standalone notifications out to every _standaloneSse connection — a direct violation. (The SDK reference enforces the stronger 'only one standalone GET per session' with a 409.) Fix: - handleGetRequest now supersedes prior connections for the resumed streamId in ALL resumable cases (POST resume, standalone resume, and fresh standalone GET) via a shared supersedePriorStreamConnections helper. At most one live connection per stream. - sendStandalone reverts to single-send: find the one standalone connection and write to it, else store-only for replay. No more broadcast loop. - Tests updated: 'sends on exactly one standalone stream', 'stores but does not write when none live', and a new 'supersedes a prior standalone GET when a fresh GET opens'. 461/461 mcp tests pass. (5 unrelated typecheck failures in ai-chat/think predate this branch — from main's recovery work.) * test(mcp): lock global event-id uniqueness across streams (spec) MCP resumability rule: the SSE event id MUST be globally unique across all streams within a session. We had monotonic-within-stream and ignore-other-streams coverage but nothing pinning cross-stream uniqueness. A future move to a global seq counter could silently break it. New test stores the same seq across three streams and asserts no id collides. Mutation-checked: dropping streamId from the id format fails it. * docs(mcp): sync changeset with shipped behaviour - 'stripped requestIds' -> stale connections are closed (supersede). - Replace the fan-out bullet (reverted) with the one-stream-per-message spec rule. - Note standalone GET events accumulate for the DO lifetime. * docs(mcp): fix stale 'lastKey' reference in replayEventsAfter comment The variable was renamed to startKey (built inline); the comment still said 'appending \x00 to lastKey'. No code change. --------- Co-authored-by: Matt Carey <matt@cloudflare.com>github.com-cloudflare-agents · f82d8978 · 2026-06-01
- 0.6ETVfeat(mcp): allow declaring url-mode elicitation capability and handling elicitation on the MCP client (#1903) * fix(mcp): support url-mode elicitation with a handler on the MCP client The client hardcoded capabilities.elicitation = {} (form-mode only), clobbering anything the caller passed, and shipped a throwing-stub handleElicitationRequest with no injection point. Servers only send elicitation modes advertised at the initialize handshake, so url-mode elicitation (MCP spec 2025-11-25) was unreachable for client agents. - New overridable Agent.onElicitRequest(request, serverId). A class method survives Durable Object hibernation and applies to restored connections, unlike a callback passed at connect time (which is why the handler is not a per-call addMcpServer option). Plumbed through MCPClientManagerOptions.elicitationHandler (scoped per connection, including the restore and id-migration paths) and a per-connection elicitationHandler option for non-Agent usage. - Capabilities follow the handler: connections advertise { form: {}, url: {} } when a handler is configured and the legacy form-only {} otherwise, so a capability is never claimed that nothing can handle. Agent wires the handler only when onElicitRequest is actually overridden. An explicit client.capabilities.elicitation wins wholesale, is persisted, and survives hibernation. - Direct handleElicitationRequest override is deprecated in favor of the elicitationHandler option. Closes #1875 * test(conformance): enable the SEP-1034 client elicitation defaults scenario ConformanceHost now overrides onElicitRequest (accepting) and declares form.applyDefaults + url, so the SDK client applies schema defaults to accepted form elicitations. Removes the corresponding expected-failure baseline entry. * docs(examples): demonstrate MCP client elicitation in the mcp-client example The agent overrides onElicitRequest to broadcast the elicitation to connected browser clients and await a human answer through a pending- response map resolved by a callable respondToElicitation. The UI renders a form generated from the request's requestedSchema (url-mode renders an open-link card) and adds a Run button per tool so elicitation-triggering tools can actually be exercised. * docs(examples): add a url-mode elicitation tool to the mcp-elicitation server * docs(examples): document the /mcp endpoint path for the elicitation demo * fix(examples): tool args form and always-visible elicitation overlay in mcp-client Run previously sent empty args (failing tools with required inputs) and pending elicitations rendered at the top of the page, off-screen from the Run button that triggered them — the tool call looked hung. Tools now get an args form generated from their inputSchema (shared SchemaFields renderer with the elicitation card) and elicitations render in a fixed overlay. * chore: format client.tsxgithub.com-cloudflare-agents · 3ba6a78c · 2026-07-09
- 0.6ETVfeat(mcp): support caller-supplied stable ids in addMcpServer (#1596) * feat(mcp): support caller-supplied stable ids in addMcpServer Both addMcpServer overloads (HTTP and RPC) now accept an optional `id` on their options object. When provided, the supplied id replaces the generated nanoid(8) for storage, restore, listServers, listTools, getAITools tool-name namespacing, and OAuth state. The id is normalized via the new exported `normalizeServerId` helper so values like "GitHub MCP!" become "github-mcp", guaranteeing the id is safe for AI SDK tool names (/^[A-Za-z0-9_]+$/ after hyphen strip) and for use as a storage primary key. Collisions against an existing server with a different (name, url) now throw instead of silently overwriting the row. Closes #1564 * fix(mcp): reject stable id when same (name,url) already has a different id Addresses review feedback on #1596: 1. Dedup path could silently return an existing auto-generated nanoid when the caller now supplied { id: "..." }, violating the contract that the returned id matches the requested one. 2. RPC path let requestedId win over the existing nanoid, but INSERT OR REPLACE on the new id left the old row in storage, causing duplicate restores after hibernation. Both have the same root cause \u2014 we only checked id\u2192(name,url) collisions, not (name,url)\u2192id. Now throw early in that case with a clear migration hint pointing at removeMcpServer(oldId). * feat(mcp): JIT-migrate auto-generated ids to stable ids instead of throwing Replaces the previous throw-on-collision with a transparent migration so adopting stable ids is fully additive \u2014 no user code breaks. When the same (name, url) is already registered under a different id (typically an auto-generated nanoid from a previous call without { id }), addMcpServer now calls MCPClientManager.migrateServerId(oldId, newId), which atomically: - renames the cf_agents_mcp_servers row - renames the in-memory mcpConnections key - renames the connection-disposables map key - updates authProvider.serverId on the live connection - moves OAuth-related DO storage keys from /{clientName}/{oldId}/... to /{clientName}/{newId}/... The id\u2192(name,url) collision check is still strict: if the supplied stable id already belongs to a different server, addMcpServer throws. Tests: - RPC: testRpcSuppliedIdMigratesExistingNanoid \u2014 round-trips a tool call through the migrated id and asserts no stale storage row remains. - HTTP: testHttpSuppliedIdMigratesNanoid \u2014 seeds fake OAuth keys under the old prefix and asserts they\u2019re moved to the new prefix. * chore: fix changeset markdown formatting * chore: bump changeset to patchgithub.com-cloudflare-agents · 091cb0fa · 2026-05-28
- 0.6ETVfix: route RPC MCP responses by request id (#1558) * fix: serialize rpc mcp messages * fix: preserve rpc transport type narrowing * fix: route rpc mcp responses by request id * test: preserve rpc continuation scenario * fix: keep rpc mcp waits alive * docs: explain rpc mcp keepalivegithub.com-cloudflare-agents · 67ff1ba1 · 2026-05-27
- 0.6ETVrefactor(mcp): WorkerTransport extends SDK WebStandardStreamableHTTPServerTransport (#1701) * refactor(mcp): WorkerTransport extends SDK WebStandardStreamableHTTPServerTransport Replace the 941-line in-house WorkerTransport with a thin subclass of the official MCP SDK's WebStandardStreamableHTTPServerTransport. The wrapper layers Workers-specific concerns on top of the SDK transport without forking it: * CORS preflight handling and response-header injection (corsOptions). * Persistent transport state across DO hibernation via the existing MCPStorageApi adapter (sessionId, initialized, initializeParams are snapshotted after each request and replayed on cold start so client capabilities are restored without a fresh initialize round-trip). * SSE keepalive - preserves the issue #1583 fix (cfc75bc9). The wrapper wraps SSE responses in a TransformStream that injects the shared KEEPALIVE_FRAME (`: keepalive\n\n`) at KEEPALIVE_INTERVAL_MS (25s) from sse-keepalive.ts. Keepalive is unconditional on POST response streams (no recovery path during a mid-flight tool call) and disabled on the standalone GET stream when an eventStore is configured (clients recover idle drops via Last-Event-ID instead). Everything else (session validation, SSE streaming, protocol-version negotiation, event-store resumability, send/close lifecycle) is delegated to the SDK transport. Net: ~500 fewer lines of code we have to maintain. Public API is unchanged: WorkerTransport, WorkerTransportOptions, MCPStorageApi and TransportState all keep the same exported shape. Test changes are minimal: * Storage tests work unchanged - MCPStorageApi is preserved. * Whitebox routing tests now reference SDK private field names (_streamMapping, _requestToStreamMapping) and the SDK's {controller, encoder} stream shape instead of the old {writer, encoder, cleanup}. * One closed-stream test updated to assert silent drop (SDK behaviour) instead of throw (old behaviour) - a deliberate resilience choice the SDK makes for close-during-send races. * post-keepalive.test.ts (the #1583 regression suite) passes unchanged. * test(mcp): cover worker transport sdk regressions * fix(mcp): preserve storage-only stateless transport * docs(mcp): document worker transport behaviour changes; cover stateless reuse throw * refactor(mcp): keep storage orthogonal to statefulness; tidy changeset * fix(mcp): retry state restore after transient storage failure; address review * fix(mcp): persist state once on init via bridge; add override keywords; tidy handler --------- Co-authored-by: Matt Carey <matt@cloudflare.com>github.com-cloudflare-agents · 6caa6e85 · 2026-06-08