-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(core): resume delegated needsApproval approvals in subagent flows #1107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| --- | ||
| "@voltagent/core": patch | ||
| --- | ||
|
|
||
| fix: make delegated `needsApproval` resume correctly in sub-agent flows | ||
|
|
||
| This patch improves Human-in-the-Loop behavior when a supervisor delegates work via `delegate_task`. | ||
|
|
||
| ### What changed | ||
|
|
||
| - Approval responses attached to `tool-delegate_task` UI parts are now matched to the guarded sub-agent tool call and correctly resume execution. | ||
| - Parent tool-context messages are forwarded to delegated sub-agents as shared context, so required arguments (for example `userId`) are not lost during handoff. | ||
| - Sub-agent forwarding defaults now include approval events (`tool-approval-request`, `tool-approval-response`) in addition to tool call/result events. | ||
|
|
||
| ### No DX change for tool authors | ||
|
|
||
| You still define approvals the same way: | ||
|
|
||
| ```ts | ||
| const deleteCrmUser = createTool({ | ||
| name: "deleteCrmUser", | ||
| parameters: z.object({ | ||
| userId: z.string(), | ||
| reason: z.string().optional(), | ||
| }), | ||
| needsApproval: true, | ||
| execute: async ({ userId }) => ({ ok: true, userId }), | ||
| }); | ||
| ``` | ||
|
|
||
| ### Delegated flow example | ||
|
|
||
| ```ts | ||
| const crmAgent = new Agent({ | ||
| name: "CRM Agent", | ||
| model: "openai/gpt-4o-mini", | ||
| instructions: "Handle CRM mutations.", | ||
| tools: [deleteCrmUser], | ||
| }); | ||
|
|
||
| const triageAgent = new Agent({ | ||
| name: "Triage Agent", | ||
| model: "openai/gpt-4o-mini", | ||
| instructions: "Route CRM delete requests to CRM Agent.", | ||
| subAgents: [crmAgent], | ||
| }); | ||
| ``` | ||
|
|
||
| When the UI sends approval on a delegated part: | ||
|
|
||
| ```ts | ||
| { | ||
| role: "assistant", | ||
| parts: [ | ||
| { | ||
| type: "tool-delegate_task", | ||
| state: "approval-responded", | ||
| approval: { id: "approval-call_123", approved: true } | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| VoltAgent now resumes the pending guarded tool call in the sub-agent instead of re-requesting approval. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| OPENAI_API_KEY=your_openai_api_key_here |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| node_modules | ||
| dist | ||
| .DS_Store | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # VoltAgent Human-in-the-Loop (HITL) Example | ||
|
|
||
| This example shows how to use tool-level approvals with `needsApproval` while keeping agent DX unchanged. | ||
|
|
||
| ## What It Demonstrates | ||
|
|
||
| - `crmHitlAgent`: direct CRM delete flow with `needsApproval`. | ||
| - `triageAgent` + `crmAgent`: subagent delegation flow where CRM delete uses `needsApproval`. | ||
| - Approval pause/resume semantics for both direct and delegated execution. | ||
|
|
||
| ## Setup | ||
|
|
||
| ```bash | ||
| pnpm install | ||
| ``` | ||
|
|
||
| Copy environment file: | ||
|
|
||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| Set: | ||
|
|
||
| ```bash | ||
| OPENAI_API_KEY=your_openai_api_key_here | ||
| ``` | ||
|
|
||
| ## Run the Server | ||
|
|
||
| ```bash | ||
| pnpm dev | ||
| ``` | ||
|
|
||
| Registered agents: | ||
|
|
||
| - `crmHitlAgent` | ||
| - `triageAgent` | ||
| - `crmAgent` | ||
|
|
||
| ## Manual Testing | ||
|
|
||
| ### 1) Direct agent (`crmHitlAgent`) | ||
|
|
||
| Trigger approval: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3141/agents/crmHitlAgent/chat \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"input":"CRMdeki user_123 kullanıcısını kalıcı olarak sil."}' | ||
| ``` | ||
|
|
||
| You should see a pending `deleteCrmUser` approval state (`approval-requested`) and approve/deny actions in compatible UIs. | ||
|
|
||
| ### 2) Subagent path (`triageAgent`) | ||
|
|
||
| Trigger delegation + CRM approval: | ||
|
|
||
| ```bash | ||
| curl -X POST http://localhost:3141/agents/triageAgent/chat \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{"input":"CRM tarafında user_123 hesabını tamamen kaldır."}' | ||
| ``` | ||
|
|
||
| Triage should delegate to `CRM Agent`, and approval should be required on CRM-side delete execution. | ||
| In this delegated path, approval UI can surface on the `delegate_task` tool part. This is expected; it still controls the underlying CRM delete approval. | ||
|
|
||
| ## Run Smoke Test | ||
|
|
||
| From this directory: | ||
|
|
||
| ```bash | ||
| pnpm test:smoke | ||
| ``` | ||
|
|
||
| Or from repo root: | ||
|
|
||
| ```bash | ||
| pnpm --filter voltagent-example-with-hitl test:smoke | ||
| ``` | ||
|
|
||
| Smoke test validates both flows: | ||
|
|
||
| - direct HITL (`crmHitlAgent`) | ||
| - subagent HITL (`triageAgent -> crmAgent`) | ||
|
|
||
| It tries `examples/with-hitl/.env` first and falls back to `examples/base/.env` for `OPENAI_API_KEY`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| { | ||
| "name": "voltagent-example-with-hitl", | ||
| "author": "", | ||
| "dependencies": { | ||
| "@ai-sdk/openai": "^3.0.0", | ||
| "@voltagent/cli": "^0.1.21", | ||
| "@voltagent/core": "^2.6.1", | ||
| "@voltagent/logger": "^2.0.2", | ||
| "@voltagent/server-hono": "^2.0.7", | ||
| "ai": "^6.0.0", | ||
| "zod": "^3.25.76" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^24.2.1", | ||
| "tsx": "^4.19.3", | ||
| "typescript": "^5.8.2" | ||
| }, | ||
| "keywords": [ | ||
| "agent", | ||
| "ai", | ||
| "voltagent" | ||
| ], | ||
| "license": "MIT", | ||
| "private": true, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/VoltAgent/voltagent.git", | ||
| "directory": "examples/with-hitl" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "dev": "tsx watch --env-file=.env ./src", | ||
| "start": "node dist/index.js", | ||
| "test:smoke": "node ./scripts/smoke-test.mjs", | ||
| "volt": "volt" | ||
| }, | ||
| "type": "module" | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { Agent, VoltAgent, createTool } from "@voltagent/core"; | ||
| import { createPinoLogger } from "@voltagent/logger"; | ||
| import { honoServer } from "@voltagent/server-hono"; | ||
| import { z } from "zod"; | ||
|
|
||
| const logger = createPinoLogger({ | ||
| name: "with-hitl", | ||
| level: "info", | ||
| }); | ||
|
|
||
| const deleteCrmUserTool = createTool({ | ||
| name: "deleteCrmUser", | ||
| description: "Permanently deletes a user record from CRM.", | ||
| parameters: z.object({ | ||
| userId: z.string().min(1), | ||
| reason: z.string().min(3).optional(), | ||
| }), | ||
| // Destructive action: always requires human approval. | ||
| needsApproval: true, | ||
| execute: async ({ userId, reason }) => { | ||
| return { | ||
| ok: true, | ||
| action: "user-deleted", | ||
| userId, | ||
| reason: reason || "not provided", | ||
| executedAt: new Date().toISOString(), | ||
| }; | ||
| }, | ||
| }); | ||
|
|
||
| const crmHitlAgent = new Agent({ | ||
| name: "CRM HITL Agent", | ||
| instructions: [ | ||
| "You are a CRM operations assistant.", | ||
| "Execute account-management changes through available actions.", | ||
| "When a user asks to delete an account and provides a user ID, perform the deletion action first, then report status.", | ||
| "Deletion reason is optional; if it is not provided, use a short default reason like 'user-requested deletion'.", | ||
| "Do not claim a deletion succeeded unless the action actually ran.", | ||
| ].join("\n"), | ||
| model: "openai/gpt-4o-mini", | ||
| tools: [deleteCrmUserTool], | ||
| }); | ||
|
|
||
| const crmAgent = new Agent({ | ||
| name: "CRM Agent", | ||
| instructions: [ | ||
| "You handle CRM account operations.", | ||
| "Execute requested account changes through available actions.", | ||
| "For delete-account requests, if user ID exists in the request or shared context, run the delete action before reporting status.", | ||
| "Deletion reason is optional; if missing, use 'user-requested deletion'.", | ||
| "Do not report deletion completion unless it actually happened.", | ||
| "Use concise operator-style responses.", | ||
| ].join("\n"), | ||
| model: "openai/gpt-4o-mini", | ||
| tools: [deleteCrmUserTool], | ||
| }); | ||
|
|
||
| const triageAgent = new Agent({ | ||
| name: "Triage Agent", | ||
| instructions: [ | ||
| "You triage incoming support and operations requests.", | ||
| "Route CRM-specific account mutations to the CRM specialist.", | ||
| "Provide the user a short, clear final response.", | ||
| ].join("\n"), | ||
| model: "openai/gpt-4o-mini", | ||
| subAgents: [ | ||
| { | ||
| agent: crmAgent, | ||
| method: "streamText", | ||
| options: { | ||
| temperature: 0, | ||
| maxSteps: 4, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| new VoltAgent({ | ||
| agents: { | ||
| crmHitlAgent, | ||
| triageAgent, | ||
| crmAgent, | ||
| }, | ||
| server: honoServer(), | ||
| logger, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "NodeNext", | ||
| "moduleResolution": "NodeNext", | ||
| "esModuleInterop": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "strict": true, | ||
| "outDir": "dist", | ||
| "skipLibCheck": true | ||
| }, | ||
| "include": ["src"], | ||
| "exclude": ["node_modules", "dist"] | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
.envto ignore list to avoid leaking API keys.The README instructs users to create
examples/with-hitl/.envcontainingOPENAI_API_KEY. Without ignoring it here, the key can be committed accidentally.🔒 Suggested fix
node_modules dist .DS_Store +.env📝 Committable suggestion
🤖 Prompt for AI Agents