Skip to content

fix(core): close supervisor streams with output guardrails - #1417

Open
noooooooookro wants to merge 1 commit into
VoltAgent:mainfrom
noooooooookro:fix/supervisor-output-guardrail-stream
Open

noooooooookro wants to merge 1 commit into
VoltAgent:mainfrom
noooooooookro:fix/supervisor-output-guardrail-stream

Conversation

@noooooooookro

@noooooooookro noooooooookro commented Sep 16, 2026

Copy link
Copy Markdown

What

  • Finalize output guardrails when the source emits its finish part.
  • Continue draining the merged supervisor stream before closing the guarded stream.
  • Add a regression test covering a supervisor with output guardrails.

Why

The guardrail pipeline previously stopped consuming the merged stream as soon as it received finish. For supervisor agents, this caused the merged stream cleanup and guardrail finalization to wait on each other, leaving the stream open indefinitely.

Verification

  • guardrail-stream.spec.ts: 8 tests passed
  • @voltagent/core typecheck passed
  • @voltagent/core build passed
  • Biome checks passed for the changed TypeScript files
  • Changeset validation passed

Fixes #1415


Summary by cubic

Fixes supervisor streams hanging indefinitely when output guardrails are enabled. Previously, the guardrail pipeline stopped consuming the merged supervisor stream as soon as it received finish, so stream cleanup and guardrail finalization waited on each other. Now the pipeline keeps draining the merged stream and finalizes guardrails before closing the guarded stream. Adds a regression test covering a supervisor with output guardrails. Fixes #1415.

Written for commit 540c815. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Fixed supervisor-agent streams that could hang when output guardrails were enabled.
    • Streams now complete reliably and emit the expected finish event after delegated agent output ends.
  • Tests

    • Added regression coverage for supervisor streaming with passthrough output guardrails.
  • Documentation

    • Documented the patch release for the core package.

@changeset-bot

changeset-bot Bot commented Sep 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 540c815

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

This PR includes changesets to release 1 package
Name Type
@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

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: eb9148d3-1839-41f3-8511-2235f226daac

📥 Commits

Reviewing files that changed from the base of the PR and between 44b4c8e and 540c815.

📒 Files selected for processing (3)
  • .changeset/calm-supervisor-streams.md
  • packages/core/src/agent/streaming/guardrail-stream.spec.ts
  • packages/core/src/agent/streaming/guardrail-stream.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The guardrail pipeline now finalizes when it receives a finish part and continues consuming the base stream. A regression test verifies supervisor stream completion and the emitted finish event. A patch changeset documents the fix.

Changes

Supervisor output guardrail completion

Layer / File(s) Summary
Guardrail finalization flow
packages/core/src/agent/streaming/guardrail-stream.ts
The stream reader finalizes guardrails once when it receives a finish part or when the stream ends. It continues iterating after the finish part and resolves completion from the shared finalization closure.
Completion regression coverage
packages/core/src/agent/streaming/guardrail-stream.spec.ts, .changeset/calm-supervisor-streams.md
The integration test consumes a supervisor stream with an output guardrail and verifies a finish event. The changeset records a patch release for @voltagent/core.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 540c8

The supervisor output-guardrail completion fix has regression coverage for the merged supervisor stream path, with no concrete unresolved risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: closing supervisor streams when output guardrails are enabled.
Description check ✅ Passed The description explains the problem, the new behavior, the fix, verification results, linked issue, regression test, and changeset. It does not use the template headings exactly and does not explicit…
Linked Issues check ✅ Passed The changes address issue #1415. createGuardrailPipeline finalizes guardrails when it receives the finish part, continues draining the base stream, emits the sanitized finish part, and then clos…
Out of Scope Changes check ✅ Passed The changed files are limited to the guardrail streaming implementation, its regression tests, and the related core package changeset. These changes directly support issue #1415. No unrelated behavior…
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/core/src/agent/streaming/guardrail-stream.ts">

<violation number="1" location="packages/core/src/agent/streaming/guardrail-stream.ts:142">
P2: When `finish` arrives, `finalizeGuardrails()` resolves `finalizePromise` before the merged supervisor stream is drained and before the sanitized finish part is enqueued and `controller.close()` runs. If the drain, the post-loop enqueue, or `controller.close()` then throws, the `catch` calls `finalizeReject(error)` on an already-resolved promise, so the rejection is silently dropped; consumers awaiting `finalizePromise` (e.g. `createSanitizedTextPromise` in `agent.ts:2598`) see success even though the guarded stream errored. Previously `finalizeResolve()` was the last statement of the success path, so any error rejected the promise.</violation>
</file>

<file name="packages/core/src/agent/streaming/guardrail-stream.spec.ts">

<violation number="1" location="packages/core/src/agent/streaming/guardrail-stream.spec.ts:387">
P3: This regression test never exercises the fix's drain-after-finish path: the mocked stream ends at `finish` with no parts after it, so the new `if (finishPart) continue;` branch never runs. Tracing `createMergedFullStream` (agent.ts:2507), the pre-fix `break` also completes cleanly on this input because the writer is closed once the final part is read, so the test passes before the fix and would not catch a re-introduction of #1415. The merged stream needs to emit parts after `finish` (e.g. sub-agent output after the parent finish) for the hang to be reproduced.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

: {};
await runner.finalize(metadata);
finalized = true;
finalizeResolve();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When finish arrives, finalizeGuardrails() resolves finalizePromise before the merged supervisor stream is drained and before the sanitized finish part is enqueued and controller.close() runs. If the drain, the post-loop enqueue, or controller.close() then throws, the catch calls finalizeReject(error) on an already-resolved promise, so the rejection is silently dropped; consumers awaiting finalizePromise (e.g. createSanitizedTextPromise in agent.ts:2598) see success even though the guarded stream errored. Previously finalizeResolve() was the last statement of the success path, so any error rejected the promise.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/agent/streaming/guardrail-stream.ts, line 142:

<comment>When `finish` arrives, `finalizeGuardrails()` resolves `finalizePromise` before the merged supervisor stream is drained and before the sanitized finish part is enqueued and `controller.close()` runs. If the drain, the post-loop enqueue, or `controller.close()` then throws, the `catch` calls `finalizeReject(error)` on an already-resolved promise, so the rejection is silently dropped; consumers awaiting `finalizePromise` (e.g. `createSanitizedTextPromise` in `agent.ts:2598`) see success even though the guarded stream errored. Previously `finalizeResolve()` was the last statement of the success path, so any error rejected the promise.</comment>

<file context>
@@ -127,14 +127,33 @@ export function createGuardrailPipeline(
+          : {};
+        await runner.finalize(metadata);
+        finalized = true;
+        finalizeResolve();
+      };
 
</file context>

{ type: "text-start", id: "text-1" },
{ type: "text-delta", id: "text-1", delta: "hello" },
{ type: "text-end", id: "text-1" },
{ type: "finish", finishReason, usage },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This regression test never exercises the fix's drain-after-finish path: the mocked stream ends at finish with no parts after it, so the new if (finishPart) continue; branch never runs. Tracing createMergedFullStream (agent.ts:2507), the pre-fix break also completes cleanly on this input because the writer is closed once the final part is read, so the test passes before the fix and would not catch a re-introduction of #1415. The merged stream needs to emit parts after finish (e.g. sub-agent output after the parent finish) for the hang to be reproduced.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/agent/streaming/guardrail-stream.spec.ts, line 387:

<comment>This regression test never exercises the fix's drain-after-finish path: the mocked stream ends at `finish` with no parts after it, so the new `if (finishPart) continue;` branch never runs. Tracing `createMergedFullStream` (agent.ts:2507), the pre-fix `break` also completes cleanly on this input because the writer is closed once the final part is read, so the test passes before the fix and would not catch a re-introduction of #1415. The merged stream needs to emit parts after `finish` (e.g. sub-agent output after the parent finish) for the hang to be reproduced.</comment>

<file context>
@@ -371,6 +371,52 @@ describe("Output guardrail streaming integration", () => {
+            { type: "text-start", id: "text-1" },
+            { type: "text-delta", id: "text-1", delta: "hello" },
+            { type: "text-end", id: "text-1" },
+            { type: "finish", finishReason, usage },
+          ],
+        }),
</file context>

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.

Bug: Output guardrail on supervisor agent causes SSE stream to never terminate

1 participant