Skip to content

[Bug]: Callback 回灌后仅写入 READY_TO_RESUME,父 Agent 不会自动续跑 #88

Description

Checklist

  • 我已经搜索过相关问题,但没有得到预期的帮助。
  • 最新版本中该错误尚未修复。
  • 请注意,如果您提交的Bug描述缺少相应的环境信息和最小可复现的demo,我们将很难复现和解决该问题,从而降低收到反馈的可能性,甚至该问题将被关闭。

🐞 问题详细描述

Callback 回灌后仅写入 READY_TO_RESUME,父 Agent 不会自动续跑

结论摘要

下游 Runtime 将终态 Task callback 到调用方 Runtime 后,当前 callback receiver 可以完成解析、幂等、
remote task 绑定查找和 shadow batch 更新,但处理链在 READY_TO_RESUME 停止,没有重新调度父
AgentHandler。现有双 Runtime 集成测试必须显式发送第二次 SendMessage("continue"),父 Task 才继续执行。

该问题的准确责任归属是 Runtime 的远程编排 / continuation 调度能力

  • 服务入口与 Push Notification 能力负责标准服务入口、callback sender/receiver 及跨特性旅程;
  • outbound 远程 Agent 编排、结果回灌和父执行链恢复由远程 Agent 编排能力承接;
  • Deep Research Agent 业务层只负责在 Runtime 注入远端工具结果后继续研究,不应接收 HTTP callback、
    扫描 shadow task、监听 READY_TO_RESUME 或向自身补发第二次 SendMessage

当前代码已经具备 runtime.remoteToolResults -> InteractiveInput -> AgentCore 的恢复输入适配,现有证据指向
“缺少自动触发”,而不是 Deep Research 业务代码缺少 callback handler。

1. 场景

1.1 特性与需求来源

主归属特性:

  • 任务驱动的远程 Agent 通信 / 远程 Agent 编排

受影响的跨特性旅程:

  • 服务入口与 Push Notification

相关设计要求:

  1. 服务入口能力范围设计 第 1 章明确:主动发现远端 Agent、outbound 编排和结果回灌由远程编排特性承接,
    不属于服务入口能力主体。
  2. 服务入口能力范围设计 第 4 章“runtime-to-runtime callback 回灌恢复”要求:callback receiver 按
    parent task/context/tool call/remote invocation 绑定结果,并恢复等待中的执行链或 Task。
  3. 服务入口详细设计 第 3.3 节要求:callback 回灌后恢复本地 Agent;本地 Agent 完成后唤醒仍存在的 bounded
    wait;等待窗口耗尽后,Task 仍应在后台推进并可通过 GetTask 观察。
  4. 服务入口详细设计 第 3.5、4.5、7.2 节要求:callback receiver 完成绑定恢复后,
    A2AEnabledServeOrchestrator 恢复本地 Agent 执行,而不是等待一条新的用户消息。
  5. 远程 Agent 编排详细设计 将“远程 COMPLETED -> InteractiveInput -> 本地 Agent resume”列为结果回灌能力。

1.2 涉及模块和角色

角色/模块 本场景职责 当前状态
Search Agent 产生搜索业务结果 已执行
Search Runtime 按 Deep Research Runtime 提供的 callback config 投递终态 Task 已执行
Deep Research Runtime callback receiver 鉴权、解析、幂等、绑定查找 已实现
Deep Research Runtime remote coordinator 将 Search outcome 写入 shadow batch 已实现到 READY_TO_RESUME
Deep Research Runtime continuation 调度 自动重新进入父 Task 执行链 缺失
AgentCore adapter runtime.remoteToolResults 转为 InteractiveInput 已实现
Deep Research Agent 消费搜索结果并继续生成报告 具备恢复输入后继续执行的框架接线;当前未被自动调度

1.3 验证基线

agent-runtime-java: develop @ acd12ce7259f66e424a8ee3e9348a6a8fb081004
agent-core-java:    730     @ e0482cd0224b76c6422189a3eb1cf13fe7b52601
agent-solution:     common  @ 55226a1d8d4ecd9c76d22e28bf241b7e04c033d6
A2A SDK:            1.0.0.Final

1.4 测试场景

普通 Client
  -> Deep Research Runtime: SendMessage(普通 Client 不携带 callback URL)
  -> Deep Research Agent 调用 Search Agent
  -> Deep Research Runtime 调用 Search Runtime,并携带 Deep Research Runtime 自身 callback config
  -> Search Agent 完成
  -> Search Runtime callback Deep Research Runtime
  -> Deep Research Runtime 返回 callback accepted,并写入 READY_TO_RESUME
  -> 预期:自动恢复 Deep Research Agent,完成父 Task
  -> 实际:没有自动重新调度;只有第二次 SendMessage 才会消费 READY_TO_RESUME

2. 问题概述

当前实现把“callback 回灌”和“父执行链恢复”拆成了两个没有自动连接的阶段:

阶段一:callback 请求
  A2aPushNotificationCallbackController
    -> A2AEnabledServeOrchestrator.onAccepted()
    -> RemoteInvocationBatchCoordinator.recoverCallback()
    -> recoverShadow()
    -> saveShadow(..., READY_TO_RESUME)
    -> HTTP 200 accepted

阶段二:后续新请求
  query()/streamQuery()
    -> syncResumePending()/tryResumePending()
    -> coordinator.resume()
    -> buildBatchResumeRequest(runtime.remoteToolResults)
    -> AgentHandler.query()/streamQuery()

阶段一不会启动阶段二。callback 已经是下游任务完成事件,不是用户输入,要求调用方再发送一次
SendMessage 会产生以下语义问题:

  • 把系统 continuation 错误表达为用户新消息;
  • 普通 Client 必须知道内部 Search callback 已到达,破坏 Runtime 封装;
  • 原始请求采用 returnImmediately + GetTask 时,父 Task 可能永久停留在非终态;
  • Deep Research Runtime 作为多级调用链中间节点时,无法完成本地任务,也无法继续向更上游 callback。

3. 问题分析

3.1 生产代码调用链

Callback Controller 在首次 notification 时调用 handler:

boolean isHandled = callbackHandler.onAccepted(
    new A2aPushNotificationCallback(notificationId, task));

A2AEnabledServeOrchestrator.onAccepted() 的全部行为是:

public boolean onAccepted(A2aPushNotificationCallback callback) {
    return callback != null && batchCoordinator.recoverCallback(callback.task());
}

RemoteInvocationBatchCoordinator.recoverShadow() 匹配 member 后只更新并保存 shadow:

batchMapper.applyOutcome(member, batchMapper.callbackOutcome(task), null);
saveShadow(batch, batchMapper.shadowState(batch));
return true;

当所有远端成员到达结果性状态时,shadowState(batch) 返回 READY_TO_RESUME。上述调用链没有:

  • 调用 AgentHandler
  • 调用 ServeOrchestrator.query() / streamQuery()
  • 投递 continuation 事件或任务;
  • 唤醒可执行父 Task 的后台调度器。

3.2 READY_TO_RESUME 只有在新请求进入时被消费

RemoteInvocationBatchCoordinator.resume() 读取 shadow,发现 READY_TO_RESUME 后调用
resumeReadyBatch()。但 resume() 只从 A2AEnabledServeOrchestrator.query() / streamQuery() 中的
syncResumePending() / tryResumePending() 进入。

因此当前状态机实际是:

callback -> READY_TO_RESUME -> 等待下一次 inbound 请求 -> resume

而设计要求是:

callback -> READY_TO_RESUME -> continuation 调度 -> resume

3.3 仓库现有双 Runtime 集成测试使用了补偿性第二次请求

DualRuntimeCallbackIntegrationTest.callerDelegatesToCalleeWaitsForCallbackThenResumesOriginalTask()

  1. 首次 SendMessage 后断言父 Task 为 TASK_STATE_INPUT_REQUIRED
  2. 等待 shadow batch 为 READY_TO_RESUME
  3. 显式发送第二次 SendMessage,内容为 continue
  4. 第二次请求之后才断言父 Task 为 TASK_STATE_COMPLETED

关键测试代码:

Map<String, Object> readyBatch = awaitReadyRemoteBatch(taskId);
assertThat(readyBatch).containsEntry("state", "READY_TO_RESUME");

Map<String, Object> resumedBody = json(postA2a(rpc(
    "SendMessage", "dual-runtime-resume", ... "continue" ...)));

该测试证明 callback outcome 可以回灌,也证明当前续跑入口依赖第二次请求;它没有覆盖设计要求的 callback
自动重新调度。

3.4 对照组:恢复结果到 AgentCore 的适配已经存在

A2AEnabledServeOrchestrator.buildBatchResumeRequest() 会写入:

metadata.put("runtime.remoteToolResults", new LinkedHashMap<>(resolution.results()));

JiuwenCoreAgentHandler.buildInputs() 会将该 Map 转成:

InteractiveInput interactiveInput = new InteractiveInput();
interactiveInput.setUserInputs(copyStringMap(resultMap));
inputs.put("query", interactiveInput);

JiuwenCoreAgentHandlerTest 已覆盖 runtime.remoteToolResults -> InteractiveInput 和恢复后返回结果。
Deep Research Demo 通过 JiuwenCoreAgentExtHandler 包装 DeepAgent,会复用该处理链。

因此目前没有证据要求 Deep Research Agent 业务层增加 callback/resume 代码。若 continuation 调度补齐后
DeepAgent 仍不能从 checkpoint/interrupt 恢复,应作为独立的 AgentCore/adapter 恢复契约问题处理。

3.5 验证限制

  • 本轮静态核对了生产代码、Runtime 单元/集成测试和 Demo 接线。
  • 用户侧实际集成已观察到 Search Runtime 发出 callback,但 Deep Research Agent 未自动继续;当前目录未保留
    可关联 taskId 的完整双方服务日志,因此本文不把该口头观察作为唯一证据。
  • 本机当前没有 mvn 命令,agent-runtime-java 也没有 Maven Wrapper,本轮无法重新执行
    DualRuntimeCallbackIntegrationTest;本文记录的是仓库现有测试源码所表达的确定性步骤,不声明本轮测试通过。
  • 正式黑盒回归应由自动化测试承接,不在 Demo 增加测试 Controller、轮询器或补偿请求。

4. 复现条件

4.1 前置条件

  1. 部署 Deep Research Runtime 和 Search Runtime。
  2. Deep Research Runtime 设置:
export SPRING_PROFILES_ACTIVE=callback-auth
export DEEP_RESEARCH_PUSH_NOTIFICATIONS=true
export DEEP_RESEARCH_PUBLIC_URL=http://DEEP_RESEARCH_HOST:18090
export DEEP_RESEARCH_CALLBACK_TOKEN=<shared-token>
export SEARCH_AGENT_URL=http://SEARCH_HOST:18091
export SEARCH_AGENT_STREAMING=false
  1. Search Runtime 可保持:
export SEARCH_AGENT_PUSH_NOTIFICATIONS=false
export SEARCH_AGENT_PUBLIC_URL=http://SEARCH_HOST:18091

SEARCH_AGENT_PUSH_NOTIFICATIONS=false 不禁止 Search Runtime 按入站 callback config 回调 Deep Research
Runtime;它只表示 Search Runtime 不请求自己的下游回调,也不开放自己的 receiver。

  1. Search Runtime 能访问:
http://DEEP_RESEARCH_HOST:18090/a2a/push-notifications/callback

4.2 复现请求

普通 Client 调用 Deep Research Runtime,不携带 callback URL:

curl -X POST http://DEEP_RESEARCH_HOST:18090/a2a/ \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": "callback-auto-resume-repro",
    "method": "SendMessage",
    "params": {
      "message": {
        "role": "ROLE_USER",
        "messageId": "callback-auto-resume-message",
        "contextId": "callback-auto-resume-context",
        "parts": [
          {
            "kind": "text",
            "text": "对比 DeepSeek 和 GLM 的 API 定价并给出来源"
          }
        ]
      },
      "configuration": {
        "returnImmediately": true
      }
    }
  }'

记录 Deep Research 父 taskId,但不要发送第二次 SendMessage

4.3 观察步骤

  1. 确认 Deep Research Agent 产生 search-agent 远程委派。
  2. 确认 Deep Research Runtime 出站请求被转换为标准 taskPushNotificationConfig
  3. 确认 Search Task 进入 COMPLETEDFAILED 结果性状态。
  4. 确认 Search Runtime POST Deep Research 固定 callback receiver,receiver 返回:
{"status":"accepted","notificationId":"..."}
  1. 检查 Deep Research Runtime 的 shadow task,remote member 已完成且 batch 为
    READY_TO_RESUME
  2. 不发送补偿性 SendMessage,等待超过原 bounded wait 窗口。
  3. 使用 GetTask 查询父 taskId;当前实现不会因为 callback 自动重新进入 Deep Research Agent 并生成最终报告。
  4. 对照执行:发送第二次带相同 taskId/contextId 的 SendMessage("continue") 后,当前代码会消费
    READY_TO_RESUME。该步骤只用于证明缺失触发点,不是期望业务协议。

5. 问题代码位置

位置 当前行为 问题
A2aPushNotificationCallbackController.java:96-107 幂等保存并调用 callback handler receiver 正常接受后依赖 handler 完成恢复
A2AEnabledServeOrchestrator.java:151-153 onAccepted() 只调用 recoverCallback() 没有 continuation 调度
RemoteInvocationBatchCoordinator.java:206-246 查找 binding、应用 outcome、保存 shadow 状态到 READY_TO_RESUME 后停止
RemoteInvocationBatchCoordinator.java:178-203 新请求进入时读取并恢复 batch 恢复被绑定到下一次 inbound 请求
A2AEnabledServeOrchestrator.java:156-214 streamQuery() 调用 tryResumePending() callback 线程不会进入该方法
A2AEnabledServeOrchestrator.java:506-520 构造带 runtime.remoteToolResults 的请求 结果注入能力已存在,但没有自动触发
DualRuntimeCallbackIntegrationTest.java:104-145 READY 后显式发第二次 SendMessage 测试未覆盖自动续跑契约
JiuwenCoreAgentHandler.java:393-413 remote results 转 InteractiveInput 说明业务 Agent 前的恢复适配已存在

6. 问题影响

  1. runtime-to-runtime callback 只能完成“结果到达并持久化”,不能完成“父 Agent 自动恢复”的闭环。
  2. Deep Research -> Search callback 到达后,Deep Research 父 Task 可能长期停留在非终态,普通 Client
    使用 GetTask 也无法获得最终研究报告。
  3. 多级 callback 链中,Deep Research Runtime 无法完成本地父 Task,因此也无法向更上游 Runtime 投递最终结果。
  4. callback 在 bounded wait 窗口内到达时,原请求等待句柄不会被本地最终结果唤醒;callback 迟到时,也没有
    后台 continuation 推进 Task。
  5. 让业务 Demo 自发第二次请求会复制 Runtime 状态机、引入虚假用户消息,并在重复 callback、多实例和重启
    场景中产生重复执行风险。

7. 建议修复方案

方案 A:增加 Runtime continuation scheduler,推荐

在 callback outcome 使 batch 首次进入 READY_TO_RESUME 时,向 Runtime 内部 continuation scheduler
提交父 Task 恢复任务:

callback accepted
  -> 原子更新 shadow batch
  -> claim continuation(parentTaskId, batchId)
  -> 从可信 Task/shadow 状态恢复执行上下文
  -> 构造 runtime.remoteToolResults
  -> 重新进入正常 Agent 执行/Task 更新管线
  -> 更新父 Task 终态

注意事项:

  • 不应直接绕过 A2AAgentExecutor/Task 事件管线裸调 AgentHandler,否则父 Task 状态、artifact、FAILED
    事件和上游 callback 可能无法正常生成;
  • continuation 必须按 parentTaskId + batchId 幂等 claim,避免重复 callback 导致重复 Agent 执行;
  • 原始请求、conversation/context、checkpoint 和身份上下文必须来自可信持久化状态,不从 callback body 反推;
  • callback handler 的 HTTP 响应不必同步等待父 Agent 完成。

方案 B:事件驱动的持久化 continuation queue

READY_TO_RESUME 视为可消费状态,写入持久化 continuation queue,由 worker 重新进入父执行链。

优点:适合 callback 迟到、进程重启和多实例部署;HTTP callback 线程不承担长时间业务执行。

要求:必须实现 claim/lease、失败重试、死信/可观察性,以及 shadow 状态与 continuation 事件的原子性或可靠
outbox,避免“READY 已保存但事件丢失”。

方案 C:活动 bounded wait 唤醒 + 后台恢复双路径

如果原 SendMessage 的 bounded wait 仍存在:

  • callback 触发 continuation;
  • 本地 Agent 最终结果完成对应 future;
  • controller 将最终 JSON-RPC result 返回原 Client。

如果等待窗口已耗尽:

  • continuation 仍在后台执行;
  • 更新父 Task,供 GetTask 查询;
  • 若父 Task 本身绑定了上游 callback config,则终态后继续向上游投递。

该方案不能只实现“唤醒现存内存 future”,否则迟到 callback 和重启场景仍无法恢复。

方案 D:补充正式自动化回归覆盖

建议在正式自动化测试中增加:

  1. callback 在 bounded wait 内到达,原请求由父 Agent 最终结果唤醒;
  2. callback 在 bounded wait 后到达,父 Task 在后台恢复,GetTask 最终为 COMPLETED / FAILED
  3. 重复同 notification id 不重复调度父 Agent;
  4. 同 notification id 不同 payload 返回冲突且不改写结果;
  5. continuation 调度后父 Agent 再次中断、失败或调用另一个远端 Agent时,状态机行为明确;
  6. 多级 callback:Search -> Deep Research 恢复完成后,Deep Research Runtime 能继续 callback 上游 Runtime;
  7. Runtime 重启/多实例下,已持久化的 READY continuation 不丢失且不会重复执行。

不建议方案

  • Demo 监听 TaskStore 或扫描 READY_TO_RESUME
  • Demo 新增私有 callback Controller 或恢复 scheduler;
  • callback 到达后由 Demo/Client 向 Deep Research Runtime 补发第二次 SendMessage
  • Runtime 通过 HTTP 调用自身公开 SendMessage 并伪造用户文本 continue

这些方案会把 Runtime continuation 责任泄漏到业务层,且无法正确覆盖身份、幂等、Task 事件和多实例语义。

详细的环境信息描述

runtime java develop分支

其他辅助信息

版本信息

感谢您的贡献 🎉!

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions