AgentScope Java A2A Streaming 重复输出问题反馈
Summary
在使用 AgentScope Java 1.0.12-SNAPSHOT 的 A2A server 作为远程 subagent,并由 A2A client 通过 stream 模式消费输出时,客户端可能收到重复文本。
核心问题不是单纯的 UI append 错误,而是 AgentScope A2A server adapter 在把内部 Event 转成 A2A TaskArtifactUpdateEvent 时,丢失或混淆了以下语义:
- 当前输出是 token/delta,还是当前累计快照 snapshot;
- 当前 artifact 是否是最后一个 chunk;
status-update final=true 中的完整 message 是否应被客户端再次渲染。
这会导致标准客户端如果按常见 streaming 语义处理:
artifact-update => append content
status-update final=true => complete
就很容易把同一段内容重复渲染。
Environment
- AgentScope Java:
1.0.12-SNAPSHOT
- A2A Java SDK:
0.3.3.Final
- Server side: AgentScope A2A server extension
- Client side: A2A streaming client consuming
TaskArtifactUpdateEvent / TaskStatusUpdateEvent
Expected Streaming Semantics
理想情况下,streaming 输出应该明确属于以下一种:
Delta 模式
artifact-update: "你"
artifact-update: "好"
artifact-update: "啊"
status-update final=true
客户端行为:append 每个 delta。
Snapshot 模式
artifact-update: "你"
artifact-update: "你好"
artifact-update: "你好啊"
status-update final=true
客户端行为:replace 同一个 artifact 的内容,或只渲染相对上一帧新增的 tail。
Final Message 模式
artifact-update: streaming chunks...
status-update final=true, message="完整最终答案"
客户端行为:如果已经渲染过 streaming artifact,final message 应只作为最终状态或兜底,不应再次 append。
Actual Behavior Observed
实际收到的事件可能类似:
artifact-update #1
append=false
lastChunk=false
artifactId=agent-response-id
text="👋 欢迎"
artifact-update #2
append=true
lastChunk=false
artifactId=same-agent-response-id
text="👋 欢迎 lizhen06!为了开始查询,请先告诉我..."
status-update
final=true
message="👋 欢迎 lizhen06!为了开始查询,请先告诉我..."
如果客户端把每个 artifact-update 都当作 delta append,就会得到:
👋 欢迎👋 欢迎 lizhen06!为了开始查询,请先告诉我...
如果客户端随后又 append status-update final=true 中的完整 message,则会进一步重复。
Root Cause Analysis
1. AgentScope 内部 Event 本身有 isLast
源码位置:
io.agentscope.core.agent.Event
agentscope-core/1.0.12-SNAPSHOT
关键字段:
private final boolean isLast;
该字段文档语义是:
true: complete message or final chunk;
false: intermediate chunk。
说明 AgentScope 内部事件模型本来有“当前事件是否为最终 chunk / 完整消息”的信息。
2. A2A server adapter 没有把 Event.isLast() 映射给 A2A lastChunk
源码位置:
io.agentscope.core.a2a.server.executor.AgentScopeAgentExecutor
agentscope-extensions-a2a-server/1.0.12-SNAPSHOT
关键方法:
StreamingFluxEventHandler#handleEvent(Event output)
当前实现核心逻辑:
taskUpdater.addArtifact(..., !isFirstArtifact.getAndSet(false), false);
其中:
- 倒数第二个参数是
append;
- 最后一个参数是
lastChunk;
lastChunk 被固定传成 false;
- 没有使用
output.isLast()。
这会导致所有 artifact-update 都表现为:
即使 AgentScope 内部已经知道某个 Event 是最后 chunk。
3. append=true 只表示“不是第一帧”,没有表达 delta / snapshot 语义
同一个方法中,append 的值来自:
!isFirstArtifact.getAndSet(false)
这意味着:
- 第一帧:
append=false
- 后续所有帧:
append=true
但这只能说明“是否第一帧”,不能说明 artifact.parts.text 是:
- 新增 delta;
- 还是同一个 artifact 当前累计快照。
实际观察中,后续 append=true 的 artifact 有时携带的是累计快照:
previous="👋 欢迎"
current ="👋 欢迎 lizhen06!为了开始查询..."
这时如果客户端按 delta append,就会重复。
4. TaskStatusUpdateEvent final=true 可能再次携带完整 message
源码位置:
AgentScopeAgentExecutor.StreamingFluxEventHandler#doOnComplete()
当前逻辑会在 complete 时构造 completeMessage,并调用:
taskUpdater.complete(completeMessage);
如果 completeMessage 携带完整最终答案,客户端又把 status message 当作普通输出 append,就会和前面的 artifact 内容重复。
这本身不是一定错误,因为 final message 可以作为完整结果兜底;但需要明确客户端语义:
- 如果前面已有 artifact 输出,final message 不应再次 append;
- 如果前面没有 artifact 输出,final message 可以作为兜底输出。
Why This Is Problematic
当前 A2A stream 中混合了三种语义:
1. delta: 新增片段
2. snapshot: 当前累计内容
3. final answer: 完整最终答案
但事件字段没有清楚地区分它们:
append=true 容易被理解为 delta append;
lastChunk=false 永远无法帮助判断最终 artifact;
status-update final=true 又可能带完整 message。
结果是 client 很难靠协议字段做可靠判断,只能做防御性兼容。
Minimal Reproduction
模拟 A2A SSE 返回:
artifact-update append=false lastChunk=false text="欢迎"
artifact-update append=true lastChunk=false text="欢迎 lizhen06,请选择区域?"
status-update final=true
如果 client 直接 append 每个 artifact text,实际输出:
期望输出:
Client-Side Workaround
我们在 client 侧做了兼容逻辑,按 artifactId 保存上一帧文本:
if (append == true && current.startsWith(previous)) {
delta = current.substring(previous.length());
}
含义:
- 如果后续 artifact text 是上一帧的前缀扩展,则认为它是累计快照,只输出新增 tail;
- 如果不是前缀扩展,则认为它是真 delta,原样 append;
status-update final=true 只作为完成信号;如果前面已经输出过 artifact,不再 append final message。
这能兼容 delta 和 snapshot 两种服务端输出,但属于 client 侧防御,并不是根治。
Suggested Fixes
建议 AgentScope A2A server adapter 明确 streaming artifact 语义。
Option A: 输出纯 delta
如果 TaskArtifactUpdateEvent.append=true,则 artifact.parts.text 只放新增片段。
客户端可以稳定执行:
Option B: 输出 snapshot,并明确 replace 语义
如果 artifact text 是当前累计快照,则不要让客户端误解成 delta。
可选方式:
- 不使用
append=true 表达 snapshot;
- 或在 metadata 中标识
streamMode=snapshot;
- 或官方文档明确要求 client 对同一
artifactId 执行 replace。
Option C: 正确映射 Event.isLast() 到 TaskArtifactUpdateEvent.lastChunk
当前实现固定:
建议至少在可判断时映射:
lastChunk=output.isLast()
如果由于 Reactor Flux 的 doOnNext 当下无法确认全局最后一个事件,可以做一帧 buffer:
收到 event1:缓存
收到 event2:发送 event1(lastChunk=false),缓存 event2
onComplete:发送 event2(lastChunk=true)
Option D: final message 仅作为完成状态或可配置兜底
当 streaming artifacts 已经完整输出时,status-update final=true 中的 message 不应被客户端再次当作增量内容渲染。
可以考虑:
completeWithMessage=false 作为 streaming 默认值;
- 或在文档中明确 final message 是完整结果,不应 append 到 artifact stream。
Code Locations To Review
AgentScope Java
io.agentscope.core.a2a.server.executor.AgentScopeAgentExecutor
重点:
StreamingFluxEventHandler#handleEvent(Event output)
问题点:
taskUpdater.addArtifact(..., !isFirstArtifact.getAndSet(false), false);
这里 lastChunk 固定 false,且没有使用 output.isLast()。
AgentScope Java
io.agentscope.core.a2a.server.executor.AgentScopeAgentExecutor
重点:
StreamingFluxEventHandler#doOnComplete()
问题点:
taskUpdater.complete(completeMessage);
如果 complete message 是完整答案,客户端需要明确知道它是 final answer,不应和前面的 streaming artifact 混合 append。
A2A SDK
io.a2a.spec.TaskArtifactUpdateEvent
字段:
append
lastChunk
artifact
这些字段本身可以表达流式更新,但当前 AgentScope adapter 没有充分使用 lastChunk,也没有明确 delta/snapshot 语义。
Conclusion
这个问题的根因在于 AgentScope Java A2A server adapter 将内部 stream event 转换为 A2A artifact-update 时,丢失了关键语义:
Event.isLast() 没有映射到 lastChunk;
append=true 仅表示“非首帧”,不能说明内容是 delta;
- artifact-update 中可能出现累计快照;
- final status message 可能再次携带完整答案。
因此,client 侧只能通过 artifactId + previous snapshot 做兼容去重。更合理的长期修复应在 AgentScope A2A server bridge 中明确 delta/snapshot/final 的边界。
AgentScope Java A2A Streaming 重复输出问题反馈
Summary
在使用 AgentScope Java
1.0.12-SNAPSHOT的 A2A server 作为远程 subagent,并由 A2A client 通过 stream 模式消费输出时,客户端可能收到重复文本。核心问题不是单纯的 UI append 错误,而是 AgentScope A2A server adapter 在把内部
Event转成 A2ATaskArtifactUpdateEvent时,丢失或混淆了以下语义:status-update final=true中的完整 message 是否应被客户端再次渲染。这会导致标准客户端如果按常见 streaming 语义处理:
就很容易把同一段内容重复渲染。
Environment
1.0.12-SNAPSHOT0.3.3.FinalTaskArtifactUpdateEvent/TaskStatusUpdateEventExpected Streaming Semantics
理想情况下,streaming 输出应该明确属于以下一种:
Delta 模式
客户端行为:append 每个 delta。
Snapshot 模式
客户端行为:replace 同一个 artifact 的内容,或只渲染相对上一帧新增的 tail。
Final Message 模式
客户端行为:如果已经渲染过 streaming artifact,final message 应只作为最终状态或兜底,不应再次 append。
Actual Behavior Observed
实际收到的事件可能类似:
如果客户端把每个
artifact-update都当作 delta append,就会得到:如果客户端随后又 append
status-update final=true中的完整 message,则会进一步重复。Root Cause Analysis
1. AgentScope 内部
Event本身有isLast源码位置:
关键字段:
该字段文档语义是:
true: complete message or final chunk;false: intermediate chunk。说明 AgentScope 内部事件模型本来有“当前事件是否为最终 chunk / 完整消息”的信息。
2. A2A server adapter 没有把
Event.isLast()映射给 A2AlastChunk源码位置:
关键方法:
当前实现核心逻辑:
其中:
append;lastChunk;lastChunk被固定传成false;output.isLast()。这会导致所有 artifact-update 都表现为:
即使 AgentScope 内部已经知道某个
Event是最后 chunk。3.
append=true只表示“不是第一帧”,没有表达 delta / snapshot 语义同一个方法中,
append的值来自:这意味着:
append=falseappend=true但这只能说明“是否第一帧”,不能说明
artifact.parts.text是:实际观察中,后续
append=true的 artifact 有时携带的是累计快照:这时如果客户端按 delta append,就会重复。
4.
TaskStatusUpdateEvent final=true可能再次携带完整 message源码位置:
当前逻辑会在 complete 时构造
completeMessage,并调用:如果
completeMessage携带完整最终答案,客户端又把 status message 当作普通输出 append,就会和前面的 artifact 内容重复。这本身不是一定错误,因为 final message 可以作为完整结果兜底;但需要明确客户端语义:
Why This Is Problematic
当前 A2A stream 中混合了三种语义:
但事件字段没有清楚地区分它们:
append=true容易被理解为 delta append;lastChunk=false永远无法帮助判断最终 artifact;status-update final=true又可能带完整 message。结果是 client 很难靠协议字段做可靠判断,只能做防御性兼容。
Minimal Reproduction
模拟 A2A SSE 返回:
如果 client 直接 append 每个 artifact text,实际输出:
期望输出:
Client-Side Workaround
我们在 client 侧做了兼容逻辑,按
artifactId保存上一帧文本:含义:
status-update final=true只作为完成信号;如果前面已经输出过 artifact,不再 append final message。这能兼容 delta 和 snapshot 两种服务端输出,但属于 client 侧防御,并不是根治。
Suggested Fixes
建议 AgentScope A2A server adapter 明确 streaming artifact 语义。
Option A: 输出纯 delta
如果
TaskArtifactUpdateEvent.append=true,则artifact.parts.text只放新增片段。客户端可以稳定执行:
Option B: 输出 snapshot,并明确 replace 语义
如果 artifact text 是当前累计快照,则不要让客户端误解成 delta。
可选方式:
append=true表达 snapshot;streamMode=snapshot;artifactId执行 replace。Option C: 正确映射
Event.isLast()到TaskArtifactUpdateEvent.lastChunk当前实现固定:
建议至少在可判断时映射:
如果由于 Reactor
Flux的doOnNext当下无法确认全局最后一个事件,可以做一帧 buffer:Option D: final message 仅作为完成状态或可配置兜底
当 streaming artifacts 已经完整输出时,
status-update final=true中的 message 不应被客户端再次当作增量内容渲染。可以考虑:
completeWithMessage=false作为 streaming 默认值;Code Locations To Review
AgentScope Java
重点:
问题点:
这里
lastChunk固定 false,且没有使用output.isLast()。AgentScope Java
重点:
问题点:
如果 complete message 是完整答案,客户端需要明确知道它是 final answer,不应和前面的 streaming artifact 混合 append。
A2A SDK
字段:
这些字段本身可以表达流式更新,但当前 AgentScope adapter 没有充分使用
lastChunk,也没有明确 delta/snapshot 语义。Conclusion
这个问题的根因在于 AgentScope Java A2A server adapter 将内部 stream event 转换为 A2A artifact-update 时,丢失了关键语义:
Event.isLast()没有映射到lastChunk;append=true仅表示“非首帧”,不能说明内容是 delta;因此,client 侧只能通过
artifactId + previous snapshot做兼容去重。更合理的长期修复应在 AgentScope A2A server bridge 中明确 delta/snapshot/final 的边界。