@migor/agentia 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +248 -0
- package/README.md +1 -2
- package/dist/AGENTS.md +253 -160
- package/dist/core/abort.js +4 -1
- package/dist/core/abort.js.map +1 -1
- package/dist/core/message.d.ts +148 -0
- package/dist/core/message.js +25 -0
- package/dist/core/message.js.map +1 -0
- package/dist/core/timeout.d.ts +72 -0
- package/dist/core/timeout.js +117 -0
- package/dist/core/timeout.js.map +1 -0
- package/dist/core/tool.d.ts +21 -9
- package/dist/engine/concurrency.d.ts +6 -30
- package/dist/engine/concurrency.js +6 -64
- package/dist/engine/concurrency.js.map +1 -1
- package/dist/engine/errors.d.ts +12 -0
- package/dist/engine/errors.js +56 -13
- package/dist/engine/errors.js.map +1 -1
- package/dist/engine/loop.d.ts +17 -11
- package/dist/engine/loop.js +34 -433
- package/dist/engine/loop.js.map +1 -1
- package/dist/engine/replay.d.ts +18 -3
- package/dist/engine/replay.js +53 -9
- package/dist/engine/replay.js.map +1 -1
- package/dist/engine/retry.d.ts +5 -4
- package/dist/engine/retry.js.map +1 -1
- package/dist/engine/spec.d.ts +5 -5
- package/dist/engine/spec.js.map +1 -1
- package/dist/engine/trace-diff.d.ts +57 -0
- package/dist/engine/trace-diff.js +188 -0
- package/dist/engine/trace-diff.js.map +1 -0
- package/dist/engine/trimming.d.ts +7 -7
- package/dist/engine/trimming.js +12 -5
- package/dist/engine/trimming.js.map +1 -1
- package/dist/engine/turn.d.ts +157 -0
- package/dist/engine/turn.js +512 -0
- package/dist/engine/turn.js.map +1 -0
- package/dist/engine/types.d.ts +6 -8
- package/dist/engine/usage.d.ts +2 -2
- package/dist/engine/usage.js +4 -1
- package/dist/engine/usage.js.map +1 -1
- package/dist/eval/defineEval.js.map +1 -1
- package/dist/eval/harvest.d.ts +2 -9
- package/dist/eval/harvest.js +20 -9
- package/dist/eval/harvest.js.map +1 -1
- package/dist/eval/scripted.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/anthropic.d.ts +41 -37
- package/dist/integrations/anthropic.js +408 -17
- package/dist/integrations/anthropic.js.map +1 -1
- package/dist/integrations/mcp.d.ts +13 -4
- package/dist/integrations/mcp.js +27 -24
- package/dist/integrations/mcp.js.map +1 -1
- package/dist/integrations/openai.d.ts +1 -1
- package/dist/integrations/openai.js +3 -3
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/otlp.js +29 -11
- package/dist/integrations/otlp.js.map +1 -1
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/session.d.ts +5 -5
- package/dist/runtime/session.js +5 -1
- package/dist/runtime/session.js.map +1 -1
- package/dist/toolkit/module.d.ts +16 -2
- package/dist/toolkit/module.js +42 -4
- package/dist/toolkit/module.js.map +1 -1
- package/dist/toolkit/skill.d.ts +3 -3
- package/dist/toolkit/subagent.d.ts +2 -2
- package/dist/toolkit/subagent.js +1 -1
- package/dist/transport/async.d.ts +2 -2
- package/package.json +4 -5
package/dist/core/abort.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
const cleanups = new WeakMap();
|
|
11
11
|
/** 合成多个中断源:任一已中止 / 后中止即中止;忽略 undefined;全空返回**永不中止**的 signal。 */
|
|
12
12
|
export function combineSignals(...signals) {
|
|
13
|
-
|
|
13
|
+
// 去重:同一个源被传两次时,下面的 listeners Map 按源建(后者覆盖前者),
|
|
14
|
+
// 却对同一源挂了两个同名监听 ⇒ 摘除只摘掉一个,另一个(连闭包)滞留在宿主级
|
|
15
|
+
// 长寿 signal 上 —— 正是这段代码要防的累积。Set 保序,行为不变。
|
|
16
|
+
const real = [...new Set(signals.filter((s) => s !== undefined))];
|
|
14
17
|
// 单源直接复用,避免多包一层 listener
|
|
15
18
|
if (real.length === 1)
|
|
16
19
|
return real[0];
|
package/dist/core/abort.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"abort.js","sourceRoot":"","sources":["../../src/core/abort.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gEAAgE;AAChE,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA2B,CAAC;AAExD,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,GAAG,OAAuC;IACvE,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"abort.js","sourceRoot":"","sources":["../../src/core/abort.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gEAAgE;AAChE,MAAM,QAAQ,GAAG,IAAI,OAAO,EAA2B,CAAC;AAExD,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,GAAG,OAAuC;IACvE,8CAA8C;IAC9C,yCAAyC;IACzC,0CAA0C;IAC1C,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC;IACpF,yBAAyB;IACzB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;IAEtC,MAAM,EAAE,GAAG,IAAI,eAAe,EAAE,CAAC;IACjC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;YACd,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACnB,MAAM;QACR,CAAC;IACH,CAAC;IACD,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,+CAA+C;QAC/C,gDAAgD;QAChD,iDAAiD;QACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAA2B,CAAC;QACrD,MAAM,MAAM,GAAG,GAAS,EAAE;YACxB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS;gBAAE,CAAC,CAAC,mBAAmB,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAClE,SAAS,CAAC,KAAK,EAAE,CAAC;YAClB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC,CAAC;QACF,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,CAAC,GAAG,GAAS,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;YACzC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACpB,CAAC,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC5D,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,EAAE,CAAC,MAAM,CAAC;AACnB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAmB;IACvD,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 消息类型族(公共类型自有化,2026-09-17)。
|
|
3
|
+
*
|
|
4
|
+
* 框架公共面(ModelClient 契约 / RunSpec / SessionStore / replay 等)的消息与内容块
|
|
5
|
+
* 类型,字段口径与 Anthropic Messages API 逐字对齐(snake_case),与
|
|
6
|
+
* `@anthropic-ai/sdk` 的对应类型**结构兼容**(双向/单向 assignability 由
|
|
7
|
+
* `tests/types/message-compat.types.ts` 钉住,SDK 因此只留在 devDependencies 做门禁)。
|
|
8
|
+
*
|
|
9
|
+
* 两条刻意的取舍(都有硬理由,别「顺手改进」):
|
|
10
|
+
*
|
|
11
|
+
* 1. **兜底成员不带索引签名**(`UnknownContentBlockParam` / `UnknownContentBlock` 只有
|
|
12
|
+
* `type: string`)。带 `[key: string]: unknown` 的形态看起来更「前向兼容」,但 SDK 的
|
|
13
|
+
* 块类型全是 **interface**(无隐式索引签名),赋给带索引签名的目标直接编译失败 ——
|
|
14
|
+
* 「使用者手里的 `Anthropic.MessageParam[]` 直接喂给框架」这条承诺就会当场破产。
|
|
15
|
+
* 只有 `type` 的最小面反而两个方向都通:未知块原样携带(历史回灌不丢内容),
|
|
16
|
+
* 要读字段请自行收窄/断言。
|
|
17
|
+
* 2. **`Role` 含 `'system'`**:SDK 0.124 的 `MessageParam.role` 是
|
|
18
|
+
* `'user' | 'assistant' | 'system'`,方向一(SDK → 自有)要求逐字对齐。
|
|
19
|
+
* 引擎自身只产出 user/assistant;给 Messages API 发 system 角色会被端点拒绝,
|
|
20
|
+
* 与 SDK 类型下的同款误用语义一致。
|
|
21
|
+
*
|
|
22
|
+
* 零依赖纯类型文件:不写运行时代码,编译后全部擦除。core 是叶子层,本文件不 import 任何模块。
|
|
23
|
+
*/
|
|
24
|
+
/** 消息角色(含 'system' 的原因见文件头注释 2) */
|
|
25
|
+
export type Role = 'user' | 'assistant' | 'system';
|
|
26
|
+
/** prompt cache 断点标记(`cache_control` 字段的值) */
|
|
27
|
+
export interface CacheControl {
|
|
28
|
+
type: 'ephemeral';
|
|
29
|
+
/** 缓存 TTL;缺省 '5m'(与 SDK 的 CacheControlEphemeral 逐字对齐) */
|
|
30
|
+
ttl?: '5m' | '1h';
|
|
31
|
+
}
|
|
32
|
+
export interface TextBlockParam {
|
|
33
|
+
type: 'text';
|
|
34
|
+
text: string;
|
|
35
|
+
cache_control?: CacheControl | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ImageBlockParam {
|
|
38
|
+
type: 'image';
|
|
39
|
+
/**
|
|
40
|
+
* 图片源:base64 / url 为已知形态;其余(如 file_id 源)走 `{ type: string }` 最小面兜底
|
|
41
|
+
* (理由同文件头注释 1)。读具体字段前先按 `source.type` 收窄。
|
|
42
|
+
*/
|
|
43
|
+
source: {
|
|
44
|
+
type: 'base64';
|
|
45
|
+
media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
|
|
46
|
+
data: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'url';
|
|
49
|
+
url: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: string;
|
|
52
|
+
};
|
|
53
|
+
cache_control?: CacheControl | null;
|
|
54
|
+
}
|
|
55
|
+
export interface ToolUseBlockParam {
|
|
56
|
+
type: 'tool_use';
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
input: unknown;
|
|
60
|
+
cache_control?: CacheControl | null;
|
|
61
|
+
}
|
|
62
|
+
export interface ToolResultBlockParam {
|
|
63
|
+
type: 'tool_result';
|
|
64
|
+
tool_use_id: string;
|
|
65
|
+
/** 工具结果正文:纯文本,或 text/image 等块(未知块型走兜底成员,原样携带) */
|
|
66
|
+
content?: string | Array<TextBlockParam | ImageBlockParam | UnknownContentBlockParam>;
|
|
67
|
+
is_error?: boolean;
|
|
68
|
+
cache_control?: CacheControl | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 前向兼容兜底:厂商新增块类型的最小面。**刻意不带索引签名**(见文件头注释 1)——
|
|
72
|
+
* 未知块在消息历史里原样携带(role 交替与 tool_use/tool_result 配对不受影响),
|
|
73
|
+
* 消费方按 `type` 排除已知块后自行断言读取。
|
|
74
|
+
*/
|
|
75
|
+
export interface UnknownContentBlockParam {
|
|
76
|
+
type: string;
|
|
77
|
+
}
|
|
78
|
+
/** 请求侧内容块联合:已知块 + 兜底成员(厂商新块型原样携带) */
|
|
79
|
+
export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | UnknownContentBlockParam;
|
|
80
|
+
export interface MessageParam {
|
|
81
|
+
role: Role;
|
|
82
|
+
content: string | ContentBlockParam[];
|
|
83
|
+
}
|
|
84
|
+
/** 工具的 input_schema 形状(与 SDK 的 Tool.InputSchema 逐字对齐) */
|
|
85
|
+
export interface ToolInputSchema {
|
|
86
|
+
type: 'object';
|
|
87
|
+
properties?: unknown;
|
|
88
|
+
required?: string[] | null;
|
|
89
|
+
[key: string]: unknown;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 发给模型的工具定义(**避让 @Tool 装饰器**,故名 ToolParam)。
|
|
93
|
+
* 引擎从 AgentTool 推导(`engine/loop.ts` 的 toApiTool);自定义 ModelClient 只读它。
|
|
94
|
+
* 与块类型同理**不带索引签名**(文件头注释 1):SDK 的 `Tool` 是 interface,
|
|
95
|
+
* 带索引签名会让「SDK 的工具定义数组直接喂给自定义 client」编译失败。
|
|
96
|
+
* 厂商新增的工具字段(defer_loading 等)读之前自行断言。
|
|
97
|
+
*/
|
|
98
|
+
export interface ToolParam {
|
|
99
|
+
name: string;
|
|
100
|
+
description?: string;
|
|
101
|
+
input_schema: ToolInputSchema;
|
|
102
|
+
strict?: boolean;
|
|
103
|
+
cache_control?: CacheControl | null;
|
|
104
|
+
}
|
|
105
|
+
export interface TextBlock {
|
|
106
|
+
type: 'text';
|
|
107
|
+
text: string;
|
|
108
|
+
}
|
|
109
|
+
export interface ToolUseBlock {
|
|
110
|
+
type: 'tool_use';
|
|
111
|
+
id: string;
|
|
112
|
+
name: string;
|
|
113
|
+
input: unknown;
|
|
114
|
+
}
|
|
115
|
+
export interface ThinkingBlock {
|
|
116
|
+
type: 'thinking';
|
|
117
|
+
thinking: string;
|
|
118
|
+
signature: string;
|
|
119
|
+
}
|
|
120
|
+
/** 响应侧兜底块(语义同 UnknownContentBlockParam,见文件头注释 1) */
|
|
121
|
+
export interface UnknownContentBlock {
|
|
122
|
+
type: string;
|
|
123
|
+
}
|
|
124
|
+
/** 响应侧内容块联合:text / tool_use / thinking + 兜底成员 */
|
|
125
|
+
export type ContentBlock = TextBlock | ToolUseBlock | ThinkingBlock | UnknownContentBlock;
|
|
126
|
+
/**
|
|
127
|
+
* 模型 stop_reason。已知取值与 SDK 逐字对齐;`| (string & {})` 收留端点未来的新值
|
|
128
|
+
* (引擎对未识别的 stop_reason 有 `unknown_stop_reason` 收尾分支,见 engine/loop.ts)。
|
|
129
|
+
*/
|
|
130
|
+
export type StopReason = 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | 'pause_turn' | 'refusal' | 'model_context_window_exceeded' | (string & {});
|
|
131
|
+
/** 模型回报的 token 计量(**避让 trace 的 `Usage`**(camelCase 聚合),故名 MessageUsage) */
|
|
132
|
+
export interface MessageUsage {
|
|
133
|
+
input_tokens: number;
|
|
134
|
+
output_tokens: number;
|
|
135
|
+
cache_creation_input_tokens?: number | null;
|
|
136
|
+
cache_read_input_tokens?: number | null;
|
|
137
|
+
}
|
|
138
|
+
/** 一次模型往返的最终响应(`ModelClient.messages.stream().finalMessage()` 的产物) */
|
|
139
|
+
export interface Message {
|
|
140
|
+
id: string;
|
|
141
|
+
type: 'message';
|
|
142
|
+
role: 'assistant';
|
|
143
|
+
content: ContentBlock[];
|
|
144
|
+
model: string;
|
|
145
|
+
stop_reason: StopReason | null;
|
|
146
|
+
stop_sequence: string | null;
|
|
147
|
+
usage: MessageUsage;
|
|
148
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 消息类型族(公共类型自有化,2026-09-17)。
|
|
3
|
+
*
|
|
4
|
+
* 框架公共面(ModelClient 契约 / RunSpec / SessionStore / replay 等)的消息与内容块
|
|
5
|
+
* 类型,字段口径与 Anthropic Messages API 逐字对齐(snake_case),与
|
|
6
|
+
* `@anthropic-ai/sdk` 的对应类型**结构兼容**(双向/单向 assignability 由
|
|
7
|
+
* `tests/types/message-compat.types.ts` 钉住,SDK 因此只留在 devDependencies 做门禁)。
|
|
8
|
+
*
|
|
9
|
+
* 两条刻意的取舍(都有硬理由,别「顺手改进」):
|
|
10
|
+
*
|
|
11
|
+
* 1. **兜底成员不带索引签名**(`UnknownContentBlockParam` / `UnknownContentBlock` 只有
|
|
12
|
+
* `type: string`)。带 `[key: string]: unknown` 的形态看起来更「前向兼容」,但 SDK 的
|
|
13
|
+
* 块类型全是 **interface**(无隐式索引签名),赋给带索引签名的目标直接编译失败 ——
|
|
14
|
+
* 「使用者手里的 `Anthropic.MessageParam[]` 直接喂给框架」这条承诺就会当场破产。
|
|
15
|
+
* 只有 `type` 的最小面反而两个方向都通:未知块原样携带(历史回灌不丢内容),
|
|
16
|
+
* 要读字段请自行收窄/断言。
|
|
17
|
+
* 2. **`Role` 含 `'system'`**:SDK 0.124 的 `MessageParam.role` 是
|
|
18
|
+
* `'user' | 'assistant' | 'system'`,方向一(SDK → 自有)要求逐字对齐。
|
|
19
|
+
* 引擎自身只产出 user/assistant;给 Messages API 发 system 角色会被端点拒绝,
|
|
20
|
+
* 与 SDK 类型下的同款误用语义一致。
|
|
21
|
+
*
|
|
22
|
+
* 零依赖纯类型文件:不写运行时代码,编译后全部擦除。core 是叶子层,本文件不 import 任何模块。
|
|
23
|
+
*/
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=message.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/core/message.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 「等待的终点」超时原语(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么在 core:engine 的工具级超时(`engine/turn.ts`)与桥的 MCP 调用超时
|
|
5
|
+
* (`integrations/mcp.ts`)**必须共用同一套判定**。此前两处各写一份,代价是
|
|
6
|
+
* 2026-09-14 的「硬保证」收紧只落进了 `engine/concurrency.ts`,桥那一份继续用竞速判定 ——
|
|
7
|
+
* 同一个承诺两套实现,于是同一个事件在 trace 里能有两种账(见 `docs/spec.md` §10 2026-09-17 ①)。
|
|
8
|
+
* `integrations` 只能依赖 core(`tests/architecture/layering.test.ts` 强制),
|
|
9
|
+
* 所以单源的落点是 core。core 是叶子:本文件零 import。
|
|
10
|
+
*/
|
|
11
|
+
/** 超时哨兵:区分「超时」与「工具恰好返回了 undefined」 */
|
|
12
|
+
export declare const TIMED_OUT: unique symbol;
|
|
13
|
+
/**
|
|
14
|
+
* 超时错误(`code === 'timeout'`)。
|
|
15
|
+
*
|
|
16
|
+
* 为什么需要它:超时此前只能靠**错误文案**辨认 —— 桥抛的是普通 `Error`,而
|
|
17
|
+
* `classifyError` 里没有 timeout 这一类,于是 trace 里记成 `error(unknown)` +
|
|
18
|
+
* `errorKind=threw`,与引擎自己判的超时(`error(timeout)` + `errorKind=timeout`)
|
|
19
|
+
* 成为同一事件的**两种账**(见 `docs/spec.md` §10 2026-09-17 ①)。
|
|
20
|
+
*
|
|
21
|
+
* 判定契约(对使用者可见):**任何** `code === 'timeout'` 的错误都会被引擎归为
|
|
22
|
+
* `errorKind='timeout'`,不需要 import 这个类(`isTimeoutError` 也认鸭子类型)。
|
|
23
|
+
*/
|
|
24
|
+
export declare class TimeoutError extends Error {
|
|
25
|
+
readonly code: 'timeout';
|
|
26
|
+
constructor(message: string);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 是否超时错误。三条判据(任一条成立即算,**全部是鸭子类型** —— 使用者不必 import 本模块):
|
|
30
|
+
*
|
|
31
|
+
* 1. 本模块的 `TimeoutError` 实例(框架自己判的超时,如 MCP 桥的兜底);
|
|
32
|
+
* 2. `code === 'timeout'`(工具作者自报超时的推荐写法);
|
|
33
|
+
* 3. 内建 `DOMException` 的 `name === 'TimeoutError'` —— `AbortSignal.timeout()` 与默认 client 的
|
|
34
|
+
* 超时合成信号(`integrations/anthropic.ts` 的 `composeSignal`)产出的就是它,`name` 是规范值、
|
|
35
|
+
* 不受压缩影响。
|
|
36
|
+
*
|
|
37
|
+
* 为什么第 3 条也算:`engine/errors.ts` 的分类**此前只有它**认这一条(当时把超时归进 `connection`),
|
|
38
|
+
* 于是「超时」在 trace 里混在「连不上」里,看板与 `trace-diff` 分不出两者。现在超时有自己的
|
|
39
|
+
* `type: 'timeout'`,这条判据由分类与工具级记账**共用**,口径一致(spec §10 2026-09-17 ②)。
|
|
40
|
+
*/
|
|
41
|
+
export declare function isTimeoutError(e: unknown): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* 给一个 promise 套超时;超时返回 `TIMED_OUT`。
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ **不取消底层** —— `AgentTool.run` 拿不到 signal(那会破坏现有签名),所以
|
|
46
|
+
* 「超时」的语义是**放弃等待**:副作用可能已经发生,只是我们不等了。
|
|
47
|
+
* 想真停下来的工具请自行读 `ToolRunContext.signal`(框架传了,但**不强制**工具中断
|
|
48
|
+
* —— 见 core/tool.ts 的说明:工具副作用无法回滚)。
|
|
49
|
+
*
|
|
50
|
+
* **超时是硬的**(2026-09-14 收紧,见 `docs/spec.md` §10):判定不看竞速结果,而看**实测耗时**。
|
|
51
|
+
* 原因:`Promise.race` 不是硬保证 —— 两个计时器在同一毫秒内建、又因事件循环被饿住而同批到期时,
|
|
52
|
+
* 列表顺序决定谁先 resolve,**超预算的工具能赢过截止计时器**(实测 `60ms 工具 vs 20ms 预算`
|
|
53
|
+
* 在 8 倍 CPU 超订下 1200 次翻转 1 次,记成 `ok: true`,护栏静默失效)。
|
|
54
|
+
* 代价是语义收紧:`21ms 完成 / 20ms 预算` 由「成功」变「超时」—— 这正确,
|
|
55
|
+
* 「预算」本来就是对**实际耗时**的承诺,不是对调度运气的承诺。
|
|
56
|
+
*
|
|
57
|
+
* `timeoutMs` 非正数 = 不设超时(直接返回原 promise)。
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ **截止计时器绝不可 `unref()`**(2026-09-14 修正,见 `docs/spec.md` §10 ④):
|
|
60
|
+
* 这个计时器的**触发本身就是「被 await 的 promise 得以 settle」的条件**。一旦 unref,
|
|
61
|
+
* 当它是事件循环里唯一的把手时,进程会在它触发前直接退出 —— `await` **永不 settle**。
|
|
62
|
+
* 实测(空事件循环、挂死的 promise,Node 22 与 26 一个样):
|
|
63
|
+
*
|
|
64
|
+
* | 计时器 | 结果 |
|
|
65
|
+
* |---|---|
|
|
66
|
+
* | `unref()` | 进程退出(顶层 await 未 settle),**什么都没返回** |
|
|
67
|
+
* | 不 unref | `settled: TIMED_OUT`,exit 0 |
|
|
68
|
+
*
|
|
69
|
+
* 「兜底计时器不该让宿主为它续命」这条理由对**没人 await 的兜底 tick**(scheduler 的下一拍、
|
|
70
|
+
* SSE 心跳、metrics 刷盘)成立,对**「等待的终点」**不成立:那正是调用方在等的东西。
|
|
71
|
+
*/
|
|
72
|
+
export declare function withTimeout<T>(p: Promise<T>, timeoutMs: number): Promise<T | typeof TIMED_OUT>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 「等待的终点」超时原语(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么在 core:engine 的工具级超时(`engine/turn.ts`)与桥的 MCP 调用超时
|
|
5
|
+
* (`integrations/mcp.ts`)**必须共用同一套判定**。此前两处各写一份,代价是
|
|
6
|
+
* 2026-09-14 的「硬保证」收紧只落进了 `engine/concurrency.ts`,桥那一份继续用竞速判定 ——
|
|
7
|
+
* 同一个承诺两套实现,于是同一个事件在 trace 里能有两种账(见 `docs/spec.md` §10 2026-09-17 ①)。
|
|
8
|
+
* `integrations` 只能依赖 core(`tests/architecture/layering.test.ts` 强制),
|
|
9
|
+
* 所以单源的落点是 core。core 是叶子:本文件零 import。
|
|
10
|
+
*/
|
|
11
|
+
/** 超时哨兵:区分「超时」与「工具恰好返回了 undefined」 */
|
|
12
|
+
export const TIMED_OUT = Symbol('agentia.timed-out');
|
|
13
|
+
/**
|
|
14
|
+
* 超时错误(`code === 'timeout'`)。
|
|
15
|
+
*
|
|
16
|
+
* 为什么需要它:超时此前只能靠**错误文案**辨认 —— 桥抛的是普通 `Error`,而
|
|
17
|
+
* `classifyError` 里没有 timeout 这一类,于是 trace 里记成 `error(unknown)` +
|
|
18
|
+
* `errorKind=threw`,与引擎自己判的超时(`error(timeout)` + `errorKind=timeout`)
|
|
19
|
+
* 成为同一事件的**两种账**(见 `docs/spec.md` §10 2026-09-17 ①)。
|
|
20
|
+
*
|
|
21
|
+
* 判定契约(对使用者可见):**任何** `code === 'timeout'` 的错误都会被引擎归为
|
|
22
|
+
* `errorKind='timeout'`,不需要 import 这个类(`isTimeoutError` 也认鸭子类型)。
|
|
23
|
+
*/
|
|
24
|
+
export class TimeoutError extends Error {
|
|
25
|
+
code = 'timeout';
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'TimeoutError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 是否超时错误。三条判据(任一条成立即算,**全部是鸭子类型** —— 使用者不必 import 本模块):
|
|
33
|
+
*
|
|
34
|
+
* 1. 本模块的 `TimeoutError` 实例(框架自己判的超时,如 MCP 桥的兜底);
|
|
35
|
+
* 2. `code === 'timeout'`(工具作者自报超时的推荐写法);
|
|
36
|
+
* 3. 内建 `DOMException` 的 `name === 'TimeoutError'` —— `AbortSignal.timeout()` 与默认 client 的
|
|
37
|
+
* 超时合成信号(`integrations/anthropic.ts` 的 `composeSignal`)产出的就是它,`name` 是规范值、
|
|
38
|
+
* 不受压缩影响。
|
|
39
|
+
*
|
|
40
|
+
* 为什么第 3 条也算:`engine/errors.ts` 的分类**此前只有它**认这一条(当时把超时归进 `connection`),
|
|
41
|
+
* 于是「超时」在 trace 里混在「连不上」里,看板与 `trace-diff` 分不出两者。现在超时有自己的
|
|
42
|
+
* `type: 'timeout'`,这条判据由分类与工具级记账**共用**,口径一致(spec §10 2026-09-17 ②)。
|
|
43
|
+
*/
|
|
44
|
+
export function isTimeoutError(e) {
|
|
45
|
+
if (e instanceof TimeoutError)
|
|
46
|
+
return true;
|
|
47
|
+
if (typeof e !== 'object' || e === null)
|
|
48
|
+
return false;
|
|
49
|
+
const o = e;
|
|
50
|
+
return o.code === 'timeout' || o.name === 'TimeoutError';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 给一个 promise 套超时;超时返回 `TIMED_OUT`。
|
|
54
|
+
*
|
|
55
|
+
* ⚠️ **不取消底层** —— `AgentTool.run` 拿不到 signal(那会破坏现有签名),所以
|
|
56
|
+
* 「超时」的语义是**放弃等待**:副作用可能已经发生,只是我们不等了。
|
|
57
|
+
* 想真停下来的工具请自行读 `ToolRunContext.signal`(框架传了,但**不强制**工具中断
|
|
58
|
+
* —— 见 core/tool.ts 的说明:工具副作用无法回滚)。
|
|
59
|
+
*
|
|
60
|
+
* **超时是硬的**(2026-09-14 收紧,见 `docs/spec.md` §10):判定不看竞速结果,而看**实测耗时**。
|
|
61
|
+
* 原因:`Promise.race` 不是硬保证 —— 两个计时器在同一毫秒内建、又因事件循环被饿住而同批到期时,
|
|
62
|
+
* 列表顺序决定谁先 resolve,**超预算的工具能赢过截止计时器**(实测 `60ms 工具 vs 20ms 预算`
|
|
63
|
+
* 在 8 倍 CPU 超订下 1200 次翻转 1 次,记成 `ok: true`,护栏静默失效)。
|
|
64
|
+
* 代价是语义收紧:`21ms 完成 / 20ms 预算` 由「成功」变「超时」—— 这正确,
|
|
65
|
+
* 「预算」本来就是对**实际耗时**的承诺,不是对调度运气的承诺。
|
|
66
|
+
*
|
|
67
|
+
* `timeoutMs` 非正数 = 不设超时(直接返回原 promise)。
|
|
68
|
+
*
|
|
69
|
+
* ⚠️ **截止计时器绝不可 `unref()`**(2026-09-14 修正,见 `docs/spec.md` §10 ④):
|
|
70
|
+
* 这个计时器的**触发本身就是「被 await 的 promise 得以 settle」的条件**。一旦 unref,
|
|
71
|
+
* 当它是事件循环里唯一的把手时,进程会在它触发前直接退出 —— `await` **永不 settle**。
|
|
72
|
+
* 实测(空事件循环、挂死的 promise,Node 22 与 26 一个样):
|
|
73
|
+
*
|
|
74
|
+
* | 计时器 | 结果 |
|
|
75
|
+
* |---|---|
|
|
76
|
+
* | `unref()` | 进程退出(顶层 await 未 settle),**什么都没返回** |
|
|
77
|
+
* | 不 unref | `settled: TIMED_OUT`,exit 0 |
|
|
78
|
+
*
|
|
79
|
+
* 「兜底计时器不该让宿主为它续命」这条理由对**没人 await 的兜底 tick**(scheduler 的下一拍、
|
|
80
|
+
* SSE 心跳、metrics 刷盘)成立,对**「等待的终点」**不成立:那正是调用方在等的东西。
|
|
81
|
+
*/
|
|
82
|
+
export async function withTimeout(p, timeoutMs) {
|
|
83
|
+
if (!(timeoutMs > 0))
|
|
84
|
+
return p;
|
|
85
|
+
const startedAt = Date.now();
|
|
86
|
+
// 在**工具 settle 的那个微任务里**记时间,而不是 `await` 恢复之后再记:
|
|
87
|
+
// 微任务紧跟在 resolve 它的那次回调之后跑,所以这个时刻最贴近「工具真正完成」。
|
|
88
|
+
// 若改成 await 恢复后再测,宿主在「工具完成 → 恢复」之间被饿住会把按时完成的工具误判成超时。
|
|
89
|
+
let settledAt = 0;
|
|
90
|
+
const tracked = p.then((v) => {
|
|
91
|
+
settledAt = Date.now();
|
|
92
|
+
return v;
|
|
93
|
+
}, (e) => {
|
|
94
|
+
settledAt = Date.now();
|
|
95
|
+
throw e;
|
|
96
|
+
});
|
|
97
|
+
let timer;
|
|
98
|
+
try {
|
|
99
|
+
const out = await Promise.race([
|
|
100
|
+
tracked,
|
|
101
|
+
new Promise((resolve) => {
|
|
102
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
103
|
+
// ⚠️ 不 unref:它的触发是「这个 await 得以结束」的条件(详见函数头注释的实测表)。
|
|
104
|
+
// unref 过它 ⇒ 空事件循环下进程先退出,await 永不 settle。
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
// 竞速只当快路径;最终以实测耗时兜底判定(见上面注释里的翻转样本)。
|
|
108
|
+
if (out === TIMED_OUT)
|
|
109
|
+
return TIMED_OUT;
|
|
110
|
+
return settledAt - startedAt >= timeoutMs ? TIMED_OUT : out;
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
if (timer)
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/core/timeout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sCAAsC;AACtC,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAErD;;;;;;;;;;GAUG;AACH,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,GAAc,SAAS,CAAC;IACrC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;IAC7B,CAAC;CACF;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,CAAU;IACvC,IAAI,CAAC,YAAY,YAAY;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,CAAC,GAAG,CAAuC,CAAC;IAClD,OAAO,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,cAAc,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,CAAa,EACb,SAAiB;IAEjB,IAAI,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7B,gDAAgD;IAChD,8CAA8C;IAC9C,qDAAqD;IACrD,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,MAAM,OAAO,GAAG,CAAC,CAAC,IAAI,CACpB,CAAC,CAAC,EAAE,EAAE;QACJ,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,OAAO,CAAC,CAAC;IACX,CAAC,EACD,CAAC,CAAC,EAAE,EAAE;QACJ,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,CAAC,CAAC;IACV,CAAC,CACF,CAAC;IACF,IAAI,KAAiC,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YAC7B,OAAO;YACP,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,EAAE;gBACxC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,CAAC;gBACxD,mDAAmD;gBACnD,0CAA0C;YAC5C,CAAC,CAAC;SACH,CAAC,CAAC;QACH,oCAAoC;QACpC,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACxC,OAAO,SAAS,GAAG,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9D,CAAC;YAAS,CAAC;QACT,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;AACH,CAAC"}
|
package/dist/core/tool.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { Message, MessageParam, TextBlockParam, ToolParam } from './message.js';
|
|
2
2
|
import type { SpanError, SpanId, SpanKind, SpanStatus, Trace, Usage } from './trace.js';
|
|
3
3
|
/**
|
|
4
4
|
* Agentia —— 工具定义。
|
|
@@ -56,17 +56,18 @@ export interface RecorderBackend {
|
|
|
56
56
|
snapshot(status: SpanStatus): Trace;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* engine 对模型端的最小结构面(R4
|
|
60
|
-
*
|
|
59
|
+
* engine 对模型端的最小结构面(R4 多模型):消息形态由 core/message.js 的自有类型族
|
|
60
|
+
* 定义(与 Anthropic Messages API 逐字对齐),其他 provider(OpenAI 兼容端点等)
|
|
61
|
+
* 只需适配出同一形态。
|
|
61
62
|
*/
|
|
62
63
|
export interface ModelClient {
|
|
63
64
|
messages: {
|
|
64
65
|
stream(params: {
|
|
65
66
|
model: string;
|
|
66
67
|
max_tokens: number;
|
|
67
|
-
system?: string |
|
|
68
|
-
tools?:
|
|
69
|
-
messages:
|
|
68
|
+
system?: string | TextBlockParam[];
|
|
69
|
+
tools?: ToolParam[];
|
|
70
|
+
messages: MessageParam[];
|
|
70
71
|
/**
|
|
71
72
|
* 中断信号(可选):实现须转发给底层请求,否则调用方无法中止在飞 run。
|
|
72
73
|
*
|
|
@@ -74,14 +75,14 @@ export interface ModelClient {
|
|
|
74
75
|
* SDK 把「传输层的 signal」放在**请求选项**里(如 `@anthropic-ai/sdk` 的
|
|
75
76
|
* `stream(body, options?)` —— `signal` 只在 `RequestOptions` 里认)。若把本 params 原样
|
|
76
77
|
* 交给 SDK,signal 会被**静默丢弃**,中止失效且**没有任何报错**。
|
|
77
|
-
* 参考实现:`integrations/anthropic.ts
|
|
78
|
-
* 以及 `integrations/openai.ts
|
|
78
|
+
* 参考实现:`integrations/anthropic.ts`(手写 fetch,signal 摘出 body 直接传给 fetch),
|
|
79
|
+
* 以及 `integrations/openai.ts`(同款)。
|
|
79
80
|
* 门禁:`tests/integrations/anthropic.test.ts`(零 key 本地假端点,abort 后必须断开)。
|
|
80
81
|
*/
|
|
81
82
|
signal?: AbortSignal;
|
|
82
83
|
}): {
|
|
83
84
|
on(event: 'text', cb: (delta: string) => void): void;
|
|
84
|
-
finalMessage(): Promise<
|
|
85
|
+
finalMessage(): Promise<Message>;
|
|
85
86
|
};
|
|
86
87
|
};
|
|
87
88
|
}
|
|
@@ -116,6 +117,17 @@ export interface ToolRunContext {
|
|
|
116
117
|
* 模型在子循环里会退化成"未定价"(成本恒 0,`maxCostUsd` 静默失效)。
|
|
117
118
|
*/
|
|
118
119
|
priceOverrides?: Record<string, ModelPricing>;
|
|
120
|
+
/**
|
|
121
|
+
* 本次工具调用的**引擎侧预算**(毫秒)= 本次 run 的 `RunAgentOptions.toolTimeoutMs`;
|
|
122
|
+
* undefined / 非正 = 引擎不设超时。
|
|
123
|
+
*
|
|
124
|
+
* 存在的意义是**划定裁判权**:工具(尤其带自己计时器的桥,见 `integrations/mcp.ts`)
|
|
125
|
+
* 据此知道「这次调用的超时由引擎判」,从而不再启动第二个计时器 —— 两个计时器判同一件事,
|
|
126
|
+
* 只会得到两种账(桥那份曾被记成 `error(unknown)` + `errorKind=threw`,见 spec §10 2026-09-17 ①)。
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ 它是本次调用的**配置值**,不是「剩余时间」。
|
|
129
|
+
*/
|
|
130
|
+
toolTimeoutMs?: number;
|
|
119
131
|
/**
|
|
120
132
|
* trace 事件正文的截断上限(见 `RunAgentOptions.maxEventChars`)。嵌套能力
|
|
121
133
|
* (@SubAgent / @Skill)拉起自己的 llm 循环时必须原样传下去,否则**同一棵调用树
|
|
@@ -8,36 +8,12 @@
|
|
|
8
8
|
* `limit` 非正数或非有限值一律视为**不限**(= 旧行为 `Promise.all`)。
|
|
9
9
|
*/
|
|
10
10
|
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
11
|
-
/** 超时哨兵:区分「超时」与「工具恰好返回了 undefined」 */
|
|
12
|
-
export declare const TIMED_OUT: unique symbol;
|
|
13
11
|
/**
|
|
14
|
-
*
|
|
12
|
+
* 超时原语(`TIMED_OUT` 哨兵 + `withTimeout`)已**下沉到 `core/timeout.ts`**:
|
|
13
|
+
* 它与 `integrations/mcp.ts` 的 MCP 调用超时共用同一实现 —— 此前是两份,而
|
|
14
|
+
* 「超时是硬的」那次收紧只落进了这里,桥那份留在竞速判定上(见 `docs/spec.md` §10 2026-09-17 ①)。
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* 想真停下来的工具请自行读 `ToolRunContext.signal`(框架传了,但**不强制**工具中断
|
|
19
|
-
* —— 见 core/tool.ts 的说明:工具副作用无法回滚)。
|
|
20
|
-
*
|
|
21
|
-
* **超时是硬的**(2026-09-14 收紧,见 `docs/spec.md` §10):判定不看竞速结果,而看**实测耗时**。
|
|
22
|
-
* 原因:`Promise.race` 不是硬保证 —— 两个计时器在同一毫秒内建、又因事件循环被饿住而同批到期时,
|
|
23
|
-
* 列表顺序决定谁先 resolve,**超预算的工具能赢过截止计时器**(实测 `60ms 工具 vs 20ms 预算`
|
|
24
|
-
* 在 8 倍 CPU 超订下 1200 次翻转 1 次,记成 `ok: true`,护栏静默失效)。
|
|
25
|
-
* 代价是语义收紧:`21ms 完成 / 20ms 预算` 由「成功」变「超时」—— 这正确,
|
|
26
|
-
* 「预算」本来就是对**实际耗时**的承诺,不是对调度运气的承诺。
|
|
27
|
-
*
|
|
28
|
-
* `timeoutMs` 非正数 = 不设超时(直接返回原 promise)。
|
|
29
|
-
*
|
|
30
|
-
* ⚠️ **截止计时器绝不可 `unref()`**(2026-09-14 修正,见 `docs/spec.md` §10 ④):
|
|
31
|
-
* 这个计时器的**触发本身就是「被 await 的 promise 得以 settle」的条件**。一旦 unref,
|
|
32
|
-
* 当它是事件循环里唯一的把手时,进程会在它触发前直接退出 —— `await` **永不 settle**。
|
|
33
|
-
* 实测(空事件循环、挂死的 promise,Node 22 与 26 一个样):
|
|
34
|
-
*
|
|
35
|
-
* | 计时器 | 结果 |
|
|
36
|
-
* |---|---|
|
|
37
|
-
* | `unref()` | 进程退出(顶层 await 未 settle),**什么都没返回** |
|
|
38
|
-
* | 不 unref | `settled: TIMED_OUT`,exit 0 |
|
|
39
|
-
*
|
|
40
|
-
* 「兜底计时器不该让宿主为它续命」这条理由对**没人 await 的兜底 tick**(scheduler 的下一拍、
|
|
41
|
-
* SSE 心跳、metrics 刷盘)成立,对**「等待的终点」**不成立:那正是调用方在等的东西。
|
|
16
|
+
* 这里**原样再导出**:`engine/turn.ts` 与既有测试的 import 路径与名字保持不变,
|
|
17
|
+
* 单源化对它们零影响。
|
|
42
18
|
*/
|
|
43
|
-
export
|
|
19
|
+
export { TIMED_OUT, withTimeout } from '../core/timeout.js';
|
|
@@ -25,71 +25,13 @@ export async function mapWithConcurrency(items, limit, fn) {
|
|
|
25
25
|
await Promise.all(Array.from({ length: width }, worker));
|
|
26
26
|
return results;
|
|
27
27
|
}
|
|
28
|
-
/** 超时哨兵:区分「超时」与「工具恰好返回了 undefined」 */
|
|
29
|
-
export const TIMED_OUT = Symbol('agentia.timed-out');
|
|
30
28
|
/**
|
|
31
|
-
*
|
|
29
|
+
* 超时原语(`TIMED_OUT` 哨兵 + `withTimeout`)已**下沉到 `core/timeout.ts`**:
|
|
30
|
+
* 它与 `integrations/mcp.ts` 的 MCP 调用超时共用同一实现 —— 此前是两份,而
|
|
31
|
+
* 「超时是硬的」那次收紧只落进了这里,桥那份留在竞速判定上(见 `docs/spec.md` §10 2026-09-17 ①)。
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* 想真停下来的工具请自行读 `ToolRunContext.signal`(框架传了,但**不强制**工具中断
|
|
36
|
-
* —— 见 core/tool.ts 的说明:工具副作用无法回滚)。
|
|
37
|
-
*
|
|
38
|
-
* **超时是硬的**(2026-09-14 收紧,见 `docs/spec.md` §10):判定不看竞速结果,而看**实测耗时**。
|
|
39
|
-
* 原因:`Promise.race` 不是硬保证 —— 两个计时器在同一毫秒内建、又因事件循环被饿住而同批到期时,
|
|
40
|
-
* 列表顺序决定谁先 resolve,**超预算的工具能赢过截止计时器**(实测 `60ms 工具 vs 20ms 预算`
|
|
41
|
-
* 在 8 倍 CPU 超订下 1200 次翻转 1 次,记成 `ok: true`,护栏静默失效)。
|
|
42
|
-
* 代价是语义收紧:`21ms 完成 / 20ms 预算` 由「成功」变「超时」—— 这正确,
|
|
43
|
-
* 「预算」本来就是对**实际耗时**的承诺,不是对调度运气的承诺。
|
|
44
|
-
*
|
|
45
|
-
* `timeoutMs` 非正数 = 不设超时(直接返回原 promise)。
|
|
46
|
-
*
|
|
47
|
-
* ⚠️ **截止计时器绝不可 `unref()`**(2026-09-14 修正,见 `docs/spec.md` §10 ④):
|
|
48
|
-
* 这个计时器的**触发本身就是「被 await 的 promise 得以 settle」的条件**。一旦 unref,
|
|
49
|
-
* 当它是事件循环里唯一的把手时,进程会在它触发前直接退出 —— `await` **永不 settle**。
|
|
50
|
-
* 实测(空事件循环、挂死的 promise,Node 22 与 26 一个样):
|
|
51
|
-
*
|
|
52
|
-
* | 计时器 | 结果 |
|
|
53
|
-
* |---|---|
|
|
54
|
-
* | `unref()` | 进程退出(顶层 await 未 settle),**什么都没返回** |
|
|
55
|
-
* | 不 unref | `settled: TIMED_OUT`,exit 0 |
|
|
56
|
-
*
|
|
57
|
-
* 「兜底计时器不该让宿主为它续命」这条理由对**没人 await 的兜底 tick**(scheduler 的下一拍、
|
|
58
|
-
* SSE 心跳、metrics 刷盘)成立,对**「等待的终点」**不成立:那正是调用方在等的东西。
|
|
33
|
+
* 这里**原样再导出**:`engine/turn.ts` 与既有测试的 import 路径与名字保持不变,
|
|
34
|
+
* 单源化对它们零影响。
|
|
59
35
|
*/
|
|
60
|
-
export
|
|
61
|
-
if (!(timeoutMs > 0))
|
|
62
|
-
return p;
|
|
63
|
-
const startedAt = Date.now();
|
|
64
|
-
// 在**工具 settle 的那个微任务里**记时间,而不是 `await` 恢复之后再记:
|
|
65
|
-
// 微任务紧跟在 resolve 它的那次回调之后跑,所以这个时刻最贴近「工具真正完成」。
|
|
66
|
-
// 若改成 await 恢复后再测,宿主在「工具完成 → 恢复」之间被饿住会把按时完成的工具误判成超时。
|
|
67
|
-
let settledAt = 0;
|
|
68
|
-
const tracked = p.then((v) => {
|
|
69
|
-
settledAt = Date.now();
|
|
70
|
-
return v;
|
|
71
|
-
}, (e) => {
|
|
72
|
-
settledAt = Date.now();
|
|
73
|
-
throw e;
|
|
74
|
-
});
|
|
75
|
-
let timer;
|
|
76
|
-
try {
|
|
77
|
-
const out = await Promise.race([
|
|
78
|
-
tracked,
|
|
79
|
-
new Promise((resolve) => {
|
|
80
|
-
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
81
|
-
// ⚠️ 不 unref:它的触发是「这个 await 得以结束」的条件(详见函数头注释的实测表)。
|
|
82
|
-
// unref 过它 ⇒ 空事件循环下进程先退出,await 永不 settle。
|
|
83
|
-
}),
|
|
84
|
-
]);
|
|
85
|
-
// 竞速只当快路径;最终以实测耗时兜底判定(见上面注释里的翻转样本)。
|
|
86
|
-
if (out === TIMED_OUT)
|
|
87
|
-
return TIMED_OUT;
|
|
88
|
-
return settledAt - startedAt >= timeoutMs ? TIMED_OUT : out;
|
|
89
|
-
}
|
|
90
|
-
finally {
|
|
91
|
-
if (timer)
|
|
92
|
-
clearTimeout(timer);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
36
|
+
export { TIMED_OUT, withTimeout } from '../core/timeout.js';
|
|
95
37
|
//# sourceMappingURL=concurrency.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../src/engine/concurrency.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAmB,EACnB,KAAa,EACb,EAA0C;IAE1C,MAAM,OAAO,GAAG,IAAI,KAAK,CAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACvC,MAAM,KAAK,GACT,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;IACjG,oDAAoD;IACpD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,SAAS,CAAC;YACR,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;YACjB,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO;YAC9B,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACzD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../../src/engine/concurrency.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAAmB,EACnB,KAAa,EACb,EAA0C;IAE1C,MAAM,OAAO,GAAG,IAAI,KAAK,CAAI,KAAK,CAAC,MAAM,CAAC,CAAC;IAC3C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IACvC,MAAM,KAAK,GACT,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;IACjG,oDAAoD;IACpD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAG,KAAK,IAAmB,EAAE;QACvC,SAAS,CAAC;YACR,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC;YACjB,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO;YAC9B,OAAO,CAAC,CAAC,CAAC,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC;IACH,CAAC,CAAC;IACF,MAAM,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC;IACzD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/engine/errors.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import type { SpanError } from '../core/trace.js';
|
|
2
|
+
/**
|
|
3
|
+
* Agentia —— 异常分类(engine 对厂商 SDK 零运行时 import)。
|
|
4
|
+
*
|
|
5
|
+
* 分类是**鸭子类型**:默认 client(`integrations/anthropic.ts`)自研化后抛的是
|
|
6
|
+
* 带数值 `status` 的 `AnthropicApiError` 与 fetch 的原生网络错误(TypeError 带 cause),
|
|
7
|
+
* 不再是 SDK 错误类。同套判法对第三方 SDK 错误同样适用(它们的错误类也带 `status`)。
|
|
8
|
+
*
|
|
9
|
+
* 历史教训(2026-09-13 实测,当时因此保留 instanceof):厂商 SDK 的错误类
|
|
10
|
+
* `name` 恒为 `'Error'`、`type` 为 null,鸭子类型只能靠 `constructor.name` ——
|
|
11
|
+
* **压缩即失效**。所以这里只认**数据属性**(`status` 数值 / `cause` / errno `code` /
|
|
12
|
+
* 内建 DOMException 的 `name`),一概不碰构造函数名。
|
|
13
|
+
*/
|
|
2
14
|
/** 是否中断异常(DOMException / 普通 Error 都可能是,靠 name 判定) */
|
|
3
15
|
export declare function isAbortError(e: unknown): boolean;
|
|
4
16
|
/** 把任意异常分类成 trace 可用的 SpanError(可重试 vs 不可重试)。 */
|