Mastra 项目原理:Agent Loop、Workflow 与状态恢复
AI 参与说明(Agent:Codex):本文由 Codex 根据 Mastra 官方文档、Context7 检索结果与固定提交源码辅助分析,并使用项目 Anatomy 制作核心机制交互、使用 humanizer-zh 编辑中文。资料整理于 2026-10-16,源码基线为
a1fbf275e587d56a86c52295eceff6c0756169e8;该提交中@mastra/core的版本字段为1.78.1-alpha.0,不代表 npmlatest。本文区分源码行为、实际验证与教学模型,采用建议属于工程判断。运行记录:模型gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai;CLI 版本0.162.0-alpha.17.2,不代表桌面 App 版本。
使用补充(2026-10-17,Agent:Codex):补充正式版本的项目创建、业务 Agent / Tool、Studio 调试、Memory、Client 调用、审批 Workflow 接入及构建运行,保留原理分析的固定提交与本文创建日期;中文按 humanizer-zh 编辑。运行记录:模型
gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai;CLI 版本0.162.0-alpha.17.2,不代表桌面 App 版本。
动画修订(2026-10-21,Agent:Codex):依据 Anatomy 的连续动作要求,重做保存原件、扫描副本、侧轨恢复、决定接合与回执生成;字段明细保留供核对。沿用原资料基线与本地教学模型边界,中文按 humanizer-zh 编辑。执行入口 Codex Desktop;本轮模型完整标识与 reasoning effort 未取得任务级运行记录。
Mastra 把模型调用、工具执行、工作流、上下文管理和存储接到同一套 TypeScript 运行时中。理解它的关键,是分清两种控制权:Agent 让模型决定下一次操作,Workflow 让程序显式规定步骤关系。两者可以嵌套,但它们对“接下来执行什么”的决定方式不同。
想先把产品用起来,可以从上手与应用接入开始;随后再沿功能地图、一次 Agent 请求和一次 Workflow 暂停恢复,理解数据怎样变成结果。正文内的 Workflow 实验可以直接操作。
先认识几个术语
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Agent Loop | 智能体循环 | 请求模型、执行其选择的工具,再把结果交回模型 |
| Tool call / Tool result | 工具调用 / 工具结果 | 模型提出的操作请求,以及程序实际执行后的返回值 |
| Workflow | 工作流 | 用代码规定步骤、分支、并发与恢复关系的过程 |
| Step | 步骤 | 有输入、输出约束和执行函数的工作单元 |
| Run | 单次运行 | Agent 或 Workflow 的一次具体执行,由 runId 标识 |
| Snapshot | 快照 | 可序列化的执行状态,用于保存结果与恢复位置 |
| Thread | 对话线程 | 将一组对话消息关联到同一段历史的对象 |
| Processor | 处理器 | 在模型请求、流式输出等边界检查或修改数据的扩展点 |
| Storage / Vector store | 存储 / 向量存储 | 前者保存结构化状态,后者支持向量相似度检索 |
如何使用:从空项目到业务 Agent
实际使用 Mastra,可以先在本地写好 Agent 和 Tools,通过 Studio 调试,再把调用接到自己的应用中。下面做一个旅行预算助手:模型理解“住几晚、每晚多少、交通多少”,budgetTool 负责计算,Memory 保存同一段对话的历史。
本节用法核验于 2026-10-17,采用 npm 正式版本 @mastra/core@1.78.0、CLI mastra@1.34.2、@mastra/memory@1.38.0、@mastra/libsql@1.26.2、@mastra/client-js@1.55.0。它们与后文固定提交的 alpha 源码分析基线分别记录。CLI 和 Core 要求 Node.js >=22.13.0;本次运行环境为 Node.js 26.0.0。官方上手入口、CLI 版本元数据、Core 版本元数据
1. 创建项目并配置模型
在自己的开发目录运行以下命令。这里显式选择 --empty,生成空项目后添加业务能力;不指定它时,当前默认模板会生成带 Workspace、Shell、Memory 和任务调度的 Agent Harness。--no-install 让我们随后统一安装精确版本,--no-skills 与 --no-git 分别跳过安装 Skills 和初始化 Git。create-mastra
npm create mastra@1.34.2 budget-assistant -- --empty --no-install --no-skills --no-git
cd budget-assistant
npm install --save-exact @mastra/core@1.78.0 @mastra/memory@1.38.0 @mastra/libsql@1.26.2 zod@4.6.7
npm install --save-dev --save-exact mastra@1.34.2
空模板不会生成 .env.example。在项目根目录创建 .env,填写自己模型服务的凭据;不要把真实值写进 Agent 源码或浏览器代码。
OPENAI_API_KEY=
下面沿用官方示例的 openai/gpt-5.6-sol。model 使用 provider/model 格式,openai 对应 OPENAI_API_KEY;若换 Provider,要同时核对模型 ID 和相应环境变量。模型路由与环境变量
2. 定义 Tool、Agent 和应用入口
在生成项目中准备这三个文件;src/mastra/index.ts 已由空模板生成,可以用下面的入口代码替换它。
src/mastra/
tools/budget-tool.ts
agents/budget-agent.ts
index.ts
src/mastra/tools/budget-tool.ts 定义输入、输出与真正执行的计算。示例金额由读者提供,不查询真实酒店价格。
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';
export const budgetTool = createTool({
id: 'calculate-budget',
description: 'Calculate a travel budget in CNY from nights, nightly rate and transport cost.',
inputSchema: z.object({
nights: z.number().int().min(0),
nightlyRate: z.number().min(0),
transport: z.number().min(0),
}),
outputSchema: z.object({
lodging: z.number(),
transport: z.number(),
total: z.number(),
currency: z.literal('CNY'),
}),
execute: async ({ nights, nightlyRate, transport }) => {
const lodging = nights * nightlyRate;
return { lodging, transport, total: lodging + transport, currency: 'CNY' as const };
},
});
src/mastra/agents/budget-agent.ts 负责告诉模型何时使用这个 Tool。execute 的第一个参数是通过 Schema 校验的工具输入;不要套用 Workflow 的 { inputData } 参数形状。Tools
import { Agent } from '@mastra/core/agent';
import { Memory } from '@mastra/memory';
import { budgetTool } from '../tools/budget-tool';
export const budgetAgent = new Agent({
id: 'budget-agent',
name: 'Budget Agent',
instructions: `
Help users calculate travel budgets in CNY.
Ask for missing nights, nightly rate or transport cost.
Use budgetTool for calculations and report its exact result.
Reply in the user's language. Do not invent prices.
`,
model: 'openai/gpt-5.6-sol',
tools: { budgetTool },
memory: new Memory({ options: { lastMessages: 10 } }),
});
src/mastra/index.ts 注册它们,并用文件 libSQL 保存消息与运行状态。agents 让 Agent 可调用;tools 还让该 Tool 出现在 Studio 的独立工具列表中。Agent 注册、Memory
import { Mastra } from '@mastra/core/mastra';
import { LibSQLStore } from '@mastra/libsql';
import { budgetAgent } from './agents/budget-agent';
import { budgetTool } from './tools/budget-tool';
export const mastra = new Mastra({
agents: { budgetAgent },
tools: { budgetTool },
storage: new LibSQLStore({
id: 'app-storage',
url: process.env.DATABASE_URL || 'file:./mastra.db',
}),
});
数据库相对路径以进程工作目录为准;独立脚本与 CLI dev 的工作目录可能不同。如果两种入口要共用数据库,应在它们的环境中设置同一个、使用绝对文件路径的 DATABASE_URL。把 .env 和 *.db* 加入自己的 .gitignore;部署时把数据库放在持久存储位置。
3. 在 Studio 调试,再核对工具结果
npm run dev
默认打开 localhost:4111,端口以终端输出为准。按下面顺序检查,能把模型连接、工具执行和对话结果分别定位。Studio
- 进入 Tools,选择
calculate-budget,输入{"nights":2,"nightlyRate":300,"transport":200}并执行。应得到lodging: 600、total: 800、currency: "CNY"。这一步不需要模型推理。 - 进入 Agents,选择 Budget Agent,发送“住 2 晚,每晚 300 元,交通 200 元,请算总预算”。检查对话中的 Tool call 与 Tool result,确认实际传入了这三个数,结果为 800;最终回复的措辞可以不同。
- 在同一段对话继续追问“刚才住宿费是多少”。此时应沿用同一 Thread 的历史;换一个 Thread 后,不应把未配置的跨 Thread 召回当作默认能力。
- 修改
instructions后发起下一次请求,比较回答方式;修改 Tool 的计算或 Schema 后,先重新独立测试 Tool,再测试 Agent。若需要逐层追踪 model/tool spans,应另外按官方文档配置 Observability 和 Exporter;本节的空项目入口没有启用 Trace 导出。Studio Observability
同一个工具也可以直接通过本地 HTTP 测试。请求体用 data 包住工具输入:
curl -sS -X POST 'http://localhost:4111/api/tools/calculate-budget/execute' \
-H 'Content-Type: application/json' \
-d '{"data":{"nights":2,"nightlyRate":300,"transport":200}}'
{"lodging":600,"transport":200,"total":800,"currency":"CNY"}
还可以打开 Swagger UI 查看当前应用实际暴露的接口。Tools 列表为空时,先核对应用入口的 tools 注册;Agents 列表为空时,核对 agents 注册与开发服务器是否成功重载。
4. 从自己的应用调用 Agent
已有 TypeScript 后端可以直接导入自己的 mastra 实例,在请求处理器内调用 Core:
const result = await mastra.getAgentById('budget-agent').generate(
'住 2 晚,每晚 300 元,交通 200 元,请算总预算',
{
maxSteps: 3,
memory: { thread: 'trip-demo', resource: 'demo-user' },
},
);
console.log(result.text);
后续请求保留相同的 thread 与 resource,才能继续这段历史。这里的标识只是演示值;真实应用应从已验证身份和业务记录派生归属。Message History
如果 Mastra 作为独立服务运行,在调用方安装 Client SDK:
npm install --save-exact @mastra/client-js@1.55.0
下面是可保存为 request.mjs 并运行 node request.mjs 的调用。前提是上一节的服务正在运行,而且已配置有效模型凭据。
import { MastraClient } from '@mastra/client-js';
const client = new MastraClient({ baseUrl: 'http://localhost:4111' });
const result = await client.getAgent('budget-agent').generate(
'住 2 晚,每晚 300 元,交通 200 元,请算总预算',
{
maxSteps: 3,
memory: { thread: 'trip-demo', resource: 'demo-user' },
},
);
console.log(result.text);
baseUrl 不重复加 /api,Client 默认会添加这个前缀。示例统一按对象的 id 查找:budget-agent 是 Agent ID,Budget Agent 是显示名称,budgetAgent 是入口对象的注册 key。同进程也可用 mastra.getAgent('budgetAgent') 按 key 查找。Mastra Client
5. 将审批 Workflow 接入产品
后文的无模型审批案例已经给出三个 Step 的完整定义。要让它出现在 Studio 和 HTTP API 中,将代码从 imports 开始到 Workflow 的 .commit() 结束这一段移到 src/mastra/workflows/approval.ts,包含 executions 与 calls 两个辅助变量;把 Workflow 声明改成下面这样。后面的 Storage、createRun()、断言和 shutdown() 属于独立测试脚本,不放进应用模块。
export const approvalWorkflow = createWorkflow({
id: 'approval-flow', inputSchema, outputSchema,
}).then(normalize).then(approval).then(receipt).commit();
然后在应用入口增加 import 和注册项,保留前面已有的 Agent、Tool 与 Storage 配置:
import { approvalWorkflow } from './workflows/approval';
// new Mastra({...}) 中增加:
// workflows: { approvalWorkflow },
重载后,在 Workflows 选择 approval-flow,以 {"amount":180} 启动,查看暂停的 approval Step 及其输入输出。默认空项目没有业务 Workflow;未注册前列表为空是正常现象。Studio Workflows
Client 调用时,先保存 Run ID,再在收到人工决定后恢复同一次运行:
const workflow = client.getWorkflow('approval-flow');
const run = await workflow.createRun();
const started = await run.startAsync({ inputData: { amount: 180 } });
console.log(started.status); // suspended
const savedRunId = run.runId; // 与业务记录一起保存
// 后续“拒绝”操作的处理逻辑:读取保存的 ID 并验证当前用户权限。
const existingRun = await workflow.createRun({ runId: savedRunId });
const resumed = await existingRun.resumeAsync({
step: 'approval',
resumeData: { approved: false },
});
console.log(resumed.status); // success
console.log(resumed.result);
// { amount: 180, decision: 'rejected', receipt: 'rejected:180.00' }
代码中的两段调用对应两次业务操作;实际产品不应在收到 suspended 后立即替用户自动拒绝。页面重载后也要能取回原 runId。只查询状态时可使用 await workflow.runById(savedRunId)。Client Workflow Run
Core and Client use different method names for a result-returning workflow call. 该正式版本中,Core 的 start() / resume() 等待本次执行结果;Client 的对应方法是 startAsync() / resumeAsync()。Client 的 start() / resume() 只返回已启动或已恢复的确认消息。这里的 Async 不是等待人工决定一直不返回:运行暂停时,当前调用就返回 suspended。Client 实现
6. 构建运行与常见排查
开发阶段使用 npm run dev 边改边测试;需要运行打包后的服务时,先构建再启动:
npm run build
npm run start
主要产物在 .mastra/output/。普通 build 不承诺包含 Studio;如果需要连同界面一起构建,使用 npm run build -- --studio。start 运行本地或服务器上的打包应用,真正上线还需要目标平台的进程、环境变量、存储和网络配置。Mastra Server、CLI
| 现象 | 先检查什么 |
|---|---|
| Studio 能打开,Agent 请求失败 | .env 的 Provider Key、模型 ID、服务权限及返回的错误;页面可打开只说明本地服务器已启动 |
| Tool 请求 HTTP 200,但没有预期数据 | 仍须检查响应中的 error 和 validationErrors;本版本的工具参数校验失败也可能返回 HTTP 200 |
| Agent 回答了,但没有 Tool result | 工具是否放入该 Agent 的 tools、输入是否完整、指令是否要求调用;先独立执行 Tool |
| Tool 参数总是错 | Tool 的 Schema、description 和 execute(inputData, context) 签名;不要使用旧式 { context } 读取参数 |
| 下一次请求忘了前文 | 是否配置 Memory 和 Storage,是否复用同一个 Thread / Resource |
| 审批恢复结果不对 | 是否保存原 runId,Client 是否用了返回结果的方法,Step 是否正确区分 resumeData === undefined 与 approved: false |
以上 HTTP 示例用于本机开发。对外提供服务前,应配置服务认证、资源归属和业务权限;模型供应商 Key 留在后端,runId 只是定位符,不是恢复权限凭据。Authentication and identity
本次实际验证了空脚手架生成、TypeScript 检查、CLI dev 启动、Studio 与资源 API 可访问、预算 Tool 的 HTTP / SDK 返回、离线模型驱动的 Agent 工具循环、同 Thread 历史读取和文件数据库跨进程读取;也用正式版本复验了 Workflow 的创建、暂停与恢复。真实付费模型问答未在本轮调用,模型回复应按上面的 Tool result 和业务结果验收,不以示例措辞为准。
功能地图:一项能力对应哪段代码
Mastra 是一个 monorepo。@mastra/core 提供运行时契约与主要执行机制,Memory、RAG、数据库、模型与部署适配等能力由其他包补齐;CLI 与 Studio 则负责开发、检查和交互。下面的源码链接全部固定到本文分析提交,避免 main 更新后行号失去参照。
| 能力 | 对外入口与作用 | 核心代码入口 |
|---|---|---|
| 组件装配 | new Mastra({ agents, workflows, storage, ... }) 统一注册并注入共享服务 | core/src/mastra/index.ts |
| Agent 与模型接入 | Agent.generate() / stream() 管理多轮请求、工具与输出;模型适配器处理 Provider 协议 | agent/agent.ts、model.loop.ts |
| Tools 与 MCP | createTool() 定义参数和执行函数;MCP Client 把远端 Tools 包装为本地可调用对象,MCP Server 可反向暴露 Agent / Workflow | tools/tool.ts、mcp/src/client/client.ts、mcp/src/server/server.ts |
| Workflow | createStep()、createWorkflow() 定义顺序、分支、并发及暂停恢复 | workflows/workflow.ts、handlers/step.ts |
| Memory | 读取历史、Working Memory、Semantic Recall 或 Observational Memory,把相关上下文加入本次请求 | memory/src/index.ts、core/src/memory/memory.ts |
| RAG | MDocument 解析与分块;createVectorQueryTool() 把检索接到 Agent 工具链 | rag/src/document/document.ts、rag/src/tools/vector-query.ts |
| Storage 与 Vector store | 按领域保存消息、Workflow、Trace 等;向量检索使用独立接口 | storage/base.ts、vector/vector.ts |
| Processor | 在输入、每轮请求、模型响应、工具结果和流式输出边界处理数据 | processors/index.ts |
| Observability 与 Evals | 前者记录调用和状态,后者按规则或 LLM judge 评价输出 | observability/mastra/src/default.ts、evals/base.ts |
| Workspace | 组合文件系统、Sandbox、检索和 Skills 等 Agent 工作能力 | workspace/workspace.ts |
| HTTP、SDK 与开发界面 | Server adapter 绑定 API,Client SDK 远程调用,CLI 运行开发与构建,Studio 查看和调试资源 | server-adapter、client-js、CLI、Studio |
这张表列的是与执行机制直接相关的主要能力。Voice、Channels、后台任务等扩展不在本文展开;不能因为某项能力存在入口,就认定任意 Provider 或部署环境都支持相同保证。
运行架构:把定义、执行、状态和界面分开
应用可以把 core 嵌进已有后端直接调用,也可以运行 Mastra Server,让前端通过 Client SDK 访问。Studio 使用同一类服务接口查看 Agent、Workflow、Memory 和执行记录;它不是每次模型推理的必经节点。Server routes
flowchart TB
App[应用代码] --> Registry[Mastra<br/>注册与共享服务]
UI[前端 / Studio] --> SDK[Client SDK]
SDK --> Server[Server adapter<br/>HTTP 路由与处理]
Server --> Registry
Registry --> Agent[Agent / Agent Loop]
Registry --> Workflow[Workflow / Execution engine]
Agent --> Model[Model adapter / Provider]
Agent --> Tools[Tool / MCP / Workspace]
Agent --> Context[Processor / Memory]
Context --> Storage[Storage / Vector store]
Workflow --> Storage
Agent -.执行记录.-> Observe[Observability]
Workflow -.执行记录.-> Observe
图中实线表示运行时调用或服务使用关系,不是 npm 依赖箭头。代码依赖方向则以 core 的抽象为中心:@mastra/memory、@mastra/rag、@mastra/mcp、@mastra/server 等围绕这些契约实现能力;@mastra/deployer 使用 Server 构建运行入口,CLI 再调用开发和构建逻辑。Studio 的前端源码位于 packages/playground 与 packages/playground-ui,远程 SDK 位于 client-sdks/,数据库实现位于 stores/,Observability 实现位于 observability/。不要只看 packages/ 就认为已经看到全仓。工作区定义、Deployer
Mastra 的作用更接近应用装配入口。注册 Agent 时,它注入 Storage、logger、其他 Agents、Vector store 等共享能力;注册 Workflow 时,也把执行引擎所需服务连接起来。注册表让组件能够协作,真正的执行规则仍在 Agent Loop 和 Workflow engine 内。Agent 注册
核心数据模型:配置对象不等于运行状态
| 对象 | 关键字段或结构 | 在执行中的作用 |
|---|---|---|
| Agent definition | instructions、model、tools、memory、inputProcessors、outputProcessors、errorProcessors | 定义模型能收到什么指令、使用什么能力 |
MessageList | 规范化消息、system messages、来源标记 | 统一处理输入、历史、响应与工具结果,再转换为 Provider 的消息格式 |
MastraDBMessage | id、role、content.parts、createdAt、threadId、resourceId | 保存结构化消息;工具调用及结果不只是拼成文本 |
| Tool invocation | toolCallId、toolName、args、状态与结果 | 把模型的调用请求关联到同一次工具执行及其返回值 |
| Workflow / Step | 输入输出 Schema、Step ID、execute、步骤图 | 定义一次执行可以沿哪些路径运行,以及数据契约 |
| Run / Snapshot | runId、context、Step 状态、suspendedPaths、结果 | 保存已发生的执行,并定位恢复入口 |
StorageThreadType | id、resourceId、时间、可选标题和元数据 | 关联跨多次请求的会话历史 |
消息的内部格式保留 content.parts,模型协议转换时才生成对应 Provider 接受的 assistant Tool call 与 tool result。MessageList 还记录消息来自 input、memory、response、system 或 context,方便不同阶段读取与处理。消息类型、MessageList
runId、threadId 和 resourceId 回答三个不同问题:这是哪次执行、属于哪段对话、归属于哪个资源。一个 Thread 可以包含多次 Agent 请求;resourceId 可以支持跨 Thread 的上下文作用域,但它本身不是身份验证或访问授权。应用仍须从可信身份中确定归属并执行权限检查。Thread 类型
Agent:一次输入怎样变成多轮模型请求
入口共享,Provider 调用方式仍有区别
Agent.generate() 和 Agent.stream() 都进入私有 #execute()。它解析模型、指令、Thread、Resource、Tools 和处理器,再创建内部的准备 Workflow。generate() 最后消费内部输出管线并返回完整结果;stream() 返回可逐步读取的 MastraModelOutput。generate、stream
Agent.generate() / Agent.stream()
→ #execute()
→ createPrepareStreamWorkflow()
parallel(prepareToolsStep, prepareMemoryStep)
→ mapResultsStep → streamStep
→ MastraLLMVNext.stream() → loop()
→ AgenticLoopBuilder
LLM execution → Tool execution → result mapping → continuation check
准备阶段把工具解析和上下文准备并行执行;prepareMemoryStep 建立 MessageList 并运行输入处理器。随后 streamStep 把解析好的模型、工具和消息交给 LLM 层。这里的内部流式管线并不意味着两种 API 都向 Provider 请求流式响应:底层仍按 methodType 选择 model.doGenerate() 或 model.doStream()。准备阶段、上下文准备、Provider 边界
模型提出请求,框架执行操作
假设应用注册一个 weather Tool,模型第一轮返回调用:toolCallId=call-1、toolName=weather、args={city: "Seattle"}。模型只是产生结构化请求;框架查找本轮可用 Tool,校验和执行应用函数,随后把结果按同一个 toolCallId 合入消息。
sequenceDiagram
participant App as 应用
participant Agent as Agent Loop
participant Model as Model adapter
participant Tool as weather Tool
App->>Agent: generate(messages, options)
Agent->>Model: 指令、历史、输入、Tool schemas
Model-->>Agent: Tool call(call-1, weather, city)
Agent->>Tool: execute(args, context)
Tool-->>Agent: Tool result
Note over Agent: 按 toolCallId 合入 MessageList
alt 仍满足继续条件
Agent->>Model: 包含 Tool result 的下一轮消息
Model-->>Agent: 最终文本 / 新 Tool calls
else 预算耗尽或暂停
Note over Agent: 本次请求不再发起下一轮模型调用
end
Agent-->>App: MastraModelOutput / 完整结果
AgenticLoopBuilder 本身使用 Workflow 的 .dowhile() 和 .foreach() 组织循环:一轮先执行模型,再调度 Tool calls,映射结果并检查是否继续。因此 Mastra 的 Agent 不只是薄薄一层 generateText() 包装,它复用了自己的编排机制来管理状态与分支。循环构造、循环外层
普通本地 Tool 最终进入 executeToolCall(),调用 tool.execute(args, toolOptions)。工具不存在或执行出错时,许多普通错误会成为模型可见的 Tool error,为下一轮纠正提供依据;授权拒绝、取消与审批挂起有各自的处理路径,不能一律当作成功返回。由 Provider 自己执行的 Tool,也不会再由本地重复执行。工具执行、Provider-executed 分支、结果与错误映射
一个实用设计是 toModelOutput:Tool 可以给业务层返回完整对象,只把精简结果交给模型。例如工具返回温度、湿度和原始数据,下一轮模型只收到天气摘要。这减少了上下文中无用的数据,但也意味着“工具原始返回值”和“模型实际看到的内容”可能不同。上游固定模型测试专门检查了这次转换。映射实现、上游测试
怎样结束,预算在数什么
maxSteps bounds counted LLM steps, not individual Tool calls. 一个模型 Step 可以提出多个 Tool calls,不能拿工具数量直接换算模型轮次。该版本在既未设置 maxSteps、也未设置 stopWhen 时,默认使用 5 个计数 Step;显式设置 maxSteps 时,它与自定义 stopWhen 构成 OR 条件。只传自定义 stopWhen 时,源码不会额外补上默认 5 轮。停止条件构造
模型正常结束、步数上限、Processor 拦截、取消、工具审批和暂停都可能让本次请求停止。length、error、content-filter 等终止原因不会因为还有 Tool call 就无条件重开循环;Processor 请求的重试另受其预算约束,网络重试也不是 maxSteps。继续判定
本次在 Node.js 26.0.0 与 @mastra/core@1.78.1-alpha.0 中运行了无 API Key 的固定模型测试:第一轮必定请求 weather,工具返回固定的 Seattle 天气;第二轮检查实际收到的消息确实包含 role: tool、原 toolCallId 和返回对象,再输出预设答案。只改变 maxSteps,得到以下结果:
| 条件 | 模型调用次数 | Tool 执行次数 | 最终文本 | finishReason |
|---|---|---|---|---|
maxSteps: 2 | 2 | 1 | Seattle: 72F, sunny. | stop |
maxSteps: 1 | 1 | 1 | 空字符串 | tool-calls |
第一轮 Tool 已完成,并不意味着程序还有预算请求模型组织答案。这个测试核对的是框架编排与消息传递;天气、模型决策和最终文本都是固定数据,不能据此评价真实模型的推理质量。上游还有更完整的 Tool / toModelOutput 测试,可继续沿同一路径阅读。
Workflow:显式连接输入、输出和执行位置
createStep() 将 Schema 和 execute 绑定为 Step;.then() 等方法构造执行图;.commit() 完成构图并登记 executionGraph;createRun() 为一次执行创建或复用 Run,并按配置保存初始状态。它们没有把业务函数编译成另一个独立程序,恢复时仍需要可用的 Workflow 定义。Step 构造、commit 与 createRun
数据怎样流向下一步
本文以下细节限定默认 DefaultExecutionEngine。Run.start() 进入 _start(),验证初始输入,再调用引擎的 execute()。引擎遍历 execution entries,executeEntry() 根据类型分派;普通 Step 最终调用 step.execute()。成功返回后,输出成为后一个 entry 的输入,并更新 Step 状态及 Snapshot。start、默认引擎遍历
| 连接方式 | 数据规则 | 容易误解之处 |
|---|---|---|
.then(step) | 上一 entry 的输出进入下一 Step | 不会按同名属性自动替应用映射任意结构 |
.map(...) | 明确从 Step 结果、初始输入等构造下一份输入 | 这是数据映射,不是数组逐项执行 |
.parallel([a, b]) | 各分支读取同一份上游数据,结果按 Step ID 聚合 | 下游收到的是聚合对象,不是某一分支的裸输出 |
.branch([...]) | 评估条件,执行所有为真的分支,再聚合结果 | 不是只执行第一个匹配项的 if / else |
.foreach(step) | 对上游数组逐项执行,结果按原索引组织 | 默认并发数为 1,不能把它直接理解成全并发 |
这些结构都在 Workflow API 构造图,但实际调度集中于 handlers。尤其 .branch() 会选择所有满足条件的分支;如果业务要求只能命中一个,应用应让条件互斥。构图 API、控制流执行
暂停后保存什么,恢复时读什么
Snapshot 保存的是可序列化状态:初始输入位于 context.input,每步输入位于 context[stepId].payload,成功输出位于 output,暂停数据位于 suspendPayload;value 保存 Workflow state,suspendedPaths 标识恢复路径。图结构也可序列化,但不会把函数闭包和 JavaScript 调用栈保存到数据库。Snapshot 类型
flowchart TB
Start[Run.start / inputData] --> Execute[按执行图调用 Step.execute]
Execute --> Decision{Step 的执行结果}
Decision -->|success| Next[保存 output<br/>作为下个 entry 的输入]
Next --> Execute
Decision -->|suspended| Snapshot[保存 payload / suspendPayload<br/>与 suspendedPaths]
Snapshot --> Wait[结束当前请求<br/>等待外部决定]
Wait --> Resume[Run.resume / resumeData]
Resume --> Restore[加载 Snapshot<br/>验证并定位目标 Step]
Restore --> Reenter[使用保存的 payload<br/>重新进入目标 execute]
Reenter --> Decision
Next -->|执行图已完成| Result[保存并返回最终 result]
Decision -->|failed / canceled 等| Exit[返回相应终态]
resume() 加载原 Snapshot,确定目标 Step 并校验 resumeSchema,再把保存的 context、value、暂停路径和新的恢复数据交给引擎。默认引擎从该路径对应的 entry 开始;目标 Step 读取自己的旧 payload,resumeData 则来自这次调用。已经成功完成的前置 entries 不会因普通恢复而全部重跑;并行、嵌套和逐项执行还有各自路径规则。恢复入口、恢复输入选择
遇到多个暂停 Step 时,需要明确恢复目标。一次请求返回 suspended 表示引擎已将执行交回外部,并不意味着原进程中一直有一个等待人工点击的函数栈。
交互实验:恢复时,金额究竟从哪里来
这里解剖的是默认 Workflow 引擎的 Step 恢复机制,对应 Run.resume()、DefaultExecutionEngine 与 Step handler。我们假设一个三步流程:normalize → approval → receipt。金额超过 100 时,approval 保存输入并暂停;恢复时,程序把 Snapshot 中保存的金额与新传入的 resumeData.approved 组合,交给 receipt。
金额、阈值和审批规则是本文的教学场景;保存 Step 输入、定位暂停路径、重新进入 execute 和注入恢复数据的规则来自源码。浏览器内运行的是依据这些规则重建的确定性模型,不请求 LLM、不处理真实审批,也不直接加载完整 Mastra。装置用工作副本的路线解释保存与恢复,动作经过放慢。交互金额只取整数;下方实际运行的代码另外验证了小数正规化。
ANATOMY · FIG. 01 / 保存与恢复
原输入留下来,新决定接进来
先看一次完整的暂停与恢复:保留的金额原件不会被下一笔草稿替换;恢复副本沿侧轨直接回到 approval,与新的决定接合,再进入 receipt。
工作副本正在经过当前步骤
180金额工作副本进入 normalize;当前 Run 从这里开始。
教学慢放 · 空闲时自动演示;播放后可暂停、慢放或拖动进度。运动时间不代表真实执行耗时。
蓝色原件与工作副本都保持当前Run金额;灰色新草稿碰到独立挡块,不会进入恢复路径。true与false都会到达合成台;false产生rejected输出,流程仍会完成。
核对教学模型的最终字段与固定源码
这里是完整演示结束后的预期值。主画面的读数按当前播放阶段出现;金额、阈值、审批规则是本文的教学场景,浏览器没有运行 Mastra、LLM 或数据库。
保存的 approval 输入
{
"amount": 180
}本次 resumeData
{
"approved": false
}组合后的 approval.output
{
"amount": 180,
"decision": "rejected"
}最终 receipt:rejected:180.00。执行次数:normalize 1 / approval 2 / receipt 1。
{
"status": "success",
"suspendedPaths": {},
"context": {
"input": {
"amount": 180
},
"normalize": {
"status": "success",
"payload": {
"amount": 180
},
"output": {
"amount": 180
}
},
"approval": {
"status": "success",
"payload": {
"amount": 180
},
"resumePayload": {
"approved": false
},
"output": {
"amount": 180,
"decision": "rejected"
}
},
"receipt": {
"status": "success",
"payload": {
"amount": 180,
"decision": "rejected"
},
"output": {
"amount": 180,
"decision": "rejected",
"receipt": "rejected:180.00"
}
}
}
}固定版本的 Step 恢复入口。恢复重新执行暂停的 Step,使用保存输入和新 resumeData;不会恢复旧 JavaScript 调用栈。实际副作用仍需业务幂等保护。
默认演示从 180 开始。先看金额经过 normalize 和 approval,再看保存的原件留在后方读台;60 草稿沿独立轨道靠近挡块,不能替换原件。扫描得到的恢复副本从侧轨直接返回 approval,与新决定接合,随后才形成 receipt。可以暂停、选择 0.25× 或拖动进度,检查复制、回送和接触的中间状态。改变“下一笔草稿”只影响独立轨道;要以 60 发起另一个 Run,需要改变初始金额。
默认传入拒绝决定,再切换批准比较。true 与 false 的决定片都会进入合成处:approved: false 是一个已经给出的决定,流程应完成并返回拒绝结果;它不等于尚未收到 resumeData。模型中的 resumeData === undefined 从 true 变为 false,因此恢复执行会越过暂停分支。输出的 amount 从保存的 inputData.amount 取值,decision 根据新的 resumeData.approved 选择 approved 或 rejected;最后,receipt 将这两个字段组合成 rejected:180.00。若把暂停条件误写成 if (!resumeData?.approved),拒绝也会再次触发暂停。
字段来源、结果和执行计数读取同一份模型。正常恢复时 normalize 不重跑,侧轨也不经过它;approval 从头执行第二次,receipt 首次执行,计数为 1 → 2 → 1。切到“100 · 不暂停”:amount > 100 为 false,工作副本直接走向输出,不取存档、不使用决定片,得到 automatic:100.00,三步各执行一次。完整字段与 Snapshot 可在图外展开核对。
Resuming a suspended Step re-enters its execution function with restored input and new resume data. It does not restore a JavaScript stack frame. 因此应在暂停点写 return await suspend(...),让暂停分支结束当前执行,避免提前触发后续业务;真正不可重复的外部副作用仍须使用业务幂等键保护。不要把 await suspend() 当成操作系统冻结线程的指令。Step handler
无模型案例:实际跑一次暂停和恢复
下面是本次实际执行并断言通过的最小案例。它只运行普通 JavaScript Steps,使用内存 libSQL 保存 Snapshot;不需要模型 API Key。金额先保留两位小数,再进入暂停与恢复分支。运行环境为 Node.js 26.0.0。
在一个已有的临时工作目录中安装精确版本,将代码保存为 approval.mjs,再运行 node approval.mjs。这些是配合本文源码快照的 prerelease;安装时使用 --legacy-peer-deps,因为该 libSQL 包声明的 Core peer range 尚不接纳这份 alpha 版本,不应将它当成稳定版通用安装模板。
npm install --save-exact --legacy-peer-deps --ignore-scripts @mastra/core@1.78.1-alpha.0 @mastra/libsql@1.26.3-alpha.0 zod@4.3.6
node approval.mjs
import assert from 'node:assert/strict';
import { createWorkflow, createStep } from '@mastra/core/workflows';
import { Mastra } from '@mastra/core/mastra';
import { LibSQLStore } from '@mastra/libsql';
import { z } from 'zod';
const inputSchema = z.object({ amount: z.number().positive() });
const approvalOutput = z.object({ amount: z.number(), decision: z.enum(['approved', 'rejected', 'automatic']) });
const outputSchema = approvalOutput.extend({ receipt: z.string() });
const executions = { normalize: 0, approval: 0, receipt: 0 };
const calls = [];
const normalize = createStep({
id: 'normalize', inputSchema, outputSchema: inputSchema,
execute: async ({ inputData }) => {
executions.normalize++;
return { amount: Math.round(inputData.amount * 100) / 100 };
},
});
const approval = createStep({
id: 'approval', inputSchema, outputSchema: approvalOutput,
resumeSchema: z.object({ approved: z.boolean() }),
suspendSchema: z.object({ amount: z.number(), reason: z.string() }),
execute: async ({ inputData, resumeData, suspendData, suspend }) => {
executions.approval++;
calls.push({ inputData, resumeData, suspendData });
if (inputData.amount > 100 && resumeData === undefined) {
return await suspend({ amount: inputData.amount, reason: 'Amount exceeds automatic approval limit' });
}
return {
amount: inputData.amount,
decision: resumeData === undefined ? 'automatic' : resumeData.approved ? 'approved' : 'rejected',
};
},
});
const receipt = createStep({
id: 'receipt', inputSchema: approvalOutput, outputSchema,
execute: async ({ inputData }) => {
executions.receipt++;
return { ...inputData, receipt: `${inputData.decision}:${inputData.amount.toFixed(2)}` };
},
});
const workflow = createWorkflow({ id: 'approval-flow', inputSchema, outputSchema })
.then(normalize).then(approval).then(receipt).commit();
const storage = new LibSQLStore({ id: 'example-storage', url: ':memory:' });
const mastra = new Mastra({ logger: false, storage, workflows: { workflow } });
const store = await storage.getStore('workflows');
const snapshot = async runId => JSON.parse(JSON.stringify(await store.loadWorkflowSnapshot({ workflowName: workflow.id, runId })));
const run = await workflow.createRun({ runId: 'approval-example' });
const started = await run.start({ inputData: { amount: 150.125 } });
assert.equal(started.status, 'suspended');
const saved = await snapshot(run.runId);
assert.deepEqual(saved.suspendedPaths, { approval: [1] });
assert.equal(saved.context.approval.payload.amount, 150.13);
console.log('start:', started.status, saved.context.approval.payload);
const resumed = await run.resume({
step: 'approval',
resumeData: { approved: false },
});
assert.equal(resumed.status, 'success');
assert.deepEqual(resumed.result, {
amount: 150.13, decision: 'rejected', receipt: 'rejected:150.13',
});
assert.deepEqual(executions, { normalize: 1, approval: 2, receipt: 1 });
console.log('resume:', resumed.status, resumed.result);
console.log('executions:', executions);
await mastra.shutdown();
预期的关键输出如下;日期和内部元数据未参与断言。
start: suspended { amount: 150.13 }
resume: success {
amount: 150.13,
decision: 'rejected',
receipt: 'rejected:150.13'
}
executions: { normalize: 1, approval: 2, receipt: 1 }
本次另测 approved: true,结果为 approved:150.13,执行次数相同;输入 50 时直接得到 automatic:50.00,三步各执行一次。示例中的 calls 记录用于核对:首次进入 approval 时没有恢复决定,第二次进入时保留原 inputData,并收到新的 resumeData 和此前的 suspendData。
这里的 url: ':memory:' 只让示例在同一进程内可复现;跨重启恢复需要耐久数据库,并以相同定义重新取得原 Run。本次验证覆盖普通顺序流程的暂停、拒绝、批准和自动通过,没有据此声称所有执行引擎、并发恢复或外部系统副作用都具有同样保证。
Memory、RAG 与 Processor:改变模型能看到什么
Memory 的作用是为本次请求组装上下文,并在输出后保存需要保留的内容。它通过 Processor 接入 Agent:输入阶段装入历史或召回资料,输出阶段保存消息并按配置建立索引。跨请求记忆来自这些状态及检索,不是模型调用后永久修改了权重。Memory 的输入处理器、输出处理器
几种能力解决的问题不同,不能都叫成“记住所有历史”:
| 能力 | 选择或生成什么 | 主要用途 |
|---|---|---|
| Message History | 最近的对话消息 | 保持眼前这段对话连续 |
| Working Memory | 按模板或 Schema 维护的结构化资料 | 保留偏好、目标等较长期的信息 |
| Semantic Recall | 与当前查询语义相关的历史消息 | 在较长历史中找回相关内容 |
| Observational Memory | 对原消息提炼的 observations,并进一步压缩 | 在上下文预算内保留长期对话的重要信息 |
该提交的基础 Memory 默认配置是 lastMessages: 10,semanticRecall 和 Working Memory 默认关闭。具体 Agent 与运行时配置可以覆盖它,不能把所有可选能力写成开箱即全部启用。默认配置
Semantic Recall 的链路是:把查询转换为向量,或由支持自嵌入的 Vector store 处理查询文本;按 Thread / Resource 作用域检索;拿命中的消息 ID 回 Storage 取原消息;把消息加入上下文。模型看到的是召回内容,不是直接阅读一串向量。检索与回取
Observational Memory 使用观察和反思来压缩上下文,与向量相似度召回采用不同机制。它能减少保留原始消息所需的上下文,但提炼后的资料不是原始记录的无损替代品;需要审计时应保留相应来源。Observational Memory 实现
RAG 面向外部知识:MDocument 负责解析、分块等预处理,应用建立向量索引;查询时 createVectorQueryTool() 运行检索并可选 rerank,再返回上下文与来源。把它注册为 Tool 后,模型可以选择调用它;它不必是每轮请求的固定前置步骤。文档分块、检索 Tool
Processor 的范围比 Memory 更广:processInput 处理一次请求的输入,processInputStep 处理每轮模型上下文,processLLMRequest / processLLMResponse 位于模型协议边界,processToolResult 位于工具结果写入历史之前。实现过滤、截断、计量或缓存时,先选择正确的生命周期;修改一次模型请求并不必然永久改写消息历史。Processor 契约
边界与采用判断
Snapshot 可用,不等于耐久执行已经配置好
该提交在 new Mastra() 未传 Storage 时,自动创建 InMemoryStore。如果显式 Storage 缺少 Workflow 或后台任务领域,还会补相应内存实现。这保证了内部调度可以使用存储接口,却不能保证进程重启后数据仍在。需要恢复真实业务时,应明确配置对应持久化领域,并在新的进程中通过同一个 Workflow ID 和 runId 加载原运行。默认存储与领域补齐
Workflow 的 Step 重试、恢复和默认包装也不自动提供外部系统的 exactly-once 副作用。发邮件、扣款、创建工单等操作应使用业务幂等键、唯一约束或目标系统的幂等接口;Snapshot 只能表达引擎保存了哪些状态,不能代替跨系统事务。并发恢复的协调还依赖 Storage 是否支持相应条件更新。默认包装、并发恢复协调
Trace、评分与正确性是三件事
Observability 记录模型、Tool、Agent 和 Workflow 的执行,Exporter 把记录发送到 Storage 或外部系统。Evals 中的 MastraScorer 则组织 preprocess → analyze → generateScore → generateReason 等评价步骤,可以使用确定性函数,也可以请求模型评分。Trace 帮助解释发生了什么,Scorer 按给定标准评价结果;两者都需要结合参考数据和业务断言才能支持质量判断。Exporter、Scorer
什么时候值得引入
以下是依据模块边界作出的工程判断:当应用同时需要多步 Tools、Memory、可暂停流程和统一调试界面时,Mastra 能让这些能力共享运行模型与服务。若只需要一次模型请求和一个简单 Tool,完整引入这些抽象未必划算;应先比较已有代码真正缺少哪一层。
确定性的业务主流程适合放进 Workflow,把开放式决策限定在某个 Agent Step 内。例如程序明确规定“收集材料 → Agent 分析 → 人工批准 → 执行”,模型只负责分析和建议;它不必获得改变批准步骤的权力。反过来,把所有业务逻辑交给一个自由 Agent,并不会因为接入框架就自然获得可预测的流程。
对数据库、模型、执行环境与部署适配分别做选择。Workspace 能接文件系统和 Sandbox,但是否隔离命令、网络和文件权限取决于具体实现;MCP 解决能力发现和调用,也不替业务完成授权。部署到某个平台时,同样要验证该 Adapter 的运行时限制与恢复语义,而不是把“TypeScript 可运行”当作全部支持的证据。Workspace 配置
版本和许可要看对应范围
本文源码快照的根工作区要求 Node.js ^22.19.0 || >=24.11.0、pnpm >=11.0.0,声明 packageManager: pnpm@11.21.0;@mastra/core@1.78.1-alpha.0 的包声明则要求 Node.js >=22.13.0。前者是开发整个仓库的条件,后者是这个包的运行条件,不宜互换。各包版本独立,也不能拿仓库根版本代表整个框架。根 package.json、Core package.json
仓库主体在其声明范围内采用 Apache-2.0;任意 ee/ 目录使用另列的 Enterprise License,第三方组件保留原许可。该 Enterprise License 明确区分 source-available 与 open source,并为生产使用列出书面协议和有效 License Key 条件。采用某项功能前要核对它所在的目录,不能把主体许可扩大到全仓。许可范围、Enterprise License
建议的源码阅读顺序
先读 packages/core/src/mastra/index.ts,了解对象如何注册并共享服务;接着看 agent/agent.ts 的 generate()、stream() 和内部执行入口,再顺着 loop/ 阅读模型步骤与工具步骤。之后单独阅读 workflows/workflow.ts、default.ts 和 handlers/step.ts,把普通 Agent Loop 与持久恢复的状态边界分开。
阅读时可以反复追问三个问题:这个字段是应用配置还是本次运行状态?这个结果只是发给前端,还是已经写入 Storage?重试或恢复后,哪段业务代码会再次执行?这三个问题比记住所有包名更能帮助定位问题。
关联阅读:Pi Agent 框架架构、Eve 的 Durable Agent 与部署边界、Agent 开发中的测试方法。