@migor/agentia 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +144 -0
- package/README.md +202 -76
- package/dist/AGENTS.md +811 -0
- package/dist/container/container.d.ts +10 -2
- package/dist/container/container.js +27 -3
- package/dist/container/container.js.map +1 -1
- package/dist/core/abort.d.ts +10 -0
- package/dist/core/abort.js +28 -0
- package/dist/core/abort.js.map +1 -0
- package/dist/core/blackboard.d.ts +37 -0
- package/dist/core/blackboard.js +2 -0
- package/dist/core/blackboard.js.map +1 -0
- package/dist/core/json.d.ts +2 -0
- package/dist/core/json.js +4 -0
- package/dist/core/json.js.map +1 -1
- package/dist/core/run.d.ts +18 -0
- package/dist/core/run.js +2 -0
- package/dist/core/run.js.map +1 -0
- package/dist/core/schema.js +2 -2
- package/dist/core/schema.js.map +1 -1
- package/dist/core/tool.d.ts +62 -7
- package/dist/core/trace.d.ts +20 -4
- package/dist/engine/budget.d.ts +49 -0
- package/dist/engine/budget.js +30 -0
- package/dist/engine/budget.js.map +1 -0
- package/dist/engine/concurrency.d.ts +43 -0
- package/dist/engine/concurrency.js +95 -0
- package/dist/engine/concurrency.js.map +1 -0
- package/dist/engine/errors.d.ts +2 -0
- package/dist/engine/errors.js +7 -0
- package/dist/engine/errors.js.map +1 -1
- package/dist/engine/loop.d.ts +35 -10
- package/dist/engine/loop.js +336 -66
- package/dist/engine/loop.js.map +1 -1
- package/dist/engine/policy.d.ts +9 -1
- package/dist/engine/policy.js +8 -5
- package/dist/engine/policy.js.map +1 -1
- package/dist/engine/replay.d.ts +8 -5
- package/dist/engine/replay.js +30 -29
- package/dist/engine/replay.js.map +1 -1
- package/dist/engine/retry.d.ts +47 -0
- package/dist/engine/retry.js +51 -0
- package/dist/engine/retry.js.map +1 -0
- package/dist/engine/spec.d.ts +61 -0
- package/dist/{run → engine}/spec.js +4 -1
- package/dist/engine/spec.js.map +1 -0
- package/dist/engine/tracer.d.ts +9 -0
- package/dist/engine/tracer.js +57 -1
- package/dist/engine/tracer.js.map +1 -1
- package/dist/engine/{context.d.ts → trimming.d.ts} +19 -20
- package/dist/engine/trimming.js +238 -0
- package/dist/engine/trimming.js.map +1 -0
- package/dist/engine/types.d.ts +109 -13
- package/dist/engine/types.js +9 -1
- package/dist/engine/types.js.map +1 -1
- package/dist/engine/usage.d.ts +28 -2
- package/dist/engine/usage.js +39 -6
- package/dist/engine/usage.js.map +1 -1
- package/dist/eval/defineEval.d.ts +73 -0
- package/dist/eval/defineEval.js +44 -0
- package/dist/eval/defineEval.js.map +1 -0
- package/dist/eval/scripted.d.ts +26 -0
- package/dist/eval/scripted.js +43 -0
- package/dist/eval/scripted.js.map +1 -0
- package/dist/index.d.ts +60 -35
- package/dist/index.js +38 -20
- package/dist/index.js.map +1 -1
- package/dist/integrations/anthropic.d.ts +53 -0
- package/dist/integrations/anthropic.js +24 -0
- package/dist/integrations/anthropic.js.map +1 -0
- package/dist/integrations/mcp.d.ts +79 -0
- package/dist/integrations/mcp.js +108 -0
- package/dist/integrations/mcp.js.map +1 -0
- package/dist/integrations/metrics.d.ts +131 -0
- package/dist/integrations/metrics.js +488 -0
- package/dist/integrations/metrics.js.map +1 -0
- package/dist/{run → integrations}/openai.d.ts +12 -5
- package/dist/integrations/openai.js +386 -0
- package/dist/integrations/openai.js.map +1 -0
- package/dist/integrations/otlp.js.map +1 -0
- package/dist/integrations/report.d.ts +78 -0
- package/dist/integrations/report.js +257 -0
- package/dist/integrations/report.js.map +1 -0
- package/dist/runtime/context.d.ts +25 -0
- package/dist/{run → runtime}/context.js +8 -1
- package/dist/runtime/context.js.map +1 -0
- package/dist/{run → runtime}/memory.d.ts +1 -1
- package/dist/{run → runtime}/memory.js +3 -1
- package/dist/runtime/memory.js.map +1 -0
- package/dist/{run → runtime}/run.d.ts +22 -5
- package/dist/runtime/run.js +218 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/session.d.ts +27 -0
- package/dist/runtime/session.js +16 -0
- package/dist/runtime/session.js.map +1 -0
- package/dist/{run → runtime}/spec.d.ts +16 -2
- package/dist/runtime/spec.js +28 -0
- package/dist/runtime/spec.js.map +1 -0
- package/dist/{run → runtime}/systemPrompt.d.ts +12 -0
- package/dist/{run → runtime}/systemPrompt.js +16 -3
- package/dist/runtime/systemPrompt.js.map +1 -0
- package/dist/{run → runtime}/types.js.map +1 -1
- package/dist/{run → store}/fsStore.d.ts +15 -0
- package/dist/{run → store}/fsStore.js +43 -1
- package/dist/store/fsStore.js.map +1 -0
- package/dist/{run → store}/redisStore.d.ts +19 -1
- package/dist/{run → store}/redisStore.js +33 -6
- package/dist/store/redisStore.js.map +1 -0
- package/dist/{run → store}/sqliteStore.js +22 -1
- package/dist/store/sqliteStore.js.map +1 -0
- package/dist/{run → store}/store.d.ts +25 -3
- package/dist/store/store.js +63 -0
- package/dist/store/store.js.map +1 -0
- package/dist/toolkit/asset.d.ts +7 -6
- package/dist/toolkit/asset.js +7 -6
- package/dist/toolkit/asset.js.map +1 -1
- package/dist/toolkit/collect.d.ts +20 -7
- package/dist/toolkit/collect.js +21 -7
- package/dist/toolkit/collect.js.map +1 -1
- package/dist/toolkit/discover.d.ts +21 -1
- package/dist/toolkit/discover.js +59 -12
- package/dist/toolkit/discover.js.map +1 -1
- package/dist/toolkit/middleware.d.ts +10 -10
- package/dist/toolkit/middleware.js +17 -4
- package/dist/toolkit/middleware.js.map +1 -1
- package/dist/toolkit/module.d.ts +100 -34
- package/dist/toolkit/module.js +112 -36
- package/dist/toolkit/module.js.map +1 -1
- package/dist/toolkit/prompt.d.ts +4 -6
- package/dist/toolkit/prompt.js +11 -10
- package/dist/toolkit/prompt.js.map +1 -1
- package/dist/toolkit/skill.d.ts +14 -16
- package/dist/toolkit/skill.js +32 -25
- package/dist/toolkit/skill.js.map +1 -1
- package/dist/toolkit/subagent.d.ts +10 -12
- package/dist/toolkit/subagent.js +23 -21
- package/dist/toolkit/subagent.js.map +1 -1
- package/dist/toolkit/tool.d.ts +19 -11
- package/dist/toolkit/tool.js +11 -8
- package/dist/toolkit/tool.js.map +1 -1
- package/dist/toolkit/zod.d.ts +4 -3
- package/dist/toolkit/zod.js +2 -1
- package/dist/toolkit/zod.js.map +1 -1
- package/dist/transport/async.d.ts +133 -0
- package/dist/transport/async.js +415 -0
- package/dist/transport/async.js.map +1 -0
- package/dist/transport/http.d.ts +146 -0
- package/dist/transport/http.js +387 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/{run → transport}/scheduler.d.ts +9 -1
- package/dist/transport/scheduler.js +133 -0
- package/dist/transport/scheduler.js.map +1 -0
- package/dist/transport/sse.d.ts +43 -0
- package/dist/transport/sse.js +51 -0
- package/dist/transport/sse.js.map +1 -0
- package/dist/{run → transport}/transport.d.ts +1 -1
- package/dist/{run → transport}/transport.js +1 -1
- package/dist/transport/transport.js.map +1 -0
- package/package.json +24 -9
- package/dist/engine/context.js +0 -146
- package/dist/engine/context.js.map +0 -1
- package/dist/run/async.d.ts +0 -59
- package/dist/run/async.js +0 -201
- package/dist/run/async.js.map +0 -1
- package/dist/run/context.d.ts +0 -16
- package/dist/run/context.js.map +0 -1
- package/dist/run/fsStore.js.map +0 -1
- package/dist/run/http.d.ts +0 -47
- package/dist/run/http.js +0 -119
- package/dist/run/http.js.map +0 -1
- package/dist/run/memory.js.map +0 -1
- package/dist/run/openai.js +0 -181
- package/dist/run/openai.js.map +0 -1
- package/dist/run/otlp.js.map +0 -1
- package/dist/run/redisStore.js.map +0 -1
- package/dist/run/run.js +0 -129
- package/dist/run/run.js.map +0 -1
- package/dist/run/scheduler.js +0 -85
- package/dist/run/scheduler.js.map +0 -1
- package/dist/run/spec.js.map +0 -1
- package/dist/run/sqliteStore.js.map +0 -1
- package/dist/run/store.js +0 -28
- package/dist/run/store.js.map +0 -1
- package/dist/run/systemPrompt.js.map +0 -1
- package/dist/run/transport.js.map +0 -1
- /package/dist/{run → integrations}/otlp.d.ts +0 -0
- /package/dist/{run → integrations}/otlp.js +0 -0
- /package/dist/{run → runtime}/types.d.ts +0 -0
- /package/dist/{run → runtime}/types.js +0 -0
- /package/dist/{run → store}/sqliteStore.d.ts +0 -0
package/dist/AGENTS.md
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
# Agentia 使用说明(给 AI 与人)
|
|
2
|
+
|
|
3
|
+
> 本文件是 **AI 辅助编码的权威入口**,也是人类速查表。
|
|
4
|
+
> 仓库根部的 `AGENTS.md` 讲的是「怎么改这个仓库」;**本文件讲的是「怎么用这个框架」**。
|
|
5
|
+
> 文中所有 API 名与选项名都由框架仓库的测试对着源码校验 —— 改名会立刻失败,不会静默过期。
|
|
6
|
+
|
|
7
|
+
包名:`@migor/agentia`(框架)/ `@migor/cli`(命令行)。Node ≥ 18,ESM,TypeScript 7。
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 0. 心智模型(先读这一段)
|
|
12
|
+
|
|
13
|
+
一次 **run** = 一个 agent 循环跑完一件事,产出一条 **trace**(`traceId === runId`)。
|
|
14
|
+
|
|
15
|
+
你声明 **四类能力**,它们进同一个「工具菜单」;**主 agent 的模型按 `description` 自己选**:
|
|
16
|
+
|
|
17
|
+
| 能力 | 装饰器 | 谁决定流程 | 典型用途 |
|
|
18
|
+
|---|---|---|---|
|
|
19
|
+
| 工具 | `@Tool` | 你的代码(一次调用 = 一个函数) | 确定性操作:查库、算数、调 API |
|
|
20
|
+
| 技能 | `@Skill` | 你的代码(脚本式,可显式调模型) | 「先取数 → 再让模型写 → 再加工」这种固定流程 |
|
|
21
|
+
| 子 agent | `@SubAgent` | **模型自己**(独立循环 + 裁剪上下文) | 需要自主多步、且中间过程不该污染主上下文 |
|
|
22
|
+
| 提示资产 | `@Prompt` | 模型拉取(本质是「按需注入的文本」) | 长文规范/模板,平时不进上下文,需要时拉 |
|
|
23
|
+
|
|
24
|
+
**关键推论**:能力是**运行时**从装饰器注册表收集的,所以 TypeScript 里**没有**「你的能力清单」这种类型 ——
|
|
25
|
+
不要写 `app.hello()`。模型通过 `description` 选能力,你通过 `schema` 约束入参。
|
|
26
|
+
|
|
27
|
+
**同样是一等公民的是可观测**:这条 trace 带每步的 token / 成本 / 耗时 / 错误类型,run 收尾经 `TraceSink`
|
|
28
|
+
出口交给你(落库 / 采样 / 脱敏都在缝外,见 §6「观测」)。一句话——**四类能力决定它能做什么,trace 决定你敢不敢上线**。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. 最小可运行示例
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Tool, createApp, SystemPrompt } from '@migor/agentia';
|
|
36
|
+
|
|
37
|
+
const OBJ = { type: 'object', properties: {} } as const;
|
|
38
|
+
|
|
39
|
+
class Greeter {
|
|
40
|
+
@Tool({
|
|
41
|
+
description: '向某人打招呼',
|
|
42
|
+
schema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: { name: { type: 'string' } },
|
|
45
|
+
required: ['name'],
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
say_hello(input: { name: string }): string {
|
|
49
|
+
return `你好,${input.name}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const app = createApp({
|
|
54
|
+
name: 'greeter',
|
|
55
|
+
providers: [{ provide: 'greeter', useClass: Greeter }],
|
|
56
|
+
system: new SystemPrompt().add('role', '你是友好的助手。', true),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const { result } = await app.run([{ role: 'user', content: '跟小明打个招呼' }]);
|
|
60
|
+
console.log(result.finalText, result.stopReason, result.trace.totalUsage);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
要点:
|
|
64
|
+
- `@Tool` 方法的**入参必须显式标注类型**(TS 无法从 JSON Schema 反向推断方法形参;`strict` 下不标注会报隐式 any)。
|
|
65
|
+
- `schema` 是给**模型**看的契约;方法形参类型是给**你和编译器**看的。想避免两处双写,见第 5 节 `fromZod<T>`。
|
|
66
|
+
- 相对 import 必须带 `.js` 后缀(NodeNext)。
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 2. 项目结构(CLI 约定)
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx @migor/cli create my-app # 脚手架
|
|
74
|
+
cd my-app && npm install
|
|
75
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
76
|
+
npx @migor/cli dev # tsx watch + 本地 inspector 面板
|
|
77
|
+
npx @migor/cli g tool fetch-weather # 生成到 src/tools/fetch-weather/(skill/prompt/subagent 同理)
|
|
78
|
+
npx @migor/cli doctor # 静态体检(未登记/悬空/命名/重复)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
四分类目录,一能力一文件夹:`src/tools/` · `src/skills/` · `src/prompts/` · `src/subagents/` —— **目录名就是类型**,不用记别名。每个文件夹的 `index.ts` 是入口,`default export` 支持三种形态:**类**(token = 文件夹名)、**Provider 对象**、**Provider 数组**。显式注册表在 `src/registry.ts`(`agentia g` 自动维护,也可手改)。
|
|
82
|
+
|
|
83
|
+
> **陷阱**:装饰器注册表是模块级 `WeakMap`。框架必须是**单一模块实例** —— 混用 `src` 与 `dist`、或在一个仓库里装两份 agentia,会让能力收集为空。让 CLI 生成的 `package.json` 里只依赖一份框架即可。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 3. 装饰器 spec 字段速查
|
|
88
|
+
|
|
89
|
+
### `@Tool(spec: ToolSpec)`
|
|
90
|
+
|
|
91
|
+
| 字段 | 说明 |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `description` | 模型据此判断何时调用(**必填**,写好它比什么都重要) |
|
|
94
|
+
| `schema` | 入参 JSON Schema;传 `fromZod<T>(...)` 可获得签名校验 |
|
|
95
|
+
| `name` | 模型可见的工具名,缺省取方法名(建议 snake_case) |
|
|
96
|
+
| `strict` | 透传给 Anthropic 的 strict 模式(**框架不校验 schema 合规性**) |
|
|
97
|
+
|
|
98
|
+
### `@Skill(spec: SkillSpec)`
|
|
99
|
+
|
|
100
|
+
| 字段 | 说明 |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `description` | 同 `@Tool` |
|
|
103
|
+
| `schema` | 主 agent 传给技能的入参 schema |
|
|
104
|
+
| `name` | 缺省取方法名 |
|
|
105
|
+
| `model` | `ctx.llm()` 的缺省模型 |
|
|
106
|
+
| `maxTokens` | 同上 |
|
|
107
|
+
| `maxIterations` | 同上 |
|
|
108
|
+
| `tools` | `ctx.llm()` 可调工具:**provider token 列表**(复用该 provider 的 `@Tool` 菜单) |
|
|
109
|
+
|
|
110
|
+
技能方法体拿到的第二参是 `SkillContext`:`ctx.llm({ prompt })` 才会真正调模型(脚本式,调几次由你写死)。
|
|
111
|
+
|
|
112
|
+
### `@SubAgent(spec: SubAgentSpec)`
|
|
113
|
+
|
|
114
|
+
| 字段 | 说明 |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `description` | 同 `@Tool` |
|
|
117
|
+
| `schema` | 主 agent 填给子 agent 的任务入参 schema |
|
|
118
|
+
| `name` | 缺省取方法名 |
|
|
119
|
+
| `system` | 子 agent 的角色提示:`string` / `SystemPrompt` / `(task) => SystemParam` |
|
|
120
|
+
| `tools` | 子 agent 可调工具:**provider token 列表** |
|
|
121
|
+
| `model` | 子 agent 自己的模型 |
|
|
122
|
+
| `maxTokens` | 同上 |
|
|
123
|
+
| `maxIterations` | 同上 |
|
|
124
|
+
| `resultSchema` | 给出后子 agent 用隐藏 `submit_result` 提交结构化结果 |
|
|
125
|
+
|
|
126
|
+
子 agent 是**独立循环 + 裁剪上下文**:主 agent 的历史它看不到,只有最终文本(或 `{ report, result }`)以 tool_result 回流。
|
|
127
|
+
|
|
128
|
+
### `@Prompt(spec: PromptSpec)`
|
|
129
|
+
|
|
130
|
+
| 字段 | 说明 |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `description` | 描述**何时该拉取**这段文本 |
|
|
133
|
+
| `name` | 缺省取方法名 |
|
|
134
|
+
| `schema` | 模板化入参 schema;缺省空对象(无参资产) |
|
|
135
|
+
|
|
136
|
+
`@Prompt` **只支持方法形态**(标准装饰器下字段拿不到值/类引用)。方法体内用 `asset(import.meta.url, './x.md')` 读同目录长文本。
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 4. `createApp` 与 `app.run` 选项
|
|
141
|
+
|
|
142
|
+
### `createApp(options: AppOptions)`
|
|
143
|
+
|
|
144
|
+
| 选项 | 说明 |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `system` | **必填**。`SystemPrompt` 实例(自动打 cache breakpoint)或已拼好的 `SystemParam` |
|
|
147
|
+
| `name` | 应用名,同时作为 run 名写进 trace |
|
|
148
|
+
| `providers` | DI providers:`useValue` / `useClass` / `useFactory` + `deps` |
|
|
149
|
+
| `modules` | 能力包(`defineModule({ providers, middleware })`),模块级先注册、应用级可覆盖同 token |
|
|
150
|
+
| `discover` | 能力目录路径:**一个目录或一组目录**(数组顺序即装配顺序,典型是四分类目录)。给出后 `createApp` 返回 `Promise<AgentApp>`;数组里任一目录不存在会**报错**(显式给出的搜索路径不该静默落空) |
|
|
151
|
+
| `model` | 缺省模型;不给则 `AGENTIA_MODEL` env,再回落 `claude-opus-5` |
|
|
152
|
+
| `maxTokens` | 缺省 `max_tokens` |
|
|
153
|
+
| `maxIterations` | 缺省循环上限 |
|
|
154
|
+
| `contextPolicy` | 上下文预算策略(`createBudgetPolicy(...)`) |
|
|
155
|
+
| `toolSources` | 白名单:只把这些 provider 的能力放进主菜单 |
|
|
156
|
+
| `tools` | 直接追加到主菜单的**裸工具**(`AgentTool[]`):给「构造期才知道有哪些工具」的场合(典型:MCP 桥,见 §6)。与能力**同过中间件、同进重名查重**,不是旁路 |
|
|
157
|
+
| `middleware` | 能力调用中间件(洋葱链,链序 = 注册顺序) |
|
|
158
|
+
| `sinks` | trace 出口,run 收尾投递 |
|
|
159
|
+
| `maxTotalTokens` | 缺省成本硬管控:整条 run 累计 token 上限(可被单次 run 覆盖) |
|
|
160
|
+
| `maxCostUsd` | 缺省成本硬管控:累计成本(美元)上限(**依赖模型在价格表内**,见 `priceOverrides`;未定价模型会留 `usage.unpriced` 事件,所以「护栏有没有真的生效」看得见) |
|
|
161
|
+
| `priceOverrides` | 价格表覆盖/追加(`$/1M tokens`):覆盖内置同名项,或给非 Anthropic 模型定价(如 `{ 'deepseek-chat': { in: 0.27, out: 1.10 } }`)。**透传给子 agent/skill 的子循环** —— 不会「主 agent 有成本、子 agent 恒 0」。非法单价在 run 开始即抛错 |
|
|
162
|
+
| `onUnpricedModel` | 遇到价格表外的模型时回调(`{ model, spanId }`,每个循环作用域内每模型一次);抛错被吞,**不改变 run 结局**(定价缺失是宿主配置问题)。用它接告警 |
|
|
163
|
+
| `toolTimeoutMs` | 缺省单工具超时(毫秒);超时该条 tool_result 记 is_error,不杀 run |
|
|
164
|
+
| `maxToolConcurrency` | 缺省同回合并行工具上限;不设 = 不限(全并行) |
|
|
165
|
+
|
|
166
|
+
### `app.run(messages, opts?: RunAppOptions)`
|
|
167
|
+
|
|
168
|
+
| 选项 | 说明 |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `system` | 单次覆盖 system(volatile 段建议每 run 重建) |
|
|
171
|
+
| `model` | 单次覆盖模型 |
|
|
172
|
+
| `maxTokens` | 单次覆盖 |
|
|
173
|
+
| `maxIterations` | 单次覆盖 |
|
|
174
|
+
| `client` | 注入 `ModelClient`(换 OpenAI 兼容端点等) |
|
|
175
|
+
| `onText` | 文本增量回调(SSE/终端) |
|
|
176
|
+
| `blackboard` | 预置黑板种子(配 `Blackboard` 声明合并有键补全) |
|
|
177
|
+
| `contextPolicy` | 单次覆盖上下文策略 |
|
|
178
|
+
| `idempotencyKey` | 幂等键(异步宿主的 at-least-once 去重依据) |
|
|
179
|
+
| `rethrow` | 硬失败是否抛出;缺省 `true`(异步宿主置 `false`,落 failed 记录而非冒泡) |
|
|
180
|
+
| `tools` | 单次覆盖工具菜单 |
|
|
181
|
+
| `resultSchema` | 结构化结果 schema;配 `fromZod<T>` 可让 `result.typed` 自动是 `T` |
|
|
182
|
+
| `maxTotalTokens` | 成本硬管控:整条 run 累计 token 上限;超限以 `stopReason='budget_exceeded'` 收尾(**算失败**) |
|
|
183
|
+
| `maxCostUsd` | 成本硬管控:累计成本(美元)上限;模型不在价格表内时**不触发**(用 `priceOverrides` 定价,或用 `maxTotalTokens` 兜底) |
|
|
184
|
+
| `priceOverrides` | 单次覆盖价格表(`$/1M tokens`);语义同 `createApp` 的 `priceOverrides` |
|
|
185
|
+
| `onUnpricedModel` | 单次覆盖未定价回调(**是函数,因此不在 transport 的 `RunInvocationOptions` 里** —— 异步宿主不会替你传) |
|
|
186
|
+
| `toolTimeoutMs` | 单个工具执行超时(毫秒);超时该条 tool_result 记 is_error,run 继续 |
|
|
187
|
+
| `maxToolConcurrency` | 同回合并行工具上限;缺省不限 |
|
|
188
|
+
| `session` | 会话持久化 `{ store, id }`:run 前拼历史、成功收尾追加本轮(见 `SessionStore`) |
|
|
189
|
+
|
|
190
|
+
返回 `AgentRunOutput`:`{ run, result }`。`result` 含 `trace` / `stopReason` / `finalText` / `iterations` / `error` / `typed`。
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 5. 类型链路(这块决定「编辑器给不给提示」)
|
|
195
|
+
|
|
196
|
+
三条链路默认是**关闭**的(为向后兼容),声明后才生效。**写 AI 代码时优先用它们**,因为它把「两处双写」变成「一处声明、编译器兜底」。
|
|
197
|
+
|
|
198
|
+
### 5.1 黑板键:`Blackboard` 声明合并
|
|
199
|
+
|
|
200
|
+
不声明时 `ctx.get('任意字符串')` 返回 `unknown`(人肉记键名)。声明一次,全局生效:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
declare module '@migor/agentia' {
|
|
204
|
+
interface Blackboard {
|
|
205
|
+
profile: { name: string; vip: boolean };
|
|
206
|
+
turnCount: number;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 之后:
|
|
210
|
+
ctx.get('profile'); // { name: string; vip: boolean } | undefined
|
|
211
|
+
ctx.set('turnCount', 1);
|
|
212
|
+
ctx.get('profil'); // ✗ 编译期报错(键不存在)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
动态键(运行期算出来的 `string`)拿不到字面量联合,按文档断言:`ctx.get(key as BlackboardKey)`。
|
|
216
|
+
|
|
217
|
+
### 5.2 schema 即单一事实来源:`fromZod<T>`
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const Weather = z.object({ city: z.string(), days: z.number().int().min(1).max(7) });
|
|
221
|
+
|
|
222
|
+
class WeatherTools {
|
|
223
|
+
@Tool({
|
|
224
|
+
description: '查天气',
|
|
225
|
+
schema: fromZod<z.infer<typeof Weather>>(z.toJSONSchema(Weather) as JsonSchema, Weather),
|
|
226
|
+
})
|
|
227
|
+
get_weather(input: { city: string; days: number }): string {
|
|
228
|
+
return `${input.city} ${input.days}`;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- 框架**永不 import zod**(duck-typed,只认 `safeParse`);
|
|
234
|
+
- `fromZod<T>` 之后,**方法签名与 `T` 不一致会编译期报错** —— 不用手写泛型;
|
|
235
|
+
- 不写 `<T>`(或直接用裸 JsonSchema)→ 入参回落 `any`,**不校验**(旧行为,不算错,只是没护栏)。
|
|
236
|
+
|
|
237
|
+
### 5.3 结构化结果:`typed` 自动推导
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const { result } = await app.run(messages, {
|
|
241
|
+
resultSchema: fromZod<{ answer: string }>(schema, zodSchema),
|
|
242
|
+
});
|
|
243
|
+
result.typed; // { answer: string } | undefined
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
模型没提交就是 `undefined`(不是失败)。`stopReason` 仍以 `end_turn` 正常收尾。
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 6. 运行时 API
|
|
251
|
+
|
|
252
|
+
### `RunContext`(`RunContext.current()`,run 内任意异步上下文可取)
|
|
253
|
+
|
|
254
|
+
| 成员 | 说明 |
|
|
255
|
+
|---|---|
|
|
256
|
+
| `runId` | 本次 run 的 id(== traceId) |
|
|
257
|
+
| `get` | 读黑板(配 `Blackboard` 有键类型) |
|
|
258
|
+
| `set` | 写黑板(链式返回 this) |
|
|
259
|
+
| `has` | 键是否存在 |
|
|
260
|
+
| `delete` | 删除键 |
|
|
261
|
+
| `keys` | 当前全部键 |
|
|
262
|
+
|
|
263
|
+
### `SkillContext`(`@Skill` 方法第二参)
|
|
264
|
+
|
|
265
|
+
| 成员 | 说明 |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `model` | 本次技能的缺省模型 |
|
|
268
|
+
| `llm` | 受限子运行:`await ctx.llm({ prompt })` / `{ messages, system, model, maxTokens, maxIterations }` |
|
|
269
|
+
|
|
270
|
+
### 装配与执行
|
|
271
|
+
|
|
272
|
+
| API | 说明 |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `createApp` | 装配应用(同步;带 `discover` 时为 `Promise<AgentApp>`) |
|
|
275
|
+
| `AgentApp` | `run` / `tools` / `container` / `name` |
|
|
276
|
+
| `defineModule` | 定义能力包 |
|
|
277
|
+
| `executeRun` | 低层 run 入口(自己管 `Run`/trace sink/记忆) |
|
|
278
|
+
| `runAgent` | 更底层:只要一个循环 + trace,不管 run 生命周期 |
|
|
279
|
+
| `SystemPrompt` | 系统提示拼装 + cache 布局 |
|
|
280
|
+
| `Container` | 显式 DI:`register` / `resolve` / `has` / `registered` |
|
|
281
|
+
| `validateJsonSchema` | 框架的 JSON Schema 子集校验(含路径的可读错误) |
|
|
282
|
+
| `classifyError` | 异常 → `{ type, message, retryable }` |
|
|
283
|
+
| `isSuccessStopReason` | `end_turn` / `stop_sequence` 都算正常收尾 |
|
|
284
|
+
| `resolveDefaultModel` | 显式 > `AGENTIA_MODEL` > `claude-opus-5` |
|
|
285
|
+
|
|
286
|
+
### 宿主(换宿主不换语义)
|
|
287
|
+
|
|
288
|
+
| API | 说明 |
|
|
289
|
+
|---|---|
|
|
290
|
+
| `createHttpHandler` | `(req,res)` handler:`POST /run` 同步(带 `Accept: text/event-stream` 则 SSE 流式)、`POST /tasks` 异步、`GET /tasks/:id`、`GET /healthz`;返回值另带 `drain()` 与 `runner` |
|
|
291
|
+
| `AsyncRunner` | 异步任务宿主(`submit` / `poll` / `awaitTask` / `resumePending` / `drain`) |
|
|
292
|
+
| `TaskSink` | 任务完成回调 `{ onFinished(rec) }`,配 `AsyncRunner({ taskSinks })`;抛错被吞 |
|
|
293
|
+
| `HttpException` | 鉴权钩子抛出以自定 HTTP 状态与响应体(抛别的错误一律按 401 处理) |
|
|
294
|
+
| `Scheduler` | 定时触发(`every` / `at`) |
|
|
295
|
+
| `runSync` | 同步 RPC(`(input, opts?) => result`) |
|
|
296
|
+
| `InMemoryTaskStore` | 内存任务存储(可设 `maxRecords` 做内存闸门) |
|
|
297
|
+
| `FileTaskStore` | JSONL 耐久存储(`compact()` 可压实日志) |
|
|
298
|
+
| `SqliteTaskStore` | `node:sqlite` 耐久存储(WAL + busy_timeout) |
|
|
299
|
+
| `RedisTaskStore` | duck-typed Redis 存储(可设 `ttlSeconds`) |
|
|
300
|
+
|
|
301
|
+
### HTTP 端点速查(`createHttpHandler` 的路由)
|
|
302
|
+
|
|
303
|
+
| 端点 | 请求 | 响应 |
|
|
304
|
+
|---|---|---|
|
|
305
|
+
| `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 错误码) |
|
|
306
|
+
| `POST /tasks` | `{ input, idempotencyKey?, options? }` —— `input` 同 `RunInput`;`options` 是 `RunInvocationOptions` | 202 `TaskRecord`(`status: 'queued'`);同 `idempotencyKey` 未失败则去重,直接返回既有记录 |
|
|
307
|
+
| `GET /tasks/:id` | — | 200 `TaskRecord`;不存在 → 404。**停机中仍可轮询**(否则拿不到在飞任务的结果) |
|
|
308
|
+
| `GET /healthz` | — | 200 `HealthResponse`;**不鉴权**,停机中也回 200 |
|
|
309
|
+
| `GET /metrics` | — | 200 Prometheus 文本(`text/plain; version=0.0.4`);**需在 `createHttpHandler` 里传 `metrics`**,**不鉴权**(与 `/healthz` 同档),停机中也回 |
|
|
310
|
+
|
|
311
|
+
方法不符 → 405(带 `Allow` 头);路径不符 → 404;body 非法 JSON → 400;body 超 `maxBodyBytes` → 413;
|
|
312
|
+
`POST /run` 超 `maxConcurrentRuns` → 503 + `Retry-After`;停机中 `POST /run`、`POST /tasks` → 503。
|
|
313
|
+
|
|
314
|
+
**换 model client 的缝**:HTTP 宿主**不持有 client** —— 同步 `/run` 走的是 `app.run(messages, opts)`,
|
|
315
|
+
而 `AppCallable` 就是 `{ name, run }`。所以要换 provider(OpenAI 兼容端点 / 自建 client),
|
|
316
|
+
**包一层**把 `client` 补进 `opts` 即可:
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
const callable = { name: app.name, run: (msgs, opts) => app.run(msgs, { ...opts, client: myClient }) };
|
|
320
|
+
const handler = createHttpHandler(callable, { runner });
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
异步侧更直接:`AsyncRunner` 的构造选项就有 `client`。完整可跑写法见仓库 `examples/complete/`。
|
|
324
|
+
|
|
325
|
+
### `createHttpHandler(app, opts?: HttpHandlerOptions)`
|
|
326
|
+
|
|
327
|
+
| 选项 | 说明 |
|
|
328
|
+
|---|---|
|
|
329
|
+
| `authenticate` | 入口鉴权钩子:**除 `/healthz` 与 `/metrics` 外所有路径**都过它,且在**读 body 之前**(未通过就不收 body)。正常返回即通过;抛 `HttpException` 按其 `status`/`body` 回;抛别的错误回 401,原文只进服务端日志。框架**不实现策略**(不读 env、不碰凭据) |
|
|
330
|
+
| `metrics` | 指标出口:给 `metricsSink()`(或任意 `{ render() }` / 返回字符串的闭包)后,`GET /metrics` 回它的 Prometheus 文本。**不鉴权**(拉取端在集群内网);要保护请放反代后面。不给则该路径 404 |
|
|
331
|
+
| `maxBodyBytes` | 请求 body 上限(字节),超限回 413;缺省 1 MiB |
|
|
332
|
+
| `maxConcurrentRuns` | 同时在跑的 `POST /run` 上限,超限回 503 + `Retry-After`;缺省 32(传 `Infinity` 恢复无上限) |
|
|
333
|
+
| `exposeErrors` | 是否把内部异常原文回给调用方;缺省 `false`(细节只进服务端日志) |
|
|
334
|
+
| `runner` | 注入 `AsyncRunner`(共用 store / 并发上限 / `resumePending`);缺省内部 `new AsyncRunner(app)` |
|
|
335
|
+
|
|
336
|
+
返回值另外挂着两样(不影响 `(req,res)` 的调用形状):
|
|
337
|
+
|
|
338
|
+
- **`handler.drain(opts?)`** —— 优雅停机:拒新单(`POST /run` 与 `/tasks` → 503,`GET /tasks/:id` 仍可轮询)→ 等异步任务与在飞同步 run 收尾 → 强制收口仍开着的 SSE 流。返回是否排空干净;超时返回 `false`,**未完成的任务留在 store 里**,下次启动由 `resumePending` 续跑(不是丢弃)。`timeoutMs` 缺省 0 = 一直等。
|
|
339
|
+
**框架不订阅信号** —— `process.on('SIGTERM', () => handler.drain())` 是宿主的事(同「框架不读 env」)。
|
|
340
|
+
- **`handler.runner`** —— 内部 `AsyncRunner`,需要时手动控制(`resumePending` / `awaitTask` / `list`)。
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const handler = createHttpHandler(app, {
|
|
344
|
+
// 只给缝:token/JWT/签名策略由你的宿主或反代实现
|
|
345
|
+
authenticate: (req) => {
|
|
346
|
+
if (req.headers['x-api-key'] !== expected) throw new HttpException(401, { error: '无效凭据' });
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
http.createServer(handler).listen(3000);
|
|
350
|
+
|
|
351
|
+
process.on('SIGTERM', async () => {
|
|
352
|
+
const clean = await handler.drain({ timeoutMs: 15_000 });
|
|
353
|
+
console.log(clean ? '已排空' : '超时收口,剩余任务下次启动续跑');
|
|
354
|
+
process.exit(0);
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### `GET /healthz` → `HealthResponse`
|
|
359
|
+
|
|
360
|
+
| 字段 | 说明 |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `ok` | 恒 `true` —— 能回这个响应就说明进程活着(停机中也是 `true`,就绪与否看 `draining`) |
|
|
363
|
+
| `inFlight` | 在飞工作量 = 正在处理的同步 run(含 SSE 流)+ 已受理未完成的异步任务(queued + running);与 `drain()` 等的范围一致 |
|
|
364
|
+
| `uptimeMs` | 本 handler 创建至今的毫秒数 |
|
|
365
|
+
| `draining` | 是否已进入优雅停机 —— 负载均衡据此摘流量 |
|
|
366
|
+
|
|
367
|
+
**不鉴权**(探针带不了凭据),且停机中也照回 200。非 `GET` 回 405。
|
|
368
|
+
|
|
369
|
+
### 取消 / 重试 / 流式 / 并发闸门
|
|
370
|
+
|
|
371
|
+
| API | 说明 |
|
|
372
|
+
|---|---|
|
|
373
|
+
| `combineSignals` | 合成多个中断源(调用方 / 超时 / 断连),任一触发即中止 |
|
|
374
|
+
| `DEFAULT_RETRY` | 缺省重试参数(maxAttempts=3、指数退避 + 抖动)—— 缺省**开启** |
|
|
375
|
+
| `isAbortError` | 判定异常是否为中断(`name === 'AbortError'`) |
|
|
376
|
+
| `mapWithConcurrency` | 有界并发 map(结果保序);`maxToolConcurrency` 的底座,也可自用 |
|
|
377
|
+
|
|
378
|
+
- **取消**:`app.run(messages, { signal })` 传 `AbortSignal` —— 框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发 `signal`),run 以 `stopReason='aborted'` 收尾(算失败)。`createHttpHandler` 已内置「客户端断开即中止」;`AsyncRunner.runTimeoutMs` 到点同样是**真中止**。
|
|
379
|
+
- **重试**:缺省自动重试可重试失败(429 / 5xx / 连接失败),指数退避 + 抖动。`retry: false` 关闭,或 `retry: { maxAttempts, baseDelayMs, maxDelayMs, jitter, onRetry }` 调参。**只在本次尝试尚未产出任何文本时重试**(已吐出的字无法撤回)。⚠️ 与 SDK 内置重试叠加 —— 建议二选一调(这里 `maxAttempts: 1` 或把 SDK 的 `maxRetries` 调小)。
|
|
380
|
+
- **流式**:`POST /run` 带 `Accept: text/event-stream` → SSE 逐帧下发(`text.delta` / `run.end` / `error`);不带该头仍回一元 JSON。
|
|
381
|
+
- **工具超时 / 并发闸门**:`toolTimeoutMs` 超时**不杀 run**(该条 tool_result 记 `is_error`,模型可换路);`maxToolConcurrency` 给同回合的并行工具设上限(默认全并行)。⚠️ 超时 = **放弃等待**,`AgentTool.run` 没有 signal 参数,**副作用可能已发生** —— 想真停的工具请自行读 `ToolRunContext.signal`。
|
|
382
|
+
|
|
383
|
+
### 观测
|
|
384
|
+
|
|
385
|
+
| API | 说明 |
|
|
386
|
+
|---|---|
|
|
387
|
+
| `TraceSink` | `{ export(trace) }`,run 收尾(成功/失败)都投递,抛错被吞 |
|
|
388
|
+
| `registerDefaultTraceSink` | 注册全局默认 sink(构造期快照合并) |
|
|
389
|
+
| `TraceRecorder` | 内存 recorder(一次 run 一个) |
|
|
390
|
+
| `createOtlpExporter` | OTLP/JSON 导出,零依赖 |
|
|
391
|
+
| `metricsSink` | 指标累加器(Prometheus 文本 / OTLP metrics),满足 `TraceSink` 即接入 —— 见 §6「指标」 |
|
|
392
|
+
| `buildRunReport` | 从一条 trace 生成**调优报告**(能力/模型的耗时、token、成本、错误率排行)—— 见 §6「调优报告」 |
|
|
393
|
+
|
|
394
|
+
> **生产落地**(按 runId 落库检索 / 日志关联 / 采样 / 脱敏)见 `docs/observability.md` ——
|
|
395
|
+
> 框架只保证 trace 出口,这些都在缝外用 sink 组合;四条现成 sink 的实码在
|
|
396
|
+
> `examples/observability/`。**完整的示例**(四类能力 + 三种触发 + 鉴权 + 全观测栈)在 `examples/complete/`;
|
|
397
|
+
> 最小可交付示例(Dockerfile + compose)在 `examples/deploy/`。
|
|
398
|
+
|
|
399
|
+
### 长上下文
|
|
400
|
+
|
|
401
|
+
| API | 说明 |
|
|
402
|
+
|---|---|
|
|
403
|
+
| `createBudgetPolicy` | 预算策略:超预算先 `trimToolPairs` 编辑,再 `compactMessages` 压缩(带滞回) |
|
|
404
|
+
| `trimToolPairs` | context editing:丢旧 tool 对(按**对数**,`keepToolPairs`) |
|
|
405
|
+
| `compactMessages` | compaction:旧前缀做摘要(摘要器由你注入,框架不替你造 token) |
|
|
406
|
+
| `estimateMessages` | 估算 token(预算决策用,不是精确记账) |
|
|
407
|
+
|
|
408
|
+
### 成本硬管控(**别与上面的上下文预算混为一谈**)
|
|
409
|
+
|
|
410
|
+
| API | 说明 |
|
|
411
|
+
|---|---|
|
|
412
|
+
| `createBudgetGuard` | 执行 `check(trace)` → `'tokens' \| 'cost' \| null` 的护栏(也可只用来自己记账) |
|
|
413
|
+
|
|
414
|
+
两者是**互补的两件事**,取舍点完全不同:
|
|
415
|
+
|
|
416
|
+
| | `createBudgetPolicy`(§长上下文) | `BudgetGuard`(本节) |
|
|
417
|
+
|---|---|---|
|
|
418
|
+
| 时机 | **发送前** | **记账后** |
|
|
419
|
+
| 干什么 | 改 messages(丢旧 tool 对 / 压缩旧前缀) | 改 run 结局(超限即停) |
|
|
420
|
+
| 为解决 | 历史太长把请求撞 400 / 过早压缩 | **控制花钱** |
|
|
421
|
+
| 怎么开 | `contextPolicy` | `maxTotalTokens` / `maxCostUsd` |
|
|
422
|
+
|
|
423
|
+
- 超限后 run 以 `stopReason='budget_exceeded'` 收尾(**算失败**),run 根记一条 `budget.exceeded` 事件(带 `{ kind, limit, actual, totalTokens, costUsd }`)。
|
|
424
|
+
- **不是硬实时**:一回合跑完才判,实际用量可能超上限一个回合的量。
|
|
425
|
+
- **模型自然收尾的那一回合超限不改判失败**(只留事件)—— 那次 run 的任务其实做完了,不该追认成失败。
|
|
426
|
+
- 子 agent 的 token **计入**总账(口径 = 整个 trace 的 `totalUsage`)。
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
const { result } = await app.run(messages, { maxTotalTokens: 200_000 });
|
|
430
|
+
if (result.stopReason === 'budget_exceeded') console.warn('这次 run 被预算拦下了', result.error);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
选项字段(`TrimOptions` / `CompactOptions` / `BudgetPolicyOptions`):
|
|
434
|
+
|
|
435
|
+
### `TrimOptions`(`trimToolPairs` 的选项)
|
|
436
|
+
|
|
437
|
+
| 字段 | 说明 |
|
|
438
|
+
|---|---|
|
|
439
|
+
| `keepToolPairs` | 保留的最近工具**对数**(tool_use→tool_result);缺省 1 |
|
|
440
|
+
|
|
441
|
+
### `CompactOptions`(`compactMessages` 的选项)
|
|
442
|
+
|
|
443
|
+
| 字段 | 说明 |
|
|
444
|
+
|---|---|
|
|
445
|
+
| `keepRecent` | 保留的最近消息**条数**;缺省 20 |
|
|
446
|
+
| `summarize` | 摘要器:输入被弃旧前缀的渲染文本,返回摘要(框架不替你造 token) |
|
|
447
|
+
|
|
448
|
+
### `BudgetPolicyOptions`(`createBudgetPolicy` 的选项)
|
|
449
|
+
|
|
450
|
+
| 字段 | 说明 |
|
|
451
|
+
|---|---|
|
|
452
|
+
| `budgetTokens` | 预算(估算 input tokens);缺省 60000 |
|
|
453
|
+
| `keepRecent` | compaction 保留的最近消息**条数**;缺省 20 |
|
|
454
|
+
| `keepToolPairs` | context editing 保留的最近工具**对数**;缺省 1 |
|
|
455
|
+
| `estimateTokens` | token 估算函数(预算决策用,非精确记账) |
|
|
456
|
+
| `editBeforeCompact` | 超预算时是否先编辑再压缩;缺省 true |
|
|
457
|
+
| `summarize` | 提供则允许 compaction(旧前缀→摘要) |
|
|
458
|
+
| `compactEvery` | 距上次压缩至少隔几个回合(滞回);缺省 1 |
|
|
459
|
+
|
|
460
|
+
> `keepRecent`(**消息条数**,compaction 用)与 `keepToolPairs`(**工具对数**,context editing 用)是两种单位,刻意分开命名 —— 别拿同一个值套过去。
|
|
461
|
+
|
|
462
|
+
### 多模型 / 记忆 / 重放
|
|
463
|
+
|
|
464
|
+
| API | 说明 |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `createAnthropicClient` | 默认 ModelClient(Anthropic):自定义只传 `apiKey` / `baseURL`,不必直接依赖厂商 SDK |
|
|
467
|
+
| `createOpenAIClient` | OpenAI 兼容端点适配(DeepSeek 等;**真流式**、图片块转 `image_url`、cache token 恒 0) |
|
|
468
|
+
| `InMemoryMemoryStore` | 跨 run 的**键值黑板**记忆(`{ store, keys }` 配 `executeRun`) |
|
|
469
|
+
| `InMemorySessionStore` | 跨 run 的**对话历史**(`{ store, id }` 配 `executeRun` / `app.run`);与前者正交,可同时用 |
|
|
470
|
+
| `traceToMessages` | 把 trace 还原成 messages(重放基底) |
|
|
471
|
+
| `applyMiddleware` | 手动包裹配置菜单(装配层已自动做) |
|
|
472
|
+
|
|
473
|
+
### MCP 桥(MCP 是「工具来源」,不是新机制)
|
|
474
|
+
|
|
475
|
+
| API | 说明 |
|
|
476
|
+
|---|---|
|
|
477
|
+
| `mcpTools` | 把 MCP server 的 `tools/list` 映射成框架 `AgentTool[]`(进 `createApp({ tools })`) |
|
|
478
|
+
| `McpClientLike` | 最小结构面:`listTools()` + `callTool(name, args)`;框架**不 import** MCP SDK |
|
|
479
|
+
| `MCP_DEFAULT_TIMEOUT_MS` | 桥的缺省单次调用超时(60000 ms) |
|
|
480
|
+
|
|
481
|
+
- **名字**:`prefix + 归一化原名`(MCP 名里的 `-` / `.` / 空格 → `_`)。归一化后**空名 / 撞名 / 超 64 字符**一律**装配期抛错**(静默改名会得到一个调不回去的名字,比启动期报错难查得多)。
|
|
482
|
+
- **原名**:每次调用写进发起 turn 的 `mcp.tool` attribute —— 审计 / 回放要还原它才能回调 server。
|
|
483
|
+
- **入参 schema**:MCP 的 `inputSchema` 已是 JSON Schema → 原样透传,由 engine 的子集校验器在 `callTool` **之前**校验(非法入参根本不会发给 server,模型自己会改)。
|
|
484
|
+
- **失败**:`callTool` 抛错 → 该条 `tool_result` 记 `is_error`,**不杀 run**(与本地工具抛错同语义)。⚠️ **协议层的 `isError: true` 框架看不见** —— 连接器必须转成抛错,否则模型以为成功了。
|
|
485
|
+
- **连接器不在框架里**(守「零运行时依赖」):stdio / StreamableHTTP 归独立可选包,或你自己接 SDK 后实现 `McpClientLike`。本仓库 `scripts/e2e-mcp.ts` 有一份最小连接器可参考。
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
// 任意实现了 listTools/callTool 的对象都能接(duck-typed,无需继承)
|
|
489
|
+
const client: McpClientLike = myStdioConnector;
|
|
490
|
+
const tools = await mcpTools(client, { server: 'time' }); // → mcp_time_get_current_time …
|
|
491
|
+
|
|
492
|
+
// 与本地 @Tool 同池:同过中间件链、同进重名查重
|
|
493
|
+
const app = createApp({ system, providers: [...], tools });
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### `McpToolsOptions`(`mcpTools` 的选项)
|
|
497
|
+
|
|
498
|
+
| 字段 | 说明 |
|
|
499
|
+
|---|---|
|
|
500
|
+
| `prefix` | 工具名前缀;缺省 `mcp_<server>_`(没给 `server` 时 `mcp_`);`''` = 不加前缀(撞名自负) |
|
|
501
|
+
| `server` | server 标识,只用于拼缺省前缀(不会发给 server) |
|
|
502
|
+
| `timeoutMs` | 单次 `callTool` 超时(毫秒);缺省 60000,非正数 = 不限 |
|
|
503
|
+
|
|
504
|
+
### evals(把 mockClient 提升为一等能力)
|
|
505
|
+
|
|
506
|
+
| API | 说明 |
|
|
507
|
+
|---|---|
|
|
508
|
+
| `scriptedClient` | 按脚本依次返回模型响应(**真把文本块经 `on('text')` 吐出去**);脚本耗时报错 |
|
|
509
|
+
| `defineEval` | 定义「用例 + 断言」,`run()` 返回 `EvalReport` |
|
|
510
|
+
|
|
511
|
+
- **为什么需要**:单测覆盖的是框架语义,evals 覆盖的是**你的 agent 语义** —— 改 prompt / 换模型 / 加工具之后有没有回归,靠断言而不是人眼。
|
|
512
|
+
- 断言源是既有 `Trace`:「先 `search` 才 `summarize`」这类顺序断言全从 trace 读,框架不为此新增埋点。
|
|
513
|
+
- `run()` **不抛**(用例失败进报告,一次跑完能看到所有回归,而不是修一个跑一次);只有「应用建不起来」才冒泡 —— 那是环境错误,不是回归。失败 case 带 `trace`,直接看现场。
|
|
514
|
+
- `scriptedClient` 的步骤**在 `finalMessage()` 成功返回后才前进**:抛错的步骤(函数步骤 `throw` 模拟 429)会在重试时**重放同一步**,想验重试就这么写。
|
|
515
|
+
|
|
516
|
+
```ts
|
|
517
|
+
const ev = defineEval<{ summary: string }>({
|
|
518
|
+
name: 'doc-review',
|
|
519
|
+
app: () => createApp({ system: new SystemPrompt({ version: 'v3' }).add('role', R), providers: [...] }),
|
|
520
|
+
// 每 case 可带 opts(透传 app.run):注入 resultSchema 就能断言 result.typed
|
|
521
|
+
cases: [{ name: '先检索再总结', input: '总结这份文档', client: scriptedClient([searchMsg, submitMsg]) }],
|
|
522
|
+
expect: (r, { trace }) => {
|
|
523
|
+
assert.equal(r.stopReason, 'end_turn');
|
|
524
|
+
const order = trace.spans.flatMap((s) => s.events)
|
|
525
|
+
.filter((e) => e.name === 'tool.input')
|
|
526
|
+
.map((e) => (e.body as { tool: string }).tool);
|
|
527
|
+
assert.deepEqual(order, ['search', 'summarize']); // 顺序断言从 trace 读
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
const report = await ev.run();
|
|
531
|
+
if (!report.ok) console.error(report.cases.filter((c) => !c.ok));
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### 指标(从 trace 派生)
|
|
535
|
+
|
|
536
|
+
| API | 说明 |
|
|
537
|
+
|---|---|
|
|
538
|
+
| `metricsSink` | 进程内累加 + Prometheus 文本 / OTLP metrics;**天然满足 `TraceSink`** → `createApp({ sinks: [metricsSink()] })` 即接入,零新出口 |
|
|
539
|
+
| `DEFAULT_BUCKETS` | 时长直方图的缺省桶边界(毫秒),可用 `buckets` 覆盖 |
|
|
540
|
+
|
|
541
|
+
三个维度,全部从既有 trace 派生,**不需要在业务代码里埋点**:
|
|
542
|
+
|
|
543
|
+
- **run 级** —— 总数 / 失败数 /四类 token / 成本 / 时长;
|
|
544
|
+
- **能力级** —— 每个 `tool` / `skill` / `subagent` 的**调用次数、失败次数、耗时、token、成本**。
|
|
545
|
+
工具的数据来自 turn 上的 `tool.output` 事件(框架已补 `durationMs` / `ok`);`skill`/`subagent`
|
|
546
|
+
来自 `capability` span。**`@Prompt` 不建 span、无独立耗时,因此不产出能力指标**(如实缺省,不硬凑)。
|
|
547
|
+
- **模型级** —— 按模型(`llm.turn` 的 span name)归因 turn 数 / token / 成本 / 耗时,并单独给出
|
|
548
|
+
`model_unpriced_turns_total`(算不出成本的 turn 数 —— **成本护栏失效的显式信号**)。
|
|
549
|
+
|
|
550
|
+
接完 `GET /metrics` 直接回 `render()` 即可 —— 交给 `createHttpHandler({ metrics })` 就是一行的事
|
|
551
|
+
(见 §6 HTTP 端点速查)。
|
|
552
|
+
|
|
553
|
+
时长同时给两种口径,**并存不冲突**:
|
|
554
|
+
|
|
555
|
+
- **histogram**(`*_bucket` / `*_sum` / `*_count`,累积语义)—— 抓取端可**跨实例任意聚合**;
|
|
556
|
+
- **窗口内精确分位**(`*{quantile="..."}` gauge)—— 单实例排障时更好读。
|
|
557
|
+
|
|
558
|
+
### `MetricsSinkOptions`(`metricsSink` 的选项)
|
|
559
|
+
|
|
560
|
+
| 字段 | 说明 |
|
|
561
|
+
|---|---|
|
|
562
|
+
| `export` | 输出形态;缺省 `'prometheus'`。`'otlp'` 走 OTLP/JSON 导出(**必须同时给 `endpoint`**,不给就构造期抛错) |
|
|
563
|
+
| `endpoint` | OTLP 采集端基地址(如 `http://localhost:4318`);尾部斜杠会被去掉 |
|
|
564
|
+
| `intervalMs` | OTLP 导出间隔(毫秒,缺省 60000);`0` = 每次 run 收尾立即导出。定时器已 `unref()`,不阻止进程退出 |
|
|
565
|
+
| `resourceAttributes` / `serviceName` | OTLP resource 属性(`service.name` 缺省 `agentia`) |
|
|
566
|
+
| `onExportError` | 导出失败回调(缺省吞掉 —— 观测失败不得击穿业务) |
|
|
567
|
+
| `windowSize` | 时长分位保留的样本数(环形窗口,缺省 1024,**run / 能力 / 模型各自独立**);非正数抛错 |
|
|
568
|
+
| `prefix` | 指标名前缀,缺省 `agentia_` |
|
|
569
|
+
| `labelMode` | 能力标签粒度:`'capability'`(缺省,`tool:search` 这种)/ `'kind'`(只按类型,基数极小)/ `'none'`(不产出能力指标) |
|
|
570
|
+
| `maxCapabilities` | 能力标签基数上限(缺省 200):超出后新能力归入 `capability="__other__"`(防标签爆炸);非正数抛错 |
|
|
571
|
+
| `buckets` | 直方图桶边界(毫秒,严格升序);缺省 `DEFAULT_BUCKETS` |
|
|
572
|
+
|
|
573
|
+
### `MetricsSink`(`metricsSink()` 的返回值)
|
|
574
|
+
|
|
575
|
+
| 成员 | 说明 |
|
|
576
|
+
|---|---|
|
|
577
|
+
| `export` | `TraceSink` 的实现(run 收尾投递)—— 也是接进 `sinks` 的形状 |
|
|
578
|
+
| `snapshot` | `{ runs, failed, latencyP50, latencyP95, tokens, costUsd, capabilities, models, droppedCapabilities }` |
|
|
579
|
+
| `render` | Prometheus 文本(`/metrics` 直接回它) |
|
|
580
|
+
| `flush` | 主动导出一次(`export:'otlp'` 时有意义;prometheus 模式为空操作) |
|
|
581
|
+
| `stop` | 停掉定时导出(进程收尾 / 测试用) |
|
|
582
|
+
| `reset` | 清空累计(含能力与模型维度) |
|
|
583
|
+
|
|
584
|
+
- `tokens` 口径 = **四类之和**(input + output + cacheRead + cacheCreation),与 `BudgetGuard` 一致;分项在 `render()` 里以 label 给出,不会丢。
|
|
585
|
+
- 分位是**窗口内精确值**(最近 rank 法),只反映最近 `windowSize` 条样本;**直方图计数是累积的**(全历史),两者语义不同、各有各的用处。
|
|
586
|
+
- **内存上限** ≈ `(1 + 能力数 + 模型数) × windowSize` —— 能力数由 `maxCapabilities` 封顶,长跑宿主不会被拖住。
|
|
587
|
+
- `costUsd` 依赖模型在价格表内(不在表里时不计、并计入 `unpricedTurns` 与 `usage.unpriced` 事件);根 span 未收尾(如失败路径的半截 trace)的 run 不进延迟样本。
|
|
588
|
+
|
|
589
|
+
### 调优报告(**哪个能力慢 / 贵 / 爱失败**)
|
|
590
|
+
|
|
591
|
+
指标回答「整体怎么样」,报告回答「**该拧哪个旋钮**」:
|
|
592
|
+
|
|
593
|
+
| API | 说明 |
|
|
594
|
+
|---|---|
|
|
595
|
+
| `buildRunReport(trace)` | 一条 trace → `RunReport`:能力排行(按总耗时降序)、模型归因、未定价模型清单 |
|
|
596
|
+
| `mergeRunReports(reports)` | 跨 run 汇总(**分位只在多条 run 上才有统计意义**) |
|
|
597
|
+
| `renderRunReport(report)` | 人类可读的纯文本表(CLI / 日志用) |
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
import { buildRunReport, mergeRunReports, renderRunReport } from '@migor/agentia';
|
|
601
|
+
|
|
602
|
+
const report = buildRunReport(trace);
|
|
603
|
+
console.log(renderRunReport(report));
|
|
604
|
+
// capability calls err total max tokens cost
|
|
605
|
+
// subagent:researcher 1 0 400ms 400ms 60 0.004000
|
|
606
|
+
// tool:search 2 1 325ms 300ms - -
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
CLI 侧有薄壳:`agentia report <trace.jsonl>` —— 每行一个 JSON(裸 Trace,或含 `result.trace` /
|
|
610
|
+
`trace` 的 TaskRecord,如 `FileTaskStore` 的导出),跨行按能力合并后打印排行。
|
|
611
|
+
|
|
612
|
+
> ⚠️ **单条 run 内样本常 < 5,分位没有意义** —— 所以报告以 `total` / `max` 为主;
|
|
613
|
+
> 要看分位请用 `mergeRunReports` 汇总多条,或用 `metricsSink` 的直方图。
|
|
614
|
+
> CLI 报告的聚合口径与 `agentia dev` 面板的能力排行同源(同一份 `@migor/trace-view` 实现)。
|
|
615
|
+
|
|
616
|
+
### 生效配置快照(「这条 run 用了哪套旋钮」)
|
|
617
|
+
|
|
618
|
+
每个 run 的**根 span** 都带一组 `config.*` attributes(`config.maxTokens` / `config.maxCostUsd` /
|
|
619
|
+
`config.retry.maxAttempts` / `config.contextPolicy.budgetTokens` / `config.priceOverrides` …),
|
|
620
|
+
缺省值也记 —— 这样「没配」「配了缺省值」「配了别的值」三者可区分。换参数前后对比、复现线上行为都有据可查。
|
|
621
|
+
函数型选项(`summarize` / `confirm` 之类)只记「配没配」,不记函数体。
|
|
622
|
+
|
|
623
|
+
### 提示词版本化
|
|
624
|
+
|
|
625
|
+
- `new SystemPrompt({ version: 'git-abc123' })` → 自动写到 **run 根 span 的 `system.version` attribute**:trace 里能查出「这个结果是哪个版本的提示词产出的」(换 prompt 前后对比、排查回归都靠它)。
|
|
626
|
+
- 版本号怎么来(git sha / 语义版本 / 手工)由你决定 —— 框架**不做**版本库与回滚平台。
|
|
627
|
+
- 单次 `app.run(..., { system })` 覆盖时,版本**跟当次那个 `SystemPrompt` 走**;`system` 传已拼好的 `SystemParam` 则无版本可记(不写空串冒充实有版本)。
|
|
628
|
+
- 直连 `runAgent` / `executeRun` 时可用引擎级选项 `systemVersion` 显式给。
|
|
629
|
+
|
|
630
|
+
### 多租户配额(组合既有缝,不是子系统)
|
|
631
|
+
|
|
632
|
+
框架不提供配额组件 —— 用 `middleware`(拦在能力调用前)+ `TraceSink`(收尾后记账)+ `BudgetGuard`(单次 run 上限)组合即可,存储与策略是你的事:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
declare module '@migor/agentia' { interface Blackboard { tenant: string } }
|
|
636
|
+
|
|
637
|
+
const spentTokens = new Map<string, number>(); // 真实场景换成 Redis / DB
|
|
638
|
+
const TENANT_LIMIT = 200_000;
|
|
639
|
+
|
|
640
|
+
const quota: CapabilityMiddleware = async (call, next) => {
|
|
641
|
+
const tenant = RunContext.current()?.get('tenant');
|
|
642
|
+
if (tenant && (spentTokens.get(tenant) ?? 0) >= TENANT_LIMIT) {
|
|
643
|
+
throw new Error(`租户 ${tenant} 的额度已用满`); // → 该条 tool_result 记 is_error,不杀 run
|
|
644
|
+
}
|
|
645
|
+
return next(); // 额度内放行
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const billing: TraceSink = {
|
|
649
|
+
export(trace) {
|
|
650
|
+
const tenant = RunContext.current()?.get('tenant'); // sink 在 run 的 async 上下文里投递,读得到黑板
|
|
651
|
+
if (!tenant) return;
|
|
652
|
+
const u = trace.totalUsage;
|
|
653
|
+
spentTokens.set(tenant, (spentTokens.get(tenant) ?? 0) + u.inputTokens + u.outputTokens);
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
createApp({ system, providers: [...], middleware: [quota], sinks: [billing] });
|
|
658
|
+
// 单次 run 再有上限就叠加 C1:app.run(msgs, { maxTotalTokens: 50_000 })
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
- 拦下来的那次 run **仍然要记账**(模型的钱已经花了)—— 记账在 sink 里、拦截在 middleware 里,两者独立。
|
|
662
|
+
- 被拦下的能力**不会执行**(副作用不发生),但 run 继续跑(模型可以换路)。
|
|
663
|
+
|
|
664
|
+
### 人工介入:审批闸门(缺口只在「跨进程挂起」,闸门现成)
|
|
665
|
+
|
|
666
|
+
框架不做审批子系统,但**闸门**这一层已经具备 —— `middleware` 可以 `await` 决策再放行,
|
|
667
|
+
引擎会等工具结果(`Promise.resolve(tool.run(...))`):
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
const DANGEROUS = new Set(['send_email', 'deploy', 'delete_records']);
|
|
671
|
+
|
|
672
|
+
const requireApproval: CapabilityMiddleware = async (call, next) => {
|
|
673
|
+
if (!DANGEROUS.has(call.capability.name)) return next();
|
|
674
|
+
const ok = await askHuman(call.capability.name, call.input); // 在这里 await —— run 就地停着等人
|
|
675
|
+
if (!ok) throw new Error(`调用 ${call.capability.name} 未获批准`); // → tool_result 记 is_error,run 不中断
|
|
676
|
+
return next();
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
createApp({ system, providers: [...], middleware: [requireApproval] });
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
三种结局都有对应写法:
|
|
683
|
+
|
|
684
|
+
- **放行**:`next()`;**拒绝**(副作用不发生):不调 `next()` —— 短路;**拒绝并让模型改道**:抛错 → 该条
|
|
685
|
+
`tool_result` 记 `is_error`,模型换路,run 不中断。
|
|
686
|
+
- 决策依据随你:`call.capability.name` / `call.input`,或在中间件里 `RunContext.current()?.get('…')` 读黑板
|
|
687
|
+
(ALS 传播,见上一节)。
|
|
688
|
+
- 等待不会被默认掐断(`toolTimeoutMs` 缺省 0 = 不限);真设了它,注意别把人的思考时间算进去。
|
|
689
|
+
|
|
690
|
+
**框架不做的**:把 run 停成「待批准」态、进程重启后从断点续跑。`RunStatus` 没有这个状态、循环位置不落库,
|
|
691
|
+
且 `traceToMessages` 重放**有损**(assistant 原文未记录)—— 拿它假装续跑只会拿到降级的上下文。
|
|
692
|
+
要跨重启审批,就自己上工作流引擎(见 §7)。
|
|
693
|
+
|
|
694
|
+
### 内容护栏(三处缝,不是子系统)
|
|
695
|
+
|
|
696
|
+
和「多租户配额」同一个形状:策略千差万别(正则 / 分类模型 / 外部审核 API),框架硬编码必错,**只给缝**。
|
|
697
|
+
|
|
698
|
+
| 关卡 | 缝 |
|
|
699
|
+
|---|---|
|
|
700
|
+
| 入参(run 之前)| 包一层 `app.run`;HTTP 侧用 `createHttpHandler({ authenticate })`,拦在**读 body 之前** |
|
|
701
|
+
| 工具调用前 | `middleware`(见上;能短路,也能用 `next(newInput)` 改写入参)|
|
|
702
|
+
| 出参 / 结果 | 包返回值,或挂 `sinks: [...]`(`TraceSink`)在收尾处审 |
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
const callable = {
|
|
706
|
+
name: app.name,
|
|
707
|
+
async run(msgs, opts) {
|
|
708
|
+
const out = await app.run(redactInput(msgs), opts); // 入参护栏
|
|
709
|
+
if (flagged(out.finalText)) throw new Error('输出被护栏拦下'); // 出参护栏
|
|
710
|
+
return out;
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### 代码执行隔离(沙箱是工具的事,不是框架的)
|
|
716
|
+
|
|
717
|
+
**框架从不执行模型生成的代码** —— `@Skill` 跑的是你写的方法体、`@Tool` 是你写的函数,
|
|
718
|
+
模型输出只会变成文本 / `tool_result`。所以「要不要沙箱」等价于「你那个*代码执行工具*要不要隔离」:
|
|
719
|
+
在**工具实现内部**做(Docker / 子进程 / 微 VM 随你),框架不参与也不该参与 ——
|
|
720
|
+
`AgentTool.run(input) → output` 这个契约把隔离整个挡在实现里。
|
|
721
|
+
|
|
722
|
+
唯一沾边的一条:工具起了子进程,**取消时要自己 kill**。框架的 `toolTimeoutMs` 是「不等了」不是取消
|
|
723
|
+
(`AgentTool.run` 收不到 `signal`);要能真停,让工具自己读 `ToolRunContext.signal`。
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## 7. 已知边界(如实标注,不要指望框架替你兜)
|
|
728
|
+
|
|
729
|
+
| 边界 | 说明 |
|
|
730
|
+
|---|---|
|
|
731
|
+
| 方法入参要自己标注 | TS 不会从 JSON Schema 反向推断方法形参;`strict` 下不标注会报隐式 any |
|
|
732
|
+
| 裸 schema 不校验签名 | 只给 `schema: {...}` 时,schema 与方法签名**互不关联**(要护栏就用 `fromZod<T>`) |
|
|
733
|
+
| 黑板键默认无类型 | 不合并 `Blackboard` 就是裸 `string` + `unknown`;动态键需 `as BlackboardKey` |
|
|
734
|
+
| 没有「能力清单」类型 | 能力是运行时从装饰器注册表收集的,所以 `app.my_tool()` 这种写法不存在 |
|
|
735
|
+
| `strict` 只是透传 | 框架**不校验** schema 的合规性(是否 `additionalProperties:false` 等) |
|
|
736
|
+
| schema 校验是**子集** | 只覆盖 `type/properties/required/additionalProperties/enum/items`;`format`/`minimum`/`oneOf` 一律放行 |
|
|
737
|
+
| 历史畸形就放弃裁剪 | `trimToolPairs` 遇到非严格交替历史会整体放弃(宁可少裁,也不切出孤立 tool_use 让请求 400) |
|
|
738
|
+
| 缺省内存 store 不淘汰 | 长跑宿主请设 `InMemoryTaskStore({ maxRecords })` 或换 `FileTaskStore` / `SqliteTaskStore` |
|
|
739
|
+
| 能力引用是 provider 粒度 | 子 agent / skill 的 `tools` 写的是 **provider token**,不是单个工具名 |
|
|
740
|
+
| 取消要传进客户端才有效 | 传 `signal` 后框架会 abort 在飞请求(内置 Anthropic / OpenAI 适配器都转发);不转发 `signal` 的自定义 `ModelClient` 只能「放弃等待」(请求在后台跑完、产物丢弃) |
|
|
741
|
+
| 观测失败被吞 | sink 抛错不影响 run(观测是辅助动作);同理记忆水合/回写失败也不击穿 run |
|
|
742
|
+
| 框架不读 env | 除 `AGENTIA_MODEL`(缺省模型覆盖)与 `OPENAI_API_KEY`(OpenAI 适配器)外不读环境变量;不含 dev 逻辑 |
|
|
743
|
+
| 鉴权只是缝 | 框架**不实现** token / JWT / 签名策略,也不碰凭据 env —— `authenticate` 只承诺「拦在入口、读 body 之前」;策略是宿主或反代的事 |
|
|
744
|
+
| 运行时是 Node | 按 Node ≥ 18 设计与测试(`engines` 写明,CI 在 18/20/22 上守);**未对 Deno / edge 做验证**。`SqliteTaskStore` 需 Node ≥ 22.5(`node:sqlite`),未提供时构造期抛可读报错 |
|
|
745
|
+
| 停机不由框架触发 | 框架给 `drain()` 但**不订阅** `SIGTERM`/`SIGINT`(不读 env、不做进程级决策);信号处理是宿主的 |
|
|
746
|
+
| 停机可能切断 SSE | `drain()` 超时后会强制关闭仍开着的 SSE 流,其 run 以 `stopReason='aborted'` 收尾 —— 客户端应把断流当作可重试 |
|
|
747
|
+
| 鉴权失败即断连 | 未通过鉴权时在读到 body 之前就回响应,连接**不可复用**(显式 `connection: close`);这是「不收body省资源」的代价 |
|
|
748
|
+
| 预算护栏不是硬实时 | 一回合记账完才判,实际用量可能超上限一个回合的量;模型自然收尾的那回合超限**不算失败**(只留 `budget.exceeded` 事件) |
|
|
749
|
+
| `maxCostUsd` 依赖价格表 | 模型不在价格表内(且未用 `priceOverrides` 覆盖)时成本恒为 0,这条护栏**不触发** —— 要无条件兜底用 `maxTotalTokens`。**但失效不再静默**:turn 上会记 `usage.unpriced` 事件、指标有 `model_unpriced_turns_total`、可回调 `onUnpricedModel` |
|
|
750
|
+
| 工具超时**不取消**工具 | `AgentTool.run` 没有 signal 参数,超时只是「不等了」;副作用可能已发生。想真停请让工具自己读 `ToolRunContext.signal` |
|
|
751
|
+
| 会话只存对话轮次 | `SessionStore` 存「用户输入 + 最终回复」,run 内部的 tool 往返**不进历史**(要完整过程用 `traceToMessages`);且只有**跑成功**的轮次才回写 |
|
|
752
|
+
| OpenAI 适配器听端点的话 | 请求发 `stream:true`,但**按响应形态解析**:端点回 JSON 就退回一次性(没有打字机效果),回 `event-stream` 才逐 token |
|
|
753
|
+
| MCP 只做 tools | `sampling`(server 反向请求模型)/ `resources` / `prompts` 原语不做;连接器(stdio / HTTP)不在框架内 |
|
|
754
|
+
| MCP 的协议层错误框架看不见 | `isError: true` 只有连接器能看见 —— 它必须转成抛错,否则模型收到的是一条「成功」的结果 |
|
|
755
|
+
| MCP 超时同样是「不等了」 | 桥自带的 `timeoutMs` 取消不了 server 侧执行(拿不到取消句柄);它与 engine 的 `toolTimeoutMs` **双重计时**,谁短谁生效 |
|
|
756
|
+
| MCP 名字可能被归一化 | 原名含 `-` / `.` / 空格 → 进菜单时变成 `_`;回调 server 用的仍是原名(`mcp.tool` attribute 里查得到) |
|
|
757
|
+
| MCP 工具不能进 DI 容器 | 它没有 provider token,也不能被别的能力的 `tools` 引用 —— 引用是 provider 粒度 |
|
|
758
|
+
| 指标分位是窗口内精确值 | `*{quantile=...}` 只反映最近 `windowSize`(缺省 1024)条样本;要跨实例聚合请用直方图(`*_bucket` / `_sum` / `_count`,累积语义) |
|
|
759
|
+
| 指标是**进程内**累加 | 不做分布式聚合与持久化:多实例各算各的(直方图可相加),重启即清零。要长期保留请把 `render()` 抓走或用 `export:'otlp'` 推给采集端 |
|
|
760
|
+
| OTLP metrics 只推当前累计 | 按 `intervalMs` 周期导出**累积值**(CUMULATIVE),不做增量/背压;导出失败按 `onExportError` 处理(缺省吞掉,不重试、不阻塞 run) |
|
|
761
|
+
| `GET /metrics` 不鉴权 | 与 `/healthz` 同档(拉取端在集群内网)。要保护请放反代之后,或不传 `metrics` 选项自行在外层挂路由 |
|
|
762
|
+
| 工具没有 token/成本指标 | 工具是**你的代码**、本身不消耗 token,所以只产出调用数/失败数/耗时;token 与成本只对 `skill`/`subagent`(有 `capability` span)与模型维度产出 |
|
|
763
|
+
| `@Prompt` 没有能力指标 | 资产类能力不建 span、无独立耗时,故不出现在能力排行里(这是刻意的:硬凑一个假耗时会误导调优) |
|
|
764
|
+
| 能力标签有基数上限 | `labelMode:'capability'`(缺省)+ `maxCapabilities`(缺省 200),超出的能力归入 `capability="__other__"`;`snapshot().droppedCapabilities` 给出被归并的能力个数。要完整明细请用 `buildRunReport`(不设上限) |
|
|
765
|
+
| 提示词版本只是标记 | 框架不存版本库、不回滚:`version` 只落 run 根 attribute;`system` 传已拼好的 `SystemParam` 时无版本可记 |
|
|
766
|
+
| 配额不是框架子系统 | 只给缝(middleware + TraceSink + BudgetGuard),计数放哪(内存 / Redis / DB)与超限怎么办都是你的策略 |
|
|
767
|
+
| 人工介入只到「闸门」 | `middleware` 能 `await` 审批决策再放行;**跨进程挂起/续跑框架不做** —— `RunStatus` 无「待批准」态、循环位置不落库,`traceToMessages` 重放有损,不能拿它假装续跑(要跨重启审批请上工作流引擎)|
|
|
768
|
+
| 内容护栏不给实现 | 同「配额」:只给缝(入参包 `app.run` / 工具前 `middleware` / 出参包返回值或 `sinks`),策略(正则 / 分类器 / 外部 API)是你的 |
|
|
769
|
+
| 框架不执行模型生成的代码 | 无沙箱可言:`@Skill` 跑你写的方法、`@Tool` 是你写的函数,模型输出只成文本 / `tool_result`;代码执行工具的隔离是**工具实现内部**的事;工具起的子进程取消时要自己 kill |
|
|
770
|
+
|
|
771
|
+
---
|
|
772
|
+
|
|
773
|
+
## 8. 常见错误
|
|
774
|
+
|
|
775
|
+
| 症状 | 原因与修法 |
|
|
776
|
+
|---|---|
|
|
777
|
+
| 菜单里没有我的能力 | ① 方法没写装饰器;② 类没注册进 `providers`;③ 框架双实例(见第 2 节陷阱);④ `toolSources` 白名单把它排除了 |
|
|
778
|
+
| `菜单能力重名` 装配期抛错 | 四类能力**共用命名空间**,改名即可 |
|
|
779
|
+
| 编译错 `不能把 X 赋给 Y` | 用了 `fromZod<T>`,方法签名与 `T` 不一致(这是护栏,不是 bug) |
|
|
780
|
+
| 模型传的入参没被拦 | 只填了 `schema` 没写 `strict`;且框架的校验是**子集校验**(`format`/`minimum` 等不校验) |
|
|
781
|
+
| `ctx.get('k')` 没有类型 | 没做 `Blackboard` 声明合并(见 5.1) |
|
|
782
|
+
| `result.typed` 是 `unknown` | `resultSchema` 用的是裸 JsonSchema;改 `fromZod<T>`(见 5.3) |
|
|
783
|
+
| TS 里想 `app.my_tool(...)` | 不要这样写:能力由模型选择,不是你的方法。要确定性调用就**直接调类方法** |
|
|
784
|
+
| 子 agent 调不到工具 | `tools` 是 **provider token**(文件夹名)列表,不是工具名 |
|
|
785
|
+
| 长跑内存涨 | 缺省内存 store 不淘汰;设 `InMemoryTaskStore({ maxRecords })` 或换耐久 store |
|
|
786
|
+
| 鉴权钩子抛错,客户端只看到「未通过鉴权」 | 这是设计:非 `HttpException` 的错误原文只进服务端日志(要回给调用方就抛 `HttpException(status, body)`) |
|
|
787
|
+
| 停机后 `POST /tasks` 回 503 | `drain()` 已被调用(或注入的 runner 已 drain)—— 这是「拒新单」的正常行为,任务没丢 |
|
|
788
|
+
| `stopReason` 是 `budget_exceeded`、任务被判失败 | 这是设计(护栏拦下的 run **没跑完**)。只想「记一笔」不想改结局,就自己用 `createBudgetGuard` 读 trace |
|
|
789
|
+
| 工具超时了,副作用却还是发生了 | 超时是「放弃等待」不是取消(`AgentTool.run` 收不到 signal)。要能真停就得让工具自己读 `ToolRunContext.signal` |
|
|
790
|
+
| 用 OpenAI 端点没看到打字机效果 | 端点没按 `stream:true` 回 `event-stream`(回了一整份 JSON)—— 适配器按响应形态解析,此时退回一次性 |
|
|
791
|
+
| 改了框架源码却看不到效果 | 确认 import 的是同一份构建产物(`npm run build` 后跑 `dist`) |
|
|
792
|
+
| MCP 工具没出现在菜单里 | `mcpTools()` 的返回值没传进 `createApp({ tools })` —— 它不走装饰器收集,也不进 DI 容器(裸工具缝) |
|
|
793
|
+
| 装配期报「归一化后撞名」 | server 侧两个工具名只差非法字符(`a-b` / `a.b`)→ 归一化后同名;给个 `prefix` 或改 server 侧名字 |
|
|
794
|
+
| MCP 调用「成功」但内容是错误文本 | 连接器没把协议层 `isError: true` 转成抛错(框架只认抛错) |
|
|
795
|
+
| eval 里模型调了不存在的工具 | 脚本里的工具名必须是**菜单里的名字**(MCP 工具是归一化后的 `mcp_<server>_<name>`) |
|
|
796
|
+
| `metricsSink` 的数字一直是 0 | 没接进 `createApp({ sinks })`(或 `registerDefaultTraceSink`)—— 它靠 run 收尾投递,不自己埋点 |
|
|
797
|
+
| 拿 `metricsSink({ export: 'otlp' })` 报错 | 这是**故意的**:OTLP metrics 导出后置,构造期响亮失败好过给你一份空指标 |
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## 9. 提交前自检
|
|
802
|
+
|
|
803
|
+
```bash
|
|
804
|
+
npm run typecheck # src 类型
|
|
805
|
+
npm run typecheck:tests # 测试目录类型(含类型断言测试)
|
|
806
|
+
npm run test # 单测(node:test)
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
框架仓库另有四道:`npm run typecheck:types`(针对构建产物的类型测试)、`npm run e2e`(CLI 端到端 +
|
|
810
|
+
`examples/complete` 真起服务跑三种触发)、`npm run e2e:examples`(只跑后者)、
|
|
811
|
+
`npm run e2e:mcp`(真接一个 MCP server 走完「映射 → 菜单 → run」;无网时自动回落本地夹具 server)。
|