Agent 審批
Agent 呼叫處理敏感操作(例如刪除資源或執行耗時程序)的 Tool 時,有時需要與 Workflow 相同的人工介入監督。透過 Agent 審批,你可以在 Tool call 執行前將其暫停,讓人員批准或拒絕,也可以讓 Tool 自行暫停,以向用戶索取額外背景資料。
何時使用 Agent 審批何時使用 Agent 審批 的直接連結
- 破壞性或不可逆轉的操作,例如刪除記錄、傳送電郵或處理付款。
- 成本高昂的操作,例如呼叫昂貴的第三方 API,而你希望先核實參數。
- 條件式確認,即 Tool 開始執行後,發現需要用戶確認或提供額外資料才能完成。
快速開始快速開始 的直接連結
將 Tool 標記為 requireApproval: true,然後檢查串流中的 tool-call-approval chunk,以批准或拒絕:
import { Agent } from '@mastra/core/agent'
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
const deleteTool = createTool({
id: 'delete-record',
description: 'Delete a record by ID',
inputSchema: z.object({ id: z.string() }),
outputSchema: z.object({ deleted: z.boolean() }),
requireApproval: true,
execute: async ({ id }) => {
await db.delete(id)
return { deleted: true }
},
})
const agent = new Agent({
id: 'my-agent',
name: 'My Agent',
model: 'openai/gpt-5-mini',
tools: { deleteTool },
})
const stream = await agent.stream('Delete record abc-123')
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-approval') {
const approved = await agent.approveToolCall({ runId: stream.runId })
for await (const c of approved.textStream) process.stdout.write(c)
}
}
Agent 審批使用快照擷取請求狀態。請在 Mastra 實例上設定儲存 Provider,否則你會看到「snapshot not found」錯誤。
Agent 執行的快照是最精簡的恢復成品:只保留恢復已暫停執行所需的內容,並會在執行完成後刪除。請使用 tracing 保存執行記錄,並使用 memory 保存對話記錄。
審批的運作方式審批的運作方式 的直接連結
Mastra 提供兩種不同的 Tool call 暫停機制:執行前審批和執行階段暫停。
執行前審批執行前審批 的直接連結
執行前審批會在 Tool call 的 execute 函式執行_之前_將其暫停。LLM 仍會決定呼叫哪個 Tool 並提供參數,但在你明確批准之前,execute 不會執行。
以下旗標配合 OR 邏輯控制此行為。只要_其中一個_為 true,呼叫便會暫停:
| 旗標 | 設定位置 | 範圍 |
|---|---|---|
requireToolApproval: true | stream() / generate() 選項 | 暫停該請求的每個 Tool call |
requireApproval: true | createTool() 定義 | 暫停對該特定 Tool 的呼叫 |
串流會發出包含 toolCallId、toolName 和 args 的 tool-call-approval chunk。使用串流的 runId 呼叫 approveToolCall() 或 declineToolCall() 以繼續:
const stream = await agent.stream("What's the weather in London?", {
requireToolApproval: true,
})
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-approval') {
console.log('Tool:', chunk.payload.toolName)
console.log('Args:', chunk.payload.args)
// Approve
const approved = await agent.approveToolCall({ runId: stream.runId })
for await (const c of approved.textStream) process.stdout.write(c)
// Or decline
const declined = await agent.declineToolCall({ runId: stream.runId })
for await (const c of declined.textStream) process.stdout.write(c)
}
}
使用函式進行條件式審批使用函式進行條件式審批 的直接連結
除了布林值外,requireToolApproval 也接受一個函式,用於逐次決定 Tool call 是否需要審批。函式會收到 toolName、模型傳入的 args、requestContext 和 workspace。傳回 true 表示該呼叫需要審批,傳回 false 則表示允許執行。這讓你可以在執行階段設定審批條件,例如只審批名稱符合某個模式的 Tool:
const stream = await agent.stream('Clean up old records', {
requireToolApproval: ({ toolName }) => /^delete_/.test(toolName),
})
Tool 本身的 requireApproval 設定優先於上述函式,其規則會決定該 Tool 是否需要審批。如果函式拋出錯誤,為安全起見,該呼叫會要求審批。
以函式設定的 requireToolApproval 僅適用於一般 stream() / generate() 呼叫。Durable agent 和已儲存的 Agent 會保存其選項,而函式無法序列化,因此它們只接受布林值。如果在這些情況下傳入函式,系統會改為要求審批每個 Tool call。
將審批綁定至確切的 Tool 參數將審批綁定至確切的 Tool 參數 的直接連結
對於敏感 Tool,應將審批綁定至向審批者顯示的確切 Tool 名稱和參數。如果這些參數在執行前有所變動,Tool 就不應以舊有審批執行。
tool-call-approval chunk 已包含 toolName、toolCallId 和 args。你可以在顯示審批請求時為這些欄位建立指紋。以下範例使用 JSON 字串作為指紋,但在正式環境中,應使用 Tool 名稱和參數的穩定雜湊值:
import { Agent } from '@mastra/core/agent'
// For your production usecase, build a stable hash of the tool name and args
function actionFingerprint(toolName: string, args: unknown) {
const payload = JSON.stringify({ toolName, args })
return `fingerprint-${payload}`
}
const sensitiveTools = new Set(['issue_refund', 'delete_record'])
const approvedFingerprints = new Set<string>()
export const approvalBoundAgent = new Agent({
id: 'approval-bound-agent',
name: 'Approval Bound Agent',
model: 'openai/gpt-5.6-sol',
tools: { issueRefundTool, deleteRecordTool },
hooks: {
beforeToolCall: ({ toolName, input }) => {
if (!sensitiveTools.has(toolName)) return
const fingerprint = actionFingerprint(toolName, input)
if (!approvedFingerprints.delete(fingerprint)) {
return {
proceed: false,
output: `Tool call blocked: approval did not match ${toolName} arguments.`,
}
}
},
},
})
const stream = await approvalBoundAgent.stream('Refund order ord-1042', {
requireToolApproval: ({ toolName }) => sensitiveTools.has(toolName),
})
async function consumeApprovalStream(currentStream: typeof stream) {
for await (const chunk of currentStream.fullStream) {
if (chunk.type === 'tool-call-approval') {
const { toolName, toolCallId, args } = chunk.payload
const fingerprint = actionFingerprint(toolName, args)
// Present toolName, args, and fingerprint to your approval UI.
const approved = await showApprovalDialog({ toolName, args, fingerprint })
const nextStream = approved
? await approveReviewedToolCall(currentStream.runId, toolCallId, fingerprint)
: await approvalBoundAgent.declineToolCall({ runId: currentStream.runId, toolCallId })
await consumeApprovalStream(nextStream)
}
}
}
async function approveReviewedToolCall(runId: string, toolCallId: string, fingerprint: string) {
approvedFingerprints.add(fingerprint)
return approvalBoundAgent.approveToolCall({ runId, toolCallId })
}
await consumeApprovalStream(stream)
在正式環境中,請將已批准的指紋儲存在耐久儲存空間,並按用戶、執行、Tool call 和政策版本劃分範圍。上述 Set 刻意保持精簡,讓界線清晰可見:審批只會使用一次,而且只適用於審批時所檢視的同一組標準 Tool 參數。
使用 suspend() 在執行階段暫停runtime-suspension-with-suspend 的直接連結
Tool 也可以在其 execute 函式執行_期間_呼叫 suspend() 來暫停。當 Tool 開始執行後,發現需要用戶提供額外資料或確認才能完成時,這種方式很有用。
串流會發出一個 tool-call-suspended chunk,其中包含由 Tool 的 suspendSchema 定義的自訂 payload。你可以使用符合 Tool resumeSchema 的資料呼叫 resumeStream() 來恢復執行。
const weatherTool = createTool({
id: 'get-weather',
inputSchema: z.object({
location: z.string().optional(),
}),
suspendSchema: z.object({
question: z.string(),
}),
resumeSchema: z.object({
location: z.string(),
}),
execute: async ({ location }, context) => {
if (!location) {
return await context?.agent?.suspend({
question: 'Which city would you like the weather for?',
})
}
return await fetchWeather(location)
},
})
suspend() 不會拋出錯誤,呼叫後請立即傳回(例如 return await suspend({ ... }))。在 Tool 暫停前,await suspend(...) 之後的程式碼仍會執行。
在 generate() 中使用 Tool 審批tool-approval-with-generate 的直接連結
Tool 審批也適用於使用 generate() 的非串流情況。當 Tool 需要審批時,generate() 會立即傳回,其中包括 finishReason: 'suspended'、包含 Tool call 詳情(toolCallId、toolName、args)的 suspendPayload,以及 runId:
const output = await agent.generate('Find user John', {
requireToolApproval: true,
})
if (output.finishReason === 'suspended') {
console.log('Tool requires approval:', output.suspendPayload.toolName)
// Approve
const result = await agent.approveToolCallGenerate({
runId: output.runId,
toolCallId: output.suspendPayload.toolCallId,
})
console.log('Final result:', result.text)
// Or decline
const result = await agent.declineToolCallGenerate({
runId: output.runId,
toolCallId: output.suspendPayload.toolCallId,
})
}
Stream 與 generate 比較Stream 與 generate 比較 的直接連結
| 項目 | stream() | generate() |
|---|---|---|
| 回應類型 | 串流 chunk | 完整回應 |
| 審批偵測 | tool-call-approval chunk | finishReason: 'suspended' |
| 批准方法 | approveToolCall({ runId }) | approveToolCallGenerate({ runId, toolCallId }) |
| 拒絕方法 | declineToolCall({ runId }) | declineToolCallGenerate({ runId, toolCallId }) |
| 結果 | 可反覆運算的串流 | 完整輸出物件 |
toolCallId 在四個方法中都是可選的。當可能有多個待處理的 Tool call 時(常見於 supervisor agent),請傳入此值。如果省略,Agent 會恢復最近暫停的 Tool call。
Tool 層級審批Tool 層級審批 的直接連結
除了在 Agent 層級暫停每個 Tool call,你也可以將個別 Tool 標記為需要審批。這提供細緻控制:只有指定的 Tool 會暫停,其他則立即執行。
使用 requireApproval 進行審批approval-using-requireapproval 的直接連結
在 Tool 定義中設定 requireApproval: true。無論 Agent 是否已設定 requireToolApproval,Tool 都會在執行前暫停:
export const testTool = createTool({
id: 'test-tool',
description: 'Fetches weather for a location',
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
weather: z.string(),
}),
resumeSchema: z.object({
approved: z.boolean(),
}),
execute: async inputData => {
const response = await fetch(`https://wttr.in/${inputData.location}?format=3`)
const weather = await response.text()
return { weather }
},
requireApproval: true,
})
當 requireApproval 為 true 時,串流會像 Agent 層級審批一樣發出 tool-call-approval chunk。使用 approveToolCall() 或 declineToolCall() 以繼續:
const stream = await agent.stream("What's the weather in London?")
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-approval') {
console.log('Approval required for:', chunk.payload.toolName)
}
}
const handleApproval = async () => {
const approvedStream = await agent.approveToolCall({ runId: stream.runId })
for await (const chunk of approvedStream.textStream) {
process.stdout.write(chunk)
}
process.stdout.write('\n')
}
使用 suspend() 進行審批approval-using-suspend 的直接連結
使用這種方式時,Agent 和 Tool 都不會使用 requireApproval。Tool 的 execute 函式會改為呼叫 suspend(),在指定位置暫停,並向用戶傳回背景資料或確認提示。當審批取決於執行階段條件,而非無條件要求時,這種方式很有用。
export const testToolB = createTool({
id: 'test-tool-b',
description: 'Fetches weather for a location',
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
weather: z.string(),
}),
resumeSchema: z.object({
approved: z.boolean(),
}),
suspendSchema: z.object({
reason: z.string(),
}),
execute: async (inputData, context) => {
const { resumeData: { approved } = {}, suspend } = context?.agent ?? {}
if (!approved) {
return suspend?.({ reason: 'Approval required.' })
}
const response = await fetch(`https://wttr.in/${inputData.location}?format=3`)
const weather = await response.text()
return { weather }
},
})
使用這種方式時,串流會包含 tool-call-suspended chunk,而 suspendPayload 會包含 Tool suspendSchema 所定義的 reason。使用 resumeSchema 資料和 runId 呼叫 resumeStream 以繼續:
const stream = await agent.stream("What's the weather in London?")
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-suspended') {
console.log(chunk.payload.suspendPayload)
}
}
const handleResume = async () => {
const resumedStream = await agent.resumeStream({ approved: true }, { runId: stream.runId })
for await (const chunk of resumedStream.textStream) {
process.stdout.write(chunk)
}
process.stdout.write('\n')
}
自動恢復 Tool自動恢復 Tool 的直接連結
使用會呼叫 suspend() 的 Tool 時,你可以啟用自動恢復,讓 Agent 根據用戶的下一則訊息恢復已暫停的 Tool。在 Agent 的預設選項或個別請求中,將 autoResumeSuspendedTools 設為 true:
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'
const agent = new Agent({
id: 'my-agent',
name: 'My Agent',
instructions: 'You are a helpful assistant',
model: 'openai/gpt-5-mini',
tools: { weatherTool },
memory: new Memory(),
defaultOptions: {
autoResumeSuspendedTools: true,
},
})
啟用後,Agent 會在收到下一則用戶訊息時,從訊息記錄中偵測已暫停的 Tool。它會根據 Tool 的 resumeSchema 擷取 resumeData,然後自動恢復 Tool。
以下範例展示完整的對話流程:
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
const weatherTool = createTool({
id: 'weather-tool',
description: 'Fetches weather for a city',
inputSchema: z.object({
city: z.string(),
}),
outputSchema: z.object({
weather: z.string(),
}),
suspendSchema: z.object({
message: z.string(),
}),
resumeSchema: z.object({
city: z.string(),
}),
execute: async (inputData, context) => {
const { resumeData, suspend } = context?.agent ?? {}
// If no city provided, ask the user
if (!inputData.city && !resumeData?.city) {
return suspend?.({ message: 'What city do you want to know the weather for?' })
}
const city = resumeData?.city ?? inputData.city
const response = await fetch(`https://wttr.in/${city}?format=3`)
const weather = await response.text()
return { weather: `${city}: ${weather}` }
},
})
const stream = await agent.stream("What's the weather like?")
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-suspended') {
console.log(chunk.payload.suspendPayload)
}
}
// User sends follow-up on the same thread
const resumedStream = await agent.stream('San Francisco')
for await (const chunk of resumedStream.textStream) {
process.stdout.write(chunk)
}
User: "What's the weather like?"
Agent: "What city do you want to know the weather for?"
User: "San Francisco"
Agent: "The weather in San Francisco is: San Francisco: ☀️ +72°F"
第二則訊息會自動恢復已暫停的 Tool。Agent 從用戶訊息中擷取 { city: "San Francisco" },並將其作為 resumeData 傳入。
要求要求 的直接連結
若要使用自動 Tool 恢復:
- 已設定 Memory:Agent 需要 Memory,才能跨訊息追蹤已暫停的 Tool
- 相同 thread:跟進訊息必須使用相同的 Memory thread 和資源識別碼
- 已定義
resumeSchema:Tool 必須定義resumeSchema,讓 Agent 知道應從用戶訊息中擷取哪種資料結構
手動與自動恢復比較手動與自動恢復比較 的直接連結
| 方式 | 使用情況 |
|---|---|
手動(resumeStream()) | 程式化控制、webhook、按鈕點擊、外部觸發器 |
自動(autoResumeSuspendedTools) | 用戶以自然語言提供恢復資料的對話流程 |
兩種方式使用相同的 Tool 定義。只有當訊息記錄中存在已暫停的 Tool,而且用戶在相同 thread 傳送新訊息時,才會觸發自動恢復。
重新啟動後恢復重新啟動後恢復 的直接連結
上述範例會在暫停和審批之間保留 stream.runId。只要程序仍在執行,這種方式便有效;但在正式環境中,審批通常會在稍後才送達,屆時可能已重新整理頁面、重新啟動伺服器,或由負載平衡器後方的另一個伺服器實例處理。
使用 listSuspendedRuns(),從儲存空間重新找出對話中待處理的執行:
// In the request handler that receives the user's decision
const { runs } = await agent.listSuspendedRuns({
threadId: 'thread-123',
resourceId: 'user-456',
})
const run = runs[0]
const toolCall = run?.toolCalls[0]
if (run && toolCall) {
let stream
if (toolCall.requiresApproval) {
// Suspended by requireApproval — approve or decline the tool call
stream = await agent.approveToolCall({ runId: run.runId, toolCallId: toolCall.toolCallId })
} else {
// Suspended by suspend() — resume with the data the tool asked for
console.log('Tool asked:', toolCall.suspendPayload)
stream = await agent.resumeStream({ name: 'San Francisco' }, { runId: run.runId })
}
for await (const chunk of stream.textStream) process.stdout.write(chunk)
}
每個傳回的執行都包含已暫停的 Tool call(toolCallId、toolName、args 和 requiresApproval)。審批暫停(requiresApproval: true)使用 approveToolCall() / declineToolCall() 回應,而以 suspend() 觸發的暫停則包含其 suspendPayload,並預期透過 resumeStream() 傳入恢復資料。這樣,即使不在記憶體中保留任何狀態,你仍可為兩種流程重建正確的 UI。
sendToolApproval() 會自動使用相同的儲存空間探索機制:如果在記憶體中找不到該 thread 的作用中執行,它會先在儲存空間查找已暫停的執行,然後才會失敗。如果有多個已暫停的執行符合該 thread,請傳入 toolCallId 以消除歧義。
同一探索功能也可透過 HTTP 的 GET /agents/:agentId/suspended-runs,以及客戶端 SDK 的 agent.listSuspendedRuns() 使用,讓建基於瀏覽器的審批 UI 可以直接重新找出待處理的執行。
只有在 Mastra 實例已設定持久儲存 Provider時,已暫停的執行才能在重新啟動後保留。預設的記憶體內儲存空間會在程序結束時遺失快照。
Tool 審批:Supervisor agentTool 審批:Supervisor agent 的直接連結
Supervisor agent 使用 .stream() 或 .generate() 協調多個 subagent。當 subagent 呼叫需要審批的 Tool 時,請求會沿委派鏈向上傳遞,並在 supervisor 層級顯示:
- Supervisor 將工作委派給 subagent。
- Subagent 呼叫具有
requireApproval: true或使用suspend()的 Tool。 - 審批請求向上傳遞至 supervisor。
- 你在 supervisor 層級批准或拒絕。
- 決定向下傳遞回 subagent。
Tool 審批也會透過多層委派傳遞。如果 supervisor 委派給 subagent A,而 subagent A 再委派給 subagent B,且 subagent B 有一個設定了 requireApproval: true 的 Tool,審批請求仍會在最上層 supervisor 顯示。
在 Supervisor agent 中批准和拒絕在 Supervisor agent 中批准和拒絕 的直接連結
以下範例建立一個具有需審批 Tool 的 subagent。當 Tool 觸發審批請求時,請求會在 supervisor 的串流中顯示為 tool-call-approval chunk:
import { Agent } from '@mastra/core/agent'
import { createTool } from '@mastra/core/tools'
import { Memory } from '@mastra/memory'
import { z } from 'zod'
const findUserTool = createTool({
id: 'find-user',
description: 'Finds user by ID in the database',
inputSchema: z.object({
userId: z.string(),
}),
outputSchema: z.object({
user: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
}),
requireApproval: true,
execute: async input => {
const user = await database.findUser(input.userId)
return { user }
},
})
const dataAgent = new Agent({
id: 'data-agent',
name: 'Data Agent',
description: 'Handles database queries and user data retrieval',
model: 'openai/gpt-5-mini',
tools: { findUserTool },
})
const supervisorAgent = new Agent({
id: 'supervisor',
name: 'Supervisor Agent',
instructions: `You coordinate data retrieval tasks.
Delegate to data-agent for user lookups.`,
model: 'openai/gpt-5.6-sol',
agents: { dataAgent },
memory: new Memory(),
})
const stream = await supervisorAgent.stream('Find user with ID 12345')
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-approval') {
console.log('Tool requires approval:', chunk.payload.toolName)
console.log('Arguments:', chunk.payload.args)
// Approve the tool call
const resumeStream = await supervisorAgent.approveToolCall({
runId: stream.runId,
toolCallId: chunk.payload.toolCallId,
})
for await (const resumeChunk of resumeStream.textStream) {
process.stdout.write(resumeChunk)
}
// To decline instead, use:
const declineStream = await supervisorAgent.declineToolCall({
runId: stream.runId,
toolCallId: chunk.payload.toolCallId,
})
}
}
在 Supervisor agent 中使用 suspend()use-suspend-in-supervisor-agents 的直接連結
Tool 也可以使用 suspend() 暫停執行,並向用戶傳回背景資料。這種方式會像 requireApproval 一樣貫穿 supervisor 委派鏈:暫停會在 supervisor 層級顯示:
const conditionalTool = createTool({
id: 'conditional-operation',
description: 'Performs an operation that may require confirmation',
inputSchema: z.object({
operation: z.string(),
}),
suspendSchema: z.object({
message: z.string(),
}),
resumeSchema: z.object({
confirmed: z.boolean(),
}),
execute: async (input, context) => {
const { resumeData } = context?.agent ?? {}
if (!resumeData?.confirmed) {
return context?.agent?.suspend({
message: `Confirm: ${input.operation}?`,
})
}
// Proceed with operation
return await performOperation(input.operation)
},
})
// When using this tool through a subagent in supervisor agents
for await (const chunk of stream.fullStream) {
if (chunk.type === 'tool-call-suspended') {
console.log('Tool suspended:', chunk.payload.suspendPayload.message)
// Resume with confirmation
const resumeStream = await supervisorAgent.resumeStream(
{ confirmed: true },
{ runId: stream.runId },
)
for await (const resumeChunk of resumeStream.textStream) {
process.stdout.write(resumeChunk)
}
}
}
在 generate() 中使用 Supervisor 審批supervisor-approval-with-generate 的直接連結
Tool 審批傳遞也適用於 Supervisor agent 的 generate():
const output = await supervisorAgent.generate('Find user with ID 12345', {
maxSteps: 10,
})
if (output.finishReason === 'suspended') {
console.log('Tool requires approval:', output.suspendPayload.toolName)
// Approve
const result = await supervisorAgent.approveToolCallGenerate({
runId: output.runId,
toolCallId: output.suspendPayload.toolCallId,
})
console.log('Final result:', result.text)
}