-
-
Notifications
You must be signed in to change notification settings - Fork 451
feat: add resumable streaming support via @voltagent/resumable-streams #921
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
🦋 Changeset detectedLatest commit: e3999c3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
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 |
📝 WalkthroughWalkthroughAdds opt-in resumable streaming: new Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
⏰ 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)
Comment |
This comment has been minimized.
This comment has been minimized.
Deploying voltagent with
|
| Latest commit: |
e3999c3
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://c6aac102.voltagent.pages.dev |
| Branch Preview URL: | https://feat-resumable-stream.voltagent.pages.dev |
There was a problem hiding this 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 () => { |
There was a problem hiding this comment.
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>
|
|
||
| useEffect(() => { | ||
| highlightCode(code, language, showLineNumbers).then(([light, dark]) => { | ||
| if (!mounted.current) { |
There was a problem hiding this comment.
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>
|
|
||
| updateCarouselState(); | ||
|
|
||
| api.on("select", () => { |
There was a problem hiding this comment.
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>
| <Badge className={cn("ml-1 rounded-full", className)} variant="secondary" {...props}> | ||
| {sources[0] ? ( | ||
| <> | ||
| {new URL(sources[0]).hostname} {sources.length > 1 && `+${sources.length - 1}`} |
There was a problem hiding this comment.
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>
| } | ||
|
|
||
| // Convert blob URLs to data URLs asynchronously | ||
| Promise.all( |
There was a problem hiding this comment.
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>
| }) { | ||
| return ( | ||
| <Dialog {...props}> | ||
| <DialogHeader className="sr-only"> |
There was a problem hiding this comment.
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>
| }; | ||
|
|
||
| export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => { | ||
| if (!(output || errorText)) { |
There was a problem hiding this comment.
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>
| - 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` |
There was a problem hiding this comment.
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>
| await navigator.clipboard.writeText(code); | ||
| setIsCopied(true); | ||
| onCopy?.(); | ||
| setTimeout(() => setIsCopied(false), timeout); |
There was a problem hiding this comment.
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>
| } | ||
| } | ||
| }, | ||
| [usingProvider, files], |
There was a problem hiding this comment.
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>
There was a problem hiding this 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-fileflag, which requires Node.js 20.6.0 or higher. Without anenginesfield, 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()wrappingoklch()values.The scrollbar thumb uses
hsl(var(--muted-foreground) / 0.2), but--muted-foregroundis defined asoklch(0.556 0 0). Wrapping anoklch()value inhsl()produces invalid CSS. Usecolor-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
SourcesPropstype is defined asComponentProps<"div">, but the component rendersCollapsible, not a nativediv. 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 exceptchildren.The memo comparison function only checks
prevProps.children === nextProps.children, which means the component will not re-render when other props (likeclassNameor 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.keyassumes all children have keys. If children are created without explicit keys, this will beundefined, 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, andsetBrancheswill 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 experimentalExperimental_GeneratedImagetype with the stableGeneratedImagetype.The
Experimental_GeneratedImagetype has been superseded by a stable API. Using experimental types exposes the code to breaking changes when upgrading theailibrary. 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.allcan reject ifconvertBlobUrlToDataUrlthrows (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
fileschanges, React runs the previous cleanup before the new effect. This cleanup captures the oldfilesarray 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
filesso 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.currentflag prevents state updates after the first render, but the cleanup resets it tofalse. Whencode,language, orshowLineNumberschange, the effect re-runs butmounted.currentis stilltruefrom 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
userIdas a query parameter. While the endpoint validates thatuserIdis provided, there is no authentication middleware validating that theuserIdmatches the authenticated user's identity. This allows any client to manipulate theuserIdquery parameter to access other users' conversations.Ensure that:
- Authentication middleware validates and extracts the authenticated user's identity before this handler executes
- The
userIdparameter is validated to match the authenticated user's identity- 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
Inputcomponent doesn't forward refs, which prevents parent components from accessing the underlying DOM element. This breaks compatibility with form libraries likereact-hook-formand 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
resolveResumableStreamDepscall 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 forgetMessagescall.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 forgetResumableStreamAdapter()call in stream route.The function at line 46 in
app/api/chat/route.tsis covered by the try-catch block, but the call at line 23 inapp/api/chat/[id]/stream/route.tsis 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/coreis listed in bothdependenciesandpeerDependencies.This can cause duplicate package instances and version conflicts at runtime. For a monorepo package,
@voltagent/coreshould typically only be inpeerDependencies(and optionallydevDependenciesfor development/testing), not independencies.🔎 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. IfInputGroupTextareais 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-contentclass 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
groupclass to the parent and ensuring the step's container hasgroupcontext, or alternatively use alast-childCSS selector in your styles. Another option is to add anisLastprop 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 invar().🔎 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
onTranscriptionChangeisn't memoized by the parent, the effect re-runs on every render, creating a newSpeechRecognitioninstance and stopping any in-progress recognition. ThePromise.resolve().then()at line 1050 is also non-idiomatic—just callsetRecognitiondirectly.🔎 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-streamsinstead ofresumable-stream/redis. The package only exports from the root path, and the actual implementation imports from@voltagent/resumable-streamsdirectly.The KV_URL documentation is accurate—the implementation supports both
REDIS_URLandKV_URLenvironment 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 whenmaxTokensis 0.If
maxTokensis 0, the divisionusedTokens / maxTokenswill result inInfinityorNaN, which could cause visual artifacts in the SVG. This same issue exists inContextTrigger(line 98) andContextContentHeader(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
ContextTriggerandContextContentHeader.examples/with-nextjs-resumable-stream/components/ai-elements/message.tsx-22-42 (1)
22-42: Suspiciousis-user:darkclass in base styles.The class
is-user:darkon line 23 appears incomplete or incorrect. If this is meant to be a Tailwind variant, it should follow the patterngroup-[.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
userIdfrom the query string but doesn't validate its presence before callinghandleResumeChatStream. According to the handler implementation inpackages/server-core/src/handlers/agent.handlers.ts(lines 414-427),userIdis 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 anycasts onresolvedDepsat lines 56 and 59 are unnecessary since bothregisterToolRoutesandregisterA2ARoutesexpectServerProviderDeps, which matches the actual type ofresolvedDeps.However, the
as anycast on line 58 forregisterMcpRoutesis necessary because that function expects the narrowerMcpDepstype, not the fullServerProviderDeps.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 forreInitevent listener.The effect subscribes to both
reInitandselectevents but only unsubscribes fromselectin 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
useEffectsubscribes to the carousel's "select" event (line 146) but doesn't return a cleanup function to remove the listener when the component unmounts or whenapichanges. 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:unsubscriberemoves all listeners, not a specific one.The
unsubscribemethod (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: Rendersundefinedwhencountprop is not provided.When
countis 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
onChangeis provided,handleChangeis bypassed (line 139), soinputValuestate is never updated. However,handleKeyDownstill callssetUrl(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/valueare 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
inputValuestays in sync while still calling the consumer'sonChangevia line 125.examples/with-nextjs-resumable-stream/components/ai-elements/node.tsx-21-30 (1)
21-30:childrenis rendered twice — once via spread and once explicitly.
childrenis included in...propswhich gets spread toCard, and thenprops.childrenis 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> );
There was a problem hiding this 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 forresumeExistingStream.
createNewResumableStreamreturnsPromise<ReadableStream<string> | null>(two states), whileresumeExistingStreamreturnsPromise<ReadableStream<string> | null | undefined>(three states). This inconsistency makes the API harder to reason about.If the distinction between
nullandundefinedis 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
📒 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 thesafeStringifyfunction 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/coreandresumable-streamare 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
waitUntilpattern appropriately supports serverless environments.
There was a problem hiding this 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,
PORTappears afterVOLTAGENT_SECRET_KEY, but it should appear before theVOLTAGENT_*keys.
↔️ Proposed fix for key orderingOPENAI_API_KEY= +PORT=3141 VOLTAGENT_PUBLIC_KEY= VOLTAGENT_SECRET_KEY= # VOLTAGENT_API_BASE_URL=https://api.voltagent.dev -PORT=3141packages/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 requiresVOLTAGENT_PUBLIC_KEYandVOLTAGENT_SECRET_KEYenvironment 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (14)
examples/README.mdexamples/with-voltops-resumable-streams/.env.exampleexamples/with-voltops-resumable-streams/README.mdexamples/with-voltops-resumable-streams/package.jsonexamples/with-voltops-resumable-streams/src/index.tsexamples/with-voltops-resumable-streams/tsconfig.jsonpackages/core/src/memory/manager/memory-manager.tspackages/core/src/voltops/global-client.tspackages/core/src/voltops/index.tspackages/resumable-streams/src/index.tspackages/resumable-streams/src/resumable-streams.tspackages/resumable-streams/src/types.tspackages/server-core/src/handlers/observability.handlers.tswebsite/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 thesafeStringifyfunction instead, imported from@voltagent/internal
Files:
packages/core/src/voltops/global-client.tspackages/core/src/voltops/index.tspackages/server-core/src/handlers/observability.handlers.tsexamples/with-voltops-resumable-streams/src/index.tspackages/core/src/memory/manager/memory-manager.tspackages/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
typekeyword 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
messageWithMetadataand its consistent use throughoutsaveMessageensures that:
- The
operationIdis 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
applyOperationMetadatamethod is cleanly implemented:
- Immutable approach using spread operators
- Defensive type checking for the
metadatafield (lines 189-192)- Idempotency check prevents redundant updates (lines 194-196)
- Early returns optimize performance
The
!operationIdcheck (line 185) provides defensive programming even thoughoperationIdis a required field inOperationContext.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=.envflag is supported bytsx, 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.
There was a problem hiding this 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 @@ | |||
| { | |||
There was a problem hiding this comment.
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>
| ## Try Example | ||
|
|
||
| ```bash | ||
| npm create voltagent-app@latest -- --example with-nextjs-resumable-stream |
There was a problem hiding this comment.
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>
| ## 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
- Install dependencies:
pnpm install- Set your API key:
cp .env.example .envUpdate OPENAI_API_KEY and REDIS_URL in .env.
- Run the app:
pnpm devOpen 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 usecreateResumableChatSessionto 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
activeStreamStoreif 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>
There was a problem hiding this 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
📒 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.
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
Migration
Written for commit e3999c3. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.