@my-life-buddies/buddy-runtime 0.3.1 → 0.4.1

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 (49) hide show
  1. package/README.md +255 -184
  2. package/dist/conversation/agent.d.ts +5 -6
  3. package/dist/conversation/agent.d.ts.map +1 -1
  4. package/dist/conversation/agent.js +63 -24
  5. package/dist/conversation/agent.js.map +1 -1
  6. package/dist/conversation/index.d.ts +1 -3
  7. package/dist/conversation/index.d.ts.map +1 -1
  8. package/dist/conversation/index.js +13 -36
  9. package/dist/conversation/index.js.map +1 -1
  10. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +7 -3
  11. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
  12. package/dist/conversation/mlbMessageAgentMessageConverter.js +19 -12
  13. package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
  14. package/dist/conversation/writeMLBMessage.d.ts +7 -3
  15. package/dist/conversation/writeMLBMessage.d.ts.map +1 -1
  16. package/dist/conversation/writeMLBMessage.js +6 -5
  17. package/dist/conversation/writeMLBMessage.js.map +1 -1
  18. package/dist/index.d.ts +1 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +0 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/platform/mlbClient.d.ts +45 -19
  23. package/dist/platform/mlbClient.d.ts.map +1 -1
  24. package/dist/platform/mlbClient.js +79 -32
  25. package/dist/platform/mlbClient.js.map +1 -1
  26. package/dist/server.js +3 -3
  27. package/dist/tools/dataAccess.d.ts +3 -3
  28. package/dist/tools/dataAccess.d.ts.map +1 -1
  29. package/dist/tools/dataAccess.js +5 -5
  30. package/dist/tools/dataAccess.js.map +1 -1
  31. package/dist/tools/registry.d.ts +1 -1
  32. package/dist/tools/registry.js +1 -1
  33. package/dist/tools/resource.d.ts +3 -3
  34. package/dist/tools/resource.js +7 -7
  35. package/dist/tools/widgets.d.ts +4 -4
  36. package/dist/tools/widgets.d.ts.map +1 -1
  37. package/dist/tools/widgets.js +13 -11
  38. package/dist/tools/widgets.js.map +1 -1
  39. package/dist/types.d.ts +118 -9
  40. package/dist/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/dist/platform/client.d.ts +0 -48
  43. package/dist/platform/client.d.ts.map +0 -1
  44. package/dist/platform/client.js +0 -24
  45. package/dist/platform/client.js.map +0 -1
  46. package/dist/platform/widgets.d.ts +0 -75
  47. package/dist/platform/widgets.d.ts.map +0 -1
  48. package/dist/platform/widgets.js +0 -88
  49. package/dist/platform/widgets.js.map +0 -1
package/README.md CHANGED
@@ -1,21 +1,27 @@
1
1
  # @my-life-buddies/buddy-runtime
2
2
 
3
- MLB 搭子运行时。基于 [`@earendil-works/pi-agent-core`](https://www.npmjs.com/package/@earendil-works/pi-agent-core) 构建,负责搭子与 MLB 平台之间的全部交互:订阅事件流、拉取消息、驱动模型、写回回复、推送在线状态。开发者只需提供搭子的配置。
3
+ 搭子运行时,基于 [`@earendil-works/pi-agent-core`](https://www.npmjs.com/package/@earendil-works/pi-agent-core)
4
+ 它提供了让搭子接入平台的基本能力:接收消息、模型调用、回复消息链路,并可以快速接入平台所提供的包括资料引入、记忆系统、授权数据、主动性、付费服务等特色化能力。
5
+ 开发者只需基于搭子的领域逻辑书写工具、人设等。
4
6
 
5
- ## 1. 安装
7
+ ## 1. 快速开始
8
+
9
+ ### 1.1 安装
6
10
 
7
11
  ```bash
8
12
  npm install @my-life-buddies/buddy-runtime typebox
9
13
  ```
10
14
 
11
- 要求 Node.js ≥ 22.19。
15
+ ### 1.2 注册搭子
16
+
17
+ 通过平台开发者接口 `POST /cli/buddies` 注册,提交名称、头像、简介,平台分配搭子 id 与令牌;之后改档案用 `PUT /cli/buddies/:id/meta`。可选的模型 id 查 `GET /cli/models`。
12
18
 
13
- ## 2. 快速开始
19
+ ### 1.3 搭子工程框架
14
20
 
15
21
  ```
16
22
  my-buddy/
17
23
  ├─ package.json
18
- ├─ .env MLB_BUDDY_ID / MLB_BUDDY_TOKEN
24
+ ├─ .env MLB_BUDDY_IDMLB_BUDDY_TOKEN
19
25
  └─ src/main.ts
20
26
  ```
21
27
 
@@ -38,7 +44,7 @@ const planMenu: AgentTool<typeof PLAN_MENU_PARAMS, undefined> = {
38
44
  };
39
45
 
40
46
  await BuddyServer.start({
41
- model: "kimi-k3", // 必填;可选值见开发者接口 GET /cli/models
47
+ model: "kimi-k3",
42
48
  buddy: () => ({
43
49
  initialState: {
44
50
  systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
@@ -56,103 +62,189 @@ await BuddyServer.start({
56
62
  }
57
63
  ```
58
64
 
59
- 完整示例见 [examples/aunt-tu/](examples/aunt-tu/)。
65
+ ### 1.4 启动
60
66
 
61
- ## 3. API
67
+ `.env` 里配好环境变量,`npm run dev` 启动
62
68
 
63
- ### 3.1 `BuddyServer.start(options)`
69
+ | 变量 | 说明 |
70
+ |---|---|
71
+ | `MLB_BUDDY_ID` | 必填,搭子 id |
72
+ | `MLB_BUDDY_TOKEN` | 必填,搭子令牌 |
64
73
 
65
- 启动搭子进程,连接平台事件流。返回 `{ stop(): Promise<void> }`。
74
+ - 在自己电脑上连测试网关时,平台把这个进程当作 `<id>-dev` 分身,会话与正式搭子分开。
75
+ - 改完代码想自动重启,在启动命令里加 `--watch`。
66
76
 
67
- | 参数 | 类型 | 说明 |
68
- |---|---|---|
69
- | `options.model` | `string`,必填 | 模型 id,取值见平台 `GET /cli/models`。进程内所有会话共用。没填,或 id 在运行时内置的模型目录中不存在时,启动失败 |
70
- | `options.welcome` / `options.errorReply` | `string`,可选 | 应用提供的静态问候,以及模型出错、空回复时发的兜底回复 |
71
- | `options.buddy` | `(conversation: Conversation) => BuddyOptions \| Promise<BuddyOptions>` | 搭子配置工厂。每场会话在第一条消息到达时调用一次 |
77
+ 完整示例:[examples/aunt-tu/](examples/aunt-tu/)(记忆、小挂件)、[examples/stacy-sims/](examples/stacy-sims/)(资料)。
78
+
79
+ ## 2. 配置
80
+
81
+ ### 2.1 `BuddyServer.start(options)`
82
+
83
+ 连上平台开始处理消息,返回 `{ stop(): Promise<void> }`。
84
+
85
+ | 参数 | 说明 |
86
+ |---|---|
87
+ | `model` | 必填,模型 id,取值见 `GET /cli/models`,进程内所有会话共用。没填或运行时不认识这个 id 时启动失败 |
88
+ | `buddy` | 必填,`(conversation: Conversation) => BuddyOptions \| Promise<BuddyOptions>`,见 2.2、2.3 |
89
+ | `welcome` | 可选,用户打开一场还没聊过的会话时发的问候,不经过模型 |
90
+ | `errorReply` | 可选,模型出错或空回复时发的兜底回复 |
72
91
 
73
- ### 3.2 `BuddyOptions`
92
+ `buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,要记住的东西写进记忆(3.2)。
74
93
 
75
- 配置的形状与字段名与 pi-agent-core `AgentOptions` / `AgentState` 一致,签名请参考 pi 的文档。
94
+ ### 2.2 `BuddyOptions`
95
+
96
+ `buddy` 的返回值。字段名和签名取自 pi-agent-core 的 `AgentOptions` / `AgentState`,详见 pi 的文档。
76
97
 
77
98
  ```ts
78
- type BuddyOptions =
79
- Pick<AgentOptions,
80
- | "beforeToolCall" | "afterToolCall"
81
- | "shouldStopAfterTurn" | "prepareNextTurnWithContext"
82
- | "thinkingBudgets" | "toolExecution">
83
- & { platformTools?: readonly PlatformToolName[]; initialState: Pick<AgentState, "systemPrompt"> & Partial<Pick<AgentState, "tools" | "thinkingLevel">> };
99
+ type BuddyOptions = Pick<AgentOptions,
100
+ | "beforeToolCall" | "afterToolCall"
101
+ | "shouldStopAfterTurn" | "prepareNextTurnWithContext"
102
+ | "thinkingBudgets" | "toolExecution"
103
+ > & {
104
+ platformTools?: readonly PlatformToolName[];
105
+ proactiveTools?: readonly string[];
106
+ initialState: Pick<AgentState, "systemPrompt"> & Partial<Pick<AgentState, "tools" | "thinkingLevel">>;
107
+ };
84
108
  ```
85
109
 
86
110
  | 字段 | 说明 |
87
111
  |---|---|
88
- | `platformTools` | 要注册给模型的平台工具名单,默认 `[]`,见 3.3 |
89
- | `initialState.systemPrompt` | 系统提示。静态内容;每回合变化的信息不应写在这里。运行时在前面拼上平台的系统提示(平台对每场会话都一样的规则,比如消息开头的时间怎么读),两者之间空一行;`platformTools` 里有 `read_resource` 且有 `resources/` 时,运行时在末尾追加资料清单 |
90
- | `initialState.tools` | 工具列表,类型为 pi 的 `AgentTool`。`label` 为必填,用作用户可见的状态行文案(空字符串表示不显示);`execute` 返回值的 `details` 为必填(可为 `undefined`);执行失败应直接抛出异常。工具名不得与平台工具或其他工具重复。先单独声明为 `AgentTool<typeof 参数 schema>` 再放入数组,`execute` 的参数才有类型;直接写在数组里时参数类型为 `unknown` |
91
- | `initialState.thinkingLevel` | 思考级别,默认 `off`。模型不支持的级别在发请求前就近换成支持的一档。显式设了模型不支持的级别时,每场会话建立时打一条 warn |
92
- | `beforeToolCall` / `afterToolCall` / `shouldStopAfterTurn` / `prepareNextTurnWithContext` / `thinkingBudgets` / `toolExecution` | 原样传递给 pi |
112
+ | `initialState.systemPrompt` | 必填,人设与做事规则,只放不随回合变化的内容。模型实际收到的系统提示见第 4 |
113
+ | `initialState.tools` | 可选,pi `AgentTool` 数组 |
114
+ | `initialState.thinkingLevel` | 可选,默认 `off`。模型不支持的级别在请求时就近换成支持的一档,建会话时打一条 warn |
115
+ | `platformTools` | 可选,给模型注册哪些平台工具,默认 `[]`,见第 3 |
116
+ | `proactiveTools` | 可选,主动回合里还能用哪些工具,默认 `[]`(一个都不给)。写工具名,`platformTools` 选中的和 `initialState.tools` 里自己的都行;写了没注册的名字建会话时抛异常。见 3.5 |
117
+ | 其余字段 | 原样交给 pi;`beforeToolCall` 抛异常时按放行处理 |
118
+
119
+ 写工具时注意:
93
120
 
94
- 密钥、会话历史与插话策略由运行时统一管理;工厂提供当前会话键与已绑定的平台能力,不传递 Token。模型通过 `BuddyServer.start` 的 `model` 选择。
121
+ - `label` 必填,是工具执行时用户看到的状态文案,空字符串表示不显示。
122
+ - `execute` 的返回值必须带 `details`,可以是 `undefined`;失败直接抛异常,模型能看到异常信息。
123
+ - 先单独声明为 `AgentTool<typeof 参数 schema>` 再放进数组,`execute` 的参数才有类型;直接写在数组里时参数类型是 `unknown`。
124
+ - 工具名不能与平台工具或其他工具重复。
95
125
 
96
- ### 3.3 `Conversation` 与平台能力
126
+ 模型、密钥与会话历史由运行时管理,不在配置里。
97
127
 
98
- 工厂拿到的对象,可在工具闭包中使用。会话之间的数据应以 `key` 隔离。
128
+ ### 2.3 `Conversation`
129
+
130
+ `buddy` 拿到的参数,工具闭包里也能用。
99
131
 
100
132
  ```ts
101
133
  interface Conversation {
102
- readonly key: string; // 会话键:私聊为 p_ 前缀,群聊为 g_ 前缀
103
- readonly log: BuddyLogger;
104
- readonly signal: AbortSignal; // 会话删除、空闲淘汰、stop 时中止
105
- readonly platform: PlatformClient; // 已绑好这场会话的平台能力
106
- }
134
+ // 会话键:私聊以 p_ 开头,群聊以 g_ 开头。开发者自己存的数据要按它隔离
135
+ key: string;
107
136
 
108
- interface PlatformClient {
109
- readonly memory: {
137
+ // 日志,每行开头带搭子 id
138
+ log: BuddyLogger;
139
+
140
+ // 资料,见 3.1:resources/ 里的 .md,进程启动时读好,所有会话共用
141
+ resources: {
142
+ list(): string[];
143
+ read(file: string): string;
144
+ };
145
+
146
+ // 记忆,见 3.2:这场会话的长期记忆,存在平台上
147
+ memory: {
110
148
  list(): Promise<MemoryEntry[]>;
111
149
  write(key: string, text: string): Promise<void>;
112
150
  delete(key: string): Promise<void>;
113
151
  };
114
- readonly dataAccess: { query(request: { datasets: string[]; memberId?: string }): Promise<DatasetAccessResult[]> };
115
- readonly proactive: { propose(request: ProactiveSubscriptionRequest): Promise<{ id: string; status: "pending" }> };
116
- readonly widgets: WidgetClient;
117
- readonly messages: { send(message: MLBMessageToWrite & { idempotencyKey: string }): void };
152
+
153
+ // 授权数据,见 3.3:私聊不传 memberId,群聊传消息前缀里的 m_…
154
+ dataAccess: {
155
+ query(request: { datasets: string[]; memberId?: string }): Promise<DatasetAccessResult[]>;
156
+ };
157
+
158
+ // 小挂件,见 3.4
159
+ widgets: {
160
+ list(): Promise<WidgetSummary[]>;
161
+ create(input: { type: string; title: string; idempotencyKey?: string }): Promise<{ id: string }>;
162
+ read(id: string): Promise<Widget>;
163
+ readModule(id: string, module: string): Promise<{ records: WidgetRecord[] }>;
164
+ rename(id: string, title: string): Promise<void>;
165
+ write(
166
+ id: string,
167
+ input: { module: string; recordId: string; data: Record<string, unknown>; expectedRevision?: number },
168
+ ): Promise<{ revision?: number }>;
169
+ deleteRecord(id: string, module: string, recordId: string): Promise<void>;
170
+ delete(id: string): Promise<void>;
171
+ setPresentation(id: string, input: { summary: string; imageMediaId: string }): Promise<void>;
172
+ send(id: string, options: { idempotencyKey: string }): void;
173
+ };
174
+
175
+ // 主动服务,见 3.5:提订阅建议,用户在 App 里确认后才生效
176
+ proactive: {
177
+ propose(request: ProactiveSubscriptionRequest): Promise<{ id: string; status: "pending" }>;
178
+ };
118
179
  }
119
180
  ```
120
181
 
121
- `platform` 下的方法由开发者在代码里调用,会话键已经绑好,不需要传 Token 或拼 HTTP 路径。其中授权数据、小挂件、资料三项,运行时还提供了给模型用的工具,把工具名写进 `BuddyOptions.platformTools` 才注册:
182
+ `memory`、`dataAccess`、`widgets`、`proactive` 已经绑好这场会话,直接调用,不用传令牌或拼接口路径。
183
+
184
+ ## 3. 平台能力
122
185
 
123
- | 能力 | 代码里调用 | 给模型的工具 | 工具注册还要满足 |
186
+ | 能力 | `conversation` 对象方法 | 默认注册成的模型工具 | 默认注册成模型工具的条件 |
124
187
  |---|---|---|---|
125
- | 记忆 | `platform.memory` | 无,要给模型用就自己包成工具,见 3.3.1 | |
126
- | 授权数据 | `platform.dataAccess.query` | `data_access_query` | 声明过至少一个 Dataset |
127
- | 小挂件 | `platform.widgets` | `widget_create` / `widget_write` / `widget_read` / `widget_delete` / `widget_send` | 登记过至少一个小挂件类型 |
128
- | 资料 | | `read_resource` | `resources/` 下有 `.md` |
129
- | 发消息 | `platform.messages.send` | 无 | |
130
- | 主动服务建议 | `platform.proactive.propose`,见第 7 节 | 无 | 用户在 App 确认后生效 |
188
+ | 资料 | `conversation.resources.list` / `conversation.resources.read` | `resource_read` | platformTools 声明 && `resources/` 下有 `.md` |
189
+ | 记忆 | `conversation.memory` | 无,默认不注册成模型工具,开发者可以根据具体场景,注册成适合场景需要的模型工具 | |
190
+ | 授权数据 | `conversation.dataAccess.query` | `data_access_query` | platformTools 声明 && 声明过至少一个 Dataset |
191
+ | 小挂件 | `conversation.widgets.list` / `conversation.widgets.create` / `conversation.widgets.read` / `conversation.widgets.readModule` / `conversation.widgets.rename` / `conversation.widgets.write` / `conversation.widgets.deleteRecord` / `conversation.widgets.delete` / `conversation.widgets.setPresentation` / `conversation.widgets.send` | `widget_create` / `widget_write` / `widget_read` / `widget_delete` / `widget_send` | platformTools 声明 && 登记过至少一个小挂件类型 |
192
+ | 主动服务 | `conversation.proactive.propose` | 无 | |
193
+
194
+ ### 3.1 资料
131
195
 
132
- - `platformTools` 默认 `[]`,一个平台工具都不注册。名单里有不认识或重复的名字时,这场会话起不来。
133
- - 注册的平台工具排在 `initialState.tools` 前面;和开发者的工具重名时,这场会话起不来。
134
- - 没写进名单的,照样可以在代码里调用 `platform`。
196
+ `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,`platformTools` 写上 `resource_read`,模型就能按需读。
197
+
198
+ - 工程目录从入口脚本往上找,与启动时的工作目录无关。
199
+ - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 与点开头的文件和目录;资料改了要重启进程。
200
+ - 系统提示末尾追加资料清单,模型按清单里的相对路径读。没有资料或没选 `resource_read` 时,不注册工具,也不追加清单。
201
+
202
+ ```
203
+ my-buddy/ 工程目录
204
+ ├─ package.json
205
+ ├─ resources/
206
+ │ └─ research/
207
+ │ └─ 01-writings.md 清单里写作 research/01-writings.md
208
+ └─ src/main.ts 入口脚本
209
+ ```
210
+
211
+ **在自己的代码里用**
212
+
213
+ 调用 `conversation.resources`,不需要写进 `platformTools`:
214
+
215
+ | 方法 | 做什么 |
216
+ |---|---|
217
+ | `list()` | 全部资料的路径,相对 `resources/`,按路径排序 |
218
+ | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填;没有这篇时抛异常 |
135
219
 
136
220
  ```ts
137
- return {
138
- platformTools: ["data_access_query", "widget_read"],
139
- initialState: { systemPrompt: PERSONA, tools: [remember] },
140
- };
221
+ const notes = conversation.resources.read("research/01-writings.md");
141
222
  ```
142
223
 
143
- #### 3.3.1 记忆
224
+ - 两个方法都是同步的,读的是进程启动时读好的那份。
225
+ - 主动服务回合里也能调。
226
+
227
+ 完整示例见 [examples/stacy-sims/](examples/stacy-sims/)。
144
228
 
145
- `platform.memory` 是这场会话的长期记忆,由平台保存,只在本会话内可见,会话删除时平台一并清除。`key` 为主题名(非空、不含空白与 `/`,最长 64 字),`text` 单条不超过 16KB;每场会话最多 64 条、合计不超过 256KB。`list` 全量列出,按 `key` 字典序;`write` 覆盖写一条;`delete` 删一条,删不存在的也算成功。超出配额、`key` 不合法或网络出错时直接抛出异常,异常信息为平台给出的原因;在工具的 `execute` 中调用时,模型能看到这个原因。
229
+ ### 3.2 记忆
230
+
231
+ 这场会话的长期记忆,存在平台上,只在本会话内可见,会话删除时一并清除。
232
+
233
+ - `list()`:列出全部,按 `key` 字典序。
234
+ - `write(key, text)`:覆盖写一条。
235
+ - `delete(key)`:删一条,不存在也算成功。
236
+
237
+ `key` 非空、不含空白与 `/`、最长 64 字;`text` 单条不超过 16 KB;每场会话最多 64 条、合计不超过 256 KB。超限、`key` 不合法或网络出错时抛异常,异常信息是平台给出的原因,在工具里调用时模型能看到。
146
238
 
147
239
  ```ts
148
240
  interface MemoryEntry {
149
- key: string; // 主题名
241
+ key: string;
150
242
  text: string;
151
243
  updatedAt: number; // 最后写入时刻,毫秒
152
244
  }
153
245
  ```
154
246
 
155
- 运行时不提供记忆工具,要让模型记东西,开发者自己包成工具:
247
+ 运行时不提供记忆工具,要让模型记东西,自己包一个:
156
248
 
157
249
  ```ts
158
250
  const REMEMBER_PARAMS = Type.Object({ topic: Type.String(), text: Type.String() });
@@ -166,7 +258,7 @@ await BuddyServer.start({
166
258
  description: "把一条关于对方的印象记下来,同一主题覆盖旧的。",
167
259
  parameters: REMEMBER_PARAMS,
168
260
  execute: async (_id, { topic, text }) => {
169
- await conversation.platform.memory.write(topic, text);
261
+ await conversation.memory.write(topic, text);
170
262
  return { content: [{ type: "text", text: "记下了。" }], details: undefined };
171
263
  },
172
264
  };
@@ -175,15 +267,13 @@ await BuddyServer.start({
175
267
  });
176
268
  ```
177
269
 
178
- 完整示例见 [examples/aunt-tu/](examples/aunt-tu/):`taste_read` / `taste_write` 两个工具用 `platform.memory` 记每个人爱吃什么、不爱吃什么。
270
+ ### 3.3 授权数据
179
271
 
180
- #### 3.3.2 授权数据
181
-
182
- 经用户同意,搭子可以读取用户授权数据。数据按 Dataset 分项,每项内容与字段固定,用户逐项授权。开发者要做的是声明需要哪些 Dataset,然后让模型用 `data_access_query` 查,或者在自己的工具里查。会话与真实用户身份由平台绑定,搭子拿不到真实用户 id。
272
+ 用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id。
183
273
 
184
274
  **声明需要哪些数据**
185
275
 
186
- 通过平台 CLI 接口 `PUT /cli/buddies/:id/data-requirements` 提交完整清单,每次提交覆盖上一次:
276
+ `PUT /cli/buddies/:id/data-requirements` 提交完整清单,每次覆盖上一次:
187
277
 
188
278
  ```json
189
279
  {
@@ -194,11 +284,9 @@ await BuddyServer.start({
194
284
  ```
195
285
 
196
286
  - `id`:从下表选。
197
- - `purpose`:写给用户看的用途,1~200 字。用户在 App 里看到这句话,再决定是否同意。
198
- - 改了某项的 `purpose`,用户之前的同意失效,要重新同意;从清单里删掉的数据,搭子立即读不到。
199
- - 声明或改动之后,重启搭子进程,模型才能看到新的清单。
200
-
201
- 目前支持的 Dataset:
287
+ - `purpose`:用途,1200 字。用户看到这句话再决定是否同意。
288
+ - 改了某项的 `purpose`,用户要重新同意;从清单里删掉的,搭子立即读不到。
289
+ - 改完清单要重启搭子进程,模型才能看到新清单。
202
290
 
203
291
  | Dataset | 内容 | `data.items` 每条的字段 |
204
292
  |---|---|---|
@@ -213,65 +301,70 @@ await BuddyServer.start({
213
301
 
214
302
  部分字段可能缺失,取值前先判断。
215
303
 
216
- **让模型去查**
217
-
218
- `platformTools` 里有 `data_access_query`,且声明过至少一项数据,模型就有了这个工具,工具说明里列着开发者声明的数据与用途。开发者可以在系统提示里写明什么时候查,例如「聊到作息时,先用 data_access_query 查 health.sleep」。
304
+ **查询**
219
305
 
220
- **在自己的工具里查**
221
-
222
- 调用 `conversation.platform.dataAccess.query`,不需要写进 `platformTools`:
306
+ - 让模型查:`platformTools` 写上 `data_access_query`,工具说明里会列出声明过的 Dataset 与用途。可以在系统提示里写明什么时候查,比如「聊到作息时,先用 data_access_query 查 health.sleep」。
307
+ - 在工具里查:调用 `conversation.dataAccess.query`。
223
308
 
224
309
  ```ts
225
- const [sleep] = await conversation.platform.dataAccess.query({ datasets: ["health.sleep"] });
310
+ const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
226
311
  if (sleep.status === "available") {
227
312
  // sleep.data.items 是每天一条的睡眠记录
228
313
  }
229
314
  ```
230
315
 
231
- - `datasets`:一次最多 8 个;响应超过 512 KiB 时抛出异常,减少 Dataset 后重试。
232
- - `memberId`:私聊不传;群聊必须传,即群消息前缀 `[名字 #m_…]` 里的 `m_…`,查的是这位成员的数据。传错时直接抛出异常。
233
-
234
- **查询结果**
316
+ - `datasets`:一次最多 8 个;响应超过 512 KiB 时抛异常,减少 Dataset 再查。
317
+ - `memberId`:私聊不传;群聊必传,取群消息开头 `[名字 #m_…]` 里的 `m_…`,查这位成员的数据,传错时抛异常。
235
318
 
236
- 每个 Dataset 各有一个状态,一个读不到不影响其他。只有 `available` 带 `data`:
319
+ 每个 Dataset 各自一个状态,只有 `available` 带 `data`:
237
320
 
238
321
  | 状态 | 意思 |
239
322
  |---|---|
240
323
  | `available` | 读到了 |
241
324
  | `notDeclared` | 搭子没有声明这项数据 |
242
- | `notGranted` | 用户没有同意,或改过用途后用户还没重新同意 |
325
+ | `notGranted` | 用户没有同意,或改过用途后还没重新同意 |
243
326
  | `disabledInGroup` | 这位成员在这个群里关掉了这项数据 |
244
327
  | `unavailable` | 平台上还没有这位用户的这项数据,或数据已过期 |
245
328
 
246
- **数据只在这一轮回复里可见**
329
+ **数据只在当轮可见**
330
+
331
+ 查到的数据只用于这一轮回复:平台不存原文,搭子进程里的那份也在这一轮结束时被抹掉,下一轮读不到。凡是可能把它带出去的地方——工具结果、工具参数、那一步的正文和心声——这一轮统统不写进平台。
332
+
333
+ 这么设计是为了**用户撤销授权时能真的收回**:数据没有留在任何地方,不需要事后去清。代价是这一轮的记录不完整,搭子重启后读不到自己当时的推理和工具参数。
334
+
335
+ 由此有两条实操结论:
247
336
 
248
- 一轮回复里只要查过授权数据,这一轮结束后,聊天记录和会话历史里这一轮的工具参数、工具结果与 thinking,都会换成「查了哪些数据、各是什么状态」,给用户看的回复照常保留。开发者自己写进记忆或小挂件的内容不会被替换,不要把查到的数据写进去。
337
+ - **想让某个结论以后还在场,就在回复里说出来**(用户也会看到)。模型对这些数据的记忆只能活在它说过的话里——下次它读到的是「查了 health.sleep,可用」,不是具体数字。
338
+ - **别把查到的数据写进记忆、小挂件或别的工具的参数**。那些地方不脱敏,写进去就永久留下了。
249
339
 
250
- #### 3.3.3 小挂件
340
+ ### 3.4 小挂件
341
+
342
+ 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widgets`,都能建小挂件、写记录、读记录、删记录,并把它发进聊天。
251
343
 
252
344
  **登记类型**
253
345
 
254
- 开发者通过平台 CLI 接口给搭子登记小挂件类型:`PUT /cli/buddies/:id/widget-types/:typeId/schema` schema,每个模块写一句 `description` 给模型读,每条记录的形状是一份 JSON Schema;`PUT /cli/buddies/:id/widget-types/:typeId/page` 交展示页面,单个 HTML 文件。
346
+ - `PUT /cli/buddies/:id/widget-types/:typeId/schema`:交 schema。每个模块写一句 `description` 给模型读,每条记录的形状是一份 JSON Schema
347
+ - `PUT /cli/buddies/:id/widget-types/:typeId/page`:交展示页面,单个 HTML 文件。
255
348
 
256
- **让模型去用**
349
+ **让模型使用**
257
350
 
258
- `platformTools` 里写了哪几个 `widget_*`,运行时就注册哪几个:进程里第一次要用时拉一次 `GET /internal/v1/widget-types`,之后共用;工具说明里附上各类型的模块与字段;一个类型都没登记时一个也不注册。这场会话里已有哪些小挂件,由平台消息流里的 `context` 记录告诉模型。
351
+ 把要用的 `widget_*` 写进 `platformTools`。
259
352
 
260
- - 平台请求失败时工具抛出异常,模型看到的是「平台请求失败(HTTP 409),请查询最新状态后再操作」这样的提示,不带平台返回的具体原因。
261
- - `widget_create` `widget-create:<toolCallId>` 作幂等键,同一次工具调用重发不会建出两个。
262
- - `widget_send` 把小挂件放进这场会话的发送队列就返回,不等平台确认送达。
353
+ - 工具说明里附上各类型的模块与字段。类型在进程里第一次用到时读取,改了类型要重启进程。
354
+ - 这场会话里已有哪些小挂件,平台会在消息里告诉模型,见第 4 节。
355
+ - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
356
+ - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
263
357
 
264
- **在自己的工具里用**
358
+ **在自己的代码里用**
265
359
 
266
- 调用 `conversation.platform.widgets`,不需要写进 `platformTools`:
360
+ 调用 `conversation.widgets`,不需要写进 `platformTools`:
267
361
 
268
362
  ```ts
269
- execute: async (toolCallId, params, signal) => {
270
- const widgets = conversation.platform.widgets.withSignal(signal);
271
- const { id } = await widgets.create({ type: "itinerary", title: "杭州行程", idempotencyKey: `itinerary:${toolCallId}` });
272
- await widgets.write(id, { module: "days", recordId: "day-1", data: { city: "杭州" } });
273
- widgets.send(id, { idempotencyKey: `itinerary-send:${toolCallId}` });
274
- return { content: [{ type: "text", text: "行程建好了。" }], details: undefined };
363
+ execute: async (toolCallId, params) => {
364
+ const { id } = await conversation.widgets.create({ type: "menu", title: "本周菜单", idempotencyKey: `menu:${toolCallId}` });
365
+ await conversation.widgets.write(id, { module: "菜单", recordId: "2026-09-14", data: { dishes: ["红烧肉", "清炒时蔬"] } });
366
+ conversation.widgets.send(id, { idempotencyKey: `menu-send:${toolCallId}` });
367
+ return { content: [{ type: "text", text: "菜单建好了。" }], details: undefined };
275
368
  },
276
369
  ```
277
370
 
@@ -281,109 +374,87 @@ execute: async (toolCallId, params, signal) => {
281
374
  | `create({ type, title, idempotencyKey? })` | 新建一个,回 `{ id }`;同一个 `idempotencyKey` 重复建回同一个 id | `POST /internal/v1/widgets` |
282
375
  | `read(id)` | 读整个小挂件,含各模块的记录 | `GET /internal/v1/widgets/:widgetId` |
283
376
  | `readModule(id, module)` | 读一个模块,回 `{ records }` | `GET /internal/v1/widgets/:widgetId/:module` |
284
- | `rename(id, title)` | 改标题,id 不变 | `PUT /internal/v1/widgets/:widgetId` |
285
- | `write(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖;带 `expectedRevision` 时版本号对不上,平台回 409 | `PUT /internal/v1/widgets/:widgetId/:module/:recordId` |
377
+ | `rename(id, title)` | 改标题,id 不变,聊天里发过的卡片跟着显示新标题 | `PUT /internal/v1/widgets/:widgetId` |
378
+ | `write(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖,回 `{ revision }`;带 `expectedRevision` 时版本号对不上,平台回 409 | `PUT /internal/v1/widgets/:widgetId/:module/:recordId` |
286
379
  | `deleteRecord(id, module, recordId)` | 删一条记录 | `DELETE /internal/v1/widgets/:widgetId/:module/:recordId` |
287
- | `setPresentation(id, { summary, imageMediaId })` | 设置聊天流里小挂件卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 | `PUT /internal/v1/widgets/:widgetId/presentation` |
288
- | `send(id, { idempotencyKey })` | 放进发送队列,按顺序发一条 `type: "widget"` 的消息 | `POST /internal/v1/messages` |
289
- | `withSignal(signal)` | 回一个同时受这个 signal 与会话 `signal` 控制的 Client | |
380
+ | `delete(id)` | 删整个小挂件连记录;聊天里发过的卡片留着,点开显示已删除 | `DELETE /internal/v1/widgets/:widgetId` |
381
+ | `setPresentation(id, { summary, imageMediaId })` | 设聊天里卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 | `PUT /internal/v1/widgets/:widgetId/presentation` |
382
+ | `send(id, { idempotencyKey })` | 发进聊天:放进发送队列就返回,不等平台确认送达;同一个 `idempotencyKey` 重复发只落一条 | `POST /internal/v1/messages` |
290
383
 
291
- - 每个方法最后都可以传 `{ signal }`。
384
+ - `list` `read` 回的 `canEdit` 为 `false` 的,是别的会话转发来的引用,只能读;改名、写记录、删除、设卡片时平台回 403。
292
385
  - `data` 要符合登记的 schema,不符合时平台回 400。
293
- - 平台回非 2xx、网络出错、超过 30 秒或响应超过 4 MiB 时抛出 `PlatformRequestError`,异常只带 HTTP 状态码。写操作不自动重试,失败后先读一遍真实状态再决定。
386
+ - 平台回非 2xx、网络出错、超过 30 秒或响应超过 4 MiB 时抛异常,异常的 `status` HTTP 状态码,不带平台给的原因。写操作不自动重试,失败后先读一遍真实状态再决定。
387
+ - 主动服务回合里,除了 `list`、`read`、`readModule`,其余方法都会抛异常,见 3.5。
294
388
 
295
389
  用法见 [examples/aunt-tu/](examples/aunt-tu/)。
296
390
 
297
- #### 3.3.4 资料
298
-
299
- 资料放在工程目录(`package.json` 所在目录)下的 `resources/`。工程目录由入口脚本向上定位,与启动进程的工作目录无关。
300
-
301
- - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 及点开头的目录/文件;资料变更后需重启进程。
302
- - `platformTools` 里有 `read_resource` 时,系统提示末尾追加资料清单;模型通过清单中的相对路径读取内容。
303
- - 没有资料或名单里没有 `read_resource` 时,不注册 `read_resource`,也不追加资料清单。
391
+ ### 3.5 主动服务
304
392
 
305
- ```
306
- my-buddy/ 工程目录
307
- ├─ package.json
308
- ├─ resources/ 资料目录
309
- │ └─ research/
310
- │ └─ 01-writings.md 清单中写作 research/01-writings.md
311
- └─ src/main.ts 入口脚本
312
- ```
393
+ 让搭子在约定的时机主动找用户说话,比如每周一次复盘。只支持私聊。
313
394
 
314
- 完整示例见 [examples/stacy-sims/](examples/stacy-sims/)。
395
+ 1. 开发者的工具调用 `conversation.proactive.propose` 提订阅建议。订阅建成时是 `pending`,用户在 App 里确认后才生效。
396
+ 2. 触发条件满足,且没被免打扰时段、冷却时间、每日上限拦下时,平台发起一次主动回合。
397
+ 3. 模型判断要不要开口:要就直接写出发给用户的消息,不要就输出跳过标记。平台复核后把消息发给用户。
315
398
 
316
- #### 3.3.5 发消息与图片
317
-
318
- **发消息**
319
-
320
- `platform.messages.send(message)` 把一条消息放进这场会话的发送队列,和模型的回复排在同一条队列里按顺序发(`POST /internal/v1/messages`),调用后立即返回,不等平台确认。
321
-
322
- - `message.type` 取 `message` / `tool` / `note` / `widget`;`idempotencyKey` 必填。
323
- - 会话已经删除或淘汰时直接抛出异常。
324
-
325
- **图片**
326
-
327
- 用户发的图不需要开发者处理。平台在拉取消息时把图片内容一起给运行时,运行时把它放进会话历史,之后每次请求模型原样带上:
328
-
329
- - 图片在用户上传时由平台统一处理成 JPEG,最长边不超过 2048 px、不超过 5 MiB。
330
- - 图片消息交给模型时是一条 user 消息:正文前标着「[图片 #消息 id]」,用户配了文字接在后面,再跟图片本身。
331
- - 引用了别的消息的,开头加一行:引用文字写「[引用 发言人:原话]」,引用图片写「[引用 发言人的图片 #被引消息 id]」,模型按 id 对上是哪一张。
332
- - 转发进聊天记录卡的图,跟在卡里那条正文后面。
333
- - 图片已经读不到时换成「[图片无法读取]」;`model` 选的模型不支持看图时换成「[图片:当前模型不支持看图]」。
334
-
335
- ## 4. 会话历史:MLBMessage → AgentMessage → LLMMessage
336
-
337
- 开发者不需要保存会话历史,也不需要把聊天里的消息换成模型消息,这些由运行时完成。每场会话的消息都保存在平台上,搭子进程重启后历史照样在。从平台上的 `MLBMessage` 到模型收到的 `LLMMessage`:
338
-
339
- ```
340
- MLBMessage[] → AgentMessage[] → convertToLlm() → LLMMessage[] → LLM
341
- ▲ (1) │ ▲ (2) │
342
- └─────(4)──────┘ └──────────────────(3)─────────────────────┘
399
+ ```ts
400
+ await conversation.proactive.propose({
401
+ title: "每周复盘",
402
+ reason: "在约定时间结合最近聊天做一次简短复盘",
403
+ sources: ["schedule"],
404
+ eventTypes: ["schedule.due"],
405
+ allowedDataScopes: [],
406
+ schedule: { at: firstReviewAt, intervalMinutes: 7 * 24 * 60 },
407
+ });
343
408
  ```
344
409
 
345
- 1. 平台上的 `MLBMessage` 换成 pi 的 `AgentMessage`,追加进会话历史。一条消息只换一次:会话起来时换一遍平台上已有的全部消息,之后来一条换一条。图片、引用和聊天记录卡怎么换,见 3.3.5。
346
- 2. `convertToLlm()`:运行时实现,开发者不能替换。每次请求模型都拿完整的会话历史来换:user、工具结果和运行时补的提醒(note)正文开头拼上它的时间,统一用 GMT、精确到秒,如「[2026-09-10 14:00:30 GMT] 」;搭子自己说的话(assistant)不拼,否则模型会照着在回复开头也写上时间;note 换成 user,其余原样,按原来的先后一条对一条,相邻的 user 不合并。换之前,运行时先在历史末尾接一条提醒,开头的时间就是现在,正文是「现在的时间(平台附注,不是谁说的话)」;这条只进这一次请求,不进会话历史。这些时间怎么读、用户所在时区去哪看,写在平台的系统提示里。换出来的 `LLMMessage`(即 pi-ai 的 `Message`)连同 `systemPrompt` 与 `tools` 发给模型。
347
- 3. 模型的回复,以及它调用工具得到的结果,直接追加进会话历史,不经过 (1)。
348
- 4. 经 (3) 追加进历史的消息,运行时再换成 `MLBMessage` 写回平台;经 (1) 从平台来的不会写回。用户在聊天里看到的回复就是这一步写上去的。用过授权数据的那一轮,写回前先脱敏,见 3.3.2。搭子重启后,这些消息经 (1) 取回。
349
-
350
- (1) 在消息到来时发生;(2) 在每次请求模型前都完整跑一遍,一轮回复里模型可能请求好几次,比如调完工具接着说,每次都从当时完整的会话历史重新算;(3)、(4) 在模型每说完一段、每跑完一步工具时发生。
410
+ | 字段 | 说明 |
411
+ |---|---|
412
+ | `title` | 必填,订阅名称,最长 100 字 |
413
+ | `reason` | 必填,为什么要主动找用户,最长 500 |
414
+ | `sources` | 必填,触发来源,`[]` 表示不限 |
415
+ | `eventTypes` | 必填,至少一个,触发事件类型 |
416
+ | `allowedDataScopes` | 必填,主动回合里允许读的 Dataset,只能从已声明的里选,`[]` 表示不读 |
417
+ | `schedule` | 可选,定时触发:`at` 为首次时刻(UTC 毫秒),`intervalMinutes` 为重复间隔(60~525600),按固定间隔算,不随夏令时调整。用它时 `eventTypes` 要含 `schedule.due`,`sources` 为 `[]` 或含 `schedule` |
418
+ | `quietHours` | 可选,免打扰时段,缺省 `{ start: "22:30", end: "08:00", timezone: "Asia/Shanghai" }` |
419
+ | `cooldownMinutes` | 可选,冷却时间,缺省 120,取 0~10080 |
420
+ | `dailyLimit` | 可选,每天最多几次,缺省 3,取 1~20 |
351
421
 
352
- ## 5. 环境变量
422
+ 字段不合法、在群聊里调用,或同一用户对这只搭子已有 20 个未撤销的订阅时抛异常,异常信息只有 HTTP 状态码。
353
423
 
354
- | 变量 | 说明 |
355
- |---|---|
356
- | `MLB_BUDDY_ID` | 搭子 id,注册时由平台分配 |
357
- | `MLB_BUDDY_TOKEN` | 搭子令牌,注册时由平台分配 |
424
+ 主动回合里:
358
425
 
359
- `MLB_GATEWAY_URL` 可覆盖平台网关。当前缺省为 ecs-test(`http://47.116.168.81:8788`);本地测试必须显式配置回环地址及虚拟凭据,不能用正式搭子 Token 启动第二个进程。
426
+ - **模型能用的工具只有 `proactiveTools` 里点名的那些,默认一个都没有。** 用户不在场,所以不默认信任任何工具——名字叫 `read_xxx` 的也可能在里面发请求、写外部系统。没点名的工具模型仍然看得见,调了会被挡回一条说明,工具本身不执行。`data_access_query` 另外只能读订阅的 `allowedDataScopes`。
427
+ - 系统提示和普通回合一字不差,记忆不会被附上去。要让模型用记忆,自己把 `conversation.memory.list` 包成工具注册进来,再写进 `proactiveTools`。
428
+ - 调用 `memory.write`、`memory.delete`、`proactive.propose`,以及 `widgets` 上除 `list`、`read`、`readModule` 以外的方法,会抛异常;开发者的钩子照常执行。
429
+ - 用户这时发来消息,主动回合中止,先回复用户;超过 4 分钟也中止。
430
+ - 主动回合的过程不下发给用户:平台在会话流里记两条不可见的记录(叫你判断了什么、你判断的结果),中间的工具步照常写回但也不下发。**开口的那条消息才是用户看得到的**,之后作为搭子说过的话出现在会话历史里。
360
431
 
361
- 搭子的档案(名称、头像、简介)由平台保存:注册时通过 `POST /cli/buddies` 提交,修改通过 `PUT /cli/buddies/:id/meta`。搭子进程不监听端口、不接收入站请求。从非回环地址连接网关时,平台将其识别为 `<id>-dev` 分身,会话与正式身份隔离。
432
+ ## 4. 模型收到什么
362
433
 
363
- ## 6. 开发
434
+ 开发者不用保存会话历史,也不用自己拼模型消息。每场会话的消息存在平台上,进程重启后运行时从平台取回。每次请求模型时,运行时组出以下内容。
364
435
 
365
- - 改完代码需要自动重启时,自行在 `package.json` 里用 `node --watch` 启动。
366
- - 会话消息拉取遇到临时网络错误时会主动指数退避重试(1 秒起步、最高 30 秒);新的 SSE 通知会取消等待并立即重拉。
436
+ **系统提示**,按顺序拼接,段间空一行:
367
437
 
368
- ## 7. 主动服务
438
+ 1. 平台的系统提示:对每场会话都一样的规则,比如消息开头的时间怎么读、用户所在时区去哪看。
439
+ 2. `initialState.systemPrompt`。
440
+ 3. 资料清单:选了 `resource_read` 且有资料时才有。
369
441
 
370
- Runtime 消费平台 SSE `proactive {runId}`,领取 Run 后恢复对话、临时召回会话 Memory,并让模型按需查询授权 Dataset。数据查询携带短期执行令牌;最终 send/skip 结果由平台复核和落库,不经过普通消息写口。
442
+ **工具**:选中的平台工具在前,`initialState.tools` 在后。
371
443
 
372
- 主动回合仅开放 `platformTools` 已选择的 `data_access_query` 和 `read_resource`,不会自动增加工具。临时 Trigger、Memory 注入和工具数据在结束后移除;普通用户消息到达会中止主动生成并优先回复。执行重试归平台,Runtime 不自行维护调度器。
444
+ **消息**:这场会话的完整历史,按时间先后排列。一轮回复里模型可能请求多次,比如调完工具接着说,每次都带上当时的完整历史。
373
445
 
374
- 普通聊天中的开发者工具可提出订阅建议:
446
+ - 用户消息、工具结果、运行时提醒的开头带 GMT 时间,如「[2026-09-10 14:00:30 GMT] 」;搭子自己说的话不带。
447
+ - 历史末尾多一条「现在的时间(平台附注,不是谁说的话)」,时间是请求时刻,只在这一次请求里。
448
+ - 群聊里成员说的话开头标「[名字 #m_…] 」。
449
+ - 平台交代的情况(比如这场会话里已有哪些小挂件)是一条以「平台交代的情况:」开头的消息,情况有变化才追加新的一份。
450
+ - 引用了别的消息时,开头加一行「[引用 发言人:原话]」,引用图片写「[引用 发言人的图片 #被引消息 id]」。
451
+ - 转发的聊天记录卡按条目展开,每条先写「【名字】」再写正文。
452
+ - 查过授权数据的那一轮,结束后按 3.3 替换。
375
453
 
376
- ```ts
377
- await conversation.platform.proactive.propose({
378
- title: "每周复盘",
379
- reason: "在约定时间结合最近聊天做一次简短复盘",
380
- sources: ["schedule"],
381
- eventTypes: ["schedule.due"],
382
- allowedDataScopes: [],
383
- schedule: { at: firstReviewAt, intervalMinutes: 7 * 24 * 60 },
384
- });
385
- ```
454
+ **图片**:用户发的图不用开发者处理。
386
455
 
387
- 返回的订阅始终是 pending,用户在 App 确认后才能触发。`at` 为毫秒 UTC 时间戳;固定间隔不承诺夏令时地区的墙上时间不变。
456
+ - 平台在上传时统一处理成 JPEG,最长边不超过 2048 px、不超过 5 MiB。
457
+ - 图片消息是一条 user 消息:先是「[图片 #消息 id]」和用户配的文字,再跟图片本身;聊天记录卡里的图跟在所在条目的正文后面。
458
+ - 图片读不到时换成「[图片无法读取]」;`model` 不支持看图时换成「[图片:当前模型不支持看图]」。
388
459
 
389
- 此能力要求匹配的 Proactive Server 版本。旧 Server 不支持这些端点,不会自动降级为直接主动写消息。
460
+ 在钩子里读会话历史时,除了 pi `user` / `assistant` / `toolResult`,还会遇到 `role` 为 `note` 的消息:运行时补给模型的提醒,比如一轮回复没说出话时的提示,发给模型时换成 user 消息。