@migor/agentia 0.6.0 → 0.6.2
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 +343 -0
- package/README.md +1 -2
- package/dist/AGENTS.md +256 -191
- package/dist/core/abort.js +4 -1
- package/dist/core/abort.js.map +1 -1
- package/dist/core/sse.d.ts +14 -0
- package/dist/core/sse.js +45 -0
- package/dist/core/sse.js.map +1 -0
- package/dist/core/stats.d.ts +18 -0
- package/dist/core/stats.js +24 -0
- package/dist/core/stats.js.map +1 -0
- package/dist/core/text.d.ts +19 -0
- package/dist/core/text.js +8 -0
- package/dist/core/text.js.map +1 -0
- package/dist/core/timeout.d.ts +92 -0
- package/dist/core/timeout.js +157 -0
- package/dist/core/timeout.js.map +1 -0
- package/dist/core/tool.d.ts +11 -0
- package/dist/core/trace.d.ts +54 -0
- package/dist/core/trace.js +43 -0
- package/dist/core/trace.js.map +1 -1
- package/dist/engine/concurrency.d.ts +6 -30
- package/dist/engine/concurrency.js +14 -65
- package/dist/engine/concurrency.js.map +1 -1
- package/dist/engine/errors.js +14 -4
- package/dist/engine/errors.js.map +1 -1
- package/dist/engine/loop.js +16 -1
- package/dist/engine/loop.js.map +1 -1
- package/dist/engine/replay.js +4 -1
- package/dist/engine/replay.js.map +1 -1
- package/dist/engine/retry.d.ts +17 -2
- package/dist/engine/retry.js +41 -22
- package/dist/engine/retry.js.map +1 -1
- package/dist/engine/spec.d.ts +22 -0
- package/dist/engine/spec.js +19 -4
- package/dist/engine/spec.js.map +1 -1
- package/dist/engine/tracer.d.ts +6 -1
- package/dist/engine/tracer.js +17 -2
- package/dist/engine/tracer.js.map +1 -1
- package/dist/engine/trimming.d.ts +3 -1
- package/dist/engine/trimming.js +35 -3
- package/dist/engine/trimming.js.map +1 -1
- package/dist/engine/turn.d.ts +8 -0
- package/dist/engine/turn.js +39 -18
- package/dist/engine/turn.js.map +1 -1
- package/dist/engine/types.d.ts +7 -1
- package/dist/engine/usage.js +4 -1
- package/dist/engine/usage.js.map +1 -1
- package/dist/eval/defineEval.d.ts +3 -0
- package/dist/eval/defineEval.js +42 -12
- package/dist/eval/defineEval.js.map +1 -1
- package/dist/eval/harvest.d.ts +0 -7
- package/dist/eval/harvest.js +20 -9
- package/dist/eval/harvest.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/anthropic.js +35 -66
- package/dist/integrations/anthropic.js.map +1 -1
- package/dist/integrations/mcp.d.ts +13 -4
- package/dist/integrations/mcp.js +31 -24
- package/dist/integrations/mcp.js.map +1 -1
- package/dist/integrations/metrics.d.ts +35 -5
- package/dist/integrations/metrics.js +101 -39
- package/dist/integrations/metrics.js.map +1 -1
- package/dist/integrations/openai.d.ts +13 -0
- package/dist/integrations/openai.js +62 -49
- package/dist/integrations/openai.js.map +1 -1
- package/dist/integrations/otlp.js +41 -11
- package/dist/integrations/otlp.js.map +1 -1
- package/dist/integrations/report.js +2 -14
- package/dist/integrations/report.js.map +1 -1
- package/dist/runtime/run.d.ts +21 -1
- package/dist/runtime/run.js +17 -0
- package/dist/runtime/run.js.map +1 -1
- package/dist/runtime/session.js +5 -1
- package/dist/runtime/session.js.map +1 -1
- package/dist/toolkit/env.js +43 -7
- package/dist/toolkit/env.js.map +1 -1
- package/dist/toolkit/module.d.ts +24 -1
- package/dist/toolkit/module.js +12 -0
- package/dist/toolkit/module.js.map +1 -1
- package/dist/toolkit/prompt.js +18 -4
- package/dist/toolkit/prompt.js.map +1 -1
- package/dist/toolkit/skill.js +3 -0
- package/dist/toolkit/skill.js.map +1 -1
- package/dist/toolkit/subagent.js +7 -0
- package/dist/toolkit/subagent.js.map +1 -1
- package/dist/transport/async.d.ts +3 -0
- package/dist/transport/async.js +28 -2
- package/dist/transport/async.js.map +1 -1
- package/dist/transport/http.js +58 -7
- package/dist/transport/http.js.map +1 -1
- package/package.json +3 -2
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,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— SSE 逐行分帧(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* Anthropic 与 OpenAI 兼容端点的事件流都是「一行一条 `data:`」(不需要处理跨行
|
|
5
|
+
* payload),两个适配器此前各写了一份**逐字相同**的 `sseLines`。合并的意义不止整洁:
|
|
6
|
+
* `finally` 里的 `reader.cancel()` 是「提前 break(abort / `[DONE]`)时底层流仍被回收」
|
|
7
|
+
* 的唯一保证 —— 这份代码在两处各存一份,漏改一处就是一处泄漏(读锁不释放,连接挂着)。
|
|
8
|
+
*
|
|
9
|
+
* 只做分帧,不解释协议:空行照常 yield,是不是 `data:` 前缀、要不要认 `[DONE]`
|
|
10
|
+
* 全由调用方判。
|
|
11
|
+
*
|
|
12
|
+
* core 是叶子层:本文件零 import(只用全局的 `ReadableStream` / `TextDecoder`)。
|
|
13
|
+
*/
|
|
14
|
+
export declare function sseLines(body: ReadableStream<Uint8Array>): AsyncGenerator<string>;
|
package/dist/core/sse.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— SSE 逐行分帧(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* Anthropic 与 OpenAI 兼容端点的事件流都是「一行一条 `data:`」(不需要处理跨行
|
|
5
|
+
* payload),两个适配器此前各写了一份**逐字相同**的 `sseLines`。合并的意义不止整洁:
|
|
6
|
+
* `finally` 里的 `reader.cancel()` 是「提前 break(abort / `[DONE]`)时底层流仍被回收」
|
|
7
|
+
* 的唯一保证 —— 这份代码在两处各存一份,漏改一处就是一处泄漏(读锁不释放,连接挂着)。
|
|
8
|
+
*
|
|
9
|
+
* 只做分帧,不解释协议:空行照常 yield,是不是 `data:` 前缀、要不要认 `[DONE]`
|
|
10
|
+
* 全由调用方判。
|
|
11
|
+
*
|
|
12
|
+
* core 是叶子层:本文件零 import(只用全局的 `ReadableStream` / `TextDecoder`)。
|
|
13
|
+
*/
|
|
14
|
+
export async function* sseLines(body) {
|
|
15
|
+
const reader = body.getReader();
|
|
16
|
+
const decoder = new TextDecoder();
|
|
17
|
+
let buf = '';
|
|
18
|
+
try {
|
|
19
|
+
for (;;) {
|
|
20
|
+
const { done, value } = await reader.read();
|
|
21
|
+
if (done)
|
|
22
|
+
break;
|
|
23
|
+
buf += decoder.decode(value, { stream: true });
|
|
24
|
+
let idx = buf.indexOf('\n');
|
|
25
|
+
while (idx >= 0) {
|
|
26
|
+
yield buf.slice(0, idx).replace(/\r$/, '');
|
|
27
|
+
buf = buf.slice(idx + 1);
|
|
28
|
+
idx = buf.indexOf('\n');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 末尾没有换行收尾的残行也要交出去(不是所有端点都补 \n)
|
|
32
|
+
if (buf)
|
|
33
|
+
yield buf.replace(/\r$/, '');
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
// 提前 break(abort / [DONE])时释放读锁,否则流不会被回收
|
|
37
|
+
try {
|
|
38
|
+
await reader.cancel();
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
/* 已结束/已取消 */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../../src/core/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,QAAQ,CAAC,IAAgC;IAC9D,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,CAAC;QACH,SAAS,CAAC;YACR,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,IAAI;gBAAE,MAAM;YAChB,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YAC/C,IAAI,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC5B,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;gBAChB,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBAC3C,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;gBACzB,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;QACD,gCAAgC;QAChC,IAAI,GAAG;YAAE,MAAM,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACxC,CAAC;YAAS,CAAC;QACT,yCAAyC;QACzC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,MAAM,EAAE,CAAC;QACxB,CAAC;QAAC,MAAM,CAAC;YACP,aAAa;QACf,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 统计小工具(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* `percentile` 此前在 `integrations/metrics.ts`(直方图窗口的分位,导出给看板)与
|
|
5
|
+
* `integrations/report.ts`(时长报告的 p50/p95)各有一份。两处口径**必须**一致
|
|
6
|
+
* (都取 Prometheus 语义的「最近 rank」),分开演化就会出现「看板 p95 ≠ 报告 p95」——
|
|
7
|
+
* 同一条 trace、两个数,用户无从判断该信哪个。
|
|
8
|
+
*
|
|
9
|
+
* 入参是**已排序**数组:排序成本留给调用方决定 —— metrics 每次要复制滑动窗口再排,
|
|
10
|
+
* report 只排一次复用给 p50/p95。本函数不替它们做这个选择。
|
|
11
|
+
*
|
|
12
|
+
* core 是叶子层:本文件零 import。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 分位数:取 `ceil(q · n)` 处的样本(Prometheus 的 quantile 语义 —— 不是线性插值)。
|
|
16
|
+
* 空数组返回 0;`rank` 钳到 `[1, n]`,所以 `q=0` 也返回最小样本,不会越界读 `-1`。
|
|
17
|
+
*/
|
|
18
|
+
export declare function percentile(sorted: readonly number[], q: number): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 统计小工具(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* `percentile` 此前在 `integrations/metrics.ts`(直方图窗口的分位,导出给看板)与
|
|
5
|
+
* `integrations/report.ts`(时长报告的 p50/p95)各有一份。两处口径**必须**一致
|
|
6
|
+
* (都取 Prometheus 语义的「最近 rank」),分开演化就会出现「看板 p95 ≠ 报告 p95」——
|
|
7
|
+
* 同一条 trace、两个数,用户无从判断该信哪个。
|
|
8
|
+
*
|
|
9
|
+
* 入参是**已排序**数组:排序成本留给调用方决定 —— metrics 每次要复制滑动窗口再排,
|
|
10
|
+
* report 只排一次复用给 p50/p95。本函数不替它们做这个选择。
|
|
11
|
+
*
|
|
12
|
+
* core 是叶子层:本文件零 import。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 分位数:取 `ceil(q · n)` 处的样本(Prometheus 的 quantile 语义 —— 不是线性插值)。
|
|
16
|
+
* 空数组返回 0;`rank` 钳到 `[1, n]`,所以 `q=0` 也返回最小样本,不会越界读 `-1`。
|
|
17
|
+
*/
|
|
18
|
+
export function percentile(sorted, q) {
|
|
19
|
+
if (sorted.length === 0)
|
|
20
|
+
return 0;
|
|
21
|
+
const rank = Math.ceil(q * sorted.length);
|
|
22
|
+
return sorted[Math.min(Math.max(rank, 1), sorted.length) - 1];
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=stats.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/core/stats.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAyB,EAAE,CAAS;IAC7D,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAE,CAAC;AACjE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— 消息文本读取(**单源**)。
|
|
3
|
+
*
|
|
4
|
+
* 合并三处同形实现:`integrations/anthropic.ts` 与 `integrations/openai.ts` 的
|
|
5
|
+
* 「非流式回落路径」各有一份 `join('')`,`engine/turn.ts` 有一份 `join('\n')`。
|
|
6
|
+
* 三份长得一样、只差一个连接符 —— 这不是巧合,是同一个语义在三处各自推导了一遍,
|
|
7
|
+
* 任何后续修正(例如「空文本块不该产生分隔符」)都只会落到其中一处。
|
|
8
|
+
*
|
|
9
|
+
* 为什么不放进 `core/message.ts`:那个文件**刻意**是「零依赖纯类型文件」
|
|
10
|
+
* (见其头注:编译后全部擦除、import 它零运行时成本)。为一个 8 行函数破掉这条性质
|
|
11
|
+
* 不划算 —— 类型在 `message.ts`、取值函数在这儿,两边各守一个职责。
|
|
12
|
+
*
|
|
13
|
+
* `separator` 是**调用方语义**,不是格式细节:
|
|
14
|
+
* - `''`(适配器):回落的原文本来就是一整段,拼回去要与流式累积的文本逐字一致;
|
|
15
|
+
* - `'\n'`(引擎):多文本块是模型分段的输出,拼成 `finalText` 要保住分段。
|
|
16
|
+
*/
|
|
17
|
+
import type { Message } from './message.js';
|
|
18
|
+
/** 取消息里的全部文本块,按 `separator` 连接(无文本块 → 空串) */
|
|
19
|
+
export declare function textOf(message: Pick<Message, 'content'>, separator: string): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/core/text.ts"],"names":[],"mappings":"AAkBA,6CAA6C;AAC7C,MAAM,UAAU,MAAM,CAAC,OAAiC,EAAE,SAAiB;IACzE,OAAO,OAAO,CAAC,OAAO;SACnB,MAAM,CAAC,CAAC,CAAC,EAAkB,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;SAChD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,IAAI,CAAC,SAAS,CAAC,CAAC;AACrB,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
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>;
|
|
73
|
+
/**
|
|
74
|
+
* 可被 signal 中断的 sleep(**单源**)。
|
|
75
|
+
*
|
|
76
|
+
* 合并两处逐字相同的实现:`engine/retry.ts` 的 `sleep`(重试退避期间收到取消就
|
|
77
|
+
* 不必再等)与 `integrations/anthropic.ts` 的 `interruptibleSleep`(client 层退避)。
|
|
78
|
+
* 那边此前自带一段「与 engine/retry.ts 的 sleep 同语义但不复用它 —— 分层约束:
|
|
79
|
+
* integrations 只能依赖 core」的注解:**正是那条约束把两份代码逼成了重复**,
|
|
80
|
+
* 所以下沉到 core 是让它们合一的唯一合法落点(见 spec §10 2026-09-17 ① 的单源化口径)。
|
|
81
|
+
*
|
|
82
|
+
* ⚠️ 只合并这个 sleep,**不合并退避计算器**:engine 是 ±20% 均匀抖动、client 是
|
|
83
|
+
* ±25% 且额外尊重 `retry-after`。函数体相同、策略不同 —— 合策略会真改行为。
|
|
84
|
+
*
|
|
85
|
+
* `abortMessage` 参数化而非统一:文案是**调用方语境**(引擎说「run 已被取消」,
|
|
86
|
+
* client 说「请求已被取消」),两者都会出现在用户眼前的报错里。为去重把两句话
|
|
87
|
+
* 改成一句,是拿可读性换整洁度。
|
|
88
|
+
*
|
|
89
|
+
* 抛的是 `name === 'AbortError'` 的错误(而非 `TimeoutError`):取消不是超时,
|
|
90
|
+
* `engine/errors.ts` 按 `name` 把它归进「已中止」,`loop` 据此以 `aborted` 收尾。
|
|
91
|
+
*/
|
|
92
|
+
export declare function interruptibleSleep(ms: number, signal?: AbortSignal, abortMessage?: string): Promise<void>;
|
|
@@ -0,0 +1,157 @@
|
|
|
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
|
+
/**
|
|
118
|
+
* 可被 signal 中断的 sleep(**单源**)。
|
|
119
|
+
*
|
|
120
|
+
* 合并两处逐字相同的实现:`engine/retry.ts` 的 `sleep`(重试退避期间收到取消就
|
|
121
|
+
* 不必再等)与 `integrations/anthropic.ts` 的 `interruptibleSleep`(client 层退避)。
|
|
122
|
+
* 那边此前自带一段「与 engine/retry.ts 的 sleep 同语义但不复用它 —— 分层约束:
|
|
123
|
+
* integrations 只能依赖 core」的注解:**正是那条约束把两份代码逼成了重复**,
|
|
124
|
+
* 所以下沉到 core 是让它们合一的唯一合法落点(见 spec §10 2026-09-17 ① 的单源化口径)。
|
|
125
|
+
*
|
|
126
|
+
* ⚠️ 只合并这个 sleep,**不合并退避计算器**:engine 是 ±20% 均匀抖动、client 是
|
|
127
|
+
* ±25% 且额外尊重 `retry-after`。函数体相同、策略不同 —— 合策略会真改行为。
|
|
128
|
+
*
|
|
129
|
+
* `abortMessage` 参数化而非统一:文案是**调用方语境**(引擎说「run 已被取消」,
|
|
130
|
+
* client 说「请求已被取消」),两者都会出现在用户眼前的报错里。为去重把两句话
|
|
131
|
+
* 改成一句,是拿可读性换整洁度。
|
|
132
|
+
*
|
|
133
|
+
* 抛的是 `name === 'AbortError'` 的错误(而非 `TimeoutError`):取消不是超时,
|
|
134
|
+
* `engine/errors.ts` 按 `name` 把它归进「已中止」,`loop` 据此以 `aborted` 收尾。
|
|
135
|
+
*/
|
|
136
|
+
export function interruptibleSleep(ms, signal, abortMessage = '已被取消') {
|
|
137
|
+
if (ms <= 0)
|
|
138
|
+
return Promise.resolve();
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const abortError = () => Object.assign(new Error(abortMessage), { name: 'AbortError' });
|
|
141
|
+
// 已中止:立即 reject(此处 timer 尚未创建,绝不能去 clear)
|
|
142
|
+
if (signal?.aborted) {
|
|
143
|
+
reject(abortError());
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
function onAbort() {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
reject(abortError());
|
|
149
|
+
}
|
|
150
|
+
const timer = setTimeout(() => {
|
|
151
|
+
signal?.removeEventListener('abort', onAbort);
|
|
152
|
+
resolve();
|
|
153
|
+
}, ms);
|
|
154
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../src/core/timeout.ts"],"names":[],"mappings":"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"}
|
package/dist/core/tool.d.ts
CHANGED
|
@@ -117,6 +117,17 @@ export interface ToolRunContext {
|
|
|
117
117
|
* 模型在子循环里会退化成"未定价"(成本恒 0,`maxCostUsd` 静默失效)。
|
|
118
118
|
*/
|
|
119
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;
|
|
120
131
|
/**
|
|
121
132
|
* trace 事件正文的截断上限(见 `RunAgentOptions.maxEventChars`)。嵌套能力
|
|
122
133
|
* (@SubAgent / @Skill)拉起自己的 llm 循环时必须原样传下去,否则**同一棵调用树
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -35,6 +35,41 @@ export interface SpanEvent {
|
|
|
35
35
|
name: string;
|
|
36
36
|
body: unknown;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* 触发来源的链路上下文(跨进程 / 跨服务关联,spec §9.2)。
|
|
40
|
+
*
|
|
41
|
+
* 语义是**入站**的:调用方(HTTP 网关、队列消费者、上游服务)把它自己的 span 标识
|
|
42
|
+
* 传进来,本次 run 的根 span 会把它记成一条 `SpanLink` 指回去 —— 于是「这条 run 是被
|
|
43
|
+
* 谁触发的」在两个系统之间可查。
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ **不改变 `traceId == runId` 的 1:1 不变量**:run 仍是自己的一棵新树,上游只是被
|
|
46
|
+
* **链接**、不是被**继承**成父 span。所以一次 run 的调用树永远自洽(不依赖上游是否
|
|
47
|
+
* 还在、是否被采样掉),而因果关系仍然成立。
|
|
48
|
+
*/
|
|
49
|
+
export interface TraceContext {
|
|
50
|
+
/** 上游 trace id:32 位 hex(W3C traceparent)或 UUID(带 '-')两种形态都接受 */
|
|
51
|
+
traceId: string;
|
|
52
|
+
/** 上游 span id(16 位 hex);缺省表示只知道 trace 粒度、没有具体 span */
|
|
53
|
+
spanId?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 一条链路引用:指向**另一个** trace 里的 span(OTLP 的 span links 同语义)。
|
|
57
|
+
* v1 只在 run 根 span 上写入(来源是 `RunInvocationOptions.traceContext`)。
|
|
58
|
+
*/
|
|
59
|
+
export interface SpanLink {
|
|
60
|
+
traceId: TraceId;
|
|
61
|
+
spanId?: SpanId;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 解析 W3C `traceparent` 头(spec §9.2 的「header 语义」)。这是入站唯一的字符串形态
|
|
65
|
+
* —— 宿主从 HTTP 头 / 队列消息属性里取到它,直接交给 `RunInvocationOptions.traceContext`。
|
|
66
|
+
*
|
|
67
|
+
* **非法或缺失一律返回 `undefined`**(调用方据此当作「没有上游上下文」继续跑):
|
|
68
|
+
* 链路关联是观测行为,不该因为一个畸形头把业务请求打成 400。被拒的形态有:
|
|
69
|
+
* 版本 `ff`(W3C 保留为非法)、trace/span id 全零、位宽不符、大小写之外的畸形。
|
|
70
|
+
* 版本号非 `00` 时按 W3C「兼容未来」规则接受(只认前四段语义)。
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseTraceparent(value: string | null | undefined): TraceContext | undefined;
|
|
38
73
|
export interface Span {
|
|
39
74
|
spanId: SpanId;
|
|
40
75
|
traceId: TraceId;
|
|
@@ -45,6 +80,12 @@ export interface Span {
|
|
|
45
80
|
endedAt?: number;
|
|
46
81
|
status: SpanStatus;
|
|
47
82
|
error?: SpanError;
|
|
83
|
+
/**
|
|
84
|
+
* 跨 trace 的链路引用(v1 只出现在 run 根 span 上,见 `TraceContext`)。
|
|
85
|
+
* 缺席(而不是空数组)表示「没有上游上下文」—— 与 links 为空的 span 是同一件事,
|
|
86
|
+
* 不要据此区分「没传」和「传了但为空」。
|
|
87
|
+
*/
|
|
88
|
+
links?: SpanLink[];
|
|
48
89
|
/**
|
|
49
90
|
* usage —— 语义按 kind 区分:
|
|
50
91
|
* - `llm.turn`:该次模型往返的**自身计量**,是 Trace.totalUsage 的唯一来源;
|
|
@@ -83,6 +124,19 @@ export interface Score {
|
|
|
83
124
|
* 出口映射:OTLP 导出时译为 `gen_ai.evaluation.result` 事件,metricsSink 聚合为 score 指标族。
|
|
84
125
|
*/
|
|
85
126
|
export declare function attachScore(trace: Trace, score: Score): void;
|
|
127
|
+
/**
|
|
128
|
+
* 能力 span 的类型标签:`attributes.skill` 有值 → `'skill'`,`attributes.subagent` 有值
|
|
129
|
+
* → `'subagent'`,否则 `'capability'`(`SpanKind` 的第三个取值,也是 span 的 kind)。
|
|
130
|
+
*
|
|
131
|
+
* 放在本文件(而不是各出口里):`skill` / `subagent` 这两个 attribute 名是
|
|
132
|
+
* `toolkit/skill.ts`、`toolkit/subagent.ts` 与本类型之间的共同契约 —— 判定写在契约
|
|
133
|
+
* 定义处,两个出口(`metrics.ts` 的指标 label、`report.ts` 的能力排行)只消费结论。
|
|
134
|
+
* 此前两处各有一份同形实现,加起来是**同一件事的三个定义**。
|
|
135
|
+
*
|
|
136
|
+
* 返回 `string` 而非 `CapabilityType`:`'capability'` 不在 `CapabilityType`
|
|
137
|
+
* (那是「菜单四类能力」的口径)里,两者刻意不混。
|
|
138
|
+
*/
|
|
139
|
+
export declare function capabilityKindOf(span: Span): string;
|
|
86
140
|
/**
|
|
87
141
|
* trace 出口:run 收尾(成功或失败)后,框架把【完整 Trace】交给每个 sink。
|
|
88
142
|
* sink 抛错由框架吞掉,绝不影响 run 结果(与 memory 回写同款防护)。
|
package/dist/core/trace.js
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
* Agentia —— Trace / 调用树 类型草稿。
|
|
3
3
|
* 设计见 docs/spec.md §9。一次 run == 一条 trace(v1 中 traceId == runId)。
|
|
4
4
|
*/
|
|
5
|
+
/** W3C traceparent:`<2位版本>-<32位trace>-<16位span>-<2位flags>`,全小写 hex */
|
|
6
|
+
const TRACEPARENT_RE = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
7
|
+
/**
|
|
8
|
+
* 解析 W3C `traceparent` 头(spec §9.2 的「header 语义」)。这是入站唯一的字符串形态
|
|
9
|
+
* —— 宿主从 HTTP 头 / 队列消息属性里取到它,直接交给 `RunInvocationOptions.traceContext`。
|
|
10
|
+
*
|
|
11
|
+
* **非法或缺失一律返回 `undefined`**(调用方据此当作「没有上游上下文」继续跑):
|
|
12
|
+
* 链路关联是观测行为,不该因为一个畸形头把业务请求打成 400。被拒的形态有:
|
|
13
|
+
* 版本 `ff`(W3C 保留为非法)、trace/span id 全零、位宽不符、大小写之外的畸形。
|
|
14
|
+
* 版本号非 `00` 时按 W3C「兼容未来」规则接受(只认前四段语义)。
|
|
15
|
+
*/
|
|
16
|
+
export function parseTraceparent(value) {
|
|
17
|
+
if (!value)
|
|
18
|
+
return undefined;
|
|
19
|
+
const m = TRACEPARENT_RE.exec(value.trim().toLowerCase());
|
|
20
|
+
if (!m)
|
|
21
|
+
return undefined;
|
|
22
|
+
const [, version, traceId, spanId] = m;
|
|
23
|
+
if (version === 'ff')
|
|
24
|
+
return undefined;
|
|
25
|
+
if (/^0+$/.test(traceId) || /^0+$/.test(spanId))
|
|
26
|
+
return undefined;
|
|
27
|
+
return { traceId, spanId };
|
|
28
|
+
}
|
|
5
29
|
/**
|
|
6
30
|
* 把评分挂到 trace 根 span(一条 `score` 事件,body 即 Score)。
|
|
7
31
|
* trace 找不到根 span 时静默忽略(观测不击穿业务);多次调用即多条事件(不同维度各记各的)。
|
|
@@ -11,4 +35,23 @@ export function attachScore(trace, score) {
|
|
|
11
35
|
const root = trace.spans.find((s) => s.spanId === trace.rootSpanId);
|
|
12
36
|
root?.events.push({ time: Date.now(), name: 'score', body: score });
|
|
13
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* 能力 span 的类型标签:`attributes.skill` 有值 → `'skill'`,`attributes.subagent` 有值
|
|
40
|
+
* → `'subagent'`,否则 `'capability'`(`SpanKind` 的第三个取值,也是 span 的 kind)。
|
|
41
|
+
*
|
|
42
|
+
* 放在本文件(而不是各出口里):`skill` / `subagent` 这两个 attribute 名是
|
|
43
|
+
* `toolkit/skill.ts`、`toolkit/subagent.ts` 与本类型之间的共同契约 —— 判定写在契约
|
|
44
|
+
* 定义处,两个出口(`metrics.ts` 的指标 label、`report.ts` 的能力排行)只消费结论。
|
|
45
|
+
* 此前两处各有一份同形实现,加起来是**同一件事的三个定义**。
|
|
46
|
+
*
|
|
47
|
+
* 返回 `string` 而非 `CapabilityType`:`'capability'` 不在 `CapabilityType`
|
|
48
|
+
* (那是「菜单四类能力」的口径)里,两者刻意不混。
|
|
49
|
+
*/
|
|
50
|
+
export function capabilityKindOf(span) {
|
|
51
|
+
if (span.attributes.skill !== undefined)
|
|
52
|
+
return 'skill';
|
|
53
|
+
if (span.attributes.subagent !== undefined)
|
|
54
|
+
return 'subagent';
|
|
55
|
+
return 'capability';
|
|
56
|
+
}
|
|
14
57
|
//# sourceMappingURL=trace.js.map
|
package/dist/core/trace.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../../src/core/trace.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../../src/core/trace.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAqEH,sEAAsE;AACtE,MAAM,cAAc,GAAG,6DAA6D,CAAC;AAErF;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAgC;IAC/D,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,CAAC,GAAG,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1D,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IACvC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,SAAS,CAAC;IAClE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC;AAqDD;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,KAAY,EAAE,KAAY;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,UAAU,CAAC,CAAC;IACpE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAU;IACzC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IACxD,IAAI,IAAI,CAAC,UAAU,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAC9D,OAAO,YAAY,CAAC;AACtB,CAAC"}
|
|
@@ -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';
|