@migor/agentia 0.8.2 → 0.8.3
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 +77 -1
- package/README.md +1 -1
- package/dist/AGENTS.md +17 -1
- package/dist/core/limits.d.ts +198 -0
- package/dist/core/limits.js +186 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/trace.d.ts +66 -2
- package/dist/core/trace.js +37 -2
- package/dist/core/trace.js.map +1 -1
- package/dist/engine/forwarded.d.ts +84 -0
- package/dist/engine/forwarded.js +39 -0
- package/dist/engine/forwarded.js.map +1 -0
- package/dist/engine/spec.d.ts +26 -1
- package/dist/engine/spec.js.map +1 -1
- package/dist/engine/tracer.d.ts +62 -1
- package/dist/engine/tracer.js +130 -14
- package/dist/engine/tracer.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/adapter-options.d.ts +0 -7
- package/dist/integrations/adapter-options.js +2 -1
- package/dist/integrations/adapter-options.js.map +1 -1
- package/dist/runtime/run.d.ts +13 -1
- package/dist/runtime/run.js +14 -3
- package/dist/runtime/run.js.map +1 -1
- package/dist/toolkit/module.d.ts +22 -1
- package/dist/toolkit/module.js +12 -0
- package/dist/toolkit/module.js.map +1 -1
- package/dist/toolkit/skill.js +4 -13
- package/dist/toolkit/skill.js.map +1 -1
- package/dist/toolkit/subagent.js +6 -17
- package/dist/toolkit/subagent.js.map +1 -1
- package/dist/transport/async.d.ts +67 -0
- package/dist/transport/async.js +96 -2
- package/dist/transport/async.js.map +1 -1
- package/dist/transport/http-route.d.ts +6 -1
- package/dist/transport/http-route.js +9 -1
- package/dist/transport/http-route.js.map +1 -1
- package/dist/transport/http.js +93 -1
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/sse.d.ts +6 -2
- package/dist/transport/sse.js +3 -2
- package/dist/transport/sse.js.map +1 -1
- package/dist/transport/task-events.d.ts +83 -0
- package/dist/transport/task-events.js +141 -0
- package/dist/transport/task-events.js.map +1 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,81 @@
|
|
|
5
5
|
(0.x 阶段:minor 可含破坏性变更,每个破坏性变更都在对应版本的「迁移」小节里写明)。
|
|
6
6
|
决策的完整证据链在 `docs/spec.md` §10(带时间线的决策日志)。
|
|
7
7
|
|
|
8
|
+
## [0.8.3] - 2026-09-21
|
|
9
|
+
|
|
10
|
+
### 变更
|
|
11
|
+
|
|
12
|
+
> 本版主题(窗口 `0.8.2 → 0.8.3`,含 #115 与 #116):**增量 trace 出口落地**。记账与交付之间
|
|
13
|
+
> 此前**没有缝** —— `TraceRecorder` 的唯一出口是收尾的 `snapshot()`,所以「等不了 run 收尾」
|
|
14
|
+
> 的消费者(终端面板 / SSE 前端 / 异步任务进度流)拿不到任何东西。本版补上那条缝,顺手把
|
|
15
|
+
> spec §7 F3 的两条后置项与 §9.4 的记录成本问题同日收口。
|
|
16
|
+
> **框架 API 无破坏性变更**:只新增(`onTraceEvent` / `traceLimits.maxEvents` /
|
|
17
|
+
> `AsyncRunner.streamBufferEvents` / `GET /tasks/:id/stream` / `/run` SSE 的 `trace.event` 帧),
|
|
18
|
+
> **既有签名、既有三帧 SSE、既有默认行为逐字不变** —— 不认识新帧的老客户端行为零变化。
|
|
19
|
+
> #116 全部落在**仓库自身**(守卫 + 一次行为等价的重构),不含任何运行时行为变更。
|
|
20
|
+
|
|
21
|
+
### 新增
|
|
22
|
+
|
|
23
|
+
- **`onTraceEvent`:run 进行中逐笔拿记账事件**。`RunInvocationOptions.onTraceEvent`(单次)与
|
|
24
|
+
`AppOptions.onTraceEvent`(应用级缺省)**叠加**(应用级在前)而不是覆盖 —— 观察者是注册不是
|
|
25
|
+
值覆盖,覆盖会让「某次 run 顺手传了个面板回调」把应用级那条静默顶掉。载荷是
|
|
26
|
+
`TraceRecordEvent`(`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`,
|
|
27
|
+
**增量 + 此刻的拷贝**)。
|
|
28
|
+
四条纪律:同步派发不 await(订阅者是观察者,不该把 run 变成它的调度)、抛错被吞(与
|
|
29
|
+
`flushSinks` 同款)、无订阅者零派发、**seq 每次记账动作都占号**(与有没有订阅者无关 ——
|
|
30
|
+
否则「订阅晚的人」看到的序号会与一直订阅的人不一致,重放与去重都会错位)。
|
|
31
|
+
它与 `TraceSink` **不互相替代**:sink 是收尾拿整棵、不保证运行期可见;这条是运行期逐笔、
|
|
32
|
+
不保证送达。要「收尾的整棵」继续用 sink。
|
|
33
|
+
- **`POST /run` 的 SSE 新增一族 `trace.event` 帧**。帧名取**一族** + body 里带 `type`,而不是
|
|
34
|
+
每类型一帧:将来加新事件类型时,老客户端只是漏掉一种 `type`,而不是漏掉一种**帧名**(后者
|
|
35
|
+
更隐蔽)。既有三帧(`text.delta` / `run.end` / `error`)逐字未变。
|
|
36
|
+
- **`GET /tasks/:id/stream`:异步任务的进度流**。从头重放 → 转实时 → 终态以 `task.end` 收口关流;
|
|
37
|
+
`Last-Event-ID` / `?from=` 指**流自己的序号**(一个任务可能跨多个 run 段 —— HITL 挂起→恢复、
|
|
38
|
+
崩溃重投,每段是独立的一次 run、`seq` 从 1 重来,而流的读者要的是一条**连续的**流)。
|
|
39
|
+
四条边界:`awaiting_approval` **不是终态**(流继续开着 —— 关掉的话「等审批结果的前端」正好在
|
|
40
|
+
最需要的时候断线);这条流的读者是**旁观者**,背压 / 断开**不 abort 任务**(与 `/run` 的 SSE
|
|
41
|
+
刻意相反:那里的下游是 run 的所有者,背压等于「别继续烧 token 了」);跨进程(别的 runner 跑的
|
|
42
|
+
任务、同一个 store)→ 一帧 `stream.unavailable` + 终态 `task.end`,**不假装实时**;缓冲超限 →
|
|
43
|
+
丢最旧并先发一帧 `stream.truncated`(**不静默**)。缓冲用 `AsyncRunner` 的
|
|
44
|
+
`streamBufferEvents` 调(缺省每任务 500 条;终态流只留最近 16 条)。事件**不落 store** 是有意
|
|
45
|
+
的:实测事件数 = 2 × 工具调用、正文 KB 级 ⇒ 每条工具调用要把 KB 级正文写库两次(写放大),而
|
|
46
|
+
宿主本来就有自己的总线(`onTraceEvent` 就是给它的缝)。
|
|
47
|
+
- **`traceLimits.maxEvents`:整条 trace 的事件总数上限**。超限即**停止记账**,交付时在 run 根写
|
|
48
|
+
一笔 `trace.truncated{droppedEvents, limit}` —— 缺口位置可预测(尾巴)且**有计数**。
|
|
49
|
+
刻意**不做**环形缓冲(丢最旧、留最近):那会让 trace 中间出现空洞,而空洞比「尾巴截断」难解释
|
|
50
|
+
得多(「这一回合怎么没有工具事件」)。`0` = 一条都不记(**有意义的值**,仍有计数);不设 = 不限。
|
|
51
|
+
坏值(NaN / ±Infinity / 负数 / 小数)在 run 入口抛 `TypeError` —— 静默接受会让闸门**形同不存在**
|
|
52
|
+
(`NaN` 让 `>=` 恒假),而使用者以为自己设了上限。
|
|
53
|
+
与既有的 `maxEventChars` **正交**:一个管「单个事件正文多长」、一个管「多少」。
|
|
54
|
+
|
|
55
|
+
### 文档
|
|
56
|
+
|
|
57
|
+
- `docs/usage-guide.md`:`onTraceEvent` / `traceLimits` / `streamBufferEvents` 与
|
|
58
|
+
`GET /tasks/:id/stream` 的用法;§7 边界表补任务进度流的边界(内存 / 跨进程)。
|
|
59
|
+
- `docs/observability.md`:采样在「不内建」之外补上**可算 + 可数** —— §2.3 新增采样率换算表,
|
|
60
|
+
示例 `sampleSink` 增加丢弃计数(`dropped()` / `onDrop`)。**框架仍然不内建采样器**(既有决策
|
|
61
|
+
不变:采样是配方 2.3,`examples/observability` 已有成品;再造一个就是同一件事的两份实现)。
|
|
62
|
+
- `docs/spec.md` §9.4 从「唯一剩下的开放问题」变为**决定**:默认全量、截断默认开、采样不内建,
|
|
63
|
+
框架侧只新增「数量上限 + 丢弃计数」这一件 —— 让「少记了数据」可数。
|
|
64
|
+
- **一处面向使用者的说明被更正**:`formatTraceparent` 的 flags 继续恒 `00`,但理由从「本框架
|
|
65
|
+
不采样」改为**「运行期不可知」** —— 记录 / 导出决策发生在**收尾之后**,出站调用发生在
|
|
66
|
+
**运行期**,那时没有答案。所以它**不会**跟随采样率变成 `01`:原理由在采样成为推荐配法后已不
|
|
67
|
+
严谨,若照它改成「跟随采样」反而是错的。
|
|
68
|
+
|
|
69
|
+
### 仓库自身(不面向使用者)
|
|
70
|
+
|
|
71
|
+
- **`docs/guards.md` §2 从 4 行清到 1 行** —— 三条待守形状建成了机器守卫(`0` 的语义真源 /
|
|
72
|
+
穷尽转发 / 队列消费者配方门禁),细节见该文件 §1.2 / §1.3。顺带登记了两个此前无人写明的事实:
|
|
73
|
+
`intervalMs` 在 `Scheduler.every`(必须 > 0)与 `metricsSink`(`0` = 立即导出)里**同名反义**;
|
|
74
|
+
数量类旋钮里只有 `mapWithConcurrency` 把 `0` 读作「不限」(`maxRetries` / `maxEvents` /
|
|
75
|
+
`maxIterations` 都是「就是不做」)。
|
|
76
|
+
- `scripts/verify-all.sh` 与 CI 的**步数不变**:新增检查一律折进已有步骤(步骤数写在 CI 的必需
|
|
77
|
+
状态检查名里,加一步就要同时改 workflow 与分支保护)。
|
|
78
|
+
|
|
79
|
+
**迁移**:无(框架 API 无破坏性变更,既有代码不需要任何改动)。若你的宿主自己解析 `/run` 的 SSE,
|
|
80
|
+
它**不需要**认识新的 `trace.event` 帧 —— 不认识就忽略,行为与升级前逐字一致。要消费增量事件,
|
|
81
|
+
新接 `onTraceEvent`(程序内)或 `GET /tasks/:id/stream`(远程看进度)即可。
|
|
82
|
+
|
|
8
83
|
## [0.8.2] - 2026-09-21
|
|
9
84
|
|
|
10
85
|
### 变更
|
|
@@ -864,7 +939,8 @@
|
|
|
864
939
|
首个公开发布:`@migor/agentia` + `@migor/cli`(scope `@migor/*`),两包版本同步。
|
|
865
940
|
框架本体单包;CLI 独立成包(workspaces)。
|
|
866
941
|
|
|
867
|
-
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.8.
|
|
942
|
+
[Unreleased]: https://github.com/retrychx/agentia/compare/v0.8.3...HEAD
|
|
943
|
+
[0.8.3]: https://github.com/retrychx/agentia/releases/tag/v0.8.3
|
|
868
944
|
[0.8.2]: https://github.com/retrychx/agentia/releases/tag/v0.8.2
|
|
869
945
|
[0.8.1]: https://github.com/retrychx/agentia/releases/tag/v0.8.1
|
|
870
946
|
[0.8.0]: https://github.com/retrychx/agentia/releases/tag/v0.8.0
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ npm run dev # = agentia dev:tsx watch + 本地 inspec
|
|
|
34
34
|
> 框架**不自动**读 `.env`:脚手架 `src/main.ts` 首行的 `loadEnvFile()` 负责把它读进 `process.env`。
|
|
35
35
|
> 真实环境变量优先(CI / docker / 命令行永远赢过文件),`loadEnvFile({ override: true })` 才反过来。
|
|
36
36
|
|
|
37
|
-
> **版本**:`0.8.
|
|
37
|
+
> **版本**:`0.8.3`(`@migor/agentia` 与 `@migor/cli` 均已发布到 npm)。`examples/` **刻意**用
|
|
38
38
|
> `file:../..` 指向本仓库而不是 npm 版本 —— 跑的是**工作区代码**,理由见
|
|
39
39
|
> [`examples/README.md`](examples/README.md) 的「依赖说明」。
|
|
40
40
|
|
package/dist/AGENTS.md
CHANGED
|
@@ -181,6 +181,7 @@ npx agentia --version # CLI 版本(= -v)
|
|
|
181
181
|
| `tools` | 直接追加到主菜单的**裸工具**(`AgentTool[]`):给「构造期才知道有哪些工具」的场合(典型:MCP 桥,见 §6)。与能力**同过中间件、同进重名查重**,不是旁路 |
|
|
182
182
|
| `middleware` | 能力调用中间件(洋葱链,链序 = 注册顺序) |
|
|
183
183
|
| `sinks` | trace 出口,run 收尾投递 |
|
|
184
|
+
| `onTraceEvent` | **增量记账出口的缺省值**:run **进行中**逐笔回调(`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`),给等不了收尾的消费者(面板 / SSE / 任务流)。与 `sinks` 是**两条缝**、可同时配;单次 run 给了自己的那个是**叠加**(应用级在前)而非覆盖。⚠️ **不保证送达**(宿主自己的流断了就断了),也不替代 `sinks` |
|
|
184
185
|
| `maxTotalTokens` | 缺省成本硬管控:整条 run(**含子 agent / skill 子循环**,上限经 `ToolRunContext` 透传)累计 token 上限(可被单次 run 覆盖) |
|
|
185
186
|
| `maxCostUsd` | 缺省成本硬管控:累计成本(美元)上限(**依赖模型在价格表内**,见 `priceOverrides`;未定价模型会留 `usage.unpriced` 事件,所以「护栏有没有真的生效」看得见) |
|
|
186
187
|
| `priceOverrides` | 价格表覆盖/追加(`$/1M tokens`):覆盖内置同名项,或给非 Anthropic 模型定价(如 `{ 'deepseek-chat': { in: 0.27, out: 1.10 } }`)。**透传给子 agent/skill 的子循环** —— 不会「主 agent 有成本、子 agent 恒 0」。非法单价在 run 开始即抛错 |
|
|
@@ -188,6 +189,7 @@ npx agentia --version # CLI 版本(= -v)
|
|
|
188
189
|
| `toolTimeoutMs` | 缺省单工具超时(毫秒);超时该条 tool_result 记 is_error,不杀 run |
|
|
189
190
|
| `maxToolConcurrency` | 缺省同回合并行工具上限;不设 = 不限(全并行) |
|
|
190
191
|
| `maxEventChars` | 缺省 trace 事件正文截断上限(可被单次 run 覆盖):数字 = 入参/出参统一用该上限,`false` = **不截断**(完整正文进 trace,面板里能展开看全文);不设 = 框架缺省 |
|
|
192
|
+
| `traceLimits` | **记账的数量上限** `{ maxEvents? }`(可被单次 run 覆盖):整条 trace 的事件总数闸。超限即停止记账,并在交付时于 run 根写一笔 `trace.truncated{droppedEvents, limit}` —— 缺口位置可预测(尾巴)且**有计数**。与管**长度**的 `maxEventChars` 正交(一个管「多长」、一个管「多少」);不设 = 不限(全量记账是本框架的承诺)。坏值(NaN / 负数 / 小数)在 run 入口抛 `TypeError` |
|
|
191
193
|
|
|
192
194
|
### `app.run(messages, opts?: RunAppOptions)`
|
|
193
195
|
|
|
@@ -199,6 +201,8 @@ npx agentia --version # CLI 版本(= -v)
|
|
|
199
201
|
| `maxIterations` | 单次覆盖 |
|
|
200
202
|
| `client` | 注入 `ModelClient`(换 OpenAI 兼容端点等) |
|
|
201
203
|
| `onText` | 文本增量回调(SSE/终端) |
|
|
204
|
+
| `onTraceEvent` | 单次 run 的**增量记账出口**(见 `createApp` 同名项):与**应用级那个叠加**(应用级在前),不是覆盖 |
|
|
205
|
+
| `traceLimits` | 单次 run 的记账数量上限(覆盖应用级缺省);见 `createApp` 同名项 |
|
|
202
206
|
| `signal` | `AbortSignal`:中止则在飞请求被取消,run 以 `stopReason='aborted'` 收尾(算失败) |
|
|
203
207
|
| `traceContext` | 入站链路上下文 `{ traceId, spanId? }`:触发本次 run 的上游 span 记成 run 根的一条 `links`(不改 `traceId == runId`)。HTTP 宿主认 `traceparent` 头,自动填 —— 见 §6「跨进程关联」 |
|
|
204
208
|
| `blackboard` | 预置黑板种子(配 `Blackboard` 声明合并有键补全) |
|
|
@@ -463,6 +467,7 @@ npm run client # 另一个终端:把四个 RPC 跑一遍
|
|
|
463
467
|
| `POST /run` | body 是 `RunInput`(string / messages / `{prompt\|text\|messages}`);带 `Accept: text/event-stream` 则走 SSE | 200 `{ runId, status, stopReason, finalText, typed?, trace, error? }` —— **`status=failed` 也照返 200**(`rethrow:false` 语义:硬失败以 `error` 字段表达,不用 HTTP 错误码) |
|
|
464
468
|
| `POST /tasks` | `{ input, idempotencyKey?, options? }` —— `input` 同 `RunInput`;`options` 是 `RunInvocationOptions` | 202 `TaskRecord`(`status: 'queued'`);同 `idempotencyKey` 未失败则去重、直接返回既有记录(**同步 store** 当场判定;**异步 store** 下只保证**同进程内并发提交**不重复执行,跨进程与终态后重提仍是 at-least-once —— 见 §7「同键去重的能力边界」) |
|
|
465
469
|
| `GET /tasks/:id` | — | 200 `TaskRecord`;不存在 → 404。**停机中仍可轮询**(否则拿不到在飞任务的结果) |
|
|
470
|
+
| `GET /tasks/:id/stream` | — | **任务进度流(SSE)**:先在 `id:` 里给流序号,逐帧下发 `trace.event`(body 即 `TraceRecordEvent`),终态发 `task.end` 并关闭。断线重连带 `Last-Event-ID`(或 `?from=<序号>`)即可续订 —— 只补该序号之后的事件。缓冲超限先发一帧 `stream.truncated{droppedBefore}`;别的进程在跑的任务发 `stream.unavailable` 后收口(**不假装实时**)。任务不存在 → 404;方法不对 → 405。⚠️ 它的读者是**旁观者**:背压/断开只收口这条流,**不中止任务** |
|
|
466
471
|
| `POST /tasks/:id/approve` | `{ decisions: { <tool_use_id>: { approved, reason? } }, decidedBy? }` | 200 `TaskRecord`(HITL 审批:批准/拒绝挂起任务,见 §6.6「人工审批」);任务不存在 → 404;不在 `awaiting_approval` 状态 → 409;body 非法 → 400。**停机中仍可审批**(与 GET 轮询同理由) |
|
|
467
472
|
| `GET /healthz` | — | 200 `HealthResponse`;**不鉴权**,停机中也回 200 |
|
|
468
473
|
| `GET /metrics` | — | 200 Prometheus 文本(`text/plain; version=0.0.4`);**需在 `createHttpHandler` 里传 `metrics`**,**不鉴权**(与 `/healthz` 同档),停机中也回 |
|
|
@@ -539,7 +544,7 @@ process.on('SIGTERM', async () => {
|
|
|
539
544
|
|
|
540
545
|
- **取消**:`app.run(messages, { signal })` 传 `AbortSignal` —— 框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发 `signal`),run 以 `stopReason='aborted'` 收尾(算失败)。`createHttpHandler` 已内置「客户端断开即中止」;`AsyncRunner.runTimeoutMs` 到点同样是**真中止**(构造期校验:必须 ≥ 0 的**有限**数 —— NaN/Infinity 会被 `setTimeout` 钳到 1ms,等于每个任务立即超时,故直接抛错;要「不限」传 0 或不设)。
|
|
541
546
|
- **重试**:缺省自动重试可重试失败(429 / 5xx / 连接失败),指数退避 + 抖动。`retry: false` 关闭,或 `retry: { maxAttempts, baseDelayMs, maxDelayMs, jitter, onRetry }` 调参。**只在本次尝试尚未产出任何文本时重试**(已吐出的字无法撤回)。⚠️ 与底层 client 的**内置重试**叠加 —— **两条内置适配器口径一致**(`createAnthropicClient` / `createOpenAIClient` 都有 `maxRetries`,缺省 2,重试同一状态码集合 408/409/429/5xx)—— 建议二选一调(这里 `maxAttempts: 1`,或 `<适配器>({ maxRetries: 0 })`)。`maxRetries` 在**构造期**校验:只收非负安全整数(`0` = 不重试),NaN / ±Infinity / 负数 / 小数一律抛 `TypeError` —— 判定是 `attempt >= maxRetries`,NaN 恒假、Infinity 永不达到,两者都等于**无限重试**(且静默)。
|
|
542
|
-
- **流式**:`POST /run` 带 `Accept: text/event-stream` → SSE 逐帧下发(`text.delta` / `run.end` / `error
|
|
547
|
+
- **流式**:`POST /run` 带 `Accept: text/event-stream` → SSE 逐帧下发(`text.delta` / `run.end` / `error`,外加一族 **`trace.event`** —— 增量记账事件,body 即 `TraceRecordEvent`,用它做实时面板;不认识这一族的老客户端行为零变化);不带该头仍回一元 JSON。
|
|
543
548
|
- **工具超时 / 并发闸门**:`toolTimeoutMs` 超时**不杀 run**(该条 tool_result 记 `is_error`,模型可换路);`maxToolConcurrency` 给同回合的并行工具设上限(默认全并行)。⚠️ 超时 = **放弃等待**:`AgentTool.run` 没有 signal 参数,**副作用可能已发生**;但引擎放弃等待时会 abort `ToolRunContext.abandoned` —— 想真停的工具监听它自行收尾(框架自带的 @SubAgent / @Skill 已这么做:超时即中止子循环,capability span 以 error 收尾)。**超时判定只有一个裁判**:`toolTimeoutMs` 是唯一判据 —— 工具自带的超时(如 MCP 桥的 `timeoutMs`)在设了本项时**不参与**判定;反过来说,工具自判的超时(抛 `code='timeout'` 的错误)与引擎判的记**同一类账**(`errorKind='timeout'`),并同样回 `is_error`。
|
|
544
549
|
|
|
545
550
|
### 6.3 上下文预算与成本
|
|
@@ -629,6 +634,7 @@ trace 出去之后能干什么:指标、调用树面板、调优报告、生
|
|
|
629
634
|
| `TraceRecorder` | 内存 recorder(一次 run 一个);`addLink(spanId, { traceId, spanId? })` 记一条跨 trace 链路(见 §6「跨进程关联」) |
|
|
630
635
|
| `parseTraceparent` | 解析 W3C `traceparent` 头 → `{ traceId, spanId? }`;**非法 / 缺头一律返回 `undefined`**(不抛、不打 400)—— 结果直接交给 `traceContext` 选项,见 §6「跨进程关联」 |
|
|
631
636
|
| `createOtlpExporter` | OTLP/JSON 导出,零依赖;选项见下面「`OtlpExporterOptions`」表 |
|
|
637
|
+
| `TraceRecordEvent` | **增量记账事件**(`onTraceEvent` 的回调参数):`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`,每条带单调 `seq`。载荷是**增量 + 此刻的拷贝**(`span.begin` 只给初始形状,属性/事件/链路各走自己的类型);按 `seq` 升序折回必须**逐字等于**收尾时的 trace |
|
|
632
638
|
| `metricsSink` | 指标累加器(Prometheus 文本 / OTLP metrics),满足 `TraceSink` 即接入 —— 见 §6「指标」 |
|
|
633
639
|
| `buildRunReport` | 从一条 trace 生成**调优报告**(能力/模型的耗时、token、成本、错误率排行)—— 见 §6「调优报告」 |
|
|
634
640
|
| `Score` | 质量评分:`{ name; value; source?; comment? }` —— LLM-judge / 人工标注 / eval 结论挂到 trace 上;约定 `value` 为 0–1(布尔结论用 0/1),`source` 记评分来源(eval 名 / `'human'` / judge 模型 id) |
|
|
@@ -696,6 +702,12 @@ runner.submit(msg.value, {
|
|
|
696
702
|
});
|
|
697
703
|
```
|
|
698
704
|
|
|
705
|
+
这条配方**有门禁真跑**:`tests/transport/queueConsumer.test.ts`(内存版 broker + 真
|
|
706
|
+
`AsyncRunner` + 真引擎)把三件事逐条跑出来 —— 同键重投**不重复执行**(断言的是副作用计数,
|
|
707
|
+
不只是 taskId)、`traceparent` 随 `spec.options` 落库后**他进程 `resumePending` 续跑**那次 run
|
|
708
|
+
仍带得上同一条 link、失败不 ack 必须 nack 重投(且**失败**的键允许新任务,否则重投永远拿不到
|
|
709
|
+
第二次执行)。真接 broker 时仍然要自己接:门禁守的是**提交位移与幂等的时机**,不是协议实现。
|
|
710
|
+
|
|
699
711
|
异步任务可以在 `POST /tasks` 的 body 里显式给 `options.traceContext`,它优先于 `traceparent` 头。
|
|
700
712
|
换宿主这条缝不变:gRPC 宿主把 metadata 的 `traceparent` 翻进 `options.traceContext`,
|
|
701
713
|
见 §6.2「gRPC 宿主」。
|
|
@@ -1255,6 +1267,10 @@ const callable = {
|
|
|
1255
1267
|
| schema 校验是**子集** | 只覆盖 `type/properties/required/additionalProperties/enum/items`;`format`/`minimum`/`oneOf` 一律放行 |
|
|
1256
1268
|
| 历史畸形就放弃裁剪 | `trimToolPairs` 遇到非严格交替历史会整体放弃(宁可少裁,也不切出孤立 tool_use 让请求 400) |
|
|
1257
1269
|
| 同键去重的能力边界 | `idempotencyKey` 的去重分三档:**同步 store** 下 `submit` 当场返回既有记录;**异步 store**(Redis / SQLite 等)下 `submit` 是同步门面、无法 await,去重靠**进程内认领表**,只覆盖「**同进程内并发提交**」(2026-09-21 修复前这里会两次都执行);**跨进程并发**与**终态之后重提同键**仍是 at-least-once —— store 的 idem 索引是 last-wins(重提即新任务,`redisStore` / `fsStore` / `sqliteStore` 头注释同口径)。要严格一次,请让副作用自身幂等(或在 store 层做唯一约束) |
|
|
1270
|
+
| 增量出口与 sink 是**两条缝** | `onTraceEvent` 是「运行期逐笔」,`sinks` 是「收尾拿整棵」——消费者与保证都不同:sink 的抛错被吞但仍会**投递**(有兜底语义),`onTraceEvent` **不保证送达**(宿主自己的流断了就断了,没有重试/重放)。要「一条都不能少」用 `sinks`;要「现在就看到」用 `onTraceEvent` |
|
|
1271
|
+
| 任务进度流的边界(内存 / 跨进程) | `GET /tasks/:id/stream` 的事件缓冲在**跑任务的进程内存**里:每任务最近 500 条(可用 `AsyncRunner` 的 `streamBufferEvents` 调),超限丢**最旧**并先发一帧 `stream.truncated`;终态流只留最近 16 条。**跨进程**(队列消费者在别的进程)时没有实时流,只能拿到一帧 `stream.unavailable` + 终态 —— 要跨进程实时请用 `onTraceEvent` 把事件转发到宿主自己的总线(Redis Streams / Kafka) |
|
|
1272
|
+
| `traceLimits` 与 `maxEventChars` 各管一头 | `maxEventChars` 管**单个事件正文多长**(既有),`traceLimits.maxEvents` 管**整条 trace 多少个事件**(本版)。两者正交、都「不设 = 不限」;上限触发时**丢弃量写在 run 根的 `trace.truncated`** 上(不静默)。⚠️ 实测 `maxEventChars: false` + 大出参会让 trace 放大 **13.7×**(`npm run bench:trace` 可复现)—— 先收长度再谈采样,收益顺序比反过来大 |
|
|
1273
|
+
| **采样是导出决策,不是记账决策** | 采样在 `sink` 外做(配方见 `docs/observability.md` 2.3):被采样掉的 trace 在框架内**仍然完整记账**,只是没发给下游。所以别拿「有采样」当「可以少记账」;也正因如此,出站 `traceparent` 的 flags 恒 `00`(记录/导出决策发生在收尾之后,运行期不可知——不替下游声明) |
|
|
1258
1274
|
| 缺省内存 store 不淘汰 | 长跑宿主请设 `InMemoryTaskStore({ maxRecords })` 或换 `FileTaskStore` / `SqliteTaskStore` |
|
|
1259
1275
|
| 能力引用两种粒度 | `tools` 写 **provider token** = 整片能力菜单;写 `'<token>/<能力名>'` = 只引单个能力(@Tool/@Skill/@SubAgent/@Prompt 都可点名,装配期校验,名字不存在即抛错并列出可用名单) |
|
|
1260
1276
|
| 能力名有格式校验 | 装饰器能力名(`name` 或缺省的方法名)必须匹配 `^[A-Za-z0-9_-]{1,64}$`(与 MCP 桥同口径),非法名在 `createApp` **装配期即抛错** —— 含空格/点/中文的名字会让模型 API 400,宁可在启动期拦住 |
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— **limits 语义的单一真源**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个文件(`docs/guards.md` §2 的第一条待守形状):
|
|
5
|
+
*
|
|
6
|
+
* 本仓把「非正数 = 机制关掉」当成一条统一口径,但它被**每个旋钮各自解释**了一遍 ——
|
|
7
|
+
* 同一个 `0` 在这里是「不限」、在那里是「一次都不做」、在第三个地方是「配置错误」。
|
|
8
|
+
* 历史事故就出在这条缝上:`handler.drain({ timeoutMs: 1 })` 跨过 deadline 后**永不返回**
|
|
9
|
+
* (调用方按「1 毫秒 = 马上就超时」写,代码里却是另一套读法),而当时没有任何一处
|
|
10
|
+
* 集中写着「这个旋钮的 0 是什么意思」。改到相关代码时只能靠人去猜 —— 于是又猜错。
|
|
11
|
+
*
|
|
12
|
+
* 本文件把「每个旋钮的 0 是什么」变成**可执行的数据**,`tests/limits.test.ts` 再拿这份
|
|
13
|
+
* 表去**驱动真实站点**逐条对账:表里声明 `0 = 不限`,用例就去调那个真 API 并断言它真的
|
|
14
|
+
* 不限。所以:
|
|
15
|
+
*
|
|
16
|
+
* - 表与实现漂了 ⇒ 集中用例红(不是文档过期,是构建失败);
|
|
17
|
+
* - 新增一个旋钮却忘了归类 ⇒ 用例里 `Record<LimitKnob, …>` 少一项,`typecheck:tests` 红。
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ 本模块**不是公共 API**(不进 `src/index.ts`):它是内部口径的真源,不是给使用者的旋钮。
|
|
20
|
+
* 面向使用者的口径在 `docs/usage-guide.md` 的「边界」一节。
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ 表里的 `zeroClause` 是**报错文案的一部分**:四处构造期校验(`runTimeoutMs` /
|
|
23
|
+
* `approvalTimeoutMs` / `traceLimits.maxEvents` / `maxRetries`)把它插进错误消息里
|
|
24
|
+
* (`…(0 = 不重试),收到 -1 —— …`)。所以「文案里怎么解释 0」与「代码里怎么实现 0」
|
|
25
|
+
* 是同一个值,改一处等于同时改另一处 —— 这正是先前缺的那条单源。
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* 一个旋钮在取 `0`(或「非正数」)时的**含义分类**。四选一,没有第五种:
|
|
29
|
+
*
|
|
30
|
+
* - `unlimited` —— **0 = 不限**。时间是「等多久 / 最多多久」的上限,0 表示去掉这个上限。
|
|
31
|
+
* 使用者的直觉通常是这个;本仓只有一部分旋钮真的是这个意思,这正是要对账的原因。
|
|
32
|
+
* - `disabled` —— **0 = 机制关掉 / 一次都不做**。数量是「做几次」,0 次就是不做。
|
|
33
|
+
* ⚠️ 与 `unlimited` 是**相反的**读法:`maxRetries: 0` 是「不重试」而不是「无限重试」;
|
|
34
|
+
* `maxEvents: 0` 是「一条都不记」而不是「不限条数」。
|
|
35
|
+
* - `immediate` —— **0 = 立即执行,不等**。等待预算为 0 ⇒ 这次等待压根不存在。
|
|
36
|
+
* 与 `disabled` 的区别:机制没被关掉,只是这一次立刻发生(`metricsSink` 的
|
|
37
|
+
* `intervalMs: 0` = 立即导出;`interruptibleSleep(0)` = 不睡)。
|
|
38
|
+
* - `invalid` —— **0 = 非法配置,构造期响亮失败**。0 在这里没有任何合理读法
|
|
39
|
+
* (`concurrency: 0` 没有 worker;`Scheduler.every(0)` 是空转),静默接受只会
|
|
40
|
+
* 把它变成另一件事(Node 会把 `setInterval(0)` 钳到 1ms ⇒ 每毫秒一轮的忙轮询)。
|
|
41
|
+
*/
|
|
42
|
+
export type ZeroMeaning = 'unlimited' | 'disabled' | 'immediate' | 'invalid';
|
|
43
|
+
/** 坏值(非有限 / 负数 / 小数 / 类型不对)的处理方式 */
|
|
44
|
+
export type BadValuePolicy =
|
|
45
|
+
/** 构造期抛错 —— 使用者以为自己设了上限,静默接受就会变成别的行为 */
|
|
46
|
+
'throws'
|
|
47
|
+
/** 一律视为「不限」(旧行为),不报错 */
|
|
48
|
+
| 'coerced-to-unlimited'
|
|
49
|
+
/** 无校验(该旋钮只做算术,坏值无处可藏) */
|
|
50
|
+
| 'none';
|
|
51
|
+
export interface LimitSemantic {
|
|
52
|
+
/** 稳定标识(用例按它建表;改名 = 用例红,故意的) */
|
|
53
|
+
readonly knob: string;
|
|
54
|
+
/** 实现位置(文件名:符号),改代码时的入口 */
|
|
55
|
+
readonly where: string;
|
|
56
|
+
readonly unit: 'ms' | 'count' | 'chars' | 'n/a';
|
|
57
|
+
readonly zero: ZeroMeaning;
|
|
58
|
+
/**
|
|
59
|
+
* 报错文案里那句「0 = 什么」(无报错的旋钮也写上 —— 文档与用例都读它)。
|
|
60
|
+
* 有构造期校验的旋钮由实现代码直接插进错误消息。
|
|
61
|
+
*/
|
|
62
|
+
readonly zeroClause: string;
|
|
63
|
+
readonly badValue: BadValuePolicy;
|
|
64
|
+
/** 为什么是这个读法(一句话;踩过坑的把坑写上) */
|
|
65
|
+
readonly note: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 全部 limits 旋钮的 0 语义。**分组按读法**,好让人一眼看出哪几个是相反的。
|
|
69
|
+
*/
|
|
70
|
+
export declare const LIMIT_SEMANTICS: readonly [{
|
|
71
|
+
readonly knob: 'AsyncRunner.runTimeoutMs';
|
|
72
|
+
readonly where: 'transport/async.ts';
|
|
73
|
+
readonly unit: 'ms';
|
|
74
|
+
readonly zero: 'unlimited';
|
|
75
|
+
readonly zeroClause: '0 = 不限';
|
|
76
|
+
readonly badValue: 'throws';
|
|
77
|
+
readonly note: 'NaN/Infinity 会被 setTimeout 钳到 1ms ⇒ 每个任务立即「超时」失败,且 NaN 会绕过 `< 0` 静默通过;所以要「不限」必须显式传 0。';
|
|
78
|
+
}, {
|
|
79
|
+
readonly knob: 'AsyncRunner.approvalTimeoutMs';
|
|
80
|
+
readonly where: 'transport/async.ts';
|
|
81
|
+
readonly unit: 'ms';
|
|
82
|
+
readonly zero: 'unlimited';
|
|
83
|
+
readonly zeroClause: '0 = 不限';
|
|
84
|
+
readonly badValue: 'throws';
|
|
85
|
+
readonly note: '同 runTimeoutMs:非有限数会让「已挂起多久」的比较静默失效或立即超时。';
|
|
86
|
+
}, {
|
|
87
|
+
readonly knob: 'AsyncRunner.drain.timeoutMs';
|
|
88
|
+
readonly where: 'transport/drain-gate.ts';
|
|
89
|
+
readonly unit: 'ms';
|
|
90
|
+
readonly zero: 'unlimited';
|
|
91
|
+
readonly zeroClause: '非正 = 一直等';
|
|
92
|
+
readonly badValue: 'none';
|
|
93
|
+
readonly note: '**历史事故现场**:`http.ts` 的优雅停机把剩余预算算成 `timeoutMs - elapsed` 再交给它,deadline 已过时算出 1 或 0 ⇒ 旧读法(`0` = 已到点)让它跨过 deadline 后**永不返回**。现在 `http.ts` 自己先判「已到点就认账」,不把 0 交给下游去猜。';
|
|
94
|
+
}, {
|
|
95
|
+
readonly knob: 'mapWithConcurrency.limit';
|
|
96
|
+
readonly where: 'engine/concurrency.ts';
|
|
97
|
+
readonly unit: 'count';
|
|
98
|
+
readonly zero: 'unlimited';
|
|
99
|
+
readonly zeroClause: '非正 = 不限';
|
|
100
|
+
readonly badValue: 'coerced-to-unlimited';
|
|
101
|
+
readonly note: '⚠️ 与 `maxToolConcurrency` 成对:这是**唯一**「数量」旋钮取 0 却读作「不限」的。另有一条反向的坑:`(0,1)` 区间的小数若交给 `Math.floor` 会压成 0 worker ⇒ 工具被静默丢弃,所以正数一律至少 1 个 worker(`maxToolConcurrency: cpus().length / 8` 在多核 < 8 的机器上正落在那个区间)。';
|
|
102
|
+
}, {
|
|
103
|
+
readonly knob: 'traceLimits.maxEvents';
|
|
104
|
+
readonly where: 'engine/tracer.ts';
|
|
105
|
+
readonly unit: 'count';
|
|
106
|
+
readonly zero: 'disabled';
|
|
107
|
+
readonly zeroClause: '0 = 一条都不记';
|
|
108
|
+
readonly badValue: 'throws';
|
|
109
|
+
readonly note: '⚠️ 与 `maxEventChars`(管「多长」)正交,一个管「多少」。0 是**有意义的值**(一条都不记,但计数照记)—— 不是「不限条数」。';
|
|
110
|
+
}, {
|
|
111
|
+
readonly knob: 'maxRetries';
|
|
112
|
+
readonly where: 'integrations/adapter-options.ts';
|
|
113
|
+
readonly unit: 'count';
|
|
114
|
+
readonly zero: 'disabled';
|
|
115
|
+
readonly zeroClause: '0 = 不重试';
|
|
116
|
+
readonly badValue: 'throws';
|
|
117
|
+
readonly note: '坏值必须响亮失败:判定是 `attempt >= maxRetries` ⇒ `NaN` 比较恒假、`Infinity` 永远达不到,两者都是**无限重试**(429 场景下每一次都是真金白银);`-1` 静默变成「不重试」;`1.5` 实际只允许 1 次。';
|
|
118
|
+
}, {
|
|
119
|
+
readonly knob: 'maxEventChars';
|
|
120
|
+
readonly where: 'engine/tool-events.ts';
|
|
121
|
+
readonly unit: 'chars';
|
|
122
|
+
readonly zero: 'disabled';
|
|
123
|
+
readonly zeroClause: '0 = 截到零长度;false = 不截断';
|
|
124
|
+
readonly badValue: 'none';
|
|
125
|
+
readonly note: '两个「关掉」的写法语义不同:`false` 才是「不截断」(调试期看全文),`0` 是把正文截成空串。⚠️ 靠 `!= null` 判定透传(真值判定会吃掉 `false`)。';
|
|
126
|
+
}, {
|
|
127
|
+
readonly knob: 'runAgent.maxIterations';
|
|
128
|
+
readonly where: 'engine/loop.ts';
|
|
129
|
+
readonly unit: 'count';
|
|
130
|
+
readonly zero: 'disabled';
|
|
131
|
+
readonly zeroClause: '0 = 一次都不跑';
|
|
132
|
+
readonly badValue: 'none';
|
|
133
|
+
readonly note: '循环写成 `for (iteration = 0; iteration < maxIterations; ...)` ⇒ 0 时循环体一次不执行,直接以「达到循环上限」收尾(**算失败**)。是「已到点」而非「不限」,与 `mapToolConcurrency` 的读法**相反**。';
|
|
134
|
+
}, {
|
|
135
|
+
readonly knob: 'withTimeout.ms';
|
|
136
|
+
readonly where: 'core/timeout.ts';
|
|
137
|
+
readonly unit: 'ms';
|
|
138
|
+
readonly zero: 'disabled';
|
|
139
|
+
readonly zeroClause: '非正 = 不设超时(原样透传)';
|
|
140
|
+
readonly badValue: 'none';
|
|
141
|
+
readonly note: '`toolTimeoutMs` 的底座。⚠️ 漏透传这个值不是「少一层保险」而是**反的**:`withTimeout(p, 0)` 直接返回原 promise = 永不超时,同时 MCP 桥找不到引擎预算又起自己的 60s 兜底 ⇒ 双计时器、双账本(`toolkit/subagent.ts` / `skill.ts` 的透传注释指的就是这条)。';
|
|
142
|
+
}, {
|
|
143
|
+
readonly knob: 'interruptibleSleep.ms';
|
|
144
|
+
readonly where: 'core/timeout.ts';
|
|
145
|
+
readonly unit: 'ms';
|
|
146
|
+
readonly zero: 'immediate';
|
|
147
|
+
readonly zeroClause: '非正 = 不睡';
|
|
148
|
+
readonly badValue: 'none';
|
|
149
|
+
readonly note: '⚠️ 这一判**先于** aborted 检查 —— 别把它「修」成「已中止就该 reject」。2026-09-19 外部复核把它当一致性缺口改反过一次,被 `tests/core/timeout.test.ts` + `tests/core/sse-text-stats.test.ts` 拦住。';
|
|
150
|
+
}, {
|
|
151
|
+
readonly knob: 'metricsSink.intervalMs';
|
|
152
|
+
readonly where: 'integrations/metrics.ts';
|
|
153
|
+
readonly unit: 'ms';
|
|
154
|
+
readonly zero: 'immediate';
|
|
155
|
+
readonly zeroClause: '0 = 立即导出';
|
|
156
|
+
readonly badValue: 'none';
|
|
157
|
+
readonly note: '⚠️ **同名反义**:`Scheduler.every` 的 `intervalMs: 0` 是**配置错误**(抛错),这里却是「关掉定时器、每次累加后立即导出」。两个同名旋钮两种读法 —— 改任何一个之前先看这张表。';
|
|
158
|
+
}, {
|
|
159
|
+
readonly knob: 'AsyncRunner.concurrency';
|
|
160
|
+
readonly where: 'transport/async.ts';
|
|
161
|
+
readonly unit: 'count';
|
|
162
|
+
readonly zero: 'invalid';
|
|
163
|
+
readonly zeroClause: '必须为正数(0 = 没有 worker)';
|
|
164
|
+
readonly badValue: 'throws';
|
|
165
|
+
readonly note: '0 个 worker 的池子会让每个任务永远排在队列里(既不跑也不失败)。';
|
|
166
|
+
}, {
|
|
167
|
+
readonly knob: 'Scheduler.every.intervalMs';
|
|
168
|
+
readonly where: 'transport/scheduler.ts';
|
|
169
|
+
readonly unit: 'ms';
|
|
170
|
+
readonly zero: 'invalid';
|
|
171
|
+
readonly zeroClause: '必须为正有限数(0 会空转)';
|
|
172
|
+
readonly badValue: 'throws';
|
|
173
|
+
readonly note: '`setInterval(0)` 退化成「尽快重复」的空转循环(Node 钳到 1ms,仍是每毫秒一轮忙轮询);超过 2^31-1ms 会被**静默**钳到 1ms,同款失败。';
|
|
174
|
+
}, {
|
|
175
|
+
readonly knob: 'createAnthropicClient.timeout';
|
|
176
|
+
readonly where: 'integrations/anthropic.ts';
|
|
177
|
+
readonly unit: 'ms';
|
|
178
|
+
readonly zero: 'invalid';
|
|
179
|
+
readonly zeroClause: '不设 = 不限;0 = 非法';
|
|
180
|
+
readonly badValue: 'throws';
|
|
181
|
+
readonly note: '「不限」的表达方式是**不传**,不是传 0 —— 与 `runTimeoutMs` 的读法**相反**(那个要求显式传 0)。';
|
|
182
|
+
}, {
|
|
183
|
+
readonly knob: 'metricsSink.windowSize';
|
|
184
|
+
readonly where: 'integrations/metrics.ts';
|
|
185
|
+
readonly unit: 'count';
|
|
186
|
+
readonly zero: 'invalid';
|
|
187
|
+
readonly zeroClause: '必须为正数(0 = 窗口里什么都没有)';
|
|
188
|
+
readonly badValue: 'throws';
|
|
189
|
+
readonly note: '同款的还有 `maxCapabilities` / `maxModels` / `maxScores` —— 都是「必须为正数」。';
|
|
190
|
+
}];
|
|
191
|
+
/** 表里全部旋钮的标识(用例按它建穷尽表:少一项 `typecheck:tests` 就红) */
|
|
192
|
+
export type LimitKnob = (typeof LIMIT_SEMANTICS)[number]['knob'];
|
|
193
|
+
/** 按旋钮取那行(拼错旋钮名 = 编译期报错,不是运行期 undefined) */
|
|
194
|
+
export declare function limitSemanticOf(knob: LimitKnob): LimitSemantic;
|
|
195
|
+
/**
|
|
196
|
+
* 报错文案里那句「(0 = …)」。**构造期校验直接插它**,所以文案与表不可能各说各话。
|
|
197
|
+
*/
|
|
198
|
+
export declare function zeroClauseOf(knob: LimitKnob): string;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentia —— **limits 语义的单一真源**。
|
|
3
|
+
*
|
|
4
|
+
* 为什么要有这个文件(`docs/guards.md` §2 的第一条待守形状):
|
|
5
|
+
*
|
|
6
|
+
* 本仓把「非正数 = 机制关掉」当成一条统一口径,但它被**每个旋钮各自解释**了一遍 ——
|
|
7
|
+
* 同一个 `0` 在这里是「不限」、在那里是「一次都不做」、在第三个地方是「配置错误」。
|
|
8
|
+
* 历史事故就出在这条缝上:`handler.drain({ timeoutMs: 1 })` 跨过 deadline 后**永不返回**
|
|
9
|
+
* (调用方按「1 毫秒 = 马上就超时」写,代码里却是另一套读法),而当时没有任何一处
|
|
10
|
+
* 集中写着「这个旋钮的 0 是什么意思」。改到相关代码时只能靠人去猜 —— 于是又猜错。
|
|
11
|
+
*
|
|
12
|
+
* 本文件把「每个旋钮的 0 是什么」变成**可执行的数据**,`tests/limits.test.ts` 再拿这份
|
|
13
|
+
* 表去**驱动真实站点**逐条对账:表里声明 `0 = 不限`,用例就去调那个真 API 并断言它真的
|
|
14
|
+
* 不限。所以:
|
|
15
|
+
*
|
|
16
|
+
* - 表与实现漂了 ⇒ 集中用例红(不是文档过期,是构建失败);
|
|
17
|
+
* - 新增一个旋钮却忘了归类 ⇒ 用例里 `Record<LimitKnob, …>` 少一项,`typecheck:tests` 红。
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ 本模块**不是公共 API**(不进 `src/index.ts`):它是内部口径的真源,不是给使用者的旋钮。
|
|
20
|
+
* 面向使用者的口径在 `docs/usage-guide.md` 的「边界」一节。
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ 表里的 `zeroClause` 是**报错文案的一部分**:四处构造期校验(`runTimeoutMs` /
|
|
23
|
+
* `approvalTimeoutMs` / `traceLimits.maxEvents` / `maxRetries`)把它插进错误消息里
|
|
24
|
+
* (`…(0 = 不重试),收到 -1 —— …`)。所以「文案里怎么解释 0」与「代码里怎么实现 0」
|
|
25
|
+
* 是同一个值,改一处等于同时改另一处 —— 这正是先前缺的那条单源。
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* 全部 limits 旋钮的 0 语义。**分组按读法**,好让人一眼看出哪几个是相反的。
|
|
29
|
+
*/
|
|
30
|
+
export const LIMIT_SEMANTICS = [
|
|
31
|
+
// ── 0 = 不限(时间的上限被去掉)──────────────────────────────────────────
|
|
32
|
+
{
|
|
33
|
+
knob: 'AsyncRunner.runTimeoutMs',
|
|
34
|
+
where: 'transport/async.ts',
|
|
35
|
+
unit: 'ms',
|
|
36
|
+
zero: 'unlimited',
|
|
37
|
+
zeroClause: '0 = 不限',
|
|
38
|
+
badValue: 'throws',
|
|
39
|
+
note: 'NaN/Infinity 会被 setTimeout 钳到 1ms ⇒ 每个任务立即「超时」失败,且 NaN 会绕过 `< 0` 静默通过;所以要「不限」必须显式传 0。',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
knob: 'AsyncRunner.approvalTimeoutMs',
|
|
43
|
+
where: 'transport/async.ts',
|
|
44
|
+
unit: 'ms',
|
|
45
|
+
zero: 'unlimited',
|
|
46
|
+
zeroClause: '0 = 不限',
|
|
47
|
+
badValue: 'throws',
|
|
48
|
+
note: '同 runTimeoutMs:非有限数会让「已挂起多久」的比较静默失效或立即超时。',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
knob: 'AsyncRunner.drain.timeoutMs',
|
|
52
|
+
where: 'transport/drain-gate.ts',
|
|
53
|
+
unit: 'ms',
|
|
54
|
+
zero: 'unlimited',
|
|
55
|
+
zeroClause: '非正 = 一直等',
|
|
56
|
+
badValue: 'none',
|
|
57
|
+
note: '**历史事故现场**:`http.ts` 的优雅停机把剩余预算算成 `timeoutMs - elapsed` 再交给它,deadline 已过时算出 1 或 0 ⇒ 旧读法(`0` = 已到点)让它跨过 deadline 后**永不返回**。现在 `http.ts` 自己先判「已到点就认账」,不把 0 交给下游去猜。',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
knob: 'mapWithConcurrency.limit',
|
|
61
|
+
where: 'engine/concurrency.ts',
|
|
62
|
+
unit: 'count',
|
|
63
|
+
zero: 'unlimited',
|
|
64
|
+
zeroClause: '非正 = 不限',
|
|
65
|
+
badValue: 'coerced-to-unlimited',
|
|
66
|
+
note: '⚠️ 与 `maxToolConcurrency` 成对:这是**唯一**「数量」旋钮取 0 却读作「不限」的。另有一条反向的坑:`(0,1)` 区间的小数若交给 `Math.floor` 会压成 0 worker ⇒ 工具被静默丢弃,所以正数一律至少 1 个 worker(`maxToolConcurrency: cpus().length / 8` 在多核 < 8 的机器上正落在那个区间)。',
|
|
67
|
+
},
|
|
68
|
+
// ── 0 = 机制关掉 / 一次都不做(数量旋钮;与上面相反)─────────────────────
|
|
69
|
+
{
|
|
70
|
+
knob: 'traceLimits.maxEvents',
|
|
71
|
+
where: 'engine/tracer.ts',
|
|
72
|
+
unit: 'count',
|
|
73
|
+
zero: 'disabled',
|
|
74
|
+
zeroClause: '0 = 一条都不记',
|
|
75
|
+
badValue: 'throws',
|
|
76
|
+
note: '⚠️ 与 `maxEventChars`(管「多长」)正交,一个管「多少」。0 是**有意义的值**(一条都不记,但计数照记)—— 不是「不限条数」。',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
knob: 'maxRetries',
|
|
80
|
+
where: 'integrations/adapter-options.ts',
|
|
81
|
+
unit: 'count',
|
|
82
|
+
zero: 'disabled',
|
|
83
|
+
zeroClause: '0 = 不重试',
|
|
84
|
+
badValue: 'throws',
|
|
85
|
+
note: '坏值必须响亮失败:判定是 `attempt >= maxRetries` ⇒ `NaN` 比较恒假、`Infinity` 永远达不到,两者都是**无限重试**(429 场景下每一次都是真金白银);`-1` 静默变成「不重试」;`1.5` 实际只允许 1 次。',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
knob: 'maxEventChars',
|
|
89
|
+
where: 'engine/tool-events.ts',
|
|
90
|
+
unit: 'chars',
|
|
91
|
+
zero: 'disabled',
|
|
92
|
+
zeroClause: '0 = 截到零长度;false = 不截断',
|
|
93
|
+
badValue: 'none',
|
|
94
|
+
note: '两个「关掉」的写法语义不同:`false` 才是「不截断」(调试期看全文),`0` 是把正文截成空串。⚠️ 靠 `!= null` 判定透传(真值判定会吃掉 `false`)。',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
knob: 'runAgent.maxIterations',
|
|
98
|
+
where: 'engine/loop.ts',
|
|
99
|
+
unit: 'count',
|
|
100
|
+
zero: 'disabled',
|
|
101
|
+
zeroClause: '0 = 一次都不跑',
|
|
102
|
+
badValue: 'none',
|
|
103
|
+
note: '循环写成 `for (iteration = 0; iteration < maxIterations; ...)` ⇒ 0 时循环体一次不执行,直接以「达到循环上限」收尾(**算失败**)。是「已到点」而非「不限」,与 `mapToolConcurrency` 的读法**相反**。',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
knob: 'withTimeout.ms',
|
|
107
|
+
where: 'core/timeout.ts',
|
|
108
|
+
unit: 'ms',
|
|
109
|
+
zero: 'disabled',
|
|
110
|
+
zeroClause: '非正 = 不设超时(原样透传)',
|
|
111
|
+
badValue: 'none',
|
|
112
|
+
note: '`toolTimeoutMs` 的底座。⚠️ 漏透传这个值不是「少一层保险」而是**反的**:`withTimeout(p, 0)` 直接返回原 promise = 永不超时,同时 MCP 桥找不到引擎预算又起自己的 60s 兜底 ⇒ 双计时器、双账本(`toolkit/subagent.ts` / `skill.ts` 的透传注释指的就是这条)。',
|
|
113
|
+
},
|
|
114
|
+
// ── 0 = 立即执行(等待预算为 0 ⇒ 这次等待不存在)─────────────────────────
|
|
115
|
+
{
|
|
116
|
+
knob: 'interruptibleSleep.ms',
|
|
117
|
+
where: 'core/timeout.ts',
|
|
118
|
+
unit: 'ms',
|
|
119
|
+
zero: 'immediate',
|
|
120
|
+
zeroClause: '非正 = 不睡',
|
|
121
|
+
badValue: 'none',
|
|
122
|
+
note: '⚠️ 这一判**先于** aborted 检查 —— 别把它「修」成「已中止就该 reject」。2026-09-19 外部复核把它当一致性缺口改反过一次,被 `tests/core/timeout.test.ts` + `tests/core/sse-text-stats.test.ts` 拦住。',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
knob: 'metricsSink.intervalMs',
|
|
126
|
+
where: 'integrations/metrics.ts',
|
|
127
|
+
unit: 'ms',
|
|
128
|
+
zero: 'immediate',
|
|
129
|
+
zeroClause: '0 = 立即导出',
|
|
130
|
+
badValue: 'none',
|
|
131
|
+
note: '⚠️ **同名反义**:`Scheduler.every` 的 `intervalMs: 0` 是**配置错误**(抛错),这里却是「关掉定时器、每次累加后立即导出」。两个同名旋钮两种读法 —— 改任何一个之前先看这张表。',
|
|
132
|
+
},
|
|
133
|
+
// ── 0 = 非法配置(响亮失败)─────────────────────────────────────────────
|
|
134
|
+
{
|
|
135
|
+
knob: 'AsyncRunner.concurrency',
|
|
136
|
+
where: 'transport/async.ts',
|
|
137
|
+
unit: 'count',
|
|
138
|
+
zero: 'invalid',
|
|
139
|
+
zeroClause: '必须为正数(0 = 没有 worker)',
|
|
140
|
+
badValue: 'throws',
|
|
141
|
+
note: '0 个 worker 的池子会让每个任务永远排在队列里(既不跑也不失败)。',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
knob: 'Scheduler.every.intervalMs',
|
|
145
|
+
where: 'transport/scheduler.ts',
|
|
146
|
+
unit: 'ms',
|
|
147
|
+
zero: 'invalid',
|
|
148
|
+
zeroClause: '必须为正有限数(0 会空转)',
|
|
149
|
+
badValue: 'throws',
|
|
150
|
+
note: '`setInterval(0)` 退化成「尽快重复」的空转循环(Node 钳到 1ms,仍是每毫秒一轮忙轮询);超过 2^31-1ms 会被**静默**钳到 1ms,同款失败。',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
knob: 'createAnthropicClient.timeout',
|
|
154
|
+
where: 'integrations/anthropic.ts',
|
|
155
|
+
unit: 'ms',
|
|
156
|
+
zero: 'invalid',
|
|
157
|
+
zeroClause: '不设 = 不限;0 = 非法',
|
|
158
|
+
badValue: 'throws',
|
|
159
|
+
note: '「不限」的表达方式是**不传**,不是传 0 —— 与 `runTimeoutMs` 的读法**相反**(那个要求显式传 0)。',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
knob: 'metricsSink.windowSize',
|
|
163
|
+
where: 'integrations/metrics.ts',
|
|
164
|
+
unit: 'count',
|
|
165
|
+
zero: 'invalid',
|
|
166
|
+
zeroClause: '必须为正数(0 = 窗口里什么都没有)',
|
|
167
|
+
badValue: 'throws',
|
|
168
|
+
note: '同款的还有 `maxCapabilities` / `maxModels` / `maxScores` —— 都是「必须为正数」。',
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
/** 按旋钮取那行(拼错旋钮名 = 编译期报错,不是运行期 undefined) */
|
|
172
|
+
export function limitSemanticOf(knob) {
|
|
173
|
+
const found = LIMIT_SEMANTICS.find((s) => s.knob === knob);
|
|
174
|
+
// 表是 `as const` 的封闭集合,knob 类型来自它 ⇒ 这里不可能落到 undefined;
|
|
175
|
+
// 保留分支是为了让「表被改空」时给出可读的失败而不是 TypeError。
|
|
176
|
+
if (!found)
|
|
177
|
+
throw new Error(`limits 表里没有旋钮 ${knob}(表被改空了?)`);
|
|
178
|
+
return found;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 报错文案里那句「(0 = …)」。**构造期校验直接插它**,所以文案与表不可能各说各话。
|
|
182
|
+
*/
|
|
183
|
+
export function zeroClauseOf(knob) {
|
|
184
|
+
return limitSemanticOf(knob).zeroClause;
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/core/limits.ts"],"names":[],"mappings":"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"}
|