@migor/agentia 0.4.2 → 0.6.0

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 (71) hide show
  1. package/README.md +1 -1
  2. package/dist/AGENTS.md +145 -10
  3. package/dist/core/message.d.ts +148 -0
  4. package/dist/core/message.js +25 -0
  5. package/dist/core/message.js.map +1 -0
  6. package/dist/core/tool.d.ts +10 -9
  7. package/dist/core/trace.d.ts +21 -0
  8. package/dist/core/trace.js +9 -1
  9. package/dist/core/trace.js.map +1 -1
  10. package/dist/engine/errors.d.ts +12 -0
  11. package/dist/engine/errors.js +46 -13
  12. package/dist/engine/errors.js.map +1 -1
  13. package/dist/engine/loop.d.ts +17 -11
  14. package/dist/engine/loop.js +46 -433
  15. package/dist/engine/loop.js.map +1 -1
  16. package/dist/engine/replay.d.ts +18 -3
  17. package/dist/engine/replay.js +49 -8
  18. package/dist/engine/replay.js.map +1 -1
  19. package/dist/engine/retry.d.ts +5 -4
  20. package/dist/engine/retry.js.map +1 -1
  21. package/dist/engine/spec.d.ts +5 -5
  22. package/dist/engine/spec.js.map +1 -1
  23. package/dist/engine/trace-diff.d.ts +57 -0
  24. package/dist/engine/trace-diff.js +188 -0
  25. package/dist/engine/trace-diff.js.map +1 -0
  26. package/dist/engine/trimming.d.ts +7 -7
  27. package/dist/engine/trimming.js +6 -5
  28. package/dist/engine/trimming.js.map +1 -1
  29. package/dist/engine/turn.d.ts +157 -0
  30. package/dist/engine/turn.js +500 -0
  31. package/dist/engine/turn.js.map +1 -0
  32. package/dist/engine/types.d.ts +18 -8
  33. package/dist/engine/usage.d.ts +2 -2
  34. package/dist/engine/usage.js.map +1 -1
  35. package/dist/eval/defineEval.d.ts +1 -1
  36. package/dist/eval/defineEval.js +12 -0
  37. package/dist/eval/defineEval.js.map +1 -1
  38. package/dist/eval/harvest.d.ts +37 -0
  39. package/dist/eval/harvest.js +132 -0
  40. package/dist/eval/harvest.js.map +1 -0
  41. package/dist/eval/scripted.js.map +1 -1
  42. package/dist/index.d.ts +8 -2
  43. package/dist/index.js +4 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/integrations/anthropic.d.ts +41 -37
  46. package/dist/integrations/anthropic.js +408 -17
  47. package/dist/integrations/anthropic.js.map +1 -1
  48. package/dist/integrations/metrics.d.ts +13 -0
  49. package/dist/integrations/metrics.js +65 -0
  50. package/dist/integrations/metrics.js.map +1 -1
  51. package/dist/integrations/openai.d.ts +1 -1
  52. package/dist/integrations/openai.js +3 -3
  53. package/dist/integrations/openai.js.map +1 -1
  54. package/dist/integrations/otlp.d.ts +10 -0
  55. package/dist/integrations/otlp.js +59 -0
  56. package/dist/integrations/otlp.js.map +1 -1
  57. package/dist/runtime/run.js +2 -0
  58. package/dist/runtime/run.js.map +1 -1
  59. package/dist/runtime/session.d.ts +5 -5
  60. package/dist/runtime/session.js.map +1 -1
  61. package/dist/toolkit/module.d.ts +8 -2
  62. package/dist/toolkit/module.js +56 -8
  63. package/dist/toolkit/module.js.map +1 -1
  64. package/dist/toolkit/prompt.d.ts +20 -0
  65. package/dist/toolkit/prompt.js +18 -4
  66. package/dist/toolkit/prompt.js.map +1 -1
  67. package/dist/toolkit/skill.d.ts +3 -3
  68. package/dist/toolkit/subagent.d.ts +2 -2
  69. package/dist/toolkit/subagent.js +1 -1
  70. package/dist/transport/async.d.ts +2 -2
  71. package/package.json +2 -4
package/README.md CHANGED
@@ -30,7 +30,7 @@ $EDITOR my-app/.env # 填 ANTHROPIC_API_KEY(也可直接 expo
30
30
  > 框架**不自动**读 `.env`:脚手架 `src/main.ts` 首行的 `loadEnvFile()` 负责把它读进 `process.env`。
31
31
  > 真实环境变量优先(CI / docker / 命令行永远赢过文件),`loadEnvFile({ override: true })` 才反过来。
32
32
 
33
- > **版本**:`0.4.2`(`@migor/agentia` 与 `@migor/cli` 均已发布到 npm)。`examples/` **刻意**用
33
+ > **版本**:`0.6.0`(`@migor/agentia` 与 `@migor/cli` 均已发布到 npm)。`examples/` **刻意**用
34
34
  > `file:../..` 指向本仓库而不是 npm 版本 —— 跑的是**工作区代码**,理由见
35
35
  > [`examples/README.md`](examples/README.md) 的「依赖说明」。
36
36
 
package/dist/AGENTS.md CHANGED
@@ -105,7 +105,7 @@ npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复
105
105
  | `model` | `ctx.llm()` 的缺省模型 |
106
106
  | `maxTokens` | 同上 |
107
107
  | `maxIterations` | 同上 |
108
- | `tools` | `ctx.llm()` 可调工具:**provider token 列表**(复用该 provider 的 `@Tool` 菜单) |
108
+ | `tools` | `ctx.llm()` 可调工具:**provider token 列表**(复用该 provider 的能力菜单),或 `'<token>/<能力名>'` 能力级路径(只引菜单里的单个能力) |
109
109
 
110
110
  技能方法体拿到的第二参是 `SkillContext`:`ctx.llm({ prompt })` 才会真正调模型(脚本式,调几次由你写死)。
111
111
 
@@ -117,7 +117,7 @@ npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复
117
117
  | `schema` | 主 agent 填给子 agent 的任务入参 schema |
118
118
  | `name` | 缺省取方法名 |
119
119
  | `system` | 子 agent 的角色提示:`string` / `SystemPrompt` / `(task) => SystemParam` |
120
- | `tools` | 子 agent 可调工具:**provider token 列表** |
120
+ | `tools` | 子 agent 可调工具:**provider token 列表**,或 `'<token>/<能力名>'` 能力级路径 |
121
121
  | `model` | 子 agent 自己的模型 |
122
122
  | `maxTokens` | 同上 |
123
123
  | `maxIterations` | 同上 |
@@ -132,6 +132,7 @@ npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复
132
132
  | `description` | 描述**何时该拉取**这段文本 |
133
133
  | `name` | 缺省取方法名 |
134
134
  | `schema` | 模板化入参 schema;缺省空对象(无参资产) |
135
+ | `version` | 资产版本号(git hash / `'v3'` 等);装配期随能力名收集,每次 run 落 run 根 span 的 `prompts.versions` attribute —— 见 §6「提示词版本化」。缺省(无版本)则该能力不进表 |
135
136
 
136
137
  `@Prompt` **只支持方法形态**(标准装饰器下字段拿不到值/类引用)。实例方法与静态方法都**沿继承链**收集(父类的 `@Prompt` 资产子类自动带上)。方法体内用 `asset(import.meta.url, './x.md')` 读同目录长文本。
137
138
 
@@ -250,6 +251,25 @@ result.typed; // { answer: string } | undefined
250
251
 
251
252
  模型没提交就是 `undefined`(不是失败)。`stopReason` 仍以 `end_turn` 正常收尾。
252
253
 
254
+ ### 5.4 消息类型族(自有公共类型,与厂商 SDK 结构兼容)
255
+
256
+ `messages` 与模型响应的类型是框架**自有定义**(不从 `@anthropic-ai/sdk` 引类型),字段口径与
257
+ Anthropic Messages API 逐字对齐(snake_case),并与 SDK 的对应类型**结构兼容**:
258
+ 手里的 `Anthropic.MessageParam[]` 可以直接喂给 `app.run` / `executeRun`;装框架**不会**连带安装厂商 SDK。
259
+
260
+ | 类型 | 说明 |
261
+ |---|---|
262
+ | `MessageParam` | 请求消息 `{ role, content: string \| ContentBlockParam[] }`(`Role` 含 `'system'`,与 SDK 逐字对齐;发给端点仍是 user/assistant 语义) |
263
+ | `ContentBlockParam` | 请求块联合:text / image / tool_use / tool_result + `{ type: string }` 兜底成员(厂商新块型原样携带,读字段先按 `type` 收窄) |
264
+ | `Message` | 模型响应(`finalMessage()` 的产物):`{ id, type, role, content, model, stop_reason, stop_sequence, usage }` |
265
+ | `ContentBlock` | 响应块联合:text / tool_use / thinking + `{ type: string }` 兜底成员 |
266
+ | `ToolParam` | 发给模型的工具定义(**避让 @Tool 装饰器**,故不叫 Tool) |
267
+ | `MessageUsage` | 响应的 token 计量(**避让 trace 的 `Usage`**,故不叫 Usage) |
268
+ | `TextBlockParam` / `ImageBlockParam` / `ToolUseBlockParam` / `ToolResultBlockParam` | 请求侧具体块(与 SDK 同名类型逐字对齐) |
269
+ | `TextBlock` / `ToolUseBlock` / `ThinkingBlock` / `CacheControl` / `Role` | 响应侧具体块 / cache 断点标记 / 角色联合 |
270
+
271
+ 注意命名避让:`Tool` 是装饰器、`Usage` 是 trace 的聚合用量(camelCase)—— 消息侧对应物分别是 `ToolParam` 与 `MessageUsage`(snake_case)。
272
+
253
273
  ---
254
274
 
255
275
  ## 6. 运行时 API
@@ -409,7 +429,7 @@ process.on('SIGTERM', async () => {
409
429
  | `mapWithConcurrency` | 有界并发 map(结果保序);`maxToolConcurrency` 的底座,也可自用 |
410
430
 
411
431
  - **取消**:`app.run(messages, { signal })` 传 `AbortSignal` —— 框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发 `signal`),run 以 `stopReason='aborted'` 收尾(算失败)。`createHttpHandler` 已内置「客户端断开即中止」;`AsyncRunner.runTimeoutMs` 到点同样是**真中止**(构造期校验:必须 ≥ 0 的**有限**数 —— NaN/Infinity 会被 `setTimeout` 钳到 1ms,等于每个任务立即超时,故直接抛错;要「不限」传 0 或不设)。
412
- - **重试**:缺省自动重试可重试失败(429 / 5xx / 连接失败),指数退避 + 抖动。`retry: false` 关闭,或 `retry: { maxAttempts, baseDelayMs, maxDelayMs, jitter, onRetry }` 调参。**只在本次尝试尚未产出任何文本时重试**(已吐出的字无法撤回)。⚠️ 与 SDK 内置重试叠加 —— 建议二选一调(这里 `maxAttempts: 1` 或把 SDK 的 `maxRetries` 调小)。
432
+ - **重试**:缺省自动重试可重试失败(429 / 5xx / 连接失败),指数退避 + 抖动。`retry: false` 关闭,或 `retry: { maxAttempts, baseDelayMs, maxDelayMs, jitter, onRetry }` 调参。**只在本次尝试尚未产出任何文本时重试**(已吐出的字无法撤回)。⚠️ 与底层 client 的内置重试叠加(默认 client 的 `maxRetries` 缺省 2)—— 建议二选一调(这里 `maxAttempts: 1`,或 `createAnthropicClient({ maxRetries: 0 })`)。
413
433
  - **流式**:`POST /run` 带 `Accept: text/event-stream` → SSE 逐帧下发(`text.delta` / `run.end` / `error`);不带该头仍回一元 JSON。
414
434
  - **工具超时 / 并发闸门**:`toolTimeoutMs` 超时**不杀 run**(该条 tool_result 记 `is_error`,模型可换路);`maxToolConcurrency` 给同回合的并行工具设上限(默认全并行)。⚠️ 超时 = **放弃等待**,`AgentTool.run` 没有 signal 参数,**副作用可能已发生** —— 想真停的工具请自行读 `ToolRunContext.signal`。
415
435
 
@@ -423,6 +443,18 @@ process.on('SIGTERM', async () => {
423
443
  | `createOtlpExporter` | OTLP/JSON 导出,零依赖;选项 `OtlpExporterOptions`:`endpoint` / `headers` / `serviceName` / `timeoutMs`(单次导出超时,缺省 10000,非正数 = 不限 —— 裸 fetch 无超时,collector 半开连接会让 run 收尾永久挂起;超时按导出失败处理,不击穿 run) |
424
444
  | `metricsSink` | 指标累加器(Prometheus 文本 / OTLP metrics),满足 `TraceSink` 即接入 —— 见 §6「指标」 |
425
445
  | `buildRunReport` | 从一条 trace 生成**调优报告**(能力/模型的耗时、token、成本、错误率排行)—— 见 §6「调优报告」 |
446
+ | `Score` | 质量评分:`{ name; value; source?; comment? }` —— LLM-judge / 人工标注 / eval 结论挂到 trace 上;约定 `value` 为 0–1(布尔结论用 0/1),`source` 记评分来源(eval 名 / `'human'` / judge 模型 id) |
447
+ | `attachScore` | `attachScore(trace, score)`:把评分挂到 run 根 span(一条 `score` 事件,body 即 `Score`)。评分通常来自 run **之外**(跑完才评),所以走事件而非 span 字段;trace 找不到根 span 时静默忽略(观测不击穿业务) |
448
+
449
+ **评分链路**(R7 质量闭环):`attachScore` 写 run 根 `score` 事件 → OTLP 导出时译为 `gen_ai.evaluation.result`
450
+ (`gen_ai.evaluation.score.name` / `.value`,`source` / `comment` 走自有 `agentia.score.*` 键)→
451
+ `metricsSink` 聚合成 `agentia_score` 指标族(见 §6「指标」)。eval / 在线评估怎么用见 §6「evals」与「在线评估采样」。
452
+
453
+ **OTLP 的 `gen_ai.*` 对齐**(R7,对齐 OTel GenAI semconv **v1.37**,**additive** —— 只追加 `gen_ai.*` 键,既有 `usage.*` 等键一律保留):
454
+ run 根 → `gen_ai.operation.name=invoke_agent` + `gen_ai.agent.name`(attributes 有 `session.id` 时另发 `gen_ai.conversation.id`);
455
+ `llm.turn` → `gen_ai.operation.name=chat` + `gen_ai.request.model` + `gen_ai.usage.input_tokens` / `output_tokens`;
456
+ capability span 按前缀分:`subagent:*` → `invoke_agent` + `gen_ai.agent.name`,`skill:*` → `execute_tool` + `gen_ai.tool.name`;
457
+ `score` 事件 → `gen_ai.evaluation.result`。映射集中在 `createOtlpExporter` 一处,下游(Langfuse / Grafana / Datadog)按 1.37+ 识别这批键做 GenAI 专项视图。
426
458
 
427
459
  > **生产落地**(按 runId 落库检索 / 日志关联 / 采样 / 脱敏)见 `docs/observability.md` ——
428
460
  > 框架只保证 trace 出口,这些都在缝外用 sink 组合;四条现成 sink 的实码在
@@ -503,11 +535,13 @@ if (result.stopReason === 'budget_exceeded') console.warn('这次 run 被预算
503
535
 
504
536
  | API | 说明 |
505
537
  |---|---|
506
- | `createAnthropicClient` | 默认 ModelClient(Anthropic):自定义只传 `apiKey` / `baseURL`,不必直接依赖厂商 SDK |
538
+ | `createAnthropicClient` | 默认 ModelClient(Anthropic):自定义只传 `apiKey` / `baseURL`;框架**零运行时依赖**,不装厂商 SDK |
507
539
  | `createOpenAIClient` | OpenAI 兼容端点适配(DeepSeek 等;**真流式**、图片块转 `image_url`、cache token 恒 0) |
508
540
  | `InMemoryMemoryStore` | 跨 run 的**键值黑板**记忆(`{ store, keys }` 配 `executeRun`) |
509
541
  | `InMemorySessionStore` | 跨 run 的**对话历史**(`{ store, id }` 配 `executeRun` / `app.run`);与前者正交,可同时用 |
510
542
  | `traceToMessages` | 把 trace 还原成 messages(重放基底) |
543
+ | `forkMessages` | 分叉重放:在主循环第 `atTurn` 回合之前截断重放历史、拼上 `append` 新消息喂回 `app.run`(「从第 N 回合换个问法重跑」的基底,**不是续跑**) |
544
+ | `diffTraces` | 两条 trace 的 A/B 比对(prompt / 模型实验):run 级 summary + 逐 span 字段差;纯函数,llm.turn 配对**忽略模型名**,缺省忽略墙钟 |
511
545
  | `applyMiddleware` | 手动包裹配置菜单(装配层已自动做) |
512
546
 
513
547
  ### MCP 桥(MCP 是「工具来源」,不是新机制)
@@ -552,6 +586,7 @@ const app = createApp({ system, providers: [...], tools });
552
586
  - 断言源是既有 `Trace`:「先 `search` 才 `summarize`」这类顺序断言全从 trace 读,框架不为此新增埋点。
553
587
  - `run()` **不抛**(用例失败进报告,一次跑完能看到所有回归,而不是修一个跑一次);只有「应用建不起来」才冒泡 —— 那是环境错误,不是回归。失败 case 带 `trace`,直接看现场。
554
588
  - `scriptedClient` 的步骤**在 `finalMessage()` 成功返回后才前进**:抛错的步骤(函数步骤 `throw` 模拟 429)会在重试时**重放同一步**,想验重试就这么写。
589
+ - **用例结论自动落 score**(R7):每个用例跑完,结论以 `{ name: 'eval', value: 0|1, source: eval 名, comment: 失败原因 }` 自动 `attachScore` 到该用例的 trace —— eval 的 trace 自带质量结论,下游 sink / `metricsSink` 可直接聚合「这个 eval 的通过率」(`app.run` 抛错拿不到 trace 时不挂)。
555
590
 
556
591
  ```ts
557
592
  const ev = defineEval<{ summary: string }>({
@@ -571,6 +606,65 @@ const report = await ev.run();
571
606
  if (!report.ok) console.error(report.cases.filter((c) => !c.ok));
572
607
  ```
573
608
 
609
+ ### 线上 trace 回流 eval 数据集(`agentia harvest`)
610
+
611
+ 线上事故 → 回归用例:CLI 把 trace 落盘文件翻成 eval 用例脚手架。
612
+
613
+ ```bash
614
+ agentia harvest trace.jsonl # 全部记录 → 脚手架打到 stdout
615
+ agentia harvest trace.jsonl --failed --limit 5 --out evals/harvested.ts
616
+ ```
617
+
618
+ - 输入同 `agentia report`:每行一个 JSON(裸 Trace,或含 `result.trace` / `trace` 的 TaskRecord,如 `FileTaskStore` 的导出);`--failed` 只留失败记录。
619
+ - 产物是**可粘贴进 eval 文件的用例字面量**:`client: scriptedClient([...])` 按 trace 的主循环 llm.turn 逐回合重建,`expect` 预填「主循环工具序列」的轨迹断言(文件顶部附跑法注释)。
620
+ - ⚠️ **脚手架不是成品,人工核对后再进 CI**:
621
+ - **trace 不记 assistant 文本**(llm.turn 只记 usage/事件),脚本里的 text 块是占位 `'[harvest] assistant 文本未入 trace'`;
622
+ - 只重建**直属 run 根**的主循环回合 —— 子 agent 的嵌套回合不走主循环脚本(要覆盖子 agent 请单独写 eval);
623
+ - 预填的 `expect` 是从原 trace **抄录的实际轨迹** —— 发生过 ≠ 应该发生;
624
+ - `EvalCase` 没有 `expect` 字段,粘贴时把断言搬进 `defineEval({ expect })`(脚手架注释会教)。
625
+
626
+ ### prompt / 模型 A/B(trace diff 与分叉重放)
627
+
628
+ 同一份输入跑两条 run(换模型、换 `SystemPrompt` 版本、换 prompt 都行),用 `diffTraces` 比出**结构与成本差**;
629
+ 想「从第 3 回合换个问法重跑」,用 `forkMessages` 在分叉点截断、拼上新消息喂回 `app.run`:
630
+
631
+ ```ts
632
+ import { diffTraces, forkMessages } from '@migor/agentia';
633
+
634
+ // A/B:同输入,只换模型(或换 system 版本),各跑一条
635
+ const a = await app.run(input, { model: 'claude-sonnet-5' });
636
+ const b = await app.run(input, { model: 'claude-opus-5' });
637
+
638
+ const diff = diffTraces(a.result.trace, b.result.trace);
639
+ // diff.summary:status / totalUsage.* / 根 attributes 差 —— A/B 模型第一眼就看 attributes.model
640
+ // diff.spans:逐 span 字段级差异,path 形如 run:main/llm.turn#0/capability:search
641
+ if (!diff.equal) console.log(diff.summary, diff.spans);
642
+
643
+ // 分叉重放:在主循环第 3 回合(0-based)之前截断,该回合及其后丢弃,换个问法继续
644
+ const messages = forkMessages(a.result.trace, {
645
+ atTurn: 3, // 合法范围 0..主循环回合数-1,越界抛可读错误
646
+ append: [{ role: 'user', content: '换个思路:先给结论,再补证据。' }],
647
+ });
648
+ const c = await app.run(messages); // 起一条新 run,沿着分叉点前的真实 tool 历史继续
649
+ ```
650
+
651
+ 不落代码也可以直比两份 trace 导出:`agentia diff a.jsonl b.jsonl`(输入形态同 `agentia report`),
652
+ 打印 run 级 summary + 逐 span 差异,**有差异时退出码 1**(diff(1) 语义)——可直接进 CI 挡
653
+ 「换 prompt / 模型后轨迹漂移」。
654
+
655
+ - **配对语义**(结构性配对,字段差异不影响配对):llm.turn 按回合序配对、**忽略 span name** ——
656
+ name 是模型 id,而「换模型重跑」正是 A/B 主用例,按 name 配对会把两侧所有 turn 报成缺失;
657
+ 模型差异降格为配对 turn 的 `name` 字段差。capability span 按 `kind:name` 配对
658
+ (`skill:foo` vs `skill:bar` 是不同能力,不该配上)。一侧多出的调用树记**一条缺侧记录**
659
+ (`SpanDiff.fields` 为空、`path` 照给),整支子树不再下钻。
660
+ - **墙钟缺省不比**:`ignoreTiming` 缺省 true(A/B 不关心时序);传 `false` 改比 span 时长
661
+ (`duration`),绝对时间戳永不比;`traceId` 是身份不是行为,同样永不比。
662
+ - ⚠️ **有损边界(与 `traceToMessages` / harvest 同源)**:trace **不记 assistant 文本与 run 的
663
+ 原始输入** —— 重放里 assistant 是标注占位(非逐字原文)、首尾说明性 user 是合成。
664
+ 因此 `forkMessages` **不是「续跑」**:它产出的是一份喂回 `app.run` / `runAgent` 的 messages,
665
+ 跑的是一条**新 run**,不是接着原 run 的循环位置。分叉的 blackboard 种子由调用方自带
666
+ (`app.run(messages, { blackboard: {…} })` —— trace 不记 blackboard)。
667
+
574
668
  ### 指标(从 trace 派生)
575
669
 
576
670
  | API | 说明 |
@@ -578,7 +672,7 @@ if (!report.ok) console.error(report.cases.filter((c) => !c.ok));
578
672
  | `metricsSink` | 进程内累加 + Prometheus 文本 / OTLP metrics;**天然满足 `TraceSink`** → `createApp({ sinks: [metricsSink()] })` 即接入,零新出口 |
579
673
  | `DEFAULT_BUCKETS` | 时长直方图的缺省桶边界(毫秒),可用 `buckets` 覆盖 |
580
674
 
581
- 三个维度,全部从既有 trace 派生,**不需要在业务代码里埋点**:
675
+ 四个维度,全部从既有 trace 派生,**不需要在业务代码里埋点**:
582
676
 
583
677
  - **run 级** —— 总数 / 失败数 /四类 token / 成本 / 时长;
584
678
  - **能力级** —— 每个 `tool` / `skill` / `subagent` 的**调用次数、失败次数、耗时、token、成本**。
@@ -586,6 +680,10 @@ if (!report.ok) console.error(report.cases.filter((c) => !c.ok));
586
680
  来自 `capability` span。**`@Prompt` 不建 span、无独立耗时,因此不产出能力指标**(如实缺省,不硬凑)。
587
681
  - **模型级** —— 按模型(`llm.turn` 的 span name)归因 turn 数 / token / 成本 / 耗时,并单独给出
588
682
  `model_unpriced_turns_total`(算不出成本的 turn 数 —— **成本护栏失效的显式信号**)。
683
+ - **评分级**(R7)—— 来自 run 根 span 的 `score` 事件(`attachScore` 写入):
684
+ `agentia_score{name,source}` gauge 记**最近一次**值(分数不是累加量),`agentia_score_total{name,source}` counter 记条数;
685
+ `snapshot().scores` 以 `name@source` 为键(source 缺省时裸 name)暴露 `{ value, count, sum }`(平均 = sum/count),
686
+ OTLP metrics payload 同样带这两个家族,`reset()` 一并清空。
589
687
 
590
688
  接完 `GET /metrics` 直接回 `render()` 即可 —— 交给 `createHttpHandler({ metrics })` 就是一行的事
591
689
  (见 §6 HTTP 端点速查)。
@@ -618,7 +716,7 @@ if (!report.ok) console.error(report.cases.filter((c) => !c.ok));
618
716
  | 成员 | 说明 |
619
717
  |---|---|
620
718
  | `export` | `TraceSink` 的实现(run 收尾投递)—— 也是接进 `sinks` 的形状 |
621
- | `snapshot` | `{ runs, failed, latencyP50, latencyP95, tokens, costUsd, capabilities, models, droppedCapabilities }` |
719
+ | `snapshot` | `{ runs, failed, latencyP50, latencyP95, tokens, costUsd, capabilities, models, scores, droppedCapabilities }` |
622
720
  | `render` | Prometheus 文本(`/metrics` 直接回它) |
623
721
  | `flush` | 主动导出一次(`export:'otlp'` 时有意义;prometheus 模式为空操作) |
624
722
  | `stop` | 停掉定时导出(进程收尾 / 测试用) |
@@ -695,12 +793,44 @@ const app = await createApp({ /* … */ sinks: [jsonl] });
695
793
  函数型选项(`summarize` / `confirm` 之类)只记「配没配」,不记函数体。
696
794
  截断关掉时记的是 `'off'` 而不是 `false` —— 后者在日志/看板里会被读成「上限为 0」。
697
795
 
698
- ### 提示词版本化
796
+ ### 提示词版本化与会话标记(run 根 attribute)
699
797
 
700
798
  - `new SystemPrompt({ version: 'git-abc123' })` → 自动写到 **run 根 span 的 `system.version` attribute**:trace 里能查出「这个结果是哪个版本的提示词产出的」(换 prompt 前后对比、排查回归都靠它)。
701
799
  - 版本号怎么来(git sha / 语义版本 / 手工)由你决定 —— 框架**不做**版本库与回滚平台。
702
800
  - 单次 `app.run(..., { system })` 覆盖时,版本**跟当次那个 `SystemPrompt` 走**;`system` 传已拼好的 `SystemParam` 则无版本可记(不写空串冒充实有版本)。
703
801
  - 直连 `runAgent` / `executeRun` 时可用引擎级选项 `systemVersion` 显式给。
802
+ - **`@Prompt` 资产版本**(R7):`@Prompt({ version })` 声明后,装配期把菜单里全部带版本的 @Prompt 收集成 `{ 能力名: 版本 }` 表(与主菜单同一条收集路径,`toolSources` 收窄同样生效),每次 run 落 run 根 span 的 `prompts.versions` attribute(`name@ver` 逗号拼接、按名排序、空表不记)—— 质量回归能定位到具体资产版本。直连 `runAgent` 时用引擎级选项 `promptVersions` 显式给。
803
+ - **会话标识**(R7 thread 维度):`app.run(..., { session })` / `executeRun` 给了 `session` 时,session id 自动落 run 根 span 的 `session.id` attribute(OTLP 导出时映射 `gen_ai.conversation.id`)—— 多轮对话的 run 由此可按会话聚合,不用手填。直连 `runAgent` 时用引擎级选项 `sessionId` 显式给。
804
+
805
+ ### 在线评估采样(recipe,不是框架功能)
806
+
807
+ 生产流量按 N% 采样跑 LLM-judge、把分数回挂 trace —— 用**现有 sink 机制**拼:采样(`examples/observability` 的 `sampleSink` 配方)+ 对抽中的 run 调一次 judge(一次 `app.run` 或裸 client 调用)+ `attachScore` 回挂 + `metricsSink` 聚合。框架不提供 judge 子系统 —— 评什么、用什么模型评、采样率多少,都是你的策略:
808
+
809
+ ```ts
810
+ import { attachScore, createAnthropicClient, metricsSink } from '@migor/agentia';
811
+ import { sampleSink } from '@migor/agentia-observability'; // examples/observability
812
+
813
+ const metrics = metricsSink();
814
+ const judge = createAnthropicClient(); // judge 也可以就是同一个 app 的一次 run
815
+
816
+ const onlineEval = sampleSink({
817
+ rate: 0.05, // 抽 5%(失败 run 永不采样掉,见 observability.md 配方 2.3)
818
+ sinks: [{
819
+ async export(trace) {
820
+ // 你的 judge:读 trace 给个 0–1 分(一次 app.run / 裸 client 调用随你)
821
+ const { value, comment } = await runMyJudge(judge, trace);
822
+ // 回挂到这条 trace 的根 span:score 事件 → OTLP gen_ai.evaluation.result / metrics agentia_score
823
+ attachScore(trace, { name: 'faithfulness', value, source: 'judge:claude-opus-5', comment });
824
+ },
825
+ }],
826
+ });
827
+
828
+ createApp({ /* … */ sinks: [onlineEval, metrics] }); // metrics 必须同链,才聚合得到分
829
+ ```
830
+
831
+ - 顺序要紧:**judge sink 在 `metricsSink` 之前**,分数事件才进指标(sink 数组顺序即投递顺序)。
832
+ - judge 本身的 run 也会产生 trace —— 给它单独一个 app / runName,或按 run 名在 judge sink 里跳过自己,避免「评估评估的评估」。
833
+ - 成本自控:judge 调一次模型就是一份钱,采样率与 judge 模型档位是你的旋钮(judge 的 run 同样受 `maxCostUsd` 等护栏约束)。
704
834
 
705
835
  ### 多租户配额(组合既有缝,不是子系统)
706
836
 
@@ -811,11 +941,13 @@ const callable = {
811
941
  | schema 校验是**子集** | 只覆盖 `type/properties/required/additionalProperties/enum/items`;`format`/`minimum`/`oneOf` 一律放行 |
812
942
  | 历史畸形就放弃裁剪 | `trimToolPairs` 遇到非严格交替历史会整体放弃(宁可少裁,也不切出孤立 tool_use 让请求 400) |
813
943
  | 缺省内存 store 不淘汰 | 长跑宿主请设 `InMemoryTaskStore({ maxRecords })` 或换 `FileTaskStore` / `SqliteTaskStore` |
814
- | 能力引用是 provider 粒度 | 子 agent / skill 的 `tools` 写的是 **provider token**,不是单个工具名 |
944
+ | 能力引用两种粒度 | `tools` 写 **provider token** = 整片能力菜单;写 `'<token>/<能力名>'` = 只引单个能力(@Tool/@Skill/@SubAgent/@Prompt 都可点名,装配期校验,名字不存在即抛错并列出可用名单) |
815
945
  | 能力名有格式校验 | 装饰器能力名(`name` 或缺省的方法名)必须匹配 `^[A-Za-z0-9_-]{1,64}$`(与 MCP 桥同口径),非法名在 `createApp` **装配期即抛错** —— 含空格/点/中文的名字会让模型 API 400,宁可在启动期拦住 |
816
946
  | `discover` 入口会回落 | 能力目录里源码与编译产物并存(`index.ts` + `index.js`)时,首选 `.ts` 加载失败会**回落 `.js` 并 warn** —— 命中的可能是**陈旧编译产物**(刚改过源码时注意);全部候选都失败才抛错并列出各自原因 |
817
947
  | `asset()` 的 rel 必须是相对路径 | 带 scheme(`file:` / `https:` …)的 rel 会让 `new URL(rel, base)` 整个忽略 base(「以为读了能力目录、实际读了别处」),显式抛错;`../` 越出能力目录是**有意放行**(共享资产如 `../../shared/x.md` 是合法用法) |
818
948
  | 取消要传进客户端才有效 | 传 `signal` 后框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发);不转发 `signal` 的自定义 `ModelClient` 只能「放弃等待」(请求在后台跑完、产物丢弃) |
949
+ | 默认 client 的真端点验证范围 | `e2e:live` 跑在 DeepSeek 的 Anthropic **兼容**端点上;官方 Anthropic 端点的行为差异(thinking 细节、cache TTL 语义、新块型)目前只有本地假端点测试在守 —— 「mock 全绿发现不了厂商真实行为」是本框架自己记过的教训,接入官方端点前自己跑一遍 `npm run e2e:live` |
950
+ | thinking 块「能收、不主动请求」 | 框架**从不**在请求里开 extended thinking;默认 client 能收拼 thinking 块(`signature_delta` 会累积),`redacted_thinking` 与未知块型**原样透传**不丢 —— 但官方 API 的 thinking 回灌要求带合法 `signature`,自定义 client 开 thinking 时自己验证这条链 |
819
951
  | 工具阶段的 abort 有盲区 | abort 只在三处被观察:**回合边界 / 在飞模型请求 / 重试退避 sleep**。没设 `toolTimeoutMs` 且工具挂死时,abort 之后 run 也不会返回(工具的 Promise 永不 settle)—— 挂死的工具要么设超时,要么自己读 `ToolRunContext.signal` |
820
952
  | 观测失败被吞 | sink 抛错不影响 run(观测是辅助动作);同理记忆水合/回写失败也不击穿 run |
821
953
  | 框架不自动读 .env | 除 `AGENTIA_MODEL`(缺省模型覆盖)与 `OPENAI_API_KEY`(OpenAI 适配器)外,框架自己不去翻环境变量,也不读 `.env`;要读就在启动代码里调 `loadEnvFile()`(脚手架已内置那行),**真实环境变量优先**于文件 |
@@ -835,7 +967,7 @@ const callable = {
835
967
  | MCP 的协议层错误框架看不见 | `isError: true` 只有连接器能看见 —— 它必须转成抛错,否则模型收到的是一条「成功」的结果 |
836
968
  | MCP 超时同样是「不等了」 | 桥自带的 `timeoutMs` 取消不了 server 侧执行(拿不到取消句柄);它与 engine 的 `toolTimeoutMs` **双重计时**,谁短谁生效 |
837
969
  | MCP 名字可能被归一化 | 原名含 `-` / `.` / 空格 → 进菜单时变成 `_`;回调 server 用的仍是原名(`mcp.tool` attribute 里查得到) |
838
- | MCP 工具不能进 DI 容器 | 它没有 provider token,也不能被别的能力的 `tools` 引用 —— 引用是 provider 粒度 |
970
+ | MCP 工具不能进 DI 容器 | 它没有 provider token,也不能被别的能力的 `tools` 引用(两种引用粒度都要先有 token) |
839
971
  | 指标分位是窗口内精确值 | `*_last{quantile=...}` 只反映最近 `windowSize`(缺省 1024)条样本;要跨实例聚合请用直方图(`*_bucket` / `_sum` / `_count`,累积语义) |
840
972
  | 指标是**进程内**累加 | 不做分布式聚合与持久化:多实例各算各的(直方图可相加),重启即清零。要长期保留请把 `render()` 抓走或用 `export:'otlp'` 推给采集端 |
841
973
  | OTLP metrics 只推当前累计 | 按 `intervalMs` 周期导出**累积值**(CUMULATIVE),不做增量/背压;导出失败按 `onExportError` 处理(缺省吞掉,不重试、不阻塞 run) |
@@ -844,6 +976,9 @@ const callable = {
844
976
  | `@Prompt` 没有能力指标 | 资产类能力不建 span、无独立耗时,故不出现在能力排行里(这是刻意的:硬凑一个假耗时会误导调优) |
845
977
  | 能力标签有基数上限 | `labelMode:'capability'`(缺省)+ `maxCapabilities`(缺省 200),超出的能力归入 `capability="__other__"`;`snapshot().droppedCapabilities` 给出被归并的能力个数。要完整明细请用 `buildRunReport`(不设上限) |
846
978
  | 提示词版本只是标记 | 框架不存版本库、不回滚:`version` 只落 run 根 attribute;`system` 传已拼好的 `SystemParam` 时无版本可记 |
979
+ | `agentia harvest` 的产物是轨迹骨架 | trace **不记 assistant 文本**(llm.turn 只记 usage/事件),故 harvest 用例脚本里的 text 块是占位、预填 `expect` 是从原 trace 抄录的实际轨迹 —— 脚手架不是成品,人工核对后再进 CI(见 §6「线上 trace 回流」) |
980
+ | 分叉重放不是续跑 | `forkMessages` 与 `traceToMessages` / harvest **同源有损**:trace 不记 assistant 文本与 run 原始输入(重放里 assistant 是标注占位、首尾 user 是合成),也不记 blackboard(分叉种子经 `RunInvocationOptions.blackboard` 自带);它产出喂回 `app.run` 的 messages、起的是**新 run**,不是接着原 run 的循环位置跑 |
981
+ | 评分来自 run 之外 | `Score` 走 run 根 `score` **事件**而非 span 字段(评分通常在 run 跑完后才产生);`attachScore` 找不到根 span 时静默忽略,多次调用即多条事件(不同维度各记各的) |
847
982
  | 配额不是框架子系统 | 只给缝(middleware + TraceSink + BudgetGuard),计数放哪(内存 / Redis / DB)与超限怎么办都是你的策略 |
848
983
  | 人工介入只到「闸门」 | `middleware` 能 `await` 审批决策再放行;**跨进程挂起/续跑框架不做** —— `RunStatus` 无「待批准」态、循环位置不落库,`traceToMessages` 重放有损,不能拿它假装续跑(要跨重启审批请上工作流引擎)|
849
984
  | 内容护栏不给实现 | 同「配额」:只给缝(入参包 `app.run` / 工具前 `middleware` / 出参包返回值或 `sinks`),策略(正则 / 分类器 / 外部 API)是你的 |
@@ -862,7 +997,7 @@ const callable = {
862
997
  | `ctx.get('k')` 没有类型 | 没做 `Blackboard` 声明合并(见 5.1) |
863
998
  | `result.typed` 是 `unknown` | `resultSchema` 用的是裸 JsonSchema;改 `fromZod<T>`(见 5.3) |
864
999
  | TS 里想 `app.my_tool(...)` | 不要这样写:能力由模型选择,不是你的方法。要确定性调用就**直接调类方法** |
865
- | 子 agent 调不到工具 | `tools` 是 **provider token**(文件夹名)列表,不是工具名 |
1000
+ | 子 agent 调不到工具 | `tools` 的元素是 **provider token** 或 `'<token>/<能力名>'` 路径,不是裸工具名(裸名字会按「未注册 provider」在装配期抛错) |
866
1001
  | 长跑内存涨 | 缺省内存 store 不淘汰;设 `InMemoryTaskStore({ maxRecords })` 或换耐久 store |
867
1002
  | 鉴权钩子抛错,客户端只看到「未通过鉴权」 | 这是设计:非 `HttpException` 的错误原文只进服务端日志(要回给调用方就抛 `HttpException(status, body)`) |
868
1003
  | 停机后 `POST /tasks` 回 503 | `drain()` 已被调用(或注入的 runner 已 drain)—— 这是「拒新单」的正常行为,任务没丢 |
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Agentia —— 消息类型族(公共类型自有化,2026-09-17)。
3
+ *
4
+ * 框架公共面(ModelClient 契约 / RunSpec / SessionStore / replay 等)的消息与内容块
5
+ * 类型,字段口径与 Anthropic Messages API 逐字对齐(snake_case),与
6
+ * `@anthropic-ai/sdk` 的对应类型**结构兼容**(双向/单向 assignability 由
7
+ * `tests/types/message-compat.types.ts` 钉住,SDK 因此只留在 devDependencies 做门禁)。
8
+ *
9
+ * 两条刻意的取舍(都有硬理由,别「顺手改进」):
10
+ *
11
+ * 1. **兜底成员不带索引签名**(`UnknownContentBlockParam` / `UnknownContentBlock` 只有
12
+ * `type: string`)。带 `[key: string]: unknown` 的形态看起来更「前向兼容」,但 SDK 的
13
+ * 块类型全是 **interface**(无隐式索引签名),赋给带索引签名的目标直接编译失败 ——
14
+ * 「使用者手里的 `Anthropic.MessageParam[]` 直接喂给框架」这条承诺就会当场破产。
15
+ * 只有 `type` 的最小面反而两个方向都通:未知块原样携带(历史回灌不丢内容),
16
+ * 要读字段请自行收窄/断言。
17
+ * 2. **`Role` 含 `'system'`**:SDK 0.124 的 `MessageParam.role` 是
18
+ * `'user' | 'assistant' | 'system'`,方向一(SDK → 自有)要求逐字对齐。
19
+ * 引擎自身只产出 user/assistant;给 Messages API 发 system 角色会被端点拒绝,
20
+ * 与 SDK 类型下的同款误用语义一致。
21
+ *
22
+ * 零依赖纯类型文件:不写运行时代码,编译后全部擦除。core 是叶子层,本文件不 import 任何模块。
23
+ */
24
+ /** 消息角色(含 'system' 的原因见文件头注释 2) */
25
+ export type Role = 'user' | 'assistant' | 'system';
26
+ /** prompt cache 断点标记(`cache_control` 字段的值) */
27
+ export interface CacheControl {
28
+ type: 'ephemeral';
29
+ /** 缓存 TTL;缺省 '5m'(与 SDK 的 CacheControlEphemeral 逐字对齐) */
30
+ ttl?: '5m' | '1h';
31
+ }
32
+ export interface TextBlockParam {
33
+ type: 'text';
34
+ text: string;
35
+ cache_control?: CacheControl | null;
36
+ }
37
+ export interface ImageBlockParam {
38
+ type: 'image';
39
+ /**
40
+ * 图片源:base64 / url 为已知形态;其余(如 file_id 源)走 `{ type: string }` 最小面兜底
41
+ * (理由同文件头注释 1)。读具体字段前先按 `source.type` 收窄。
42
+ */
43
+ source: {
44
+ type: 'base64';
45
+ media_type: 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
46
+ data: string;
47
+ } | {
48
+ type: 'url';
49
+ url: string;
50
+ } | {
51
+ type: string;
52
+ };
53
+ cache_control?: CacheControl | null;
54
+ }
55
+ export interface ToolUseBlockParam {
56
+ type: 'tool_use';
57
+ id: string;
58
+ name: string;
59
+ input: unknown;
60
+ cache_control?: CacheControl | null;
61
+ }
62
+ export interface ToolResultBlockParam {
63
+ type: 'tool_result';
64
+ tool_use_id: string;
65
+ /** 工具结果正文:纯文本,或 text/image 等块(未知块型走兜底成员,原样携带) */
66
+ content?: string | Array<TextBlockParam | ImageBlockParam | UnknownContentBlockParam>;
67
+ is_error?: boolean;
68
+ cache_control?: CacheControl | null;
69
+ }
70
+ /**
71
+ * 前向兼容兜底:厂商新增块类型的最小面。**刻意不带索引签名**(见文件头注释 1)——
72
+ * 未知块在消息历史里原样携带(role 交替与 tool_use/tool_result 配对不受影响),
73
+ * 消费方按 `type` 排除已知块后自行断言读取。
74
+ */
75
+ export interface UnknownContentBlockParam {
76
+ type: string;
77
+ }
78
+ /** 请求侧内容块联合:已知块 + 兜底成员(厂商新块型原样携带) */
79
+ export type ContentBlockParam = TextBlockParam | ImageBlockParam | ToolUseBlockParam | ToolResultBlockParam | UnknownContentBlockParam;
80
+ export interface MessageParam {
81
+ role: Role;
82
+ content: string | ContentBlockParam[];
83
+ }
84
+ /** 工具的 input_schema 形状(与 SDK 的 Tool.InputSchema 逐字对齐) */
85
+ export interface ToolInputSchema {
86
+ type: 'object';
87
+ properties?: unknown;
88
+ required?: string[] | null;
89
+ [key: string]: unknown;
90
+ }
91
+ /**
92
+ * 发给模型的工具定义(**避让 @Tool 装饰器**,故名 ToolParam)。
93
+ * 引擎从 AgentTool 推导(`engine/loop.ts` 的 toApiTool);自定义 ModelClient 只读它。
94
+ * 与块类型同理**不带索引签名**(文件头注释 1):SDK 的 `Tool` 是 interface,
95
+ * 带索引签名会让「SDK 的工具定义数组直接喂给自定义 client」编译失败。
96
+ * 厂商新增的工具字段(defer_loading 等)读之前自行断言。
97
+ */
98
+ export interface ToolParam {
99
+ name: string;
100
+ description?: string;
101
+ input_schema: ToolInputSchema;
102
+ strict?: boolean;
103
+ cache_control?: CacheControl | null;
104
+ }
105
+ export interface TextBlock {
106
+ type: 'text';
107
+ text: string;
108
+ }
109
+ export interface ToolUseBlock {
110
+ type: 'tool_use';
111
+ id: string;
112
+ name: string;
113
+ input: unknown;
114
+ }
115
+ export interface ThinkingBlock {
116
+ type: 'thinking';
117
+ thinking: string;
118
+ signature: string;
119
+ }
120
+ /** 响应侧兜底块(语义同 UnknownContentBlockParam,见文件头注释 1) */
121
+ export interface UnknownContentBlock {
122
+ type: string;
123
+ }
124
+ /** 响应侧内容块联合:text / tool_use / thinking + 兜底成员 */
125
+ export type ContentBlock = TextBlock | ToolUseBlock | ThinkingBlock | UnknownContentBlock;
126
+ /**
127
+ * 模型 stop_reason。已知取值与 SDK 逐字对齐;`| (string & {})` 收留端点未来的新值
128
+ * (引擎对未识别的 stop_reason 有 `unknown_stop_reason` 收尾分支,见 engine/loop.ts)。
129
+ */
130
+ export type StopReason = 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | 'pause_turn' | 'refusal' | 'model_context_window_exceeded' | (string & {});
131
+ /** 模型回报的 token 计量(**避让 trace 的 `Usage`**(camelCase 聚合),故名 MessageUsage) */
132
+ export interface MessageUsage {
133
+ input_tokens: number;
134
+ output_tokens: number;
135
+ cache_creation_input_tokens?: number | null;
136
+ cache_read_input_tokens?: number | null;
137
+ }
138
+ /** 一次模型往返的最终响应(`ModelClient.messages.stream().finalMessage()` 的产物) */
139
+ export interface Message {
140
+ id: string;
141
+ type: 'message';
142
+ role: 'assistant';
143
+ content: ContentBlock[];
144
+ model: string;
145
+ stop_reason: StopReason | null;
146
+ stop_sequence: string | null;
147
+ usage: MessageUsage;
148
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Agentia —— 消息类型族(公共类型自有化,2026-09-17)。
3
+ *
4
+ * 框架公共面(ModelClient 契约 / RunSpec / SessionStore / replay 等)的消息与内容块
5
+ * 类型,字段口径与 Anthropic Messages API 逐字对齐(snake_case),与
6
+ * `@anthropic-ai/sdk` 的对应类型**结构兼容**(双向/单向 assignability 由
7
+ * `tests/types/message-compat.types.ts` 钉住,SDK 因此只留在 devDependencies 做门禁)。
8
+ *
9
+ * 两条刻意的取舍(都有硬理由,别「顺手改进」):
10
+ *
11
+ * 1. **兜底成员不带索引签名**(`UnknownContentBlockParam` / `UnknownContentBlock` 只有
12
+ * `type: string`)。带 `[key: string]: unknown` 的形态看起来更「前向兼容」,但 SDK 的
13
+ * 块类型全是 **interface**(无隐式索引签名),赋给带索引签名的目标直接编译失败 ——
14
+ * 「使用者手里的 `Anthropic.MessageParam[]` 直接喂给框架」这条承诺就会当场破产。
15
+ * 只有 `type` 的最小面反而两个方向都通:未知块原样携带(历史回灌不丢内容),
16
+ * 要读字段请自行收窄/断言。
17
+ * 2. **`Role` 含 `'system'`**:SDK 0.124 的 `MessageParam.role` 是
18
+ * `'user' | 'assistant' | 'system'`,方向一(SDK → 自有)要求逐字对齐。
19
+ * 引擎自身只产出 user/assistant;给 Messages API 发 system 角色会被端点拒绝,
20
+ * 与 SDK 类型下的同款误用语义一致。
21
+ *
22
+ * 零依赖纯类型文件:不写运行时代码,编译后全部擦除。core 是叶子层,本文件不 import 任何模块。
23
+ */
24
+ export {};
25
+ //# sourceMappingURL=message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/core/message.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
@@ -1,4 +1,4 @@
1
- import type Anthropic from '@anthropic-ai/sdk';
1
+ import type { Message, MessageParam, TextBlockParam, ToolParam } from './message.js';
2
2
  import type { SpanError, SpanId, SpanKind, SpanStatus, Trace, Usage } from './trace.js';
3
3
  /**
4
4
  * Agentia —— 工具定义。
@@ -56,17 +56,18 @@ export interface RecorderBackend {
56
56
  snapshot(status: SpanStatus): Trace;
57
57
  }
58
58
  /**
59
- * engine 对模型端的最小结构面(R4 多模型):Anthropic SDK 天然满足,
60
- * 其他 provider(OpenAI 兼容端点等)只需适配出同一形态。
59
+ * engine 对模型端的最小结构面(R4 多模型):消息形态由 core/message.js 的自有类型族
60
+ * 定义(与 Anthropic Messages API 逐字对齐),其他 provider(OpenAI 兼容端点等)
61
+ * 只需适配出同一形态。
61
62
  */
62
63
  export interface ModelClient {
63
64
  messages: {
64
65
  stream(params: {
65
66
  model: string;
66
67
  max_tokens: number;
67
- system?: string | Anthropic.TextBlockParam[];
68
- tools?: Anthropic.Tool[];
69
- messages: Anthropic.MessageParam[];
68
+ system?: string | TextBlockParam[];
69
+ tools?: ToolParam[];
70
+ messages: MessageParam[];
70
71
  /**
71
72
  * 中断信号(可选):实现须转发给底层请求,否则调用方无法中止在飞 run。
72
73
  *
@@ -74,14 +75,14 @@ export interface ModelClient {
74
75
  * SDK 把「传输层的 signal」放在**请求选项**里(如 `@anthropic-ai/sdk` 的
75
76
  * `stream(body, options?)` —— `signal` 只在 `RequestOptions` 里认)。若把本 params 原样
76
77
  * 交给 SDK,signal 会被**静默丢弃**,中止失效且**没有任何报错**。
77
- * 参考实现:`integrations/anthropic.ts` 的 `splitSignal()`(把 signal 摘出来搬到 options),
78
- * 以及 `integrations/openai.ts`(手写 fetch,直接透传)。
78
+ * 参考实现:`integrations/anthropic.ts`(手写 fetch,signal 摘出 body 直接传给 fetch),
79
+ * 以及 `integrations/openai.ts`(同款)。
79
80
  * 门禁:`tests/integrations/anthropic.test.ts`(零 key 本地假端点,abort 后必须断开)。
80
81
  */
81
82
  signal?: AbortSignal;
82
83
  }): {
83
84
  on(event: 'text', cb: (delta: string) => void): void;
84
- finalMessage(): Promise<Anthropic.Message>;
85
+ finalMessage(): Promise<Message>;
85
86
  };
86
87
  };
87
88
  }
@@ -62,6 +62,27 @@ export interface Trace {
62
62
  status: SpanStatus;
63
63
  totalUsage: Usage;
64
64
  }
65
+ /**
66
+ * 质量评分(R7 质量闭环):LLM-judge / 人工标注 / eval 结论挂到 trace 上,
67
+ * 让「这条 run 好不好」与「这条 run 发生了什么」同处一份数据。
68
+ * 评分通常来自 run **之外**(跑完后才评),所以走事件而非 span 字段。
69
+ */
70
+ export interface Score {
71
+ /** 评分维度名(如 'faithfulness'、'eval')—— 指标聚合的 label */
72
+ name: string;
73
+ /** 数值分;约定 0–1(布尔结论用 0/1) */
74
+ value: number;
75
+ /** 评分来源(eval 名 / 'human' / judge 模型 id 等) */
76
+ source?: string;
77
+ /** 备注(失败原因、评语) */
78
+ comment?: string;
79
+ }
80
+ /**
81
+ * 把评分挂到 trace 根 span(一条 `score` 事件,body 即 Score)。
82
+ * trace 找不到根 span 时静默忽略(观测不击穿业务);多次调用即多条事件(不同维度各记各的)。
83
+ * 出口映射:OTLP 导出时译为 `gen_ai.evaluation.result` 事件,metricsSink 聚合为 score 指标族。
84
+ */
85
+ export declare function attachScore(trace: Trace, score: Score): void;
65
86
  /**
66
87
  * trace 出口:run 收尾(成功或失败)后,框架把【完整 Trace】交给每个 sink。
67
88
  * sink 抛错由框架吞掉,绝不影响 run 结果(与 memory 回写同款防护)。
@@ -2,5 +2,13 @@
2
2
  * Agentia —— Trace / 调用树 类型草稿。
3
3
  * 设计见 docs/spec.md §9。一次 run == 一条 trace(v1 中 traceId == runId)。
4
4
  */
5
- export {};
5
+ /**
6
+ * 把评分挂到 trace 根 span(一条 `score` 事件,body 即 Score)。
7
+ * trace 找不到根 span 时静默忽略(观测不击穿业务);多次调用即多条事件(不同维度各记各的)。
8
+ * 出口映射:OTLP 导出时译为 `gen_ai.evaluation.result` 事件,metricsSink 聚合为 score 指标族。
9
+ */
10
+ export function attachScore(trace, score) {
11
+ const root = trace.spans.find((s) => s.spanId === trace.rootSpanId);
12
+ root?.events.push({ time: Date.now(), name: 'score', body: score });
13
+ }
6
14
  //# sourceMappingURL=trace.js.map
@@ -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;AAuFH;;;;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"}
@@ -1,4 +1,16 @@
1
1
  import type { SpanError } from '../core/trace.js';
2
+ /**
3
+ * Agentia —— 异常分类(engine 对厂商 SDK 零运行时 import)。
4
+ *
5
+ * 分类是**鸭子类型**:默认 client(`integrations/anthropic.ts`)自研化后抛的是
6
+ * 带数值 `status` 的 `AnthropicApiError` 与 fetch 的原生网络错误(TypeError 带 cause),
7
+ * 不再是 SDK 错误类。同套判法对第三方 SDK 错误同样适用(它们的错误类也带 `status`)。
8
+ *
9
+ * 历史教训(2026-09-13 实测,当时因此保留 instanceof):厂商 SDK 的错误类
10
+ * `name` 恒为 `'Error'`、`type` 为 null,鸭子类型只能靠 `constructor.name` ——
11
+ * **压缩即失效**。所以这里只认**数据属性**(`status` 数值 / `cause` / errno `code` /
12
+ * 内建 DOMException 的 `name`),一概不碰构造函数名。
13
+ */
2
14
  /** 是否中断异常(DOMException / 普通 Error 都可能是,靠 name 判定) */
3
15
  export declare function isAbortError(e: unknown): boolean;
4
16
  /** 把任意异常分类成 trace 可用的 SpanError(可重试 vs 不可重试)。 */