@migor/agentia 0.10.2 → 0.10.4

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.
Files changed (144) hide show
  1. package/CHANGELOG.md +969 -64
  2. package/LICENSE +1 -1
  3. package/README.en.md +93 -38
  4. package/README.md +105 -218
  5. package/dist/AGENTS.md +234 -41
  6. package/dist/core/limits.d.ts +33 -1
  7. package/dist/core/limits.js +37 -1
  8. package/dist/core/limits.js.map +1 -1
  9. package/dist/core/run.d.ts +2 -2
  10. package/dist/core/timeout.d.ts +5 -5
  11. package/dist/core/timeout.js +5 -5
  12. package/dist/core/tool.d.ts +2 -2
  13. package/dist/core/trace-sink.d.ts +8 -0
  14. package/dist/core/trace-sink.js +24 -0
  15. package/dist/core/trace-sink.js.map +1 -0
  16. package/dist/core/trace.d.ts +12 -2
  17. package/dist/core/trace.js +10 -1
  18. package/dist/core/trace.js.map +1 -1
  19. package/dist/engine/concurrency.d.ts +1 -1
  20. package/dist/engine/concurrency.js +1 -1
  21. package/dist/engine/errors.d.ts +2 -2
  22. package/dist/engine/errors.js +2 -2
  23. package/dist/engine/loop-result.d.ts +3 -3
  24. package/dist/engine/loop.js +99 -56
  25. package/dist/engine/loop.js.map +1 -1
  26. package/dist/engine/mcp-server-http.js +1 -1
  27. package/dist/engine/mcp-server-http.js.map +1 -1
  28. package/dist/engine/mcp-server-stdio.js +7 -3
  29. package/dist/engine/mcp-server-stdio.js.map +1 -1
  30. package/dist/engine/mcp-server.d.ts +17 -2
  31. package/dist/engine/mcp-server.js +42 -17
  32. package/dist/engine/mcp-server.js.map +1 -1
  33. package/dist/engine/resume-input.d.ts +1 -1
  34. package/dist/engine/resume-input.js +1 -1
  35. package/dist/engine/retry.d.ts +15 -0
  36. package/dist/engine/retry.js +51 -2
  37. package/dist/engine/retry.js.map +1 -1
  38. package/dist/engine/span-scope.d.ts +15 -0
  39. package/dist/engine/span-scope.js +8 -1
  40. package/dist/engine/span-scope.js.map +1 -1
  41. package/dist/engine/spec.d.ts +5 -1
  42. package/dist/engine/spec.js.map +1 -1
  43. package/dist/engine/tracer.d.ts +8 -4
  44. package/dist/engine/tracer.js +24 -17
  45. package/dist/engine/tracer.js.map +1 -1
  46. package/dist/engine/turn.d.ts +2 -2
  47. package/dist/engine/turn.js +19 -4
  48. package/dist/engine/turn.js.map +1 -1
  49. package/dist/engine/types.d.ts +13 -5
  50. package/dist/engine/types.js +1 -0
  51. package/dist/engine/types.js.map +1 -1
  52. package/dist/eval/export.js +2 -2
  53. package/dist/eval/export.js.map +1 -1
  54. package/dist/eval/harvest.d.ts +4 -1
  55. package/dist/eval/harvest.js +32 -6
  56. package/dist/eval/harvest.js.map +1 -1
  57. package/dist/index.d.ts +3 -1
  58. package/dist/index.js +5 -2
  59. package/dist/index.js.map +1 -1
  60. package/dist/integrations/anthropic.d.ts +1 -1
  61. package/dist/integrations/anthropic.js +1 -1
  62. package/dist/integrations/anthropic.js.map +1 -1
  63. package/dist/integrations/file-sink.d.ts +0 -16
  64. package/dist/integrations/file-sink.js +24 -6
  65. package/dist/integrations/file-sink.js.map +1 -1
  66. package/dist/integrations/mcp-http.js +4 -3
  67. package/dist/integrations/mcp-http.js.map +1 -1
  68. package/dist/integrations/mcp-protocol.d.ts +35 -10
  69. package/dist/integrations/mcp-protocol.js +19 -1
  70. package/dist/integrations/mcp-protocol.js.map +1 -1
  71. package/dist/integrations/mcp-stdio.js +91 -50
  72. package/dist/integrations/mcp-stdio.js.map +1 -1
  73. package/dist/integrations/mcp.d.ts +1 -1
  74. package/dist/integrations/mcp.js +19 -3
  75. package/dist/integrations/mcp.js.map +1 -1
  76. package/dist/integrations/metrics-otlp.d.ts +1 -1
  77. package/dist/integrations/metrics-otlp.js +8 -2
  78. package/dist/integrations/metrics-otlp.js.map +1 -1
  79. package/dist/integrations/metrics-render.js +17 -6
  80. package/dist/integrations/metrics-render.js.map +1 -1
  81. package/dist/integrations/metrics-state.d.ts +2 -2
  82. package/dist/integrations/metrics-state.js +4 -4
  83. package/dist/integrations/metrics-state.js.map +1 -1
  84. package/dist/integrations/metrics.d.ts +2 -2
  85. package/dist/integrations/otlp.d.ts +29 -6
  86. package/dist/integrations/otlp.js +128 -7
  87. package/dist/integrations/otlp.js.map +1 -1
  88. package/dist/integrations/report.d.ts +14 -2
  89. package/dist/integrations/report.js +39 -6
  90. package/dist/integrations/report.js.map +1 -1
  91. package/dist/runtime/run.d.ts +1 -1
  92. package/dist/runtime/run.js +19 -17
  93. package/dist/runtime/run.js.map +1 -1
  94. package/dist/store/fsStore.js +9 -5
  95. package/dist/store/fsStore.js.map +1 -1
  96. package/dist/store/record.d.ts +2 -2
  97. package/dist/store/record.js +1 -1
  98. package/dist/store/sqliteStore.d.ts +2 -2
  99. package/dist/store/sqliteStore.js +3 -3
  100. package/dist/store/sqliteStore.js.map +1 -1
  101. package/dist/store/store.d.ts +27 -7
  102. package/dist/store/store.js +43 -5
  103. package/dist/store/store.js.map +1 -1
  104. package/dist/toolkit/collect.d.ts +11 -5
  105. package/dist/toolkit/collect.js +10 -4
  106. package/dist/toolkit/collect.js.map +1 -1
  107. package/dist/toolkit/discover.js +27 -1
  108. package/dist/toolkit/discover.js.map +1 -1
  109. package/dist/toolkit/module.js +183 -50
  110. package/dist/toolkit/module.js.map +1 -1
  111. package/dist/toolkit/prompt.d.ts +1 -1
  112. package/dist/toolkit/prompt.js +2 -2
  113. package/dist/toolkit/prompt.js.map +1 -1
  114. package/dist/toolkit/subagent.d.ts +1 -1
  115. package/dist/toolkit/subagent.js +17 -3
  116. package/dist/toolkit/subagent.js.map +1 -1
  117. package/dist/transport/approval-policy.js +1 -1
  118. package/dist/transport/approval-policy.js.map +1 -1
  119. package/dist/transport/approval-supervisor.d.ts +8 -0
  120. package/dist/transport/approval-supervisor.js +23 -3
  121. package/dist/transport/approval-supervisor.js.map +1 -1
  122. package/dist/transport/async.d.ts +3 -0
  123. package/dist/transport/async.js +128 -25
  124. package/dist/transport/async.js.map +1 -1
  125. package/dist/transport/http-endpoints.js +1 -1
  126. package/dist/transport/http-route.js +2 -2
  127. package/dist/transport/http-route.js.map +1 -1
  128. package/dist/transport/http-shapes.d.ts +1 -1
  129. package/dist/transport/http-shapes.js +1 -1
  130. package/dist/transport/resume-scanner.d.ts +7 -0
  131. package/dist/transport/resume-scanner.js +40 -9
  132. package/dist/transport/resume-scanner.js.map +1 -1
  133. package/dist/transport/run-call-options.js +7 -3
  134. package/dist/transport/run-call-options.js.map +1 -1
  135. package/dist/transport/scheduler.js +5 -0
  136. package/dist/transport/scheduler.js.map +1 -1
  137. package/dist/transport/serve.d.ts +92 -0
  138. package/dist/transport/serve.js +155 -0
  139. package/dist/transport/serve.js.map +1 -0
  140. package/dist/transport/signal-supervisor.d.ts +10 -2
  141. package/dist/transport/signal-supervisor.js +15 -2
  142. package/dist/transport/signal-supervisor.js.map +1 -1
  143. package/dist/transport/wake-policy.d.ts +1 -1
  144. package/package.json +23 -10
package/dist/AGENTS.md CHANGED
@@ -41,10 +41,41 @@
41
41
 
42
42
  ## 1. 最小可运行示例
43
43
 
44
+ ### 1.1 先跑一个裸工具(不需要装饰器 / DI)
45
+
44
46
  ```ts
45
- import { Tool, createApp, SystemPrompt } from '@migor/agentia';
47
+ import { createApp } from '@migor/agentia';
48
+
49
+ const app = createApp({
50
+ system: '你是友好的助手。',
51
+ tools: [
52
+ {
53
+ name: 'say_hello',
54
+ description: '向某人打招呼',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: { name: { type: 'string' } },
58
+ required: ['name'],
59
+ },
60
+ run: (input) => `你好,${(input as { name: string }).name}`,
61
+ },
62
+ ],
63
+ });
64
+
65
+ const { result } = await app.run([{ role: 'user', content: '跟小明打个招呼' }]);
66
+ if (result.error) throw new Error(`${result.stopReason}: ${result.error.message}`);
67
+ console.log(result.finalText, result.trace.totalUsage);
68
+ ```
69
+
70
+ 这是最短路径:`tools + createApp + app.run`。`inputSchema` 约束模型入参,`run` 是实际函数;
71
+ `result.error` 必须检查,不能只打印 `finalText`。`trace` 从第一次 run 就在,不需要先配观测后端。
46
72
 
47
- const OBJ = { type: 'object', properties: {} } as const;
73
+ 也可以直接 `npx @migor/cli create my-app --minimal` 生成同形态的单文件工程(见下一节)。
74
+
75
+ ### 1.2 能力变多后再用装饰器组织
76
+
77
+ ```ts
78
+ import { Tool, createApp, SystemPrompt } from '@migor/agentia';
48
79
 
49
80
  class Greeter {
50
81
  @Tool({
@@ -80,21 +111,30 @@ console.log(result.finalText, result.stopReason, result.trace.totalUsage);
80
111
  ## 2. 项目结构(CLI 约定)
81
112
 
82
113
  ```bash
83
- npx @migor/cli create my-app # 首次创建:必须带 scope(短名 agentia 在 npm 上是别人的包)
114
+ # 只想先跑一个 agent:单文件 + 裸工具,无装饰器 / registry / HTTP 宿主
115
+ npx @migor/cli create my-app --minimal
84
116
  cd my-app && npm install # 框架与 CLI 都装进工程
85
117
  $EDITOR .env # 填 ANTHROPIC_API_KEY(脚手架已生成,且已被 .gitignore 挡住)
118
+ npm run dev -- "你的问题" # = tsx src/main.ts,跑一次并退出
119
+
120
+ # 需要 inspector、能力目录与生产服务时:不带 --minimal 的默认完整模板
121
+ npx @migor/cli create my-service
122
+ cd my-service && npm install
86
123
  npm run dev # = agentia dev:本地 inspector 面板(输入 prompt / 选能力 / 选工作目录)
87
- npx agentia g tool fetch-weather # 生成到 src/tools/fetch-weather/(skill/prompt/subagent 同理)
124
+ npx agentia g tool fetch-weather # 生成到 src/tools/fetch-weather/(skill/prompt/subagent 同理)
88
125
  npx agentia doctor # 静态体检(未登记/悬空/命名/重复)
89
126
  npx agentia --version # CLI 版本(= -v)
90
127
  ```
91
128
 
129
+ **两种模板只是起点不同**:minimal 用于先跑通模型与工具,完整模板用于继续组织能力和交付服务;
130
+ 默认仍是不带 `--minimal` 的完整服务模板,既有项目与脚本不变。
131
+
92
132
  > **命令从哪来**:脚手架把 `@migor/cli` 装进工程的 `devDependencies`,所以**工程内**用短名
93
133
  > `npx agentia …` 即可(走本地 bin:离线可用、版本与工程一同 pin)。**首次创建**必须用带 scope 的
94
134
  > `npx @migor/cli create` —— npm 上另有一个别人的 `agentia` 包,短名会装错东西。
95
135
  > 想全局装上(到处都能敲 `agentia`):`npm i -g @migor/cli`。
96
136
 
97
- 四分类目录,一能力一文件夹:`src/tools/` · `src/skills/` · `src/prompts/` · `src/subagents/` —— **目录名就是类型**,不用记别名。每个文件夹的 `index.ts` 是入口,`default export` 支持三种形态:**类**(token = 文件夹名)、**Provider 对象**、**Provider 数组**。显式注册表在 `src/registry.ts`(`agentia g` 自动维护,也可手改)。
137
+ 四分类目录,一能力一文件夹:`src/tools/` · `src/skills/` · `src/prompts/` · `src/subagents/` —— **目录名就是类型**,不用记别名。每个文件夹的 `index.ts` 是入口,`default export` 支持三种形态:**类**(token = 文件夹名)、**Provider 对象**、**Provider 数组**。类形态要注入依赖时,在类上写 `static readonly deps = ['WORKDIR']` —— `discover` 会把它带上(**不必**把该能力挪到显式 `providers` 再注册一遍,那样同一 token 会有两份、靠「后注册覆盖先注册」碰巧成立);`deps` 写成非字符串数组会**响亮报错**,不当成「没有依赖」。显式注册表在 `src/registry.ts`(`agentia g` 自动维护,也可手改)。
98
138
 
99
139
  ### 2.1 脚手架生成的文件(**装配与启动是分开的**)
100
140
 
@@ -206,15 +246,40 @@ token 的作用是挡住**本机其它进程**,别把它当网络边界:这
206
246
 
207
247
  ```bash
208
248
  agentia report .agentia/traces.jsonl # 哪个能力慢 / 贵 / 爱失败
249
+ agentia view .agentia/traces.jsonl --open # 把落盘的 trace 画成一棵树(单文件 HTML,见下)
209
250
  agentia diff .agentia/traces.jsonl ... # 改 prompt 前后逐 span 比对(有差异时退出码 1)
210
251
  agentia harvest .agentia/traces.jsonl --failed # 失败的 run → eval 用例骨架
211
252
  agentia export .agentia/traces.jsonl # → 训练数据集 JSONL
212
253
  ```
213
254
 
255
+ **`agentia view` —— 事后看一棵树**(面板只管「在飞」的,这个管「盘上」的):
256
+
257
+ ```bash
258
+ agentia view .agentia/traces.jsonl --open # 渲完直接用默认浏览器打开
259
+ agentia view .agentia/traces.jsonl -o r.html # 指定产物名(缺省 = 输入名去扩展名 .html)
260
+ agentia view .agentia/traces.jsonl --trace-id <id> # 只看一条
261
+ agentia view .agentia/traces.jsonl --limit 20 # 默认最近 50 条(超了会明说,不静默截断)
262
+ agentia view .agentia/traces.jsonl --limit 20 --head # 从头看(排查「第一次出现的问题」时用)
263
+ ```
264
+
265
+ ⚠️ **`--limit` 缺省取「最近」N 条,不是前 N 条**:`.agentia/traces.jsonl` 只增不减,最新的一次
266
+ run 在**文件末尾** —— 取最早那种写法在长跑 dev 环里等于「想复盘刚才那次,给你今早第一批」。
267
+ 同一节奏:产物缺省写在输入旁边,但输入在**点目录**里(`.agentia/`)时**上跳到最近的可见目录**,
268
+ 不把 HTML 藏进隐藏目录;产物路径等于输入文件本身会直接报错(否则把 trace 数据覆盖成 HTML);
269
+ 覆盖同名文件会在 stdout 明说(HTML 是派生物,重跑是常态,所以不像 `harvest` 那样要 `--force`)。
270
+
271
+ 产物是**单文件自包含 HTML**:CSS 与渲染器**全部内联**,零外链、离线可开、可以直接贴进 PR 或
272
+ 发给同事(业界同类工具在 2026 已收敛到这个形态)。⚠️ 三件事要知道:
273
+ ① **默认脱敏**(`--raw` 关闭,且此时 stderr 会警告)—— 产物的默认命运是「被转发」,而 trace
274
+ 含完整 prompt 与工具出入参;② 脱敏是 **best-effort**,覆盖 6 类值内凭证形态
275
+ (bearer / jwt / aws-access-key / llm-api-key / email / phone-cn),**`password=` 这类不在内**,
276
+ 业务自有字段用 `--redact-key <字段名>`(按字段名整字段抹平,与前者是两种机制);
277
+ ③ 多条 trace 时顶部下拉切换,`#N` 锚点可直达。
278
+
214
279
  落盘**不替代**右栏的实时树:那是两条缝(落盘管「事后能查」,`onTraceEvent` 管「此刻看到」)。
215
280
  三条如实标注:① 文件**只增不减**(要清是你自己 `rm`);② 落盘是**同步写**,单条 trace 以百 KB
216
281
  计、且 `traceLimits.maxEvents` 本来兜上限;③ 面板的 run 列表是**内存** ring(50 条),
217
- 重启就空了 —— 落盘让数据**没丢**,但面板不会替你回看历史。
282
+ 重启就空了 —— 落盘让数据**没丢**,但面板不会替你回看历史,**回看历史走 `agentia view`**。
218
283
 
219
284
  **并排 A/B(比两条 run差在哪)**:run 列表里 **⌘/Ctrl + 单击**选两条(再点一次取消、
220
285
  选第三条挤掉最早那条),右栏切**双栏**并排两棵调用树,顶部一条差异摘要
@@ -248,6 +313,10 @@ agentia export .agentia/traces.jsonl # → 训练数据集 JSONL
248
313
 
249
314
  ## 3. 装饰器 spec 字段速查
250
315
 
316
+ 四个装饰器都只接**实例方法**:私有方法(`#method`)与静态方法(`static`)都在装饰期直接抛错
317
+ —— 能力收集沿实例原型链扫描(调用要绑定 `this`),私有名不可见、静态方法挂在构造函数上,
318
+ 不拦的话能力会静默从菜单里消失。唯一例外是 `@Prompt`(纯文本资产),静态方法同样收集(见下)。
319
+
251
320
  ### `@Tool(spec: ToolSpec)`
252
321
 
253
322
  | 字段 | 说明 |
@@ -321,7 +390,7 @@ agentia export .agentia/traces.jsonl # → 训练数据集 JSONL
321
390
  | `retry` | 缺省模型请求重试策略(可被单次 run 覆盖):缺省**开启**(`DEFAULT_RETRY`:maxAttempts=3、指数退避 + 抖动);`false` 关闭 |
322
391
  | `contextPolicy` | 上下文预算策略(`createBudgetPolicy(...)`);带状态的策略应实现 `forRun()` 按 run 隔离(见 §长上下文) |
323
392
  | `toolSources` | 白名单:只把这些 provider 的能力放进主菜单 |
324
- | `tools` | 直接追加到主菜单的**裸工具**(`AgentTool[]`):给「构造期才知道有哪些工具」的场合(典型:MCP 桥,见 §6)。与能力**同过中间件、同进重名查重**,不是旁路 |
393
+ | `tools` | 直接追加到主菜单的**裸工具**(`AgentTool[]`):给「构造期才知道有哪些工具」的场合(典型:MCP 桥,见 §6)。与能力**同过中间件、同进重名查重**,不是旁路。重名查重覆盖**主菜单 + 子菜单**(@SubAgent/@Skill 的 `tools` 引用展开结果也查,报错指到哪个能力的哪个引用片) |
325
394
  | `middleware` | 能力调用中间件(洋葱链,链序 = 注册顺序) |
326
395
  | `sinks` | trace 出口,run 收尾投递 |
327
396
  | `onTraceEvent` | **增量记账出口的缺省值**:run **进行中**逐笔回调(`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`),给等不了收尾的消费者(面板 / SSE / 任务流)。与 `sinks` 是**两条缝**、可同时配;单次 run 给了自己的那个是**叠加**(应用级在前)而非覆盖。⚠️ **不保证送达**(宿主自己的流断了就断了),也不替代 `sinks` |
@@ -366,7 +435,7 @@ agentia export .agentia/traces.jsonl # → 训练数据集 JSONL
366
435
  | `maxToolConcurrency` | 同回合并行工具上限;缺省不限 |
367
436
  | `maxEventChars` | trace 事件正文截断上限(字符):数字 = 入参/出参统一用该上限,`false` = **不截断**;缺省按类型收敛(入参/成功出参 2000、失败出参 1000)。**透传给子 agent/skill 的子循环** —— 同一棵调用树上口径一致。只影响**记账**,回给模型的 tool_result 永远完整 |
368
437
  | `session` | 会话持久化 `{ store, id }`:run 前拼历史、成功收尾追加本轮(见 `SessionStore`) |
369
- | `sessionId` | 会话**引用**(可序列化):单次覆盖后**记进 run 根 `session.id`**(引擎侧读它,见 `loop.ts` 的属性写入)。⚠️ 直接 `app.run` 时它**不注入历史** —— 注入要上一行的 `session: { store, id }`;「`sessionId` + runner 的 `sessionStore`」是**异步宿主**那条路(任务里只落 id,store 实例不可序列化) |
438
+ | `sessionId` | 会话**引用**(可序列化):**只被异步宿主消费** —— 「`sessionId` + runner 的 `sessionStore`」那条路(任务里只落 id,store 实例不可序列化;runner 没配 `sessionStore` 而任务带了它 ⇒ submit 当场报错)。⚠️ 直接 `app.run` 传它**不生效**(不建 `session.id`、不接历史),入口会 `console.warn` 一次指出改用上一行的 `session: { store, id }`;只要 run 根的 `session.id` 标记而不要历史,直连 `executeRun` / `runAgent` 的引擎级 `sessionId`(见 §6.4「提示词版本化与会话标记」) |
370
439
  | `memory` | 跨 run 记忆 `{ store, keys }`:run 前水合进 blackboard(用户种子优先)、收尾写回;与 `session` 正交(见 `MemoryStore`) |
371
440
  | `beforeFlush` | `(trace, result) => void \| Promise<void>`:**sinks 冲刷之前**的最后一笔(run 正常收尾后调一次,抛错被吞)。给「**拿到结果才判得出**的结论」用的缝 —— 典型是 `defineEval` 的 score:等 `app.run` 返回再 `attachScore`,`metricsSink` 早在导出那一刻聚完账,分数就永远进不了指标。读 trace 就够的判断不必用它,写进 sinks 里即可(见 §6 判官配方) |
372
441
  | `approvals` | HITL 审批决定(`Record<tool_use_id, ApprovalDecision>`):恢复 `suspended` 的 run 时传入(异步宿主会自动带,见 §6.6「人工审批」);手工续跑「assistant 结尾带 tool_use」的消息历史时也可直接给 |
@@ -441,7 +510,7 @@ const { result } = await app.run(messages, {
441
510
  result.typed; // { answer: string } | undefined
442
511
  ```
443
512
 
444
- 模型没提交就是 `undefined`(不是失败)。`stopReason` 仍以 `end_turn` 正常收尾。
513
+ 模型没提交时 `typed` 为 `undefined`,且 run 以 `stopReason='result_missing'` **失败**(trace 同样记 `error`,`error.type='result_missing'`);不再把「声明了结构化结果却没收尾」当成功。子 agent 声明 `resultSchema` 后未提交也会以 `is_error` 回主 agent。
445
514
 
446
515
  ### 5.4 消息类型族(自有公共类型,与厂商 SDK 结构兼容)
447
516
 
@@ -561,6 +630,12 @@ const app = await createApp({ ... });
561
630
  ⚠️ 框架**刻意不替你调**:`drain()` 不关 store(同一个 store 可能被调度器或另一个宿主共用,
562
631
  关掉它是**宿主的**生命周期决定)。压实频率建议低频(如 cron 每小时一次)。
563
632
 
633
+ **副本契约(自定义 store 必读)**:`save` 存**副本**、`get` / `byIdempotency` / `list` 交**副本** ——
634
+ 内存推进只许由 `save` 驱动。内建实现经 `cloneTaskRecord` 坐实(深拷纯数据;`AbortSignal` /
635
+ 函数等进程内对象按引用保留,不是序列化往返);sqlite / redis 走序列化天然满足。自实现若交
636
+ 活引用,「改 rec → `await save`」的纪律会让未落库的中间态对其他读者可见,save 失败时
637
+ 污染留在库里(approve 重试撞 409、任务搁死到重启那一类)。
638
+
564
639
  #### gRPC 宿主(第 4 个宿主,**框架不内置**)
565
640
 
566
641
  gRPC 不是「另一种 broker」,它与 HTTP 是同一档的东西:**触发宿主**。所以正确形状与 HTTP 宿主
@@ -579,7 +654,7 @@ MCP stdio 只用标准库(`spawn` + 全局 `fetch`)⇒ **内置不新增一
579
654
  gRPC 必须引第三方客户端(`@grpc/grpc-js`)⇒ 落在「可选能力一律 duck-typed / peer」那一侧。
580
655
  「零运行时依赖」是公开承诺(`package.json` 三个依赖字段全空),所以它只能以**配方 + 示例**存在。
581
656
  真有第二个使用方要同一份逻辑时再考虑独立包,且粒度是**一个第三方客户端一个包** ——
582
- 不是把所有集成塞进一个「服务包」(理由见 spec §10 2026-09-18 ⑪)。
657
+ 不是把所有集成塞进一个「服务包」(理由见 spec §10 2026-09-18 ref-host-integration-recipe)。
583
658
 
584
659
  **宿主必须自己接上的四处**(漏掉任何一条都**不会报错**,只会静默丢东西):
585
660
 
@@ -650,6 +725,38 @@ npm run client # 另一个终端:把四个 RPC 跑一遍
650
725
  方法不符 → 405(带 `Allow` 头);路径不符 → 404;body 非法 JSON → 400;body 超 `maxBodyBytes` → 413;
651
726
  `POST /run` 超 `maxConcurrentRuns` → 503 + `Retry-After`;停机中 `POST /run`、`POST /tasks` → 503。
652
727
 
728
+ #### 本地怎么调(可直接复制的命令)
729
+
730
+ ⚠️ 先认下这条,否则会照着别的框架的经验找错东西:**没有「一个业务一个 URL」的接口层**。
731
+ HTTP 面就上面那几条**固定**端点 —— 加一个 `@Tool` 不会多出一条路,**调哪个能力由模型决定,
732
+ 不在 URL 里**(这是「换宿主不换语义」的另一半:换 HTTP / gRPC / MCP,能力代码一行不动)。
733
+ ⇒ 要「一个能力一个接口」的形态,走下面的 ④;要常规联调,走 ①②③。
734
+
735
+ | # | 想干什么 | 怎么做 |
736
+ |---|---|---|
737
+ | ① | **不起服务**,最快跑一次 | `npm run start:batch -- "你的问题"`(脚手架的 `batch.ts`:只跑一轮、打印 `finalText`、失败置非零退出码) |
738
+ | ② | 起了服务后用 curl 调 | 见下面的命令块 |
739
+ | ③ | 带调用树可视化地调 | `agentia dev` —— 终端打出 `Inspector: http://127.0.0.1:<随机端口>/?t=<token>`;面板里选能力 / 工作目录、输 prompt,右栏实时调用树,改代码自动重启 |
740
+ | ④ | **单挑某一个工具**(不让模型决定) | `createMcpServer(app, { transport: 'stdio' })` ⇒ 每个 `@Tool` 变成一个标准 MCP tool,用 MCP inspector 或 `tools/call` 直接调(见 §6.5「MCP 反向桥」) |
741
+
742
+ ```bash
743
+ curl -s localhost:3000/healthz # 先确认活着(不鉴权)
744
+ curl -s localhost:3000/run \
745
+ -H 'content-type: application/json' \
746
+ -d '{"prompt":"介绍一下你自己"}' # body 也收裸字符串 "..." 与 messages 数组
747
+ curl -sN localhost:3000/run \
748
+ -H 'content-type: application/json' \
749
+ -H 'Accept: text/event-stream' -d '"你的问题"' # -N 关掉 curl 缓冲 ⇒ 逐帧滚动
750
+ curl -s localhost:3000/tasks \
751
+ -H 'content-type: application/json' \
752
+ -d '{"input":"你的问题"}' # → 202 TaskRecord(拿 id 去 GET /tasks/:id 轮询)
753
+ ```
754
+
755
+ ⚠️ **两个别只看 curl 退出码的判据**:
756
+ ① `POST /run` **失败也回 200** —— 硬失败写在响应体的 `status: "failed"` + `error` 字段里
757
+ (`rethrow:false` 语义:不用 HTTP 错误码表达失败)⇒ 脚本里要判 `status`,不是判 `$?`;
758
+ ② SSE 那条要加 `-N`,否则 curl 自己缓冲着,看起来像「服务没吐东西」。
759
+
653
760
  **换 model client 的缝**:HTTP 宿主**不持有 client** —— 同步 `/run` 走的是 `app.run(messages, opts)`,
654
761
  而 `AppCallable` 就是 `{ name, run }`。所以要换 provider(OpenAI 兼容端点 / 自建 client),
655
762
  **包一层**把 `client` 补进 `opts` 即可:
@@ -661,7 +768,7 @@ const handler = createHttpHandler(callable, { runner });
661
768
 
662
769
  异步侧更直接:`AsyncRunner` 的构造选项就有 `client`。完整可跑写法见仓库 `examples/complete/`
663
770
  (**不随 npm 包发布** —— 包里只有 `dist/`、README、LICENSE 与本说明):
664
- <https://github.com/retrychx/agentia/tree/main/examples/complete>
771
+ <https://github.com/migoric/agentia/tree/main/examples/complete>
665
772
 
666
773
  #### `createHttpHandler(app, opts?: HttpHandlerOptions)`
667
774
 
@@ -685,21 +792,43 @@ const handler = createHttpHandler(callable, { runner });
685
792
  `abort` 收尾的,不是排空的;只关掉**旁观者长连**(`GET /tasks/:id/stream`)⇒ 不损失任何工作,
686
793
  不影响这个布尔。宿主的 SIGTERM 决策(退出 / 再等)就依赖它。
687
794
  **框架不订阅信号** —— `process.on('SIGTERM', () => handler.drain())` 是宿主的事(同「框架不读 env」)。
795
+ 不想自己写那 ~20 行停机样板就用 `serve()` + `gracefulShutdown()`(2026-10-22 ref-serve-graceful-shutdown):它们把
796
+ 「起监听 / 收连接 / 二次信号 / 停完回调」收进实现,但仍要**宿主显式调用、显式传 `signals`**
797
+ —— 不传就一个信号都不订阅。
688
798
  - **`handler.runner`** —— 内部 `AsyncRunner`,需要时手动控制(`resumePending` / `awaitTask` / `list`)。
689
799
 
690
800
  ```ts
801
+ import {
802
+ createHttpHandler,
803
+ gracefulShutdown,
804
+ serve,
805
+ HttpException,
806
+ } from '@migor/agentia';
807
+
691
808
  const handler = createHttpHandler(app, {
692
809
  // 只给缝:token/JWT/签名策略由你的宿主或反代实现
693
810
  authenticate: (req) => {
694
811
  if (req.headers['x-api-key'] !== expected) throw new HttpException(401, { error: '无效凭据' });
695
812
  },
696
813
  });
697
- http.createServer(handler).listen(3000);
698
814
 
699
- process.on('SIGTERM', async () => {
700
- const clean = await handler.drain({ timeoutMs: 15_000 });
701
- console.log(clean ? '已排空' : '超时收口,剩余任务下次启动续跑');
702
- process.exit(0);
815
+ // `serve` = createServer + listen + 等真的在听;`port` 是**实际**端口(传 0 时由 OS 分配)。
816
+ // 端口冲突 / 权限不足直接 reject —— boot 阶段静默起不来比崩更难查。
817
+ serve(handler, { port: 3000 }).then(({ server }) => {
818
+ gracefulShutdown({
819
+ server,
820
+ drain: () => handler.drain({ timeoutMs: 15_000 }),
821
+ // 不传 = 一个信号都不订阅(框架不做进程级决策);传了即宿主显式认领
822
+ signals: ['SIGTERM', 'SIGINT'],
823
+ timeoutMs: 15_000,
824
+ // 第二次信号:本原语**不吞** —— 给了这个钩子就交给你,没给就摘掉监听让 Node 默认行为接管
825
+ onSecondSignal: () => process.exit(1),
826
+ // 停机完成(`clean` = 排空且连接关干净)。本原语自己不 exit —— 退出码是宿主的编排
827
+ onShutdown: (clean) => {
828
+ console.log(clean ? '已排空' : '超时收口,剩余任务下次启动续跑');
829
+ process.exit(clean ? 0 : 1);
830
+ },
831
+ });
703
832
  });
704
833
  ```
705
834
 
@@ -728,7 +857,7 @@ process.on('SIGTERM', async () => {
728
857
  | `mapWithConcurrency` | 有界并发 map(结果保序);`maxToolConcurrency` 的底座,也可自用 |
729
858
 
730
859
  - **取消**:`app.run(messages, { signal })` 传 `AbortSignal` —— 框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发 `signal`),run 以 `stopReason='aborted'` 收尾(算失败)。`createHttpHandler` 已内置「客户端断开即中止」;`AsyncRunner.runTimeoutMs` 到点同样是**真中止**(构造期校验:必须 ≥ 0 的有限数、且不超过 `2^31-1`ms ≈ 24.86 天 —— NaN/Infinity 与超上限的值都会被 `setTimeout` 钳到 1ms,等于每个任务立即超时,故直接抛错;要「不限」传 0 或不设)。
731
- - **重试**:缺省自动重试可重试失败(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 永不达到,两者都等于**无限重试**(且静默)。
860
+ - **重试**:缺省自动重试可重试失败(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 永不达到,两者都等于**无限重试**(且静默)。引擎层 `retry` 的数值旋钮同款纪律:`maxAttempts` 只收非负安全整数(`0` = 关闭重试),`baseDelayMs` / `maxDelayMs` 只收 0 ~ 2^31-1ms 的有限数(`0` = 不退避 / 封顶为零),`jitter` 只收 0~1 —— 坏值在 run 入口抛 `TypeError`(`maxAttempts: NaN` 曾静默关闭重试、`baseDelayMs: NaN` 曾被误报成「用户取消」)。
732
861
  - **流式**:`POST /run` 带 `Accept: text/event-stream` → SSE 逐帧下发(`text.delta` / `run.end` / `error`,外加一族 **`trace.event`** —— 增量记账事件,body 即 `TraceRecordEvent`,用它做实时面板;不认识这一族的老客户端行为零变化);不带该头仍回一元 JSON。
733
862
  - **工具超时 / 并发闸门**:`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`。
734
863
 
@@ -815,12 +944,12 @@ trace 出去之后能干什么:指标、调用树面板、调优报告、生
815
944
 
816
945
  | API | 说明 |
817
946
  |---|---|
818
- | `TraceSink` | `{ export(trace) }`,run 收尾(成功/失败)都投递,抛错被吞(吞之前落一条 `console.warn`,文案含「trace sink」) |
947
+ | `TraceSink` | `{ export(trace) }`,run 收尾(成功/失败)都投递。抛错只告警、不影响 run;单个 sink 默认最多等待 10 秒,超时同样告警并放弃等待(**不取消底层导出**),后续 sink 仍照常接收。run 宿主与 MCP 反向桥共用这套口径 |
819
948
  | `registerDefaultTraceSink` | 注册全局默认 sink(构造期快照合并) |
820
949
  | `TraceRecorder` | 内存 recorder(一次 run 一个);`addLink(spanId, { traceId, spanId? })` 记一条跨 trace 链路(见 §6「跨进程关联」) |
821
950
  | `parseTraceparent` | 解析 W3C `traceparent` 头 → `{ traceId, spanId? }`;**非法 / 缺头一律返回 `undefined`**(不抛、不打 400)—— 结果直接交给 `traceContext` 选项,见 §6「跨进程关联」 |
822
951
  | `createOtlpExporter` | OTLP/JSON 导出,零依赖;选项见下面「`OtlpExporterOptions`」表 |
823
- | `jsonlTraceSink` | JSONL 文件 sink:`jsonlTraceSink({ path })` → `TraceSink`,一行一条裸 Trace 追加落盘(父目录自动创建、追加不覆盖)—— **`agentia report` / `diff` / `harvest` 的输入格式**,见 §6「调优报告」 |
952
+ | `jsonlTraceSink` | JSONL 文件 sink:`jsonlTraceSink({ path })` → `TraceSink`,一行一条裸 Trace 追加落盘(父目录自动创建、追加不覆盖)—— **`agentia report` / `diff` / `harvest` / `export` / `view` 的输入格式**,见 §6「调优报告」。⚠️ **同一路径进程内幂等**(两次调用返回同一个 sink):两条 sink 对口一个文件会让一次 run 落两行同 traceId,而下面的命令按「一行 = 一次 run」计数 ⇒ 一次 run 被数两次(不报错,比不落盘更难发现)。所以「应用级 `sinks` 与 `registerDefaultTraceSink` 都往 `.agentia/traces.jsonl` 写」是安全的 |
824
953
  | `TraceRecordEvent` | **增量记账事件**(`onTraceEvent` 的回调参数):`span.begin` / `span.end` / `span.event` / `span.attribute` / `span.link`,每条带单调 `seq`。载荷是**增量 + 此刻的拷贝**(`span.begin` 只给初始形状,属性/事件/链路各走自己的类型);按 `seq` 升序折回必须**逐字等于**收尾时的 trace |
825
954
  | `metricsSink` | 指标累加器(Prometheus 文本 / OTLP metrics),满足 `TraceSink` 即接入 —— 见 §6「指标」 |
826
955
  | `buildRunReport` | 从一条 trace 生成**调优报告**(能力/模型的耗时、token、成本、错误率排行)—— 见 §6「调优报告」 |
@@ -836,6 +965,7 @@ trace 出去之后能干什么:指标、调用树面板、调优报告、生
836
965
  | `serviceName` | OTLP resource 的 `service.name`,缺省 `agentia` |
837
966
  | `timeoutMs` | 单次导出请求超时(毫秒,缺省 10000,非正数 = 不限)——裸 `fetch` 没有超时,collector 半开连接会让 run 收尾**永久挂起**;超时按导出失败处理 |
838
967
  | `onExportError` | 导出失败回调:给了它,**所有**失败(非 2xx / 超时 / **HTTP 200 但 collector 报部分接收**)都交给它、不再向 `TraceSink` 调用方抛;不给则维持既有行为(抛出,由 `flushSinks` 吞掉并落 `console.warn` —— 观测失败不击穿业务)。存在理由:`TraceSink` 的失败缺省只有一条泛化的 warn,「导出其实少了一半数据」这类**带分类的**消息得有人能收到 |
968
+ | `projectToolSpans` | 把 span 上按 `tool_use_id` 配对的 `tool.input` / `tool.output` 事件**投影**成 `execute_tool` child span,缺省 **false**(关闭时导出产物与既往一致):name 为 `execute_tool <工具名>`、带 `gen_ai.operation.name=execute_tool` + `gen_ai.tool.name`,start/end 取两事件的真实记账时刻;`ok:false` → status Error + `error.type=errorKind`;孤儿 input(`traceLimits.maxEvents` 截断可造出)→ **不写 `endTimeUnixNano` / status** 并带 `agentia.projection.orphan: true`(不编零时长)。原事件保留在父 span 上,engine 记账点不动 |
839
969
 
840
970
  导出器的两条线缆口径(都有守卫钉着,别按直觉改):
841
971
 
@@ -853,11 +983,14 @@ trace 出去之后能干什么:指标、调用树面板、调优报告、生
853
983
  (`gen_ai.evaluation.name` / `.score.value`,`source` / `comment` 走自有 `agentia.score.*` 键)→
854
984
  `metricsSink` 聚合成 `agentia_score` 指标族(见 §6「指标」)。eval / 在线评估怎么用见 §6「evals」与「在线评估采样」。
855
985
 
856
- **OTLP 的 `gen_ai.*` 对齐**(对齐 OTel GenAI semconv **v1.37**,**additive** —— 只追加 `gen_ai.*` 键,既有 `usage.*` 等键一律保留):
986
+ **OTLP 的 `gen_ai.*` 对齐**(对齐 `open-telemetry/semantic-conventions-genai` @ `6fd0d763`(2026-10-09)—— GenAI 约定自 v1.42(2026-06-12)起移出语义约定主线、该独立仓**无 tag、未版本化**,所以基准钉 commit,**additive** —— 只追加 `gen_ai.*` 键,既有 `usage.*` 等键一律保留):
857
987
  run 根 → `gen_ai.operation.name=invoke_agent` + `gen_ai.agent.name`(attributes 有 `session.id` 时另发 `gen_ai.conversation.id`);
858
988
  `llm.turn` → `gen_ai.operation.name=chat` + `gen_ai.request.model` + `gen_ai.usage.input_tokens` / `output_tokens`;
989
+ 缓存读数 > 0 时另发 `gen_ai.usage.cache_read.input_tokens` / `gen_ai.usage.cache_write.input_tokens`(上游口径「when applicable」:0 = 没走缓存 ⇒ 不留键,与自有 `usage.cache*` 键双发不替换);
990
+ 父 span 上有早于本回合开始的 `context.budget` 事件 ⇒ `gen_ai.conversation.compacted: true`(上游口径:只在能可靠判定时置真,**不设 false**);
859
991
  capability span 按 **attributes** 分(`subagent` / `skill`;span 的 `name` 是**裸能力名**):`subagent` → `invoke_agent` + `gen_ai.agent.name`,`skill` → `execute_tool` + `gen_ai.tool.name`;
860
- `score` 事件 → `gen_ai.evaluation.result`。映射集中在 `createOtlpExporter` 一处,下游(Langfuse / Grafana / Datadog)按 1.37+ 识别这批键做 GenAI 专项视图。
992
+ `score` 事件 → `gen_ai.evaluation.result`。映射集中在 `createOtlpExporter` 一处。
993
+ 普通 @Tool 只有 `tool.input` / `tool.output` 事件、没有 span(刻意口径);要上游六家框架同款的 `execute_tool` span,开 `projectToolSpans`(导出侧投影,见上表)。
861
994
  从 Tempo / Grafana 按 run 找回 trace 的做法(id 的 hex 投影、run 根按 `invoke_agent` 过滤)见 `docs/observability.md` §2.7。
862
995
 
863
996
  > **生产落地**(按 runId 落库检索 / 日志关联 / 采样 / 脱敏)见 `docs/observability.md` ——
@@ -871,11 +1004,12 @@ capability span 按 **attributes** 分(`subagent` / `skill`;span 的 `name`
871
1004
  把它接回上游 —— 触发它的那个 span(网关请求 / 队列消息 / 另一个服务)记在 run 根 span 的 `links` 上,
872
1005
  OTLP 导出时就是标准的 **span links**,后端能把因果边画出来。
873
1006
 
874
- **入站两种给法**:
1007
+ **入站三种给法**:
875
1008
 
876
1009
  | 场景 | 写法 |
877
1010
  |---|---|
878
1011
  | HTTP 宿主(网关 / 消费者通过 HTTP 打过来) | 请求头 `traceparent: 00-<32位trace>-<16位span>-01`(W3C)—— `createHttpHandler` 自动解析,`POST /run` 与 `POST /tasks` 都认;**畸形头静默当作没有上游**,不会打 400 |
1012
+ | MCP 反向桥(Claude Code / 别的 agent 框架经 `tools/call` 调你的工具) | 请求 `params._meta.traceparent`(**SEP-414** 定的载体:`_meta` 里的**裸键**,不加前缀)—— `createMcpServer` 自动解析,合法即记成该次 trace run 根的一条 `links`;**缺失 / 畸形静默当作没有上游**(不造空上下文、不影响调用成败) |
879
1013
  | 程序内直接调 `app.run` / `runner.submit` | `options.traceContext: { traceId, spanId? }` —— 也可写 `parseTraceparent(头值)` 自己解析 |
880
1014
 
881
1015
  **队列消费者(Kafka / RabbitMQ / SQS)的形态**:拿消息键当 `idempotencyKey`,把消息里带的
@@ -926,7 +1060,11 @@ class OrderTools {
926
1060
 
927
1061
  - **粒度**:普通工具与 `@Prompt` **不建 span**,它们的「当前 span」就是发起它们的那次 `llm.turn`;
928
1062
  `@Skill` / `@SubAgent` 的方法体内则是它自己的 `capability` span。内层覆盖外层。
929
- - **不自动注入**:框架不创建出站请求,注入那一行是你自己的(同上 `fetch`;gRPC 宿主塞进 metadata)。
1063
+ - **不自动注入**(一个例外):框架不替你创建出站请求,注入那一行是你自己的
1064
+ (同上 `fetch`;gRPC 宿主塞进 metadata)。例外是 **MCP 正向桥**:`tools/call` 的请求由桥自己发,
1065
+ 所以在 run 内经 `mcpTools` 调 MCP 工具时,桥自动把当前 span 的 `traceparent` 放进
1066
+ `params._meta`(SEP-414 裸键,**不进 `arguments`**;不在 run 内则不带;对端不认 `_meta`
1067
+ 静默忽略 —— 这是观测,不是协议要求)。
930
1068
  - 与入站同一个格式、同一份 id 投影:`currentTraceparent()` 的输出喂回 `parseTraceparent()` 逐字还原,
931
1069
  下游 OTLP 里看到的 span id 与本地 collector 里的是**同一个数**(见 §7 已知边界)。
932
1070
 
@@ -976,7 +1114,7 @@ class OrderTools {
976
1114
  | `intervalMs` | OTLP 导出间隔(毫秒,缺省 60000);`0` = 每次 run 收尾立即导出。定时器已 `unref()`,不阻止进程退出 |
977
1115
  | `resourceAttributes` / `serviceName` | OTLP resource 属性(`service.name` 缺省 `agentia`) |
978
1116
  | `timeoutMs` | OTLP 单次导出请求超时(毫秒,缺省 10000,非正数 = 不限)——collector 半开连接时兜底,`intervalMs: 0` 模式不被挂死;超时按导出失败处理 |
979
- | `onExportError` | 导出失败回调(缺省吞掉 —— 观测失败不得击穿业务) |
1117
+ | `onExportError` | 导出失败回调(缺省每次失败落一条 `console.warn` —— 观测失败不得击穿业务,但不零信号) |
980
1118
  | `windowSize` | 时长分位保留的样本数(环形窗口,缺省 1024,**run / 能力 / 模型各自独立**);非正数抛错 |
981
1119
  | `prefix` | 指标名前缀,缺省 `agentia_` |
982
1120
  | `labelMode` | 能力标签粒度:`'capability'`(缺省,`tool:search` 这种)/ `'kind'`(只按类型,基数极小)/ `'none'`(不产出能力指标) |
@@ -1048,8 +1186,8 @@ class OrderTools {
1048
1186
 
1049
1187
  | API | 说明 |
1050
1188
  |---|---|
1051
- | `buildRunReport(trace)` | 一条 trace → `RunReport`:能力排行(按总耗时降序)、模型归因、未定价模型清单 |
1052
- | `mergeRunReports(reports)` | 跨 run 汇总(**分位只在多条 run 上才有统计意义**) |
1189
+ | `buildRunReport(trace)` | 一条 trace → `RunReport`:能力排行(按总耗时降序;嵌套能力的 usage **只计顶层行** —— 内层行的 `tokens`/`costUsd` 为 `null`,各行可加总,与 `agentia report` 同口径)、模型归因、未定价模型清单 |
1190
+ | `mergeRunReports(reports)` | 跨 run 汇总(**分位只在多条 run 上才有统计意义**);按 `traceId` 去重、同一 traceId 只留最后一份(append-only 落盘的同一棵 trace 会被写多次,与 `agentia report` 同口径) |
1053
1191
  | `renderRunReport(report)` | 人类可读的纯文本表(CLI / 日志用) |
1054
1192
 
1055
1193
  ```ts
@@ -1065,29 +1203,48 @@ console.log(renderRunReport(report));
1065
1203
  CLI 侧有薄壳:`agentia report <trace.jsonl>` —— 每行一个 JSON(裸 Trace,或含 `result.trace` /
1066
1204
  `trace` 的 TaskRecord,如 `FileTaskStore` 的导出),跨行按能力合并后打印排行。
1067
1205
  加 `--json` 得到机器可读输出:stdout 只有一个 JSON 文档(`{ file, runs, failed, skippedLines,
1068
- failures, capabilities, totals }`)、没有人类装饰,适合脚本与 CI 断言;出错仍走 stderr
1206
+ failures, capabilities, sessions, totals }`)、没有人类装饰,适合脚本与 CI 断言;出错仍走 stderr
1069
1207
  (`错误:…`)并把退出码置 1、stdout 保持为空 —— 脚本据此区分「有结果」与「没跑成」。
1070
1208
 
1209
+ **会话 / 任务维度(`sessions[]`)**:能力排行之外另打一张按**分组键**归组的表 —— 键取 run 根
1210
+ span 的 `session.id`(宿主传 `session: { store, id }` 才会写,见 §6.4「提示词版本化与会话标记」),没有就退到记录
1211
+ 里的 `taskId`(`FileTaskStore` / `SqliteTaskStore` 的导出),再没有落 `(无会话)` 桶。
1212
+ 每组给 `runs / ok / fail / total / cost / cost-ok` 六列。三条口径值得记住:
1213
+ **成功率一定带分母**(小组上只报百分比是噪声,所以 `runs` 与 `ok` 都单独出列);
1214
+ **成功数为 0 时 `cost-ok` 是 `-`(JSON 里 `null`),不是 0** —— 「除不了」不等于「不花钱」;
1215
+ **挂起计入 `ok`** —— 成败判据与引擎同一条(`trace.status !== 'error'`),而引擎的口径是
1216
+ 「挂起不是失败」(取消才是 `error`)⇒ 10 条 run 全挂在审批上会报「成功率 100%」。
1217
+ 这是刻意的取舍,读数时把「等审批」与「跑成了」分开看。
1218
+ 成本取该 run 的 `totalUsage.costEstimate`(= 各 `llm.turn` 之和,**不含** capability 聚合 ⇒
1219
+ `@SubAgent` 里套 `@Skill` 时不会把同一批 token 数两遍)。
1220
+ 能力排行的 tokens / cost 列同一条纪律:**只认顶层 capability**(没有 capability 祖先的)——
1221
+ capability span 的 usage 是其子孙 `llm.turn` 的聚合,嵌套时内层行的 tokens / cost 显示为 `-`
1222
+ (那批 token 已在外层行里 ⇒ 各行的 tokens / cost 可加总、不重复计数);内层行的
1223
+ calls / err / 耗时照记(那是它自己这次调用的真账)。
1224
+
1071
1225
  > ⚠️ **单条 run 内样本常 < 5,分位没有意义** —— 所以报告以 `total` / `max` 为主;
1072
1226
  > 要看分位请用 `mergeRunReports` 汇总多条,或用 `metricsSink` 的直方图。
1073
1227
  > CLI 报告的聚合口径与 `agentia dev` 面板的能力排行同源(同一份 `@migor/trace-view` 实现)。
1074
1228
 
1075
1229
  那个 jsonl 从哪来 —— 框架内置了文件 sink,一行接入(零依赖)。它产出的格式就是
1076
- **CLI 三件套的输入**:`agentia report` / `agentia diff` / `agentia harvest` 都吃这个文件
1077
- (一行一个 JSON,裸 Trace):
1230
+ **CLI 五件套的输入**:`agentia report` / `agentia diff` / `agentia harvest` / `agentia export` /
1231
+ `agentia view` 都吃这个文件(一行一个 JSON,裸 Trace):
1078
1232
 
1079
1233
  ```ts
1080
1234
  import { createApp, jsonlTraceSink } from '@migor/agentia';
1081
1235
 
1082
1236
  const app = await createApp({ /* … */ sinks: [jsonlTraceSink({ path: 'trace.jsonl' })] });
1083
- // 之后:agentia report trace.jsonl / agentia diff a.jsonl b.jsonl / agentia harvest trace.jsonl
1237
+ // 之后:agentia report trace.jsonl / agentia diff a.jsonl b.jsonl / agentia harvest trace.jsonl /
1238
+ // agentia export trace.jsonl / agentia view trace.jsonl --open
1084
1239
  ```
1085
1240
 
1086
1241
  父目录不存在会自动递归创建;**追加不覆盖**(多跑几次就累积多行);写失败抛给 `flushSinks`
1087
1242
  吞掉并落 `console.warn`(观测失败不击穿业务,但不再零信号)。
1088
1243
 
1089
1244
  异步宿主更省事:把 `FileTaskStore` 的落盘文件直接喂给它 —— `TaskRecord` 里带 `result.trace`,
1090
- `report` 认这种形态,不用另写 sink。
1245
+ `report` 认这种形态,不用另写 sink。⚠️ 它是 **append-only**(每次 save 追加一行):审批流里
1246
+ 同一棵挂起段 trace 会出现多行(挂起 / approve / 接管各 save 一次)。`report` 按 **`traceId`
1247
+ 去重、只留最后一行**(append-only 里后面的是新状态),runs / 成功率 / 成本不会按行数翻倍。
1091
1248
 
1092
1249
  #### 生效配置快照(「这条 run 用了哪套旋钮」)
1093
1250
 
@@ -1107,9 +1264,36 @@ const app = await createApp({ /* … */ sinks: [jsonlTraceSink({ path: 'trace.js
1107
1264
  - 单次 `app.run(..., { system })` 覆盖时,版本**跟当次那个 `SystemPrompt` 走**;`system` 传已拼好的 `SystemParam` 则无版本可记(不写空串冒充实有版本)。
1108
1265
  - 直连 `runAgent` / `executeRun` 时可用引擎级选项 `systemVersion` 显式给。
1109
1266
  - **`@Prompt` 资产版本**:`@Prompt({ version })` 声明后,装配期把菜单里全部带版本的 @Prompt 收集成 `{ 能力名: 版本 }` 表(与主菜单同一条收集路径,`toolSources` 收窄同样生效),每次 run 落 run 根 span 的 `prompts.versions` attribute(`name@ver` 逗号拼接、按名排序、空表不记)—— 质量回归能定位到具体资产版本。直连 `runAgent` 时用引擎级选项 `promptVersions` 显式给。
1110
- - **会话标识**:`app.run(..., { session })` / `executeRun` 给了 `session` 时,session id 自动落 run 根 span 的 `session.id` attribute(OTLP 导出时映射 `gen_ai.conversation.id`)—— 多轮对话的 run 由此可按会话聚合,不用手填。直连 `runAgent` 时用引擎级选项 `sessionId` 显式给。
1267
+ - **会话标识**:`app.run(..., { session })` / `executeRun` 给了 `session` 时,session id 自动落 run 根 span 的 `session.id` attribute(OTLP 导出时映射 `gen_ai.conversation.id`)—— 多轮对话的 run 由此可按会话聚合,不用手填。直连 `runAgent` / `executeRun` 时用引擎级选项 `sessionId` 显式给。⚠️ **别把 `sessionId` 传给 `app.run`**:那个字段是异步宿主的可序列化会话引用(见 §4 选项表),`app.run` 不消费它 —— 直传会 `console.warn` 一次且不生效。
1111
1268
  - **菜单签名**:每次 run 落 run 根 span 的 `tools.names`(装配后菜单的名字、按名排序、超长截断带 `…(+N)`)与 `tools.menuHash`(12 位摘要,材料 = 排序后的「名字 + **输入 schema**」规范序列化)—— 与 `prompts.versions` 同动机:质量回归能回答「这两段 run 跑在同一个菜单上吗」。口径:与菜单顺序、schema 键序无关;**`description` 不参与**(润色文案不算菜单变化,代价是「只改描述」看不见 —— 有意)。给了 `tools` 就自动记,无需开关。
1112
- - **菜单漂移(续跑时菜单变了)**:挂起段之后**续跑**时,未决 tool_use 引用的工具若已不在当前菜单(删了 / 改名了),框架记**三处信号**:`menu.drift` 事件(含 `{ missing, tool_use_ids, menu_size }`;时间线与 `GET /tasks/:id/stream` 都看得到)、父 span 的 `menu.drift` attribute(如 `missing:danger`)、一条 `console.warn`。⚠️ **run 照常收尾**(不判失败):那些 tool_use 会以 `unknown tool` 回给模型 —— 为什么不做严格模式见 spec §10 2026-09-27 ⑧。注意这与「回合内模型编了个不存在的工具名」不同:那个是幻觉,走既有 `unknown_tool` 路径、**不**报漂移。
1269
+ - **菜单漂移(续跑时菜单变了)**:挂起段之后**续跑**时,未决 tool_use 引用的工具若已不在当前菜单(删了 / 改名了),框架记**三处信号**:`menu.drift` 事件(含 `{ missing, tool_use_ids, menu_size }`;时间线与 `GET /tasks/:id/stream` 都看得到)、父 span 的 `menu.drift` attribute(如 `missing:danger`)、一条 `console.warn`。⚠️ **run 照常收尾**(不判失败):那些 tool_use 会以 `unknown tool` 回给模型 —— 为什么不做严格模式见 spec §10 2026-09-27 ref-menu-drift-signals。注意这与「回合内模型编了个不存在的工具名」不同:那个是幻觉,走既有 `unknown_tool` 路径、**不**报漂移。
1270
+
1271
+ #### 交接与护栏(`agentia.handoff.*` / `agentia.guardrail.*`)
1272
+
1273
+ **交接**(agent → agent)与**护栏**(审批)在这个框架里都不是 span:前者就是一条 `capability`
1274
+ span 本身(`attributes.subagent`),后者**只是事件**(`approval.requested` / `approval.decided`)。
1275
+ 这两样对外部 OTLP 消费者都等于「要解析才看得见」⇒ 各补一个 additive 属性当入口:
1276
+
1277
+ | 键 | 值 | 写在哪 | 何时 |
1278
+ |---|---|---|---|
1279
+ | `agentia.handoff.from` | **调用方** agent 名 | 被调 `@SubAgent` 的 capability span | 该 span 由另一个 agent 的回合发起时 |
1280
+ | `agentia.handoff.to` | **被调** agent 名 | 同上 | 同上 |
1281
+ | `agentia.guardrail.name` | 待决的**首个**工具名 | 与 `approval.requested` **同一个 span**:首段是发起回合的 `llm.turn`;**续跑段再挂起时是 run 根**(续跑入口把 run 根 id 当 turnId,本段没有对应 `llm.turn`) | 记 `approval.requested` 时 |
1282
+ | `agentia.guardrail.triggered` | `true` | 同上 | 同上 |
1283
+
1284
+ - 名字从哪来:run 根用 `runName`(默认 `agent.run`,与 OTLP 的 `gen_ai.agent.name` 同源),
1285
+ 子 agent 用它注册的 `name`。嵌套交接会**逐层换名**(内层 `from` 是外层子 agent,不是应用)。
1286
+ - ⚠️ **刻意不做**:不把审批**升级成 span**。它现在是事件、与「普通 `@Tool` 只发
1287
+ `tool.input` / `tool.output`」同形;升级会动引擎记账点,且下游已能按事件消费。
1288
+ - ⚠️ 两处**只发一半**是口径不是遗漏:`guardrail.*` **只在触发时发**,不设 `false`
1289
+ (与 `gen_ai.conversation.compacted` 同款);`handoff.*` 拿不到调用方身份时(宿主手搓
1290
+ `recorder` + `ctx` 直调 `subagentToTool`)**两个键都不发** —— 编不出来就不编。
1291
+ - **视图**:`@migor/trace-view` 的 summary(`agentia report` 与 `agentia dev` 面板同源)把
1292
+ `approval.decided` 聚成 `guardrail:<工具名>` 一行 —— `calls` = 走到决定的次数、
1293
+ `err` = **被拒**次数(护栏真挡下的)、`total` / `max` = 等审批时长(事件的 `waitedMs`;
1294
+ 宿主没回填 `requestedAt` 时记 0,**不编造**)。聚合**不限 span kind**(首段的决定记在
1295
+ `llm.turn`、恢复段的记在 run 根,两处都聚)。⚠️ 只有 `approval.requested` 而始终没恢复的
1296
+ 挂起**不产出行**:它没有结论也没有时长。
1113
1297
 
1114
1298
  ### 6.5 集成
1115
1299
 
@@ -1182,7 +1366,7 @@ class MyMemoryStore implements MemoryStore {
1182
1366
  | API | 说明 |
1183
1367
  |---|---|
1184
1368
  | `mcpTools` | 把 MCP server 的 `tools/list` 映射成框架 `AgentTool[]`(进 `createApp({ tools })`) |
1185
- | `McpClientLike` | 最小结构面:`listTools()` + `callTool(name, args)`;框架**不 import** MCP SDK |
1369
+ | `McpClientLike` | 最小结构面:`listTools()` + `callTool(name, args, opts?)`(`opts` 带 `abandoned` 与 SEP-414 的 `traceparent`,均可选);框架**不 import** MCP SDK |
1186
1370
  | `createStdioMcpConnector` | **出厂 stdio 连接器**:spawn 一个 MCP server 子进程,走换行分隔 JSON-RPC(只用 `node:child_process`) |
1187
1371
  | `createStreamableHttpMcpConnector` | **出厂 StreamableHTTP 连接器**:一个 endpoint POST JSON-RPC;`application/json` 与 `text/event-stream` 两种响应都接(只用全局 `fetch`) |
1188
1372
  | `McpConnector` | 两个连接器的公共面 = `McpClientLike` + `close()`。自己写传输时实现 `McpClientLike` 即可,不必碰它 |
@@ -1196,6 +1380,7 @@ class MyMemoryStore implements MemoryStore {
1196
1380
  - **超时**:**只有一个裁判**。引擎设了 `toolTimeoutMs` 时,桥的 `timeoutMs`(缺省 `MCP_DEFAULT_TIMEOUT_MS` = 60000)**不参与判定**;它只在「桥脱离引擎单用」或「引擎没设 `toolTimeoutMs`」时作为兜底。两条路径共用同一判定(`core/timeout.ts`:**看实测耗时,不看竞速**),超时都记 `errorKind='timeout'` + `is_error` 回模型、**不杀 run**。你自己写的工具要报超时,抛一个 `code === 'timeout'` 的错误即可(不必 import 框架的类)。
1197
1381
  - **连接器出厂自带,但仍只给缝**:两个连接器只用标准库(`node:child_process` + 全局 `fetch`)⇒ **不新增任何第三方依赖**。要接官方 SDK / 远程 server / 自研传输,实现 `McpClientLike` 两个方法即可(同 `RedisLike` 的形状)。
1198
1382
  - **连接器替你兜住三件只有它能做的事**:① spawn 失败(命令不存在 → `ENOENT`)是**异步 `'error'` 事件**,不接住会把宿主进程带崩;② stdout 的分帧 —— 一条报文可能跨多个 chunk;③ **协议层 `isError: true` 转成抛错**。
1383
+ - **trace 上下文(SEP-414)**:在 run 内调用时,桥把当前 span 的 W3C `traceparent` 放进 `tools/call` 的 `params._meta`(**裸键、不加前缀**;两个出厂连接器都做),认出它的 server 就能把这次调用链回你的 trace;不在 run 内(桥脱离引擎单用)则不带 —— 不造空上下文。自己实现 `McpClientLike` 时读 `callTool` 第三参的 `traceparent` 即可对齐(不读也兼容,只是链路断在你的传输上)。
1199
1384
  - **连接器的 `timeoutMs` 只管装配期**(握手 + `tools/list`):那两步**没有任何别的裁判**,server 卡住会让 `createApp` 永久挂起;`callTool` 的裁判仍是引擎 / 桥(一次调用只有一个)。
1200
1385
 
1201
1386
  ```ts
@@ -1262,9 +1447,11 @@ server —— Claude Code / Cursor / 任何 MCP 宿主都能直接调你的 `@To
1262
1447
 
1263
1448
  - **两个传输都只用标准库**:stdio = stdin/stdout 换行分隔 JSON-RPC(日志只去 stderr,stdout 是协议面);http = StreamableHTTP(POST 收报文、应答 `application/json`,简单应答不上 SSE;GET → 405、DELETE → 200)。
1264
1449
  - **trace 叙事不破**:每次 `tools/call` 造一棵 trace —— run 根 `mcp.tools/call` 下挂一个 capability span(name = 工具名),记 `tool.input` / `tool.output` 事件(与引擎同账目形状),收尾投递 `sinks`;`initialize` / `tools/list` 不建 trace。capability span 无 skill/subagent 属性 ⇒ metrics 里以 `capability:<工具名>` 标签进能力指标。
1450
+ - **入站 trace 上下文(SEP-414)**:`tools/call` 的 `params._meta.traceparent` 合法时记成那棵 trace run 根的一条 `links`(与 HTTP 宿主认 `traceparent` 头同款缝合);缺失 / 畸形静默当作孤立根 —— 不报错、不影响调用成败、不造空上下文。对端是 agentia 正向桥(或 MCP Python SDK 等已按 SEP-414 注入的实现)时这条链自动接上。
1265
1451
  - **结果映射与正向桥方向对称**:工具返回 string → `content: [{ type: 'text', text }]`,其他 JSON 化进 text;抛错 → 协议层成功 + `isError: true`(MCP 惯例;正向桥的连接器正好把 `isError` 转回抛错)。
1266
1452
  - **取消是协作式**:客户端断连 / `close()` 会中止该次调用的 `ToolRunContext.signal`;`toolTimeoutMs` 与引擎同款语义(非正数 / 不设 = 不限;超时 = **放弃等待** + abort `abandoned`,该次调用回 `isError`,server 不挂)。
1267
1453
  - **默认 client 是惰性构造的**:`client` 不给时第一次 `tools/call` 才走 `createAnthropicClient()`(与引擎默认同款)—— 纯工具 server 不需要 API key 在场。
1454
+ - **想看一个真用它干活的例子**:`examples/observability-mcp/` 把 `buildRunReport` / `diffTraces` 包成两个只读工具(`report_trace` / `diff_traces`)暴露给任意 MCP 宿主,让 agent 自己查「我上一轮慢在哪」;还带一条**实边界**(工具参数由模型给 ⇒ 只读 trace 根内的文件,realpath 比前缀),以及一条端到端的验收脚本(起真 server ⇐ 出厂连接器 + `mcpTools`)。
1268
1455
 
1269
1456
  ```ts
1270
1457
  import { createMcpServer, jsonlTraceSink } from '@migor/agentia';
@@ -1296,6 +1483,8 @@ console.log(mcp.url); // http://127.0.0.1:8787/mcp
1296
1483
  | `server` | 挂进**既有** `http.Server`(框架不替它 listen;`close()` 只摘本 handler 不关它)。非本 `path` 的请求本 handler 直接忽略(交给宿主自己的路由) |
1297
1484
  | `client` | 进 `ToolRunContext.client`(子 agent / skill 类工具要它拉子循环);缺省惰性 `createAnthropicClient()` |
1298
1485
  | `sinks` | trace 出口:每次 `tools/call` 收尾投递(抛错被吞 + `console.warn`,观测不击穿业务) |
1486
+ | `traceLimits` | 每次 `tools/call` 那棵 trace 的事件数闸(与引擎 `traceLimits` 同款:超限停记 + run 根留 `trace.truncated` 簿记;缺省 = 不限;坏值构造期抛 TypeError) |
1487
+ | `maxEventChars` | 事件正文截断上限(与引擎 `maxEventChars` 同款;`false` = 不截断;缺省 = 引擎缺省) |
1299
1488
  | `toolTimeoutMs` | 单次 `tools/call` 的工具执行超时(毫秒),语义与引擎 `toolTimeoutMs` 同款;非正数 / 不设 = 不限 |
1300
1489
  | `auth` | http 侧鉴权钩子:在读 body 之前调用,抛错即 401(原文只进服务端日志);stdio 侧信任父进程、本项不生效 |
1301
1490
  | `name` | `serverInfo.name`(握手回给客户端),缺省 `'agentia'` |
@@ -1346,7 +1535,10 @@ agentia harvest trace.jsonl --out evals/harvested.ts --force # 覆盖已存在
1346
1535
  - 产物是**可粘贴进 eval 文件的用例字面量**:`client: scriptedClient([...])` 按 trace 的主循环 llm.turn 逐回合重建,`expect` 预填「主循环工具序列」的轨迹断言(文件顶部附跑法注释)。
1347
1536
  - `--out` 指向已存在的文件时**默认拒绝覆盖**(产物是要人工核对的脚手架,重跑一次就抹掉你改过的断言与 input);要覆盖显式加 `--force`。
1348
1537
  - ⚠️ **脚手架不是成品,人工核对后再进 CI**:
1349
- - **trace 不记 assistant 文本**(llm.turn 只记 usage/事件),脚本里的 text 块是占位 `'[harvest] assistant 文本未入 trace'`;
1538
+ - **assistant 文本要看 run 开没开 `traceContent: 'full'`**(§4 的 opt-in 开关):开了就取
1539
+ llm.turn 的 `output.text` 真值;没开则脚本**只含工具调用**,终端回合的 text 是占位
1540
+ `'[harvest] assistant 文本未入 trace'`(纯 tool_use 回合**不补占位** —— 那会变成「模型
1541
+ 说过这句话」的假事实)。骨架头注会写明这一次是哪一种,不会让你自己去猜;
1350
1542
  - 只重建**直属 run 根**的主循环回合 —— 子 agent 的嵌套回合不走主循环脚本(要覆盖子 agent 请单独写 eval);
1351
1543
  - 预填的 `expect` 是从原 trace **抄录的实际轨迹** —— 发生过 ≠ 应该发生;
1352
1544
  - `EvalCase` 没有 `expect` 字段,粘贴时把断言搬进 `defineEval({ expect })`(脚手架注释会教)。
@@ -1404,8 +1596,8 @@ const c = await app.run(messages); // 起一条新 run,沿着分叉点前的
1404
1596
  每个 span 带 `path` / `missing` / `fields`),**退出码语义不变**(有差异仍为 1)—— 门禁照旧可用,
1405
1597
  只是「差在哪」也能被读。`agentia doctor --json` 同理(体检结果结构化,有错误仍退出 1)。
1406
1598
 
1407
- > `report` / `diff` / `doctor` 支持 `--json`;`harvest` **没有**:它的 stdout 本身就是产物
1408
- > (生成的 eval 文件源码),加 `--json` 会自相矛盾。
1599
+ > `report` / `diff` / `doctor` 支持 `--json`;`harvest` / `export` **没有**:它们的 stdout
1600
+ > 本身就是产物(生成的 eval 文件源码 / 训练数据 JSONL),加 `--json` 会自相矛盾。
1409
1601
 
1410
1602
  - **配对语义**(结构性配对,字段差异不影响配对):llm.turn 按回合序配对、**忽略 span name** ——
1411
1603
  name 是模型 id,而「换模型重跑」正是 A/B 主用例,按 name 配对会把两侧所有 turn 报成缺失;
@@ -1727,7 +1919,7 @@ const callable = {
1727
1919
  | 观测失败被吞 | sink 抛错不影响 run(观测是辅助动作);但吞之前落一条 `console.warn`(含「trace sink」字样)—— sink 天天挂不再零信号。同理记忆水合/回写失败也不击穿 run |
1728
1920
  | 框架不自动读 .env | 除 `AGENTIA_MODEL`(缺省模型覆盖)与 `OPENAI_API_KEY`(OpenAI 适配器)外,框架自己不去翻环境变量,也不读 `.env`;要读就在启动代码里调 `loadEnvFile()`(脚手架已内置那行),**真实环境变量优先**于文件 |
1729
1921
  | 鉴权只是缝 | 框架**不实现** token / JWT / 签名策略,也不碰凭据 env —— `authenticate` 只承诺「拦在入口、读 body 之前」;策略是宿主或反代的事 |
1730
- | 运行时是 Node | 按 Node ≥ 18 设计与测试(`engines` 写明;CI 覆盖 18 / 20 / 24 —— `import-floor` 矩阵跑下限、其余 job 跟当前 LTS);**未对 Deno / edge 做验证**。`SqliteTaskStore` 需 Node ≥ 22.5(`node:sqlite`),未提供时构造期抛可读报错 |
1922
+ | 运行时:Node 承诺,Bun / Deno 实测 | 按 Node ≥ 18 设计与测试(`engines` 写明;CI 覆盖 18 / 20 / 24 —— `import-floor` 矩阵跑下限、其余 job 跟当前 LTS);Bun 1.4 / Deno 2.9 已实测可跑(四步判定 + 各宿主最小代码见 `docs/frameworks.md`),但**不在 `engines` 承诺内**;**edge 未验证**。`SqliteTaskStore` 需 Node ≥ 22.5(`node:sqlite`),未提供时构造期抛可读报错 |
1731
1923
  | 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不做进程级决策);信号处理是宿主的 |
1732
1924
  | 停机可能切断 SSE | `drain()` 收口时会强制关闭仍开着的 SSE 流,`/run` 的流被收口时其 run 以 `stopReason='aborted'` 收尾(客户端应把断流当作可重试)。**没有 `timeoutMs` 时也收口**(否则等待没有终点,见上);只关旁观者长连(`/tasks/:id/stream`)**不损失工作**,那种收口不影响 `drain()` 的返回值 |
1733
1925
  | 多进程共库:续跑认领靠**租约**,不靠时间(2026-09-28) | `resumePending({ staleAfterMs })` 的 `staleAfterMs` **不是**租约而是**兜底启发式**。`> 0` 时按两档判他进程的记录:① `ownerId` 里的主机名是本机 ⇒ **问那个 pid 还在不在**(`process.kill(pid, 0)`)——**在的绝不抢**(不管记录多老)、**不在的立刻可抢**(崩溃孤儿不等保鲜期);② **判不了**(异主机 / 升级前写下的旧格式 `ownerId` / 自定义串)⇒ 才用 `staleAfterMs`(「记录看起来还新」)。缺省 `0` = 不问他进程,与升级前逐字一致 |
@@ -1760,7 +1952,8 @@ const callable = {
1760
1952
  | MCP 工具不能进 DI 容器 | 它没有 provider token,也不能被别的能力的 `tools` 引用(两种引用粒度都要先有 token) |
1761
1953
  | 指标分位是窗口内精确值 | `*_last{quantile=...}` 只反映最近 `windowSize`(缺省 1024)条样本;要跨实例聚合请用直方图(`*_bucket` / `_sum` / `_count`,累积语义) |
1762
1954
  | 指标是**进程内**累加 | 不做分布式聚合与持久化:多实例各算各的(直方图可相加),重启即清零。要长期保留请把 `render()` 抓走或用 `export:'otlp'` 推给采集端 |
1763
- | OTLP metrics 只推当前累计 | 按 `intervalMs` 周期导出**累积值**(CUMULATIVE),不做增量/背压;导出失败按 `onExportError` 处理(缺省吞掉,不重试、不阻塞 run)。`reset()` 语义是**开启新窗口**:计数清零**且**数据点起点前移(同一 `startTime` 下 counter 只能单调不减,只清零会让后端算出负增量或丢样本) |
1955
+ | OTLP metrics 只推当前累计 | 按 `intervalMs` 周期导出**累积值**(CUMULATIVE),不做增量/背压;导出失败按 `onExportError` 处理(缺省每次失败落一条 `console.warn`,不重试、不阻塞 run)。`reset()` 语义是**开启新窗口**:计数清零**且**数据点起点前移(同一 `startTime` 下 counter 只能单调不减,只清零会让后端算出负增量或丢样本) |
1956
+ | OTLP trace 导出**只发 JSON**(直打 Phoenix 会 415) | `createOtlpExporter` 的 content-type 写死 `application/json`,`OtlpExporterOptions` **没有编码选项**;只收 `application/x-protobuf` 的接收端(Phoenix 的 traces 路由对别的 content-type 回 415,源码级核实 2026-10-12)**直打不进去** —— OTLP 规范对接收端只是 SHOULD 双收,Phoenix 未遵循推荐但不违规。处置:中间加一层 OTLP Collector 收 JSON、转 binary(最小配置见 `docs/observability.md` §7)。**不内建 protobuf 编码器**:零运行时依赖是硬约定,手写编码器几百行进核心不值 |
1764
1957
  | `GET /metrics` 不鉴权 | 与 `/healthz` 同档(拉取端在集群内网)。要保护请放反代之后,或不传 `metrics` 选项自行在外层挂路由 |
1765
1958
  | 工具没有 token/成本指标 | 工具是**你的代码**、本身不消耗 token,所以只产出调用数/失败数/耗时;token 与成本只对 `skill`/`subagent`(有 `capability` span)与模型维度产出 |
1766
1959
  | `@Prompt` 没有能力指标 | 资产类能力不建 span、无独立耗时,故不出现在能力排行里(这是刻意的:硬凑一个假耗时会误导调优) |
@@ -1770,7 +1963,7 @@ const callable = {
1770
1963
  | `agentia harvest` 的产物是轨迹骨架 | trace **不记 assistant 文本**(llm.turn 只记 usage/事件),故 harvest 用例脚本里的 text 块是占位、预填 `expect` 是从原 trace 抄录的实际轨迹 —— 脚手架不是成品,人工核对后再进 CI(见 §6「线上 trace 回流」) |
1771
1964
  | 分叉重放不是续跑 | `forkMessages` 与 `traceToMessages` / harvest **同源有损**:trace **缺省**不记 assistant 文本(opt-in 例外:`traceContent: 'full'`,见 §4)与 run 原始输入(重放里 assistant 是标注占位、首尾 user 是合成),也不记 blackboard(分叉种子经 `RunInvocationOptions.blackboard` 自带);它产出喂回 `app.run` 的 messages、起的是**新 run**,不是接着原 run 的循环位置跑 |
1772
1965
  | 评分来自 run 之外 | `Score` 走 run 根 `score` **事件**而非 span 字段(评分通常在 run 跑完后才产生);`attachScore` 找不到根 span 时静默忽略,多次调用即多条事件(不同维度各记各的) |
1773
- | 链路关联:入站自动、**出站只给读取器** | `traceContext` / `traceparent` 头把**上游**接进来(run 根的 `links`);出站方向给 `currentTraceparent()`(当前 span 的 W3C 串,回合 / 能力调用粒度),但框架**不替你做注入** —— 它不创建出站请求,那一行由宿主的 `fetch` / metadata 自己写。两个边界:① `run` 根 span 由 `runAgent` 打开 ⇒ 更早的 `contextInit` / 记忆水合取到 `undefined`(那时确实没有 span);② flags 恒 `00`(本框架不采样)。id 宽度经**同一份投影**压到 16 位(与 OTLP 导出共用,单一真源)—— 故下游收到的 span id 与 collector 里的是同一个数。另:link 只落在 run 根(子 span 不散),且**一进程内**不跨进程自动传播 —— 队列场景要自己把 `traceContext` 传下去(HTTP 头带走,或随 `TaskRecord.spec.options` 落库) |
1966
+ | 链路关联:入站自动、出站默认只给读取器(MCP 桥除外) | `traceContext` / `traceparent` 头把**上游**接进来(run 根的 `links`);出站方向给 `currentTraceparent()`(当前 span 的 W3C 串,回合 / 能力调用粒度),但框架**不替你做注入** —— 它不创建出站请求,那一行由宿主的 `fetch` / metadata 自己写。**唯一的例外是 MCP 正向桥**:`tools/call` 的请求由桥自己发,故 run 内经 `mcpTools` 的调用自动带 `params._meta.traceparent`(SEP-414)。两个边界:① `run` 根 span 由 `runAgent` 打开 ⇒ 更早的 `contextInit` / 记忆水合取到 `undefined`(那时确实没有 span);② flags 恒 `00`(本框架不采样)。id 宽度经**同一份投影**压到 16 位(与 OTLP 导出共用,单一真源)—— 故下游收到的 span id 与 collector 里的是同一个数。另:link 只落在 run 根(子 span 不散),且**一进程内**不跨进程自动传播 —— 队列场景要自己把 `traceContext` 传下去(HTTP 头带走,或随 `TaskRecord.spec.options` 落库) |
1774
1967
  | 配额不是框架子系统 | 只给缝(middleware + TraceSink + BudgetGuard),计数放哪(内存 / Redis / DB)与超限怎么办都是你的策略 |
1775
1968
  | 人工审批两种形态 | 进程内闸门用 `middleware`(`await` 决策再放行);跨进程耐久审批用 `@Tool({ approval: 'required' })` + `POST /tasks/:id/approve`(挂起/恢复,见 §6.6「人工审批」) |
1776
1969
  | 审批超时是**惰性**判定 | `approvalTimeoutMs` 不起定时器:`approve` / `poll` / `resumePending` 读到过期挂起任务时才自动全拒并重派 —— 没人读的任务不会自己超时(要定期扫就调 `resumePending()`) |
@@ -1797,7 +1990,7 @@ const callable = {
1797
1990
  | 症状 | 原因与修法 |
1798
1991
  |---|---|
1799
1992
  | 菜单里没有我的能力 | ① 方法没写装饰器;② 类没注册进 `providers`;③ 框架双实例(见第 2 节陷阱);④ `toolSources` 白名单把它排除了 |
1800
- | `菜单能力重名` 装配期抛错 | 四类能力**共用命名空间**,改名即可 |
1993
+ | `菜单能力重名` / `子菜单能力重名` 装配期抛错 | 四类能力**共用命名空间**,改名即可;查重覆盖主菜单与 @SubAgent/@Skill 的 `tools` 引用展开出的子菜单(两个 provider 各有同名能力被同一能力整片引用时,报错会指到哪个能力的哪个引用片,用能力级路径 `<token>/<能力名>` 收窄即可) |
1801
1994
  | 编译错 `不能把 X 赋给 Y` | 用了 `fromZod<T>`,方法签名与 `T` 不一致(这是护栏,不是 bug) |
1802
1995
  | 模型传的入参没被拦 | 只填了 `schema` 没写 `strict`;且框架的校验是**子集校验**(`format`/`minimum` 等不校验) |
1803
1996
  | `ctx.get('k')` 没有类型 | 没做 `Blackboard` 声明合并(见 5.1) |