Checklist
🐞 问题详细描述
FAILED Task 缺少稳定错误码,Core 结构化错误在 Runtime/A2A 映射中丢失
结论摘要
当前 Runtime 能在部分执行失败路径中形成 TASK_STATE_FAILED,并把失败文本放入:
Task.status.message.parts[0].text
但该文本主要来自 Throwable#getMessage()、QueryChunk 业务文本或固定兜底文案,属于面向人的描述,不是稳定的程序化契约。调用方 Runtime 收到 FAILED Task 后,仅根据 TaskState 统一推导出 REMOTE_BUSINESS_FAILURE,无法区分模型超时、模型限流、配置错误、工具失败、业务拒绝或 Runtime 内部异常。
Core 已有 BaseError、StatusCode、数值 code、状态名和结构化 toMap()。RunnerImpl 也会在异常链中保留 BaseError。但是 Runtime A2AAgentExecutor.failAndDrain() 构造 A2A TaskStatus.message 时只读取 error.getMessage(),Core 的稳定 code/status 在 Core -> Runtime -> A2A 边界丢失。
服务入口能力范围设计 已明确要求 FAILED callback 返回“错误码和失败原因”、failed Task 携带可供客户端程序化判断的信息,并在可形成 Task 的异常路径携带结构化错误 payload。当前代码只满足“失败状态 + 可读文本”,未满足稳定错误码和结构化错误契约。
本 Issue 与 FAILED Task callback 可能丢失 是两个独立问题:
- callback 丢失 Issue:解决 FAILED 通知是否发出。
- 本 Issue:解决通知已经发出时,上游能否稳定识别具体失败原因。
1. 场景
1.1 特性与模块
- 特性:
服务入口与 Push Notification
- Core:
BaseError
StatusCode
RunnerImpl
- Runtime:
A2AAgentExecutor
HttpPushNotificationSender
A2ARemoteAgentClient
RemoteInvocationBatchMapper
- 使用方:
multi-deep-research-demo
- Deep Research Runtime 调用 Search Runtime
- Search Task FAILED 后通过 streaming、blocking 或 callback 返回失败
- Deep Research Runtime 需要决定重试、降级、向父 Agent 返回工具错误或终止任务
1.2 设计要求来源
服务入口能力范围设计:
- 第 2 章 callback 触发范围:
FAILED 返回异常状态、错误码和失败原因。
- 第 5.1.4 节:
FAILED 必须通知异常状态、错误码和失败原因。
- 第 5.1.7 节:failed Task 必须携带可供客户端程序化判断的错误信息。
- 第 5.1.8 节:可形成 Task 的 handler/runtime exception 应携带结构化错误 payload。
详细设计 当前列出了 JSON-RPC request error code,但没有定义异步 FAILED Task 中稳定业务/执行错误码的具体字段、命名空间、兼容和透传规则。
1.3 验证基线
验证日期:2026-08-08
agent-core-java: 730 @ e0482cd0224b76c6422189a3eb1cf13fe7b52601
agent-runtime-java: develop @ acd12ce7259f66e424a8ee3e9348a6a8fb081004
agent-solution: common @ 55226a1d8d4ecd9c76d22e28bf241b7e04c033d6
2. 问题概述
2.1 FAILED text 不是稳定错误契约
当前 FAILED text 可能来自:
| 路径 |
text 来源 |
稳定性问题 |
| 非流式执行抛异常 |
Throwable#getMessage() |
文案受异常包装、依赖版本和实现细节影响 |
Streaming onError |
Throwable#getMessage() |
同上,且可能暴露底层连接信息 |
QueryChunk.TYPE_ERROR |
business text、data["error"]、字符串或固定兜底 |
结构任意,生产者之间没有统一 code 契约 |
Core 普通 RuntimeException |
Runner 包装成 Failed to invoke agent / Failed to stream agent |
底层具体原因可能只保留在 cause 和日志中 |
Core BaseError |
BaseError#getMessage() |
code/status 仍存在于异常对象,但 Runtime 未写入 A2A Task |
SDK InternalError -> FAILED |
当前 Task snapshot 可能只有 FAILED state |
还叠加 callback 丢失及失败详情丢失问题,见关联 Issue |
因此,上游不能依赖 text 做如下判断:
- 是否可重试;
- 是否需要换模型或降级;
- 是否是用户输入/业务拒绝;
- 是否是限流、超时、服务不可达;
- 是否应终止父 Task;
- 是否需要告警到模型、工具、Runtime 或业务 Agent 的不同责任方。
2.2 上游只有粗分类
A2ARemoteAgentClient.resultCategory(TaskState) 和 RemoteInvocationBatchMapper.resultCategory(TaskState) 当前固定映射:
TASK_STATE_FAILED -> REMOTE_BUSINESS_FAILURE
这个分类只说明“远端 Task 失败”,不说明“为什么失败”。例如以下失败都会得到同一个结果:
MODEL_CALL_FAILED
MODEL_SERVICE_CONFIG_ERROR
AGENT_TOOL_EXECUTION_ERROR
REMOTE_AGENT_EXECUTION_TIMEOUT
业务校验拒绝
未知 Runtime 异常
2.3 当前 callback 示例
通过生产 HttpPushNotificationSender.callbackBody() 序列化的 FAILED Task 结构如下:
{
"jsonrpc": "2.0",
"notificationId": "notification-failed-example",
"result": {
"task": {
"id": "task-failed-example",
"contextId": "ctx-failed-example",
"status": {
"state": "TASK_STATE_FAILED",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Agent streaming execution failed"
}
],
"messageId": "<generated-message-id>"
},
"timestamp": "<timestamp>"
},
"artifacts": [],
"history": []
}
}
}
payload 中没有独立稳定错误码、错误域、可重试标志或来源错误码。
3. 问题分析
3.1 集成验证与现有测试执行结果
执行现有 Runtime 测试:
mvn -pl service/agent-service-app `
'-Dtest=A2AAgentExecutorTest#failurePathEmitsFailedStatusWithBusinessError,RemoteAgentAnswerExtractorTest#remoteStatesMapToStableResultCategories' `
test
结果:
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
测试一 failurePathEmitsFailedStatusWithBusinessError:
IllegalStateException("remote boom")
-> TASK_STATE_FAILED
-> status.message.parts[0].text == "remote boom"
断言只检查 text,没有 code/metadata 断言。
测试二 remoteStatesMapToStableResultCategories:
TASK_STATE_FAILED -> REMOTE_BUSINESS_FAILURE
该测试确认所有远端 FAILED 当前只映射到同一粗分类。
3.2 对照组分析
| 对照组 |
生产端已有信息 |
A2A FAILED Task |
上游最终可见信息 |
Core BaseError(MODEL_CALL_FAILED) |
status name + numeric code + message + details |
只有 message text |
REMOTE_BUSINESS_FAILURE + text |
普通 RuntimeException |
exception type + message + cause |
只有包装后的 message text |
REMOTE_BUSINESS_FAILURE + text |
| Runtime 本地调用超时 |
Runtime 可识别 timeout |
不一定形成远端 FAILED callback |
本地 mapper 可生成 REMOTE_TIMEOUT |
| 已收到远端 FAILED callback |
Task state + status text |
没有具体 code |
固定 REMOTE_BUSINESS_FAILURE |
这说明 Runtime 对“本地观察到的 transport/queue 异常”已有一定分类能力,例如 REMOTE_TIMEOUT、REMOTE_OVERLOADED、REMOTE_RATE_LIMITED、REMOTE_PROTOCOL_ERROR 和 REMOTE_UNAVAILABLE;但“远端 Agent 自己报告的 FAILED”只剩 REMOTE_BUSINESS_FAILURE,没有远端具体错误码。
3.3 为什么不能解析 text 作为补偿
不应通过包含关系或正则解析 text,原因包括:
- Core、Runtime、模型 SDK 和业务 Agent 都可能修改异常文案。
- 同类错误可能出现不同语言、不同 provider 文案或不同 cause 包装。
Failed to invoke agent 等泛化文案不包含实际原因。
- 底层异常 message 可能包含 URL、文件路径、请求信息或其他敏感内容。
- 文案面向人,错误码面向程序,两者生命周期和兼容要求不同。
4. 复现条件
4.1 最小源码级复现
在 A2AAgentExecutorTest 中让 ServeOrchestrator.query() 抛出:
new IllegalStateException("remote boom")
观察生成的 TaskStatusUpdateEvent:
state = TASK_STATE_FAILED
message.parts[0].text = remote boom
message.metadata = null / 无错误结构
随后将该 Task 交给:
RemoteInvocationBatchMapper.callbackOutcome(task)
结果:
remoteState = TASK_STATE_FAILED
resultCategory = REMOTE_BUSINESS_FAILURE
result = remote boom
4.2 Core BaseError 对照
Core 已支持:
BaseError error = ...;
error.getCode();
error.getStatus().name();
error.toMap();
RunnerImpl.wrapRunnerRuntime() 会遍历 cause 链并返回原始 BaseError。将该异常传到 Runtime 后,A2AAgentExecutor.failAndDrain() 仍只调用 getMessage(),最终 callback 中看不到 getCode() 和 getStatus()。
4.3 Demo 观察方式
- 让 Search Agent 的模型调用产生一个有明确类别的失败,例如模型连接失败、超时或受控
BaseError。
- Deep Research Runtime 以 callback 模式调用 Search Runtime。
- 确认 Search Task 进入
TASK_STATE_FAILED 并成功发送 callback。
- 检查 callback body:当前只有
status.message.parts[].text。
- 检查 Deep Research Runtime 恢复结果:当前 code 为
REMOTE_BUSINESS_FAILURE,无法获得 Search 侧具体错误码。
注意:若进入 InternalError -> FAILED callback 丢失路径,应先按关联 Issue 处理通知丢失,否则本 Issue 的 payload 检查无法进行。
5. 问题代码分析
5.1 Core 已有稳定错误结构
文件:
agent-core-java/src/main/java/com/openjiuwen/core/common/exception/BaseError.java
BaseError 保存:
StatusCode status
int code
Map<String, Object> params
Object details
boolean recoverable
boolean fatal
toMap() 输出:
map.put("code", code);
map.put("status", status.name());
map.put("message", templateMessage);
map.put("params", params);
map.put("raw_message", message);
map.put("details", details);
现有 StatusCode 示例:
REMOTE_AGENT_EXECUTION_TIMEOUT = 110100
REMOTE_AGENT_EXECUTION_ERROR = 110101
AGENT_TOOL_EXECUTION_ERROR = 120001
MODEL_CALL_FAILED = 181001
5.2 Runner 会保留 BaseError
文件:
agent-core-java/src/main/java/com/openjiuwen/core/runner/RunnerImpl.java:935-958
private RuntimeException wrapRunnerRuntime(String message, RuntimeException error) {
BaseError baseError = findBaseError(error);
if (baseError != null) {
return baseError;
}
return new RuntimeException(message, error);
}
因此 Core -> Runner 边界没有必然丢失 BaseError code;主要丢失发生在 Runtime -> A2A Task 映射。
5.3 Runtime 只保留 Throwable#getMessage()
文件:
agent-runtime-java/service/agent-service-app/src/main/java/
com/openjiuwen/service/app/controller/a2a/A2AAgentExecutor.java:316-320
String errorMessage = error.getMessage() == null ? "Agent execution failed" : error.getMessage();
Message message = Message.builder()
.role(Message.Role.ROLE_AGENT)
.parts(List.of(new TextPart(errorMessage)))
.build();
emitter.fail(message);
这里没有读取:
BaseError#getCode()
BaseError#getStatus()
BaseError#isRecoverable()
- 其他 Runtime 可识别的异常类别
5.4 TYPE_ERROR 仍只有非结构化文本
streamChunkFailure() 按 business text、map["error"]、字符串和固定兜底选择 message,最后包装成 IllegalStateException。即使 QueryChunk.data 原来带有结构化错误字段,当前也只提取文本。
5.5 Sender 和 receiver 没有主动丢字段
HttpPushNotificationSender.callbackBody() 直接序列化完整 Task。A2aPushNotificationCallbackController 也会把 result.task 反序列化为 SDK Task。
因此,如果错误结构放在 SDK 支持的 Message.metadata 或其他约定字段中,sender/receiver 原则上可以随 Task 透传。当前问题主要是生产端没有写入、消费端没有读取。
5.6 上游消费端再次降级为粗分类
A2ARemoteAgentClient:
status.message.parts -> statusText
TASK_STATE_FAILED -> REMOTE_BUSINESS_FAILURE
RemoteInvocationBatchMapper.callbackOutcome() 同样只提取 status text,并根据 TaskState 计算 resultCategory。
最后父 Agent 得到的工具错误类似:
{
"ok": false,
"code": "REMOTE_BUSINESS_FAILURE",
"message": "remote boom",
"remoteAgentId": "search-agent"
}
没有远端具体错误码。
6. 问题影响
- Deep Research Agent 无法按具体失败类型决定重试、降级、切换模型或终止。
- 不同责任域的故障全部表现为
REMOTE_BUSINESS_FAILURE,告警和问题定位不准确。
- 调用方只能解析不稳定 text,形成隐式且脆弱的跨 Agent 契约。
- Core 已建立的错误码体系无法贯穿 Runtime 和 A2A callback,跨仓能力未真正集成。
- 直接返回底层 exception message 存在暴露内部实现、URL、路径或敏感上下文的风险。
- 即使修复 FAILED callback 丢失,上游仍无法满足异步失败契约的“程序化判断”要求。
7. 建议修复方案
方案 A:在 TaskStatus.message.metadata 定义 namespaced 错误结构(推荐)
不增加 callback 顶层私有字段,保持 callback 继续承载标准 A2A Task;在 FAILED status message 的 metadata 中增加版本化扩展:
{
"state": "TASK_STATE_FAILED",
"message": {
"role": "ROLE_AGENT",
"parts": [
{
"text": "Model invocation failed"
}
],
"metadata": {
"openjiuwen.error": {
"schemaVersion": "1",
"code": "MODEL_CALL_FAILED",
"numericCode": 181001,
"category": "MODEL",
"retryable": false,
"origin": "CORE"
}
}
}
}
约束:
text:安全、可读、允许调整的描述。
code:稳定的程序化错误码,版本升级不得随文案变化。
numericCode:复用 Core code,可选但建议保留。
category:错误域,例如 MODEL、TOOL、AGENT、RUNTIME、REMOTE。
retryable:由明确策略生成,不能只按异常类名猜测。
origin:错误最初产生层,用于责任定位。
- 不得把堆栈、密钥、完整请求、内部 URL 等敏感内容放入 callback。
优点:
- 使用 A2A Message 已有 metadata 扩展点。
- sender 和 receiver 可以继续原样序列化 Task。
- 旧客户端忽略 metadata 仍能读取 text。
- 新客户端可读取稳定结构。
方案 B:定义统一 A2aFailureMapper / FailureDescriptor,避免散落映射
在 Runtime 定义统一映射组件:
Throwable / QueryChunk error
-> FailureDescriptor
code
numericCode
category
publicMessage
retryable
origin
-> TaskStatus.message
默认映射至少覆盖:
- Core
BaseError:保留 status name 和 numeric code。
- Runtime 明确异常:映射为 Runtime 自有稳定 code。
- 业务 Agent 主动失败:允许业务通过受控接口提供 code 和 public message。
- 未识别异常:回退到
AGENT_EXECUTION_FAILED 或等价通用 code,不暴露原始底层 message。
QueryChunk.TYPE_ERROR:优先读取结构化 failure descriptor,旧格式仅作为 text fallback。
该方案应与方案 A 配合,方案 A 定义传输结构,方案 B 统一生产规则。
方案 C:消费端保留“粗分类 + 远端具体错误”两级语义
为了兼容现有 resultCategory,不建议直接删除 REMOTE_BUSINESS_FAILURE。父 Agent 工具结果可调整为:
{
"ok": false,
"code": "REMOTE_BUSINESS_FAILURE",
"message": "Model invocation failed",
"remoteAgentId": "search-agent",
"remoteError": {
"code": "MODEL_CALL_FAILED",
"numericCode": 181001,
"category": "MODEL",
"retryable": false
}
}
其中:
code=REMOTE_BUSINESS_FAILURE 保留现有调用方粗分类兼容性。
remoteError.code 保留下游具体失败原因。
- 本地 transport timeout 等仍使用
REMOTE_TIMEOUT、REMOTE_UNAVAILABLE 等本地分类,不伪装成下游业务错误。
A2ARemoteAgentClient 和 RemoteInvocationBatchMapper.callbackOutcome() 应复用同一个错误提取器,保证 streaming、blocking、callback 三种入口得到一致结果。
方案 D:在 详细设计 中补齐错误契约和兼容规则
详细设计 至少需要明确:
- FAILED Task 稳定错误结构的 JSON path。
- code 命名、数值 code 与错误域的关系。
- Core、Runtime、业务 Agent 分别负责生成哪些 code。
- text/public message 的脱敏规则。
- 未携带新 metadata 的旧 Agent 如何 fallback。
- callback、streaming status event、blocking Task 和
GetTask 必须返回一致错误结构。
- 上游是否重试必须基于显式字段,而不是解析 text。
8. 建议验收标准
- Core
BaseError(MODEL_CALL_FAILED) 经 Runtime 转换后,FAILED Task 保留稳定 code/status。
- FAILED callback body 同时包含 state、可读 text 和结构化 error metadata。
GetTask、blocking、streaming 和 callback 对同一失败返回一致 error descriptor。
- Deep Research Runtime 收到 Search FAILED callback 后,能够同时获得:
REMOTE_BUSINESS_FAILURE 粗分类;
- Search 侧具体
remoteError.code。
- 未携带新 metadata 的旧 Agent 继续回退为
REMOTE_BUSINESS_FAILURE + text,不破坏兼容性。
- 不同 text 但相同 code 的错误被上游识别为同一类型。
- 相同 text 但不同 code 的错误不会被误判为同一类型。
- callback payload 不包含堆栈、密钥、完整内部 URL 或其他敏感 details。
InternalError -> FAILED 路径修复后也必须使用同一 error descriptor,不得再次形成无 message/code 的 FAILED Task。
9. 建议自动化回归覆盖
建议在正式自动化测试中覆盖:
- Search 抛 Core
BaseError(MODEL_CALL_FAILED),Deep 收到具体 code。
- Search 模型超时,Deep 能读取 retryable/timeout 类 code。
- Search 业务主动拒绝,code 与模型/transport 错误不同。
- Search 抛未知异常,callback 使用脱敏通用 code,不暴露内部 cause。
- 同一错误分别通过 streaming、blocking、callback、
GetTask 返回,结构一致。
- 老版本 Search 只返回 text 时,Deep 正常 fallback。
- 多级 callback:Search 失败 -> Deep 恢复 -> Deep 最终 FAILED callback,错误链不丢失且不重复覆盖。
10. 关联 Issue
failed-task-push-notification-may-be-lost.md
- 解决
InternalError 和 callback config 绑定竞态导致的 FAILED callback 丢失。
- 其中“保留 FAILED 错误码和失败原因”建议由本 Issue 进一步定义具体契约。
callback-url-trust-documentation-mismatch.md
- 处理 callback URL 信任策略的文档与代码差异,与本 Issue 无直接实现依赖。
11. 证据索引
agent-core-java/src/main/java/com/openjiuwen/core/common/exception/
BaseError.java:30,57,172,203,213
StatusCode.java:183,221,223,560
agent-core-java/src/main/java/com/openjiuwen/core/runner/
RunnerImpl.java:855,879,935-958
agent-runtime-java/service/agent-service-app/src/main/java/com/openjiuwen/service/app/
controller/a2a/A2AAgentExecutor.java:110-112,143-168,193-205,316-320
controller/a2a/HttpPushNotificationSender.java:145-150
controller/a2a/client/A2ARemoteAgentClient.java:352-399,453-463
orchestrator/RemoteInvocationBatchMapper.java:109-164,262-287
agent-runtime-java/service/agent-service-app/src/test/java/com/openjiuwen/service/app/
controller/a2a/A2AAgentExecutorTest.java:190-249
controller/a2a/client/RemoteAgentAnswerExtractorTest.java:114-122,240-256
orchestrator/RemoteInvocationBatchMapperTest.java:69-82,102-118,169-172
服务入口能力范围设计:
相关章节:45,119,143,153
详细的环境信息描述
agent-runtime-java: develop
其他辅助信息
版本信息
感谢您的贡献 🎉!
Checklist
🐞 问题详细描述
FAILED Task 缺少稳定错误码,Core 结构化错误在 Runtime/A2A 映射中丢失
结论摘要
当前 Runtime 能在部分执行失败路径中形成
TASK_STATE_FAILED,并把失败文本放入:但该文本主要来自
Throwable#getMessage()、QueryChunk业务文本或固定兜底文案,属于面向人的描述,不是稳定的程序化契约。调用方 Runtime 收到 FAILED Task 后,仅根据TaskState统一推导出REMOTE_BUSINESS_FAILURE,无法区分模型超时、模型限流、配置错误、工具失败、业务拒绝或 Runtime 内部异常。Core 已有
BaseError、StatusCode、数值 code、状态名和结构化toMap()。RunnerImpl也会在异常链中保留BaseError。但是 RuntimeA2AAgentExecutor.failAndDrain()构造 A2ATaskStatus.message时只读取error.getMessage(),Core 的稳定 code/status 在 Core -> Runtime -> A2A 边界丢失。服务入口能力范围设计 已明确要求 FAILED callback 返回“错误码和失败原因”、failed Task 携带可供客户端程序化判断的信息,并在可形成 Task 的异常路径携带结构化错误 payload。当前代码只满足“失败状态 + 可读文本”,未满足稳定错误码和结构化错误契约。
本 Issue 与
FAILED Task callback 可能丢失是两个独立问题:1. 场景
1.1 特性与模块
服务入口与 Push NotificationBaseErrorStatusCodeRunnerImplA2AAgentExecutorHttpPushNotificationSenderA2ARemoteAgentClientRemoteInvocationBatchMappermulti-deep-research-demo1.2 设计要求来源
服务入口能力范围设计:FAILED返回异常状态、错误码和失败原因。FAILED必须通知异常状态、错误码和失败原因。详细设计 当前列出了 JSON-RPC request error code,但没有定义异步 FAILED Task 中稳定业务/执行错误码的具体字段、命名空间、兼容和透传规则。
1.3 验证基线
验证日期:2026-08-08
2. 问题概述
2.1 FAILED text 不是稳定错误契约
当前 FAILED text 可能来自:
Throwable#getMessage()onErrorThrowable#getMessage()QueryChunk.TYPE_ERRORdata["error"]、字符串或固定兜底RuntimeExceptionFailed to invoke agent/Failed to stream agentBaseErrorBaseError#getMessage()InternalError -> FAILED因此,上游不能依赖 text 做如下判断:
2.2 上游只有粗分类
A2ARemoteAgentClient.resultCategory(TaskState)和RemoteInvocationBatchMapper.resultCategory(TaskState)当前固定映射:这个分类只说明“远端 Task 失败”,不说明“为什么失败”。例如以下失败都会得到同一个结果:
2.3 当前 callback 示例
通过生产
HttpPushNotificationSender.callbackBody()序列化的 FAILED Task 结构如下:{ "jsonrpc": "2.0", "notificationId": "notification-failed-example", "result": { "task": { "id": "task-failed-example", "contextId": "ctx-failed-example", "status": { "state": "TASK_STATE_FAILED", "message": { "role": "ROLE_AGENT", "parts": [ { "text": "Agent streaming execution failed" } ], "messageId": "<generated-message-id>" }, "timestamp": "<timestamp>" }, "artifacts": [], "history": [] } } }payload 中没有独立稳定错误码、错误域、可重试标志或来源错误码。
3. 问题分析
3.1 集成验证与现有测试执行结果
执行现有 Runtime 测试:
结果:
测试一
failurePathEmitsFailedStatusWithBusinessError:断言只检查 text,没有 code/metadata 断言。
测试二
remoteStatesMapToStableResultCategories:该测试确认所有远端 FAILED 当前只映射到同一粗分类。
3.2 对照组分析
BaseError(MODEL_CALL_FAILED)REMOTE_BUSINESS_FAILURE+ textRuntimeExceptionREMOTE_BUSINESS_FAILURE+ textREMOTE_TIMEOUTREMOTE_BUSINESS_FAILURE这说明 Runtime 对“本地观察到的 transport/queue 异常”已有一定分类能力,例如
REMOTE_TIMEOUT、REMOTE_OVERLOADED、REMOTE_RATE_LIMITED、REMOTE_PROTOCOL_ERROR和REMOTE_UNAVAILABLE;但“远端 Agent 自己报告的 FAILED”只剩REMOTE_BUSINESS_FAILURE,没有远端具体错误码。3.3 为什么不能解析 text 作为补偿
不应通过包含关系或正则解析 text,原因包括:
Failed to invoke agent等泛化文案不包含实际原因。4. 复现条件
4.1 最小源码级复现
在
A2AAgentExecutorTest中让ServeOrchestrator.query()抛出:观察生成的
TaskStatusUpdateEvent:随后将该 Task 交给:
结果:
4.2 Core
BaseError对照Core 已支持:
RunnerImpl.wrapRunnerRuntime()会遍历 cause 链并返回原始BaseError。将该异常传到 Runtime 后,A2AAgentExecutor.failAndDrain()仍只调用getMessage(),最终 callback 中看不到getCode()和getStatus()。4.3 Demo 观察方式
BaseError。TASK_STATE_FAILED并成功发送 callback。status.message.parts[].text。REMOTE_BUSINESS_FAILURE,无法获得 Search 侧具体错误码。注意:若进入
InternalError -> FAILEDcallback 丢失路径,应先按关联 Issue 处理通知丢失,否则本 Issue 的 payload 检查无法进行。5. 问题代码分析
5.1 Core 已有稳定错误结构
文件:
BaseError保存:toMap()输出:现有
StatusCode示例:5.2 Runner 会保留 BaseError
文件:
因此 Core -> Runner 边界没有必然丢失
BaseErrorcode;主要丢失发生在 Runtime -> A2A Task 映射。5.3 Runtime 只保留
Throwable#getMessage()文件:
这里没有读取:
BaseError#getCode()BaseError#getStatus()BaseError#isRecoverable()5.4 TYPE_ERROR 仍只有非结构化文本
streamChunkFailure()按 business text、map["error"]、字符串和固定兜底选择 message,最后包装成IllegalStateException。即使QueryChunk.data原来带有结构化错误字段,当前也只提取文本。5.5 Sender 和 receiver 没有主动丢字段
HttpPushNotificationSender.callbackBody()直接序列化完整Task。A2aPushNotificationCallbackController也会把result.task反序列化为 SDKTask。因此,如果错误结构放在 SDK 支持的
Message.metadata或其他约定字段中,sender/receiver 原则上可以随 Task 透传。当前问题主要是生产端没有写入、消费端没有读取。5.6 上游消费端再次降级为粗分类
A2ARemoteAgentClient:RemoteInvocationBatchMapper.callbackOutcome()同样只提取 status text,并根据 TaskState 计算resultCategory。最后父 Agent 得到的工具错误类似:
{ "ok": false, "code": "REMOTE_BUSINESS_FAILURE", "message": "remote boom", "remoteAgentId": "search-agent" }没有远端具体错误码。
6. 问题影响
REMOTE_BUSINESS_FAILURE,告警和问题定位不准确。7. 建议修复方案
方案 A:在
TaskStatus.message.metadata定义 namespaced 错误结构(推荐)不增加 callback 顶层私有字段,保持 callback 继续承载标准 A2A Task;在 FAILED status message 的 metadata 中增加版本化扩展:
{ "state": "TASK_STATE_FAILED", "message": { "role": "ROLE_AGENT", "parts": [ { "text": "Model invocation failed" } ], "metadata": { "openjiuwen.error": { "schemaVersion": "1", "code": "MODEL_CALL_FAILED", "numericCode": 181001, "category": "MODEL", "retryable": false, "origin": "CORE" } } } }约束:
text:安全、可读、允许调整的描述。code:稳定的程序化错误码,版本升级不得随文案变化。numericCode:复用 Core code,可选但建议保留。category:错误域,例如MODEL、TOOL、AGENT、RUNTIME、REMOTE。retryable:由明确策略生成,不能只按异常类名猜测。origin:错误最初产生层,用于责任定位。优点:
方案 B:定义统一
A2aFailureMapper/FailureDescriptor,避免散落映射在 Runtime 定义统一映射组件:
默认映射至少覆盖:
BaseError:保留 status name 和 numeric code。AGENT_EXECUTION_FAILED或等价通用 code,不暴露原始底层 message。QueryChunk.TYPE_ERROR:优先读取结构化 failure descriptor,旧格式仅作为 text fallback。该方案应与方案 A 配合,方案 A 定义传输结构,方案 B 统一生产规则。
方案 C:消费端保留“粗分类 + 远端具体错误”两级语义
为了兼容现有
resultCategory,不建议直接删除REMOTE_BUSINESS_FAILURE。父 Agent 工具结果可调整为:{ "ok": false, "code": "REMOTE_BUSINESS_FAILURE", "message": "Model invocation failed", "remoteAgentId": "search-agent", "remoteError": { "code": "MODEL_CALL_FAILED", "numericCode": 181001, "category": "MODEL", "retryable": false } }其中:
code=REMOTE_BUSINESS_FAILURE保留现有调用方粗分类兼容性。remoteError.code保留下游具体失败原因。REMOTE_TIMEOUT、REMOTE_UNAVAILABLE等本地分类,不伪装成下游业务错误。A2ARemoteAgentClient和RemoteInvocationBatchMapper.callbackOutcome()应复用同一个错误提取器,保证 streaming、blocking、callback 三种入口得到一致结果。方案 D:在 详细设计 中补齐错误契约和兼容规则
详细设计 至少需要明确:
GetTask必须返回一致错误结构。8. 建议验收标准
BaseError(MODEL_CALL_FAILED)经 Runtime 转换后,FAILED Task 保留稳定 code/status。GetTask、blocking、streaming 和 callback 对同一失败返回一致 error descriptor。REMOTE_BUSINESS_FAILURE粗分类;remoteError.code。REMOTE_BUSINESS_FAILURE + text,不破坏兼容性。InternalError -> FAILED路径修复后也必须使用同一 error descriptor,不得再次形成无 message/code 的 FAILED Task。9. 建议自动化回归覆盖
建议在正式自动化测试中覆盖:
BaseError(MODEL_CALL_FAILED),Deep 收到具体 code。GetTask返回,结构一致。10. 关联 Issue
failed-task-push-notification-may-be-lost.mdInternalError和 callback config 绑定竞态导致的 FAILED callback 丢失。callback-url-trust-documentation-mismatch.md11. 证据索引
详细的环境信息描述
agent-runtime-java: develop
其他辅助信息
版本信息
感谢您的贡献 🎉!