Conversation
🦋 Changeset detectedLatest commit: ec3bab9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 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 pre-execution validators for tools and workflows. Validators run (sync/async) before tool hooks or workflow steps and can block execution by returning Changes
Sequence Diagram(s)sequenceDiagram
rect rgba(220,230,241,0.5)
participant App as Application
participant Agent as Agent
participant Validator as Execution Validator
participant Tool as Tool
end
App->>Agent: request tool execution (args, executionValidators)
Agent->>Validator: validateToolExecution(context)
alt Validation Passes
Validator-->>Agent: void / pass
Agent->>Tool: run tool hooks → execute(args)
Tool-->>Agent: result
Agent-->>App: success response
else Validation Fails
Validator-->>Agent: { pass: false } / throws ExecutionValidationError
Agent-->>App: error response (message, code, httpStatus)
end
sequenceDiagram
rect rgba(241,230,220,0.5)
participant App as Application
participant Workflow as Workflow
participant Validator as Execution Validator
participant Step as Workflow Step
end
App->>Workflow: run(input, { executionValidators })
Workflow->>Validator: runExecutionValidators(context)
alt Validation Passes
Validator-->>Workflow: void / pass
Workflow->>Step: execute step(s)
Step-->>Workflow: results
Workflow-->>App: workflow output
else Validation Fails
Validator-->>Workflow: { pass: false } / throws ExecutionValidationError
Workflow-->>App: error response (message, code, httpStatus)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
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 docstrings
🧪 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 |
This comment has been minimized.
This comment has been minimized.
f5dbf49 to
ec3bab9
Compare
Deploying voltagent with
|
| Latest commit: |
ec3bab9
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://fad555f1.voltagent.pages.dev |
| Branch Preview URL: | https://feat-execution-validators.voltagent.pages.dev |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 925-927: The aiSDKOptions object being passed into the provider
calls mistakenly includes the VoltAgent-specific executionValidators field, so
update the option destructuring in the places that call generateText,
streamText, generateObject, and streamObject (including the aiSDKOptions
construction around functions/methods where aiSDKOptions is built) to explicitly
exclude executionValidators (e.g., const { executionValidators, ...aiSDKOptions
} = options) before forwarding; apply this same destructuring pattern at the
other listed sites (around the calls referenced by symbols generateText,
streamText, generateObject, streamObject) so validator functions are stripped
out of the options passed to the AI SDK.
- Around line 1183-1186: The method currently resolves validators from options
or this.executionValidators but ignores operationContext.executionValidators;
update the resolver to prefer operationContext?.executionValidators first, then
options?.executionValidators, then this.executionValidators so validators merged
by createOperationContext are honored; locate the code around the validators
assignment (the const validators = ... line) and change the lookup order to
check operationContext.executionValidators before falling back to options and
agent-level executionValidators, and keep the existing empty-array/length check
behavior.
In `@packages/core/src/workflow/core.ts`:
- Around line 1222-1242: Validators are currently run inside executeInternal
after startAsync has already set the workflow state to "running", causing
validation failures to appear only as async background errors; extract the
validator merge and run logic (the executionValidators array construction and
the runExecutionValidators call) into a new helper (e.g., runWorkflowValidators
or validateBeforeStart) and call that helper from startAsync before it calls
setWorkflowState/runs the "running" commit; then have executeInternal accept a
flag/param (or remove its own validation call) to skip re-running the same
validators when startAsync already validated, ensuring pre-start validation
blocks creation of a running execution record.
In `@packages/server-core/src/handlers/workflow.handlers.ts`:
- Around line 562-566: The consumeWorkflowStream path drops ClientHTTPError
details when the async validation fails later (via streamExecution.result) —
update consumeWorkflowStream/wherever the workflow.stream result is consumed to
await or attach a .catch handler on streamExecution.result and, if it rejects
with a ClientHTTPError, preserve and propagate its properties (code, name,
httpStatus) into the broadcast payload instead of only broadcasting the message;
use the existing mapClientHTTPError function or copy its mapped fields so the
downstream consumer receives the same structured error as the synchronous catch
that checks "if (error instanceof ClientHTTPError)" and calls
mapClientHTTPError, ensuring consistency between initial try/catch and
post-stream rejection handling.
In `@website/docs/workflows/overview.md`:
- Around line 598-603: The example calls await workflow.run with tenant-a which
will be rejected because tenant-a is not in the allowedTenants map; update the
docs example to explicitly handle the expected validation rejection by wrapping
the call to workflow.run (the workflow.run invocation using context/new Map with
allowedTenants) in a try/catch and logging/returning the error in the catch so
the documented behavior is explicit rather than causing an unhandled rejection.
🪄 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: e4be4d40-39bd-430b-af54-f6dcf54f7f18
📒 Files selected for processing (17)
.changeset/execution-validators.mdpackages/core/src/agent/agent.spec.tspackages/core/src/agent/agent.tspackages/core/src/agent/errors/client-http-errors.tspackages/core/src/agent/errors/index.tspackages/core/src/agent/types.tspackages/core/src/execution-validation.tspackages/core/src/index.tspackages/core/src/workflow/core.spec.tspackages/core/src/workflow/core.tspackages/core/src/workflow/types.tspackages/server-core/src/handlers/tool.handlers.spec.tspackages/server-core/src/handlers/tool.handlers.tspackages/server-core/src/handlers/workflow.handlers.spec.tspackages/server-core/src/handlers/workflow.handlers.tswebsite/docs/agents/tools.mdwebsite/docs/workflows/overview.md
| // Execution validators (can add per-call validators) | ||
| executionValidators?: AgentExecutionValidators; | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Strip executionValidators before forwarding options to the AI SDK.
executionValidators is a VoltAgent-only option, but it remains in aiSDKOptions and is forwarded into generateText, streamText, generateObject, and streamObject. That can leak validator functions into provider/AI SDK call options and create hard-to-debug behavior.
Proposed fix
parentOperationContext,
hooks,
+ executionValidators: _executionValidators,
feedback: _feedback,
maxSteps: userMaxSteps,Apply the same destructuring exclusion in the streamText, generateObject, and streamObject option destructures.
Also applies to: 1337-1356, 1956-1976, 2860-2879, 3234-3254
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/src/agent/agent.ts` around lines 925 - 927, The aiSDKOptions
object being passed into the provider calls mistakenly includes the
VoltAgent-specific executionValidators field, so update the option destructuring
in the places that call generateText, streamText, generateObject, and
streamObject (including the aiSDKOptions construction around functions/methods
where aiSDKOptions is built) to explicitly exclude executionValidators (e.g.,
const { executionValidators, ...aiSDKOptions } = options) before forwarding;
apply this same destructuring pattern at the other listed sites (around the
calls referenced by symbols generateText, streamText, generateObject,
streamObject) so validator functions are stripped out of the options passed to
the AI SDK.
| const validators = options?.executionValidators?.tools ?? this.executionValidators?.tools; | ||
| if (!validators || validators.length === 0) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
Resolve validators from the operation context first.
createOperationContext already merges parent, agent, and per-call validators into OperationContext.executionValidators, but this method ignores operationContext?.executionValidators. Direct calls that pass an operation context can silently skip operation-scoped validators; direct calls with options.executionValidators also skip agent-level validators.
Proposed fix
- const validators = options?.executionValidators?.tools ?? this.executionValidators?.tools;
+ const validators =
+ operationContext?.executionValidators?.tools ??
+ mergeAgentExecutionValidators(this.executionValidators, options?.executionValidators)?.tools;
if (!validators || validators.length === 0) {
return;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/src/agent/agent.ts` around lines 1183 - 1186, The method
currently resolves validators from options or this.executionValidators but
ignores operationContext.executionValidators; update the resolver to prefer
operationContext?.executionValidators first, then options?.executionValidators,
then this.executionValidators so validators merged by createOperationContext are
honored; locate the code around the validators assignment (the const validators
= ... line) and change the lookup order to check
operationContext.executionValidators before falling back to options and
agent-level executionValidators, and keep the existing empty-array/length check
behavior.
| const executionValidators = [ | ||
| ...(workflowExecutionValidators ?? []), | ||
| ...(options?.executionValidators ?? []), | ||
| ]; | ||
| await runExecutionValidators( | ||
| executionValidators, | ||
| { | ||
| type: "workflow", | ||
| workflowId: id, | ||
| workflowName: name, | ||
| input, | ||
| options, | ||
| executionId, | ||
| context: contextMap, | ||
| workflowState: workflowStateStore, | ||
| timestamp: new Date(), | ||
| logger: runLogger, | ||
| }, | ||
| `Workflow ${id} execution blocked by validation.`, | ||
| "WORKFLOW_VALIDATION_FAILED", | ||
| ); |
There was a problem hiding this comment.
Run validators before startAsync commits workflow state.
startAsync writes a running workflow state before calling executeInternal, so a validation denial still creates an execution record and only becomes an async background failure. That weakens the “pre-execution” boundary for async starts.
Consider extracting this validator merge/run into a helper that startAsync can call before setWorkflowState, then skip the duplicate validation when it delegates to executeInternal.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/core/src/workflow/core.ts` around lines 1222 - 1242, Validators are
currently run inside executeInternal after startAsync has already set the
workflow state to "running", causing validation failures to appear only as async
background errors; extract the validator merge and run logic (the
executionValidators array construction and the runExecutionValidators call) into
a new helper (e.g., runWorkflowValidators or validateBeforeStart) and call that
helper from startAsync before it calls setWorkflowState/runs the "running"
commit; then have executeInternal accept a flag/param (or remove its own
validation call) to skip re-running the same validators when startAsync already
validated, ensuring pre-start validation blocks creation of a running execution
record.
| } catch (error) { | ||
| logger.error("Failed to initiate workflow stream", { error }); | ||
| if (error instanceof ClientHTTPError) { | ||
| return mapClientHTTPError(error); | ||
| } |
There was a problem hiding this comment.
Preserve ClientHTTPError details for asynchronous workflow streams too.
This catch only handles errors thrown while initiating the stream. For workflow.stream(...), validator failures can reject later via streamExecution.result, so consumeWorkflowStream currently broadcasts only the message and drops code, name, and httpStatus.
Proposed fix
} catch (error) {
logger.error("Failed during workflow stream:", { error });
@@
- broadcastWorkflowStreamEvent(session, {
- type: "error",
- error: error instanceof Error ? error.message : "Stream failed",
- });
+ broadcastWorkflowStreamEvent(
+ session,
+ error instanceof ClientHTTPError
+ ? {
+ type: "error",
+ error: error.message,
+ code: error.code,
+ name: error.name,
+ httpStatus: error.httpStatus,
+ }
+ : {
+ type: "error",
+ error: error instanceof Error ? error.message : "Stream failed",
+ },
+ );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/server-core/src/handlers/workflow.handlers.ts` around lines 562 -
566, The consumeWorkflowStream path drops ClientHTTPError details when the async
validation fails later (via streamExecution.result) — update
consumeWorkflowStream/wherever the workflow.stream result is consumed to await
or attach a .catch handler on streamExecution.result and, if it rejects with a
ClientHTTPError, preserve and propagate its properties (code, name, httpStatus)
into the broadcast payload instead of only broadcasting the message; use the
existing mapClientHTTPError function or copy its mapped fields so the downstream
consumer receives the same structured error as the synchronous catch that checks
"if (error instanceof ClientHTTPError)" and calls mapClientHTTPError, ensuring
consistency between initial try/catch and post-stream rejection handling.
| await workflow.run( | ||
| { tenantId: "tenant-a" }, | ||
| { | ||
| context: new Map([["allowedTenants", ["tenant-b"]]]), | ||
| } | ||
| ); |
There was a problem hiding this comment.
Handle the expected validation rejection in the docs example.
This run is denied because tenant-a is not in allowedTenants; wrapping it makes the documented behavior explicit instead of ending in an unhandled rejection.
📝 Proposed docs adjustment
-await workflow.run(
- { tenantId: "tenant-a" },
- {
- context: new Map([["allowedTenants", ["tenant-b"]]]),
- }
-);
+try {
+ await workflow.run(
+ { tenantId: "tenant-a" },
+ {
+ context: new Map([["allowedTenants", ["tenant-b"]]]),
+ }
+ );
+} catch (error) {
+ console.error("Workflow blocked by execution validator:", error);
+}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await workflow.run( | |
| { tenantId: "tenant-a" }, | |
| { | |
| context: new Map([["allowedTenants", ["tenant-b"]]]), | |
| } | |
| ); | |
| try { | |
| await workflow.run( | |
| { tenantId: "tenant-a" }, | |
| { | |
| context: new Map([["allowedTenants", ["tenant-b"]]]), | |
| } | |
| ); | |
| } catch (error) { | |
| console.error("Workflow blocked by execution validator:", error); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@website/docs/workflows/overview.md` around lines 598 - 603, The example calls
await workflow.run with tenant-a which will be rejected because tenant-a is not
in the allowedTenants map; update the docs example to explicitly handle the
expected validation rejection by wrapping the call to workflow.run (the
workflow.run invocation using context/new Map with allowedTenants) in a
try/catch and logging/returning the error in the catch so the documented
behavior is explicit rather than causing an unhandled rejection.
There was a problem hiding this comment.
1 issue found across 17 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/workflow/core.ts">
<violation number="1" location="packages/core/src/workflow/core.ts:1226">
P1: Validation failures can leave pre-created `startAsync` workflow state stuck as `running` because validators run before the `skipStateInit` state-update path.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| await runExecutionValidators( | ||
| executionValidators, | ||
| { | ||
| type: "workflow", | ||
| workflowId: id, | ||
| workflowName: name, | ||
| input, | ||
| options, | ||
| executionId, | ||
| context: contextMap, | ||
| workflowState: workflowStateStore, | ||
| timestamp: new Date(), | ||
| logger: runLogger, | ||
| }, | ||
| `Workflow ${id} execution blocked by validation.`, | ||
| "WORKFLOW_VALIDATION_FAILED", | ||
| ); |
There was a problem hiding this comment.
P1: Validation failures can leave pre-created startAsync workflow state stuck as running because validators run before the skipStateInit state-update path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/workflow/core.ts, line 1226:
<comment>Validation failures can leave pre-created `startAsync` workflow state stuck as `running` because validators run before the `skipStateInit` state-update path.</comment>
<file context>
@@ -1217,6 +1219,28 @@ export function createWorkflow<
+ ...(workflowExecutionValidators ?? []),
+ ...(options?.executionValidators ?? []),
+ ];
+ await runExecutionValidators(
+ executionValidators,
+ {
</file context>
| await runExecutionValidators( | |
| executionValidators, | |
| { | |
| type: "workflow", | |
| workflowId: id, | |
| workflowName: name, | |
| input, | |
| options, | |
| executionId, | |
| context: contextMap, | |
| workflowState: workflowStateStore, | |
| timestamp: new Date(), | |
| logger: runLogger, | |
| }, | |
| `Workflow ${id} execution blocked by validation.`, | |
| "WORKFLOW_VALIDATION_FAILED", | |
| ); | |
| try { | |
| await runExecutionValidators( | |
| executionValidators, | |
| { | |
| type: "workflow", | |
| workflowId: id, | |
| workflowName: name, | |
| input, | |
| options, | |
| executionId, | |
| context: contextMap, | |
| workflowState: workflowStateStore, | |
| timestamp: new Date(), | |
| logger: runLogger, | |
| }, | |
| `Workflow ${id} execution blocked by validation.`, | |
| "WORKFLOW_VALIDATION_FAILED", | |
| ); | |
| } catch (error) { | |
| if (options?.skipStateInit) { | |
| await executionMemory.updateWorkflowState(executionId, { | |
| status: "error", | |
| error, | |
| updatedAt: new Date(), | |
| }); | |
| } | |
| throw error; | |
| } |
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 (2)
packages/core/src/workflow/core.ts (1)
1113-1114:⚠️ Potential issue | 🟠 MajorValidate against the resumed workflow state.
For resumed runs, validators receive
workflowState: options?.workflowState ?? {}, but the actual runtime state is later replaced fromoptions.resumeFrom.checkpoint.workflowState. Validators that enforce tenant/permission state can inspect the wrong state on resume.Proposed fix
- const workflowStateStore = options?.workflowState ?? {}; + const workflowStateStore = + options?.resumeFrom?.checkpoint?.workflowState ?? options?.workflowState ?? {};Also applies to: 1226-1237, 1455-1466
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/workflow/core.ts` around lines 1113 - 1114, The validators are using workflowStateStore from const workflowStateStore = options?.workflowState ?? {}, which is wrong for resumed runs because the runtime replaces state from options.resumeFrom.checkpoint.workflowState; update places where validators are passed workflowStateStore (e.g., validator invocation sites around core.ts lines where validators are called near the const workflowStateStore declaration and the similar blocks around the other noted ranges) to instead derive the state from options.resumeFrom?.checkpoint?.workflowState when options.resumeFrom exists, falling back to options?.workflowState ?? {} otherwise—ensure all validator calls (the same symbol usages referenced) receive the resumed checkpoint state so tenant/permission checks validate against the actual runtime state.packages/core/src/agent/agent.ts (1)
6392-6406:⚠️ Potential issue | 🟠 MajorAvoid firing the tool end hook twice on failures.
Validation failures now flow through
handleToolError, so this duplicatedtool.hooks?.onEndcall can double-run side effects such as audit logging, cleanup, or metrics.Proposed fix
await tool.hooks?.onEnd?.({ tool, args, output: undefined, error: voltAgentError, options: executionOptions, }); - - await tool.hooks?.onEnd?.({ - tool, - args, - output: undefined, - error: voltAgentError, - options: executionOptions, - }); const onToolErrorResult = await hooks.onToolError?.({🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/agent/agent.ts` around lines 6392 - 6406, The duplicate invocation of tool.hooks?.onEnd with the same parameters causes side effects to run twice on failures; remove the redundant call so onEnd is awaited only once (keep a single await tool.hooks?.onEnd? invocation), or centralize the call to happen after handleToolError returns/throws to ensure validation failures handled by handleToolError do not cause a second onEnd invocation; update the block around tool.hooks?.onEnd, voltAgentError, args and executionOptions so only one onEnd is executed for a given failure path.
♻️ Duplicate comments (4)
packages/server-core/src/handlers/workflow.handlers.ts (1)
248-251:⚠️ Potential issue | 🟠 MajorPreserve
ClientHTTPErrorfields in streamed error events.
handleStreamWorkflownow mapsClientHTTPErroronly during stream creation, but validator failures can reject later throughsession.streamExecution.result. This catch still broadcasts onlymessage, droppingcode,name, andhttpStatus.Proposed fix
- broadcastWorkflowStreamEvent(session, { - type: "error", - error: error instanceof Error ? error.message : "Stream failed", - }); + broadcastWorkflowStreamEvent( + session, + error instanceof ClientHTTPError + ? { + type: "error", + error: error.message, + code: error.code, + name: error.name, + httpStatus: error.httpStatus, + } + : { + type: "error", + error: error instanceof Error ? error.message : "Stream failed", + }, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/server-core/src/handlers/workflow.handlers.ts` around lines 248 - 251, The catch block in handleStreamWorkflow currently calls broadcastWorkflowStreamEvent with only error.message, losing ClientHTTPError fields; update the catch to detect ClientHTTPError (or an error object present on session.streamExecution.result) and include its code, name, and httpStatus when calling broadcastWorkflowStreamEvent so the emitted event preserves all ClientHTTPError properties rather than only message; reference broadcastWorkflowStreamEvent, handleStreamWorkflow, ClientHTTPError, and session.streamExecution.result to locate and change the broadcast call accordingly.packages/core/src/workflow/core.ts (1)
1222-1242:⚠️ Potential issue | 🟠 MajorRun validators before
startAsynccommits a running state.
startAsyncpersists arunningexecution before delegating toexecuteInternal, where validation runs. A denied async start therefore returns success and leaves an execution record that later flips to error, instead of blocking pre-execution.Proposed direction
+ await validateWorkflowExecution(input, { + ...options, + executionId, + }); + await executionMemory.setWorkflowState(executionId, { id: executionId, workflowId: id, workflowName: name, status: "running",Then have
executeInternalskip the duplicate validation whenstartAsyncalready validated.Also applies to: 3124-3147
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/workflow/core.ts` around lines 1222 - 1242, Run the execution validators before persisting the running state in startAsync: move the executionValidators construction and the await runExecutionValidators(...) call to occur prior to the code that commits a running execution (i.e., before the logic that sets the execution record to "running" inside startAsync). Then update executeInternal to accept a flag (e.g., alreadyValidated or skipValidation) and short-circuit/skip calling runExecutionValidators when that flag is true to avoid duplicate validation; reference the existing symbols workflowExecutionValidators, options?.executionValidators, runExecutionValidators, startAsync, and executeInternal when making these changes.packages/core/src/agent/agent.ts (2)
1183-1186:⚠️ Potential issue | 🟠 MajorResolve validators from the effective operation context.
createOperationContext()already builds the merged validator set onOperationContext, but this resolver can skip it when callers passoperationContextseparately. That makes direct validation paths miss parent/agent/per-call validators.Proposed fix
- const validators = options?.executionValidators?.tools ?? this.executionValidators?.tools; + const validators = + operationContext?.executionValidators?.tools ?? + mergeAgentExecutionValidators(this.executionValidators, options?.executionValidators)?.tools; if (!validators || validators.length === 0) { return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/agent/agent.ts` around lines 1183 - 1186, The validator lookup is bypassing the merged validators on OperationContext; instead of using options?.executionValidators?.tools ?? this.executionValidators?.tools, fetch the effective validators from the resolved OperationContext created by createOperationContext() (or use operationContext if passed in) so parent/agent/per-call validators are honored — modify the code that sets the validators variable (the block referencing options?.executionValidators?.tools and this.executionValidators?.tools) to read from operationContext.executionValidators.tools (falling back to options and then this.executionValidators) so the merged set on OperationContext is used.
1337-1356:⚠️ Potential issue | 🟠 MajorStrip
executionValidatorsbefore forwarding AI SDK options.
executionValidatorsis a VoltAgent-only option, but it still lands inaiSDKOptionsand is forwarded togenerateText,streamText,generateObject, andstreamObject.Proposed fix pattern
parentAgentId, parentOperationContext, hooks, + executionValidators: _executionValidators, feedback: _feedback, maxSteps: userMaxSteps,Apply the same exclusion in all four option destructures.
Also applies to: 1956-1976, 2860-2879, 3234-3254
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/agent/agent.ts` around lines 1337 - 1356, The options destructuring in the agent methods is leaking the VoltAgent-only option executionValidators into aiSDKOptions so it gets forwarded to AI SDK calls; update the destructuring that builds aiSDKOptions (the blocks that currently collect ...aiSDKOptions in the methods handling generateText, streamText, generateObject, and streamObject) to explicitly extract and exclude executionValidators (e.g., add executionValidators to the left-hand list alongside context, _requestHeaders, _feedback, etc.) so executionValidators is not present in aiSDKOptions before calling generateText/streamText/generateObject/streamObject.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/server-core/src/handlers/tool.handlers.ts`:
- Around line 207-229: The current call to agent.validateToolExecution passes a
hard-coded empty messages array which prevents validators from falling back to
options.toolContext.messages; update the call so it forwards the actual
toolContext messages (e.g., use executionOptions.toolContext.messages) or omit
the messages property so the validator can use its fallback. Modify the
validateToolExecution invocation near executionOptions and toolContext (symbols:
executionOptions, toolContext, agent.validateToolExecution,
validateToolExecution) to pass the real messages buffer instead of [].
---
Outside diff comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 6392-6406: The duplicate invocation of tool.hooks?.onEnd with the
same parameters causes side effects to run twice on failures; remove the
redundant call so onEnd is awaited only once (keep a single await
tool.hooks?.onEnd? invocation), or centralize the call to happen after
handleToolError returns/throws to ensure validation failures handled by
handleToolError do not cause a second onEnd invocation; update the block around
tool.hooks?.onEnd, voltAgentError, args and executionOptions so only one onEnd
is executed for a given failure path.
In `@packages/core/src/workflow/core.ts`:
- Around line 1113-1114: The validators are using workflowStateStore from const
workflowStateStore = options?.workflowState ?? {}, which is wrong for resumed
runs because the runtime replaces state from
options.resumeFrom.checkpoint.workflowState; update places where validators are
passed workflowStateStore (e.g., validator invocation sites around core.ts lines
where validators are called near the const workflowStateStore declaration and
the similar blocks around the other noted ranges) to instead derive the state
from options.resumeFrom?.checkpoint?.workflowState when options.resumeFrom
exists, falling back to options?.workflowState ?? {} otherwise—ensure all
validator calls (the same symbol usages referenced) receive the resumed
checkpoint state so tenant/permission checks validate against the actual runtime
state.
---
Duplicate comments:
In `@packages/core/src/agent/agent.ts`:
- Around line 1183-1186: The validator lookup is bypassing the merged validators
on OperationContext; instead of using options?.executionValidators?.tools ??
this.executionValidators?.tools, fetch the effective validators from the
resolved OperationContext created by createOperationContext() (or use
operationContext if passed in) so parent/agent/per-call validators are honored —
modify the code that sets the validators variable (the block referencing
options?.executionValidators?.tools and this.executionValidators?.tools) to read
from operationContext.executionValidators.tools (falling back to options and
then this.executionValidators) so the merged set on OperationContext is used.
- Around line 1337-1356: The options destructuring in the agent methods is
leaking the VoltAgent-only option executionValidators into aiSDKOptions so it
gets forwarded to AI SDK calls; update the destructuring that builds
aiSDKOptions (the blocks that currently collect ...aiSDKOptions in the methods
handling generateText, streamText, generateObject, and streamObject) to
explicitly extract and exclude executionValidators (e.g., add
executionValidators to the left-hand list alongside context, _requestHeaders,
_feedback, etc.) so executionValidators is not present in aiSDKOptions before
calling generateText/streamText/generateObject/streamObject.
In `@packages/core/src/workflow/core.ts`:
- Around line 1222-1242: Run the execution validators before persisting the
running state in startAsync: move the executionValidators construction and the
await runExecutionValidators(...) call to occur prior to the code that commits a
running execution (i.e., before the logic that sets the execution record to
"running" inside startAsync). Then update executeInternal to accept a flag
(e.g., alreadyValidated or skipValidation) and short-circuit/skip calling
runExecutionValidators when that flag is true to avoid duplicate validation;
reference the existing symbols workflowExecutionValidators,
options?.executionValidators, runExecutionValidators, startAsync, and
executeInternal when making these changes.
In `@packages/server-core/src/handlers/workflow.handlers.ts`:
- Around line 248-251: The catch block in handleStreamWorkflow currently calls
broadcastWorkflowStreamEvent with only error.message, losing ClientHTTPError
fields; update the catch to detect ClientHTTPError (or an error object present
on session.streamExecution.result) and include its code, name, and httpStatus
when calling broadcastWorkflowStreamEvent so the emitted event preserves all
ClientHTTPError properties rather than only message; reference
broadcastWorkflowStreamEvent, handleStreamWorkflow, ClientHTTPError, and
session.streamExecution.result to locate and change the broadcast call
accordingly.
🪄 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: bd3ffdbb-ddad-467e-b7fa-269c60855e23
📒 Files selected for processing (17)
.changeset/execution-validators.mdpackages/core/src/agent/agent.spec.tspackages/core/src/agent/agent.tspackages/core/src/agent/errors/client-http-errors.tspackages/core/src/agent/errors/index.tspackages/core/src/agent/types.tspackages/core/src/execution-validation.tspackages/core/src/index.tspackages/core/src/workflow/core.spec.tspackages/core/src/workflow/core.tspackages/core/src/workflow/types.tspackages/server-core/src/handlers/tool.handlers.spec.tspackages/server-core/src/handlers/tool.handlers.tspackages/server-core/src/handlers/workflow.handlers.spec.tspackages/server-core/src/handlers/workflow.handlers.tswebsite/docs/agents/tools.mdwebsite/docs/workflows/overview.md
✅ Files skipped from review due to trivial changes (7)
- .changeset/execution-validators.md
- website/docs/agents/tools.md
- packages/core/src/workflow/types.ts
- packages/server-core/src/handlers/workflow.handlers.spec.ts
- website/docs/workflows/overview.md
- packages/core/src/agent/errors/client-http-errors.ts
- packages/core/src/agent/agent.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/core/src/agent/types.ts
- packages/server-core/src/handlers/tool.handlers.spec.ts
- packages/core/src/index.ts
| // Build a minimal execution context for tools | ||
| const result = await tool.execute(parsedInput, { | ||
| const executionOptions = { | ||
| userId, | ||
| conversationId, | ||
| context: contextMap, | ||
| systemContext: new Map(), | ||
| abortController, | ||
| toolContext: { | ||
| name: tool.name, | ||
| callId: generateId(), | ||
| callId: toolCallId, | ||
| messages: [], | ||
| abortSignal: abortController.signal, | ||
| }, | ||
| logger, | ||
| }; | ||
|
|
||
| await agent.validateToolExecution?.({ | ||
| tool, | ||
| args: parsedInput, | ||
| options: executionOptions, | ||
| toolCallId, | ||
| messages: [], | ||
| }); |
There was a problem hiding this comment.
Don’t hard-code empty validator messages.
Passing messages: [] bypasses Agent.validateToolExecution’s fallback to options.toolContext.messages, so direct tool validators cannot inspect conversation/request history even when it is available.
Proposed fix
+ const requestMessages =
+ Array.isArray(body?.messages)
+ ? body.messages
+ : Array.isArray(contextMap.get("messages"))
+ ? (contextMap.get("messages") as unknown[])
+ : [];
+
// Build a minimal execution context for tools
const executionOptions = {
userId,
conversationId,
context: contextMap,
@@
toolContext: {
name: tool.name,
callId: toolCallId,
- messages: [],
+ messages: requestMessages,
abortSignal: abortController.signal,
},
logger,
};
@@
args: parsedInput,
options: executionOptions,
toolCallId,
- messages: [],
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Build a minimal execution context for tools | |
| const result = await tool.execute(parsedInput, { | |
| const executionOptions = { | |
| userId, | |
| conversationId, | |
| context: contextMap, | |
| systemContext: new Map(), | |
| abortController, | |
| toolContext: { | |
| name: tool.name, | |
| callId: generateId(), | |
| callId: toolCallId, | |
| messages: [], | |
| abortSignal: abortController.signal, | |
| }, | |
| logger, | |
| }; | |
| await agent.validateToolExecution?.({ | |
| tool, | |
| args: parsedInput, | |
| options: executionOptions, | |
| toolCallId, | |
| messages: [], | |
| }); | |
| // Build a minimal execution context for tools | |
| const requestMessages = | |
| Array.isArray(body?.messages) | |
| ? body.messages | |
| : Array.isArray(contextMap.get("messages")) | |
| ? (contextMap.get("messages") as unknown[]) | |
| : []; | |
| const executionOptions = { | |
| userId, | |
| conversationId, | |
| context: contextMap, | |
| systemContext: new Map(), | |
| abortController, | |
| toolContext: { | |
| name: tool.name, | |
| callId: toolCallId, | |
| messages: requestMessages, | |
| abortSignal: abortController.signal, | |
| }, | |
| logger, | |
| }; | |
| await agent.validateToolExecution?.({ | |
| tool, | |
| args: parsedInput, | |
| options: executionOptions, | |
| toolCallId, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/server-core/src/handlers/tool.handlers.ts` around lines 207 - 229,
The current call to agent.validateToolExecution passes a hard-coded empty
messages array which prevents validators from falling back to
options.toolContext.messages; update the call so it forwards the actual
toolContext messages (e.g., use executionOptions.toolContext.messages) or omit
the messages property so the validator can use its fallback. Modify the
validateToolExecution invocation near executionOptions and toolContext (symbols:
executionOptions, toolContext, agent.validateToolExecution,
validateToolExecution) to pass the real messages buffer instead of [].
PR Checklist
Please check if your PR fulfills the following requirements:
Bugs / Features
What is the current behavior?
VoltAgent supports guardrails, hooks, approval, and schema validation, but there is no reusable execution-boundary validation primitive that can deterministically inspect the final tool/workflow payload and context immediately before execution.
Tool and workflow execution policies must be implemented ad hoc inside individual hooks, tools, or workflow steps. Direct server handlers also do not preserve client HTTP error details for validation-style failures.
What is the new behavior?
executionValidatorsfor agents and workflows.ExecutionValidationErrorwith custom message, code, HTTP status, and metadata support.ClientHTTPErrordetails in server-core direct tool and workflow handlers.fixes #1213
Notes for reviewers
Validation run:
pnpm --filter @voltagent/core typecheckpnpm --filter @voltagent/server-core typecheckpnpm --filter @voltagent/core exec vitest run src/agent/agent.spec.ts src/workflow/core.spec.ts --reporter=defaultpnpm --filter @voltagent/server-core test -- src/handlers/tool.handlers.spec.ts src/handlers/workflow.handlers.spec.tsgit diff --checkSummary by cubic
Adds pre-execution validators for tools and workflows to enforce deterministic policies right before execution. Server handlers return structured validation errors (name, code, httpStatus) instead of generic 500s.
executionValidatorsfor tools and workflows; run before tool start/execution (including provider tools) and before workflow steps.falseor{ pass: false }to block.ExecutionValidationErrorwith message, code,httpStatus, and metadata.@voltagent/server-coredirect tool and workflow handlers preserveClientHTTPErrordetails (name, code,httpStatus) in responses.@voltagent/coreand@voltagent/server-core.Written for commit ec3bab9. Summary will update on new commits.
Summary by CodeRabbit
New Features
Tests
Documentation