现象
CapabilityExecutor 用一张供应商码白名单判断「暂时不可用」,但生产链路的错误根本不会带这些码,
于是 unavailable 状态永不产生,四类「不可用」全部被记成 failed。
// packages/shared/src/capabilities/executor.ts:14-19
const UNAVAILABLE_ERROR_CODES = {
LONGBRIDGE_NOT_INSTALLED: true,
LONGBRIDGE_NOT_AUTHED: true,
LONGBRIDGE_RATE_LIMITED: true,
LONGBRIDGE_TIMEOUT: true,
} as const;
而生产环境构建 capability registry 时用的是 router 版 fetchers:
// apps/electron/src/main/kernelHost.ts:349-359
const routerFetchers = createRouterFetchers(this.providerRouter, { … });
const capabilityFetchers = demoData ? withDemoDataFallback(routerFetchers) : routerFetchers;
this.registry = createFullRegistry(capabilityFetchers);
createRouterFetchers 失败时抛的是归一化后的 provider 码:
// packages/shared/src/providers/router-fetchers.ts:109
throw new ProviderFetchError(result.error);
该码由 providers/longbridge/adapter.ts 的 toProviderError 产生(:81-108):
| 供应商码(白名单里的) |
归一化码(真实到达的) |
语义 |
LONGBRIDGE_NOT_AUTHED |
AUTH_EXPIRED |
会话过期 / 未登录 |
LONGBRIDGE_RATE_LIMITED |
RATE_LIMITED |
限流 |
LONGBRIDGE_TIMEOUT |
TIMEOUT |
超时 |
两张码表零交集 ⇒ isUnavailableError()(executor.ts:175-181)恒为 false ⇒
classifyStatus()(:169-173)永远返回 failed。
期望
上述四类「can't serve right now」记成 unavailable,与状态机定义一致。
判据(源码自述契约)
-
executor.ts:9-13 注释写明这张表存在的理由:
Provider error codes that mean "can't serve right now" (missing CLI, missing auth, rate limited, or the provider timed out) rather than a data/logic failure. These map a run to 'unavailable'.
注意它自称是 provider error codes —— 而列出的却是供应商 CLI 码。
-
router-fetchers.ts:40-44 明确了 caller 应当基于归一码分支:
Normalized failure thrown by the router-backed fetchers. Carries the stable ProviderError.code and user-safe message so callers can branch on the machine code without importing vendor types.
-
kernelHost.ts:326 同向声明业务层看不到供应商码:
Business layers see only the neutral capability …
-
providers/resilience.ts:39-43 已确立规范 —— 不要在各处散落字符串匹配:
Free-form provider code strings are normalized into this closed union by classifyFailure; routing/retry/breaker decisions always switch on the kind, never on string-matching scattered across call sites.
-
现有测试是测试替身:executor.test.ts:52-63 直接构造 code: 'LONGBRIDGE_NOT_AUTHED',
所以一直绿;真实链路不会产生该码 —— 典型的「测试替身 vs 真实实现语义漂移」。
影响
finance-tool-registry.ts:98-102 按状态决定抛给 Agent 的错误码:
function failureCode(status: string) {
if (status === 'cancelled') return 'RUN_CANCELLED';
if (status === 'unavailable') return 'CAPABILITY_UNAVAILABLE';
return 'CAPABILITY_FAILED';
}
因此「未登录 / 限流 / 超时」这三类可恢复情况,对 Agent 一律表现为 CAPABILITY_FAILED
(不可恢复的失败),而不是 CAPABILITY_UNAVAILABLE —— 影响 Agent 的失败归因与后续重试/提示策略;
evaluation/langfuse/exporter.ts:203 也把两者分别统计。
复现
bun test packages/shared/src/capabilities/executor.test.ts --isolate
# 修复前:10 pass / 3 fail(三条均为 Expected: "unavailable" / Received: "failed")
新增用例只用「router 链路上真实会到达的码」构造失败,不引入任何供应商类型。
建议修复方向
把归一化码补进同一张表(保持两张表的并集,兼容直接用 @finagent/longbridge-tools fetchers 的调用方):
const UNAVAILABLE_ERROR_CODES = {
LONGBRIDGE_NOT_INSTALLED: true,
LONGBRIDGE_NOT_AUTHED: true,
LONGBRIDGE_RATE_LIMITED: true,
LONGBRIDGE_TIMEOUT: true,
AUTH_EXPIRED: true,
RATE_LIMITED: true,
TIMEOUT: true,
} as const;
PROVIDER_ERROR 不在其列:toProviderError 的 default 分支也用它,纳入会把真正的数据/逻辑失败
误判为「不可用」。这也暴露一个相关但独立的问题:LONGBRIDGE_NOT_INSTALLED 被压成通用的
PROVIDER_ERROR(adapter.ts:101-102),因此在 router 链路上无法与「请求失败」区分 ——
那需要在 adapter 侧给出更具体的码,本次不纳入。
验证方式
- 修复前
bun test packages/shared/src/capabilities/executor.test.ts --isolate = 10 pass / 3 fail
(三条新用例全部 Expected: "unavailable" / Received: "failed"),修复后 = 13 pass / 0 fail;
bun test packages/shared/src/capabilities --isolate = 46 pass / 0 fail;
bun run typecheck 五个工作区 exit 0。
现象
CapabilityExecutor用一张供应商码白名单判断「暂时不可用」,但生产链路的错误根本不会带这些码,于是
unavailable状态永不产生,四类「不可用」全部被记成failed。而生产环境构建 capability registry 时用的是 router 版 fetchers:
createRouterFetchers失败时抛的是归一化后的 provider 码:该码由
providers/longbridge/adapter.ts的toProviderError产生(:81-108):LONGBRIDGE_NOT_AUTHEDAUTH_EXPIREDLONGBRIDGE_RATE_LIMITEDRATE_LIMITEDLONGBRIDGE_TIMEOUTTIMEOUT两张码表零交集 ⇒
isUnavailableError()(executor.ts:175-181)恒为 false ⇒classifyStatus()(:169-173)永远返回failed。期望
上述四类「can't serve right now」记成
unavailable,与状态机定义一致。判据(源码自述契约)
executor.ts:9-13注释写明这张表存在的理由:router-fetchers.ts:40-44明确了 caller 应当基于归一码分支:kernelHost.ts:326同向声明业务层看不到供应商码:providers/resilience.ts:39-43已确立规范 —— 不要在各处散落字符串匹配:现有测试是测试替身:
executor.test.ts:52-63直接构造code: 'LONGBRIDGE_NOT_AUTHED',所以一直绿;真实链路不会产生该码 —— 典型的「测试替身 vs 真实实现语义漂移」。
影响
finance-tool-registry.ts:98-102按状态决定抛给 Agent 的错误码:因此「未登录 / 限流 / 超时」这三类可恢复情况,对 Agent 一律表现为
CAPABILITY_FAILED(不可恢复的失败),而不是
CAPABILITY_UNAVAILABLE—— 影响 Agent 的失败归因与后续重试/提示策略;evaluation/langfuse/exporter.ts:203也把两者分别统计。复现
新增用例只用「router 链路上真实会到达的码」构造失败,不引入任何供应商类型。
建议修复方向
把归一化码补进同一张表(保持两张表的并集,兼容直接用
@finagent/longbridge-toolsfetchers 的调用方):PROVIDER_ERROR不在其列:toProviderError的default分支也用它,纳入会把真正的数据/逻辑失败误判为「不可用」。这也暴露一个相关但独立的问题:
LONGBRIDGE_NOT_INSTALLED被压成通用的PROVIDER_ERROR(adapter.ts:101-102),因此在 router 链路上无法与「请求失败」区分 ——那需要在 adapter 侧给出更具体的码,本次不纳入。
验证方式
bun test packages/shared/src/capabilities/executor.test.ts --isolate= 10 pass / 3 fail(三条新用例全部
Expected: "unavailable" / Received: "failed"),修复后 = 13 pass / 0 fail;bun test packages/shared/src/capabilities --isolate= 46 pass / 0 fail;bun run typecheck五个工作区 exit 0。