@mastra/libsql 1.18.0-alpha.1 → 1.19.0-alpha.0
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 +47 -0
- package/dist/docs/SKILL.md +4 -3
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-agent-builder-deploying.md +2 -2
- package/dist/docs/references/docs-agent-builder-overview.md +1 -1
- package/dist/docs/references/docs-agents-agent-approval.md +85 -11
- package/dist/docs/references/docs-agents-networks.md +2 -2
- package/dist/docs/references/docs-deployment-workers.md +137 -0
- package/dist/docs/references/docs-memory-memory-processors.md +10 -10
- package/dist/docs/references/docs-memory-message-history.md +11 -5
- package/dist/docs/references/docs-memory-multi-user-threads.md +6 -6
- package/dist/docs/references/docs-memory-overview.md +10 -10
- package/dist/docs/references/docs-memory-semantic-recall.md +3 -3
- package/dist/docs/references/docs-memory-working-memory.md +8 -8
- package/dist/docs/references/docs-rag-retrieval.md +18 -18
- package/dist/docs/references/docs-storage-overview.md +2 -2
- package/dist/docs/references/docs-workflows-snapshots.md +3 -3
- package/dist/docs/references/reference-core-mastra-class.md +48 -1
- package/dist/docs/references/reference-file-based-agents-storage.md +1 -1
- package/dist/docs/references/reference-memory-memory-class.md +4 -4
- package/dist/docs/references/reference-storage-dynamodb.md +7 -7
- package/dist/docs/references/reference-storage-retention.md +33 -33
- package/dist/docs/references/reference-vectors-libsql.md +2 -2
- package/dist/index.cjs +24 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/storage/domains/datasets/index.d.ts.map +1 -1
- package/dist/storage/domains/experiments/index.d.ts.map +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @mastra/libsql
|
|
2
2
|
|
|
3
|
+
## 1.19.0-alpha.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added persistence for dataset item undeclared tool policies. ([#19643](https://github.com/mastra-ai/mastra/pull/19643))
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
await dataset.addItem({
|
|
11
|
+
input: 'What is the weather?',
|
|
12
|
+
unmockedToolPolicy: 'deny',
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Added a comment column to experiment results so review comments persist. The column is added automatically and non-destructively on startup for existing databases (https://github.com/mastra-ai/mastra/issues/19857). ([#19865](https://github.com/mastra-ai/mastra/pull/19865))
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [[`c5e56ff`](https://github.com/mastra-ai/mastra/commit/c5e56ff3bcabdf062708f2d48744fec304df6792), [`4e35a56`](https://github.com/mastra-ai/mastra/commit/4e35a56cdf8d74a5ff6d5eda01f2c1deaf6cc7be)]:
|
|
21
|
+
- @mastra/core@1.56.0-alpha.1
|
|
22
|
+
|
|
23
|
+
## 1.18.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- Added exact metadata filtering to message history queries across Memory APIs and supported storage providers. ([#19991](https://github.com/mastra-ai/mastra/pull/19991))
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const messages = await memory.recall({
|
|
31
|
+
threadId: 'thread-1',
|
|
32
|
+
filter: {
|
|
33
|
+
metadata: {
|
|
34
|
+
status: 'done',
|
|
35
|
+
priority: 'high',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Multiple fields use AND semantics. Supported values are strings, finite numbers, booleans, and `null`.
|
|
42
|
+
|
|
43
|
+
### Patch Changes
|
|
44
|
+
|
|
45
|
+
- Fixed Factory database lock errors by serializing writes that share a LibSQL connection. ([#20278](https://github.com/mastra-ai/mastra/pull/20278))
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [[`ce93a3c`](https://github.com/mastra-ai/mastra/commit/ce93a3c114ea1cbfbd576f3db41d7c26c9844f5b), [`5718a22`](https://github.com/mastra-ai/mastra/commit/5718a229281dcfd36bcd1f42a242e3717e510a33), [`a211d09`](https://github.com/mastra-ai/mastra/commit/a211d09185dc65a746534914cf38b67f21ee9bac), [`0dca9d0`](https://github.com/mastra-ai/mastra/commit/0dca9d0b1356024a53b72ea6f040db528b126caa), [`6218217`](https://github.com/mastra-ai/mastra/commit/62182171b6cfca0b099f1c6a77a2e65e7639ab86), [`5807d3a`](https://github.com/mastra-ai/mastra/commit/5807d3ae1d259b8b7d6df7e5bf2b485c694af9c8), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`05db566`](https://github.com/mastra-ai/mastra/commit/05db566fcbdcbf33d0bffca0c72ec30129e2e3ca), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`5718a22`](https://github.com/mastra-ai/mastra/commit/5718a229281dcfd36bcd1f42a242e3717e510a33), [`57661af`](https://github.com/mastra-ai/mastra/commit/57661afeca52ff9af4e72675ede2134fa503d5a5), [`d1b7e3a`](https://github.com/mastra-ai/mastra/commit/d1b7e3a978a309a5653eeaa490d2d6c7c53bd093), [`29c584a`](https://github.com/mastra-ai/mastra/commit/29c584a13a88831e5ed1fdeb0ff8e82eae180433), [`c093146`](https://github.com/mastra-ai/mastra/commit/c0931466404d3c521308ea119cb165bb7e695155), [`8124754`](https://github.com/mastra-ai/mastra/commit/8124754ae89fbc69f8136d1df4a91904d0f84c4e), [`d12b2e4`](https://github.com/mastra-ai/mastra/commit/d12b2e4023fd9e3d3e93a9169f5088bcee2a849c)]:
|
|
48
|
+
- @mastra/core@1.54.0
|
|
49
|
+
|
|
3
50
|
## 1.18.0-alpha.1
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
package/dist/docs/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: mastra-libsql
|
|
|
3
3
|
description: Documentation for @mastra/libsql. Use when working with @mastra/libsql APIs, configuration, or implementation.
|
|
4
4
|
metadata:
|
|
5
5
|
package: "@mastra/libsql"
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.19.0-alpha.0"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## When to use
|
|
@@ -18,8 +18,9 @@ Read the individual reference documents for detailed explanations and code examp
|
|
|
18
18
|
|
|
19
19
|
- [Deploying](references/docs-agent-builder-deploying.md) - Swap local Agent Builder primitives for cloud-backed storage, filesystems, sandboxes, an EE license, auth, and a public channel URL for production.
|
|
20
20
|
- [Agent Builder overview](references/docs-agent-builder-overview.md) - Let teammates create, configure, and operate Mastra agents from a browser, with admin-pinned defaults, RBAC, and channel integrations.
|
|
21
|
-
- [Agent approval](references/docs-agents-agent-approval.md) - Learn how to require approvals
|
|
21
|
+
- [Agent approval](references/docs-agents-agent-approval.md) - Learn how to require approvals and suspend tool execution, plus automatically resume suspended tools while keeping humans in control of agent workflows.
|
|
22
22
|
- [Agent networks](references/docs-agents-networks.md) - Coordinate multiple agents, workflows, and tools using agent networks for complex, non-deterministic task execution.
|
|
23
|
+
- [Workers](references/docs-deployment-workers.md) - Separate background processing from the API layer by running workflow execution, cron schedules, and background tasks in dedicated worker processes.
|
|
23
24
|
- [Memory processors](references/docs-memory-memory-processors.md) - Learn how to use memory processors in Mastra to filter, trim, and transform messages before they're sent to the language model to manage context window limits.
|
|
24
25
|
- [Message history](references/docs-memory-message-history.md) - Learn how to configure message history in Mastra to store recent messages from the current conversation.
|
|
25
26
|
- [Multi-user threads](references/docs-memory-multi-user-threads.md) - Share one Mastra thread between multiple users by carrying speaker identity in the message body.
|
|
@@ -41,7 +42,7 @@ Read the individual reference documents for detailed explanations and code examp
|
|
|
41
42
|
- [Reference: Mastra class](references/reference-core-mastra-class.md) - Documentation for the `Mastra` class in Mastra, the core entry point for managing agents, workflows, MCP servers, and server endpoints.
|
|
42
43
|
- [Memory](references/reference-file-based-agents-memory.md) - Give a file-based agent persistent memory with a memory.ts module.
|
|
43
44
|
- [Storage](references/reference-file-based-agents-storage.md) - Set the default Mastra store by file convention with storage.ts.
|
|
44
|
-
- [Reference: Memory class](references/reference-memory-memory-class.md) - Documentation for the `Memory` class in Mastra, which provides a
|
|
45
|
+
- [Reference: Memory class](references/reference-memory-memory-class.md) - Documentation for the `Memory` class in Mastra, which provides a reliable system for managing conversation history and thread-based message storage.
|
|
45
46
|
- [Reference: Composite storage](references/reference-storage-composite.md) - Documentation for combining multiple storage backends in Mastra.
|
|
46
47
|
- [Reference: DynamoDB storage](references/reference-storage-dynamodb.md) - Documentation for the DynamoDB storage implementation in Mastra, using a single-table design with ElectroDB.
|
|
47
48
|
- [Reference: libSQL storage](references/reference-storage-libsql.md) - Documentation for the libSQL storage implementation in Mastra.
|
|
@@ -10,8 +10,8 @@ Production deployments swap the local primitives in the Quickstart for cloud-bac
|
|
|
10
10
|
|
|
11
11
|
1. **EE license**: A valid `MASTRA_EE_LICENSE` so the server will start with the Builder enabled.
|
|
12
12
|
2. **Hosted storage**: A shared store for agents, skills, runs, and memory.
|
|
13
|
-
3. **Shared workspace filesystem**: Survives across instances
|
|
14
|
-
4. **Cloud sandbox**: Runs agent commands safely
|
|
13
|
+
3. **Shared workspace filesystem**: Survives across instances. `local` is single-node only.
|
|
14
|
+
4. **Cloud sandbox**: Runs agent commands safely. `local` is unsafe in shared environments.
|
|
15
15
|
5. **Auth and RBAC**: Gates the Builder UI and `/agent-builder/*` routes.
|
|
16
16
|
6. **Public base URL for channels**: Slack and other channel providers need a reachable URL.
|
|
17
17
|
|
|
@@ -58,7 +58,7 @@ yarn add @mastra/editor @mastra/libsql
|
|
|
58
58
|
bun add @mastra/editor @mastra/libsql
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
The example below defines a storage adapter
|
|
61
|
+
The example below defines a storage adapter and registers a builder agent, plus enables the editor as explained in the prerequisites:
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
64
|
import { Mastra } from '@mastra/core/mastra'
|
|
@@ -6,7 +6,7 @@ Agents sometimes require the same [human-in-the-loop](https://mastra.ai/docs/wor
|
|
|
6
6
|
|
|
7
7
|
## When to use agent approval
|
|
8
8
|
|
|
9
|
-
- **Destructive or irreversible actions** such as deleting records
|
|
9
|
+
- **Destructive or irreversible actions** such as deleting records or sending emails, or alternatively processing payments.
|
|
10
10
|
- **Cost-heavy operations** like calling expensive third-party APIs where you want to verify arguments first.
|
|
11
11
|
- **Conditional confirmation** where a tool starts executing and then discovers it needs the user to confirm or supply extra data before finishing.
|
|
12
12
|
|
|
@@ -60,7 +60,7 @@ Mastra offers two distinct mechanisms for pausing tool calls: **pre-execution ap
|
|
|
60
60
|
|
|
61
61
|
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.
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
The flags control this, combined with OR logic. If _either_ is `true`, the call pauses:
|
|
64
64
|
|
|
65
65
|
| Flag | Where to set it | Scope |
|
|
66
66
|
| --------------------------- | --------------------------------- | ------------------------------------------- |
|
|
@@ -92,7 +92,7 @@ for await (const chunk of stream.fullStream) {
|
|
|
92
92
|
|
|
93
93
|
#### Conditional approval with a function
|
|
94
94
|
|
|
95
|
-
Instead of a boolean, `requireToolApproval` accepts a function that decides per tool call. It receives the `toolName`, the `args` the model passed, the `requestContext`, and the `workspace`. Return `true` to require approval for that call, or `false` to allow it. This lets you gate approval
|
|
95
|
+
Instead of a boolean, `requireToolApproval` accepts a function that decides per tool call. It receives the `toolName`, the `args` the model passed, the `requestContext`, and the `workspace`. Return `true` to require approval for that call, or `false` to allow it. This lets you gate approval at runtime, for example, only for tools whose name matches a pattern:
|
|
96
96
|
|
|
97
97
|
```typescript
|
|
98
98
|
const stream = await agent.stream('Clean up old records', {
|
|
@@ -100,10 +100,82 @@ const stream = await agent.stream('Clean up old records', {
|
|
|
100
100
|
})
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
A tool's own `requireApproval` setting
|
|
103
|
+
A tool's own `requireApproval` setting takes precedence over the function above. Its rule decides whether that tool needs approval. If the function throws, the call requires approval as a fail-safe.
|
|
104
104
|
|
|
105
105
|
> **Note:** Function-based `requireToolApproval` is only available on regular `stream()` / `generate()` calls. Durable agents and stored agents persist their options, and a function can't be serialized, so they accept only a boolean. If you pass a function in those contexts it falls back to requiring approval for every tool call.
|
|
106
106
|
|
|
107
|
+
#### Bind approval to the exact tool arguments
|
|
108
|
+
|
|
109
|
+
For sensitive tools, bind the approval to the exact tool name and arguments that were shown to the reviewer. If those arguments drift before execution, the tool shouldn't run under the old approval.
|
|
110
|
+
|
|
111
|
+
The `tool-call-approval` chunk already includes `toolName`, `toolCallId`, and `args`. You can fingerprint those fields when the approval request is shown. The example below uses a simple JSON string as the fingerprint, but in production you should use a stable hash of the tool name and arguments:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { Agent } from '@mastra/core/agent'
|
|
115
|
+
|
|
116
|
+
// For your production usecase, build a stable hash of the tool name and args
|
|
117
|
+
function actionFingerprint(toolName: string, args: unknown) {
|
|
118
|
+
const payload = JSON.stringify({ toolName, args })
|
|
119
|
+
return `fingerprint-${payload}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const sensitiveTools = new Set(['issue_refund', 'delete_record'])
|
|
123
|
+
const approvedFingerprints = new Set<string>()
|
|
124
|
+
|
|
125
|
+
export const approvalBoundAgent = new Agent({
|
|
126
|
+
id: 'approval-bound-agent',
|
|
127
|
+
name: 'Approval Bound Agent',
|
|
128
|
+
model: 'openai/gpt-5.6-sol',
|
|
129
|
+
tools: { issueRefundTool, deleteRecordTool },
|
|
130
|
+
hooks: {
|
|
131
|
+
beforeToolCall: ({ toolName, input }) => {
|
|
132
|
+
if (!sensitiveTools.has(toolName)) return
|
|
133
|
+
|
|
134
|
+
const fingerprint = actionFingerprint(toolName, input)
|
|
135
|
+
if (!approvedFingerprints.delete(fingerprint)) {
|
|
136
|
+
return {
|
|
137
|
+
proceed: false,
|
|
138
|
+
output: `Tool call blocked: approval did not match ${toolName} arguments.`,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const stream = await approvalBoundAgent.stream('Refund order ord-1042', {
|
|
148
|
+
requireToolApproval: ({ toolName }) => sensitiveTools.has(toolName),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
async function consumeApprovalStream(currentStream: typeof stream) {
|
|
152
|
+
for await (const chunk of currentStream.fullStream) {
|
|
153
|
+
if (chunk.type === 'tool-call-approval') {
|
|
154
|
+
const { toolName, toolCallId, args } = chunk.payload
|
|
155
|
+
const fingerprint = actionFingerprint(toolName, args)
|
|
156
|
+
|
|
157
|
+
// Present toolName, args, and fingerprint to your approval UI.
|
|
158
|
+
const approved = await showApprovalDialog({ toolName, args, fingerprint })
|
|
159
|
+
|
|
160
|
+
const nextStream = approved
|
|
161
|
+
? await approveReviewedToolCall(currentStream.runId, toolCallId, fingerprint)
|
|
162
|
+
: await approvalBoundAgent.declineToolCall({ runId: currentStream.runId, toolCallId })
|
|
163
|
+
|
|
164
|
+
await consumeApprovalStream(nextStream)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function approveReviewedToolCall(runId: string, toolCallId: string, fingerprint: string) {
|
|
170
|
+
approvedFingerprints.add(fingerprint)
|
|
171
|
+
return approvalBoundAgent.approveToolCall({ runId, toolCallId })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await consumeApprovalStream(stream)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
In production, store the approved fingerprint in durable storage scoped to the user, run, tool call, and policy version. The `Set` above is intentionally small so the boundary is easy to see: the approval is consumed once, and only for the same canonical tool arguments that were reviewed.
|
|
178
|
+
|
|
107
179
|
### Runtime suspension with `suspend()`
|
|
108
180
|
|
|
109
181
|
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.
|
|
@@ -133,7 +205,7 @@ const weatherTool = createTool({
|
|
|
133
205
|
})
|
|
134
206
|
```
|
|
135
207
|
|
|
136
|
-
> **Note:** `suspend()` doesn't throw
|
|
208
|
+
> **Note:** `suspend()` doesn't throw, return immediately after calling it (e.g. `return await suspend({ ... })`). Code after `await suspend(...)` still runs before the tool pauses.
|
|
137
209
|
|
|
138
210
|
## Tool approval with `generate()`
|
|
139
211
|
|
|
@@ -172,11 +244,11 @@ if (output.finishReason === 'suspended') {
|
|
|
172
244
|
| Decline method | `declineToolCall({ runId })` | `declineToolCallGenerate({ runId, toolCallId })` |
|
|
173
245
|
| Result | Stream to iterate | Full output object |
|
|
174
246
|
|
|
175
|
-
> **Note:** `toolCallId` is optional on all four methods. Pass it when multiple tool calls may be pending
|
|
247
|
+
> **Note:** `toolCallId` is optional on all four methods. Pass it when multiple tool calls may be pending (common in supervisor agents). When omitted, the agent resumes the most recent suspended tool call.
|
|
176
248
|
|
|
177
249
|
## Tool-level approval
|
|
178
250
|
|
|
179
|
-
Instead of pausing every tool call at the agent level, you can mark individual tools as requiring approval.
|
|
251
|
+
Instead of pausing every tool call at the agent level, you can mark individual tools as requiring approval. You get fine-grained control: only specific tools pause, while others execute immediately.
|
|
180
252
|
|
|
181
253
|
### Approval using `requireApproval`
|
|
182
254
|
|
|
@@ -303,7 +375,9 @@ const agent = new Agent({
|
|
|
303
375
|
})
|
|
304
376
|
```
|
|
305
377
|
|
|
306
|
-
When enabled, the agent detects suspended tools from message history on the next user message
|
|
378
|
+
When enabled, the agent detects suspended tools from message history on the next user message. It extracts `resumeData` based on the tool's `resumeSchema`, then automatically resumes the tool.
|
|
379
|
+
|
|
380
|
+
The following example shows a complete conversational flow:
|
|
307
381
|
|
|
308
382
|
```typescript
|
|
309
383
|
import { createTool } from '@mastra/core/tools'
|
|
@@ -386,7 +460,7 @@ Both approaches work with the same tool definitions. Automatic resumption trigge
|
|
|
386
460
|
|
|
387
461
|
## Resuming after a restart
|
|
388
462
|
|
|
389
|
-
The examples above hold on to `stream.runId` between suspension and approval. That works while the process stays alive, but in production the approval often arrives later
|
|
463
|
+
The examples above hold on to `stream.runId` between suspension and approval. That works while the process stays alive, but in production the approval often arrives later, after a page refresh, a server restart, or on a different server instance behind a load balancer.
|
|
390
464
|
|
|
391
465
|
Use [`listSuspendedRuns()`](https://mastra.ai/reference/agents/listSuspendedRuns) to rediscover the pending run for a conversation from storage:
|
|
392
466
|
|
|
@@ -414,7 +488,7 @@ if (run && toolCall) {
|
|
|
414
488
|
}
|
|
415
489
|
```
|
|
416
490
|
|
|
417
|
-
Each returned run includes the suspended tool calls (`toolCallId`, `toolName`, `args`, and `requiresApproval`). Approval suspensions (`requiresApproval: true`) are answered with `approveToolCall()` / `declineToolCall()`, while `suspend()`-based suspensions carry their `suspendPayload` and expect `resumeStream()` with resume data
|
|
491
|
+
Each returned run includes the suspended tool calls (`toolCallId`, `toolName`, `args`, and `requiresApproval`). Approval suspensions (`requiresApproval: true`) are answered with `approveToolCall()` / `declineToolCall()`, while `suspend()`-based suspensions carry their `suspendPayload` and expect `resumeStream()` with resume data, so you can rebuild the right UI for either flow without keeping any state in memory.
|
|
418
492
|
|
|
419
493
|
`sendToolApproval()` uses the same storage-backed discovery automatically: when no active run is found in memory for the thread, it looks up the suspended run in storage before failing. If several suspended runs match the thread, pass a `toolCallId` to disambiguate.
|
|
420
494
|
|
|
@@ -477,7 +551,7 @@ const supervisorAgent = new Agent({
|
|
|
477
551
|
name: 'Supervisor Agent',
|
|
478
552
|
instructions: `You coordinate data retrieval tasks.
|
|
479
553
|
Delegate to data-agent for user lookups.`,
|
|
480
|
-
model: 'openai/gpt-5.
|
|
554
|
+
model: 'openai/gpt-5.6-sol',
|
|
481
555
|
agents: { dataAgent },
|
|
482
556
|
memory: new Memory(),
|
|
483
557
|
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Agent networks
|
|
4
4
|
|
|
5
|
-
> **Deprecated
|
|
5
|
+
> **Deprecated:** Agent networks are deprecated and will be removed in a future major release. [Supervisor agents](https://mastra.ai/docs/agents/supervisor-agents) using `agent.stream()` or `agent.generate()` are now the recommended approach. It provides the same multi-agent coordination with better control, a simpler API, and easier debugging.
|
|
6
6
|
>
|
|
7
7
|
> See the [migration guide](https://mastra.ai/guides/migrations/network-to-supervisor) to upgrade.
|
|
8
8
|
|
|
@@ -29,7 +29,7 @@ export const routingAgent = new Agent({
|
|
|
29
29
|
name: 'Routing Agent',
|
|
30
30
|
instructions: `
|
|
31
31
|
You are a network of writers and researchers. The user will ask you to research a topic. Always respond with a complete report—no bullet points. Write in full paragraphs, like a blog post. Do not answer with incomplete or uncertain information.`,
|
|
32
|
-
model: 'openai/gpt-5.
|
|
32
|
+
model: 'openai/gpt-5.6-sol',
|
|
33
33
|
agents: {
|
|
34
34
|
researchAgent,
|
|
35
35
|
writingAgent,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
> Discover all available pages from the documentation index: https://mastra.ai/llms.txt
|
|
2
|
+
|
|
3
|
+
# Workers
|
|
4
|
+
|
|
5
|
+
> **Beta:** This feature is in beta. The API is stable enough for production use, but some details may change. See [known limitations](#known-limitations) for current gaps.
|
|
6
|
+
|
|
7
|
+
Workers handle background processing outside the request-response cycle. Workflow step execution, cron-based scheduling, and long-running tool calls all run in workers, keeping the API responsive.
|
|
8
|
+
|
|
9
|
+
By default, workers run in the same process as the API. For production workloads, you can split them into separate processes or containers and scale each one independently.
|
|
10
|
+
|
|
11
|
+
## When to use workers
|
|
12
|
+
|
|
13
|
+
Workers matter when any of these apply:
|
|
14
|
+
|
|
15
|
+
- Workflow steps take more than a few seconds and shouldn't block API responses
|
|
16
|
+
- You need event durability so in-flight work survives process restarts
|
|
17
|
+
- Different parts of the system need to scale independently (e.g., more orchestration capacity without more API instances)
|
|
18
|
+
- Background tool calls should run on dedicated compute
|
|
19
|
+
|
|
20
|
+
If your application handles light traffic and workflows complete quickly, the default in-process setup works fine. Skip the worker infrastructure until you need it.
|
|
21
|
+
|
|
22
|
+
## Worker types
|
|
23
|
+
|
|
24
|
+
Mastra has three built-in worker types. Each handles a specific kind of background processing.
|
|
25
|
+
|
|
26
|
+
### Orchestration worker
|
|
27
|
+
|
|
28
|
+
Subscribes to workflow events on the [PubSub](https://mastra.ai/docs/server/pubsub) bus and executes workflow steps. Every `workflow.start`, step transition, and lifecycle event flows through this worker.
|
|
29
|
+
|
|
30
|
+
In a split deployment, the orchestration worker pulls events from a distributed PubSub backend and delegates step execution back to the API over HTTP. In-process, it runs steps directly.
|
|
31
|
+
|
|
32
|
+
The orchestration worker requires a PubSub backend that supports pull mode (e.g., [`RedisStreamsPubSub`](https://mastra.ai/reference/pubsub/redis-streams) or [`GoogleCloudPubSub`](https://mastra.ai/reference/pubsub/google-cloud-pubsub)).
|
|
33
|
+
|
|
34
|
+
### Scheduler worker
|
|
35
|
+
|
|
36
|
+
Polls storage for due cron schedules and publishes `workflow.start` events. It is a producer only, meaning it creates work for the orchestration worker to pick up.
|
|
37
|
+
|
|
38
|
+
The scheduler reads declarative `schedule` fields from your workflow definitions automatically. See [Scheduled workflows](https://mastra.ai/docs/workflows/scheduled-workflows) for how to declare schedules.
|
|
39
|
+
|
|
40
|
+
**Do not run more than one scheduler instance.** Multiple schedulers polling the same storage would fire duplicate events for the same schedule.
|
|
41
|
+
|
|
42
|
+
### Background task worker
|
|
43
|
+
|
|
44
|
+
Executes agent tool calls marked with `background: { enabled: true }`. When an agent invokes a background tool, the API dispatches the task to this worker instead of blocking the response stream.
|
|
45
|
+
|
|
46
|
+
The background task worker manages concurrency limits, task lifecycle, and result delivery through the PubSub bus.
|
|
47
|
+
|
|
48
|
+
## How workers run
|
|
49
|
+
|
|
50
|
+
### In-process (default)
|
|
51
|
+
|
|
52
|
+
With no configuration, Mastra creates and starts workers inside the API process. Events flow through an in-memory PubSub, and everything shares a single Node.js runtime.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { Mastra } from '@mastra/core/mastra'
|
|
56
|
+
|
|
57
|
+
export const mastra = new Mastra({
|
|
58
|
+
// Workers run in-process by default.
|
|
59
|
+
// No pubsub or worker config needed.
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This setup needs no external infrastructure beyond your storage adapter. It doesn't survive process crashes, and you can't scale individual components.
|
|
64
|
+
|
|
65
|
+
### Split processes
|
|
66
|
+
|
|
67
|
+
To run workers separately, configure a distributed [PubSub](https://mastra.ai/docs/server/pubsub) backend and use the `MASTRA_WORKERS` environment variable to control which workers start in each process.
|
|
68
|
+
|
|
69
|
+
**Redis Streams + PostgreSQL**:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { Mastra } from '@mastra/core/mastra'
|
|
73
|
+
import { RedisStreamsPubSub } from '@mastra/redis-streams'
|
|
74
|
+
import { PostgresStore } from '@mastra/pg'
|
|
75
|
+
|
|
76
|
+
export const mastra = new Mastra({
|
|
77
|
+
storage: new PostgresStore({
|
|
78
|
+
connectionString: process.env.DATABASE_URL!,
|
|
79
|
+
}),
|
|
80
|
+
pubsub: new RedisStreamsPubSub({
|
|
81
|
+
url: process.env.REDIS_URL!,
|
|
82
|
+
}),
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Google Cloud Pub/Sub + LibSQL**:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { Mastra } from '@mastra/core/mastra'
|
|
90
|
+
import { GoogleCloudPubSub } from '@mastra/google-cloud-pubsub'
|
|
91
|
+
import { LibSQLStore } from '@mastra/libsql'
|
|
92
|
+
|
|
93
|
+
export const mastra = new Mastra({
|
|
94
|
+
storage: new LibSQLStore({
|
|
95
|
+
url: process.env.DATABASE_URL!,
|
|
96
|
+
}),
|
|
97
|
+
pubsub: new GoogleCloudPubSub({
|
|
98
|
+
projectId: process.env.GCP_PROJECT_ID!,
|
|
99
|
+
}),
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Any [supported storage backend](https://mastra.ai/reference/workers/overview) works — swap the storage adapter for your preferred database.
|
|
104
|
+
|
|
105
|
+
Run the same build artifact in multiple containers, each with a different [`MASTRA_WORKERS`](https://mastra.ai/reference/workers/overview) value to control which worker starts in each process.
|
|
106
|
+
|
|
107
|
+
Split deployments require a distributed PubSub backend ([`RedisStreamsPubSub`](https://mastra.ai/reference/pubsub/redis-streams) or [`GoogleCloudPubSub`](https://mastra.ai/reference/pubsub/google-cloud-pubsub)), a shared [storage backend](https://mastra.ai/reference/workers/overview), and network connectivity between the orchestration worker and the API.
|
|
108
|
+
|
|
109
|
+
The [worker deployment guide](https://mastra.ai/guides/deployment/mastra-workers) walks through this setup with a Docker Compose example.
|
|
110
|
+
|
|
111
|
+
## Network architecture
|
|
112
|
+
|
|
113
|
+
Workers are internal infrastructure. They are not exposed to end users and do not need their own subdomain, public URL, or inbound HTTP route.
|
|
114
|
+
|
|
115
|
+
In a split deployment:
|
|
116
|
+
|
|
117
|
+
- **The API server is the only public-facing process.** It serves all client HTTP requests — REST endpoints, agent interactions, workflow triggers, and any custom routes.
|
|
118
|
+
- **Workers connect outbound only.** They pull events from the distributed PubSub backend and read/write to the shared storage database. They do not accept inbound traffic from clients.
|
|
119
|
+
- **The orchestration worker calls the API internally.** It sends step execution requests to the API over the container network using `MASTRA_STEP_EXECUTION_URL`. This is internal service-to-service communication, not a public endpoint.
|
|
120
|
+
|
|
121
|
+
All three worker types (orchestration, scheduler, background task) sit behind the API on a private network. They share access to the PubSub backend and storage database but never receive traffic directly from clients. If a worker-related feature needs an HTTP route (for example, token minting for a voice integration), that route runs on the API server, not on the worker process.
|
|
122
|
+
|
|
123
|
+
## Known limitations
|
|
124
|
+
|
|
125
|
+
- **No dead-letter queue**: Failed events are nacked and retried, but there is no DLQ for events that repeatedly fail.
|
|
126
|
+
- **No built-in health endpoint**: Workers don't expose an HTTP health check. Use container-level liveness probes or process monitoring.
|
|
127
|
+
- **Scheduler is single-instance**: Running multiple scheduler processes causes duplicate schedule fires.
|
|
128
|
+
- **Runs stuck in "running" after API crash**: If the API process crashes while executing a workflow step, the run remains in `running` status with no automatic retry. For [durable agents](https://mastra.ai/docs/long-running-agents/durable-agents), set `recovery.durableAgents` to `'auto'` in the Mastra config to automatically re-drive orphaned runs on server restart. See [Crash recovery](https://mastra.ai/docs/long-running-agents/durable-agents) for details.
|
|
129
|
+
|
|
130
|
+
## Related
|
|
131
|
+
|
|
132
|
+
- [Worker deployment guide](https://mastra.ai/guides/deployment/mastra-workers): Docker Compose example and topology options
|
|
133
|
+
- [Worker authentication](https://mastra.ai/docs/server/auth/workers): Secure worker-to-API communication
|
|
134
|
+
- [Workers reference](https://mastra.ai/reference/workers/overview): Environment variables, worker types, and storage backends
|
|
135
|
+
- [CLI reference](https://mastra.ai/reference/cli/mastra): `mastra worker build` and `mastra worker start`
|
|
136
|
+
- [PubSub](https://mastra.ai/docs/server/pubsub): Event delivery backends
|
|
137
|
+
- [Scheduled workflows](https://mastra.ai/docs/workflows/scheduled-workflows): Declare cron schedules on workflows
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# Memory processors
|
|
4
4
|
|
|
5
|
-
Memory processors transform and filter messages as they pass through an agent with memory enabled. They manage context window limits
|
|
5
|
+
Memory processors transform and filter messages as they pass through an agent with memory enabled. They manage context window limits and remove unnecessary content, plus optimize the information sent to the language model.
|
|
6
6
|
|
|
7
|
-
When memory is enabled on an agent, Mastra adds memory processors to the agent's processor pipeline. These processors retrieve message history
|
|
7
|
+
When memory is enabled on an agent, Mastra adds memory processors to the agent's processor pipeline. These processors retrieve message history and working memory, plus semantically relevant messages, then persist new messages after the model responds.
|
|
8
8
|
|
|
9
9
|
Memory processors are [processors](https://mastra.ai/docs/agents/processors) that operate specifically on memory-related messages and state.
|
|
10
10
|
|
|
@@ -47,7 +47,7 @@ const agent = new Agent({
|
|
|
47
47
|
id: 'test-agent',
|
|
48
48
|
name: 'Test Agent',
|
|
49
49
|
instructions: 'You are a helpful assistant',
|
|
50
|
-
model: 'openai/gpt-5.
|
|
50
|
+
model: 'openai/gpt-5.6-sol',
|
|
51
51
|
memory: new Memory({
|
|
52
52
|
storage: new LibSQLStore({
|
|
53
53
|
id: 'memory-store',
|
|
@@ -97,7 +97,7 @@ import { openai } from '@ai-sdk/openai'
|
|
|
97
97
|
const agent = new Agent({
|
|
98
98
|
name: 'semantic-agent',
|
|
99
99
|
instructions: 'You are a helpful assistant with semantic memory',
|
|
100
|
-
model: 'openai/gpt-5.
|
|
100
|
+
model: 'openai/gpt-5.6-sol',
|
|
101
101
|
memory: new Memory({
|
|
102
102
|
storage: new LibSQLStore({
|
|
103
103
|
id: 'memory-store',
|
|
@@ -150,7 +150,7 @@ import { openai } from '@ai-sdk/openai'
|
|
|
150
150
|
const agent = new Agent({
|
|
151
151
|
name: 'working-memory-agent',
|
|
152
152
|
instructions: 'You are an assistant with working memory',
|
|
153
|
-
model: 'openai/gpt-5.
|
|
153
|
+
model: 'openai/gpt-5.6-sol',
|
|
154
154
|
memory: new Memory({
|
|
155
155
|
storage: new LibSQLStore({
|
|
156
156
|
id: 'memory-store',
|
|
@@ -182,7 +182,7 @@ const customMessageHistory = new MessageHistory({
|
|
|
182
182
|
const agent = new Agent({
|
|
183
183
|
name: 'custom-memory-agent',
|
|
184
184
|
instructions: 'You are a helpful assistant',
|
|
185
|
-
model: 'openai/gpt-5.
|
|
185
|
+
model: 'openai/gpt-5.6-sol',
|
|
186
186
|
memory: new Memory({
|
|
187
187
|
storage: new LibSQLStore({ id: 'memory-store', url: 'file:memory.db' }),
|
|
188
188
|
lastMessages: 10, // This would normally add MessageHistory(10)
|
|
@@ -207,7 +207,7 @@ Understanding the execution order is important when combining guardrails with me
|
|
|
207
207
|
1. **Memory processors run FIRST**: `WorkingMemory`, `MessageHistory`, `SemanticRecall`
|
|
208
208
|
2. **Your input processors run AFTER**: guardrails, filters, validators
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
As a result, memory loads message history before your processors can validate or filter the input.
|
|
211
211
|
|
|
212
212
|
### Output Processors
|
|
213
213
|
|
|
@@ -253,7 +253,7 @@ const agent = new Agent({
|
|
|
253
253
|
id: 'safe-agent',
|
|
254
254
|
name: 'safe-agent',
|
|
255
255
|
instructions: 'You are a helpful assistant',
|
|
256
|
-
model: 'openai/gpt-5.
|
|
256
|
+
model: 'openai/gpt-5.6-sol',
|
|
257
257
|
memory: new Memory({ lastMessages: 10 }),
|
|
258
258
|
// Your guardrail runs BEFORE memory saves
|
|
259
259
|
outputProcessors: [contentBlocker],
|
|
@@ -293,7 +293,7 @@ const agent = new Agent({
|
|
|
293
293
|
id: 'validated-agent',
|
|
294
294
|
name: 'validated-agent',
|
|
295
295
|
instructions: 'You are a helpful assistant',
|
|
296
|
-
model: 'openai/gpt-5.
|
|
296
|
+
model: 'openai/gpt-5.6-sol',
|
|
297
297
|
memory: new Memory({ lastMessages: 10 }),
|
|
298
298
|
// Your guardrail runs AFTER memory loads history
|
|
299
299
|
inputProcessors: [inputValidator],
|
|
@@ -370,7 +370,7 @@ export const supportAgent = new Agent({
|
|
|
370
370
|
id: 'support-agent',
|
|
371
371
|
name: 'Support agent',
|
|
372
372
|
instructions: 'Answer customer support questions.',
|
|
373
|
-
model: 'openai/gpt-5.
|
|
373
|
+
model: 'openai/gpt-5.6-sol',
|
|
374
374
|
memory: new Memory({ lastMessages: 10 }),
|
|
375
375
|
inputProcessors: [new AttachmentUploader()],
|
|
376
376
|
})
|
|
@@ -134,7 +134,7 @@ export const supportAgent = new Agent({
|
|
|
134
134
|
id: 'support-agent',
|
|
135
135
|
name: 'Support agent',
|
|
136
136
|
instructions: 'Answer customer support questions.',
|
|
137
|
-
model: 'openai/gpt-5.
|
|
137
|
+
model: 'openai/gpt-5.6-sol',
|
|
138
138
|
memory: new Memory({
|
|
139
139
|
options: {
|
|
140
140
|
generateTitle: true,
|
|
@@ -155,7 +155,7 @@ export const supportAgent = new Agent({
|
|
|
155
155
|
id: 'support-agent',
|
|
156
156
|
name: 'Support agent',
|
|
157
157
|
instructions: 'Answer customer support questions.',
|
|
158
|
-
model: 'openai/gpt-5.
|
|
158
|
+
model: 'openai/gpt-5.6-sol',
|
|
159
159
|
memory: new Memory({
|
|
160
160
|
options: {
|
|
161
161
|
generateTitle: {
|
|
@@ -176,7 +176,7 @@ const agent = mastra.getAgentById('test-agent')
|
|
|
176
176
|
const memory = await agent.getMemory()
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
The `Memory` instance gives you access to functions for listing threads
|
|
179
|
+
The `Memory` instance gives you access to functions for listing threads and recalling messages, plus cloning conversations, and more.
|
|
180
180
|
|
|
181
181
|
## Querying
|
|
182
182
|
|
|
@@ -279,7 +279,13 @@ const { messages } = await memory.recall({
|
|
|
279
279
|
})
|
|
280
280
|
```
|
|
281
281
|
|
|
282
|
-
Metadata filters match shallow scalar values only: `string`, finite `number`, `boolean`, and `null`.
|
|
282
|
+
Metadata filters match shallow scalar values only: `string`, finite `number`, `boolean`, and `null`.
|
|
283
|
+
|
|
284
|
+
All specified metadata keys use AND semantics. A `null` filter matches only an explicit `null` value. A missing metadata key doesn't match.
|
|
285
|
+
|
|
286
|
+
Metadata keys must start with a letter or underscore and contain only alphanumeric characters. They must be 128 characters or fewer and can't use reserved prototype keys such as `__proto__`, `constructor`, or `prototype`.
|
|
287
|
+
|
|
288
|
+
Performance depends on the storage backend. Some backends can push parts of the filter into the database, while others scan candidate messages after thread, resource, and date constraints are applied but before pagination.
|
|
283
289
|
|
|
284
290
|
Fetch a single message by ID:
|
|
285
291
|
|
|
@@ -324,7 +330,7 @@ Message queries return `MastraDBMessage[]` format. To display messages in a fron
|
|
|
324
330
|
|
|
325
331
|
## Thread cloning
|
|
326
332
|
|
|
327
|
-
Thread cloning creates a copy of an existing thread with its messages. This is useful for branching conversations
|
|
333
|
+
Thread cloning creates a copy of an existing thread with its messages. This is useful for branching conversations or creating checkpoints before a potentially destructive operation, or alternatively testing variations of a conversation.
|
|
328
334
|
|
|
329
335
|
```typescript
|
|
330
336
|
const { thread, clonedMessages } = await memory.cloneThread({
|
|
@@ -14,13 +14,13 @@ Use multi-user threads when several people collaborate on the same subject throu
|
|
|
14
14
|
|
|
15
15
|
## Share one `resourceId` across all participants
|
|
16
16
|
|
|
17
|
-
A thread belongs to exactly one `resourceId`, so all participants on a shared thread need to pass the same value. Instead of using a user id (the default for single-user apps), key `resourceId` on the conversation itself
|
|
17
|
+
A thread belongs to exactly one `resourceId`, so all participants on a shared thread need to pass the same value. Instead of using a user id (the default for single-user apps), key `resourceId` on the conversation itself, for example `doc_${docId}` for a shared document, or `room_${roomId}` for a group chat. With everyone pointing at the same `resourceId`, they read and write the same history.
|
|
18
18
|
|
|
19
19
|
## Tag each user message with the speaker's identity
|
|
20
20
|
|
|
21
21
|
The model needs to know who's talking on every turn. Since the message body is the one place that survives into history and back into context, wrap each user message in a small `<turn>` tag with the speaker's id, name, and role. The tag stays attached to the message, so when prior turns are recalled the model still sees who said what.
|
|
22
22
|
|
|
23
|
-
Build the tag with a small helper. The example below is one way to do it
|
|
23
|
+
Build the tag with a small helper. The example below is one way to do it, copy it into your project and adapt it to your shape of user data:
|
|
24
24
|
|
|
25
25
|
```typescript
|
|
26
26
|
export type Speaker = {
|
|
@@ -113,11 +113,11 @@ The `<turn>` tag persists in the message body, so when history is recalled on la
|
|
|
113
113
|
|
|
114
114
|
The user-tagging pattern composes with every memory layer. Pick the layer based on how long the conversation needs to remember per-user facts:
|
|
115
115
|
|
|
116
|
-
- **Short conversations** (a single session, or a thread small enough to fit in `lastMessages`), or when you need a verbatim record of who said what: use [message history alone](#message-history-alone). The user tags in history are enough
|
|
116
|
+
- **Short conversations** (a single session, or a thread small enough to fit in `lastMessages`), or when you need a verbatim record of who said what: use [message history alone](#message-history-alone). The user tags in history are enough. No extra memory layer needed.
|
|
117
117
|
- **Long-running threads** (conversations that outgrow `lastMessages`, where you need per-user facts to survive history eviction): use [observational memory](#with-observational-memory-recommended).
|
|
118
118
|
- **Need a structured participants list, or your storage adapter doesn't support OM** (OM requires LibSQL, PG, or MongoDB): use [working memory](#with-working-memory).
|
|
119
119
|
|
|
120
|
-
We recommend using observational memory or working memory
|
|
120
|
+
We recommend using either observational memory or working memory because they cover overlapping needs. Running both adds latency and token cost without much benefit.
|
|
121
121
|
|
|
122
122
|
### Message history alone
|
|
123
123
|
|
|
@@ -139,7 +139,7 @@ The model reads identity from the `<turn>` tag on the current message and from p
|
|
|
139
139
|
|
|
140
140
|
### With Observational Memory (recommended)
|
|
141
141
|
|
|
142
|
-
[Observational Memory](https://mastra.ai/docs/memory/observational-memory) (OM) extracts per-user facts into a background log without burning the agent's tool budget. The default Observer model reads `<turn>` tags natively and produces
|
|
142
|
+
[Observational Memory](https://mastra.ai/docs/memory/observational-memory) (OM) extracts per-user facts into a background log without burning the agent's tool budget. The default Observer model reads `<turn>` tags natively and produces attribution like `Alice stated her favorite color is teal.` and `Bob asked for QA sign-off before publish.`
|
|
143
143
|
|
|
144
144
|
Prefer OM over working memory for multi-user threads when your storage supports it. OM extracts facts automatically, scales to any number of participants, and doesn't need template upkeep. Enable it with no overrides:
|
|
145
145
|
|
|
@@ -162,7 +162,7 @@ OM requires a storage adapter that supports it: `@mastra/libsql`, `@mastra/pg`,
|
|
|
162
162
|
|
|
163
163
|
### With working memory
|
|
164
164
|
|
|
165
|
-
Use working memory when OM isn't an option
|
|
165
|
+
Use working memory when OM isn't an option, for example, when your storage adapter doesn't support OM, or when you need a structured, deterministic participants list the agent can read and write on every turn.
|
|
166
166
|
|
|
167
167
|
The default [working memory](https://mastra.ai/docs/memory/working-memory) template assumes one user per thread ("First Name", "Last Name", etc.). For multi-user threads, provide a template with a participants list:
|
|
168
168
|
|