@my-life-buddies/buddy-runtime 0.16.0 → 0.18.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 (44) hide show
  1. package/README.md +204 -170
  2. package/dist/conversation/agent.d.ts.map +1 -1
  3. package/dist/conversation/agent.js +10 -5
  4. package/dist/conversation/agent.js.map +1 -1
  5. package/dist/conversation/index.d.ts +3 -1
  6. package/dist/conversation/index.d.ts.map +1 -1
  7. package/dist/conversation/index.js +13 -5
  8. package/dist/conversation/index.js.map +1 -1
  9. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +1 -0
  10. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
  11. package/dist/conversation/mlbMessageAgentMessageConverter.js +5 -5
  12. package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
  13. package/dist/conversation/writeMLBMessage.d.ts +1 -0
  14. package/dist/conversation/writeMLBMessage.d.ts.map +1 -1
  15. package/dist/conversation/writeMLBMessage.js.map +1 -1
  16. package/dist/dataAccessRequirements.d.ts +5 -0
  17. package/dist/dataAccessRequirements.d.ts.map +1 -0
  18. package/dist/dataAccessRequirements.js +53 -0
  19. package/dist/dataAccessRequirements.js.map +1 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/mlbClient.d.ts +6 -4
  23. package/dist/mlbClient.d.ts.map +1 -1
  24. package/dist/mlbClient.js +12 -6
  25. package/dist/mlbClient.js.map +1 -1
  26. package/dist/server.d.ts +2 -2
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +38 -36
  29. package/dist/server.js.map +1 -1
  30. package/dist/tools/askQuestion.d.ts +2 -1
  31. package/dist/tools/askQuestion.d.ts.map +1 -1
  32. package/dist/tools/askQuestion.js +4 -1
  33. package/dist/tools/askQuestion.js.map +1 -1
  34. package/dist/tools/dataAccess.d.ts +14 -4
  35. package/dist/tools/dataAccess.d.ts.map +1 -1
  36. package/dist/tools/dataAccess.js +60 -10
  37. package/dist/tools/dataAccess.js.map +1 -1
  38. package/dist/tools/dataAccessRequest.js +1 -1
  39. package/dist/tools/dataAccessRequest.js.map +1 -1
  40. package/dist/tools/proactive.js +1 -1
  41. package/dist/tools/proactive.js.map +1 -1
  42. package/dist/types.d.ts +1 -9
  43. package/dist/types.d.ts.map +1 -1
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -20,15 +20,25 @@ npm install @my-life-buddies/buddy-runtime typebox
20
20
 
21
21
  ### 1.2 创建搭子
22
22
 
23
- 通过 `@my-life-buddies/cli` 完成搭搭账号注册,并创建一个新的搭子,获取 buddyId 与开发令牌(平台另有一把正式令牌,只在托管运行时注入,不经 CLI 下发)
23
+ 通过 `@my-life-buddies/cli` 完成搭搭账号注册,并创建一个新的搭子,获取 buddyId 与开发令牌(平台另有一把正式令牌,只在托管运行时注入,不经 CLI 下发)。buddyId 写进 `package.json` 的 `buddyId` 字段,开发令牌写进 `.env` 的 `BUDDY_TOKEN`。
24
24
 
25
25
  ### 1.3 搭子工程框架
26
26
 
27
27
  ```
28
28
  my-buddy/
29
- ├─ .env BUDDY_ID、BUDDY_TOKEN
29
+ ├─ .env BUDDY_TOKEN
30
30
  ├─ src/index.ts
31
- └─ package.json
31
+ └─ package.json buddyId 字段
32
+ ```
33
+
34
+ ```json
35
+ {
36
+ "name": "my-buddy",
37
+ "buddyId": "b_1234567890abcdef",
38
+ "private": true,
39
+ "type": "module",
40
+ "scripts": { "start": "node --env-file-if-exists=.env src/index.ts" }
41
+ }
32
42
  ```
33
43
 
34
44
  ```ts
@@ -49,44 +59,27 @@ const planMenu: AgentTool<typeof PLAN_MENU_PARAMS, undefined> = {
49
59
  }),
50
60
  };
51
61
 
52
- await BuddyServer.start({
53
- dataRequirements: [],
54
- buddy: () => ({
55
- initialState: {
56
- model: "kimi-k3",
57
- systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
58
- tools: [planMenu],
59
- },
60
- }),
61
- });
62
+ await BuddyServer.start(() => ({
63
+ initialState: {
64
+ model: "kimi-k3",
65
+ systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
66
+ tools: [planMenu],
67
+ },
68
+ }));
62
69
  ```
63
70
 
64
- 完整示例:[examples/aunt-tu/](examples/aunt-tu/)(提问、记忆、小挂件)、[examples/cardio/](examples/cardio/)(授权数据、主动服务、提问、记忆、小挂件)。
65
-
66
71
  ## 2. 配置
67
72
 
68
- ### 2.1 `BuddyServer.start(options)`
73
+ ### 2.1 `BuddyServer.start(buddy)`
69
74
 
70
75
  连上平台开始处理消息,返回 `{ stop(): Promise<void> }`。
71
76
 
72
- ```ts
73
- interface StartOptions {
74
- // 必填。这只搭子需要的用户数据及用途,见 3.4;不需要时传 []
75
- dataRequirements: { id: DataAccessDatasetId; purpose: string }[];
76
-
77
- // 必填。每场会话建起来时调一次,返回这场会话的配置,见 2.2、2.3
78
- buddy: (conversation: Conversation) => BuddyOptions | Promise<BuddyOptions>;
79
- }
80
- ```
81
-
82
- Runtime 启动时只在本地校验 `dataRequirements`,不会额外调用接口注册声明。Runtime 根据自身导出的能力目录补齐标题、来源和 Schema 版本;实际调用 `data_access_query`、发送 HealthKit 权限卡或申请使用数据的主动服务时,会把完整声明随业务请求携带给平台。平台在该次请求中更新用途版本并校验 Grant。
77
+ `buddy` 是每场会话建起来时调用一次的工厂函数,返回这场会话的配置,见 2.2、2.3。DataAccess 声明不属于会话工厂,单独写在工程根目录的 `data-access.json`,见 3.4。
83
78
 
84
- **身份与令牌不在参数里**:搭子 id 和令牌从环境变量 `BUDDY_ID`、`BUDDY_TOKEN` 读,缺一个就启动失败。本地写在工程的 `.env` 里(`npm start` `--env-file-if-exists=.env` 会读进来),平台托管时由网关注入同名变量,环境变量优先于 `.env`,发布上去不用改代码。
79
+ **buddyId token**:buddyId 从工程 `package.json` 顶层的 `buddyId` 字段读,token 从环境变量 `BUDDY_TOKEN` 读,缺一个都启动失败。创建搭子时会下发 buddyId 与开发 token:buddyId 写进 `package.json`,随工程进 git 与发布包,平台收包时会比对它与推送目标是否一致;开发 token 写在 `.env`,由启动命令带进环境变量。正式上线时平台托管注入正式 token 到同名变量,对开发者无感。
85
80
 
86
81
  **buddy 的生命周期**:`buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,需要存储会话状态可以根据场景使用 3.3 记忆、3.5 小挂件实现。
87
82
 
88
- **同一时间只有一个进程**:同一个搭子身份同一时间只有一个进程连着平台,本地调试用的 `<buddyId>-dev` 也一样。新进程连上平台后,原来那个进程会打一条 ERROR 日志说明被顶掉,然后退出、不再重连。本地不小心起了多个进程时,留下的是最后连上平台的那个。
89
-
90
83
  **buddy 的会话压缩**:会话历史长到一定程度时,运行时在会话被回收后把早先的对话压成一份摘要写回平台,下次重建时模型看到的是「摘要 + 最近的原文」。开发者不用做任何事,也没有开关。
91
84
 
92
85
  ### 2.2 `BuddyOptions`
@@ -216,8 +209,6 @@ interface Conversation {
216
209
  }
217
210
  ```
218
211
 
219
- `tools`、`memory`、`dataAccess`、`widget`、`proactive` 已经绑好这场会话,直接用,不用传令牌或拼接口路径。
220
-
221
212
  ## 3. 平台能力
222
213
 
223
214
  | 能力 | `conversation` 对象方法 | 模型工具(`conversation.tools` 上) |
@@ -241,11 +232,11 @@ buddy: (conversation) => ({
241
232
 
242
233
  ### 3.1 资料
243
234
 
244
- `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,把 `conversation.tools.resource_list` 和 `conversation.tools.resource_read` 放进 `initialState.tools`,模型先列出有哪些资料,再按需读。
235
+ 搭子随身带的只读资料:把 `.md` 放进工程目录下的 `resources/`,模型先列出有哪些资料,再按需读一篇。资料在进程启动时一次读好,整场会话、每个回合读到的都是同一份。
245
236
 
246
- - 工程目录从入口脚本往上找,与启动时的工作目录无关。
247
- - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 与点开头的文件和目录;资料改了要重启进程。
248
- - `resource_list` 列出全部资料的相对路径,`resource_read` 按路径读一篇。没有资料时 `resource_list` 回「没有资料」。
237
+ **准备工作**
238
+
239
+ 资料放在工程目录(`package.json` 所在目录)下的 `resources/`:
249
240
 
250
241
  ```
251
242
  my-buddy/ 工程目录
@@ -256,41 +247,63 @@ my-buddy/ 工程目录
256
247
  └─ src/index.ts 入口脚本
257
248
  ```
258
249
 
259
- **在自己的代码里用**
250
+ - 资料更改要重启进程,建议 watch `resources/` 实时重启。
260
251
 
261
- 调用 `conversation.resource`,不需要把这两个工具放进 `initialState.tools`:
252
+ **`conversation` 对象方法**
262
253
 
263
254
  | 方法 | 做什么 |
264
255
  |---|---|
265
256
  | `list()` | 全部资料的路径,相对 `resources/`,按路径排序 |
266
- | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填;没有这篇时抛异常 |
257
+ | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填 |
267
258
 
268
259
  ```ts
269
260
  const notes = conversation.resource.read("research/01-writings.md");
270
261
  ```
271
262
 
263
+ **模型工具**
264
+
265
+ 把 `conversation.tools.resource_list` 和 `conversation.tools.resource_read` 放进 `initialState.tools`,模型就能先列出有哪些资料,再按路径读一篇。`systemPrompt` 里交代清楚这只搭子带了哪类资料、什么时候该去翻。
266
+
267
+ **限制与异常**
268
+
272
269
  - 两个方法都是同步的,读的是进程启动时读好的那份。
270
+ - 没有资料时 `resource_list` 回「没有资料」。
271
+ - `read(file)` 读不存在的那篇时抛异常。
273
272
  - 主动服务的回合内也能调。
274
273
 
275
274
  ### 3.2 提问
276
275
 
277
- 让模型发一张题卡请用户选:一个问题加 2 到 4 个可以直接点的选项。私聊、群聊都能用。
276
+ 让模型发一张题卡请用户选:一个问题加 2 到 4 个可以直接点的选项。私聊、群聊都能用。题卡发出,这一轮就到此结束,等用户操作。
277
+
278
+ **`conversation` 对象方法**
279
+
280
+ 没有。`conversation` 不提供发题卡的方法,题卡只由模型发。
281
+
282
+ **模型工具**
278
283
 
279
284
  把 `conversation.tools.ask_question` 放进 `initialState.tools`,模型就能发题卡。
280
285
 
281
- - 参数是 `question`(题目,不超过 500 字)和 `options`(2 到 4 个,每个不超过 30 字,不能重复);不合规时模型会看到错误,改了再调。
282
- - 用户点选项,就是用选项原文发一条消息,跟自己打字一样;也可以不点,直接打字。
283
- - `conversation` 对象不提供发题卡的方法,题卡只由模型发。
286
+ - 参数是 `question`(题目,不超过 500 字)和 `options`(2 到 4 个,每个不超过 30 字,不能重复)。
287
+ - 题目和选项只写在参数里,回复正文里不再说一遍。
288
+ - 用户点选项,就是用选项原文发一条消息,跟自己打字一样;也可以不点,直接打字回答,或者说别的。
289
+
290
+ **限制与异常**
291
+
292
+ - 参数不合规时模型会看到错误,改了再调。
293
+ - 这一轮读过用户授权数据之后不能再发题卡,调用抛异常,异常信息模型能看到;原因见 3.4 的「数据只在当轮可见」。
294
+ - 主动服务的回合里照常能发,运行时不拦。
284
295
 
285
296
  ### 3.3 记忆
286
297
 
287
- 这场会话的长期记忆,存在平台上,只在本会话内可见,会话删除时一并清除。
298
+ 这场会话的长期记忆,搭子写给自己的印象,存在平台上,平台只存不读。只在本会话内可见,会话删除时一并清除。
288
299
 
289
- - `list()`:列出全部,按 `key` 字典序。
290
- - `write(key, text)`:覆盖写一条。
291
- - `delete(key)`:删一条,不存在也算成功。
300
+ **`conversation` 对象方法**
292
301
 
293
- `key` 非空、不含空白与 `/`、最长 64 字;`text` 单条不超过 16 KB;每场会话最多 64 条、合计不超过 256 KB。超限、`key` 不合法或网络出错时抛异常,异常信息是平台给出的原因,在工具里调用时模型能看到。
302
+ | 方法 | 做什么 |
303
+ |---|---|
304
+ | `list()` | 列出全部,按 `key` 字典序 |
305
+ | `write(key, text)` | 覆盖写一条 |
306
+ | `delete(key)` | 删一条,不存在也算成功 |
294
307
 
295
308
  ```ts
296
309
  interface MemoryEntry {
@@ -300,46 +313,50 @@ interface MemoryEntry {
300
313
  }
301
314
  ```
302
315
 
303
- 运行时不提供记忆工具,要让模型记东西,自己包一个:
316
+ **模型工具**
317
+
318
+ 运行时不提供记忆工具。要让模型自己记东西,按这只搭子要记什么,自己包一个:
304
319
 
305
320
  ```ts
306
321
  const REMEMBER_PARAMS = Type.Object({ topic: Type.String(), text: Type.String() });
307
322
 
308
- await BuddyServer.start({
309
- dataRequirements: [],
310
- buddy: (conversation) => {
311
- const remember: AgentTool<typeof REMEMBER_PARAMS, undefined> = {
312
- name: "remember",
313
- label: "正在记下",
314
- description: "把一条关于对方的印象记下来,同一主题覆盖旧的。",
315
- parameters: REMEMBER_PARAMS,
316
- execute: async (_id, { topic, text }) => {
317
- await conversation.memory.write(topic, text);
318
- return { content: [{ type: "text", text: "记下了。" }], details: undefined };
319
- },
320
- };
321
- return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [remember] } };
322
- },
323
+ await BuddyServer.start((conversation) => {
324
+ const remember: AgentTool<typeof REMEMBER_PARAMS, undefined> = {
325
+ name: "remember",
326
+ label: "正在记下",
327
+ description: "把一条关于对方的印象记下来,同一主题覆盖旧的。",
328
+ parameters: REMEMBER_PARAMS,
329
+ execute: async (_id, { topic, text }) => {
330
+ await conversation.memory.write(topic, text);
331
+ return { content: [{ type: "text", text: "记下了。" }], details: undefined };
332
+ },
333
+ };
334
+ return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [remember] } };
323
335
  });
324
336
  ```
325
337
 
338
+ **限制与异常**
339
+
340
+ - `key` 非空、不含空白与 `/`、最长 64 字;`text` 单条不超过 16 KB;每场会话最多 64 条、合计不超过 256 KB。
341
+ - 超限、`key` 不合法或网络出错时抛异常,异常信息是平台给出的原因,在工具里调用时模型能看到。
342
+ - 这一轮读过用户授权数据之后,运行时拒绝本回合的记忆写入,见 3.4 的「数据只在当轮可见」。
343
+ - 主动服务的回合里跟普通回合一样能读能写,运行时不拦。
344
+
326
345
  ### 3.4 用户授权数据
327
346
 
328
- 用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id
347
+ 用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id。查到的数据只在当轮可见,见本节末尾。
329
348
 
330
349
  这里有三层不同概念:
331
350
 
332
351
  1. **Runtime 能力目录**:当前 SDK 支持哪些 Dataset,由包导出的 `DATA_ACCESS_CAPABILITIES` 提供。
333
- 2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在 `BuddyServer.start.dataRequirements`。
352
+ 2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在工程根目录的 `data-access.json`。
334
353
  3. **用户 Grant**:用户是否同意这只搭子读取某个已声明 Dataset,由客户端授权流程管理,搭子不能代替用户授予。
335
354
 
336
- `data_access_query`、`data_access_request` 等平台工具放进 `initialState.tools`,只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权。
337
-
338
- 位置和日历是当前会话的一次性授权结果,通过 `data_access_request` 请求,不属于 Dataset,也不写进 `dataRequirements`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在 `dataRequirements` 中声明。
355
+ 位置和日历是当前会话的一次性授权结果,通过 `data_access_request` 请求,不属于 Dataset,也不写进 `data-access.json`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在配置中声明。
339
356
 
340
- **读取 Runtime 能力目录**
357
+ **准备工作**
341
358
 
342
- 开发者工具和项目代码直接从 Runtime 包导入,不需要网络请求:
359
+ Runtime 能力目录直接从 Runtime 包导入,开发者工具和项目代码都不需要网络请求:
343
360
 
344
361
  ```ts
345
362
  import {
@@ -357,25 +374,26 @@ const sleep = dataAccessCapability("health.sleep");
357
374
 
358
375
  这份导出是开发者可选 `id` 的权威来源。新增 Dataset 随 Runtime 版本发布;developer-platform、脚手架和预检都应读取该导出,不再维护一份手写枚举。
359
376
 
360
- **声明需要哪些数据**
361
-
362
- 声明写在 `BuddyServer.start` 的初始化参数里,Runtime 在本地展开成带标题、来源和 Schema 版本的完整声明:
377
+ 声明写在工程根目录(与 `package.json` 同级)的 `data-access.json`:
363
378
 
364
- ```ts
365
- await BuddyServer.start({
366
- dataRequirements: [
367
- { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
368
- ],
369
- buddy: () => ({ /* ... */ }),
370
- });
379
+ ```json
380
+ {
381
+ "requirements": [
382
+ { "id": "health.sleep", "purpose": "根据你最近的睡眠调整作息建议" }
383
+ ]
384
+ }
371
385
  ```
372
386
 
373
- - `id`:从 `DATA_ACCESS_CAPABILITIES` 中选,TypeScript 会收窄为 `DataAccessDatasetId`。
387
+ - `id`:从 `DATA_ACCESS_CAPABILITIES` 中选。
374
388
  - `purpose`:用途,1~200 字。用户看到这句话再决定是否同意。
375
- - Runtime 不在启动时注册声明;查询、HealthKit 权限卡和主动服务请求会携带当前完整声明。
376
- - Server 在实际请求时按声明用途和 Schema 计算版本;`id`、`purpose` Schema 都没变化时保留已有 Grant,变化后要求重新授权。
389
+ - `requirements` 最多 32 项,不允许重复;配置对象和每一项都不接受额外字段。
390
+ - 文件缺失等价于 `{ "requirements": [] }`。Runtime 启动时先严格校验,再把整份声明上传平台;即使为空也会上传,以清除旧声明。上传失败则启动失败。
391
+ - Server 在注册时按声明用途和 Schema 计算版本;`id`、`purpose` 与 Schema 都没变化时保留已有 Grant,变化后要求重新授权。
392
+ - 后续查询、HealthKit 权限卡和主动服务请求只发送 Dataset ID,不再重复携带用途声明。
377
393
  - 本地 `.env` 里放的是 CLI 发的开发令牌,进程连上平台后身份是 `<buddyId>-dev`,实际使用形成的授权上下文与正式搭子隔离。
378
394
 
395
+ 可以声明的 Dataset:
396
+
379
397
  | Dataset | 内容 | `data.items` 每条的字段 |
380
398
  |---|---|---|
381
399
  | `health.sleep` | 睡眠,每天一条 | `date`、`totalSleepMinutes`、`sleepStartLocal`、`sleepEndLocal`、`awakeMinutes`、`coreSleepMinutes`、`deepSleepMinutes`、`remSleepMinutes`、`unspecifiedSleepMinutes` |
@@ -389,34 +407,45 @@ await BuddyServer.start({
389
407
 
390
408
  部分字段可能缺失,取值前先判断。
391
409
 
392
- **模型工具与代码接口**
410
+ **`conversation` 对象方法**
411
+
412
+ 开发者自己的工具直接调用 `conversation.dataAccess`,不用把 `data_access_*` 放进 `initialState.tools`:
393
413
 
394
- - `data_access_query` / `conversation.dataAccess.query`:查询已授权 Dataset。
395
- - `data_access_request` / `conversation.dataAccess.request`:在私聊中发数据授权卡;发卡后当前回合结束,等待用户操作。
396
- - `data_access_read_result` / `conversation.dataAccess.readResult`:按授权卡的 `requestId` 读取一次性位置或日历结果;HealthKit 不走这个接口。
414
+ | 方法 | 做什么 |
415
+ |---|---|
416
+ | `query({ datasets, memberId? })` | 查询已授权 Dataset,每个 Dataset 各自一个状态 |
417
+ | `request({ kind, reason, datasets?, calendarDays?, idempotencyKey })` | 在私聊里发数据授权卡,`kind` 取 `healthkit`、`location` 或 `calendar` |
418
+ | `readResult(requestId)` | 按授权卡的 `requestId` 读一次性的位置或日历结果;HealthKit 不走这个接口 |
419
+
420
+ ```ts
421
+ const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
422
+ if (sleep.status === "available") {
423
+ // sleep.data.items 是每天一条的睡眠记录
424
+ }
425
+ ```
426
+
427
+ - `datasets`:一次最多 8 个。
428
+ - `memberId`:私聊不传;群聊必传,取群消息开头 `[名字 #m_…]` 里的 `m_…`,查这位成员的数据,传错时抛异常。
397
429
 
398
- 模型工具要从 `conversation.tools` 取来放进 `initialState.tools` 才能调用;没声明 Dataset 时照常注册,查询的每一项都回 `notDeclared`。开发者自己的工具直接调用 `conversation.dataAccess`,不用把这几个工具放进去。
430
+ **模型工具**
399
431
 
400
- **HealthKit:先查询,缺授权再发卡**
432
+ 把 `conversation.tools.data_access_query`、`data_access_request`、`data_access_read_result` 放进 `initialState.tools`,模型就能自己查数据、发授权卡。放进去只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权;没声明 Dataset 时照常注册,查询的每一项都回 `notDeclared`。
433
+
434
+ HealthKit 先查询,缺授权再发卡:
401
435
 
402
436
  ```ts
403
- await BuddyServer.start({
404
- dataRequirements: [
405
- { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
406
- ],
407
- buddy: (conversation) => ({
408
- initialState: {
409
- model: "kimi-k3",
410
- systemPrompt: [
411
- "聊到作息时,先用 data_access_query 查询 health.sleep。",
412
- "返回 notGranted 时,说明当前问题为什么需要数据,再用 data_access_request 发 HealthKit 授权卡。",
413
- "用户处理授权卡后会开启新回合;再次调用 data_access_query,不要调用 data_access_read_result。",
414
- "用户拒绝后不要在同一任务里反复申请。",
415
- ].join("\n"),
416
- tools: [conversation.tools.data_access_query, conversation.tools.data_access_request],
417
- },
418
- }),
419
- });
437
+ await BuddyServer.start((conversation) => ({
438
+ initialState: {
439
+ model: "kimi-k3",
440
+ systemPrompt: [
441
+ "聊到作息时,先用 data_access_query 查询 health.sleep。",
442
+ "返回 notGranted 时,说明当前问题为什么需要数据,再用 data_access_request 发 HealthKit 授权卡。",
443
+ "用户处理授权卡后会开启新回合;再次调用 data_access_query,不要调用 data_access_read_result。",
444
+ "用户拒绝后不要在同一任务里反复申请。",
445
+ ].join("\n"),
446
+ tools: [conversation.tools.data_access_query, conversation.tools.data_access_request],
447
+ },
448
+ }));
420
449
  ```
421
450
 
422
451
  完整链路:
@@ -431,11 +460,7 @@ data_access_query
431
460
  → available + data
432
461
  ```
433
462
 
434
- 查询接口不会把“从未授权”和“用途变化后需重新确认”分成两种模型状态,都会返回 `notGranted`;重新确认原因由用户侧授权页面展示。
435
-
436
- **位置和日历:读取一次性结果**
437
-
438
- 位置和日历不属于 Dataset,不写进 `dataRequirements`。模型先用 `data_access_request` 发卡,用户处理后,隐藏结果会带回这张卡的 `requestId`;再用 `data_access_read_result` 读取。结果只在平台短期保存,读不到时返回 `unavailable`。
463
+ 位置和日历不属于 Dataset,不写进 `data-access.json`。模型先用 `data_access_request` 发卡,用户处理后,隐藏结果会带回这张卡的 `requestId`;再用 `data_access_read_result` 读取。结果只在平台短期保存,读不到时返回 `unavailable`。
439
464
 
440
465
  ```text
441
466
  data_access_request { kind: "location", reason: "查找你附近的地点" }
@@ -445,19 +470,9 @@ data_access_request { kind: "location", reason: "查找你附近的地点" }
445
470
  → available + data,或 unavailable
446
471
  ```
447
472
 
448
- `calendar` 还可传 `calendarDays`,范围为 1~31,默认 7。授权请求第一期只支持私聊;群聊需要引导目标成员去私聊操作。
449
-
450
- **开发者代码查询 Dataset**
451
-
452
- ```ts
453
- const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
454
- if (sleep.status === "available") {
455
- // sleep.data.items 是每天一条的睡眠记录
456
- }
457
- ```
473
+ `calendar` 还可传 `calendarDays`,范围为 1~31,默认 7
458
474
 
459
- - `datasets`:一次最多 8 个;响应超过 512 KiB 时抛异常,减少 Dataset 再查。
460
- - `memberId`:私聊不传;群聊必传,取群消息开头 `[名字 #m_…]` 里的 `m_…`,查这位成员的数据,传错时抛异常。
475
+ **限制与异常**
461
476
 
462
477
  每个 Dataset 各自一个状态,只有 `available` 带 `data`:
463
478
 
@@ -469,22 +484,27 @@ if (sleep.status === "available") {
469
484
  | `disabledInGroup` | 这位成员在这个群里关掉了这项数据 |
470
485
  | `unavailable` | 平台上还没有这位用户的这项数据,或数据已过期 |
471
486
 
487
+ - 查询接口不会把「从未授权」和「用途变化后需重新确认」分成两种模型状态,都会返回 `notGranted`;重新确认原因由用户侧授权页面展示。
488
+ - 一次查询的响应超过 512 KiB 时抛异常,减少 Dataset 再查。
489
+ - 发出授权卡后当前回合结束,等待用户操作。
490
+ - 授权请求第一期只支持私聊;群聊需要引导目标成员去私聊操作。
491
+
472
492
  **数据只在当轮可见**
473
493
 
474
- 查到的数据只用于这一轮回复:平台不存原文,搭子进程里的那份也在这一轮结束时被抹掉,下一轮读不到。凡是可能把它带出去的地方——工具结果、工具参数、那一步的正文和心声——这一轮统统不写进平台。
494
+ 查到的数据只用于这一轮回复:平台不存原文,搭子进程里的那份也在这一轮结束时被抹掉,下一轮读不到。Runtime 按步骤追踪数据流:读取数据之前已经生成的工具步骤正常保留;DataAccess 结果只保留 Dataset 状态;模型看到数据后生成的工具参数、工具结果、工具步骤正文和心声不写进平台,最终给用户看的普通回复只保留正文、不保留心声。
475
495
 
476
- 这么设计是为了**用户撤销授权时能真的收回**:数据没有留在任何地方,不需要事后去清。代价是这一轮的记录不完整,搭子重启后读不到自己当时的推理和工具参数。
496
+ 这么设计是为了**用户撤销授权时能真的收回**:确保模型不会记住授权的具体数据。
477
497
 
478
498
  由此有两条实操结论:
479
499
 
480
500
  - **想让某个结论以后还在场,就在回复里说出来**(用户也会看到)。模型对这些数据的记忆只能活在它说过的话里——下次它读到的是「查了 health.sleep,可用」,不是具体数字。
481
- - **别把查到的数据写进记忆、小挂件或别的工具的参数**。那些地方不脱敏,写进去就永久留下了。
501
+ - **不要把查到的数据写进记忆、题卡、小挂件或别的工具参数**。Runtime 会直接拒绝当前回合的记忆写入和题卡发送,并脱敏后续工具调用;其他开发者自定义持久化能力仍需自行遵守这条边界。
482
502
 
483
503
  ### 3.5 小挂件
484
504
 
485
- 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widget`,都能建小挂件、读整个或一个模块、改标题、写记录、删记录、删整个小挂件,并把它发进聊天。
505
+ 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。建小挂件、读整个或一个模块、改标题、写记录、删记录、删整个小挂件,并把它发进聊天,模型和开发者代码都能做。
486
506
 
487
- **放进工程目录**
507
+ **准备工作**
488
508
 
489
509
  一个小挂件是工程目录下 `widgets/` 里的一个子目录,目录名就是小挂件类型 id。
490
510
 
@@ -498,18 +518,22 @@ my-buddy/ 工程目录
498
518
  └─ src/index.ts
499
519
  ```
500
520
 
501
- **让模型使用**
521
+ 资料小挂件要重启进程,建议 watch `widgets/` 实时重启。
502
522
 
503
- 把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`。
523
+ **`conversation` 对象方法**
504
524
 
505
- - 工具说明里附上各类型的模块与字段。改了 `widgets/` 要重启进程。
506
- - 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
507
- - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
508
- - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
509
-
510
- **在自己的代码里用**
511
-
512
- 调用 `conversation.widget`,不需要把 `widget_*` 放进工具:
525
+ | 方法 | 做什么 |
526
+ |---|---|
527
+ | `list()` | 列出这场会话里的小挂件,不带记录 |
528
+ | `create({ type, title, idempotencyKey? })` | 新建一个,回 `{ id }`;同一个 `idempotencyKey` 重复建回同一个 id |
529
+ | `read(id)` | 读整个小挂件,含各模块的记录 |
530
+ | `readModule(id, module)` | 读一个模块,回 `{ records }` |
531
+ | `rename(id, title)` | 改标题,id 不变,聊天里发过的卡片跟着显示新标题 |
532
+ | `writeRecord(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖,回 `{ revision }`;带 `expectedRevision` 时版本号对不上,平台回 409 |
533
+ | `deleteRecord(id, module, recordId)` | 删一条记录 |
534
+ | `delete(id)` | 删整个小挂件连记录;聊天里发过的卡片留着,点开显示已删除 |
535
+ | `setPresentation(id, { summary, imageMediaId })` | 设聊天里卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 |
536
+ | `send(id, { idempotencyKey })` | 发进聊天:放进发送队列就返回,不等平台确认送达;同一个 `idempotencyKey` 重复发只落一条 |
513
537
 
514
538
  ```ts
515
539
  execute: async (toolCallId, params) => {
@@ -520,18 +544,16 @@ execute: async (toolCallId, params) => {
520
544
  },
521
545
  ```
522
546
 
523
- | 方法 | 做什么 | 平台接口 |
524
- |---|---|---|
525
- | `list()` | 列出这场会话里的小挂件,不带记录 | `GET /internal/v1/widgets` |
526
- | `create({ type, title, idempotencyKey? })` | 新建一个,回 `{ id }`;同一个 `idempotencyKey` 重复建回同一个 id | `POST /internal/v1/widgets` |
527
- | `read(id)` | 读整个小挂件,含各模块的记录 | `GET /internal/v1/widgets/:widgetId` |
528
- | `readModule(id, module)` | 读一个模块,回 `{ records }` | `GET /internal/v1/widgets/:widgetId/:module` |
529
- | `rename(id, title)` | 改标题,id 不变,聊天里发过的卡片跟着显示新标题 | `PUT /internal/v1/widgets/:widgetId` |
530
- | `writeRecord(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖,回 `{ revision }`;带 `expectedRevision` 时版本号对不上,平台回 409 | `PUT /internal/v1/widgets/:widgetId/:module/:recordId` |
531
- | `deleteRecord(id, module, recordId)` | 删一条记录 | `DELETE /internal/v1/widgets/:widgetId/:module/:recordId` |
532
- | `delete(id)` | 删整个小挂件连记录;聊天里发过的卡片留着,点开显示已删除 | `DELETE /internal/v1/widgets/:widgetId` |
533
- | `setPresentation(id, { summary, imageMediaId })` | 设聊天里卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 | `PUT /internal/v1/widgets/:widgetId/presentation` |
534
- | `send(id, { idempotencyKey })` | 发进聊天:放进发送队列就返回,不等平台确认送达;同一个 `idempotencyKey` 重复发只落一条 | `POST /internal/v1/messages` |
547
+ **模型工具**
548
+
549
+ 把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`,模型就能自己建、读、改、发小挂件。
550
+
551
+ - 工具说明里附上各类型的模块与字段,模型不用另外交代形状。
552
+ - 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
553
+ - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
554
+ - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
555
+
556
+ **限制与异常**
535
557
 
536
558
  - `list` 和 `read` 回的 `canEdit` 为 `false` 的,是别的会话转发来的引用,只能读;改名、写记录、删除、设卡片时平台回 403。
537
559
  - `data` 要符合 `schema.json` 里这个模块的 `record`,不符合时平台回 400。
@@ -546,9 +568,17 @@ execute: async (toolCallId, params) => {
546
568
  2. 触发条件满足,且没被免打扰时段、冷却时间、每日上限拦下时,平台向搭子发起一次主动服务的回合。
547
569
  3. 模型判断要不要开口:要就直接写出发给用户的消息,平台在落库那一刻再过一遍闸门,过了才发给用户,拦下就不发;不要就输出跳过标记,这一轮到此结束,用户什么也看不到。
548
570
 
549
- 订阅有两种方式,二选一或并用。字段一样(见下表),产物都是一条订阅,只是谁决定何时订阅不同:
571
+ **`conversation` 对象方法**
550
572
 
551
- - 你的代码显式调 `conversation.proactive.subscribe`。它挂在这场会话的 `conversation` 上,凡是拿得到 `conversation` 的地方都能调:自己工具的 `execute` 里、`beforeToolCall` / `afterToolCall` 里、配置工厂函数体里:
573
+ | 方法 | 做什么 |
574
+ |---|---|
575
+ | `subscribe(key, request)` | 按稳定 `key` 创建或更新一条订阅,相同 `key` 更新原订阅并重新启用 |
576
+ | `list()` | 当前会话的全部订阅,包括已关闭项 |
577
+ | `unsubscribe(key)` | 关闭指定订阅,回是否找到 |
578
+ | `unsubscribeAll()` | 关闭全部 active 订阅,回关掉几条 |
579
+ | `isRunning()` | 现在是不是主动服务的回合 |
580
+
581
+ 它挂在这场会话的 `conversation` 上,凡是拿得到 `conversation` 的地方都能调:自己工具的 `execute` 里、`beforeToolCall` / `afterToolCall` 里、配置工厂函数体里。
552
582
 
553
583
  ```ts
554
584
  await conversation.proactive.subscribe("weekly-review", {
@@ -558,20 +588,7 @@ await conversation.proactive.subscribe("weekly-review", {
558
588
  });
559
589
  ```
560
590
 
561
- - 模型决定是否订阅或关闭时,把需要的 `proactive_*` 工具放进 `initialState.tools`。什么场景可以订阅、何时只能查询或关闭,要在 `systemPrompt` 里说清楚:
562
-
563
- ```ts
564
- await BuddyServer.start({
565
- dataRequirements: [{ id: "health.workouts", purpose: "跑完后帮你复盘这次训练" }],
566
- buddy: (conversation) => ({
567
- initialState: {
568
- model: "kimi-k3",
569
- systemPrompt: "你是跑步搭子。用户表达想坚持训练时,可以用 proactive_subscribe 订阅跑后复盘;用户要求关闭时先查询再关闭。",
570
- tools: [conversation.tools.proactive_subscribe, conversation.tools.proactive_list, conversation.tools.proactive_unsubscribe, conversation.tools.data_access_query],
571
- },
572
- }),
573
- });
574
- ```
591
+ `subscribe` 的字段:
575
592
 
576
593
  | 字段 | 说明 |
577
594
  |---|---|
@@ -583,11 +600,28 @@ await BuddyServer.start({
583
600
  | `cooldownMinutes` | 可选,冷却时间,缺省 120,取 0~10080 |
584
601
  | `dailyLimit` | 可选,每天最多几次,缺省 3,取 1~20 |
585
602
 
586
- `list()` 返回当前会话的全部订阅(包括已关闭项);`unsubscribe(key)` 关闭指定订阅并返回是否找到;`unsubscribeAll()` 关闭全部 active 订阅并返回数量。关闭时尚未完成的主动 Run 会同步取消。
603
+ **模型工具**
604
+
605
+ 让模型自己决定是否订阅或关闭时,把需要的 `conversation.tools.proactive_*` 放进 `initialState.tools`,字段与上面那张表一样。什么场景可以订阅、何时只能查询或关闭,要在 `systemPrompt` 里说清楚:
606
+
607
+ ```ts
608
+ await BuddyServer.start((conversation) => ({
609
+ initialState: {
610
+ model: "kimi-k3",
611
+ systemPrompt: "你是跑步搭子。用户表达想坚持训练时,可以用 proactive_subscribe 订阅跑后复盘;用户要求关闭时先查询再关闭。",
612
+ tools: [conversation.tools.proactive_subscribe, conversation.tools.proactive_list, conversation.tools.proactive_unsubscribe, conversation.tools.data_access_query],
613
+ },
614
+ }));
615
+ ```
616
+
617
+ 代码订阅和模型订阅二选一或并用,产物都是一条订阅,只是谁决定何时订阅不同。
618
+
619
+ **限制与异常**
587
620
 
588
- 字段不合法、在群聊里调用,或同一用户对这只搭子已有 20 个 active 订阅时抛异常。
621
+ - 字段不合法、在群聊里调用,或同一用户对这只搭子已有 20 个 active 订阅时抛异常。
622
+ - 关闭订阅时,尚未完成的主动服务会同步取消。
589
623
 
590
- #### 主动服务的回合,不允许使用部分模型工具
624
+ **主动服务的回合,不允许使用部分模型工具**
591
625
 
592
626
  主动服务的回合里工具清单一个字都不动,平台工具和你自己的工具都照常执行,运行时不按回合性质拦任何一个。有时我们想禁用部分模型工具,比如一些写操作,可以使用 `beforeToolCall` 的钩子函数,用 `conversation.proactive.isRunning()` 能判断当前是不是主动服务的回合:
593
627
 
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/conversation/agent.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAExF,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7D,OAAO,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAiB,MAAM,aAAa,CAAC;AAE5I,MAAM,WAAW,SAAS;IACzB,mCAAmC;IACnC,cAAc,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,uDAAuD;IACvD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,WAAW,CAAC;IACjB,oCAAoC;IACpC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACnD;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,EAAE,sBAAsB,CAAC;IACxC,iFAAiF;IACjF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,kFAAkF;AAClF,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAmMxE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iHAAuB,CAAC;AAEtD,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAErF;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQxD"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/conversation/agent.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAExF,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7D,OAAO,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAKrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAiB,MAAM,aAAa,CAAC;AAE5I,MAAM,WAAW,SAAS;IACzB,mCAAmC;IACnC,cAAc,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,CAAC;IAClB,uEAAuE;IACvE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,uDAAuD;IACvD,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,gBAAgB,EAAE,SAAS,eAAe,EAAE,CAAC;IAC7C,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,WAAW,CAAC;IACjB,oCAAoC;IACpC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACnD;AAED,wDAAwD;AACxD,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,eAAe,EAAE,sBAAsB,CAAC;IACxC,iFAAiF;IACjF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,kFAAkF;AAClF,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAwMxE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iHAAuB,CAAC;AAEtD,iEAAiE;AACjE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAErF;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQxD"}
@@ -43,7 +43,7 @@ export async function createAgent(deps) {
43
43
  proactive_unsubscribe_all: proactiveUnsubscribeAll,
44
44
  resource_list: resourceListTool(deps.resources),
45
45
  resource_read: resourceReadTool(deps.resources),
46
- ask_question: askQuestionTool(deps.enqueueMessage, waitForUser),
46
+ ask_question: askQuestionTool(deps.enqueueMessage, waitForUser, dataAccessUsage),
47
47
  data_access_query: dataAccessTool(client, deps.key, dataAccessUsage, deps.dataRequirements),
48
48
  data_access_request: dataAccessRequest,
49
49
  data_access_read_result: dataAccessReadResult,
@@ -73,15 +73,20 @@ export async function createAgent(deps) {
73
73
  },
74
74
  memory: {
75
75
  list: () => client.memoryList(deps.key),
76
- write: (name, text) => client.memoryWrite(deps.key, name, text),
76
+ write: async (name, text) => {
77
+ if (dataAccessUsage.isPersistenceTainted()) {
78
+ throw new Error("本回合已经读取用户授权数据,不能写入长期记忆;请只在当前回复中使用,并避免复述敏感数值。");
79
+ }
80
+ await client.memoryWrite(deps.key, name, text);
81
+ },
77
82
  delete: (name) => client.memoryDelete(deps.key, name),
78
83
  },
79
84
  // 经 dataAccessUsage 查:开发者工具里查过的那一轮,写回平台和留在历史里的内容也会脱敏;主动服务回合里改走执行令牌
80
85
  dataAccess: {
81
- query: ({ datasets, memberId }) => dataAccessUsage.query(client, deps.key, datasets, deps.dataRequirements, memberId),
86
+ query: ({ datasets, memberId }) => dataAccessUsage.query(client, deps.key, datasets, memberId),
82
87
  request: async (input) => {
83
88
  const validated = validateDataAccessRequest(input, deps.key, deps.dataRequirements);
84
- deps.enqueueMessage({ type: "permissionRequest", permissionRequest: validated, dataRequirements: deps.dataRequirements.map(({ id, purpose }) => ({ id, purpose })), text: validated.reason, idempotencyKey: input.idempotencyKey });
89
+ deps.enqueueMessage({ type: "permissionRequest", permissionRequest: validated, text: validated.reason, idempotencyKey: input.idempotencyKey });
85
90
  waitForUser();
86
91
  },
87
92
  readResult: async (requestId) => {
@@ -93,7 +98,7 @@ export async function createAgent(deps) {
93
98
  proactive: {
94
99
  subscribe: (key, request) => {
95
100
  const validated = validateProactiveSubscribe(key, request, deps.dataRequirements);
96
- return client.subscribeProactive(deps.key, validated.key, validated.request, deps.dataRequirements);
101
+ return client.subscribeProactive(deps.key, validated.key, validated.request);
97
102
  },
98
103
  list: () => client.listProactive(deps.key),
99
104
  unsubscribe: (key) => client.unsubscribeProactive(deps.key, key),