Skip to content

Conversation

@omeraplak
Copy link
Member

@omeraplak omeraplak commented Jan 7, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

What is the new behavior?

fixes (issue)

Notes for reviewers


Summary by cubic

Adds resumable streaming with @voltagent/resumable-streams so clients can reconnect to in‑flight chat streams. Includes a new resume endpoint, server adapter integration, and examples for Next.js and Hono.

  • New Features

    • New @voltagent/resumable-streams package (adapter, Redis/memory/VoltOps stores, session/helpers, chat handlers).
    • Core updates: ResumableStreamAdapter types, StreamTextOptions.resumableStream flag, server deps support.
    • Server: Hono/serverless integration with GET /agents/:id/chat/:conversationId/stream (auth‑protected) and handler updates.
    • Examples: Next.js + AI Elements chat with resumable streams, plain Hono demo, and a VoltOps‑backed example (no Redis).
  • Migration

    • Add @voltagent/resumable-streams and set REDIS_URL (or use VoltOps with VOLTAGENT_PUBLIC_KEY/SECRET_KEY).
    • Pass resumableStream.adapter to server config; optionally set resumableStreamDefault.
    • Start chat with options: { userId, conversationId, resumableStream: true }.
    • Resume via GET /agents/:id/chat/:conversationId/stream?userId=... (204 if none).
    • For Next.js, use after() and createResumableChatSession to manage stream lifecycle.

Written for commit e3999c3. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Opt-in resumable streaming for chat (resume endpoint using conversationId + userId), Redis/VoltOps-backed stores, and client resume-on-refresh flows.
    • New Next.js example with a resume-enabled chat UI and many reusable UI primitives/components for chat interfaces.
  • Documentation

    • Extensive resumable streaming guide, API reference, and example walkthroughs added to docs.
  • Chores

    • New resumable-streams package, example projects, and associated config/manifest files.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 7, 2026

🦋 Changeset detected

Latest commit: e3999c3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 5 packages
Name Type
@voltagent/resumable-streams Patch
@voltagent/serverless-hono Patch
@voltagent/server-core Patch
@voltagent/server-hono Patch
@voltagent/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Adds opt-in resumable streaming: new @voltagent/resumable-streams package (stores, adapter, session, handlers), core types/options, server routes/handlers to start and resume streams, Hono/Serverless/Next integrations, Redis/VoltOps/memory stores, a Next.js example with UI, and documentation.

Changes

Cohort / File(s) Summary
Core types & agent options
packages/core/src/types.ts, packages/core/src/agent/agent.ts, packages/server-core/src/utils/options.ts
New ResumableStream types and adapter interfaces; added resumableStream?: boolean to stream options and processed agent options.
Resumable streams package
packages/resumable-streams/*, packages/resumable-streams/package.json, packages/resumable-streams/tsup.config.ts, packages/resumable-streams/tsconfig.json
New package exposing stores (memory/Redis/generic/VoltOps), active-stream store, pub/sub, adapter creation/resolution, types, chat-session and chat-handler factories, and index exports.
Server-core handlers & API
packages/server-core/src/handlers/agent.handlers.ts, packages/server-core/src/routes/definitions.ts, packages/server-core/src/schemas/agent.schemas.ts, packages/server-core/src/auth/defaults.ts, packages/server-core/src/edge.ts
Integrated resumable lifecycle into chat stream handler, added handleResumeChatStream, route definition and schema updates, protected-route registration, and edge re-export.
Server integrations (Hono / Serverless / Next)
packages/server-hono/*, packages/serverless-hono/*, packages/server-hono/src/app-factory.ts, packages/serverless-hono/src/app-factory.ts, packages/server-hono/src/routes/agent.routes.ts, packages/server-hono/src/routes/index.ts, packages/server-hono/src/types.ts, packages/serverless-hono/src/types.ts
Resolve resumable deps in app factories, extend server config types to accept adapter + defaultEnabled, add resume route(s) with OpenAPI metadata, and wire resolved deps into routes.
Next.js example app
examples/with-nextjs-resumable-stream/**
New full Next.js example wiring resumable adapter, VoltAgent and shared memory, API endpoints (/api/chat, /api/chat/[id]/stream, /api/messages), many UI primitives and AI Elements, global styles/layout, utils, and package/config files.
Non-Next examples (Hono / VoltOps)
examples/with-resumable-streams/**, examples/with-voltops-resumable-streams/**
New Hono and VoltOps example apps demonstrating Redis- and VoltOps-backed resumable stream stores and server wiring.
UI primitives & AI Elements
examples/with-nextjs-resumable-stream/components/ui/*, examples/with-nextjs-resumable-stream/components/ai-elements/*, examples/with-nextjs-resumable-stream/components/*
Many new UI components and higher-level AI Elements (chat interface, prompt input, message/response, carousel, reasoning, branch, etc.) used by the Next.js example to expose resume UX.
Example helpers & utilities
examples/with-nextjs-resumable-stream/lib/resumable-stream.ts, examples/with-nextjs-resumable-stream/lib/utils.ts
Lazy initializer for a resumable adapter and a cn utility (clsx + twMerge) for example code.
Package manifests & build config
various package.json, tsconfig.json, next.config.ts, postcss.config.mjs
New package manifests for examples and @voltagent/resumable-streams, tsconfigs, tsup/next/postcss configs, and dependency additions (e.g., @voltagent/resumable-streams).
Observability, memory & VoltOps helpers
packages/server-core/src/handlers/observability.handlers.ts, packages/core/src/memory/manager/memory-manager.ts, packages/core/src/voltops/global-client.ts, packages/core/src/voltops/index.ts
Observability now reports resumable store info; memory manager applies operationId metadata to persisted messages; added getGlobalVoltOpsClient.
Auth & tests
packages/server-core/src/auth/defaults.spec.ts, packages/server-core/src/auth/defaults.ts
Added protected-route entry and test assertion for the resume endpoint.
Docs & changelog
website/docs/agents/resumable-streaming.md, website/sidebars.ts, .changeset/common-geese-fetch.md
New documentation page, sidebar entry, and changeset describing resumable streaming.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant API as Server (Next/Hono/Serverless)
  participant Adapter as ResumableAdapter
  participant Store as Stream Store (Redis/VoltOps/Memory)
  participant Agent

  rect rgba(220,237,200,0.6)
  Note over Client,API: Start resumable stream
  Client->>API: POST /api/chat (messages, conversationId, userId, resumableStream=true)
  API->>Adapter: createResumableChatSession(conversationId, userId, agentId?)
  Adapter->>Store: createStream -> returns streamId
  API->>Agent: agent.streamText(..., resumableStream=true)
  Agent-->>API: SSE/text chunks
  API->>Adapter: consumeSseStream(stream)
  Adapter->>Store: persist/publish chunks (streamId)
  API-->>Client: streaming HTTP response (SSE/text chunks)
  end

  rect rgba(200,225,245,0.6)
  Note over Client,API: Resume after reconnect
  Client->>API: GET /api/chat/{id}/stream?userId=...
  API->>Adapter: getActiveStreamId(conversationId, userId)
  Adapter->>Store: fetch streamId / resumeExistingStream(streamId)
  Store-->>Adapter: resumed ReadableStream
  Adapter-->>API: resumed stream (encoded)
  API-->>Client: resumed streaming HTTP response
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through streams both old and new, 🐰
Chunks tucked in Redis, waiting like dew.
Agents whisper lines, paused mid-run,
Refresh, reconnect — the resume is done.
A gentle hop, and streaming's back in view.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete and does not fulfill the repository's template requirements. All checklist items are unchecked (commit guidelines, linked issues, tests, docs, changesets), and the description lacks the required structured sections. Complete all checklist items. Verify commit message compliance, link related issues, add tests, update docs, and create changesets. Fill in the 'What is the current behavior?' and 'What is the new behavior?' sections properly.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add resumable streaming support via @voltagent/resumable-streams' clearly and specifically describes the primary feature being added—resumable streaming support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef0e91f and e3999c3.

📒 Files selected for processing (1)
  • examples/with-voltops-resumable-streams/package.json
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Cloudflare Pages

Comment @coderabbitai help to get the list of available commands and usage tips.

@joggrbot

This comment has been minimized.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 7, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: e3999c3
Status: ✅  Deploy successful!
Preview URL: https://c6aac102.voltagent.pages.dev
Branch Preview URL: https://feat-resumable-stream.voltagent.pages.dev

View logs

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

23 issues found across 105 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="examples/with-nextjs-resumable-stream/package.json">

<violation number="1" location="examples/with-nextjs-resumable-stream/package.json:2">
P2: Package name doesn't match directory. This file is in `with-nextjs-resumable-stream/` but the name references `ai-elements`. Should be `voltagent-example-with-nextjs-resumable-stream`.</violation>

<violation number="2" location="examples/with-nextjs-resumable-stream/package.json:62">
P2: Repository directory path is incorrect. Should be `examples/with-nextjs-resumable-stream` to match actual file location.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ui/carousel.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ui/carousel.tsx:101">
P2: Missing cleanup for 'reInit' event listener. The effect subscribes to both 'reInit' and 'select' events but only unsubscribes from 'select', causing a potential memory leak.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/lib/resumable-stream.ts">

<violation number="1" location="examples/with-nextjs-resumable-stream/lib/resumable-stream.ts:12">
P1: Rejected promise will be cached permanently. If Redis is temporarily unavailable during the first call, `adapterPromise` will hold a rejected promise, and all subsequent calls will fail forever (the rejected promise is truthy, so the `if (!adapterPromise)` check won't re-initialize). Consider clearing `adapterPromise` on rejection to allow recovery.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/web-preview.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/web-preview.tsx:139">
P2: Input will not update when `onChange` prop is passed without `value` prop. The `handleChange` function already forwards to the optional `onChange` callback, so it should always be used to ensure internal state stays in sync.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ui/tooltip.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ui/tooltip.tsx:45">
P2: Invalid Tailwind CSS syntax for CSS custom property. Tailwind uses square brackets with `var()` for arbitrary CSS variable values: `origin-[var(--radix-tooltip-content-transform-origin)]` instead of `origin-(--radix-tooltip-content-transform-origin)`.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/message.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/message.tsx:23">
P2: Invalid CSS class `is-user:dark` appears to be leftover or incomplete code. This class will have no effect. Either remove it, or if dark mode styling was intended, use the standard `dark:` variant instead.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/artifact.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/artifact.tsx:94">
P2: Button may lack accessible name when `icon` is provided without `label` or `tooltip`. Screen readers won't be able to announce this button's purpose. Consider requiring at least one of these props or adding a fallback accessible name.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/response.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/response.tsx:16">
P2: Incomplete memo comparison function ignores `className` and other props. If `className` or any spread prop changes but `children` stays the same, the component won't re-render, causing visual or behavioral bugs. Consider comparing all relevant props or using the default shallow comparison.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/app/api/chat/[id]/stream/route.ts">

<violation number="1" location="examples/with-nextjs-resumable-stream/app/api/chat/[id]/stream/route.ts:35">
P2: Returning 204 for all errors masks actual failures. HTTP 204 means "success with no content", not "error occurred". Consider returning 500 for unexpected errors, or use the existing `jsonError` helper to return a proper error response.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/sources.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/sources.tsx:44">
P2: Inconsistent `className` handling: unlike other components in this file that use `cn()` to merge classNames, `Source` hardcodes `className` and spreads props after, causing any passed `className` to override (not merge with) the base styles.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/app/globals.css">

<violation number="1" location="examples/with-nextjs-resumable-stream/app/globals.css:318">
P2: Color function mismatch: `hsl()` is used with a CSS variable that contains an `oklch()` value. The `--muted-foreground` variable is defined as `oklch(0.556 0 0)`, so `hsl(var(--muted-foreground) / 0.2)` produces invalid CSS. Use `oklch(from var(--muted-foreground) l c h / 0.2)` or `color-mix()` instead.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ui/input-group.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ui/input-group.tsx:73">
P2: Click handler only focuses `input` elements but won't work with `InputGroupTextarea`. Should query for both input and textarea elements to support all exported controls.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/shimmer.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/shimmer.tsx:9">
P2: The `as` prop type is too permissive. It accepts `ElementType` (any React element), but only 9 elements are actually supported. Unsupported elements silently fall back to `<p>`, which could cause unexpected rendering. Consider restricting the type to only supported elements.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ui/command.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ui/command.tsx:44">
P2: The `DialogHeader` with `DialogTitle` and `DialogDescription` should be placed inside `DialogContent`, not as a sibling. Since `DialogContent` wraps the Radix UI portal, these accessibility elements won't be properly associated with the dialog when placed outside. Screen readers won't correctly announce the dialog's accessible name and description.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/tool.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/tool.tsx:109">
P2: Falsy value check may hide valid tool outputs. If `output` is `0`, `false`, or `""`, the condition `!(output || errorText)` evaluates to true (when no errorText), causing the component to return null. Use nullish check `output == null` to allow falsy but valid outputs.</violation>
</file>

<file name="examples/with-resumable-streams/README.md">

<violation number="1" location="examples/with-resumable-streams/README.md:10">
P2: Incorrect package name in documentation. The package is `@voltagent/resumable-streams`, not `resumable-stream/redis`. This could confuse users trying to understand the dependencies.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/code-block.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/code-block.tsx:88">
P1: The mounted ref check is inverted, causing setState to run on unmounted components. When the async `highlightCode` completes after unmount, `!mounted.current` evaluates to `true` (since cleanup sets it to `false`), so state updates will still execute. This causes React warnings and potential memory leaks.</violation>

<violation number="2" location="examples/with-nextjs-resumable-stream/components/ai-elements/code-block.tsx:156">
P2: The setTimeout for resetting `isCopied` is not cleaned up on unmount. If the component unmounts before the timeout expires, it will attempt to set state on an unmounted component. Consider using a ref to track the timeout and clearing it in a cleanup effect, or using a custom hook like `useTimeout`.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/inline-citation.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/inline-citation.tsx:53">
P1: `new URL(sources[0])` will throw a `TypeError` if `sources[0]` is not a valid URL, crashing the component. Consider wrapping in a try-catch or validating the URL first.</violation>

<violation number="2" location="examples/with-nextjs-resumable-stream/components/ai-elements/inline-citation.tsx:146">
P1: Memory leak: The `api.on("select", ...)` event listener is never cleaned up. Return a cleanup function from useEffect to unsubscribe when the component unmounts or `api` changes.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx">

<violation number="1" location="examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx:611">
P2: Cleanup effect may revoke URLs prematurely. Having `files` in the dependency array causes cleanup to run on every files change, potentially revoking URLs that are still in use. Consider using a ref to track files for cleanup or only running cleanup on unmount.</violation>

<violation number="2" location="examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx:661">
P1: Missing `.catch()` handler for `Promise.all()`. If blob URL conversion fails (network error, FileReader error), the promise rejection will be unhandled, causing an unhandled rejection error and preventing proper error feedback to the user.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


export function getResumableStreamAdapter() {
if (!adapterPromise) {
adapterPromise = (async () => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Rejected promise will be cached permanently. If Redis is temporarily unavailable during the first call, adapterPromise will hold a rejected promise, and all subsequent calls will fail forever (the rejected promise is truthy, so the if (!adapterPromise) check won't re-initialize). Consider clearing adapterPromise on rejection to allow recovery.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/lib/resumable-stream.ts, line 12:

<comment>Rejected promise will be cached permanently. If Redis is temporarily unavailable during the first call, `adapterPromise` will hold a rejected promise, and all subsequent calls will fail forever (the rejected promise is truthy, so the `if (!adapterPromise)` check won't re-initialize). Consider clearing `adapterPromise` on rejection to allow recovery.</comment>

<file context>
@@ -0,0 +1,19 @@
+
+export function getResumableStreamAdapter() {
+  if (!adapterPromise) {
+    adapterPromise = (async () => {
+      const streamStore = await createResumableStreamRedisStore({ waitUntil: after });
+      return createResumableStreamAdapter({ streamStore });
</file context>
Fix with Cubic


useEffect(() => {
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
if (!mounted.current) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The mounted ref check is inverted, causing setState to run on unmounted components. When the async highlightCode completes after unmount, !mounted.current evaluates to true (since cleanup sets it to false), so state updates will still execute. This causes React warnings and potential memory leaks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/code-block.tsx, line 88:

<comment>The mounted ref check is inverted, causing setState to run on unmounted components. When the async `highlightCode` completes after unmount, `!mounted.current` evaluates to `true` (since cleanup sets it to `false`), so state updates will still execute. This causes React warnings and potential memory leaks.</comment>

<file context>
@@ -0,0 +1,175 @@
+
+  useEffect(() => {
+    highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
+      if (!mounted.current) {
+        setHtml(light);
+        setDarkHtml(dark);
</file context>
Fix with Cubic


updateCarouselState();

api.on("select", () => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Memory leak: The api.on("select", ...) event listener is never cleaned up. Return a cleanup function from useEffect to unsubscribe when the component unmounts or api changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/inline-citation.tsx, line 146:

<comment>Memory leak: The `api.on("select", ...)` event listener is never cleaned up. Return a cleanup function from useEffect to unsubscribe when the component unmounts or `api` changes.</comment>

<file context>
@@ -0,0 +1,255 @@
+
+    updateCarouselState();
+
+    api.on("select", () => {
+      setCurrent(api.selectedScrollSnap() + 1);
+    });
</file context>
Fix with Cubic

<Badge className={cn("ml-1 rounded-full", className)} variant="secondary" {...props}>
{sources[0] ? (
<>
{new URL(sources[0]).hostname} {sources.length > 1 && `+${sources.length - 1}`}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: new URL(sources[0]) will throw a TypeError if sources[0] is not a valid URL, crashing the component. Consider wrapping in a try-catch or validating the URL first.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/inline-citation.tsx, line 53:

<comment>`new URL(sources[0])` will throw a `TypeError` if `sources[0]` is not a valid URL, crashing the component. Consider wrapping in a try-catch or validating the URL first.</comment>

<file context>
@@ -0,0 +1,255 @@
+    <Badge className={cn("ml-1 rounded-full", className)} variant="secondary" {...props}>
+      {sources[0] ? (
+        <>
+          {new URL(sources[0]).hostname} {sources.length > 1 && `+${sources.length - 1}`}
+        </>
+      ) : (
</file context>
Fix with Cubic

}

// Convert blob URLs to data URLs asynchronously
Promise.all(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing .catch() handler for Promise.all(). If blob URL conversion fails (network error, FileReader error), the promise rejection will be unhandled, causing an unhandled rejection error and preventing proper error feedback to the user.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx, line 661:

<comment>Missing `.catch()` handler for `Promise.all()`. If blob URL conversion fails (network error, FileReader error), the promise rejection will be unhandled, causing an unhandled rejection error and preventing proper error feedback to the user.</comment>

<file context>
@@ -0,0 +1,1226 @@
+    }
+
+    // Convert blob URLs to data URLs asynchronously
+    Promise.all(
+      files.map(async (item) => {
+        if (item.url?.startsWith("blob:")) {
</file context>
Fix with Cubic

}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The DialogHeader with DialogTitle and DialogDescription should be placed inside DialogContent, not as a sibling. Since DialogContent wraps the Radix UI portal, these accessibility elements won't be properly associated with the dialog when placed outside. Screen readers won't correctly announce the dialog's accessible name and description.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ui/command.tsx, line 44:

<comment>The `DialogHeader` with `DialogTitle` and `DialogDescription` should be placed inside `DialogContent`, not as a sibling. Since `DialogContent` wraps the Radix UI portal, these accessibility elements won't be properly associated with the dialog when placed outside. Screen readers won't correctly announce the dialog's accessible name and description.</comment>

<file context>
@@ -0,0 +1,161 @@
+}) {
+  return (
+    <Dialog {...props}>
+      <DialogHeader className="sr-only">
+        <DialogTitle>{title}</DialogTitle>
+        <DialogDescription>{description}</DialogDescription>
</file context>
Fix with Cubic

};

export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => {
if (!(output || errorText)) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Falsy value check may hide valid tool outputs. If output is 0, false, or "", the condition !(output || errorText) evaluates to true (when no errorText), causing the component to return null. Use nullish check output == null to allow falsy but valid outputs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/tool.tsx, line 109:

<comment>Falsy value check may hide valid tool outputs. If `output` is `0`, `false`, or `""`, the condition `!(output || errorText)` evaluates to true (when no errorText), causing the component to return null. Use nullish check `output == null` to allow falsy but valid outputs.</comment>

<file context>
@@ -0,0 +1,137 @@
+};
+
+export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => {
+  if (!(output || errorText)) {
+    return null;
+  }
</file context>
Fix with Cubic

- VoltAgent Hono server with resumable stream adapter
- Opt-in resumable streaming via `options.resumableStream: true`
- Resume endpoint using `userId` + `conversationId`
- Redis-backed stream storage via `resumable-stream/redis`
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Incorrect package name in documentation. The package is @voltagent/resumable-streams, not resumable-stream/redis. This could confuse users trying to understand the dependencies.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-resumable-streams/README.md, line 10:

<comment>Incorrect package name in documentation. The package is `@voltagent/resumable-streams`, not `resumable-stream/redis`. This could confuse users trying to understand the dependencies.</comment>

<file context>
@@ -0,0 +1,65 @@
+- VoltAgent Hono server with resumable stream adapter
+- Opt-in resumable streaming via `options.resumableStream: true`
+- Resume endpoint using `userId` + `conversationId`
+- Redis-backed stream storage via `resumable-stream/redis`
+
+## Setup
</file context>
Fix with Cubic

await navigator.clipboard.writeText(code);
setIsCopied(true);
onCopy?.();
setTimeout(() => setIsCopied(false), timeout);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The setTimeout for resetting isCopied is not cleaned up on unmount. If the component unmounts before the timeout expires, it will attempt to set state on an unmounted component. Consider using a ref to track the timeout and clearing it in a cleanup effect, or using a custom hook like useTimeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/code-block.tsx, line 156:

<comment>The setTimeout for resetting `isCopied` is not cleaned up on unmount. If the component unmounts before the timeout expires, it will attempt to set state on an unmounted component. Consider using a ref to track the timeout and clearing it in a cleanup effect, or using a custom hook like `useTimeout`.</comment>

<file context>
@@ -0,0 +1,175 @@
+      await navigator.clipboard.writeText(code);
+      setIsCopied(true);
+      onCopy?.();
+      setTimeout(() => setIsCopied(false), timeout);
+    } catch (error) {
+      onError?.(error as Error);
</file context>
Fix with Cubic

}
}
},
[usingProvider, files],
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Cleanup effect may revoke URLs prematurely. Having files in the dependency array causes cleanup to run on every files change, potentially revoking URLs that are still in use. Consider using a ref to track files for cleanup or only running cleanup on unmount.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx, line 611:

<comment>Cleanup effect may revoke URLs prematurely. Having `files` in the dependency array causes cleanup to run on every files change, potentially revoking URLs that are still in use. Consider using a ref to track files for cleanup or only running cleanup on unmount.</comment>

<file context>
@@ -0,0 +1,1226 @@
+        }
+      }
+    },
+    [usingProvider, files],
+  );
+
</file context>
Fix with Cubic

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In @examples/README.md:
- Line 116: Update the example link in README.md to point to the actual folder
name: replace the path string "./with-nextjs-ai-elements" with
"./with-nextjs-resumable-stream" so the list entry "[Next.js + AI
Elements](./with-nextjs-ai-elements) — AI Elements chat UI with VoltAgent and
resumable streams." correctly links to the existing example directory; ensure
you update the same link text location in examples/README.md (the line
containing the Next.js example) so the markdown target matches the real folder.

In @examples/with-nextjs-resumable-stream/components/ui/progress.tsx:
- Around line 8-24: The Progress component destructures a value prop but never
passes it to ProgressPrimitive.Root, so ARIA attributes like aria-valuenow
aren’t set; update the Progress function to forward the value to
ProgressPrimitive.Root (e.g., include value={value ?? 0} or value={value} on the
<ProgressPrimitive.Root> props) while keeping the existing visual transform on
ProgressPrimitive.Indicator so the component remains visually identical but now
exposes the correct accessibility attributes.

In @examples/with-nextjs-resumable-stream/package.json:
- Line 2: The package.json "name" value is inconsistent with the repository
directory (examples/with-nextjs-resumable-stream); update the "name" field to
match the directory (e.g., change "voltagent-example-with-nextjs-ai-elements" to
"voltagent-example-with-nextjs-resumable-stream") and also search for any other
occurrences of the old name (the occurrence referenced at the second location)
and update them as well; if there is a "repository" or related metadata field,
ensure it correctly references the repo/path that matches the package name and
directory.

In @packages/resumable-streams/src/chat-session.ts:
- Around line 38-51: Currently local activeStreamId can diverge from adapter
state causing orphaned streams; remove the local activeStreamId variable and
always query the adapter for the current stream id, or if you prefer to keep
local caching ensure it is updated on every adapter call. Specifically,
eliminate or stop relying on the module-level activeStreamId, change
getActiveStreamId to return adapter.getActiveStreamId(context) (used
everywhere), update createStream/resumeStream/clearActiveStream to not rely on
local state (or to set it from adapter responses), and modify onFinish to fetch
the current id from adapter.getActiveStreamId(context) and call
adapter.clearActiveStream({ ...context, streamId }) unconditionally when an id
exists so adapter state is always cleared even if the local variable is null.

In @website/docs/agents/resumable-streaming.md:
- Around line 382-383: The current guidance incorrectly suggests using
crypto.randomUUID() directly which yields a new id each time; update the docs to
instruct storing a stable client id across sessions (e.g., explain and show a
getOrCreateUserId() helper that checks localStorage for a saved id, generates
one with crypto.randomUUID() only if missing, saves it, and returns it) and
replace the existing line "If you do not have a user identity, generate a stable
id (for example crypto.randomUUID())." with the new explanation referencing
getOrCreateUserId() so clients retain the same userId across reconnects.
🟠 Major comments (16)
examples/with-resumable-streams/package.json-32-36 (1)

32-36: Add engines field to specify minimum Node.js version.

The scripts use the --env-file flag, which requires Node.js 20.6.0 or higher. Without an engines field, users on older Node.js versions will encounter runtime errors when trying to run the example.

🔎 Proposed fix to add engines field
   "license": "MIT",
   "private": true,
+  "engines": {
+    "node": ">=20.6.0"
+  },
   "repository": {

Committable suggestion skipped: line range outside the PR's diff.

examples/with-nextjs-resumable-stream/app/globals.css-317-324 (1)

317-324: Color function mismatch: hsl() wrapping oklch() values.

The scrollbar thumb uses hsl(var(--muted-foreground) / 0.2), but --muted-foreground is defined as oklch(0.556 0 0). Wrapping an oklch() value in hsl() produces invalid CSS. Use color-mix() or the oklch value directly with alpha.

🔎 Suggested fix
   *::-webkit-scrollbar-thumb {
-    background: hsl(var(--muted-foreground) / 0.2);
+    background: color-mix(in oklch, var(--muted-foreground) 20%, transparent);
     border-radius: 3px;
   }

   *::-webkit-scrollbar-thumb:hover {
-    background: hsl(var(--muted-foreground) / 0.3);
+    background: color-mix(in oklch, var(--muted-foreground) 30%, transparent);
   }

Alternatively, define the color directly:

background: oklch(0.556 0 0 / 0.2);
examples/with-nextjs-resumable-stream/components/ai-elements/sources.tsx-8-12 (1)

8-12: Fix type mismatch between props and rendered component.

The SourcesProps type is defined as ComponentProps<"div">, but the component renders Collapsible, not a native div. This breaks type safety because:

  • Collapsible-specific props won't have proper type inference
  • Invalid div-only attributes might pass type checking
  • IDE autocomplete won't suggest the correct props
🔎 Proposed fix
-export type SourcesProps = ComponentProps<"div">;
+export type SourcesProps = ComponentProps<typeof Collapsible>;
examples/with-nextjs-resumable-stream/components/ai-elements/response.tsx-9-17 (1)

9-17: Custom memo comparison ignores all props except children.

The memo comparison function only checks prevProps.children === nextProps.children, which means the component will not re-render when other props (like className or any Streamdown-specific props) change. This can lead to stale UI and incorrect rendering.

🔎 Proposed fixes

Solution 1 (Recommended): Remove the custom comparison to use default shallow comparison

 export const Response = memo(
   ({ className, ...props }: ResponseProps) => (
     <Streamdown
       className={cn("size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", className)}
       {...props}
     />
   ),
-  (prevProps, nextProps) => prevProps.children === nextProps.children,
 );

Solution 2: If you need custom comparison, check all relevant props

 export const Response = memo(
   ({ className, ...props }: ResponseProps) => (
     <Streamdown
       className={cn("size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", className)}
       {...props}
     />
   ),
-  (prevProps, nextProps) => prevProps.children === nextProps.children,
+  (prevProps, nextProps) => 
+    prevProps.children === nextProps.children &&
+    prevProps.className === nextProps.className
+    // Add other prop comparisons as needed
 );
examples/with-nextjs-resumable-stream/components/ai-elements/branch.tsx-87-98 (1)

87-98: Ensure stable keys for branch rendering.

Using branch.key assumes all children have keys. If children are created without explicit keys, this will be undefined, causing React warnings and potential rendering issues.

🔎 Proposed fix
  return childrenArray.map((branch, index) => (
    <div
      className={cn(
        "grid gap-2 overflow-hidden [&>div]:pb-0",
        index === currentBranch ? "block" : "hidden",
      )}
-     key={branch.key}
+     key={branch.key ?? index}
      {...props}
    >
      {branch}
    </div>
  ));

Alternatively, if branches are expected to have unique identifiers, consider enforcing that via TypeScript or generating stable keys based on branch content.

examples/with-nextjs-resumable-stream/components/ai-elements/branch.tsx-55-62 (1)

55-62: Memoize contextValue to prevent unnecessary re-renders.

The context value is recreated on every render, causing all consumers (BranchMessages, BranchSelector, BranchPrevious, BranchNext, BranchPage) to re-render even when the actual values haven't changed.

🔎 Proposed fix
+ const contextValue: BranchContextType = useMemo(
+   () => ({
-  const contextValue: BranchContextType = {
     currentBranch,
     totalBranches: branches.length,
     goToPrevious,
     goToNext,
     branches,
     setBranches,
-  };
+   }),
+   [currentBranch, branches, goToPrevious, goToNext, setBranches],
+ );

Note: goToPrevious, goToNext, and setBranches will also need memoization to avoid the memo being invalidated:

+ const goToPrevious = useCallback(() => {
-  const goToPrevious = () => {
     const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
     handleBranchChange(newBranch);
-  };
+  }, [currentBranch, branches.length, handleBranchChange]);

+ const goToNext = useCallback(() => {
-  const goToNext = () => {
     const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
     handleBranchChange(newBranch);
-  };
+  }, [currentBranch, branches.length, handleBranchChange]);

And wrap handleBranchChange:

+ const handleBranchChange = useCallback((newBranch: number) => {
-  const handleBranchChange = (newBranch: number) => {
     setCurrentBranch(newBranch);
     onBranchChange?.(newBranch);
-  };
+  }, [onBranchChange]);
examples/with-nextjs-resumable-stream/components/ai-elements/image.tsx-2-2 (1)

2-2: Replace the experimental Experimental_GeneratedImage type with the stable GeneratedImage type.

The Experimental_GeneratedImage type has been superseded by a stable API. Using experimental types exposes the code to breaking changes when upgrading the ai library. Both image components should be updated to use the stable alternative.

examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx-661-697 (1)

661-697: Unhandled promise rejection if blob conversion fails.

Promise.all can reject if convertBlobUrlToDataUrl throws (e.g., network error or revoked URL), but there's no .catch() on the chain. This will surface as an unhandled promise rejection.

Additionally, in non-provider mode, form.reset() runs before this async block, so users lose their input even if blob conversion fails—contradicting the "don't clear on error" intent.

🔎 Suggested fix
     Promise.all(
       files.map(async (item) => {
         if (item.url?.startsWith("blob:")) {
           return {
             ...item,
             url: await convertBlobUrlToDataUrl(item.url),
           };
         }
         return item;
       }),
-    ).then((convertedFiles: FileUIPart[]) => {
+    )
+    .then((convertedFiles: FileUIPart[]) => {
       try {
         const result = onSubmit({ text, files: convertedFiles }, event);
         // ... rest of handler
       } catch {
         // Don't clear on error - user may want to retry
       }
-    });
+    })
+    .catch((err) => {
+      console.error("Failed to convert attachments:", err);
+      // Consider restoring form state or notifying user
+    });

Consider deferring form.reset() until after successful submission to preserve user input on failure.

examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx-603-612 (1)

603-612: Cleanup effect may prematurely revoke in-use object URLs.

When files changes, React runs the previous cleanup before the new effect. This cleanup captures the old files array and revokes those URLs—even though they're still referenced in the new state. This can cause broken image previews mid-session.

Use a ref to capture the latest files so cleanup only runs on unmount:

🔎 Suggested fix
+  const filesForCleanupRef = useRef(files);
+  filesForCleanupRef.current = files;
+
   useEffect(
     () => () => {
       if (!usingProvider) {
-        for (const f of files) {
+        for (const f of filesForCleanupRef.current) {
           if (f.url) URL.revokeObjectURL(f.url);
         }
       }
     },
-    [usingProvider, files],
+    [usingProvider],
   );
examples/with-nextjs-resumable-stream/components/ai-elements/code-block.tsx-86-98 (1)

86-98: Bug: Highlighting won't update when props change after initial mount.

The mounted.current flag prevents state updates after the first render, but the cleanup resets it to false. When code, language, or showLineNumbers change, the effect re-runs but mounted.current is still true from the previous render, blocking the update.

🔎 Proposed fix
 export const CodeBlock = ({
   code,
   language,
   showLineNumbers = false,
   className,
   children,
   ...props
 }: CodeBlockProps) => {
   const [html, setHtml] = useState<string>("");
   const [darkHtml, setDarkHtml] = useState<string>("");
-  const mounted = useRef(false);

   useEffect(() => {
-    highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
-      if (!mounted.current) {
-        setHtml(light);
-        setDarkHtml(dark);
-        mounted.current = true;
-      }
+    let cancelled = false;
+    highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
+      if (!cancelled) {
+        setHtml(light);
+        setDarkHtml(dark);
+      }
     });

     return () => {
-      mounted.current = false;
+      cancelled = true;
     };
   }, [code, language, showLineNumbers]);

Committable suggestion skipped: line range outside the PR's diff.

packages/serverless-hono/src/routes.ts-284-295 (1)

284-295: Validate userId against authenticated user identity.

The resume chat stream endpoint accepts userId as a query parameter. While the endpoint validates that userId is provided, there is no authentication middleware validating that the userId matches the authenticated user's identity. This allows any client to manipulate the userId query parameter to access other users' conversations.

Ensure that:

  1. Authentication middleware validates and extracts the authenticated user's identity before this handler executes
  2. The userId parameter is validated to match the authenticated user's identity
  3. The authenticated user cannot be bypassed or spoofed via query parameters
examples/with-nextjs-resumable-stream/components/ui/input.tsx-5-19 (1)

5-19: Add ref forwarding to support form libraries and focus management.

The Input component doesn't forward refs, which prevents parent components from accessing the underlying DOM element. This breaks compatibility with form libraries like react-hook-form and makes imperative operations (focus, blur, etc.) impossible.

🔎 Recommended implementation with ref forwarding
+import * as React from "react";
-import type * as React from "react";

 import { cn } from "@/lib/utils";

-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
+  ({ className, type, ...props }, ref) => {
-  return (
-    <input
-      type={type}
-      data-slot="input"
-      className={cn(
-        "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
-        "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
-        "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
-        className,
-      )}
-      {...props}
-    />
-  );
-}
+    return (
+      <input
+        type={type}
+        data-slot="input"
+        ref={ref}
+        className={cn(
+          "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+          "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+          "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
+          className,
+        )}
+        {...props}
+      />
+    );
+  }
+);
+Input.displayName = "Input";

 export { Input };

Committable suggestion skipped: line range outside the PR's diff.

packages/server-hono/src/app-factory.ts-39-48 (1)

39-48: Add error handling for resumable stream dependency resolution.

The resolveResumableStreamDeps call on Line 40 is not wrapped in error handling. If resolution fails (e.g., Redis connection issues), the error will propagate without context, making debugging difficult.

🔎 Recommended error handling
  const logger = getOrCreateLogger(deps, "api-server");
  const resumableStreamConfig = config.resumableStream;
-  const baseDeps = await resolveResumableStreamDeps(deps, resumableStreamConfig?.adapter, logger);
+  let baseDeps: ServerProviderDeps;
+  try {
+    baseDeps = await resolveResumableStreamDeps(deps, resumableStreamConfig?.adapter, logger);
+  } catch (error) {
+    logger.error("Failed to resolve resumable stream dependencies", { error });
+    // Fall back to original deps if resolution fails
+    baseDeps = deps;
+  }
  const resumableStreamDefault =
    typeof resumableStreamConfig?.defaultEnabled === "boolean"
      ? resumableStreamConfig.defaultEnabled
      : baseDeps.resumableStreamDefault;
examples/with-nextjs-resumable-stream/app/api/messages/route.ts-12-16 (1)

12-16: Add error handling for getMessages call.

The sharedMemory.getMessages() call is not wrapped in try-catch. If it throws an error (e.g., database connection failure, Redis timeout), the request will fail with an unhandled error and return a generic 500 response.

🔎 Recommended fix with error handling
 export async function GET(request: Request) {
   const { searchParams } = new URL(request.url);
   const conversationId = searchParams.get("conversationId");
   const userId = searchParams.get("userId");
 
   if (!conversationId || !userId) {
     return Response.json({ error: "conversationId and userId are required" }, { status: 400 });
   }
 
+  try {
     const uiMessages = await sharedMemory.getMessages(userId, conversationId);
 
     return Response.json({
       data: uiMessages || [],
     });
+  } catch (error) {
+    console.error("Failed to fetch messages:", error);
+    return Response.json(
+      { error: "Failed to fetch messages" },
+      { status: 500 }
+    );
+  }
 }
examples/with-nextjs-resumable-stream/lib/resumable-stream.ts-10-19 (1)

10-19: Add error handling for getResumableStreamAdapter() call in stream route.

The function at line 46 in app/api/chat/route.ts is covered by the try-catch block, but the call at line 23 in app/api/chat/[id]/stream/route.ts is not wrapped in error handling. If Redis initialization fails, the unhandled promise rejection will crash the endpoint. Additionally, the lazy initialization pattern means a failed initialization will persist—all subsequent calls will fail without retry capability. Wrap the call in a try-catch or move it inside the existing error handler.

packages/resumable-streams/package.json-6-11 (1)

6-11: @voltagent/core is listed in both dependencies and peerDependencies.

This can cause duplicate package instances and version conflicts at runtime. For a monorepo package, @voltagent/core should typically only be in peerDependencies (and optionally devDependencies for development/testing), not in dependencies.

🔎 Proposed fix
  "dependencies": {
-   "@voltagent/core": "^2.0.5",
    "@voltagent/internal": "^1.0.2",
    "redis": "^4.7.0",
    "resumable-stream": "^2.2.10"
  },
+ "devDependencies": {
+   "@voltagent/core": "^2.0.5"
+ },

Committable suggestion skipped: line range outside the PR's diff.

🟡 Minor comments (18)
examples/with-nextjs-resumable-stream/components/ui/input-group.tsx-69-74 (1)

69-74: The focus handler doesn't account for textarea elements.

The querySelector("input") only targets <input> elements. If InputGroupTextarea is used, clicking the addon won't focus it.

🔎 Proposed fix to support both input and textarea
       onClick={(e) => {
         if ((e.target as HTMLElement).closest("button")) {
           return;
         }
-        e.currentTarget.parentElement?.querySelector("input")?.focus();
+        const parent = e.currentTarget.parentElement;
+        const control = parent?.querySelector<HTMLElement>("input, textarea");
+        control?.focus();
       }}
examples/with-nextjs-resumable-stream/app/globals.css-139-144 (1)

139-144: Hardcoded dark theme color may cause contrast issues in light mode.

The .markdown-content class uses a hardcoded light gray color (#e5e7eb) that assumes a dark background. In light mode, this would result in poor contrast. Consider using a CSS variable or adding a light mode override.

🔎 Suggested fix
 .markdown-content {
   font-size: 0.9375rem;
   line-height: 1.7;
-  color: #e5e7eb;
+  color: var(--foreground);
 }

Alternatively, scope this to dark mode only:

.dark .markdown-content {
  color: #e5e7eb;
}

Committable suggestion skipped: line range outside the PR's diff.

examples/with-nextjs-resumable-stream/components/ai-elements/chain-of-thought.tsx-118-121 (1)

118-121: Connector line displays on the last step.

The vertical connector line will render on all steps including the last one, which may look incomplete since there's nothing below to connect to. Consider hiding it on the last step using CSS or a prop.

🔎 Suggested CSS fix using last-child
       <div className="relative mt-0.5">
         <Icon className="size-4" />
-        <div className="-mx-px absolute top-7 bottom-0 left-1/2 w-px bg-border" />
+        <div className="-mx-px absolute top-7 bottom-0 left-1/2 w-px bg-border group-last:hidden" />
       </div>

Note: This requires adding group class to the parent and ensuring the step's container has group context, or alternatively use a last-child CSS selector in your styles. Another option is to add an isLast prop if the parent has knowledge of step positions.

Committable suggestion skipped: line range outside the PR's diff.

examples/with-nextjs-resumable-stream/components/ui/select.tsx-47-82 (1)

47-82: Fix CSS variable syntax inconsistency.

Line 73 uses Tailwind v3 bracket syntax with explicit var() for CSS variables, while line 59 correctly uses Tailwind v4 parentheses syntax. In Tailwind v4, CSS variables should use parentheses and Tailwind automatically wraps them in var().

🔎 Proposed fix
           position === "popper" &&
-            "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
+            "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
         )}
examples/with-nextjs-resumable-stream/components/ai-elements/prompt-input.tsx-1001-1058 (1)

1001-1058: Unstable dependencies cause speech recognition to reset on each render.

If onTranscriptionChange isn't memoized by the parent, the effect re-runs on every render, creating a new SpeechRecognition instance and stopping any in-progress recognition. The Promise.resolve().then() at line 1050 is also non-idiomatic—just call setRecognition directly.

🔎 Suggested fix

Store the callback in a ref to stabilize it:

+  const onTranscriptionChangeRef = useRef(onTranscriptionChange);
+  onTranscriptionChangeRef.current = onTranscriptionChange;
+
   useEffect(() => {
     if (
       typeof window !== "undefined" &&
       ("SpeechRecognition" in window || "webkitSpeechRecognition" in window)
     ) {
       const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
       const speechRecognition = new SpeechRecognition();
       // ... setup code ...
       
       speechRecognition.onresult = (event) => {
         // ...
         if (finalTranscript && textareaRef?.current) {
           // ...
-          onTranscriptionChange?.(newValue);
+          onTranscriptionChangeRef.current?.(newValue);
         }
       };

       recognitionRef.current = speechRecognition;
-      Promise.resolve().then(() => setRecognition(speechRecognition));
+      setRecognition(speechRecognition);
     }
     // ...
-  }, [textareaRef, onTranscriptionChange]);
+  }, [textareaRef]);
examples/with-resumable-streams/README.md-1-65 (1)

1-65: Fix incorrect import path on line 10.

The import reference should be @voltagent/resumable-streams instead of resumable-stream/redis. The package only exports from the root path, and the actual implementation imports from @voltagent/resumable-streams directly.

The KV_URL documentation is accurate—the implementation supports both REDIS_URL and KV_URL environment variables as alternatives. The curl endpoint examples are also correct and match the actual route definitions.

examples/with-nextjs-resumable-stream/components/ai-elements/context.tsx-53-57 (1)

53-57: Potential division by zero when maxTokens is 0.

If maxTokens is 0, the division usedTokens / maxTokens will result in Infinity or NaN, which could cause visual artifacts in the SVG. This same issue exists in ContextTrigger (line 98) and ContextContentHeader (line 130).

🔎 Proposed fix
 const ContextIcon = () => {
   const { usedTokens, maxTokens } = useContextValue();
   const circumference = 2 * Math.PI * ICON_RADIUS;
-  const usedPercent = usedTokens / maxTokens;
+  const usedPercent = maxTokens > 0 ? usedTokens / maxTokens : 0;
   const dashOffset = circumference * (1 - usedPercent);

Apply the same guard in ContextTrigger and ContextContentHeader.

examples/with-nextjs-resumable-stream/components/ai-elements/message.tsx-22-42 (1)

22-42: Suspicious is-user:dark class in base styles.

The class is-user:dark on line 23 appears incomplete or incorrect. If this is meant to be a Tailwind variant, it should follow the pattern group-[.is-user]:dark:.... If it's a typo, it should be removed.

🔎 Proposed fix (if unintentional)
 const messageContentVariants = cva(
-  "is-user:dark flex flex-col gap-2 overflow-hidden rounded-lg text-sm",
+  "flex flex-col gap-2 overflow-hidden rounded-lg text-sm",
   {
packages/server-hono/src/routes/index.ts-127-137 (1)

127-137: Add userId validation before calling handler.

The route extracts userId from the query string but doesn't validate its presence before calling handleResumeChatStream. According to the handler implementation in packages/server-core/src/handlers/agent.handlers.ts (lines 414-427), userId is required and returns a 400 error if missing. It's better to validate required parameters at the route level for fail-fast behavior and clearer error messages.

🔎 Proposed fix
  // GET /agents/:id/chat/:conversationId/stream - Resume chat stream (UI message stream SSE)
  app.openapi(resumeChatStreamRoute, async (c) => {
    const agentId = c.req.param("id");
    const conversationId = c.req.param("conversationId");
    const userId = c.req.query("userId");
-   if (!agentId || !conversationId) {
-     throw new Error("Missing agent or conversation id parameter");
+   if (!agentId || !conversationId || !userId) {
+     throw new Error("Missing required parameters: agentId, conversationId, or userId");
    }

    return handleResumeChatStream(agentId, conversationId, deps, logger, userId);
  });
packages/server-hono/src/app-factory.ts-51-59 (1)

51-59: Remove unnecessary type assertions on resolvedDeps (lines 56, 59).

The as any casts on resolvedDeps at lines 56 and 59 are unnecessary since both registerToolRoutes and registerA2ARoutes expect ServerProviderDeps, which matches the actual type of resolvedDeps.

However, the as any cast on line 58 for registerMcpRoutes is necessary because that function expects the narrower McpDeps type, not the full ServerProviderDeps.

Remove the unnecessary casts on lines 56 and 59 to preserve type safety:

  • registerToolRoutes(app as any, resolvedDeps, logger)
  • registerA2ARoutes(app as any, resolvedDeps, logger)
examples/with-nextjs-resumable-stream/components/ui/carousel.tsx-94-103 (1)

94-103: Missing cleanup for reInit event listener.

The effect subscribes to both reInit and select events but only unsubscribes from select in the cleanup function. This can cause memory leaks and stale callback invocations.

🔎 Proposed fix
  React.useEffect(() => {
    if (!api) return;
    onSelect(api);
    api.on("reInit", onSelect);
    api.on("select", onSelect);

    return () => {
+     api?.off("reInit", onSelect);
      api?.off("select", onSelect);
    };
  }, [api, onSelect]);
packages/resumable-streams/src/chat-handlers.ts-181-184 (1)

181-184: Returning 204 for errors hides server failures from clients.

Currently, any exception in the GET handler results in a 204 response, making it indistinguishable from "no active stream." This can make client-side debugging difficult. Consider returning a 500 status for unexpected errors.

🔎 Proposed fix
     } catch (error) {
       logger.error("Failed to resume chat stream", { error });
-      return new Response(null, { status: 204 });
+      return jsonError(500, "Failed to resume stream");
     }

Committable suggestion skipped: line range outside the PR's diff.

examples/with-nextjs-resumable-stream/components/ai-elements/suggestion.tsx-13-13 (1)

13-13: Ensure viewport remains keyboard-accessible despite hidden scrollbar.

The horizontal ScrollBar is hidden with className="hidden", but this only affects the visual indicator. Radix UI's ScrollArea maintains native scrolling behavior and keyboard support by default, so the content remains navigable via keyboard and screen readers. However, verify that the scrollable viewport or its child elements receive focus during keyboard navigation to ensure the hidden scrollbar doesn't create an unexpected user experience where overflow content exists but isn't obviously scrollable.

examples/with-nextjs-resumable-stream/components/ai-elements/inline-citation.tsx-125-162 (1)

125-162: Missing cleanup for carousel event listener.

The useEffect subscribes to the carousel's "select" event (line 146) but doesn't return a cleanup function to remove the listener when the component unmounts or when api changes. This could cause memory leaks or stale state updates.

🔎 Add cleanup function
   useEffect(() => {
     if (!api) {
       return;
     }

     const updateCarouselState = () => {
       setCount(api.scrollSnapList().length);
       setCurrent(api.selectedScrollSnap() + 1);
     };

     updateCarouselState();

-    api.on("select", () => {
+    const onSelect = () => {
       setCurrent(api.selectedScrollSnap() + 1);
-    });
+    };
+
+    api.on("select", onSelect);
+
+    return () => {
+      api.off("select", onSelect);
+    };
   }, [api]);
packages/resumable-streams/src/resumable-streams.ts-119-220 (1)

119-220: unsubscribe removes all listeners, not a specific one.

The unsubscribe method (line 214-216) deletes the entire channel, removing all listeners. If multiple subscribers exist on the same channel, this would unsubscribe them all. This may be intentional for this use case, but differs from typical pub/sub semantics where you unsubscribe a specific callback.

🔎 If per-callback unsubscribe is needed
 const subscriber: ResumableStreamSubscriber = {
   async connect() {},
   async subscribe(channel, callback) {
     let listeners = channels.get(channel);
     if (!listeners) {
       listeners = new Set();
       channels.set(channel, listeners);
     }

     listeners.add(callback);
     return listeners.size;
   },
-  async unsubscribe(channel) {
-    channels.delete(channel);
+  async unsubscribe(channel, callback?) {
+    if (callback) {
+      const listeners = channels.get(channel);
+      if (listeners) {
+        listeners.delete(callback);
+        if (listeners.size === 0) {
+          channels.delete(channel);
+        }
+      }
+    } else {
+      channels.delete(channel);
+    }
   },
 };
examples/with-nextjs-resumable-stream/components/ai-elements/queue.tsx-199-213 (1)

199-213: Renders undefined when count prop is not provided.

When count is not passed, line 210 renders "undefined {label}". Consider handling the missing count case.

🔎 Suggested fix
     <span>
-      {count} {label}
+      {count !== undefined && `${count} `}{label}
     </span>

Or if count should always be shown:

     <span>
-      {count} {label}
+      {count ?? 0} {label}
     </span>
examples/with-nextjs-resumable-stream/components/ai-elements/web-preview.tsx-136-146 (1)

136-146: Controlled/uncontrolled mode logic may cause inconsistent state.

When onChange is provided, handleChange is bypassed (line 139), so inputValue state is never updated. However, handleKeyDown still calls setUrl(target.value) on Enter, which updates context but not necessarily the local input state. This creates a mismatch between the input's displayed value and the context URL.

Consider simplifying: either fully delegate to the consumer when onChange/value are provided, or always sync internal state.

🔎 Suggested fix — always use internal handler, call consumer's onChange additionally
   return (
     <Input
       className="h-8 flex-1 text-sm"
-      onChange={onChange ?? handleChange}
+      onChange={handleChange}
       onKeyDown={handleKeyDown}
       placeholder="Enter URL..."
       value={value ?? inputValue}
       {...props}
     />
   );

This ensures inputValue stays in sync while still calling the consumer's onChange via line 125.

examples/with-nextjs-resumable-stream/components/ai-elements/node.tsx-21-30 (1)

21-30: children is rendered twice — once via spread and once explicitly.

children is included in ...props which gets spread to Card, and then props.children is rendered again inside the Card. This causes children to be rendered twice.

🔎 Proposed fix
-export const Node = ({ handles, className, ...props }: NodeProps) => (
+export const Node = ({ handles, className, children, ...props }: NodeProps) => (
   <Card
     className={cn("node-container relative size-full h-auto w-sm gap-0 rounded-md p-0", className)}
     {...props}
   >
     {handles.target && <Handle position={Position.Left} type="target" />}
     {handles.source && <Handle position={Position.Right} type="source" />}
-    {props.children}
+    {children}
   </Card>
 );

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/resumable-streams/src/types.ts (1)

7-17: Verify the three-state return type for resumeExistingStream.

createNewResumableStream returns Promise<ReadableStream<string> | null> (two states), while resumeExistingStream returns Promise<ReadableStream<string> | null | undefined> (three states). This inconsistency makes the API harder to reason about.

If the distinction between null and undefined is intentional (e.g., undefined = "stream not found" vs. null = "error condition"), consider documenting this with JSDoc comments. Otherwise, align both methods to use the same two-state pattern for consistency.

♻️ Proposed fix for consistent return types
  resumeExistingStream: (
    streamId: string,
    skipCharacters?: number,
- ) => Promise<ReadableStream<string> | null | undefined>;
+ ) => Promise<ReadableStream<string> | null>;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77bc378 and 5985ca3.

📒 Files selected for processing (1)
  • packages/resumable-streams/src/types.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/resumable-streams/src/types.ts
🧬 Code graph analysis (1)
packages/resumable-streams/src/types.ts (1)
packages/core/src/types.ts (1)
  • ResumableStreamContext (44-48)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test core
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (2)
packages/resumable-streams/src/types.ts (2)

1-5: LGTM: Type imports and re-exports are well-structured.

The imports from @voltagent/core and resumable-stream are correctly typed, and the re-export aliases provide a consistent naming convention for the package.


19-43: LGTM: Type definitions are well-structured and type-safe.

The type hierarchy is logical, with clear distinctions between Redis-specific and generic store options. The waitUntil pattern appropriately supports serverless environments.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @examples/with-voltops-resumable-streams/package.json:
- Around line 6-13: The dependency "@voltagent/[email protected]" in
package.json is not published to npm (404); update package.json to either point
to a published version (e.g., a known released version of
@voltagent/resumable-streams) or remove/replace the dependency with an
alternative, then run npm install to verify resolution and update lockfile;
ensure the change references the exact dependency name
"@voltagent/resumable-streams" so reviewers can confirm the fix.

In @website/docs/agents/resumable-streaming.md:
- Around line 443-444: The doc's advice to use crypto.randomUUID() directly is
wrong because it generates a new id each call; change the guidance to require a
persisted stable userId across reconnects and show how to generate-and-store one
instead (e.g., implement getOrCreateUserId that checks localStorage or cookies
on browsers and only calls crypto.randomUUID() when none exists), and for
server/native clients recommend session storage, secure storage, or a
database-backed identity so that the keying rule (`userId + "-" +
conversationId`) uses a consistent userId across reconnects.
🧹 Nitpick comments (4)
examples/with-voltops-resumable-streams/.env.example (1)

1-5: Reorder environment variables alphabetically.

The dotenv-linter tool flagged that keys should be ordered alphabetically. Currently, PORT appears after VOLTAGENT_SECRET_KEY, but it should appear before the VOLTAGENT_* keys.

↔️ Proposed fix for key ordering
 OPENAI_API_KEY=
+PORT=3141
 VOLTAGENT_PUBLIC_KEY=
 VOLTAGENT_SECRET_KEY=
 # VOLTAGENT_API_BASE_URL=https://api.voltagent.dev
-PORT=3141
packages/server-core/src/handlers/observability.handlers.ts (1)

169-184: Consider defining proper types for the resumable stream adapter interface.

The current implementation uses runtime type checks and multiple type assertions to access duck-typed properties (__voltagentResumableStoreType, __voltagentResumableStoreDisplayName) on the adapter. While functionally correct, this approach reduces type safety and makes the code harder to maintain.

♻️ Suggested refactor to improve type safety

Define an interface for the resumable stream adapter with these optional marker properties:

interface ResumableStreamAdapterMetadata {
  __voltagentResumableStoreType?: string;
  __voltagentResumableStoreDisplayName?: string;
}

Then simplify the extraction logic:

-    const resumableStreamAdapter = deps.resumableStream;
+    const resumableStreamAdapter = deps.resumableStream as ResumableStreamAdapterMetadata | undefined;
     const resumableStreamEnabled = !!resumableStreamAdapter;
-    const resumableStreamStoreType =
-      typeof (resumableStreamAdapter as { __voltagentResumableStoreType?: unknown })
-        ?.__voltagentResumableStoreType === "string"
-        ? ((resumableStreamAdapter as { __voltagentResumableStoreType?: string })
-            .__voltagentResumableStoreType as string)
-        : resumableStreamEnabled
-          ? "custom"
-          : null;
-    const resumableStreamStoreDisplayName =
-      typeof (resumableStreamAdapter as { __voltagentResumableStoreDisplayName?: unknown })
-        ?.__voltagentResumableStoreDisplayName === "string"
-        ? ((resumableStreamAdapter as { __voltagentResumableStoreDisplayName?: string })
-            .__voltagentResumableStoreDisplayName as string)
-        : null;
+    const resumableStreamStoreType = 
+      resumableStreamAdapter?.__voltagentResumableStoreType ?? 
+      (resumableStreamEnabled ? "custom" : null);
+    const resumableStreamStoreDisplayName = 
+      resumableStreamAdapter?.__voltagentResumableStoreDisplayName ?? null;

This assumes the adapter implementations guarantee these properties are strings when present, eliminating the need for runtime type checks.

examples/with-voltops-resumable-streams/src/index.ts (1)

15-17: Consider validating VoltOps credentials before creating the store.

The createResumableStreamVoltOpsStore() likely requires VOLTAGENT_PUBLIC_KEY and VOLTAGENT_SECRET_KEY environment variables (per PR objectives). Consider validating these are set before attempting to create the store, providing a clearer error message if they're missing.

✨ Proposed validation
 async function start() {
+  const { VOLTAGENT_PUBLIC_KEY, VOLTAGENT_SECRET_KEY } = process.env;
+  if (!VOLTAGENT_PUBLIC_KEY || !VOLTAGENT_SECRET_KEY) {
+    throw new Error("Missing required VoltOps credentials: VOLTAGENT_PUBLIC_KEY and VOLTAGENT_SECRET_KEY");
+  }
+
   const streamStore = await createResumableStreamVoltOpsStore();
   const resumableStreamAdapter = await createResumableStreamAdapter({ streamStore });
website/docs/agents/resumable-streaming.md (1)

33-40: Consider rewording for better flow.

Three successive sentences begin with "When", which slightly impacts readability.

📝 Suggested rewording
-When a stream starts, a new `streamId` is created, the stream store persists the output, and the active stream store is updated. When the client reconnects, the active stream store is used to find the `streamId`, and the stream store resumes from the last position. When the stream finishes, the active stream store entry is cleared.
+When a stream starts, a new `streamId` is created, the stream store persists the output, and the active stream store is updated. On reconnect, the active stream store provides the `streamId` and the stream store resumes from the last position. Upon completion, the active stream store entry is cleared.
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f200379 and dd87298.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • examples/README.md
  • examples/with-voltops-resumable-streams/.env.example
  • examples/with-voltops-resumable-streams/README.md
  • examples/with-voltops-resumable-streams/package.json
  • examples/with-voltops-resumable-streams/src/index.ts
  • examples/with-voltops-resumable-streams/tsconfig.json
  • packages/core/src/memory/manager/memory-manager.ts
  • packages/core/src/voltops/global-client.ts
  • packages/core/src/voltops/index.ts
  • packages/resumable-streams/src/index.ts
  • packages/resumable-streams/src/resumable-streams.ts
  • packages/resumable-streams/src/types.ts
  • packages/server-core/src/handlers/observability.handlers.ts
  • website/docs/agents/resumable-streaming.md
✅ Files skipped from review due to trivial changes (1)
  • examples/with-voltops-resumable-streams/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • examples/README.md
  • packages/resumable-streams/src/resumable-streams.ts
  • packages/resumable-streams/src/types.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

**/*.ts: Maintain type safety in TypeScript-first codebase
Never use JSON.stringify; use the safeStringify function instead, imported from @voltagent/internal

Files:

  • packages/core/src/voltops/global-client.ts
  • packages/core/src/voltops/index.ts
  • packages/server-core/src/handlers/observability.handlers.ts
  • examples/with-voltops-resumable-streams/src/index.ts
  • packages/core/src/memory/manager/memory-manager.ts
  • packages/resumable-streams/src/index.ts
🧠 Learnings (1)
📚 Learning: 2026-01-07T05:09:23.216Z
Learnt from: CR
Repo: VoltAgent/voltagent PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-07T05:09:23.216Z
Learning: Applies to **/*.ts : Maintain type safety in TypeScript-first codebase

Applied to files:

  • examples/with-voltops-resumable-streams/tsconfig.json
🧬 Code graph analysis (2)
packages/core/src/voltops/global-client.ts (2)
packages/core/src/voltops/index.ts (4)
  • getGlobalVoltOpsClient (10-10)
  • VoltOpsClient (9-9)
  • VoltOpsClient (15-15)
  • VoltOpsClient (85-85)
packages/core/src/voltops/client.ts (1)
  • VoltOpsClient (64-1059)
packages/core/src/memory/manager/memory-manager.ts (1)
packages/core/src/agent/types.ts (1)
  • OperationContext (893-941)
🪛 dotenv-linter (4.0.0)
examples/with-voltops-resumable-streams/.env.example

[warning] 5-5: [UnorderedKey] The PORT key should go before the VOLTAGENT_PUBLIC_KEY key

(UnorderedKey)

🪛 LanguageTool
website/docs/agents/resumable-streaming.md

[style] ~40-~40: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...m store resumes from the last position. When the stream finishes, the active stream ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Build (Node 20)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (18)
examples/with-voltops-resumable-streams/tsconfig.json (1)

1-9: LGTM!

Standard TypeScript configuration for an example project. The setup correctly extends the base config and defines appropriate output/source directories with proper include/exclude patterns.

packages/core/src/voltops/index.ts (1)

10-10: LGTM!

The new export is properly positioned and follows the existing module organization pattern.

packages/core/src/voltops/global-client.ts (1)

1-5: LGTM!

The implementation is clean and follows best practices:

  • Proper type imports with type keyword for type-only import
  • Clear delegation pattern to AgentRegistry singleton
  • Appropriate return type handling optional global client
packages/core/src/memory/manager/memory-manager.ts (2)

106-107: LGTM! Consistent metadata enrichment improves observability.

The introduction of messageWithMetadata and its consistent use throughout saveMessage ensures that:

  • The operationId is captured in OpenTelemetry spans (line 115, 117-118)
  • The enriched message is persisted to memory (line 143)
  • Debug logs reflect the complete metadata (line 164)

This change enhances traceability for resumable streams without altering the public API.

Also applies to: 115-115, 117-118, 143-143, 164-164


183-205: LGTM! Well-structured helper with proper defensive checks.

The applyOperationMetadata method is cleanly implemented:

  • Immutable approach using spread operators
  • Defensive type checking for the metadata field (lines 189-192)
  • Idempotency check prevents redundant updates (lines 194-196)
  • Early returns optimize performance

The !operationId check (line 185) provides defensive programming even though operationId is a required field in OperationContext.

packages/server-core/src/handlers/observability.handlers.ts (1)

237-241: LGTM! Clear addition of resumable stream metadata to observability status.

The resumableStream field appropriately extends the status response with configuration metadata, following the same pattern as existing storage-related fields.

examples/with-voltops-resumable-streams/package.json (7)

1-5: LGTM!

Package metadata is appropriate for an example project. The name, description, and version are clear and follow conventions.


19-25: LGTM!

Keywords are relevant and aid discoverability.


26-27: LGTM!

MIT license and private flag are appropriate for an example project.


28-32: LGTM!

Repository configuration correctly references the monorepo and example directory.


33-37: LGTM!

Scripts are well-structured. The --env-file=.env flag is supported by tsx, enabling convenient environment variable loading during development.


38-38: LGTM!

ESM module type aligns with modern Node.js practices and the project's architecture.


14-18: All specified devDependency versions (@types/[email protected], [email protected], and [email protected]) are published and available on npm. No issues to address.

examples/with-voltops-resumable-streams/src/index.ts (4)

1-13: LGTM!

The imports and logger setup are correctly configured for the VoltOps resumable streaming example.


19-24: Inconsistent AI summary.

The AI summary claims this file defines three tools (uppercase, countWords, writeStory), three agents, and a Supervisor agent. However, the actual code only defines a single agent named "assistant". The summary appears to describe a different implementation.


35-38: LGTM!

The error handling is appropriate for this example, with proper logging and process termination on startup failure.


26-32: This follows the established VoltAgent pattern and requires no changes.

Across the codebase, the vast majority of VoltAgent examples (80+) do not store the instance. The pattern shows that VoltAgent self-registers during instantiation—the side effects (server startup, agent registration) are the primary concern. Instances are only stored in specific architectural patterns (serverless, singleton management). Since this file uses a server-based setup like other non-stored examples, the code is correct as written.

packages/resumable-streams/src/index.ts (1)

1-26: LGTM! Clean public API structure.

The exports are well-organized, grouping types first followed by related implementations. The structure clearly separates concerns (types, core resumable streams, chat handlers, chat sessions) for good API discoverability.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="examples/with-nextjs-resumable-stream/package.json">

<violation number="1" location="examples/with-nextjs-resumable-stream/package.json:2">
P2: Package name was updated but `repository.directory` still references the old directory `examples/with-nextjs-ai-elements`. This should be updated to `examples/with-nextjs-resumable-stream` to match the new package location.</violation>
</file>

<file name="examples/with-nextjs-resumable-stream/README.md">

<violation number="1" location="examples/with-nextjs-resumable-stream/README.md:49">
P2: This README change removes essential setup documentation. Users will not know that `OPENAI_API_KEY` and `REDIS_URL` environment variables are required, nor will they understand the project structure (adapter location, route handlers, component paths). Consider keeping the Setup and Notes sections from the original README below the 'Try Example' section, similar to how `with-resumable-streams` documents its example.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@@ -0,0 +1,70 @@
{
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Package name was updated but repository.directory still references the old directory examples/with-nextjs-ai-elements. This should be updated to examples/with-nextjs-resumable-stream to match the new package location.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/package.json, line 2:

<comment>Package name was updated but `repository.directory` still references the old directory `examples/with-nextjs-ai-elements`. This should be updated to `examples/with-nextjs-resumable-stream` to match the new package location.</comment>

<file context>
@@ -1,5 +1,5 @@
 {
-  "name": "voltagent-example-with-nextjs-ai-elements",
+  "name": "voltagent-example-with-nextjs-resumable-stream",
   "version": "0.1.0",
   "dependencies": {
</file context>
Fix with Cubic

Comment on lines +49 to +52
## Try Example

```bash
npm create voltagent-app@latest -- --example with-nextjs-resumable-stream
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This README change removes essential setup documentation. Users will not know that OPENAI_API_KEY and REDIS_URL environment variables are required, nor will they understand the project structure (adapter location, route handlers, component paths). Consider keeping the Setup and Notes sections from the original README below the 'Try Example' section, similar to how with-resumable-streams documents its example.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At examples/with-nextjs-resumable-stream/README.md, line 49:

<comment>This README change removes essential setup documentation. Users will not know that `OPENAI_API_KEY` and `REDIS_URL` environment variables are required, nor will they understand the project structure (adapter location, route handlers, component paths). Consider keeping the Setup and Notes sections from the original README below the 'Try Example' section, similar to how `with-resumable-streams` documents its example.</comment>

<file context>
@@ -1,43 +1,53 @@
-3. Run the app:
+VoltAgent is an open-source TypeScript framework for creating and managing AI agents. It provides modular components to build, customize, and scale agents with ease. From connecting to APIs and memory management to supporting multiple LLMs, VoltAgent simplifies the process of creating sophisticated AI systems. It enables fast development, maintains clean code, and offers flexibility to switch between models and tools without vendor lock-in.
+
+## Try Example
 
 ```bash
</file context>
Suggested change
## Try Example
```bash
npm create voltagent-app@latest -- --example with-nextjs-resumable-stream
## Try Example
```bash
npm create voltagent-app@latest -- --example with-nextjs-resumable-stream

Setup

  1. Install dependencies:
pnpm install
  1. Set your API key:
cp .env.example .env

Update OPENAI_API_KEY and REDIS_URL in .env.

  1. Run the app:
pnpm dev

Open http://localhost:3000.

Notes

  • The AI Elements components live in components/ai-elements.
  • Resumable adapter is created in lib/resumable-stream.ts, and route handlers use createResumableChatSession to manage lifecycle.
  • Stream creation is in app/api/chat/route.ts.
  • Stream resume endpoint is app/api/chat/[id]/stream/route.ts.
  • The active stream index defaults to the same Redis store; override activeStreamStore if you need a different backend.
  • The agent setup is in voltagent/.

<a href="https://www.cubic.dev/action/fix/violation/9fd7708a-c33e-4e13-8e1a-348862399475" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://cubic.dev/buttons/fix-with-cubic-dark.svg">
    <source media="(prefers-color-scheme: light)" srcset="https://cubic.dev/buttons/fix-with-cubic-light.svg">
    <img alt="Fix with Cubic" src="https://cubic.dev/buttons/fix-with-cubic-dark.svg">
  </picture>
</a>

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @website/docs/agents/resumable-streaming.md:
- Around line 443-444: The docs incorrectly recommend using crypto.randomUUID()
directly for userId (it changes each call); update the "Keying rule: `userId +
"-" + conversationId`" guidance to require a stable persisted identifier and
replace the example with instructions to generate-and-store a stable ID
client-side (e.g., describe a getOrCreateUserId flow that checks
localStorage/cookies and only calls crypto.randomUUID() once then persists it)
and note server/native alternatives (session/secure storage or DB-backed
identity) so userId remains consistent across reconnects.
🧹 Nitpick comments (2)
website/docs/agents/resumable-streaming.md (2)

35-40: Improve sentence flow in "How it works" section.

Three successive sentences begin with "When", affecting readability. Vary the sentence structure.

♻️ Proposed revision for improved flow
 When a stream starts, a new `streamId` is created, the stream store persists the output, and the active stream store is updated. When the client reconnects, the active stream store is used to find the `streamId`, and the stream store resumes from the last position. When the stream finishes, the active stream store entry is cleared.
+A new `streamId` is created when a stream starts, and the stream store persists the output while the active stream store is updated. On reconnection, the active stream store locates the `streamId`, and the stream store resumes from the last position. Finally, the active stream store entry is cleared when the stream finishes.

247-248: Format the GitHub URL as a proper markdown link.

The bare URL at line 248 should be converted to markdown link syntax for better documentation rendering.

♻️ Proposed fix
-This section mirrors the `with-nextjs-resumable-stream` example. Full project:  
-https://github.com/VoltAgent/voltagent/tree/main/examples/with-nextjs-resumable-stream
+This section mirrors the `with-nextjs-resumable-stream` example. Full project:  
+[with-nextjs-resumable-stream](https://github.com/VoltAgent/voltagent/tree/main/examples/with-nextjs-resumable-stream)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a2bf38 and ef0e91f.

📒 Files selected for processing (1)
  • website/docs/agents/resumable-streaming.md
🧰 Additional context used
🪛 LanguageTool
website/docs/agents/resumable-streaming.md

[style] ~40-~40: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...m store resumes from the last position. When the stream finishes, the active stream ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.18.1)
website/docs/agents/resumable-streaming.md

248-248: Bare URL used

(MD034, no-bare-urls)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Test cli
  • GitHub Check: Build (Node 20)
  • GitHub Check: Build (Node 22)
  • GitHub Check: Build (Node 24)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
website/docs/agents/resumable-streaming.md (1)

1-244: Comprehensive documentation with strong technical detail.

The setup instructions, store options, and API reference sections are well-structured and provide clear examples for Redis, VoltOps, and memory-backed stores. The Next.js integration examples clearly demonstrate the adapter initialization pattern and resumption flow. Architecture overview (two-store model at lines 35-40) appropriately sets expectations for the feature.

@omeraplak omeraplak merged commit c4591fa into main Jan 8, 2026
23 checks passed
@omeraplak omeraplak deleted the feat/resumable-stream branch January 8, 2026 01:33
@coderabbitai coderabbitai bot mentioned this pull request Jan 11, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants