feat(core): add parallel input guardrails#1347
Conversation
🦋 Changeset detectedLatest commit: e47be1e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughIntroduces parallel input guardrail execution for ChangesParallel Input Guardrails for streamText
Sequence Diagram(s)sequenceDiagram
participant Client
participant Agent as Agent.streamText
participant BlockingGuards as Blocking Input Guardrails
participant ParallelGuards as Parallel Input Guardrails
participant Model as LLM Model Stream
participant GateIterator as gateIterableUntilSpeculativeInputPass
participant Buffer as ConversationBuffer
participant StepHandler as Step Persistence Handler
Client->>Agent: streamText(input, guardrails)
Agent->>BlockingGuards: execute immediately (allowModify: true)
BlockingGuards-->>Agent: pass or block/modify
alt Blocking Guardrail Blocks
Agent-->>Client: error thrown
else Blocking Guardrails Pass
Agent->>ParallelGuards: start async (allowModify: false)
Note over ParallelGuards: runs concurrently with model
Agent->>Model: begin streaming
Agent->>GateIterator: wrap stream with pending decision
rect rgba(255, 200, 0, 0.5)
Model-->>GateIterator: emit text chunks (buffered)
end
alt Parallel Guardrail Passes
ParallelGuards-->>GateIterator: decision: passed
GateIterator-->>Client: yield buffered chunks
Agent->>StepHandler: persist steps (hasPassed=true)
else Parallel Guardrail Blocks
ParallelGuards-->>Agent: decision: blocked + message
Agent->>Buffer: restoreCheckpoint()
Agent->>GateIterator: replace stream
GateIterator-->>Client: yield block message only (finish: error)
Agent->>StepHandler: skip persistence (hasPassed=false)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/agent/agent.ts (1)
7584-7610: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winGate
recordStepResultsbefore persisting blocked output.The new
hasPassed()check only guardspersistQueue, butrecordStepResultsabove it can persist step records viamemoryManager.saveConversationStepswhile the speculative guardrail is still pending. If the guardrail later blocks, assistant/tool-step data may already be stored; skip or deferrecordStepResultsuntil there is no speculative guardrail or it has passed.Suggested direction
- if (conversationPersistence.mode === "step") { + const speculativeInputGuardrailPassed = + !speculativeInputGuardrail || speculativeInputGuardrail.hasPassed(); + + if (conversationPersistence.mode === "step" && speculativeInputGuardrailPassed) { await this.recordStepResults(undefined, oc, { awaitPersistence: shouldFlushStepPersistence, }); } @@ shouldPersistMemory && persistQueue && - (!speculativeInputGuardrail || speculativeInputGuardrail.hasPassed()) && + speculativeInputGuardrailPassed &&🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/agent.ts` around lines 7584 - 7610, The recordStepResults call is not gated by the speculative guardrail check, allowing step records to be persisted via memoryManager.saveConversationSteps even when a speculative guardrail is still pending. Add the same guardrail condition that protects persistQueue to also guard the recordStepResults method call within the conversationPersistence.mode === "step" block, ensuring that the condition (!speculativeInputGuardrail || speculativeInputGuardrail.hasPassed()) is satisfied before calling recordStepResults.
🧹 Nitpick comments (2)
packages/core/src/agent/guardrail.integration.spec.ts (1)
211-215: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert the stream is still gated before guardrail release.
Line 212 starts collection, but Line 215 only checks the final text after release; this can pass even if output leaks early. Add a pre-release unresolved assertion so the “holdUntilPass” behavior is actually verified.
♻️ Proposed test hardening
const streamResult = await agent.streamText("hello"); const textPromise = collectTextStream(streamResult.textStream); await guardrailStarted; + const settledBeforeRelease = await Promise.race([ + textPromise.then(() => true), + Promise.resolve(false), + ]); + expect(settledBeforeRelease).toBe(false); releaseGuardrail(); await expect(textPromise).resolves.toBe("guarded output");🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/guardrail.integration.spec.ts` around lines 211 - 215, The test for guardrail stream gating is incomplete because it only verifies the final output after the guardrail is released, not that the stream was actually blocked before release. Add an assertion between the await guardrailStarted statement and the releaseGuardrail() call to verify that the textPromise has not yet resolved, which confirms the stream is being properly gated and will only proceed after guardrail release. This ensures the "holdUntilPass" behavior is actually tested rather than just checking the eventual result.packages/core/src/agent/agent.ts (1)
437-440: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAvoid
any[]for UI replacement chunks.This factory builds public UI stream chunks, but
any[]hides invalid chunk shapes from the compiler. Give the replacement chunk structure a typed alias or make the function generic over the UI stream chunk type instead of casting later. As per coding guidelines,**/*.ts: Maintain type safety in TypeScript-first codebase.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/agent.ts` around lines 437 - 440, Replace the `any[]` return type in the `createInputGuardrailBlockedUIStreamChunks` function with a properly typed return type. Either create a typed alias for the UI stream chunk structure that this function returns, or make the function generic over the UI stream chunk type parameter. This ensures type safety for the stream chunks being built and eliminates the need for casting elsewhere in the codebase.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 2081-2093: A parallel guardrail created in the
SpeculativeInputGuardrailRun can block before streamText finishes its setup,
causing the abort signal to reject the model startup and probing operations
prematurely. Add a blocked-decision check immediately before the model startup
and probing logic begins, and if a block decision already exists from the
guardrail, return the replacement stream result from the buffer instead of
allowing the abort to propagate and reject the call. This ensures that
fast-blocking guardrails are handled before the gated replacement streams are
returned, preventing setup failures.
- Line 6779: The waitForSpeculativeInputGuardrail method throws
guardrail-specific errors that should not be processed as generic tool errors
through handleToolError, which converts them to tool error results. Wrap the
await this.waitForSpeculativeInputGuardrail(oc) calls (at lines 6779 and 6840)
in a try-catch block that identifies and rethrows guardrail block/config errors
before they are caught by the outer exception handler that calls
handleToolError, ensuring guardrail errors bypass the tool error conversion
flow.
- Around line 327-329: The abort call on the abortController in the
decision.error handling block triggers the normal cancellation path through
setupAbortSignalListener which fires onEnd hooks with a cancellation error. This
conflicts with guardrail blocks which should return early without firing final
hooks. Modify the abort logic to avoid triggering the onEnd hooks for guardrail
blocks, either by conditionally skipping the abort call when handling guardrail
errors or by using an alternative mechanism that prevents
setupAbortSignalListener from executing the onEnd hooks in the guardrail block
scenario.
- Around line 375-381: When handling a source-error result type in the code
block checking result.type === "source-error", ensure the guardrail decision is
fully resolved before throwing the error. Currently, getDecision() may return
undefined if the decision is still pending. Add logic to await the guardrail
decision completion if it hasn't been resolved yet, then check if the resolved
decision status is "blocked" to yield the replacement message, otherwise throw
the result.error. This prevents premature error throwing while a guardrail block
decision is still being determined.
- Around line 282-295: The catch block in the input guardrails error handling is
treating all errors as blocked guardrail decisions by unconditionally setting
status to "blocked", which masks configuration or runtime errors like
GUARDRAIL_INPUT_MODIFY_UNSUPPORTED that should be raised to the caller.
Differentiate between actual guardrail blocks (errors with code
"GUARDRAIL_INPUT_BLOCKED") and configuration/runtime errors: only convert
guardrail block errors to a decision with status "blocked", while re-throwing or
propagating configuration errors like GUARDRAIL_INPUT_MODIFY_UNSUPPORTED so they
are not masked as blocked text replacement.
In `@packages/core/src/agent/guardrail.ts`:
- Around line 374-386: The guard against modifying input in parallel mode only
checks when modifiedInput is defined, but a guardrail can still return action:
"modify" without providing modifiedInput and bypass this restriction. In the
guardrail execution block where you check options.allowModify === false, also
verify that the resolvedDecision does not have action: "modify" set. Update the
condition to reject the guardrail execution whenever action is "modify" in
parallel mode, regardless of whether modifiedInput is present or omitted.
---
Outside diff comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 7584-7610: The recordStepResults call is not gated by the
speculative guardrail check, allowing step records to be persisted via
memoryManager.saveConversationSteps even when a speculative guardrail is still
pending. Add the same guardrail condition that protects persistQueue to also
guard the recordStepResults method call within the conversationPersistence.mode
=== "step" block, ensuring that the condition (!speculativeInputGuardrail ||
speculativeInputGuardrail.hasPassed()) is satisfied before calling
recordStepResults.
---
Nitpick comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 437-440: Replace the `any[]` return type in the
`createInputGuardrailBlockedUIStreamChunks` function with a properly typed
return type. Either create a typed alias for the UI stream chunk structure that
this function returns, or make the function generic over the UI stream chunk
type parameter. This ensures type safety for the stream chunks being built and
eliminates the need for casting elsewhere in the codebase.
In `@packages/core/src/agent/guardrail.integration.spec.ts`:
- Around line 211-215: The test for guardrail stream gating is incomplete
because it only verifies the final output after the guardrail is released, not
that the stream was actually blocked before release. Add an assertion between
the await guardrailStarted statement and the releaseGuardrail() call to verify
that the textPromise has not yet resolved, which confirms the stream is being
properly gated and will only proceed after guardrail release. This ensures the
"holdUntilPass" behavior is actually tested rather than just checking the
eventual result.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e4fb843d-e4c9-4903-b800-d44c33f59b6c
📒 Files selected for processing (9)
.changeset/parallel-input-guardrails.mdpackages/core/src/agent/agent.tspackages/core/src/agent/conversation-buffer.spec.tspackages/core/src/agent/conversation-buffer.tspackages/core/src/agent/guardrail.integration.spec.tspackages/core/src/agent/guardrail.spec.tspackages/core/src/agent/guardrail.tspackages/core/src/agent/types.tswebsite/docs/guardrails/overview.md
There was a problem hiding this comment.
7 issues found across 9 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/guardrail.ts">
<violation number="1" location="packages/core/src/agent/guardrail.ts:374">
P2: The `allowModify === false` check is only reached when `resolvedDecision.modifiedInput !== undefined`. A guardrail returning `action: "modify"` without providing `modifiedInput` will bypass this validation and silently pass in parallel mode, violating the contract that parallel guardrails can only allow or block. Move the `allowModify` check to trigger on `action === "modify"` regardless of whether `modifiedInput` is set.</violation>
</file>
<file name="packages/core/src/agent/agent.ts">
<violation number="1" location="packages/core/src/agent/agent.ts:282">
P2: All errors thrown by `executeInputGuardrails` are caught and converted to a `blocked` decision. This includes configuration errors like `GUARDRAIL_INPUT_MODIFY_UNSUPPORTED` (thrown when a parallel guardrail returns `action: "modify"`). Instead of surfacing the configuration error to the developer, it will be silently shown to the user as blocked stream text. Distinguish configuration/runtime errors from intentional guardrail blocks and rethrow non-block errors.</violation>
<violation number="2" location="packages/core/src/agent/agent.ts:328">
P2: Aborting the operation context controller here fires the normal cancellation path via `setupAbortSignalListener`, which calls `onEnd` with a cancellation error. This conflicts with the blocked-stream path that intentionally returns early to skip final hooks. The guardrail block abort should be distinguishable from user cancellation to avoid spurious `onEnd` hook invocations.</violation>
<violation number="3" location="packages/core/src/agent/agent.ts:6779">
P2: `waitForSpeculativeInputGuardrail` throws the guardrail block error, but it's inside a `try` block whose `catch` routes all errors through `handleToolError`. This will invoke tool error hooks and emit a tool error result for what is actually a guardrail block. The guardrail error should be rethrown (or the catch should detect it) before the generic tool-error handling path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/core/src/agent/agent.ts (3)
1886-1888: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winClear already-created feedback state on block.
resolveFeedbackDeferred(null)is a no-op once feedback metadata has already resolved, leavingfeedbackValue/feedbackMetadataValueavailable for a blocked stream. Clear those cached values inonBlockbefore resolving.Proposed fix
buffer, onBlock: () => { + feedbackMetadataValue = null; + feedbackValue = null; resolveFeedbackDeferred(null); }, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/agent.ts` around lines 1886 - 1888, In the onBlock callback handler, before calling resolveFeedbackDeferred(null), clear the cached feedback state variables feedbackValue and feedbackMetadataValue by setting them to null or undefined. This ensures that when the feedback deferred has already been resolved, any previously cached feedback metadata values are properly cleared and not left available for blocked streams.
1878-1890: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy liftDefer memory writes until the parallel guardrail passes.
The speculative guardrail starts here, but
prepareExecutionruns immediately afterward and can still write the current input through the semantic-memoryqueueSaveInputpath and the regularprepareConversationContext(..., { persistInput: true })path before the guardrail decision. A later buffer checkpoint restore cannot undo those external memory writes, so blocked inputs can still be persisted.Please thread the speculative decision into message preparation and disable/defer input persistence while the guardrail is pending, then persist only after
hasPassed()/wait()resolves withpassed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/agent.ts` around lines 1878 - 1890, The issue is that SpeculativeInputGuardrailRun is created and stored in the operation context, but input persistence through the queueSaveInput path and prepareConversationContext with persistInput: true flag can still occur before the speculative guardrail has completed its check. To fix this, you need to defer all input memory writes until after the guardrail decision is resolved. Thread the speculativeInputGuardrail decision into the message preparation logic by checking the guardrail status before executing persistence operations, and ensure that queueSaveInput and persistInput: true are only called after the guardrail has passed by checking hasPassed() or waiting for wait() to resolve with a passed result. This ensures blocked inputs are never persisted to semantic memory.
7336-7362: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winGate step-history persistence before
recordStepResultswrites.The new
hasPassed()check only guardspersistQueue, butrecordStepResultsabove it can already save step text/tool calls/tool results while the speculative guardrail is pending. If the guardrail later blocks, generated output has still been persisted.Proposed fix
return async (event: StepResult<ToolSet>) => { const speculativeInputGuardrail = this.getSpeculativeInputGuardrail(oc); + const canPersistSpeculativeStep = + !speculativeInputGuardrail || speculativeInputGuardrail.hasPassed(); const { shouldFlushForToolCompletion, bailedResult } = this.processStepContent(oc, event); @@ - if (conversationPersistence.mode === "step") { + if (conversationPersistence.mode === "step" && canPersistSpeculativeStep) { await this.recordStepResults(undefined, oc, { awaitPersistence: shouldFlushStepPersistence, }); } @@ shouldPersistMemory && persistQueue && - (!speculativeInputGuardrail || speculativeInputGuardrail.hasPassed()) && + canPersistSpeculativeStep && conversationPersistence.mode === "step" && (hasResponseMessages || shouldFlushStepPersistence) ) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/agent.ts` around lines 7336 - 7362, The recordStepResults method call in the block checking if conversationPersistence.mode === "step" is not being gated by the speculativeInputGuardrail check, allowing step data to be persisted before the guardrail completes its validation. Move the entire recordStepResults call block (the if statement checking conversationPersistence.mode === "step") inside a condition that also checks either no speculativeInputGuardrail exists or speculativeInputGuardrail.hasPassed() returns true, ensuring no step history is persisted if the guardrail later blocks the request.
🧹 Nitpick comments (5)
packages/core/src/agent/streaming/input-guardrail-stream.ts (3)
250-265: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTighten the UI-chunk builder return type.
createInputGuardrailBlockedUIStreamChunksreturnsany[], which the callsite then re-casts (as UIStreamChunk[]). Typing the elements (e.g. the UI message stream chunk union) would preserve type safety and remove the downstream cast.As per coding guidelines: "Maintain type safety in TypeScript-first codebase".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/streaming/input-guardrail-stream.ts` around lines 250 - 265, The createInputGuardrailBlockedUIStreamChunks function currently returns any[] which requires downstream casting at call sites, reducing type safety. Change the return type from any[] to UIStreamChunk[] (or the appropriate UI stream chunk union type) to properly type the returned array of stream chunk objects. This will eliminate the need for downstream as UIStreamChunk[] casts and maintain type safety throughout the codebase.Source: Coding guidelines
157-223: 🚀 Performance & Scalability | 🔵 TrivialGating buffers the entire source with no backpressure.
Within a single consumer pull, the
while (true)loop keeps callingiterator.next()and pushing intobufferedwithout yielding until the guardrail resolves. For a fast model stream paired with a slow (e.g. LLM-based)holdUntilPassguardrail, this drains and holds the full response in memory, and applies no backpressure to the source. This is acceptable for typical short responses but can cause memory pressure for large/long-running streams.Consider bounding the buffer (cap size / abort + replace when exceeded) or documenting the upper bound so operators understand the memory envelope under slow guardrails.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/streaming/input-guardrail-stream.ts` around lines 157 - 223, The gateIterableUntilSpeculativeInputPass function buffers all items from the source iterator into the buffered array without any size limit or backpressure mechanism. When the guardrail is slow to resolve (like a holdUntilPass guardrail), the while true loop continuously calls iterator.next() and pushes items into the buffered array, causing all data to be held in memory. Fix this by implementing a buffer size limit in the gateIterableUntilSpeculativeInputPass function: add a configurable maximum buffer size parameter, check the buffered array length before pushing new items, and when the limit is reached, either pause consuming from the iterator until items are yielded, or abort the operation and use the replacement function. Alternatively, if bounding is not feasible, add clear documentation about the memory behavior and upper bound expectations for operators using this function with slow guardrails.
116-122: 🩺 Stability & Availability | 🔵 TrivialFunction signature misleads about input type; however, actual callers always provide
AsyncIterableStream.The function accepts
AsyncIterable<string>but both call sites inagent.ts(lines 2631, 2636) passAsyncIterableStream<string>:guardrailPipeline.textStream(explicitlyAsyncIterableStream<string>perGuardrailPipelineinterface) andresult.textStream(fromstreamTextin the "ai" SDK). The cast at line 121 is therefore safe in practice since the input is already the correct type.However, the function signature doesn't reflect actual usage. Consider either:
- Changing the parameter type to
AsyncIterableStream<string>to match reality, or- Adding runtime wrapping via
iterableToStreamfor any plainAsyncIterable<string>inputs to maintain the current signature's generality.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/streaming/input-guardrail-stream.ts` around lines 116 - 122, The function applySpeculativeInputGuardrailToTextStream accepts AsyncIterable<string> in its params parameter but all actual callers pass AsyncIterableStream<string>. Update the baseStream property type in the params object from AsyncIterable<string> to AsyncIterableStream<string> to accurately reflect the actual usage pattern and eliminate the misleading type signature. This makes the function contract honest about what it expects to receive from its callers.packages/core/src/agent/guardrail.integration.spec.ts (1)
345-345: 📐 Maintainability & Code Quality | 🔵 TrivialRemove explicit
<any>type parameter to preserve type inference.Line 345 uses
collectStream<any>(...)which bypasses TypeScript's type checking. Removing the explicit type parameter allows the compiler to infer the correct type fromuiStreamResult.toUIMessageStream(), maintaining type safety without reducing functionality.Suggested fix
- const uiChunks = await collectStream<any>(uiStreamResult.toUIMessageStream()); + const uiChunks = await collectStream(uiStreamResult.toUIMessageStream());🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/guardrail.integration.spec.ts` at line 345, The collectStream function call at line 345 uses an explicit `<any>` type parameter which bypasses TypeScript's type checking and prevents proper type inference. Remove the `<any>` type parameter from the collectStream call so that TypeScript can automatically infer the correct type from the uiStreamResult.toUIMessageStream() return type. This change maintains functionality while improving type safety by allowing the compiler to determine the appropriate type for uiChunks instead of accepting any type.Source: Coding guidelines
packages/core/src/agent/agent.spec.ts (1)
1673-1688: 📐 Maintainability & Code Quality | 🔵 TrivialRemove
as anycasts from the new speculative guardrail tests.Lines 1673, 1682, 1687, 1709, 1719, and 1728 use
as any, which weakens type safety. For accessing private Agent methods, use a narrow test-only typed helper. For the model, prefer a concrete type assertion. This aligns with the TypeScript-first guideline to maintain type safety in**/*.tsfiles.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/agent.spec.ts` around lines 1673 - 1688, Remove the `as any` type casts used throughout the speculative guardrail tests (appearing in the mockModel assignment, and on calls to createOperationContext and createToolExecutionFactory) by creating a narrow test-only typed helper for accessing private Agent methods and using concrete type assertions for the mockModel instead of broad `as any` casts. This maintains type safety in the test file and aligns with TypeScript-first guidelines.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 2616-2656: The `partialOutputStream` is not being protected by the
speculative input guardrail like the other streams (fullStream, textStream, and
UIStream). Create a new wrapper function
`getGuardrailAwarePartialOutputStream()` following the same pattern as
`getGuardrailAwareFullStream`, `getGuardrailAwareTextStream`, and
`getGuardrailAwareUIStream` that applies the speculative input guardrail to the
partial output stream, checking if guardrailPipeline exists and applying the
guardrail appropriately. Then update the `partialOutputStream` getter to return
the result of `getGuardrailAwarePartialOutputStream()` instead of delegating
directly.
---
Outside diff comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 1886-1888: In the onBlock callback handler, before calling
resolveFeedbackDeferred(null), clear the cached feedback state variables
feedbackValue and feedbackMetadataValue by setting them to null or undefined.
This ensures that when the feedback deferred has already been resolved, any
previously cached feedback metadata values are properly cleared and not left
available for blocked streams.
- Around line 1878-1890: The issue is that SpeculativeInputGuardrailRun is
created and stored in the operation context, but input persistence through the
queueSaveInput path and prepareConversationContext with persistInput: true flag
can still occur before the speculative guardrail has completed its check. To fix
this, you need to defer all input memory writes until after the guardrail
decision is resolved. Thread the speculativeInputGuardrail decision into the
message preparation logic by checking the guardrail status before executing
persistence operations, and ensure that queueSaveInput and persistInput: true
are only called after the guardrail has passed by checking hasPassed() or
waiting for wait() to resolve with a passed result. This ensures blocked inputs
are never persisted to semantic memory.
- Around line 7336-7362: The recordStepResults method call in the block checking
if conversationPersistence.mode === "step" is not being gated by the
speculativeInputGuardrail check, allowing step data to be persisted before the
guardrail completes its validation. Move the entire recordStepResults call block
(the if statement checking conversationPersistence.mode === "step") inside a
condition that also checks either no speculativeInputGuardrail exists or
speculativeInputGuardrail.hasPassed() returns true, ensuring no step history is
persisted if the guardrail later blocks the request.
---
Nitpick comments:
In `@packages/core/src/agent/agent.spec.ts`:
- Around line 1673-1688: Remove the `as any` type casts used throughout the
speculative guardrail tests (appearing in the mockModel assignment, and on calls
to createOperationContext and createToolExecutionFactory) by creating a narrow
test-only typed helper for accessing private Agent methods and using concrete
type assertions for the mockModel instead of broad `as any` casts. This
maintains type safety in the test file and aligns with TypeScript-first
guidelines.
In `@packages/core/src/agent/guardrail.integration.spec.ts`:
- Line 345: The collectStream function call at line 345 uses an explicit `<any>`
type parameter which bypasses TypeScript's type checking and prevents proper
type inference. Remove the `<any>` type parameter from the collectStream call so
that TypeScript can automatically infer the correct type from the
uiStreamResult.toUIMessageStream() return type. This change maintains
functionality while improving type safety by allowing the compiler to determine
the appropriate type for uiChunks instead of accepting any type.
In `@packages/core/src/agent/streaming/input-guardrail-stream.ts`:
- Around line 250-265: The createInputGuardrailBlockedUIStreamChunks function
currently returns any[] which requires downstream casting at call sites,
reducing type safety. Change the return type from any[] to UIStreamChunk[] (or
the appropriate UI stream chunk union type) to properly type the returned array
of stream chunk objects. This will eliminate the need for downstream as
UIStreamChunk[] casts and maintain type safety throughout the codebase.
- Around line 157-223: The gateIterableUntilSpeculativeInputPass function
buffers all items from the source iterator into the buffered array without any
size limit or backpressure mechanism. When the guardrail is slow to resolve
(like a holdUntilPass guardrail), the while true loop continuously calls
iterator.next() and pushes items into the buffered array, causing all data to be
held in memory. Fix this by implementing a buffer size limit in the
gateIterableUntilSpeculativeInputPass function: add a configurable maximum
buffer size parameter, check the buffered array length before pushing new items,
and when the limit is reached, either pause consuming from the iterator until
items are yielded, or abort the operation and use the replacement function.
Alternatively, if bounding is not feasible, add clear documentation about the
memory behavior and upper bound expectations for operators using this function
with slow guardrails.
- Around line 116-122: The function applySpeculativeInputGuardrailToTextStream
accepts AsyncIterable<string> in its params parameter but all actual callers
pass AsyncIterableStream<string>. Update the baseStream property type in the
params object from AsyncIterable<string> to AsyncIterableStream<string> to
accurately reflect the actual usage pattern and eliminate the misleading type
signature. This makes the function contract honest about what it expects to
receive from its callers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9fb3ff74-88b4-45b8-8714-282fe98733ba
📒 Files selected for processing (7)
.changeset/parallel-input-guardrails.mdpackages/core/src/agent/agent.spec.tspackages/core/src/agent/agent.tspackages/core/src/agent/context-keys.tspackages/core/src/agent/guardrail.integration.spec.tspackages/core/src/agent/streaming/input-guardrail-stream.tswebsite/docs/guardrails/overview.md
✅ Files skipped from review due to trivial changes (1)
- .changeset/parallel-input-guardrails.md
🚧 Files skipped from review as they are similar to previous changes (1)
- website/docs/guardrails/overview.md
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/core/src/agent/streaming/input-guardrail-stream.ts (1)
243-259: 🎯 Functional Correctness | 🟠 MajorRemove the non-standard
deltafield from text-delta and add requiredrawFinishReasonandtotalUsageto the finish part to match ai SDK v6.0.0 TextStreamPart shape.These objects flow into
fullStreamand are cast toVoltAgentTextStreamPart(which extends ai'sTextStreamPart). In ai@6.0.0:
text-deltaexpects only{ type, id, text }— thedeltafield is not recognized and will be ignored by consumers.finishrequires{ type, finishReason, rawFinishReason, totalUsage }— bothrawFinishReasonandtotalUsageare missing. Consumers computing token usage on this error path will break or return incomplete data.Current shape (incorrect)
{ type: "text-delta", id: textId, delta: data.message, // ← not in ai@6.0.0 TextStreamPart text: data.message, } { type: "finish", finishReason: "error", // ← missing rawFinishReason and totalUsage }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/streaming/input-guardrail-stream.ts` around lines 243 - 259, In the stream parts returned in the input guardrail blocking logic around the text-delta and finish objects, remove the non-standard delta field from the text-delta part (keeping only type, id, and text fields), and add the required rawFinishReason and totalUsage fields to the finish part object to align with ai SDK v6.0.0 TextStreamPart shape expectations. These fields are necessary for consumers that depend on complete token usage and finish reason information.
🧹 Nitpick comments (1)
packages/core/src/agent/streaming/input-guardrail-stream.ts (1)
263-282: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTighten the
any[]return type.
createInputGuardrailBlockedUIStreamChunksreturnsany[], which erases all type checking on the emitted UI chunks and on theINPUT_GUARDRAIL_BLOCKED_UI_EVENT_TYPEpayload. Consider a discriminated union (e.g. aUIStreamChunktype) so the start/event/text/finish chunk shapes are validated at compile time.As per coding guidelines: "Maintain type safety in TypeScript-first codebase".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/core/src/agent/streaming/input-guardrail-stream.ts` around lines 263 - 282, The function createInputGuardrailBlockedUIStreamChunks currently returns any[] which eliminates type safety for the emitted UI chunks. Define a discriminated union type (such as UIStreamChunk) that explicitly describes the shape of each chunk type in the array (start, event, text-start, text-delta, text-end, and finish), capturing the specific properties and payload shapes for each. Then replace the any[] return type annotation with this new discriminated union type or an array of it to ensure compile-time validation of all chunk shapes and the INPUT_GUARDRAIL_BLOCKED_UI_EVENT_TYPE payload structure.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/src/agent/subagent/types.ts`:
- Around line 195-199: The VoltAgentTextStreamPart type now includes
InputGuardrailBlockedStreamPart, but the default forwarding allowlist that
controls which stream events are propagated to the parent agent does not include
the new "input-guardrail-blocked" type. Locate the default forwarding allowlist
(which currently contains only "tool-call" and "tool-result") and add
"input-guardrail-blocked" to this allowlist to ensure guardrail-blocked events
are properly forwarded instead of being silently dropped during subagent stream
forwarding.
---
Outside diff comments:
In `@packages/core/src/agent/streaming/input-guardrail-stream.ts`:
- Around line 243-259: In the stream parts returned in the input guardrail
blocking logic around the text-delta and finish objects, remove the non-standard
delta field from the text-delta part (keeping only type, id, and text fields),
and add the required rawFinishReason and totalUsage fields to the finish part
object to align with ai SDK v6.0.0 TextStreamPart shape expectations. These
fields are necessary for consumers that depend on complete token usage and
finish reason information.
---
Nitpick comments:
In `@packages/core/src/agent/streaming/input-guardrail-stream.ts`:
- Around line 263-282: The function createInputGuardrailBlockedUIStreamChunks
currently returns any[] which eliminates type safety for the emitted UI chunks.
Define a discriminated union type (such as UIStreamChunk) that explicitly
describes the shape of each chunk type in the array (start, event, text-start,
text-delta, text-end, and finish), capturing the specific properties and payload
shapes for each. Then replace the any[] return type annotation with this new
discriminated union type or an array of it to ensure compile-time validation of
all chunk shapes and the INPUT_GUARDRAIL_BLOCKED_UI_EVENT_TYPE payload
structure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2e642599-b59d-43b0-9a03-e9541682509e
📒 Files selected for processing (8)
.changeset/parallel-input-guardrails.mdpackages/core/src/agent/guardrail.integration.spec.tspackages/core/src/agent/guardrail.spec.tspackages/core/src/agent/guardrail.tspackages/core/src/agent/streaming/input-guardrail-stream.tspackages/core/src/agent/subagent/types.tspackages/core/src/index.tswebsite/docs/guardrails/overview.md
✅ Files skipped from review due to trivial changes (2)
- .changeset/parallel-input-guardrails.md
- website/docs/guardrails/overview.md
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/core/src/agent/guardrail.integration.spec.ts
- packages/core/src/agent/guardrail.ts
- packages/core/src/agent/guardrail.spec.ts
There was a problem hiding this comment.
1 issue found across 8 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
PR Checklist
Please check if your PR fulfills the following requirements:
Bugs / Features
What is the current behavior?
Input guardrails are fully blocking before
streamTextstarts. Slow async checks, including LLM-based policy checks, delay the first stream output. There is no stream-level support for starting those checks in parallel, stopping the stream if they block, overriding visible output, and keeping speculative assistant output out of conversation memory.What is the new behavior?
Adds parallel input guardrails for
streamTextthroughexecution: "parallel"andstreamPolicy: "holdUntilPass".Parallel input guardrails now start while the model stream starts, buffer full/text/UI stream output until the guardrails pass, and replace blocked streams with the guardrail message. Blocked runs restore the conversation buffer checkpoint, skip assistant memory persistence, and stop before final hooks/evals persist generated output.
Blocked parallel input guardrails emit an AI SDK-compatible structured frontend event before the replacement text:
data-input-guardrail-blockedwith{ code, reason, message, guardrailId?, guardrailName?, severity? }, souseChatconsumers can show localized UI without matching the fallback message string.UIMessage<unknown, { "input-guardrail-blocked": InputGuardrailBlockedEventData }>.finishReason: "error".fullStreamconsumers receive aninput-guardrail-blockedpart with the same payload.Parallel input guardrails can allow or block only. If a parallel guardrail returns
action: "modify", VoltAgent raises a configuration error; input modification remains supported by the default blocking mode.The speculative input guardrail runtime now lives in
agent/streaming/input-guardrail-stream.ts, keepingagent.tsfocused on orchestration.Tool execution waits for pending parallel input guardrails before running side-effectful server-side tool handlers.
fixes (issue)
N/A
Notes for reviewers
Validation run:
pnpm --filter @voltagent/core typecheckpnpm --filter @voltagent/core test:single src/agent/agent.spec.ts src/agent/streaming/guardrail-stream.spec.ts src/agent/streaming/output-guardrail-stream-runner.spec.ts src/agent/memory-persist-queue.spec.ts src/agent/memory-persistence.integration.spec.ts src/agent/guardrail.integration.spec.ts src/agent/guardrail.spec.ts src/agent/conversation-buffer.spec.ts(176 tests)pnpm --filter @voltagent/core test:single src/agent/guardrail.integration.spec.ts src/agent/guardrail.spec.ts src/agent/streaming/guardrail-stream.spec.ts(39 tests after the AI SDK compatibility polish)pnpm --filter @voltagent/core lintlintexits successfully and still reports the existing cognitive complexity warnings inagent.ts,workflow/steps/and-agent.ts, andworkspace/filesystem/backends/filesystem.ts.Summary by CodeRabbit
streamText, including configurable execution/stream buffering behavior.modifyinputs now fail when modifications are disallowed.data-input-guardrail-blocked).