SEP-1686:任务
社区译文;来源: MCP 29affa6b
| 字段 | 值 |
|---|---|
| SEP | 1686 |
| 标题 | 任务 |
| 状态 | Final |
| 类型 | 标准跟踪 |
| 创建日期 | 2025-10-20 |
| 作者 | Surbhi Bansal, Luca Chang |
| 发起人 | 无 |
| PR | #1686 |
摘要
本 SEP 作为历史记录保留,记录
2025-11-25版规范中交付的实验性任务功能。下文代码示例是基于 v1 SDK 编写的非规范性伪代码。草案规范已将任务从核心协议移出,放入io.modelcontextprotocol/tasks扩展(SEP-2663)。
本 SEP 改进模型上下文协议(MCP)对基于任务的工作流的支持。它同时引入任务原语和关联的任务 ID;在任务完成后、服务器定义的保留期内,可以用它们查询任务状态和结果。该原语用于增强其他请求(例如工具调用),让支持此原语的服务器可以对所有请求采用“立即调用、稍后获取”的执行模式。
动机
当前 MCP 规范支持执行请求并最终收到响应的工具调用,也允许为工具调用传入进度令牌,以便集成 MCP 的进度跟踪功能,使宿主应用能够通过通知接收工具调用的状态更新。但是,客户端无法显式请求工具调用的状态,因此可能出现工具调用已在服务器端丢失、却无法确定响应或通知是否还会到达的情况。同样,工具调用完成后,客户端也无法显式重新获取其结果;如果结果丢失,客户端只能再次调用工具,而对于预计耗时数分钟甚至更久的工具,这并不可取。对于封装现有工作流 API 的 MCP 服务器,这一点尤为重要,例如 AWS Step Functions、Workflows for Google Cloud、代表 CI/CD 流水线的 API,以及其他类似应用。
目前,单个 MCP 服务器可以通过某些折中方式来表达支持这种模式的工具。例如,服务器可能公开一个 long_running_tool,并希望支持该模式,于是将其拆分为三个独立工具:
start_long_running_tool:启动long_running_tool所代表的工作,并返回某种跟踪令牌,例如作业 ID。get_long_running_tool_status(token):接收跟踪令牌并返回工具调用的当前状态,告知调用方操作仍在进行。get_long_running_tool_result(token):接收跟踪令牌,并在结果可用时返回工具调用结果。
以这种方式表达工具看似解决了用例,却引入了新问题:工具通常由 Agent 编排,而由 Agent 驱动的轮询既造成不必要的成本,也缺乏一致性,因为它依赖提示词工程来引导 Agent 执行轮询。在原始 long_running_tool 场景中,客户端无法知道自己是否还会收到响应;在 start_long_running_tool 场景中,应用又无法知道 Agent 是否会按照服务器的特定合同编排工具。
宿主应用也无法接管这种编排,因为此类工具拆分依赖约定,并且不同 MCP 服务器可能采用不同实现:一个服务器可能像示例一样,用三个工具表示一个概念操作;对于更复杂的多步骤操作,也可能需要更多工具。
另一方面,如果不需要主动轮询任务,现有 MCP 服务器可以用单个工具调用完整封装工作流 API,并在内部轮询结果,但这会带来不理想的实现成本:封装现有工作流 API 的 MCP 服务器,最终只是在轮询其他系统。
受影响的客户用例 这些问题有 Amazon 在内部和外部客户中观察到的真实用例作为支撑(非公开身份已隐去):
1. 医疗保健与生命科学数据分析 挑战: Amazon 的医疗保健和生命科学行业客户正尝试使用 MCP 封装现有计算工具,以分析分子属性并预测药物相互作用;每个作业会从化学库处理数十万个数据点,并同时经过多个推理模型。这些复杂的多步骤工作流需要主动检查状态,因为它们可能持续数小时,重试并不可取。 当前变通方案: 尚未确定。 影响: 无法与实时研究工作流集成,阻碍交互式药物发现平台,并阻塞自动化研究流水线。这些客户正在寻找基于工作流的工具调用最佳实践,也指出 MCP 缺少一等支持是一项问题。如果无法解决长时间运行的工具调用,他们很可能放弃 MCP,继续使用现有平台。 理想方案: 对数分钟量级的操作提供可并发、可轮询的工具调用;对数小时量级的长时间分析,提供某种推送通知系统,避免阻塞 Agent。本 SEP 支持前一种用例,并提供可扩展以支持后一种用例的框架。
2. 企业自动化平台 挑战: Amazon 的大型企业客户希望开发内部 MCP 平台,在组织内自动化 SDLC 流程,并扩展到销售、客户服务、法务、人力资源和跨部门团队。他们指出,其复杂业务流程自动化包含长时间运行的 Agent 及 Agent-工具交互。 当前变通方案: 尚未确定;正在考虑基于 webhook、位于 MCP 之外的应用层系统。 影响: 宿主应用不了解工具执行状态,会阻碍复杂业务流程自动化,并限制复杂的多步骤操作。这些客户希望并发分派流程、稍后收集结果,也把缺少显式延迟获取能力视为问题,并考虑采用复杂的应用层通知系统作为变通方案。 理想方案: 提供内置机制主动检查进行中工作的状态,避免各服务器按照自己的工具约定分别实现通知系统。
3. 代码迁移工作流
挑战: Amazon 已有自动执行代码迁移和转换的工具,用于升级自身及外部客户的代码库,并正尝试将这些工具封装到 MCP 服务器中。这些迁移会分析依赖项、转换代码以避开已弃用的运行时功能,并在多个仓库中验证变更。迁移耗时从数分钟到数小时不等,取决于迁移范围、复杂度和验证要求。
当前变通方案: 开发者将一个作业拆成 create 和 get 工具来手动跟踪,迫使模型管理状态并反复轮询完成情况。
影响: 许多工具都要重复实现这种手工轮询机制,导致开发体验不佳。某个团队曾调试一个问题:如果模型没有先列出作业,就会臆造作业名称。在大型工具集中验证大量工具都不会发生这种情况,既耗时又容易出错。
理想方案: 在数据层原生支持轮询工具状态,使工具可以转入后台而不阻塞聊天会话中的其他任务,同时仍支持确定性的轮询和结果获取。团队需要在 MCP 服务器的许多工具中采用相同模式,并希望有一套通用方案;本 SEP 直接支持这一点。
4. 测试执行平台 挑战: Amazon 的内部测试基础设施会执行包含数千个测试用例、跨服务集成测试和性能基准的综合测试套件。他们已构建 MCP 服务器来封装这套现有基础设施。 当前变通方案: 为了流式读取测试日志,MCP 服务器公开了一个可读取指定日志行范围的工具,因为它无法在执行完成时有效通知客户端。对于执行测试运行本身,尚无变通方案。 影响: 如果不采用单次持续数小时的工具调用,就无法同时运行测试套件和流式读取日志;而这种调用会在客户端或服务器端超时。这会阻止 Agent 在整个测试套件完成之前调查尚未结束的测试运行中的失败,等待时间可能长达数小时。 理想方案: 支持由宿主应用驱动的工具轮询,以获取中间结果,使客户端能在长时间运行的工具完成时收到通知。本 SEP 并未完整支持该用例(但确实支持轮询);如“未来工作”一节所述,可以扩展任务执行模型来支持它。
5. 深度研究
挑战: 深度研究工具会启动多个研究 Agent 来收集和总结主题信息,并在内部经过多轮搜索和对话,最终为调用方应用生成结果。工具执行时间较长,而且并不总能明确知道它是否仍在运行。
当前变通方案: 将研究工具拆为单独的 create 工具来创建报告作业,以及稍后获取该作业状态/结果的 get 工具。
影响: 与宿主应用配合使用时,Agent 有时会在重复调用 get 工具时出问题;特别是它在结束当前对话轮次前只调用一次工具,并声称正在“等待”,之后必须收到新的用户消息才能恢复。这也使过期时间更复杂,因为无法预测发生这种情况时客户端何时获取结果。可以通过给模型增加 wait 工具来规避,但这会阻止模型并发执行其他工作。
理想方案: 支持以确定性方式轮询工具调用状态,并在结果就绪时通知模型,以便立即获取工具结果并从服务器删除。除通知模型这一宿主应用职责外,本 SEP 完整支持该用例。
6. Agent 间通信(多 Agent 系统) 挑战: Amazon 的一个内部多 Agent 客户问答系统会遇到这样的场景:Agent 需要大量处理时间来完成复杂推理、研究或分析。当 Agent 通过 MCP 通信时,较慢的 Agent 会在整个系统中造成级联延迟,因为其他 Agent 被迫等待同伴完成工作。 当前变通方案: 尚未确定。 影响: 这种通信模式造成级联延迟,阻止 Agent 并行处理,并降低其他时效敏感交互的系统响应能力。 理想方案: 允许 Agent 并发处理其他工作,并在长时间运行的任务完成后收到通知。本 SEP 允许宿主应用为选定工具调用实现后台轮询而不阻塞 Agent,从而支持该用例。
这些用例表明,在生产环境中的此类 MCP 部署里,主动跟踪工具调用并延迟获取结果的机制是一项真实需求。
与现有架构集成 许多工作流驱动系统已提供主动执行跟踪能力,并内置状态元数据、监控和数据保留策略。本提案允许 MCP 服务器使用轻量 MCP 封装公开这些现有 API,同时维持它们原有的可靠性。
对现有架构的益处:
- 利用现有状态管理: AWS Step Functions、Workflows for Google Cloud 和 CI/CD 平台等系统已经维护执行状态、日志和结果。MCP 服务器可以公开这些系统的现有 API,而无需把轮询责任交给并不可靠的 Agent。
- 保留原生监控: 现有监控、告警和可观测性工具无需修改即可继续工作。执行几乎完全发生在现有工作流管理系统中。
- 降低实现开销: 服务器实现者不必构建新的状态管理、持久化或监控基础设施,可以专注于把现有 API 映射为 MCP 任务。
本 SEP 简化了与现有工作流的集成,使工作流服务能继续管理自身状态并提供良好客户体验,无需把职责转嫁给 Agent 轮询,也无需构建只负责轮询其他服务的 MCP 服务器。
规范
本 SEP 引入一种机制,使请求方(取决于通信方向,可以是客户端或服务器)可以用任务增强请求。任务是持久状态机,携带其所封装请求的底层执行状态信息,用于请求方轮询和延迟获取结果。每个任务都由请求方生成的任务 ID 唯一标识。
1. 用户交互模型
任务被设计为应用驱动:接收方严格控制哪些请求(如有)支持基于任务的执行,并管理这些任务的生命周期;请求方则负责用任务增强请求,并轮询任务结果。
实现可以通过任何符合自身需求的界面模式公开任务;协议本身不强制规定特定的用户交互模型。
2. 能力
支持任务增强请求的服务器和客户端在初始化时 必须(MUST) 声明 tasks capability。tasks capability 按请求类别组织,其中布尔属性表示哪些具体请求类型支持任务增强。
详情参阅 https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1732 。
3. 协议消息
3.1. 创建任务
要创建任务,请求方发送请求时在 _meta 中包含 modelcontextprotocol.io/task 键,其 taskId 值表示任务 ID。请求方 可以(MAY) 包含 keepAlive,其值表示任务完成后希望结果保留多长时间。
请求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "some_method",
"params": {
"_meta": {
"modelcontextprotocol.io/task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"keepAlive": 60000
}
}
}
}3.2. 获取任务
要获取任务状态,请求方发送 tasks/get 请求:
请求:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/get",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}响应:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"keepAlive": 30000,
"pollFrequency": 5000,
"status": "submitted",
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}3.3. 获取任务结果
要获取已完成任务的结果,请求方发送 tasks/result 请求:
请求:
{
"jsonrpc": "2.0",
"id": 4,
"method": "tasks/result",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}响应:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
}
],
"isError": false,
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}3.4. 任务创建通知
接收方创建任务时, 必须(MUST) 发送 notifications/tasks/created 通知,告知请求方任务已经创建,可以开始轮询。
通知:
{
"jsonrpc": "2.0",
"method": "notifications/tasks/created",
"params": {
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}任务 ID 通过 modelcontextprotocol.io/related-task 元数据键传递。除此之外,通知参数为空。
该通知解决了请求方可能在接收方完成任务创建前就尝试轮询的竞态条件。接收方在创建任务后立即发送通知,以此表示任务已可通过 tasks/get 查询。
不支持任务的接收方(因此会忽略请求中的任务元数据)不会发送该通知,请求方可以回退为等待原始请求响应。
3.5. 列出任务
要获取任务列表,请求方发送 tasks/list 请求。此操作支持分页。
请求:
{
"jsonrpc": "2.0",
"id": 5,
"method": "tasks/list",
"params": {
"cursor": "optional-cursor-value"
}
}响应:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"tasks": [
{
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "working",
"keepAlive": 30000,
"pollFrequency": 5000
},
{
"taskId": "abc123-def456-ghi789",
"status": "completed",
"keepAlive": 60000
}
],
"nextCursor": "next-page-cursor"
}
}3.6 删除任务
要显式删除任务及其关联结果,请求方发送 tasks/delete 请求。
请求:
{
"jsonrpc": "2.0",
"id": 6,
"method": "tasks/delete",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}响应:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}4. 行为要求
这些要求适用于所有支持接收任务增强请求的参与方。
4.1. 任务支持与处理
- 不支持对某个请求进行任务增强的接收方, 必须(MUST) 正常处理该请求,并忽略
_meta中的任何任务元数据。 - 支持任务增强的接收方 可以(MAY) 选择哪些请求类型支持任务。
4.2. 任务 ID 要求
- 任务 ID 必须(MUST) 是字符串值。
- 任务 ID 在接收方控制的所有任务中 应该(SHOULD) 保持唯一。
- 收到
_meta中带任务 ID 的请求时,接收方 可以(MAY) 验证所提供的任务 ID 尚未与其控制的其他任务关联。
4.3. 任务状态生命周期
- 任务创建时 必须(MUST) 以
submitted状态开始。 - 接收方 必须(MUST) 仅允许任务沿以下有效路径转换:
- 从
submitted:可转为working、input_required、completed、failed、cancelled或unknown - 从
working:可转为input_required、completed、failed、cancelled或unknown - 从
input_required:可转为working、completed、failed、cancelled或unknown - 处于
completed、failed、cancelled或unknown状态的任务 不得(MUST NOT) 转换到任何其他状态(终态)
- 从
- 如果执行立即完成,接收方 可以(MAY) 让任务直接从
submitted转为completed。 unknown是面向意外错误条件的终止回退状态。只要可能,接收方就 应该(SHOULD) 改用带错误消息的failed。
任务状态图:
stateDiagram-v2
[*] --> submitted
submitted --> working
submitted --> terminal
working --> input_required
working --> terminal
input_required --> working
input_required --> terminal
terminal --> [*]
note right of terminal
Terminal states:
• completed
• failed
• cancelled
• unknown
end note
4.4. 需要输入状态
- 当接收方发送与任务关联的请求(例如信息征询、采样)时,接收方 必须(MUST) 把任务转为
input_required状态。 - 接收方 必须(MUST) 在请求中包含
modelcontextprotocol.io/related-task元数据,以将其与任务关联。 - 接收方收到所有必需响应后,任务 可以(MAY) 离开
input_required状态(通常回到working)。 - 如果有多个关联请求待处理,任务在所有请求解决前 应该(SHOULD) 保持
input_required状态。
4.5. 保活与资源管理
- 接收方 可以(MAY) 覆盖请求的
keepAlive时长。 - 接收方在
tasks/get响应中 必须(MUST) 包含实际keepAlive时长(无限期则为null)。 - 任务进入终态(
completed、failed或cancelled)且keepAlive时长已过后,接收方 可以(MAY) 删除任务及其结果。 - 接收方 可以(MAY) 在
tasks/get响应中包含pollFrequency值(单位为毫秒),用于建议轮询间隔。提供该值时,请求方 应该(SHOULD) 遵循它。
4.6. 结果获取
- 仅当任务状态为
completed时,接收方才 必须(MUST) 从tasks/result返回结果。 - 对任何其他状态的任务调用
tasks/result时,接收方 必须(MUST) 返回错误。 - 在任务仍可用期间,请求方 可以(MAY) 对同一任务多次调用
tasks/result。
4.7. 关联任务相关消息
- 与任务相关的所有请求、通知和响应都 必须(MUST) 在其
_meta中包含modelcontextprotocol.io/related-task键;该值应是一个对象,其taskId与关联任务 ID 匹配。 - 例如,任务增强工具调用所依赖的信息征询请求 必须(MUST) 与该工具调用的任务共享同一个关联任务 ID。
4.8. 任务取消
- 接收方收到针对任务增强请求 JSON-RPC 请求 ID 的
notifications/cancelled通知时, 应该(SHOULD) 立即把任务转为cancelled状态,并停止与该任务关联的所有处理。 - 由于通知具有异步性质,接收方 可能(MAY) 无法立即取消任务处理。接收方 应该(SHOULD) 尽最大努力尽快停止执行。
- 如果
notifications/cancelled通知到达时任务已经进入终态(completed、failed、cancelled或unknown),接收方 应该(SHOULD) 忽略该通知。 - 任务进入
cancelled状态且keepAlive时长已过后,接收方 可以(MAY) 删除任务及其元数据。 - 请求方在任务执行期间 可以(MAY) 随时发送
notifications/cancelled,包括任务处于input_required状态时。若任务在input_required状态下被取消,接收方还 应该(SHOULD) 丢弃关联请求的所有待处理响应。 - 由于通知不会提供接收确认,请求方在发送取消通知后 应该(SHOULD) 继续通过
tasks/get轮询,以确认任务已转为cancelled状态。如果任务未在合理时间内转为cancelled,请求方 可以(MAY) 认为取消未被处理。
4.9. 任务列表
- 接收方 应该(SHOULD) 使用基于 cursor 的分页,以限制单个响应返回的任务数量。
- 如果还有更多任务,接收方 必须(MUST) 在响应中包含
nextCursor。 - 请求方 必须(MUST) 将 cursor 视为不透明令牌,不得尝试解析或修改它。
- 如果请求方能够通过
tasks/get获取某个任务,也 必须(MUST) 能够通过tasks/list获取该任务。
4.10 任务删除
- 接收方 可以(MAY) 自行决定接受或拒绝任意任务的删除请求。
- 如果接收方接受删除请求, 应该(SHOULD) 删除该任务及所有关联结果和元数据。
- 接收方 可以(MAY) 完全不支持删除,也可以只支持删除特定状态的任务(例如仅终态任务)。
- 对于包含敏感数据的任务,请求方 应该(SHOULD) 及时删除,而不应只依赖
keepAlive到期后清理。
5. 消息流
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686#issuecomment-3452378176
6. 数据类型
Task
任务表示请求的执行状态。任务元数据包括:
taskId:任务的唯一标识符keepAlive:完成后结果继续保持可用的时长,单位为毫秒pollFrequency:两次状态检查之间的建议间隔,单位为毫秒status:任务执行的当前状态
Task Status
任务可以处于以下状态之一:
submitted:请求已收到并进入执行队列working:请求当前正在处理input_required:请求正在等待请求方提供更多输入completed:请求已成功完成,结果可用failed:任务生命周期本身遇到错误,与关联请求逻辑无关cancelled:请求在完成前被取消unknown:接收方无法确定实际任务状态时,用于意外错误条件的终止回退状态
Task Metadata
使用任务执行增强请求时,在 _meta 中包含 modelcontextprotocol.io/task 键:
{
"modelcontextprotocol.io/task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"keepAlive": 60000
}
}字段:
taskId(字符串,必需):客户端生成的任务唯一标识符keepAlive(数字,可选):请求的结果保留时长,单位为毫秒
Task Creation Notification
接收方创建任务时,会发送 notifications/tasks/created 通知,表示任务已可供轮询。通知参数为空,任务 ID 通过 modelcontextprotocol.io/related-task 元数据键传递:
{
"jsonrpc": "2.0",
"method": "notifications/tasks/created",
"params": {
"_meta": {
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}
}
}该通知使请求方可以开始轮询,不会遇到任务可能尚未在接收方存在的竞态条件。
Task Get Request
tasks/get 请求获取任务的当前状态:
{
taskId: string; // The task identifier to query
}Task Get Response
tasks/get 响应包含:
{
taskId: string; // The task identifier
status: TaskStatus; // Current task state
keepAlive: number | null; // Actual retention duration in milliseconds, null for unlimited
pollFrequency?: number; // Suggested polling interval in milliseconds
error?: string; // Error message if status is "failed"
}Task Result Request
tasks/result 请求获取已完成任务的结果:
{
taskId: string; // The task identifier to retrieve results for
}Task Result Response
tasks/result 响应返回原请求本应返回的结果:
{
// The structure matches the result type of the original request
// For example, a tools/call task would return CallToolResult structure
[key: string]: unknown;
}结果结构取决于原始请求类型。接收方返回的结果结构,与请求不使用任务增强执行时本应返回的结构相同。
Task List Request
tasks/list 请求获取任务列表:
{
cursor?: string; // Optional cursor for pagination
}Task List Response
tasks/list 响应包含:
{
tasks: Array<{
taskId: string; // The task identifier
status: TaskStatus; // Current task state
keepAlive: number | null; // Retention duration in milliseconds, null for unlimited
pollFrequency?: number; // Suggested polling interval in milliseconds
error?: string; // Error message if status is "failed"
}>;
nextCursor?: string; // Cursor for next page, absent if no more results
}Related Task Metadata
与任务关联的所有请求、响应和通知都 必须(MUST) 在 _meta 中包含 modelcontextprotocol.io/related-task 键:
{
"modelcontextprotocol.io/related-task": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}这会在整个请求生命周期中把消息与其原始任务关联起来。
7. 错误处理
任务使用两种错误报告机制:
- 协议错误:针对协议层问题的标准 JSON-RPC 错误
- 任务执行错误:底层请求执行中的错误,通过任务状态报告
7.1. 协议错误
对于以下协议错误场景,接收方 必须(MUST) 返回标准 JSON-RPC 错误:
tasks/get、tasks/list或tasks/result中的taskId无效或不存在:-32602(Invalid params)tasks/list中的 cursor 无效或不存在:-32602(Invalid params)- 请求使用已被其他任务使用的
taskId(如果接收方验证任务 ID 唯一性):-32602(Invalid params) - 尝试在任务未处于
completed状态时获取结果:-32602(Invalid params) - 内部错误:
-32603(Internal error)
接收方 应该(SHOULD) 提供信息充分的错误消息,说明错误原因。
示例:找不到任务
{
"jsonrpc": "2.0",
"id": 70,
"error": {
"code": -32602,
"message": "Failed to retrieve task: Task not found"
}
}示例:任务已过期
{
"jsonrpc": "2.0",
"id": 71,
"error": {
"code": -32602,
"message": "Failed to retrieve task: Task has expired"
}
}注意:接收方没有义务无限期保留任务元数据。如果接收方已清除过期任务,返回“找不到”错误属于合规行为。
示例:为未完成任务请求结果
{
"jsonrpc": "2.0",
"id": 72,
"error": {
"code": -32602,
"message": "Cannot retrieve result: Task status is 'working', not 'completed'"
}
}示例:任务 ID 重复(如果接收方验证唯一性)
{
"jsonrpc": "2.0",
"id": 73,
"error": {
"code": -32602,
"message": "Task ID already exists: 786512e2-9e0d-44bd-8f29-789f320fe840"
}
}7.2. 任务执行错误
底层请求在执行期间失败时,任务转为 failed 状态。tasks/get 响应 应该(SHOULD) 包含 error 字段,提供失败详情:
{
taskId: string;
status: "failed";
keepAlive: number | null;
pollFrequency?: number;
error?: string; // Description of what went wrong
}示例:发生执行错误的任务
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "failed",
"keepAlive": 30000,
"error": "Tool execution failed: API rate limit exceeded"
}
}对于所封装请求自身已有错误语义的任务(例如带 isError: true 的 tools/call),任务仍应进入 completed 状态,错误信息通过原始请求类型的结果结构传递。
8. 安全考虑
8.1. 任务隔离与访问控制
- 接收方 应该(SHOULD) 限定任务 ID 的作用域,以防止未授权访问:
- 如果支持会话,将任务绑定到创建它的会话
- 如果使用身份验证,将任务绑定到身份验证上下文
- 拒绝来自不同会话或身份验证上下文、针对任务的
tasks/get、tasks/list或tasks/result请求
- 未实现会话或身份验证绑定的接收方 应该(SHOULD) 清楚记录此限制,因为任何能够猜出任务 ID 的请求方都可能访问任务结果。
- 接收方 应该(SHOULD) 对以下操作实施速率限制:
- 任务创建,以防资源耗尽
- 任务状态轮询,以防拒绝服务
- 任务结果获取尝试
- 任务列表请求,以防拒绝服务
8.2. 资源管理
警告:任务结果的持久时间可能超过原始请求执行时间。对于敏感操作,请求方应仔细考虑延长结果保留时间带来的安全影响,并考虑及时获取结果以及请求更短的
keepAlive时长。
- 接收方 应该(SHOULD):
- 限制每个请求方的并发任务数量
- 限制最大
keepAlive时长,以防资源无限期保留 - 及时清理过期任务以释放资源
- 接收方 应该(SHOULD):
- 记录支持的最大
keepAlive时长 - 记录每个请求方允许的最大并发任务数量
- 实现资源使用监控与告警
- 记录支持的最大
8.3. 审计与日志
- 接收方 应该(SHOULD):
- 记录任务创建、完成和获取事件,以供审计
- 在可用时将会话/身份验证上下文写入日志
- 监控可疑模式(例如大量失败的任务查询、过度轮询)
- 请求方 应该(SHOULD):
- 记录任务生命周期事件,以便调试和审计
- 跟踪任务 ID 及其关联操作
理由
设计决策:通用任务原语
选择将任务实现为通用的请求增强机制(而非工具专用或方法专用机制),是为了最大化协议的简洁性和灵活性。
任务被设计为适用于 MCP 协议中的任何请求类型,而不只适用于工具调用。这意味着 resources/read、prompts/get、sampling/createMessage 以及任何未来请求类型都可以用任务元数据增强。与工具专用设计相比,这种方法具有显著优势。
从协议角度看,该设计不需要为每种请求类型分别实现任务。工具、资源和提示词不必各自定义不同的异步模式,一组统一的任务管理方法(tasks/get 和 tasks/result)就能一致适用于所有请求类型。这种统一性降低了实现者的认知负担,并为使用协议的应用提供一致体验。
通用设计也提高了实现灵活性。服务器可以选择哪些请求支持任务增强,而无需修改协议或执行版本协商。如果服务器不支持某类请求的任务,只需忽略任务元数据并正常处理请求。服务器因此可以逐步为请求增加任务支持,从高价值操作开始,再根据实际使用模式扩展。
从架构上看,任务被视为元数据,而不是单独的执行模型。它们增强现有请求,而非取代请求。原始请求/响应流保持不变,请求最终仍会收到响应。任务只是额外提供一种基于轮询的结果获取机制。无论底层请求类型如何,该设计都能通过 modelcontextprotocol.io/related-task 元数据键一致关联相关消息(例如任务执行期间的信息征询)。
设计决策:基于元数据的增强
选择使用 _meta 承载任务信息,而不是使用专门的请求参数,是为了在请求语义和执行跟踪之间维持清晰的关注点分离。
任务信息本质上与请求语义正交。任务 ID 和 keepAlive 时长不会影响请求执行什么,只会影响结果的获取和保留方式。无论是否包含任务元数据,tools/call 请求执行的操作都相同;任务元数据只是提供另一种访问结果的机制。
通过把任务信息放在 _meta 中,我们在“执行什么”(请求参数)和“如何跟踪执行”(任务元数据)之间建立了清晰的架构边界。该边界让实现者更容易推理协议。请求参数定义要执行的操作;元数据则提供进度跟踪、任务管理及其他执行相关的正交信息。
这种方式也自然实现向后兼容。不支持任务的服务器可以忽略 _meta 内容,而不会破坏请求处理。请求参数仍然有效且完整,因此操作可以正常进行。这意味着不需要协议版本协商,新功能纯粹是增量式的,不会造成破坏。
SDK 可以在维持关注点分离的同时,为任务原语提供易用抽象,例如:
// === MCP SDK (Pseudocode based loosely on modelcontextprotocol/typescript-sdk) ===
/**
* NEW: A request that resolves to a result, either directly or by polling a task.
*/
class PendingRequest<TResult> {
constructor(readonly protocol: Protocol, readonly result: Promise<TResult>, readonly taskId?: string) {}
/**
* Waits for a result, calling onTaskStatus if provided and a task was created.
*/
async result({ onTaskStatus }): Promise<TResult> => {
if (!onTaskStatus || !this.taskId) {
// No task listener or task ID provided, just block for the result
return await result;
}
// Whichever is successful first (or a failure if all fail) is returned.
return Promise.any([
result, // Blocks for result
(async () => {
// Blocks for a notifications/tasks/created with the provided task ID
await this.protocol.waitForTask(this.taskId);
return await taskHandler(this.taskId);
})(),
]);
}
/**
* Encapsulates polling for a result, calling onTaskStatus after querying the task.
*/
private async taskHandler({ onTaskStatus }): Promise<TResult> => {
// Poll for completion
let task: Task;
do {
task = await this.protocol.getTask(this.taskId);
await onTaskStatus(task);
await sleep(task.pollFrequency ?? DEFAULT_POLLING_INTERNAL);
} while (!task.isTerminal());
// Process result
return await this.protocol.getTaskResult(this.taskId);
}
}
/**
* Simplified/partial client session implementation for illustration purposes.
* Extends a base class it shares with the server.
*/
class Client extends Protocol {
/**
* Existing request method, but with most implementation refactored to beginCallTool
*/
async callTool<TResult>(
params: CallToolRequest['params'],
resultSchema: Schema<TResult>,
) {
// Existing request methods can be changed to reuse new methods exposed for
// separating request/response flows.
const request = await this.beginCallTool(params, resultSchema);
return request.result();
}
/**
* NEW: Low-level method that starts a tool call and returns a PendingRequest
* object for more granular control.
*/
async beginCallTool<TResult>(
params: CallToolRequest['params'],
resultSchema: Schema<TResult>,
) {
const request = await this.beginRequest({ method: 'tools/call', params }, resultSchema, options);
return request;
}
}
// === HOST APPLICATION ===
// Begin a tool call with task support
const pending: PendingRequest<CallToolResult> = await client.beginCallTool(
{
name: "analyze_dataset",
arguments: { dataset: "large_file.csv" },
},
CallToolResultSchema,
{
keepAlive: 3600000,
},
);
// Client code can assume tasks are supported, and the fallback case can be handled internally
const result = await pending.result({
onTaskStatus: async (task) => {
await sendLatestStateSomewhere(task);
},
});由于该设计不改变基本请求语义,现有形式也会继续工作:
const result = await client.callTool(
{
name: "analyze_dataset",
arguments: { dataset: "large_file.csv" },
},
CallToolResultSchema,
);设计决策:客户端生成任务 ID
选择由客户端生成任务 ID,而不是由服务器分配,可以带来几项关键收益:
幂等性与容错: 最主要的收益是可以幂等地创建任务。客户端生成任务 ID 后,如果没有收到响应,可以安全重试任务增强请求,因为它知道服务器会识别重复任务 ID 并返回错误。这对于在不可靠网络上可靠运行至关重要:
- 如果请求超时,客户端可以安全重试,而不会创建重复任务
- 如果连接在响应到达前断开,客户端可以重新连接并重试
- 服务器验证任务 ID 唯一性,并对重复项返回错误,从而确认任务是否已创建
如果任务 ID 由服务器生成,超时或连接失败会造成不确定性:客户端不知道任务是否已创建,也无法在不冒创建重复任务风险的情况下安全重试。
简化客户端: 客户端生成任务 ID 后,不再需要把初始响应与任务标识符相关联,从而简化客户端实现。客户端可以立即使用自己生成的任务 ID 开始轮询任务状态,无需等待并解析响应来提取服务器分配的标识符。对于希望在响应到达前存储任务 ID 的异步编程模型,这一点尤其有价值。
服务器端权衡:
主要权衡是:封装已有任务标识符的工作流系统时,服务器通常需要维护客户端提供的任务 ID 与底层系统标识符之间的映射。例如,封装 AWS Step Functions 的 MCP 服务器可能收到客户端生成的任务 ID "client-abc-123",并需要记录它对应 Step Functions 执行 ARN "arn:aws:states:...:exec-xyz"。
这需要:
- 用于存储任务 ID 映射的持久存储(通常是简单的键值存储)
- 在任务的 keepAlive 时长内维护映射
- 在获取任务状态和结果时执行映射查询
不过,与把现有工作流系统集成到 MCP 的整体工作相比,这种复杂度通常很小。大多数工作流系统本就需要状态管理来跟踪执行,而维护任务 ID 映射只是直接增加一项功能。映射结构很简单(客户端任务 ID 映射到内部标识符),可以使用此类服务器很可能已经用于其他状态管理的数据库或键值存储来实现。
设计决策:任务创建通知
选择使用 notifications/tasks/created 通知,而不是像 #1391 提议的那样修改响应语义,是为了认可任务创建的异步性质,并在基于任务的轮询与传统请求/响应流之间实现高效竞速模式。
服务器创建任务时,必须向客户端发出任务已可轮询的信号。至少有两种可能方式:(1)初始请求可以同步返回任务元数据;(2)服务器可以发送通知。本提案选择通知,主要理由如下:
- 通知支持“发出后不等待”的请求处理。服务器可以接受请求、开始处理,并在任务创建后发送通知,而不必阻塞初始请求/响应周期。对于把工作分派到后台系统或队列的服务器,这一点尤为重要:它们可以立即确认请求,并在后台系统确认任务创建后发送通知。
- 通知支持竞速模式,使系统能够平滑降级。客户端可以让“等待原始请求响应”与“等待
notifications/tasks/created通知”竞速。如果服务器不支持任务,就不会有通知到达,由原始响应胜出。如果服务器支持任务,通知通常会先到达(或近乎同时到达),从而可以开始轮询。同步响应会迫使客户端等待响应后才能知道是否需要轮询。 - 通知避免混淆现有协议语义。如果初始请求响应包含任务元数据,而客户端随后轮询结果,就会改变现有通知类型的隐含含义:
- 进度通知:当前 MCP 规范要求进度通知引用“与进行中操作关联”的令牌。虽然“操作”没有正式定义,但其隐含理解是操作受请求/响应对约束,即发送响应后进度通知即停止。如果同步响应中包含任务元数据,任务执行期间仍需继续发送进度通知,这会把“操作”的隐含含义扩展为包含生命周期超出原始请求/响应周期的异步任务。基于通知的方式让进度通知继续绑定初始请求的生命周期,同时使未来的任务进度能够通过
modelcontextprotocol.io/related-task元数据明确关联,从而避免这种语义扩展。我们建议未来的 SEP 澄清进度规范中的“操作”定义。 - 取消语义:使用基于通知的方式时,
notifications/cancelled明确以原始请求 ID 为目标,并使关联任务转为cancelled状态,从而保持请求取消与任务生命周期管理之间的清晰分离。
- 进度通知:当前 MCP 规范要求进度通知引用“与进行中操作关联”的令牌。虽然“操作”没有正式定义,但其隐含理解是操作受请求/响应对约束,即发送响应后进度通知即停止。如果同步响应中包含任务元数据,任务执行期间仍需继续发送进度通知,这会把“操作”的隐含含义扩展为包含生命周期超出原始请求/响应周期的异步任务。基于通知的方式让进度通知继续绑定初始请求的生命周期,同时使未来的任务进度能够通过
尽管规范要求创建任务的服务器发送通知,但在某些边界场景中通知可能不可用:
- 不支持 stream 的 sHTTP:在客户端或服务器不支持 SSE stream 的环境中,无法传递通知。在这种情况下,客户端可以选择采用指数退避,主动使用
tasks/get轮询;不过这种方式并非标准行为,如果服务器不支持任务,可能产生不必要的轮询尝试。 - 连接降级场景:如果通知在传输中丢失,客户端应实现合理的超时行为,并回退到原始响应。
标准且推荐的方式是在开始轮询前等待 notifications/tasks/created 通知。未等待通知就主动轮询,只应视为受限环境下的回退机制。
设计决策:不声明能力
与工具、资源和提示词等其他协议功能不同,任务不要求能力协商。作出该决策是为了实现平滑降级和逐请求灵活性。
任务支持可以通过使用情况隐式确定,而无需显式能力声明。客户端发送任务增强请求时,服务器会根据自身能力处理。如果服务器不支持该请求类型的任务,只需忽略任务元数据,并通过原始请求/响应流正常返回结果。随后,客户端可以尝试调用 tasks/get,并处理由此产生的任何错误,以检测服务器不支持任务。
这种方式不需要复杂的握手或功能检测协议。客户端可以乐观地尝试任务增强,并在需要时平滑回退到直接处理响应。这使协议更有韧性,也更容易实现。
此外,该设计提供逐请求灵活性,而这很难通过能力表达。服务器可能只支持某些请求类型的任务,或者支持情况会随资源可用性或负载等运行时条件而变化。要求按请求类型声明细粒度能力,会显著增加协议复杂度,却没有带来足够收益。隐式检测模型更简单、更灵活。
考虑过的替代设计
工具专用异步执行:
本提案的早期版本(#1391)专门关注工具调用,在工具定义上引入 invocationMode 字段,把工具标记为支持同步、异步或两种执行模式。该方案还会在工具调用请求和响应结构中增加专用字段,并通过服务器端能力声明表示对异步工具执行的支持。
虽然该设计可以解决长时间运行工具调用的直接需要,但由于多项原因,最终选择了更通用的任务原语。首先,它把异步执行模式人为限制在工具上,而其他请求类型也有类似需求。资源读取可能成本高昂,提示词可能需要复杂处理,采样请求可能涉及较长的用户交互。为每种请求类型分别创建异步模式会导致协议碎片化和实现模式不一致。
其次,工具专用方式要求更复杂的能力协商和版本处理。服务器需要根据客户端能力过滤工具列表,SDK 则需要管理同步和异步工具的不同调用模式。这种复杂度会传导到实现栈的每一层。
最后,工具专用设计没有解决在所有 MCP 请求类型上延迟获取结果的更广泛架构需求。把方案泛化为可增强任意请求的任务原语后,整个协议都可以采用一致模式。更关键的是,这一基础可以扩展到子任务等未来协议消息和功能,因此更适合作为协议演进的构建块。
传输层方案: 另一种方式是在传输层完全解决此问题,不引入新的数据层原语。已有多个提案(#1335、#1442、#1597)处理传输相关问题,例如 sHTTP 的连接韧性、请求重试语义和 stream 管理。这些改进很有价值,可以缓解持续执行较长时间的请求所带来的许多扩展性和可靠性挑战。
但是,仅靠传输层方案不足以解决本 SEP 的用例。即使传输层完全可靠,仍然存在多项数据层问题。
首先,服务器和客户端需要一种方式来传达对执行模式的预期。没有这种信息,宿主应用无法就用户体验作出明智决定:应该阻塞、显示加载指示,还是允许用户继续工作?单独使用注解可以表示请求可能耗时较长,却无法提供主动检查状态或稍后获取结果的机制。
其次,传输层方案无法查看仍在进行中的请求执行状态。如果请求停止发送进度通知,客户端无法区分“服务器正在执行高成本工作”和“请求已丢失”。传输层重试可以确认连接仍然存活,但无法回答“这个特定请求是否仍在执行”。对于用户需要确信其工作仍在推进的操作,这种可见性至关重要。
再次,不同传输需要采用不同机制来处理这些问题。sHTTP 提案通过调整 stream 管理和重试语义来满足要求,但 stdio 没有相应的扩展点。这会形成传输专用的碎片化,使实现者必须根据传输选择以不同方式解决相同问题。数据层操作则可以在所有传输上提供一致语义。
最后,延迟获取结果和主动检查状态属于数据层问题,无法只靠传输改进解决。多次获取结果、指定保留时长以及处理清理工作的能力,都与底层消息如何传递相互独立。
基于资源的方案: 另一种可能的方式是利用现有 MCP 资源跟踪长时间运行的操作。例如,工具可以返回一个传达操作状态的链接资源,客户端订阅该资源,以便在操作完成时接收更新。这样,服务器可以用资源原语表示任务状态,并可能通过注解提供建议轮询频率。
虽然这种方式在技术上可行,服务器也仍可自由采用此类约定,但它存在与“动机”一节所述工具拆分模式相似的限制。与 start_tool 和 get_tool 约定一样,基于资源的跟踪系统依赖约定而非标准,因此会带来多项挑战。
最根本的问题是,没有一致方式区分普通资源(用于向模型公开)和状态跟踪资源(用于由应用轮询)。状态资源是否应向模型展示?客户端应如何把返回的资源与原始工具调用关联?如果缺少标准化,不同服务器会实现不同约定,迫使客户端、宿主和模型分别处理各服务器的特定方式。为资源扩展轮询频率、保活时长和显式状态等任务语义,会让资源承担一种全新且不同的用途,而这很难与其作为模型可访问内容的既有用途区分。
资源订阅模型还有一个额外问题:它以推送为基础,要求客户端等待资源变更通知,而不是主动轮询状态。这种方式适合某些用例,却无法处理客户端需要主动检查状态的场景,例如主动且确定性地确认工作是否仍在推进,而这正是本提案的原始意图。
任务原语提供专为此用例设计的标准协议层机制,其一致语义可由任何客户端使用,宿主应用无需理解服务器特有的约定,因此解决了这些问题。偏好或已经使用资源跟踪的服务器仍可继续采用该方式;本 SEP 提供的是能够满足更广泛已识别需求的一等替代方案。
向后兼容性
本 SEP 不会引入向后不兼容性。所有现有 MCP 功能保持不变:
兼容性保证:
- 无论是否带任务元数据,现有请求的工作方式都相同
- 不理解任务的服务器正常处理请求
- 不需要协议版本协商
- 不需要能力声明
平滑降级:
- 客户端让“等待原始请求响应”与“等待
notifications/tasks/created通知后进行轮询”相互竞速 - 客户端使用最先完成的一方(原始响应或基于任务的获取)
- 如果服务器不支持任务,就不会发送
notifications/tasks/created,客户端使用原始请求响应 - 如果服务器支持任务,就会发送
notifications/tasks/created通知,使客户端可以开始轮询结果 - 这种竞速模式无需能力协商或版本检测即可确保平滑降级
- 可以实现部分支持:服务器可以只为某些请求支持任务
采用路径:
- 服务器可以逐步实现任务支持,从高价值请求类型开始
- 客户端可以在支持任务的地方机会式地使用任务
- 客户端和服务器更新无需协调
未来工作
本 SEP 引入的任务原语为多项重要扩展奠定基础,这些扩展将增强 MCP 的工作流能力。
推送通知
本 SEP 关注客户端驱动的轮询,未来工作可以为任务状态变更引入服务器发起的通知。对于持续数小时甚至更久、连续轮询并不现实的操作,这尤其有价值。
基于通知的方式允许服务器在以下情况主动告知客户端:
- 任务完成或失败
- 任务到达里程碑或发生重要状态转换
- 任务需要输入(作为
input_required状态的补充)
具体实现可以根据传输能力采用 webhook 风格机制或持久通知通道。提议的任务 ID 和状态模型提供了必要基础设施,使服务器能够识别哪些任务需要通知,也使客户端能够把通知与未完成任务关联。
中间结果
当前任务模型只在完成时返回结果。未来扩展可以让任务在执行期间报告中间结果或进度制品。这将支持服务器在最终完成前产生部分输出的用例,例如:
- 在分析结果可用时逐步流式返回
- 报告多步骤操作中已完成的阶段
- 在完整处理继续进行时提供预览数据
中间结果将建立在提议的任务 ID 关联机制上,使服务器可以在整个生命周期中发送与同一任务 ID 关联的多个结果通知或响应消息。
嵌套任务执行
一项重要的未来增强是支持分层任务关系,使任务能在执行过程中派生子任务。这将支持由服务器编排的复杂多步骤工作流。
在嵌套任务模型中,服务器可以:
- 当父任务进入需要额外操作的状态时创建子任务
- 向客户端传达子任务要求,可能包括必需的工具调用或采样请求
- 跟踪子任务完成情况,并使用子任务结果推进父任务
- 通过任务 ID 层级维持来源关系,展示父任务与子任务之间的关系
例如,一个复杂分析任务可能派生多个数据收集子任务;每个子任务由自己的任务 ID 表示,但与父任务关联。父任务会保持等待状态(可能使用新的 tool_required 状态),直到所有必需子任务完成。
这种分层模型支持复杂的服务器控制工作流,同时维持客户端在任务树任意层级监控和获取结果的能力。
嵌套任务流示例
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: Client Creates Parent Task
C->>S: tools/call "deploy_application"<br/>_meta: {taskId: "deploy-123"}
S--)C: notifications/tasks/created
C->>S: tasks/get (taskId: "deploy-123")
S->>C: status: working
Note over S: Server determines subtasks needed
Note over C,S: Server Responds with Subtask Requirements
C->>S: tasks/get (taskId: "deploy-123")
S->>C: status: working<br/>childTasks: [{<br/> taskId: "build-456",<br/> toolName: "run_build",<br/> arguments: {...}<br/>}, {<br/> taskId: "test-789",<br/> toolName: "run_tests",<br/> arguments: {...}<br/>}]
Note over C: Client initiates subtasks
C->>S: tools/call "run_build"<br/>_meta: {taskId: "build-456", parentTaskId: "deploy-123"}
S--)C: notifications/tasks/created
C->>S: tools/call "run_tests"<br/>_meta: {taskId: "test-789", parentTaskId: "deploy-123"}
S--)C: notifications/tasks/created
Note over C: Client polls subtasks
C->>S: tasks/get (taskId: "build-456")
S->>C: status: completed
C->>S: tasks/get (taskId: "test-789")
S->>C: status: completed
Note over S: All subtasks complete, parent continues
C->>S: tasks/get (taskId: "deploy-123")
S->>C: status: completed
C->>S: tasks/result (taskId: "deploy-123")
S->>C: Deployment complete
潜在数据模型扩展: 任务状态响应可以扩展为包含父任务和子任务关系:
{
taskId: string;
status: TaskStatus;
keepAlive: number | null;
pollFrequency?: number;
error?: string;
// Extensions for nested tasks
parentTaskId?: string; // ID of parent task, if this is a subtask
childTasks?: Array<{ // Subtasks required by this task
taskId: string; // Pre-generated task ID for the subtask
toolName: string; // Tool to call for this subtask
arguments?: object; // Arguments for the tool call
}>;
}这使客户端能够:
- 通过
childTasks数组发现父任务所需的子任务 - 使用预生成的任务 ID 和提供的参数,发起所需的子任务工具调用
- 通过
parentTaskId沿父子关系浏览任务层级 - 通过轮询每个子任务 ID 监控所有子任务
- 等待所有子任务完成后,再检查父任务是否完成
现有任务元数据和状态生命周期的设计可以向前兼容这些扩展。