Skip to content

feat: allow tool-specific hooks and let onToolEnd override tool out…#978

Merged
omeraplak merged 4 commits into
mainfrom
feat/tool-hooks
Jan 23, 2026
Merged

feat: allow tool-specific hooks and let onToolEnd override tool out…#978
omeraplak merged 4 commits into
mainfrom
feat/tool-hooks

Conversation

@omeraplak

@omeraplak omeraplak commented Jan 22, 2026

Copy link
Copy Markdown
Member

…put #975

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

What is the new behavior?

fixes (issue)

Notes for reviewers


Summary by cubic

Adds tool-level hooks and lets agent onToolEnd override tool outputs with validation to improve control over tool results. Addresses #975.

  • New Features
    • Tool hooks: per-tool onStart/onEnd; onEnd can return { output } to override.
    • Agent onToolEnd can also return { output } and wins over tool hooks; PlanAgent chains onToolEnd.
    • Overrides are re-validated when an output schema exists and apply only to the final chunk for streaming.

Written for commit 52809f2. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Per-tool lifecycle hooks (onStart, onEnd) that run alongside agent hooks and can run before agent onToolEnd.
    • Tool and agent end-hooks can return an { output } override; overrides are re-validated against output schemas and apply to final outputs (including async/streaming tools).
    • Tools preserve provided hook callbacks on the tool instance.
  • Documentation

    • New guides, recipes, and docs with examples explaining hook usage and override semantics.

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

@changeset-bot

changeset-bot Bot commented Jan 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 52809f2

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 Minor

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

@omeraplak omeraplak linked an issue Jan 22, 2026 that may be closed by this pull request
@ghost

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jan 22, 2026

Copy link
Copy Markdown
Contributor

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds per-tool lifecycle hooks (onStart, onEnd) and enables tool.onEnd and agent onToolEnd hooks to return an { output } override; overrides are applied sequentially and re-validated against a tool output schema when present.

Changes

Cohort / File(s) Summary
Tool Hook Types & Tool Class
packages/core/src/tool/index.ts
Adds hook types (ToolHookOnStartArgs, ToolHookOnEndArgs, ToolHookOnEndResult, ToolHookOnStart, ToolHookOnEnd, ToolHooks), adds hooks?: ToolHooks to ToolOptions, and exposes readonly hooks?: ToolHooks on Tool.
Tool Hooks Tests
packages/core/src/tool/index.spec.ts
Tests assert constructor preserves provided hooks.onStart and hooks.onEnd.
Agent Hook Type Update
packages/core/src/agent/hooks/index.ts
Adds OnToolEndHookResult and updates AgentHookOnToolEnd to allow returning `OnToolEndHookResult
Agent Tool Execution & Hook Orchestration
packages/core/src/agent/agent.ts
Introduces runToolStartHooks, hasOutputOverride, resolveToolEndOutput, and finalizeToolSpan; applies tool.onEnd then agent onToolEnd overrides, re-validates against output schema, updates streaming and non-streaming flows, and centralizes span/error handling.
PlanAgent Hook Chaining
packages/core/src/planagent/plan-agent.ts
Adds chainToolEndHooks to sequentially run onToolEnd hooks and propagate output overrides; replaces prior void-only chaining for tool-end hooks.
Agent Tests & Declarations
packages/core/src/agent/agent.spec.ts, packages/core/src/agent/agent.spec-d.ts
Adds tests verifying onToolEnd can override tool output and tool-level hooks run before agent-level hooks; updates test typings to reflect onToolEnd returning { output }.
Docs — Hooks & Tools
website/docs/agents/hooks.md, website/docs/agents/tools.md, website/recipes/tool-hooks.md
Adds/explains per-tool hooks (onStart/onEnd), override semantics, streaming behavior, validation with output schemas, examples, and a new recipe.
Changeset & Sidebar
.changeset/tool-hooks-and-overrides.md, website/sidebarsRecipes.ts
Adds a changeset describing the feature and registers a "Tool Hooks" recipe doc in the sidebar.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Agent as Agent
    participant Tool as Tool
    participant Schema as OutputSchema

    Client->>Agent: executeTool(args)
    Agent->>Agent: runToolStartHooks()
    Agent->>Tool: tool.onStart?(args)
    Agent->>Tool: execute(args)
    Tool-->>Agent: originalOutput

    Agent->>Tool: tool.onEnd?({ output: originalOutput, error: none })
    Tool-->>Agent: toolEndResult (maybe { output })
    Agent->>Agent: compute intermediateOutput

    Agent->>Schema: validate(intermediateOutput) 
    Schema-->>Agent: validation result

    Agent->>Agent: agent.onToolEnd({ output: intermediateOutput, error: none })
    Agent-->>Agent: agentHookResult (maybe { output })
    Agent->>Schema: validate(finalOutput)
    Schema-->>Agent: validation result

    Agent-->>Client: return finalOutput
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I nibble on hooks at dawn's first light,
Tools start and end, outputs take flight.
A gentle override, then a quick validate—
Hop, patch, and flourish; the pipeline is great! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main feature: tool-specific hooks and onToolEnd override capability for tool output.
Description check ✅ Passed The description follows the template structure with all required sections completed: PR checklist items marked, current/new behavior documented, issue linked, and comprehensive context provided.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jan 22, 2026

Copy link
Copy Markdown

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2e55636
Status: ✅  Deploy successful!
Preview URL: https://28427b94.voltagent.pages.dev
Branch Preview URL: https://feat-tool-hooks.voltagent.pages.dev

View logs

@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.

1 issue found across 10 files

Prompt for AI agents (all issues)

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


<file name="website/docs/agents/tools.md">

<violation number="1" location="website/docs/agents/tools.md:49">
P2: This adds a second "## Tool Hooks" heading in the same document, which will generate duplicate anchors and confuse navigation/links. Rename this new section to a distinct title (e.g., "Tool-Specific Hooks") to avoid conflicts with the existing agent hook section.</violation>
</file>

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

Comment thread website/docs/agents/tools.md

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

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)

5158-5165: onToolEnd handler must return output override to match AgentHookOnToolEnd type signature.

Lines 5162-5165 currently return void, but the AgentHookOnToolEnd type requires Promise<OnToolEndHookResult | undefined> | OnToolEndHookResult | undefined. This causes a TS2322 type error. Chain the option hook result into the agent hook and return the final override (agent wins) so output overrides propagate correctly.

Suggested fix
-      onToolEnd: async (...args) => {
-        await options.hooks?.onToolEnd?.(...args);
-        await this.hooks.onToolEnd?.(...args);
-      },
+      onToolEnd: async (...args) => {
+        const [payload] = args;
+        let output = payload?.output;
+        let hasOverride = false;
+
+        const optionResult = await options.hooks?.onToolEnd?.(...args);
+        if (optionResult && typeof optionResult === "object" && "output" in optionResult) {
+          output = (optionResult as { output?: unknown }).output;
+          hasOverride = true;
+        }
+
+        const agentResult = await this.hooks.onToolEnd?.({
+          ...(payload ?? {}),
+          output,
+        });
+        if (agentResult && typeof agentResult === "object" && "output" in agentResult) {
+          return { output: (agentResult as { output?: unknown }).output };
+        }
+
+        return hasOverride ? { output } : undefined;
+      },
🤖 Fix all issues with AI agents
In `@packages/core/src/agent/agent.ts`:
- Around line 4665-4693: The resolveToolEndOutput flow currently only
revalidates when output !== currentOutput, which skips validation if a hook
explicitly returns or mutates to the same object; change resolveToolEndOutput to
track an explicit override flag when tool.hooks?.onEnd or hooks.onToolEnd
returns a value satisfying hasOutputOverride (e.g., set overrideProvided =
true), and call this.validateToolOutput(output, tool) whenever overrideProvided
is true (instead of only on inequality) so any explicit hook override goes
through outputSchema validation; adjust logic around hasOutputOverride,
toolHookResult, and agentHookResult to set and check overrideProvided before
calling validateToolOutput.

In `@website/docs/agents/tools.md`:
- Around line 49-81: This file contains a duplicate "## Tool Hooks" section;
merge the two sections or rename one to avoid split/contradictory guidance.
Locate the example block that defines summarizeTool via createTool (the snippet
with parameters, execute, and hooks: { onStart, onEnd }) and either consolidate
its content into the other "Tool Hooks" section or remove the redundant heading
and cross-reference the single canonical section so documentation remains
consistent.

Comment thread packages/core/src/agent/agent.ts
Comment thread website/docs/agents/tools.md

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@packages/core/src/agent/agent.ts`:
- Around line 4638-4699: The runToolStartHooks implementation calls the
agent-level hook (hooks.onToolStart) before the tool-level hook
(tool.hooks.onStart), which contradicts the docs; modify runToolStartHooks so it
invokes tool.hooks?.onStart(...) first and then awaits hooks.onToolStart(...)
while keeping the same argument objects (tool, args, options: executionOptions,
and agent/context where applicable) to preserve behavior and ordering
consistency between the tool-level onStart and the agent-level onToolStart.
- Around line 4701-4709: handleToolSuccess and handleToolError both call
toolSpan.end(), and the outer finally calls
oc.traceContext.endChildSpan(toolSpan, "completed", {}), causing double-ending;
either remove the explicit toolSpan.end() calls from handleToolSuccess and
handleToolError (so span closing is centralized in the finally via
oc.traceContext.endChildSpan) or guard the finally span-close with a recording
check (use toolSpan.isRecording() before calling oc.traceContext.endChildSpan)
so endChildSpan is only invoked for active spans; update the functions
handleToolSuccess, handleToolError, and the finally block accordingly to use one
consistent span-end path.

Comment thread packages/core/src/agent/agent.ts
Comment thread packages/core/src/agent/agent.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

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)

4769-4815: Yield validated stream chunks to preserve output normalization.

validateToolOutput returns the parsed/normalized output, but the streaming loop yields the raw pendingOutput. This can make streamed chunks diverge from the validated output (e.g., defaults/coercions/stripping), while the final chunk uses the parsed result. Consider storing/yielding the validated value.

🔧 Suggested fix
-            while (true) {
-              const next = await oc.traceContext.withSpan(toolSpan, () => iterator.next());
-              if (next.done) {
-                break;
-              }
-
-              if (hasOutput) {
-                yield pendingOutput;
-              }
-
-              pendingOutput = next.value;
-              hasOutput = true;
-              validatedResult = await this.validateToolOutput(pendingOutput, tool);
-            }
+            while (true) {
+              const next = await oc.traceContext.withSpan(toolSpan, () => iterator.next());
+              if (next.done) {
+                break;
+              }
+
+              const validatedNext = await this.validateToolOutput(next.value, tool);
+              if (hasOutput) {
+                yield pendingOutput;
+              }
+
+              pendingOutput = validatedNext;
+              hasOutput = true;
+              validatedResult = validatedNext;
+            }

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.

[FEAT] Middleware Interception & Truncation for Tool Results

1 participant