Skip to content

[BUG]: 流式中断标记退化:SSE 丢弃 QueryChunk.type 信封,导致中断只能靠 items/message 弱推断 #96

Description

流式中断标记退化:SSE 丢弃 QueryChunk.type 信封,导致中断只能靠 items/message 弱推断

一句话

REST 流式(SSE)场景下,QueryChunk.type"interrupt")这个本可一锤定音的中断判别器,在序列化时被 QuerySseSupport.payload() 丢弃,客户端被迫退回"有 message/items 就当中断"的启发式推断;而仓内已有测试断言业务输出也可能带 items,因此该启发式已知不可靠。非流式 JSON 不受影响(result._interrupt 是可靠标记)。

背景与影响范围

  • 受影响接口:REST POST /v1/queryPOST /v1/query/reactive(及兼容路径 /query),仅 stream=true(SSE)。
  • 不受影响stream=false(非流式 JSON,靠 result._interrupt 区分,可靠);A2A JSON-RPC(走独立封装,不在本缺陷范围)。
  • 严重度:高。前端无法在流式下可靠区分"中断(需用户输入)"与"普通中间/最终文本块",会误判中断为应答(提前关闭流)或误判应答为中断(卡住等输入)。

问题详述

1. QueryChunk 设计了显式 type 判别器,但 SSE 没把它发出去

QueryChunk 在内存里有明确的 type 字段区分中断与普通块:

// service/agent-service-spec/.../dto/QueryChunk.java:16-23
public static final String TYPE_INTERRUPT = "interrupt";
public static final String TYPE_CHUNK = "chunk";
public static final String TYPE_REMOTE_AGENT_OUTPUT = "remote_agent_output";
public static final String TYPE_ERROR = "error";
private String type = TYPE_CHUNK;
private Object data;

但 SSE 序列化层只取 data、丢弃 type 信封:

// service/agent-service-app/.../controller/query/QuerySseSupport.java:30-37
public static Object payload(QueryChunk chunk) {
    if (chunk.getData() != null) {
        return chunk.getData();              // ← 只返回 data,type 信封被丢
    }
    Map<String, Object> fallback = new LinkedHashMap<>();
    fallback.put("type", chunk.getType());   // ← 仅 data==null 时回退,中断块 data 永远非空
    return fallback;
}

MVC 与 WebFlux 控制器又都只 .data(...)、不设 SSE 事件名:

// QueryMvcController.java:142
emitter.send(SseEmitter.event().data(QuerySseSupport.toSseData(chunk, objectMapper)));
// QueryWebFluxController.java:125
sink.next(ServerSentEvent.builder(QuerySseSupport.toSseData(chunk, objectMapper)).build());

结果:SSE 线格式 = 纯 data 的 JSON,既无 type 信封,也无 event: 名称。

2. 客户端实际看到的中断数据形状(无统一 type 标记)

中断来源 SSE 上发出的 JSON 内部有 type 构造点
单个本地中断(ask_user) {type:"__interaction__", index, payload, message, toolCallId, ...} __interaction__ JiuwenCoreAgentHandler.toInterruptDataJiuwenCoreAgentHandler.java:635-646
批量本地中断 {message:"interaction batch", items:[...]} ❌ 无 JiuwenCoreAgentHandler.normalizeInterruptsJiuwenCoreAgentHandler.java:416-428
远端 a2a_delegate 批量中断 {message:..., items:[{toolCallId, toolName, message}]} ❌ 无 RemoteInvocationBatchMapper.publicInterruptRemoteInvocationBatchMapper.java:243-260
普通中间/最终块 {type:"answer"/"chunk"/..., payload} 或纯字符串 ✅ 其他值 JiuwenCoreAgentHandler.streamQuery 透传

远端批量中断的发出点明确带 TYPE_INTERRUPT 信封,但被 payload() 丢掉:

// A2AEnabledServeOrchestrator.java:454
observer.onNext(new QueryChunk(QueryChunk.TYPE_INTERRUPT, resolution.interrupt()));

3. 弱推断已被本仓测试否决

由于批量/远端中断在 SSE data 内部无 type,客户端只能退回"有 items 就当中断""有 message 且无 type 就当中断"这类启发式。但仓内已有测试断言业务输出也可能带 items

矛盾点:服务端自己已否决"items 推断",但 SSE 线格式却把客户端逼回这个被否决的启发式,因为唯一可靠的判别器 QueryChunk.type="interrupt" 被序列化层丢了。

4. 对比:非流式 JSON 是可靠的

非流式响应靠 result Map 中的 _interrupt key 做判别器,所有中断路径都置、普通应答从不置;服务端自己也用它判别:

// A2AEnabledServeOrchestrator.java:429-435
if (response.getResult() instanceof Map<?, ?> m && m.get("_interrupt") instanceof Map<?, ?> interrupt) {
    return (Map<String, Object>) interrupt;
}

普通应答只写 role/content,不写 _interruptJiuwenCoreAgentHandler.java:366-371)。故 result._interrupt != null 100% 可靠。本缺陷仅限流式 SSE。

复现/证据清单

影响

  1. 前端误判:流式下无法可靠区分中断与普通块。批量本地中断、远端 a2a_delegate 中断在 SSE data 内部无 type,与带 items 的业务输出形状重叠。
  2. 行为不一致:同一中断语义,非流式有 result._interrupt 强标记、流式却退化为弱推断,客户端需写两套解析逻辑。
  3. 被本仓测试自相矛盾:服务端已认定"items 推断不可靠",SSE 线格式却使客户端别无选择。

修复方向

最小修复集中在 QuerySseSupport,让 SSE 报文携带 QueryChunk.type 信封。二选一:

  1. 信封包裹(推荐):把 SSE data 从纯 payload 改为 {type, data},例如中断块发 {"type":"interrupt","data":{message,items}},普通块发 {"type":"chunk","data":{...}}。客户端只需判顶层 type
  2. SSE 事件名:给 SseEmitter.event().name(chunk.getType()) / ServerSentEvent.builder(...).event(chunk.getType())event: 字段,客户端按事件名分发。

注意:方案 1 会改变 SSE data 形状,是线格式变更,需评估对既有客户端的兼容性;方案 2 不动 data、只加事件名,兼容性更好。两种方案都应同时给批量/远端中断在 data 内部补一个稳定的 type 标记(与非流式 _interrupt 语义对齐),消除"单中断有 __interaction__、批量/远端无"的内部不一致。

关联

  • 姊妹问题(并发 conversation_id 拼装 + _remote_invocation 单层):见主 issue(本仓 #​71)。
  • 背景文档:documents/zh/deepagent-a2a-delegate-dual-channel-leak-defect.cn.md

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions