Skip to content

[Bug]:AgentScope Java A2A Streaming 重复输出问题反馈 #1378

@lzbjut

Description

@lzbjut

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 都表现为:

lastChunk=false

即使 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,实际输出:

欢迎欢迎 lizhen06,请选择区域?

期望输出:

欢迎 lizhen06,请选择区域?

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 只放新增片段。

客户端可以稳定执行:

append artifact text

Option B: 输出 snapshot,并明确 replace 语义

如果 artifact text 是当前累计快照,则不要让客户端误解成 delta。

可选方式:

  • 不使用 append=true 表达 snapshot;
  • 或在 metadata 中标识 streamMode=snapshot
  • 或官方文档明确要求 client 对同一 artifactId 执行 replace。

Option C: 正确映射 Event.isLast()TaskArtifactUpdateEvent.lastChunk

当前实现固定:

lastChunk=false

建议至少在可判断时映射:

lastChunk=output.isLast()

如果由于 Reactor FluxdoOnNext 当下无法确认全局最后一个事件,可以做一帧 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 的边界。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    Projects

    Status

    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions