@mastra/libsql 0.0.0-workflow-timeout-issue-20260220185702 → 0.0.0-workflow-graph-crash-20260527191009
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1085 -3
- package/LICENSE.md +15 -0
- package/dist/docs/SKILL.md +18 -19
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-agent-builder-deploying.md +121 -0
- package/dist/docs/references/docs-agent-builder-overview.md +97 -0
- package/dist/docs/references/docs-agents-agent-approval.md +336 -202
- package/dist/docs/references/docs-agents-networks.md +107 -215
- package/dist/docs/references/docs-memory-memory-processors.md +86 -88
- package/dist/docs/references/docs-memory-message-history.md +71 -63
- package/dist/docs/references/docs-memory-multi-user-threads.md +206 -0
- package/dist/docs/references/docs-memory-overview.md +221 -24
- package/dist/docs/references/docs-memory-semantic-recall.md +162 -75
- package/dist/docs/references/docs-memory-storage.md +72 -73
- package/dist/docs/references/docs-memory-working-memory.md +62 -62
- package/dist/docs/references/docs-rag-retrieval.md +132 -135
- package/dist/docs/references/docs-workflows-snapshots.md +31 -31
- package/dist/docs/references/guides-agent-frameworks-ai-sdk.md +36 -36
- package/dist/docs/references/reference-core-getMemory.md +15 -16
- package/dist/docs/references/reference-core-listMemory.md +17 -18
- package/dist/docs/references/reference-core-mastra-class.md +37 -27
- package/dist/docs/references/reference-memory-memory-class.md +41 -43
- package/dist/docs/references/reference-storage-composite.md +65 -55
- package/dist/docs/references/reference-storage-dynamodb.md +36 -36
- package/dist/docs/references/reference-storage-libsql.md +27 -27
- package/dist/docs/references/reference-vectors-libsql.md +72 -72
- package/dist/index.cjs +7896 -3257
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7888 -3259
- package/dist/index.js.map +1 -1
- package/dist/storage/db/index.d.ts +14 -1
- package/dist/storage/db/index.d.ts.map +1 -1
- package/dist/storage/db/utils.d.ts +16 -0
- package/dist/storage/db/utils.d.ts.map +1 -1
- package/dist/storage/domains/agents/index.d.ts.map +1 -1
- package/dist/storage/domains/background-tasks/index.d.ts +18 -0
- package/dist/storage/domains/background-tasks/index.d.ts.map +1 -0
- package/dist/storage/domains/blobs/index.d.ts +17 -0
- package/dist/storage/domains/blobs/index.d.ts.map +1 -0
- package/dist/storage/domains/channels/index.d.ts +20 -0
- package/dist/storage/domains/channels/index.d.ts.map +1 -0
- package/dist/storage/domains/datasets/index.d.ts +43 -0
- package/dist/storage/domains/datasets/index.d.ts.map +1 -0
- package/dist/storage/domains/experiments/index.d.ts +31 -0
- package/dist/storage/domains/experiments/index.d.ts.map +1 -0
- package/dist/storage/domains/favorites/index.d.ts +17 -0
- package/dist/storage/domains/favorites/index.d.ts.map +1 -0
- package/dist/storage/domains/mcp-clients/index.d.ts.map +1 -1
- package/dist/storage/domains/mcp-servers/index.d.ts +26 -0
- package/dist/storage/domains/mcp-servers/index.d.ts.map +1 -0
- package/dist/storage/domains/memory/index.d.ts +7 -3
- package/dist/storage/domains/memory/index.d.ts.map +1 -1
- package/dist/storage/domains/observability/index.d.ts +2 -1
- package/dist/storage/domains/observability/index.d.ts.map +1 -1
- package/dist/storage/domains/prompt-blocks/index.d.ts.map +1 -1
- package/dist/storage/domains/schedules/index.d.ts +19 -0
- package/dist/storage/domains/schedules/index.d.ts.map +1 -0
- package/dist/storage/domains/scorer-definitions/index.d.ts.map +1 -1
- package/dist/storage/domains/skills/index.d.ts +26 -0
- package/dist/storage/domains/skills/index.d.ts.map +1 -0
- package/dist/storage/domains/workflows/index.d.ts +1 -0
- package/dist/storage/domains/workflows/index.d.ts.map +1 -1
- package/dist/storage/domains/workspaces/index.d.ts +26 -0
- package/dist/storage/domains/workspaces/index.d.ts.map +1 -0
- package/dist/storage/index.d.ts +38 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/vector/index.d.ts +14 -1
- package/dist/vector/index.d.ts.map +1 -1
- package/package.json +12 -12
- package/dist/docs/references/docs-agents-agent-memory.md +0 -212
- package/dist/docs/references/docs-agents-network-approval.md +0 -275
- package/dist/docs/references/docs-observability-overview.md +0 -70
- package/dist/docs/references/docs-observability-tracing-exporters-default.md +0 -211
|
@@ -1,119 +1,127 @@
|
|
|
1
|
-
# Agent
|
|
1
|
+
# Agent approval
|
|
2
2
|
|
|
3
|
-
Agents sometimes require the same [human-in-the-loop](https://mastra.ai/docs/workflows/human-in-the-loop) oversight used in workflows when calling tools that handle sensitive operations, like deleting resources or
|
|
3
|
+
Agents sometimes require the same [human-in-the-loop](https://mastra.ai/docs/workflows/human-in-the-loop) oversight used in workflows when calling tools that handle sensitive operations, like deleting resources or running long processes. With agent approval you can suspend a tool call before it executes so a human can approve or decline it, or let tools suspend themselves to request additional context from the user.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## When to use agent approval
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Destructive or irreversible actions** such as deleting records, sending emails, or processing payments.
|
|
8
|
+
- **Cost-heavy operations** like calling expensive third-party APIs where you want to verify arguments first.
|
|
9
|
+
- **Conditional confirmation** where a tool starts executing and then discovers it needs the user to confirm or supply extra data before finishing.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Quickstart
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
Mark a tool with `requireApproval: true`, then check for the `tool-call-approval` chunk in the stream to approve or decline:
|
|
12
14
|
|
|
13
15
|
```typescript
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
})
|
|
22
|
-
})
|
|
23
|
-
|
|
16
|
+
import { Agent } from '@mastra/core/agent'
|
|
17
|
+
import { createTool } from '@mastra/core/tools'
|
|
18
|
+
import { z } from 'zod'
|
|
19
|
+
|
|
20
|
+
const deleteTool = createTool({
|
|
21
|
+
id: 'delete-record',
|
|
22
|
+
description: 'Delete a record by ID',
|
|
23
|
+
inputSchema: z.object({ id: z.string() }),
|
|
24
|
+
outputSchema: z.object({ deleted: z.boolean() }),
|
|
25
|
+
requireApproval: true,
|
|
26
|
+
execute: async ({ id }) => {
|
|
27
|
+
await db.delete(id)
|
|
28
|
+
return { deleted: true }
|
|
29
|
+
},
|
|
30
|
+
})
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
const agent = new Agent({
|
|
33
|
+
id: 'my-agent',
|
|
34
|
+
name: 'My Agent',
|
|
35
|
+
model: 'openai/gpt-5-mini',
|
|
36
|
+
tools: { deleteTool },
|
|
37
|
+
})
|
|
26
38
|
|
|
27
|
-
|
|
39
|
+
const stream = await agent.stream('Delete record abc-123')
|
|
28
40
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
for await (const chunk of stream.fullStream) {
|
|
42
|
+
if (chunk.type === 'tool-call-approval') {
|
|
43
|
+
const approved = await agent.approveToolCall({ runId: stream.runId })
|
|
44
|
+
for await (const c of approved.textStream) process.stdout.write(c)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
33
47
|
```
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
> **Note:** Agent approval uses snapshots to capture request state. Configure a [storage provider](https://mastra.ai/docs/memory/storage) on your Mastra instance or you'll see a "snapshot not found" error.
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
## How approval works
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
const handleApproval = async () => {
|
|
41
|
-
const approvedStream = await agent.approveToolCall({ runId: stream.runId });
|
|
53
|
+
Mastra offers two distinct mechanisms for pausing tool calls: **pre-execution approval** and **runtime suspension**.
|
|
42
54
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
55
|
+
### Pre-execution approval
|
|
56
|
+
|
|
57
|
+
Pre-execution approval pauses a tool call _before_ its `execute` function runs. The LLM still decides which tool to call and provides arguments, but `execute` doesn't run until you explicitly approve.
|
|
58
|
+
|
|
59
|
+
Two flags control this, combined with OR logic. If _either_ is `true`, the call pauses:
|
|
49
60
|
|
|
50
|
-
|
|
61
|
+
| Flag | Where to set it | Scope |
|
|
62
|
+
| --------------------------- | --------------------------------- | ------------------------------------------- |
|
|
63
|
+
| `requireToolApproval: true` | `stream()` / `generate()` options | Pauses **every** tool call for that request |
|
|
64
|
+
| `requireApproval: true` | `createTool()` definition | Pauses calls to **that specific tool** |
|
|
51
65
|
|
|
52
|
-
|
|
66
|
+
The stream emits a `tool-call-approval` chunk containing the `toolCallId`, `toolName`, and `args`. Call `approveToolCall()` or `declineToolCall()` with the stream's `runId` to continue:
|
|
53
67
|
|
|
54
68
|
```typescript
|
|
55
|
-
const
|
|
56
|
-
|
|
69
|
+
const stream = await agent.stream("What's the weather in London?", {
|
|
70
|
+
requireToolApproval: true,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
for await (const chunk of stream.fullStream) {
|
|
74
|
+
if (chunk.type === 'tool-call-approval') {
|
|
75
|
+
console.log('Tool:', chunk.payload.toolName)
|
|
76
|
+
console.log('Args:', chunk.payload.args)
|
|
77
|
+
|
|
78
|
+
// Approve
|
|
79
|
+
const approved = await agent.approveToolCall({ runId: stream.runId })
|
|
80
|
+
for await (const c of approved.textStream) process.stdout.write(c)
|
|
57
81
|
|
|
58
|
-
|
|
59
|
-
|
|
82
|
+
// Or decline
|
|
83
|
+
const declined = await agent.declineToolCall({ runId: stream.runId })
|
|
84
|
+
for await (const c of declined.textStream) process.stdout.write(c)
|
|
60
85
|
}
|
|
61
|
-
|
|
62
|
-
};
|
|
86
|
+
}
|
|
63
87
|
```
|
|
64
88
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
Tool approval also works with the `generate()` method for non-streaming use cases. When using `generate()` with `requireToolApproval: true`, the method returns immediately when a tool requires approval instead of executing it.
|
|
89
|
+
### Runtime suspension with `suspend()`
|
|
68
90
|
|
|
69
|
-
|
|
91
|
+
A tool can also pause _during_ its `execute` function by calling `suspend()`. This is useful when the tool starts running and then discovers it needs additional user input or confirmation before it can finish.
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
The stream emits a `tool-call-suspended` chunk with a custom payload defined by the tool's `suspendSchema`. You resume by calling `resumeStream()` with data matching the tool's `resumeSchema`.
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
- `suspendPayload` - contains tool call details (`toolCallId`, `toolName`, `args`)
|
|
75
|
-
- `runId` - needed to approve or decline the tool call
|
|
95
|
+
> **Note:** `suspend()` does not throw — return immediately after calling it (e.g. `return await suspend({ ... })`). Code after `await suspend(...)` still runs before the tool pauses.
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
## Tool approval with `generate()`
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
Tool approval also works with `generate()` for non-streaming use cases. When a tool requires approval, `generate()` returns immediately with `finishReason: 'suspended'`, a `suspendPayload` containing the tool call details (`toolCallId`, `toolName`, `args`), and a `runId`:
|
|
80
100
|
|
|
81
101
|
```typescript
|
|
82
|
-
const output = await agent.generate(
|
|
102
|
+
const output = await agent.generate('Find user John', {
|
|
83
103
|
requireToolApproval: true,
|
|
84
|
-
})
|
|
104
|
+
})
|
|
85
105
|
|
|
86
|
-
if (output.finishReason ===
|
|
87
|
-
console.log(
|
|
88
|
-
console.log("Arguments:", output.suspendPayload.args);
|
|
106
|
+
if (output.finishReason === 'suspended') {
|
|
107
|
+
console.log('Tool requires approval:', output.suspendPayload.toolName)
|
|
89
108
|
|
|
90
|
-
// Approve
|
|
109
|
+
// Approve
|
|
91
110
|
const result = await agent.approveToolCallGenerate({
|
|
92
111
|
runId: output.runId,
|
|
93
112
|
toolCallId: output.suspendPayload.toolCallId,
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
console.log("Final result:", result.text);
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Declining tool calls
|
|
101
|
-
|
|
102
|
-
To decline a tool call, use the `declineToolCallGenerate` method:
|
|
113
|
+
})
|
|
114
|
+
console.log('Final result:', result.text)
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
if (output.finishReason === "suspended") {
|
|
116
|
+
// Or decline
|
|
106
117
|
const result = await agent.declineToolCallGenerate({
|
|
107
118
|
runId: output.runId,
|
|
108
119
|
toolCallId: output.suspendPayload.toolCallId,
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
// Agent will respond acknowledging the declined tool
|
|
112
|
-
console.log(result.text);
|
|
120
|
+
})
|
|
113
121
|
}
|
|
114
122
|
```
|
|
115
123
|
|
|
116
|
-
### Stream vs
|
|
124
|
+
### Stream vs generate comparison
|
|
117
125
|
|
|
118
126
|
| Aspect | `stream()` | `generate()` |
|
|
119
127
|
| ------------------ | ---------------------------- | ------------------------------------------------ |
|
|
@@ -123,224 +131,191 @@ if (output.finishReason === "suspended") {
|
|
|
123
131
|
| Decline method | `declineToolCall({ runId })` | `declineToolCallGenerate({ runId, toolCallId })` |
|
|
124
132
|
| Result | Stream to iterate | Full output object |
|
|
125
133
|
|
|
134
|
+
> **Note:** `toolCallId` is optional on all four methods. Pass it when multiple tool calls may be pending at the same time (common in supervisor agents). When omitted, the agent resumes the most recent suspended tool call.
|
|
135
|
+
|
|
126
136
|
## Tool-level approval
|
|
127
137
|
|
|
128
|
-
|
|
138
|
+
Instead of pausing every tool call at the agent level, you can mark individual tools as requiring approval. This gives you granular control: only specific tools pause, while others execute immediately.
|
|
129
139
|
|
|
130
|
-
###
|
|
140
|
+
### Approval using `requireApproval`
|
|
131
141
|
|
|
132
|
-
|
|
142
|
+
Set `requireApproval: true` on a tool definition. The tool pauses before execution regardless of whether `requireToolApproval` is set on the agent:
|
|
133
143
|
|
|
134
144
|
```typescript
|
|
135
145
|
export const testTool = createTool({
|
|
136
|
-
id:
|
|
137
|
-
description:
|
|
146
|
+
id: 'test-tool',
|
|
147
|
+
description: 'Fetches weather for a location',
|
|
138
148
|
inputSchema: z.object({
|
|
139
|
-
location: z.string()
|
|
149
|
+
location: z.string(),
|
|
140
150
|
}),
|
|
141
151
|
outputSchema: z.object({
|
|
142
|
-
weather: z.string()
|
|
152
|
+
weather: z.string(),
|
|
143
153
|
}),
|
|
144
154
|
resumeSchema: z.object({
|
|
145
|
-
approved: z.boolean()
|
|
155
|
+
approved: z.boolean(),
|
|
146
156
|
}),
|
|
147
|
-
execute: async
|
|
148
|
-
const response = await fetch(`https://wttr.in/${inputData.location}?format=3`)
|
|
149
|
-
const weather = await response.text()
|
|
157
|
+
execute: async inputData => {
|
|
158
|
+
const response = await fetch(`https://wttr.in/${inputData.location}?format=3`)
|
|
159
|
+
const weather = await response.text()
|
|
150
160
|
|
|
151
|
-
return { weather }
|
|
161
|
+
return { weather }
|
|
152
162
|
},
|
|
153
|
-
requireApproval: true
|
|
154
|
-
})
|
|
163
|
+
requireApproval: true,
|
|
164
|
+
})
|
|
155
165
|
```
|
|
156
166
|
|
|
157
|
-
When `requireApproval` is true
|
|
167
|
+
When `requireApproval` is `true`, the stream emits `tool-call-approval` chunks the same way agent-level approval does. Use `approveToolCall()` or `declineToolCall()` to continue:
|
|
158
168
|
|
|
159
169
|
```typescript
|
|
160
|
-
const stream = await agent.stream("What's the weather in London?")
|
|
170
|
+
const stream = await agent.stream("What's the weather in London?")
|
|
161
171
|
|
|
162
172
|
for await (const chunk of stream.fullStream) {
|
|
163
|
-
if (chunk.type ===
|
|
164
|
-
console.log(
|
|
173
|
+
if (chunk.type === 'tool-call-approval') {
|
|
174
|
+
console.log('Approval required for:', chunk.payload.toolName)
|
|
165
175
|
}
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
const
|
|
169
|
-
const
|
|
178
|
+
const handleApproval = async () => {
|
|
179
|
+
const approvedStream = await agent.approveToolCall({ runId: stream.runId })
|
|
170
180
|
|
|
171
|
-
for await (const chunk of
|
|
172
|
-
process.stdout.write(chunk)
|
|
181
|
+
for await (const chunk of approvedStream.textStream) {
|
|
182
|
+
process.stdout.write(chunk)
|
|
173
183
|
}
|
|
174
|
-
process.stdout.write(
|
|
175
|
-
}
|
|
184
|
+
process.stdout.write('\n')
|
|
185
|
+
}
|
|
176
186
|
```
|
|
177
187
|
|
|
178
|
-
###
|
|
188
|
+
### Approval using `suspend()`
|
|
179
189
|
|
|
180
|
-
With this approach, neither the agent nor the tool uses `requireApproval`. Instead, the tool
|
|
190
|
+
With this approach, neither the agent nor the tool uses `requireApproval`. Instead, the tool's `execute` function calls `suspend()` to pause at a specific point and return context or confirmation prompts to the user. This is useful when approval depends on runtime conditions rather than being unconditional.
|
|
181
191
|
|
|
182
192
|
```typescript
|
|
183
193
|
export const testToolB = createTool({
|
|
184
|
-
id:
|
|
185
|
-
description:
|
|
194
|
+
id: 'test-tool-b',
|
|
195
|
+
description: 'Fetches weather for a location',
|
|
186
196
|
inputSchema: z.object({
|
|
187
|
-
location: z.string()
|
|
197
|
+
location: z.string(),
|
|
188
198
|
}),
|
|
189
199
|
outputSchema: z.object({
|
|
190
|
-
weather: z.string()
|
|
200
|
+
weather: z.string(),
|
|
191
201
|
}),
|
|
192
202
|
resumeSchema: z.object({
|
|
193
|
-
approved: z.boolean()
|
|
203
|
+
approved: z.boolean(),
|
|
194
204
|
}),
|
|
195
205
|
suspendSchema: z.object({
|
|
196
|
-
reason: z.string()
|
|
206
|
+
reason: z.string(),
|
|
197
207
|
}),
|
|
198
208
|
execute: async (inputData, context) => {
|
|
199
|
-
const { resumeData: { approved } = {}, suspend } = context?.agent ?? {}
|
|
209
|
+
const { resumeData: { approved } = {}, suspend } = context?.agent ?? {}
|
|
200
210
|
|
|
201
211
|
if (!approved) {
|
|
202
|
-
return suspend?.({ reason:
|
|
212
|
+
return suspend?.({ reason: 'Approval required.' })
|
|
203
213
|
}
|
|
204
214
|
|
|
205
|
-
const response = await fetch(`https://wttr.in/${inputData.location}?format=3`)
|
|
206
|
-
const weather = await response.text()
|
|
215
|
+
const response = await fetch(`https://wttr.in/${inputData.location}?format=3`)
|
|
216
|
+
const weather = await response.text()
|
|
207
217
|
|
|
208
|
-
return { weather }
|
|
209
|
-
}
|
|
210
|
-
})
|
|
218
|
+
return { weather }
|
|
219
|
+
},
|
|
220
|
+
})
|
|
211
221
|
```
|
|
212
222
|
|
|
213
|
-
With this approach the stream
|
|
223
|
+
With this approach the stream includes a `tool-call-suspended` chunk, and the `suspendPayload` contains the `reason` defined by the tool's `suspendSchema`. Call `resumeStream` with the `resumeSchema` data and `runId` to continue:
|
|
214
224
|
|
|
215
225
|
```typescript
|
|
216
|
-
const stream = await agent.stream("What's the weather in London?")
|
|
226
|
+
const stream = await agent.stream("What's the weather in London?")
|
|
217
227
|
|
|
218
228
|
for await (const chunk of stream.fullStream) {
|
|
219
|
-
if (chunk.type ===
|
|
220
|
-
console.log(chunk.payload.suspendPayload)
|
|
229
|
+
if (chunk.type === 'tool-call-suspended') {
|
|
230
|
+
console.log(chunk.payload.suspendPayload)
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
|
|
224
234
|
const handleResume = async () => {
|
|
225
|
-
const resumedStream = await agent.resumeStream({ approved: true }, { runId: stream.runId })
|
|
235
|
+
const resumedStream = await agent.resumeStream({ approved: true }, { runId: stream.runId })
|
|
226
236
|
|
|
227
237
|
for await (const chunk of resumedStream.textStream) {
|
|
228
|
-
process.stdout.write(chunk)
|
|
238
|
+
process.stdout.write(chunk)
|
|
229
239
|
}
|
|
230
|
-
process.stdout.write(
|
|
231
|
-
}
|
|
240
|
+
process.stdout.write('\n')
|
|
241
|
+
}
|
|
232
242
|
```
|
|
233
243
|
|
|
234
244
|
## Automatic tool resumption
|
|
235
245
|
|
|
236
|
-
When using tools that call `suspend()`, you can enable automatic resumption so the agent resumes suspended tools based on the user's next message.
|
|
237
|
-
|
|
238
|
-
### Enabling auto-resume
|
|
239
|
-
|
|
240
|
-
Set `autoResumeSuspendedTools` to `true` in the agent's default options or when calling `stream()`:
|
|
246
|
+
When using tools that call `suspend()`, you can enable automatic resumption so the agent resumes suspended tools based on the user's next message. Set `autoResumeSuspendedTools` to `true` in the agent's default options or per-request:
|
|
241
247
|
|
|
242
248
|
```typescript
|
|
243
|
-
import { Agent } from
|
|
244
|
-
import { Memory } from
|
|
249
|
+
import { Agent } from '@mastra/core/agent'
|
|
250
|
+
import { Memory } from '@mastra/memory'
|
|
245
251
|
|
|
246
|
-
// Option 1: In agent configuration
|
|
247
252
|
const agent = new Agent({
|
|
248
|
-
id:
|
|
249
|
-
name:
|
|
250
|
-
instructions:
|
|
251
|
-
model:
|
|
253
|
+
id: 'my-agent',
|
|
254
|
+
name: 'My Agent',
|
|
255
|
+
instructions: 'You are a helpful assistant',
|
|
256
|
+
model: 'openai/gpt-5-mini',
|
|
252
257
|
tools: { weatherTool },
|
|
253
258
|
memory: new Memory(),
|
|
254
259
|
defaultOptions: {
|
|
255
260
|
autoResumeSuspendedTools: true,
|
|
256
261
|
},
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
// Option 2: Per-request
|
|
260
|
-
const stream = await agent.stream("What's the weather?", {
|
|
261
|
-
autoResumeSuspendedTools: true,
|
|
262
|
-
});
|
|
262
|
+
})
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
When `autoResumeSuspendedTools` is enabled:
|
|
268
|
-
|
|
269
|
-
1. A tool suspends execution by calling `suspend()` with a payload (e.g., requesting more information)
|
|
270
|
-
|
|
271
|
-
2. The suspension is persisted to memory along with the conversation
|
|
272
|
-
|
|
273
|
-
3. When the user sends their next message on the same thread, the agent:
|
|
274
|
-
|
|
275
|
-
- Detects the suspended tool from message history
|
|
276
|
-
- Extracts `resumeData` from the user's message based on the tool's `resumeSchema`
|
|
277
|
-
- Automatically resumes the tool with the extracted data
|
|
278
|
-
|
|
279
|
-
### Example
|
|
265
|
+
When enabled, the agent detects suspended tools from message history on the next user message, extracts `resumeData` based on the tool's `resumeSchema`, and automatically resumes the tool. The following example shows a complete conversational flow:
|
|
280
266
|
|
|
281
267
|
```typescript
|
|
282
|
-
import { createTool } from
|
|
283
|
-
import { z } from
|
|
268
|
+
import { createTool } from '@mastra/core/tools'
|
|
269
|
+
import { z } from 'zod'
|
|
284
270
|
|
|
285
|
-
|
|
286
|
-
id:
|
|
287
|
-
description:
|
|
271
|
+
const weatherTool = createTool({
|
|
272
|
+
id: 'weather-tool',
|
|
273
|
+
description: 'Fetches weather for a city',
|
|
274
|
+
inputSchema: z.object({
|
|
275
|
+
city: z.string(),
|
|
276
|
+
}),
|
|
277
|
+
outputSchema: z.object({
|
|
278
|
+
weather: z.string(),
|
|
279
|
+
}),
|
|
288
280
|
suspendSchema: z.object({
|
|
289
281
|
message: z.string(),
|
|
290
282
|
}),
|
|
291
283
|
resumeSchema: z.object({
|
|
292
284
|
city: z.string(),
|
|
293
285
|
}),
|
|
294
|
-
execute: async (
|
|
295
|
-
|
|
296
|
-
if (!context?.agent?.resumeData) {
|
|
297
|
-
// First call - suspend and ask for the city
|
|
298
|
-
return context?.agent?.suspend({
|
|
299
|
-
message: "What city do you want to know the weather for?",
|
|
300
|
-
});
|
|
301
|
-
}
|
|
286
|
+
execute: async (inputData, context) => {
|
|
287
|
+
const { resumeData, suspend } = context?.agent ?? {}
|
|
302
288
|
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
289
|
+
// If no city provided, ask the user
|
|
290
|
+
if (!inputData.city && !resumeData?.city) {
|
|
291
|
+
return suspend?.({ message: 'What city do you want to know the weather for?' })
|
|
292
|
+
}
|
|
307
293
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
294
|
+
const city = resumeData?.city ?? inputData.city
|
|
295
|
+
const response = await fetch(`https://wttr.in/${city}?format=3`)
|
|
296
|
+
const weather = await response.text()
|
|
311
297
|
|
|
312
|
-
|
|
313
|
-
id: "my-agent",
|
|
314
|
-
name: "My Agent",
|
|
315
|
-
instructions: "You are a helpful assistant",
|
|
316
|
-
model: "openai/gpt-4o-mini",
|
|
317
|
-
tools: { weatherTool },
|
|
318
|
-
memory: new Memory(),
|
|
319
|
-
defaultOptions: {
|
|
320
|
-
autoResumeSuspendedTools: true,
|
|
298
|
+
return { weather: `${city}: ${weather}` }
|
|
321
299
|
},
|
|
322
|
-
})
|
|
300
|
+
})
|
|
301
|
+
```
|
|
323
302
|
|
|
324
|
-
|
|
303
|
+
```typescript
|
|
304
|
+
const stream = await agent.stream("What's the weather like?")
|
|
325
305
|
|
|
326
306
|
for await (const chunk of stream.fullStream) {
|
|
327
|
-
if (chunk.type ===
|
|
328
|
-
console.log(chunk.payload.suspendPayload)
|
|
307
|
+
if (chunk.type === 'tool-call-suspended') {
|
|
308
|
+
console.log(chunk.payload.suspendPayload)
|
|
329
309
|
}
|
|
330
310
|
}
|
|
331
311
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
process.stdout.write("\n");
|
|
339
|
-
};
|
|
312
|
+
// User sends follow-up on the same thread
|
|
313
|
+
const resumedStream = await agent.stream('San Francisco')
|
|
314
|
+
for await (const chunk of resumedStream.textStream) {
|
|
315
|
+
process.stdout.write(chunk)
|
|
316
|
+
}
|
|
340
317
|
```
|
|
341
318
|
|
|
342
|
-
**Conversation flow:**
|
|
343
|
-
|
|
344
319
|
```text
|
|
345
320
|
User: "What's the weather like?"
|
|
346
321
|
Agent: "What city do you want to know the weather for?"
|
|
@@ -349,7 +324,7 @@ User: "San Francisco"
|
|
|
349
324
|
Agent: "The weather in San Francisco is: San Francisco: ☀️ +72°F"
|
|
350
325
|
```
|
|
351
326
|
|
|
352
|
-
The second message automatically resumes the suspended tool
|
|
327
|
+
The second message automatically resumes the suspended tool. The agent extracts `{ city: "San Francisco" }` from the user's message and passes it as `resumeData`.
|
|
353
328
|
|
|
354
329
|
### Requirements
|
|
355
330
|
|
|
@@ -368,10 +343,169 @@ For automatic tool resumption to work:
|
|
|
368
343
|
|
|
369
344
|
Both approaches work with the same tool definitions. Automatic resumption triggers only when suspended tools exist in the message history and the user sends a new message on the same thread.
|
|
370
345
|
|
|
346
|
+
## Tool approval: Supervisor agents
|
|
347
|
+
|
|
348
|
+
A [supervisor agent](https://mastra.ai/docs/agents/supervisor-agents) coordinates multiple subagents using `.stream()` or `.generate()`. When a subagent calls a tool that requires approval, the request propagates up through the delegation chain and surfaces at the supervisor level:
|
|
349
|
+
|
|
350
|
+
1. The supervisor delegates a task to a subagent.
|
|
351
|
+
2. The subagent calls a tool that has `requireApproval: true` or uses `suspend()`.
|
|
352
|
+
3. The approval request bubbles up to the supervisor.
|
|
353
|
+
4. You approve or decline at the supervisor level.
|
|
354
|
+
5. The decision propagates back down to the subagent.
|
|
355
|
+
|
|
356
|
+
Tool approvals also propagate through multiple levels of delegation. If a supervisor delegates to subagent A, which delegates to subagent B that has a tool with `requireApproval: true`, the approval request still surfaces at the top-level supervisor.
|
|
357
|
+
|
|
358
|
+
### Approve and decline in supervisor agents
|
|
359
|
+
|
|
360
|
+
The example below creates a subagent with a tool requiring approval. When the tool triggers an approval request, it surfaces in the supervisor's stream as a `tool-call-approval` chunk:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { Agent } from '@mastra/core/agent'
|
|
364
|
+
import { createTool } from '@mastra/core/tools'
|
|
365
|
+
import { Memory } from '@mastra/memory'
|
|
366
|
+
import { z } from 'zod'
|
|
367
|
+
|
|
368
|
+
const findUserTool = createTool({
|
|
369
|
+
id: 'find-user',
|
|
370
|
+
description: 'Finds user by ID in the database',
|
|
371
|
+
inputSchema: z.object({
|
|
372
|
+
userId: z.string(),
|
|
373
|
+
}),
|
|
374
|
+
outputSchema: z.object({
|
|
375
|
+
user: z.object({
|
|
376
|
+
id: z.string(),
|
|
377
|
+
name: z.string(),
|
|
378
|
+
email: z.string(),
|
|
379
|
+
}),
|
|
380
|
+
}),
|
|
381
|
+
requireApproval: true,
|
|
382
|
+
execute: async input => {
|
|
383
|
+
const user = await database.findUser(input.userId)
|
|
384
|
+
return { user }
|
|
385
|
+
},
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const dataAgent = new Agent({
|
|
389
|
+
id: 'data-agent',
|
|
390
|
+
name: 'Data Agent',
|
|
391
|
+
description: 'Handles database queries and user data retrieval',
|
|
392
|
+
model: 'openai/gpt-5-mini',
|
|
393
|
+
tools: { findUserTool },
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
const supervisorAgent = new Agent({
|
|
397
|
+
id: 'supervisor',
|
|
398
|
+
name: 'Supervisor Agent',
|
|
399
|
+
instructions: `You coordinate data retrieval tasks.
|
|
400
|
+
Delegate to data-agent for user lookups.`,
|
|
401
|
+
model: 'openai/gpt-5.4',
|
|
402
|
+
agents: { dataAgent },
|
|
403
|
+
memory: new Memory(),
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
const stream = await supervisorAgent.stream('Find user with ID 12345')
|
|
407
|
+
|
|
408
|
+
for await (const chunk of stream.fullStream) {
|
|
409
|
+
if (chunk.type === 'tool-call-approval') {
|
|
410
|
+
console.log('Tool requires approval:', chunk.payload.toolName)
|
|
411
|
+
console.log('Arguments:', chunk.payload.args)
|
|
412
|
+
|
|
413
|
+
// Approve the tool call
|
|
414
|
+
const resumeStream = await supervisorAgent.approveToolCall({
|
|
415
|
+
runId: stream.runId,
|
|
416
|
+
toolCallId: chunk.payload.toolCallId,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
for await (const resumeChunk of resumeStream.textStream) {
|
|
420
|
+
process.stdout.write(resumeChunk)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// To decline instead, use:
|
|
424
|
+
const declineStream = await supervisorAgent.declineToolCall({
|
|
425
|
+
runId: stream.runId,
|
|
426
|
+
toolCallId: chunk.payload.toolCallId,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Use `suspend()` in supervisor agents
|
|
433
|
+
|
|
434
|
+
Tools can also use [`suspend()`](#approval-using-suspend) to pause execution and return context to the user. This approach works through the supervisor delegation chain the same way `requireApproval` does: the suspension surfaces at the supervisor level:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
const conditionalTool = createTool({
|
|
438
|
+
id: 'conditional-operation',
|
|
439
|
+
description: 'Performs an operation that may require confirmation',
|
|
440
|
+
inputSchema: z.object({
|
|
441
|
+
operation: z.string(),
|
|
442
|
+
}),
|
|
443
|
+
suspendSchema: z.object({
|
|
444
|
+
message: z.string(),
|
|
445
|
+
}),
|
|
446
|
+
resumeSchema: z.object({
|
|
447
|
+
confirmed: z.boolean(),
|
|
448
|
+
}),
|
|
449
|
+
execute: async (input, context) => {
|
|
450
|
+
const { resumeData } = context?.agent ?? {}
|
|
451
|
+
|
|
452
|
+
if (!resumeData?.confirmed) {
|
|
453
|
+
return context?.agent?.suspend({
|
|
454
|
+
message: `Confirm: ${input.operation}?`,
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Proceed with operation
|
|
459
|
+
return await performOperation(input.operation)
|
|
460
|
+
},
|
|
461
|
+
})
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
// When using this tool through a subagent in supervisor agents
|
|
466
|
+
for await (const chunk of stream.fullStream) {
|
|
467
|
+
if (chunk.type === 'tool-call-suspended') {
|
|
468
|
+
console.log('Tool suspended:', chunk.payload.suspendPayload.message)
|
|
469
|
+
|
|
470
|
+
// Resume with confirmation
|
|
471
|
+
const resumeStream = await supervisorAgent.resumeStream(
|
|
472
|
+
{ confirmed: true },
|
|
473
|
+
{ runId: stream.runId },
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
for await (const resumeChunk of resumeStream.textStream) {
|
|
477
|
+
process.stdout.write(resumeChunk)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### Supervisor approval with `generate()`
|
|
484
|
+
|
|
485
|
+
Tool approval propagation also works with `generate()` in supervisor agents:
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
const output = await supervisorAgent.generate('Find user with ID 12345', {
|
|
489
|
+
maxSteps: 10,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
if (output.finishReason === 'suspended') {
|
|
493
|
+
console.log('Tool requires approval:', output.suspendPayload.toolName)
|
|
494
|
+
|
|
495
|
+
// Approve
|
|
496
|
+
const result = await supervisorAgent.approveToolCallGenerate({
|
|
497
|
+
runId: output.runId,
|
|
498
|
+
toolCallId: output.suspendPayload.toolCallId,
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
console.log('Final result:', result.text)
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
371
505
|
## Related
|
|
372
506
|
|
|
373
|
-
- [
|
|
374
|
-
- [Agent
|
|
375
|
-
- [
|
|
376
|
-
- [
|
|
377
|
-
- [Request
|
|
507
|
+
- [Tools](https://mastra.ai/docs/agents/using-tools)
|
|
508
|
+
- [Agent overview](https://mastra.ai/docs/agents/overview)
|
|
509
|
+
- [MCP overview](https://mastra.ai/docs/mcp/overview)
|
|
510
|
+
- [Memory](https://mastra.ai/docs/memory/overview)
|
|
511
|
+
- [Request context](https://mastra.ai/docs/server/request-context)
|