DeepAgent 对 a2a_delegate 的「控制 / 内容双通道」——内容镜像泄漏与屏蔽不对称
一句话:同样是 a2a_delegate 委派,在 DeepAgent(deep-research)拓扑下,控制通道的中断被 orchestrator
屏蔽(内部驱动远端调用),但 DeepAgent 任务回路额外发布了一条 controller_output 内容镜像,把原始
a2a_delegate 的 __interaction__ 原样透传给客户端;而非 DeepAgent 拓扑(edpa plan-agent)只有控制通道、
被完全屏蔽。屏蔽不对称的根因是 agent 类型(是否走 DeepAgent 任务回路),与并发无关。
本文汇总问题现象、多轮代码追踪(双通道、生产者 RemoteA2aInterruptRail、屏蔽根因)与 DeepAgent 原理,
供后续提 issue 使用。
路径前缀:
RT/ = agent-runtime-java/(SUT 仓,acceptance 仓只做抓包/断言)
EXT/ = third_party/agent-solution/common/agent-runtime-ext-java/agent-service-adapters/agent-service-adapters-agentcore-ext/src/main/java/com/openjiuwen/service/adapters/agentcore/ext/(deepagent 专用扩展模块,被 agent-deep-research 应用引入)
CORE/ = RT/third_party/agent-core-java/src/main/java/
- 行号均为 2026-07-30/31 抓取快照。
1. 问题现象
三个抓包 run,分属三种 A→B 委派拓扑(均经 A2A_STREAM 协议):
| run |
拓扑(A→B) |
B / A 的实现 |
客户端能否检索到 __interaction__ / a2a_delegate |
run-20260730-212851 |
DA-09 deep-research → search-agent(3 路并发)+ verify-agent |
DeepAgent(agent-deep-research) |
✅ 既触发 3 次并发远端调用,又收到 controller_output 镜像(a2a_delegate 原样泄漏) |
run-20260730-175330 |
edpa-plan-agent → versatile-adapter |
plan-agent(versatile-orch-demo-plan-agent) |
❌ 完全屏蔽,只看到 Remote agent requires input 代理中断 |
run-20260730-165126 |
expense-review-main → expense-review-workflow |
agent-core WorkflowAgent |
✅ __interaction__ ↔ request_user_input 14/14 绑定(原生询问,与 a2a_delegate 无关) |
核心疑问(经多轮追踪后已收敛):第 1 行里 a2a_delegate 既被屏蔽(控制通道驱动了 3 次调用)又同时以 __interaction__
原样出现在内容流里;第 2 行里同样的 a2a_delegate 却被完全屏蔽。为什么?
2. 黑盒现象(抓包证据)
2.1 DA-09(deep-research:run-20260730-212851)——未屏蔽 + 3 并发调用
marker 统计(单文件 DA-09-A2A_STREAM-r1.log):
_remote_invocation 0 ← 见 §6.5 解释(不代表没发调用)
"Remote agent requires input" 0
INPUT_REQUIRED 0
statusUpdate 3 ← 仅终态,非中断代理
artifactUpdate 3873 ← 内容通道主导
__interaction__ 4
a2a_delegate 4
toolName: search-agent ×6, verify-agent ×2
agent-core 应用日志(同毫秒 3 行,注意:这只是日志 for 循环,并非并发执行,见 §6.4):
2026-07-30 21:28:58.511 [react-agent-stream-f7eb6a7e-...] [agent] INFO agent - Executing tool: search-agent with args: {"remoteInput": "DeepSeek V3 API 输入定价 每百万token价格"}
2026-07-30 21:28:58.511 [react-agent-stream-f7eb6a7e-...] [agent] INFO agent - Executing tool: search-agent with args: {"remoteInput": "Qwen-Max API 输入定价 每百万token价格"}
2026-07-30 21:28:58.511 [react-agent-stream-f7eb6a7e-...] [agent] INFO agent - Executing tool: search-agent with args: {"remoteInput": "Doubao-pro API 输入定价 每百万token价格"}
客户端收到的内容镜像(artifactUpdate,索引 828):
{"payload":{"metadata":{"task_id":"deep_agent_task_f7eb6a7e-..._1","stream_kind":"inner_agent"},
"data":[
{"data":{"payload":{"id":"call_00_...","value":{"toolCallId":"call_00_...","payloadSchema":{},
"context":{"_interrupt_kind":"a2a_delegate","agentName":"search-agent"},
"message":"DeepSeek V3 API 输入定价 每百万token价格","toolName":"search-agent"}},"type":"__interaction__"}},
{"data":{... call_01 ... "Qwen-Max ..." ...},"type":"__interaction__"},
{"data":{... call_02 ... "Doubao-pro ..." ...},"type":"__interaction__"}
],"type":"task_interaction"},"index":828.0,"type":"controller_output"}
要点:① 走 artifactUpdate(内容通道);② 3 条 a2a_delegate __interaction__ 被打包在一个
controller_output/task_interaction 产物里;③ 整轮以 ANSWER/COMPLETED 收尾,INPUT_REQUIRED=0。
2.2 edpa(plan-agent→versatile-adapter:run-20260730-175330)——屏蔽
_remote_invocation 14 文件
INPUT_REQUIRED 26 文件(39 处)
__interaction__ 0
a2a_delegate 0(客户端侧)
statusUpdate(控制通道)里的代理中断:
"statusUpdate":{"status":{"state":"TASK_STATE_INPUT_REQUIRED",
"message":{"parts":[{"text":"Remote agent requires input"}],
"metadata":{"_interrupt":{
"message":"Remote agent requires input",
"items":[{"toolCallId":"call_00_...","message":"Remote agent requires input","toolName":"versatile-adapter"}]}}}}}
要点:① 走 statusUpdate(控制通道);② _interrupt 是 items[] + toolName=versatile-adapter,
没有 type:"__interaction__"、没有 a2a_delegate;③ 该 toolCallId 与 _remote_invocation 一致——
原始 a2a_delegate 已被消费、替换成无标签代理。
2.3 WorkflowAgent 原生询问(run-20260730-165126)——对照
__interaction__ 14 文件、request_user_input 15 文件,同时出现 14/14,无一例外;toolName 38 处全是
request_user_input;_remote_invocation=0。即此处 __interaction__ 来自原生 ask_user(request_user_input
工具),与 a2a_delegate 无关,仅作"客户端本应能收到 __interaction__"的基线对照。
3. 核心结论(TL;DR)
- 双通道:同一个
a2a_delegate 事件,在 DeepAgent 里同时走两条通道——
- 控制通道(
TYPE_INTERRUPT):被 orchestrator 捕获 → isCoordinatorInterrupt → batchCoordinator →
并发调远端 → resume。被屏蔽(不下发客户端)。
- 内容通道(
TYPE_CHUNK / controller_output):被 orchestrator onNext 直接转发。未屏蔽(原样到客户端)。
- DeepAgent = ReActAgent + 任务回路外壳。DeepAgent 本身不推理;它按轮驱动一个内层
ReActAgent,并把它每一轮
的流额外收集、包装成 controller_output 内容镜像——这条镜像就是 a2a_delegate 泄漏的来源。
- 屏蔽不对称的根因是 agent 类型(是否走 DeepAgent 任务回路),与并发无关。edpa plan-agent 非 DeepAgent,只有
控制通道 → 完全屏蔽;DeepAgent 多发内容镜像 → 部分泄漏。并发只决定镜像里 data:[N] 的 N,不决定镜像是否存在。
对早前结论的纠正:一度认为"DA-09 的 a2a_delegate 不进协调器、纯走内容通道"。逐行追踪后确认:DA-09 的 3 次
HTTP 调用确实是 serve 层 RemoteInvocationBatchCoordinator → A2ARemoteAgentClient 发的(与 edpa 同一条协调器
路径);_remote_invocation=0 另有原因(§6.5)。真正的图景是"既进协调器(屏蔽、驱动调用)又泄漏内容镜像"。
4. DeepAgent 原理:为什么会有内容镜像
4.1 DeepAgent = ReActAgent + 任务回路外壳
DeepAgent 的内层 agent 字段就是 ReActAgent,构造时直接 new:
CORE/com/openjiuwen/harness/deep_agent/DeepAgent.java:79 private final ReActAgent agent;
DeepAgent.java:144-146
this.agent = new ReActAgent(this.card);
...
this.agent.configure(buildReActAgentConfig());
任务回路受开关控制,启用后把 CoreTaskLoopEventExecutor 注册为 DEEP_TASK_TYPE 的执行器,回调 invokeInnerRound:
DeepAgent.java:503-504 if (config.isEnableTaskLoop()) ensureTaskLoopRuntime();
DeepAgent.java:1062-1070
taskScheduler = new TaskScheduler(controllerConfig, taskManager, agent.getContextEngine(), ...);
...
taskScheduler.getTaskExecutorRegistry().addTaskExecutor(TaskLoopEventExecutor.DEEP_TASK_TYPE,
dependencies -> new CoreTaskLoopEventExecutor(dependencies, this, this::invokeInnerRound));
CoreTaskLoopEventExecutor 全仓只被 DeepAgent.java 引用——它是 DeepAgent 任务回路特有的执行器,不是通用机制。
4.2 每轮驱动内层 ReActAgent + 收集流
任务回路每一轮回调 invokeInnerRound,直接跑内层 ReActAgent(DeepAgent.java:1341-1357),分流式/同步两条:
- 同步
invokeInnerRoundOnce(DeepAgent.java:1368-1370):return agent.invoke(effectiveInputs, session);
- 流式
invokeInnerRoundStreaming(DeepAgent.java:1380-1401):
agent.stream(effectiveInputs, innerSession, List.of(StreamMode.OUTPUT))
.forEachRemaining(chunk -> {
streamItems.add(chunk);
if (chunk instanceof OutputSchema outputSchema) {
session.writeStream(outputSchema); // 内层输出直接写进流
}
});
...
result.put("stream_chunks", normalizeStreamChunks(streamItems)); // 收集成 stream_chunks
所以前面观察到的所有现象,全部来自这个内层 ReActAgent,DeepAgent 只在外面"按轮驱动 + 收集镜像":
| 现象 |
真正产出者 |
react-agent-stream-<sid> 线程 |
内层 ReActAgent 流线程(CORE/.../singleagent/agents/ReActAgent.java:813) |
Executing tool: search-agent ×3 |
内层 ReActAgent executeToolCallEntries(ReActAgent.java:475/483/703) |
__interaction__(a2a_delegate) |
内层 ReActAgent commitInterrupt(ReActAgent.java:919) |
controller_output 镜像 |
DeepAgent 任务回路把收集到的 stream_chunks 交给 CoreTaskLoopEventExecutor 包装 |
4.3 controller_output 镜像的产生
CoreTaskLoopEventExecutor 读内层结果的 stream_chunks,把每个 __interaction__ 包装成
controller_output/task_interaction 镜像(CORE/.../harness/task_loop/CoreTaskLoopEventExecutor.java):
processingChunks(:303-319):读 result 的 stream_chunks/streamChunks/chunks/inner_stream,逐项处理。
toProcessingChunk(:348-376):关键——
if (item instanceof OutputSchema outputSchema && "__interaction__".equals(outputSchema.getType())) {
return new ControllerOutputChunk(index,
new ControllerOutputPayload(TASK_INTERACTION,
List.of(new DataFrame.JsonDataFrame(
Map.of("type", outputSchema.getType(), "payload", outputSchema.getPayload()))),
Map.of("task_id", taskId, "stream_kind", "inner_agent")),
false);
}
executeOnce(:162-176):终态 chunk eventType = isInterruptResult(result) ? TASK_INTERACTION : TASK_COMPLETION,
即中断轮也会产出 task_interaction 镜像(与抓包一致)。
4.4 与 ReActAgent 单独跑的对比
ReActAgent 不产生 controller_output,也不走任务回路(grep ControllerOutput|TaskScheduler|CoreTaskLoopEventExecutor|task_loop
在 ReActAgent.java 零命中)。它直接把产出 writeStream 成顶层 OutputSchema(ReActAgent.java:919/1341/1366):
__interaction__ 作为顶层 OutputSchema 写出 → 适配层 normalizeChunk 看到顶层 __interaction__ → TYPE_INTERRUPT
(控制通道),不会被包成 controller_output。
结论:单独跑的 ReActAgent 没有外面那层 DeepAgent 任务回路去收集+包装它的流,所以只有控制通道一条,没有内容镜像。
同一份 ReActAgent 输出,被 DeepAgent 外壳额外镜像成一条 controller_output 内容流——这条流就是 a2a_delegate 泄漏的来源。
5. a2a_delegate 的生产者:RemoteA2aInterruptRail(EXT 模块)
a2a_delegate 与 _interrupt_kind 既不在 agent-core-java,也不在 agent-runtime-java——它们由第三个模块
agent-runtime-ext-java(制品 agent-service-adapters-agentcore-ext,被 agent-deep-research 引入)里的
RemoteA2aInterruptRail 硬编码产生。这就是在 SUT 仓里 grep 不到 a2a_delegate 的原因。
5.1 远端 agent 如何变成"工具"
search-agent/verify-agent 不是有执行体的 Tool,而是一张纯元数据 ToolCard(schema 只有 remoteInput),
执行被 rail 拦截——真正干活的是 rail + serve 层协调器。
注册链路(启动时一次性):
openjiuwen.service.a2a.remote-agents → RT/service/agent-service-app/.../config/A2AProperties.java:51 绑定。
ApplicationReadyEvent → RT/.../controller/a2a/client/A2AAgentCardDiscovery.java:87-97,142-146 抓取远端 agent card,
登进 A2ARemoteAgentCardRegistry。
EXT/autoconfigure/AgentCoreExtAutoConfiguration.java:21-25 建 RemoteA2aToolInstaller bean。
- deep-research 应用选用 handler
agentcore-ext:EXT/agentfw/JiuwenCoreAgentExtHandler.java:94-108,其
streamQuery/query 都在每次 run 前 installBeforeRun()(:110-115)→ remoteToolInstaller.install(getAgent())。
EXT/external/RemoteA2aToolInstaller.java:76-111(install):读 registry,每张卡建一个 RemoteA2aToolSpec
(toolName = remoteAgentName,即 search-agent;toSpec :113-126),全包进一个 RemoteA2aInterruptRail,
targetAgent.registerRail(rail)(:103)。目标经 DeepAgent.getAgent() 解析(:158-166)。
CORE/.../singleagent/BaseAgent.java:211 → AgentCallbackManager.registerRail(:117-122)→
AbilityManager.add(toolCard)(:107-110),把 search-agent/verify-agent 的 ToolCard 插进 AbilityManager,
listToolInfo() 暴露给 LLM。
5.2 a2a_delegate 的字面来源
EXT/external/RemoteA2aInterruptRail.java:44-60(resolveInterrupt),resumeInput == null 时:
InterruptRequest request = InterruptRequest.builder()
.message(message) // = tool call 的 remoteInput 参数(extractMessage :78-93)
.context(Map.of(
"agentName", spec.remoteAgentId(), // "search-agent"
"_interrupt_kind", "a2a_delegate")) // ← 字面来源
.build();
return interrupt(request);
spec.remoteAgentId() == toolName == "search-agent",与抓包镜像 {context:{_interrupt_kind:"a2a_delegate", agentName:"search-agent"}, toolName:"search-agent"} 完全吻合;payloadSchema:{} 也一致(rail 构造的 request 不设 payload schema)。
resume 时(:46-48)return reject(resumeInput);。
6. 完整调用链路(端到端)
6.1 工具执行 → 中断
内层 ReActAgent 拿到 LLM 的 3 个 search-agent tool call:
ReActAgent.executeToolCallEntries(:475/483,被 :703 正常轮、:664 resume 轮调用;:450 是零调用方的死代码)
→ AbilityManager.execute(:303),顺序逐个跑(:311 for (ToolCall singleToolCall : toolCalls),无并发)。
- 每个工具:
railedExecuteSingleToolCall(:323)→ RailExecutor BEFORE_TOOL_CALL →
BaseInterruptRail.beforeToolCall(:156-172)命中 search-agent → RemoteA2aInterruptRail.resolveInterrupt
抛出 a2a_delegate InterruptRequest。
BaseInterruptRail.applyDecision(:226-258)把它转成 throw new ToolInterruptException(..., toolCall)(:256)。
AbilityManager.execute 逐个 catch(:340-346),存为 ToolExecutionEntry(interruptException, null) 并 continue——
保证本轮所有 tool call 都跑到、各自产生中断。
ReActAgent.collectToolInterrupts(:939-961)汇总;commitInterrupt(:900-927)把每个中断包成一个
__interaction__ OutputSchema(:917-919 ToolCallInterruptRequest.fromToolCall),返回
{result_type:"interrupt", state:[OutputSchema...], interrupt_ids}。
6.2 双通道分叉(同一批 __interaction__ ×3 走两条路)
适配层 JiuwenCoreAgentHandler(RT/.../agentcore/agentfw/JiuwenCoreAgentHandler.java)按最外层 type 分流:
normalizeChunk(:569-581):顶层 __interaction__ → toInterruptData(:574,打 "interrupt detected" 日志);
controller_output → 通用 {type, index, payload} map(:576),内层 __interaction__ 不被拆看。
mapToQueryChunkType(:609-620):顶层 __interaction__ → TYPE_INTERRUPT(:613);controller_output → TYPE_CHUNK(:619)。
streamQuery(:223-243):TYPE_INTERRUPT 收进 interrupts 列表(:235-236)、不立即转发,流末合并成 1 条
normalizeInterrupts(:242);其余(TYPE_CHUNK)立即 observer.onNext(:238)。
|
通道 A:控制(捕获、屏蔽、驱动调用) |
通道 B:内容(镜像、透传、客户端可见) |
| 来源 |
顶层 __interaction__ → TYPE_INTERRUPT 合并成 {type:__interaction__, items:[3]} |
DeepAgent 任务回路把 __interaction__ 包成 controller_output/task_interaction data:[3] → TYPE_CHUNK |
orchestrator onNext |
:236-238 捕获(不转发) |
:240 直接转发(不解析内层) |
| 处理 |
handleInterrupt(:292-324)→ isCoordinatorInterrupt(:533-551)→ batchCoordinator.execute |
不进屏蔽判定 |
| 结果 |
3 次并发 HTTP + resume(见 §6.3) |
客户端收到那条 controller_output(索引 828) |
6.3 通道 A:3 次并发调用 + resume(serve 层)
A2AEnabledServeOrchestrator.handleInterrupt(:295-299):batchCoordinator.execute(rawInterrupt, current, observer).get()。
RemoteInvocationBatchCoordinator.execute(:152-169):parseBatch(:855-891,断言 _interrupt_kind=="a2a_delegate" :873)
→ for (Member member : batch.members) submit(...)(:165-167)——3 次紧凑 submit。
submit(:345-381)→ start(:400-425)→ client.callOutcome(call, …)(:425);client 是 serve 层
A2ARemoteAgentClient(:241),把真实 A2A JSON-RPC/流式 HTTP 提交到 ioExecutor(ThreadPoolExecutor :112-119,submit :265)。
- 全部 member 解析完 →
streamBatchResolution(:437-457)+ buildBatchResumeRequest(:506-521):结果塞进 metadata
runtime.remoteToolResults(:517,按 toolCallId 索引)+ runtime.remoteBatchId,构造 resume ServeRequest。
- 回灌:
JiuwenCoreAgentHandler.buildInputs(:393-414)读 runtime.remoteToolResults(:402)→ 建 InteractiveInput(:403-407)。
- 内层 resume:
ReActAgent resume 分支(:654-677)检测到 interruptionState,重跑被中断的 tool call(:663-664);
rail 再触发时 getUserInput(BaseInterruptRail:194-216)按 toolCallId 取回远端答案 → resolveInterrupt 收到
resumeInput != null → reject(resumeInput)(RemoteA2aInterruptRail:46-48)→ applyDecision(:241-252)
置 _skip_tool=true 并把远端答案塞进 toolResult,不再调任何工具实例。
- 全部 tool 完成、无新中断 →
collectToolInterrupts 返回 null → commitInterrupt 跳过 → 主循环继续 →
最终 result_type:"answer"(ReActAgent:722-726),故 INPUT_REQUIRED=0、以 ANSWER 收尾。
6.4 并发的真相:不在 ReActAgent,在 orchestrator 扇出
- agent-core 内
ReActAgent/AbilityManager 对工具是顺序执行(AbilityManager:311,无 CompletableFuture/线程池/虚线程)。
同毫秒 3 行 Executing tool: search-agent 只是 executeToolCallEntries(:481-487)在真正执行前的日志 for 循环连打 3 行。
- 真正的 3 路并发在 serve 层:
RemoteInvocationBatchCoordinator.execute 紧凑循环 submit 3 个 member →
A2ARemoteAgentClient.ioExecutor(ThreadPoolExecutor)在各自线程并发发 HTTP,受 maxConcurrency(默认 16,
A2AProperties:62)限制。
6.5 为什么 _remote_invocation=0 却确实发了调用
_remote_invocation 只在 ChunkMapper.toParts 处理 TYPE_REMOTE_AGENT_OUTPUT chunk 时打
(RT/.../controller/a2a/ChunkMapper.java:40-44)——它标记的是"被转发给客户端的远端产物投影"。DA-09 里 search-agent
的结果被当作 runtime.remoteToolResults 回灌进 agent 续跑了,从没作为内容 chunk 转发给客户端,所以该标记为 0;
但 3 次 HTTP 确实由 serve 层 A2ARemoteAgentClient 发出了。edpa 的 versatile-adapter 因 streaming: true
会向客户端转发远端流式产物,故 _remote_invocation=14。
7. 屏蔽不对称根因
屏蔽是"通道作用域"的,只作用于控制通道(TYPE_INTERRUPT);内容通道(TYPE_CHUNK)整体透传,不解析内部
__interaction__/_interrupt_kind。 在此之上,屏蔽不对称的真正分界线是 agent 类型(是否走 DeepAgent 任务回路):
- DeepAgent(deep-research):任务回路(
CoreTaskLoopEventExecutor)把内层 ReActAgent 每轮的流额外收集、包装成
controller_output 内容镜像 → 走内容通道 → 原始 a2a_delegate __interaction__ 原样到客户端。控制通道的中断仍被正常屏蔽、
驱动远端调用。→ 既屏蔽(控制)又泄漏(内容)。
- 非 DeepAgent(edpa plan-agent):无任务回路外壳,
a2a_delegate 只作为顶层 __interaction__ 走控制通道 → 完全屏蔽。→ 只屏蔽。
与并发无关:DeepAgent 里单个 a2a_delegate tool call 也会产出 controller_output 镜像(data:[1]);并发只让
data:[N] 的 N 变 3。edpa 即便并发,只要不是 DeepAgent 任务回路,也不会有这条内容镜像。
a2a_delegate 这个 kind 名在两种拓扑下含义其实不同(DeepAgent 是经 RemoteA2aInterruptRail 的远端委派;
其 rail 行为与 edpa 的 versatile-adapter 委派一致,但外壳不同),这也是不对称值得提 issue 的点之一。
8. 影响 / 是否缺陷(取决于契约口径)
口径 A —— "凡委派/输入需求都应以统一信封(如 __interaction__)上报,且不应向客户端泄漏内部委派细节"
则 DeepAgent 路径把 a2a_delegate 的 task_interaction 镜像泄漏给客户端,是生产者侧(agent-core-java 任务回路 /
agent-runtime-ext-java)的可观测性缺陷:应在发布 task_interaction/controller_output 产物前,对其中
_interrupt_kind=a2a_delegate 的 item 做脱敏/屏蔽,使 DeepAgent 与非 DeepAgent 对齐。
口径 B —— "a2a_delegate(委派)与 ask_user(原生询问)语义不同,控制通道行为正确"
则当前控制通道屏蔽是对的,但存在可观测性不对称:DeepAgent 泄漏委派镜像、非 DeepAgent 不泄漏。需明确客户端应不应当
看到 a2a_delegate 委派细节:若不应,DeepAgent 侧脱敏;若应,则非 DeepAgent 路径也应补发同样镜像。
口径 C —— 抓包/断言层(acceptance 仓)
同一"输入需求/委派"语义在不同拓扑下形状不一(controller_output 嵌套 __interaction__ vs items[] 代理 vs 原生
request_user_input),断言易误判。建议在 acceptance 侧抽一个跨拓扑统一的判定。
9. 建议方向(供 issue 讨论)
- 先定契约:客户端是否应看到
a2a_delegate 的委派/镜像细节?DeepAgent 的 controller_output 内容镜像是否属于
"应脱敏的内部可观测性"?
- 若需脱敏(推荐,口径 A):在 agent-core-java
CoreTaskLoopEventExecutor 发布 task_interaction/controller_output
前,或 DeepAgent.normalizeStreamChunks 收集前,对 _interrupt_kind=a2a_delegate 的 item 做屏蔽,使 DeepAgent 路径
不再泄漏委派细节。修改点集中在 third_party/agent-core-java + agent-runtime-ext-java。
- 若需统一上报:在
A2AEnabledServeOrchestrator.buildInterruptQueryResponse / RemoteInvocationBatchCoordinator
合成代理中断处补 type:"__interaction__",使非 DeepAgent 路径与 DeepAgent/WorkflowAgent 对齐。
- acceptance 侧:在
VersatileEventMapping 增加对 items[]/_interrupt/controller_output 嵌套 __interaction__
形状的 INTERACTION 识别,或抽跨拓扑统一判定,消除断言的拓扑相关歧义。
附录 A:DeepAgent 结构速查
DeepAgent (任务回路: TaskScheduler + CoreTaskLoopEventExecutor,enableTaskLoop 时按轮调度)
│ 每轮回调 invokeInnerRound
▼
ReActAgent (this.agent = new ReActAgent(card)) ← 真正的推理 + 工具执行
│ agent.stream(...) 产出 OutputSchema 流(llm_output / llm_reasoning / __interaction__ / answer)
▼
DeepAgent 收集该流 → stream_chunks → CoreTaskLoopEventExecutor 包装成 controller_output 镜像(内容通道)
同时 __interaction__ 作为顶层中断上浮(控制通道)
- DeepAgent 还能递归 spawn 子 DeepAgent(
instantiateConfiguredSubagent :992-996,
HarnessFactory.createDeepAgent),每个子 DeepAgent 内层又是一个 ReActAgent。但 DA-09 的 search-agent 是经
RemoteA2aInterruptRail 的远端 agent,不是本地子 DeepAgent。
附录 B:关键证据定位
- DA-09 新 run:
target/sit-logs/wire/run-20260730-212851/DA-09-A2A_STREAM-r1.log
(artifactUpdate + controller_output/task_interaction data:[3],索引 828;_remote_invocation=0)
- edpa 屏蔽 run:
target/sit-logs/wire/run-20260730-175330/balanceThen-A2A_STREAM-r7.log
(statusUpdate + _interrupt.items[],toolCallId 与 _remote_invocation 一致)
- WorkflowAgent 对照 run:
target/sit-logs/wire/run-20260730-165126/*
(__interaction__ ↔ request_user_input 14/14 绑定)
- 配置:
src/test/resources/application-openjiuwen.yml(:124-126 deep-research / :132-136 remote-agents /
:175-177 edpa-plan-agent)
- SUT 代码(
agent-runtime-java):
service/agent-service-app/.../orchestrator/A2AEnabledServeOrchestrator.java
(onNext 235-241、handleInterrupt 292-324、isCoordinatorInterrupt 533-551、streamBatchResolution 437-457、
buildBatchResumeRequest 506-521)
service/agent-service-app/.../orchestrator/RemoteInvocationBatchCoordinator.java(152-169 / 345-381 / 400-425 / 855-891)
service/agent-service-app/.../controller/a2a/client/A2ARemoteAgentClient.java(112-119 / 241 / 265)
service/agent-service-app/.../controller/a2a/ChunkMapper.java(40-44,_remote_invocation 触发条件)
service/agent-service-adapters/.../agentcore/agentfw/JiuwenCoreAgentHandler.java
(streamQuery 223-243、normalizeChunk 569-581、mapToQueryChunkType 609-620、buildInputs 393-414)
- agent-core 代码(
third_party/agent-core-java):
harness/deep_agent/DeepAgent.java(79 / 144-146 / 503-504 / 1062-1070 / 1341-1401 / 992-996)
harness/task_loop/CoreTaskLoopEventExecutor.java(162-176 / 303-319 / 348-376)
core/singleagent/agents/ReActAgent.java(475 / 483 / 703 / 813 / 900-927 / 939-961 / 654-677 / 722-726)
core/singleagent/AbilityManager.java(107-110 / 303 / 311 / 340-346)
core/singleagent/BaseAgent.java(211)、AgentCallbackManager.java(117-122)
harness/rails/interrupt/BaseInterruptRail.java(156-172 / 194-216 / 226-258)
- EXT 模块(
third_party/agent-solution/common/agent-runtime-ext-java/.../ext/):
external/RemoteA2aInterruptRail.java(44-60,a2a_delegate 字面来源)
external/RemoteA2aToolInstaller.java(76-111 / 113-126 / 158-166)
agentfw/JiuwenCoreAgentExtHandler.java(94-115)
autoconfigure/AgentCoreExtAutoConfiguration.java(21-25)
DeepAgent 对
a2a_delegate的「控制 / 内容双通道」——内容镜像泄漏与屏蔽不对称1. 问题现象
三个抓包 run,分属三种 A→B 委派拓扑(均经
A2A_STREAM协议):__interaction__/a2a_delegaterun-20260730-212851agent-deep-research)controller_output镜像(a2a_delegate 原样泄漏)run-20260730-175330versatile-orch-demo-plan-agent)Remote agent requires input代理中断run-20260730-165126WorkflowAgent__interaction__↔request_user_input14/14 绑定(原生询问,与 a2a_delegate 无关)核心疑问(经多轮追踪后已收敛):第 1 行里
a2a_delegate既被屏蔽(控制通道驱动了 3 次调用)又同时以__interaction__原样出现在内容流里;第 2 行里同样的
a2a_delegate却被完全屏蔽。为什么?2. 黑盒现象(抓包证据)
2.1 DA-09(deep-research:
run-20260730-212851)——未屏蔽 + 3 并发调用marker 统计(单文件
DA-09-A2A_STREAM-r1.log):agent-core 应用日志(同毫秒 3 行,注意:这只是日志 for 循环,并非并发执行,见 §6.4):
客户端收到的内容镜像(
artifactUpdate,索引 828):{"payload":{"metadata":{"task_id":"deep_agent_task_f7eb6a7e-..._1","stream_kind":"inner_agent"}, "data":[ {"data":{"payload":{"id":"call_00_...","value":{"toolCallId":"call_00_...","payloadSchema":{}, "context":{"_interrupt_kind":"a2a_delegate","agentName":"search-agent"}, "message":"DeepSeek V3 API 输入定价 每百万token价格","toolName":"search-agent"}},"type":"__interaction__"}}, {"data":{... call_01 ... "Qwen-Max ..." ...},"type":"__interaction__"}, {"data":{... call_02 ... "Doubao-pro ..." ...},"type":"__interaction__"} ],"type":"task_interaction"},"index":828.0,"type":"controller_output"}要点:① 走
artifactUpdate(内容通道);② 3 条a2a_delegate__interaction__被打包在一个controller_output/task_interaction产物里;③ 整轮以ANSWER/COMPLETED 收尾,INPUT_REQUIRED=0。2.2 edpa(plan-agent→versatile-adapter:
run-20260730-175330)——屏蔽statusUpdate(控制通道)里的代理中断:要点:① 走
statusUpdate(控制通道);②_interrupt是items[]+toolName=versatile-adapter,没有
type:"__interaction__"、没有a2a_delegate;③ 该toolCallId与_remote_invocation一致——原始
a2a_delegate已被消费、替换成无标签代理。2.3 WorkflowAgent 原生询问(
run-20260730-165126)——对照__interaction__14 文件、request_user_input15 文件,同时出现 14/14,无一例外;toolName38 处全是request_user_input;_remote_invocation=0。即此处__interaction__来自原生ask_user(request_user_input工具),与
a2a_delegate无关,仅作"客户端本应能收到__interaction__"的基线对照。3. 核心结论(TL;DR)
a2a_delegate事件,在 DeepAgent 里同时走两条通道——TYPE_INTERRUPT):被 orchestrator 捕获 →isCoordinatorInterrupt→batchCoordinator→并发调远端 → resume。被屏蔽(不下发客户端)。
TYPE_CHUNK/controller_output):被 orchestratoronNext直接转发。未屏蔽(原样到客户端)。ReActAgent,并把它每一轮的流额外收集、包装成
controller_output内容镜像——这条镜像就是 a2a_delegate 泄漏的来源。控制通道 → 完全屏蔽;DeepAgent 多发内容镜像 → 部分泄漏。并发只决定镜像里
data:[N]的 N,不决定镜像是否存在。4. DeepAgent 原理:为什么会有内容镜像
4.1 DeepAgent = ReActAgent + 任务回路外壳
DeepAgent 的内层 agent 字段就是
ReActAgent,构造时直接 new:CORE/com/openjiuwen/harness/deep_agent/DeepAgent.java:79private final ReActAgent agent;DeepAgent.java:144-146任务回路受开关控制,启用后把
CoreTaskLoopEventExecutor注册为DEEP_TASK_TYPE的执行器,回调invokeInnerRound:DeepAgent.java:503-504if (config.isEnableTaskLoop()) ensureTaskLoopRuntime();DeepAgent.java:1062-10704.2 每轮驱动内层 ReActAgent + 收集流
任务回路每一轮回调
invokeInnerRound,直接跑内层 ReActAgent(DeepAgent.java:1341-1357),分流式/同步两条:invokeInnerRoundOnce(DeepAgent.java:1368-1370):return agent.invoke(effectiveInputs, session);invokeInnerRoundStreaming(DeepAgent.java:1380-1401):所以前面观察到的所有现象,全部来自这个内层 ReActAgent,DeepAgent 只在外面"按轮驱动 + 收集镜像":
react-agent-stream-<sid>线程CORE/.../singleagent/agents/ReActAgent.java:813)Executing tool: search-agent×3executeToolCallEntries(ReActAgent.java:475/483/703)__interaction__(a2a_delegate)commitInterrupt(ReActAgent.java:919)controller_output镜像stream_chunks交给CoreTaskLoopEventExecutor包装4.3 controller_output 镜像的产生
CoreTaskLoopEventExecutor读内层结果的stream_chunks,把每个__interaction__包装成controller_output/task_interaction镜像(CORE/.../harness/task_loop/CoreTaskLoopEventExecutor.java):processingChunks(:303-319):读result的stream_chunks/streamChunks/chunks/inner_stream,逐项处理。toProcessingChunk(:348-376):关键——executeOnce(:162-176):终态 chunkeventType = isInterruptResult(result) ? TASK_INTERACTION : TASK_COMPLETION,即中断轮也会产出
task_interaction镜像(与抓包一致)。4.4 与 ReActAgent 单独跑的对比
ReActAgent 不产生 controller_output,也不走任务回路(grep
ControllerOutput|TaskScheduler|CoreTaskLoopEventExecutor|task_loop在
ReActAgent.java零命中)。它直接把产出writeStream成顶层OutputSchema(ReActAgent.java:919/1341/1366):__interaction__作为顶层 OutputSchema 写出 → 适配层normalizeChunk看到顶层__interaction__→TYPE_INTERRUPT(控制通道),不会被包成 controller_output。
5.
a2a_delegate的生产者:RemoteA2aInterruptRail(EXT 模块)a2a_delegate与_interrupt_kind既不在agent-core-java,也不在agent-runtime-java——它们由第三个模块agent-runtime-ext-java(制品agent-service-adapters-agentcore-ext,被agent-deep-research引入)里的RemoteA2aInterruptRail硬编码产生。这就是在 SUT 仓里 grep 不到a2a_delegate的原因。5.1 远端 agent 如何变成"工具"
search-agent/verify-agent不是有执行体的 Tool,而是一张纯元数据ToolCard(schema 只有remoteInput),执行被 rail 拦截——真正干活的是 rail + serve 层协调器。
注册链路(启动时一次性):
openjiuwen.service.a2a.remote-agents→RT/service/agent-service-app/.../config/A2AProperties.java:51绑定。ApplicationReadyEvent→RT/.../controller/a2a/client/A2AAgentCardDiscovery.java:87-97,142-146抓取远端 agent card,登进
A2ARemoteAgentCardRegistry。EXT/autoconfigure/AgentCoreExtAutoConfiguration.java:21-25建RemoteA2aToolInstallerbean。agentcore-ext:EXT/agentfw/JiuwenCoreAgentExtHandler.java:94-108,其streamQuery/query都在每次 run 前installBeforeRun()(:110-115)→remoteToolInstaller.install(getAgent())。EXT/external/RemoteA2aToolInstaller.java:76-111(install):读 registry,每张卡建一个RemoteA2aToolSpec(
toolName = remoteAgentName,即search-agent;toSpec:113-126),全包进一个RemoteA2aInterruptRail,targetAgent.registerRail(rail)(:103)。目标经DeepAgent.getAgent()解析(:158-166)。CORE/.../singleagent/BaseAgent.java:211→AgentCallbackManager.registerRail(:117-122)→AbilityManager.add(toolCard)(:107-110),把search-agent/verify-agent的ToolCard插进AbilityManager,listToolInfo()暴露给 LLM。5.2
a2a_delegate的字面来源EXT/external/RemoteA2aInterruptRail.java:44-60(resolveInterrupt),resumeInput == null时:spec.remoteAgentId()==toolName=="search-agent",与抓包镜像{context:{_interrupt_kind:"a2a_delegate", agentName:"search-agent"}, toolName:"search-agent"}完全吻合;payloadSchema:{}也一致(rail 构造的 request 不设 payload schema)。resume 时(:46-48)
return reject(resumeInput);。6. 完整调用链路(端到端)
6.1 工具执行 → 中断
内层 ReActAgent 拿到 LLM 的 3 个
search-agenttool call:ReActAgent.executeToolCallEntries(:475/483,被:703正常轮、:664resume 轮调用;:450是零调用方的死代码)→
AbilityManager.execute(:303),顺序逐个跑(:311for (ToolCall singleToolCall : toolCalls),无并发)。railedExecuteSingleToolCall(:323)→RailExecutorBEFORE_TOOL_CALL →BaseInterruptRail.beforeToolCall(:156-172)命中search-agent→RemoteA2aInterruptRail.resolveInterrupt抛出
a2a_delegateInterruptRequest。BaseInterruptRail.applyDecision(:226-258)把它转成throw new ToolInterruptException(..., toolCall)(:256)。AbilityManager.execute逐个 catch(:340-346),存为ToolExecutionEntry(interruptException, null)并continue——保证本轮所有 tool call 都跑到、各自产生中断。
ReActAgent.collectToolInterrupts(:939-961)汇总;commitInterrupt(:900-927)把每个中断包成一个__interaction__OutputSchema(:917-919ToolCallInterruptRequest.fromToolCall),返回{result_type:"interrupt", state:[OutputSchema...], interrupt_ids}。6.2 双通道分叉(同一批
__interaction__×3 走两条路)适配层
JiuwenCoreAgentHandler(RT/.../agentcore/agentfw/JiuwenCoreAgentHandler.java)按最外层 type 分流:normalizeChunk(:569-581):顶层__interaction__→toInterruptData(:574,打 "interrupt detected" 日志);controller_output→ 通用{type, index, payload}map(:576),内层__interaction__不被拆看。mapToQueryChunkType(:609-620):顶层__interaction__→TYPE_INTERRUPT(:613);controller_output→TYPE_CHUNK(:619)。streamQuery(:223-243):TYPE_INTERRUPT收进interrupts列表(:235-236)、不立即转发,流末合并成 1 条normalizeInterrupts(:242);其余(TYPE_CHUNK)立即observer.onNext(:238)。__interaction__→TYPE_INTERRUPT合并成{type:__interaction__, items:[3]}__interaction__包成controller_output/task_interactiondata:[3]→TYPE_CHUNKonNext:236-238捕获(不转发):240直接转发(不解析内层)handleInterrupt(:292-324)→isCoordinatorInterrupt(:533-551)→batchCoordinator.executecontroller_output(索引 828)6.3 通道 A:3 次并发调用 + resume(serve 层)
A2AEnabledServeOrchestrator.handleInterrupt(:295-299):batchCoordinator.execute(rawInterrupt, current, observer).get()。RemoteInvocationBatchCoordinator.execute(:152-169):parseBatch(:855-891,断言_interrupt_kind=="a2a_delegate":873)→
for (Member member : batch.members) submit(...)(:165-167)——3 次紧凑submit。submit(:345-381)→start(:400-425)→client.callOutcome(call, …)(:425);client是 serve 层A2ARemoteAgentClient(:241),把真实 A2A JSON-RPC/流式 HTTP 提交到ioExecutor(ThreadPoolExecutor:112-119,submit :265)。streamBatchResolution(:437-457)+buildBatchResumeRequest(:506-521):结果塞进 metadataruntime.remoteToolResults(:517,按toolCallId索引)+runtime.remoteBatchId,构造 resumeServeRequest。JiuwenCoreAgentHandler.buildInputs(:393-414)读runtime.remoteToolResults(:402)→ 建InteractiveInput(:403-407)。ReActAgentresume 分支(:654-677)检测到interruptionState,重跑被中断的 tool call(:663-664);rail 再触发时
getUserInput(BaseInterruptRail:194-216)按toolCallId取回远端答案 →resolveInterrupt收到resumeInput != null→reject(resumeInput)(RemoteA2aInterruptRail:46-48)→applyDecision(:241-252)置
_skip_tool=true并把远端答案塞进toolResult,不再调任何工具实例。collectToolInterrupts返回 null →commitInterrupt跳过 → 主循环继续 →最终
result_type:"answer"(ReActAgent:722-726),故INPUT_REQUIRED=0、以ANSWER收尾。6.4 并发的真相:不在 ReActAgent,在 orchestrator 扇出
ReActAgent/AbilityManager对工具是顺序执行(AbilityManager:311,无CompletableFuture/线程池/虚线程)。同毫秒 3 行
Executing tool: search-agent只是executeToolCallEntries(:481-487)在真正执行前的日志 for 循环连打 3 行。RemoteInvocationBatchCoordinator.execute紧凑循环submit3 个 member →A2ARemoteAgentClient.ioExecutor(ThreadPoolExecutor)在各自线程并发发 HTTP,受maxConcurrency(默认 16,A2AProperties:62)限制。6.5 为什么
_remote_invocation=0却确实发了调用_remote_invocation只在ChunkMapper.toParts处理TYPE_REMOTE_AGENT_OUTPUTchunk 时打(
RT/.../controller/a2a/ChunkMapper.java:40-44)——它标记的是"被转发给客户端的远端产物投影"。DA-09 里 search-agent的结果被当作
runtime.remoteToolResults回灌进 agent 续跑了,从没作为内容 chunk 转发给客户端,所以该标记为 0;但 3 次 HTTP 确实由 serve 层
A2ARemoteAgentClient发出了。edpa 的 versatile-adapter 因streaming: true会向客户端转发远端流式产物,故
_remote_invocation=14。7. 屏蔽不对称根因
屏蔽是"通道作用域"的,只作用于控制通道(
TYPE_INTERRUPT);内容通道(TYPE_CHUNK)整体透传,不解析内部__interaction__/_interrupt_kind。 在此之上,屏蔽不对称的真正分界线是 agent 类型(是否走 DeepAgent 任务回路):CoreTaskLoopEventExecutor)把内层 ReActAgent 每轮的流额外收集、包装成controller_output内容镜像 → 走内容通道 → 原始a2a_delegate__interaction__原样到客户端。控制通道的中断仍被正常屏蔽、驱动远端调用。→ 既屏蔽(控制)又泄漏(内容)。
a2a_delegate只作为顶层__interaction__走控制通道 → 完全屏蔽。→ 只屏蔽。a2a_delegate这个 kind 名在两种拓扑下含义其实不同(DeepAgent 是经RemoteA2aInterruptRail的远端委派;其 rail 行为与 edpa 的 versatile-adapter 委派一致,但外壳不同),这也是不对称值得提 issue 的点之一。
8. 影响 / 是否缺陷(取决于契约口径)
口径 A —— "凡委派/输入需求都应以统一信封(如
__interaction__)上报,且不应向客户端泄漏内部委派细节"则 DeepAgent 路径把
a2a_delegate的task_interaction镜像泄漏给客户端,是生产者侧(agent-core-java 任务回路 /agent-runtime-ext-java)的可观测性缺陷:应在发布
task_interaction/controller_output产物前,对其中_interrupt_kind=a2a_delegate的 item 做脱敏/屏蔽,使 DeepAgent 与非 DeepAgent 对齐。口径 B —— "
a2a_delegate(委派)与ask_user(原生询问)语义不同,控制通道行为正确"则当前控制通道屏蔽是对的,但存在可观测性不对称:DeepAgent 泄漏委派镜像、非 DeepAgent 不泄漏。需明确客户端应不应当
看到
a2a_delegate委派细节:若不应,DeepAgent 侧脱敏;若应,则非 DeepAgent 路径也应补发同样镜像。口径 C —— 抓包/断言层(acceptance 仓)
同一"输入需求/委派"语义在不同拓扑下形状不一(
controller_output嵌套__interaction__vsitems[]代理 vs 原生request_user_input),断言易误判。建议在 acceptance 侧抽一个跨拓扑统一的判定。9. 建议方向(供 issue 讨论)
a2a_delegate的委派/镜像细节?DeepAgent 的controller_output内容镜像是否属于"应脱敏的内部可观测性"?
CoreTaskLoopEventExecutor发布task_interaction/controller_output前,或
DeepAgent.normalizeStreamChunks收集前,对_interrupt_kind=a2a_delegate的 item 做屏蔽,使 DeepAgent 路径不再泄漏委派细节。修改点集中在
third_party/agent-core-java+agent-runtime-ext-java。A2AEnabledServeOrchestrator.buildInterruptQueryResponse/RemoteInvocationBatchCoordinator合成代理中断处补
type:"__interaction__",使非 DeepAgent 路径与 DeepAgent/WorkflowAgent 对齐。VersatileEventMapping增加对items[]/_interrupt/controller_output嵌套__interaction__形状的 INTERACTION 识别,或抽跨拓扑统一判定,消除断言的拓扑相关歧义。
附录 A:DeepAgent 结构速查
instantiateConfiguredSubagent:992-996,HarnessFactory.createDeepAgent),每个子 DeepAgent 内层又是一个 ReActAgent。但 DA-09 的 search-agent 是经RemoteA2aInterruptRail的远端 agent,不是本地子 DeepAgent。附录 B:关键证据定位
target/sit-logs/wire/run-20260730-212851/DA-09-A2A_STREAM-r1.log(
artifactUpdate+controller_output/task_interactiondata:[3],索引 828;_remote_invocation=0)target/sit-logs/wire/run-20260730-175330/balanceThen-A2A_STREAM-r7.log(
statusUpdate+_interrupt.items[],toolCallId与_remote_invocation一致)target/sit-logs/wire/run-20260730-165126/*(
__interaction__↔request_user_input14/14 绑定)src/test/resources/application-openjiuwen.yml(:124-126 deep-research / :132-136 remote-agents /:175-177 edpa-plan-agent)
agent-runtime-java):service/agent-service-app/.../orchestrator/A2AEnabledServeOrchestrator.java(
onNext235-241、handleInterrupt292-324、isCoordinatorInterrupt533-551、streamBatchResolution437-457、buildBatchResumeRequest506-521)service/agent-service-app/.../orchestrator/RemoteInvocationBatchCoordinator.java(152-169 / 345-381 / 400-425 / 855-891)service/agent-service-app/.../controller/a2a/client/A2ARemoteAgentClient.java(112-119 / 241 / 265)service/agent-service-app/.../controller/a2a/ChunkMapper.java(40-44,_remote_invocation触发条件)service/agent-service-adapters/.../agentcore/agentfw/JiuwenCoreAgentHandler.java(
streamQuery223-243、normalizeChunk569-581、mapToQueryChunkType609-620、buildInputs393-414)third_party/agent-core-java):harness/deep_agent/DeepAgent.java(79 / 144-146 / 503-504 / 1062-1070 / 1341-1401 / 992-996)harness/task_loop/CoreTaskLoopEventExecutor.java(162-176 / 303-319 / 348-376)core/singleagent/agents/ReActAgent.java(475 / 483 / 703 / 813 / 900-927 / 939-961 / 654-677 / 722-726)core/singleagent/AbilityManager.java(107-110 / 303 / 311 / 340-346)core/singleagent/BaseAgent.java(211)、AgentCallbackManager.java(117-122)harness/rails/interrupt/BaseInterruptRail.java(156-172 / 194-216 / 226-258)third_party/agent-solution/common/agent-runtime-ext-java/.../ext/):external/RemoteA2aInterruptRail.java(44-60,a2a_delegate字面来源)external/RemoteA2aToolInstaller.java(76-111 / 113-126 / 158-166)agentfw/JiuwenCoreAgentExtHandler.java(94-115)autoconfigure/AgentCoreExtAutoConfiguration.java(21-25)