跳至主要內容

Agent 審批

Agent 呼叫處理敏感操作(例如刪除資源或執行耗時程序)的 Tool 時,有時需要與 Workflow 相同的人工介入監督。透過 Agent 審批,你可以在 Tool call 執行前將其暫停,讓人員批准或拒絕,也可以讓 Tool 自行暫停,以向用戶索取額外背景資料。

何時使用 Agent 審批
何時使用 Agent 審批 的直接連結

  • 破壞性或不可逆轉的操作,例如刪除記錄、傳送電郵或處理付款。
  • 成本高昂的操作,例如呼叫昂貴的第三方 API,而你希望先核實參數。
  • 條件式確認,即 Tool 開始執行後,發現需要用戶確認或提供額外資料才能完成。

快速開始
快速開始 的直接連結

將 Tool 標記為 requireApproval: true,然後檢查串流中的 tool-call-approval chunk,以批准或拒絕:

src/mastra/agents/index.ts
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: truestream() / generate() 選項暫停該請求的每個 Tool call
requireApproval: truecreateTool() 定義暫停對該特定 Tool 的呼叫

串流會發出包含 toolCallIdtoolNameargstool-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、模型傳入的 argsrequestContextworkspace。傳回 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 已包含 toolNametoolCallIdargs。你可以在顯示審批請求時為這些欄位建立指紋。以下範例使用 JSON 字串作為指紋,但在正式環境中,應使用 Tool 名稱和參數的穩定雜湊值:

src/mastra/agents/approval-bound-agent.ts
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 詳情(toolCallIdtoolNameargs)的 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 chunkfinishReason: '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 都會在執行前暫停:

src/mastra/tools/test-tool.ts
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,
})

requireApprovaltrue 時,串流會像 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(),在指定位置暫停,並向用戶傳回背景資料或確認提示。當審批取決於執行階段條件,而非無條件要求時,這種方式很有用。

src/mastra/tools/test-tool-b.ts
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

src/mastra/agents/my-agent.ts
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。

以下範例展示完整的對話流程:

src/mastra/tools/weather-tool.ts
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)
}
Console output
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(toolCallIdtoolNameargsrequiresApproval)。審批暫停(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 agent
Tool 審批:Supervisor agent 的直接連結

Supervisor agent 使用 .stream().generate() 協調多個 subagent。當 subagent 呼叫需要審批的 Tool 時,請求會沿委派鏈向上傳遞,並在 supervisor 層級顯示:

  1. Supervisor 將工作委派給 subagent。
  2. Subagent 呼叫具有 requireApproval: true 或使用 suspend() 的 Tool。
  3. 審批請求向上傳遞至 supervisor。
  4. 你在 supervisor 層級批准或拒絕。
  5. 決定向下傳遞回 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 層級顯示:

src/mastra/tools/conditional-tool.ts
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)
}