@my-life-buddies/buddy-runtime 0.9.0 → 0.10.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 (62) hide show
  1. package/README.md +200 -102
  2. package/dist/conversation/agent.d.ts +10 -4
  3. package/dist/conversation/agent.d.ts.map +1 -1
  4. package/dist/conversation/agent.js +93 -68
  5. package/dist/conversation/agent.js.map +1 -1
  6. package/dist/conversation/compaction.d.ts +2 -2
  7. package/dist/conversation/compaction.d.ts.map +1 -1
  8. package/dist/conversation/compaction.js +1 -1
  9. package/dist/conversation/compaction.js.map +1 -1
  10. package/dist/conversation/index.d.ts +12 -10
  11. package/dist/conversation/index.d.ts.map +1 -1
  12. package/dist/conversation/index.js +20 -20
  13. package/dist/conversation/index.js.map +1 -1
  14. package/dist/dataAccessCatalog.d.ts +56 -0
  15. package/dist/dataAccessCatalog.d.ts.map +1 -0
  16. package/dist/dataAccessCatalog.js +18 -0
  17. package/dist/dataAccessCatalog.js.map +1 -0
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -0
  21. package/dist/index.js.map +1 -1
  22. package/dist/mlbClient.d.ts +5 -7
  23. package/dist/mlbClient.d.ts.map +1 -1
  24. package/dist/mlbClient.js +6 -18
  25. package/dist/mlbClient.js.map +1 -1
  26. package/dist/model.d.ts +8 -1
  27. package/dist/model.d.ts.map +1 -1
  28. package/dist/model.js +12 -6
  29. package/dist/model.js.map +1 -1
  30. package/dist/server.d.ts.map +1 -1
  31. package/dist/server.js +18 -24
  32. package/dist/server.js.map +1 -1
  33. package/dist/tools/dataAccess.d.ts +1 -1
  34. package/dist/tools/dataAccess.d.ts.map +1 -1
  35. package/dist/tools/dataAccess.js +4 -4
  36. package/dist/tools/dataAccess.js.map +1 -1
  37. package/dist/tools/dataAccessRequest.d.ts +7 -0
  38. package/dist/tools/dataAccessRequest.d.ts.map +1 -0
  39. package/dist/tools/{permissions.js → dataAccessRequest.js} +7 -7
  40. package/dist/tools/dataAccessRequest.js.map +1 -0
  41. package/dist/tools/registry.d.ts +1 -4
  42. package/dist/tools/registry.d.ts.map +1 -1
  43. package/dist/tools/registry.js +1 -15
  44. package/dist/tools/registry.js.map +1 -1
  45. package/dist/tools/resource.d.ts +4 -3
  46. package/dist/tools/resource.d.ts.map +1 -1
  47. package/dist/tools/resource.js +18 -13
  48. package/dist/tools/resource.js.map +1 -1
  49. package/dist/tools/{widgets.d.ts → widget.d.ts} +6 -4
  50. package/dist/tools/widget.d.ts.map +1 -0
  51. package/dist/tools/{widgets.js → widget.js} +13 -9
  52. package/dist/tools/widget.js.map +1 -0
  53. package/dist/types.d.ts +52 -33
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/types.js +5 -3
  56. package/dist/types.js.map +1 -1
  57. package/package.json +2 -2
  58. package/dist/tools/permissions.d.ts +0 -7
  59. package/dist/tools/permissions.d.ts.map +0 -1
  60. package/dist/tools/permissions.js.map +0 -1
  61. package/dist/tools/widgets.d.ts.map +0 -1
  62. package/dist/tools/widgets.js.map +0 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install @my-life-buddies/buddy-runtime typebox
14
14
 
15
15
  ### 1.2 注册搭子
16
16
 
17
- 通过平台开发者接口 `POST /cli/buddies` 注册,提交名称、头像、简介,平台分配搭子 id 与令牌;之后改档案用 `PUT /cli/buddies/:id/meta`。可选的模型 id `GET /cli/models`,可声明的数据能力查 `GET /cli/data-access/capabilities`。
17
+ 通过平台开发者接口 `POST /cli/buddies` 注册,提交名称、头像、简介,平台分配搭子 id 与令牌;之后改档案用 `PUT /cli/buddies/:id/meta`。用哪个模型在 `initialState.model` 里选,见 2.2。可声明的数据能力由 Runtime 导出的 `DATA_ACCESS_CAPABILITIES` 提供。
18
18
 
19
19
  ### 1.3 搭子工程框架
20
20
 
@@ -26,8 +26,8 @@ my-buddy/
26
26
  ```
27
27
 
28
28
  ```ts
29
- import type { AgentTool } from "@earendil-works/pi-agent-core";
30
29
  import { BuddyServer } from "@my-life-buddies/buddy-runtime";
30
+ import type { AgentTool } from "@my-life-buddies/buddy-runtime";
31
31
  import { Type } from "typebox";
32
32
 
33
33
  const PLAN_MENU_PARAMS = Type.Object({ date: Type.String() });
@@ -44,10 +44,10 @@ const planMenu: AgentTool<typeof PLAN_MENU_PARAMS, undefined> = {
44
44
  };
45
45
 
46
46
  await BuddyServer.start({
47
- model: "kimi-k3",
48
47
  dataRequirements: [],
49
48
  buddy: () => ({
50
49
  initialState: {
50
+ model: "kimi-k3",
51
51
  systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
52
52
  tools: [planMenu],
53
53
  },
@@ -83,14 +83,17 @@ await BuddyServer.start({
83
83
 
84
84
  连上平台开始处理消息,返回 `{ stop(): Promise<void> }`。
85
85
 
86
- | 参数 | 说明 |
87
- |---|---|
88
- | `model` | 必填,模型 id,取值见 `GET /cli/models`,进程内所有会话共用。没填或运行时不认识这个 id 时启动失败 |
89
- | `dataRequirements` | 必填,这只搭子需要的用户数据及用途;不需要时传 `[]`。Runtime 启动时整份上传平台,失败则不启动 |
90
- | `buddy` | 必填,`(conversation: Conversation) => BuddyOptions \| Promise<BuddyOptions>`,见 2.2、2.3 |
91
- | `errorReply` | 可选,模型出错或空回复时发的兜底回复 |
86
+ ```ts
87
+ interface StartOptions {
88
+ // 必填。这只搭子需要的用户数据及用途,见 3.4;不需要时传 []
89
+ dataRequirements: { id: DataAccessDatasetId; purpose: string }[];
92
90
 
93
- Runtime 启动时先校验 `dataRequirements`,再凭搭子令牌把整份声明上传平台;平台补齐 Dataset 的标题、来源、Schema 版本和声明版本。上传失败时启动直接失败,不连接事件流,也不会出现“进程在线但没有正确数据权限”的半成品状态。
91
+ // 必填。每场会话建起来时调一次,返回这场会话的配置,见 2.2、2.3
92
+ buddy: (conversation: Conversation) => BuddyOptions | Promise<BuddyOptions>;
93
+ }
94
+ ```
95
+
96
+ Runtime 启动时只在本地校验 `dataRequirements`,不会额外调用接口注册声明。Runtime 根据自身导出的能力目录补齐标题、来源和 Schema 版本;实际调用 `data_access_query`、发送 HealthKit 权限卡或申请使用数据的主动服务时,会把完整声明随业务请求携带给平台。平台在该次请求中更新用途版本并校验 Grant。
94
97
 
95
98
  用户新加搭子、新建群时,搭子会先开口打个招呼,跟平时回复一样调模型、逐字出现。开场白怎么说,写在 system prompt 里。
96
99
 
@@ -100,37 +103,52 @@ Runtime 启动时先校验 `dataRequirements`,再凭搭子令牌把整份声
100
103
 
101
104
  ### 2.2 `BuddyOptions`
102
105
 
103
- `buddy` 的返回值。字段名和签名取自 pi-agent-core 的 `AgentOptions` / `AgentState`,详见 pi 的文档。
106
+ `buddy` 的返回值。
104
107
 
105
108
  ```ts
106
- type BuddyOptions = Pick<AgentOptions,
107
- | "beforeToolCall" | "afterToolCall"
108
- | "shouldStopAfterTurn" | "prepareNextTurnWithContext"
109
- | "thinkingBudgets" | "toolExecution"
110
- > & {
111
- platformTools?: readonly PlatformToolName[];
112
- proactiveTools?: readonly string[];
113
- initialState: Pick<AgentState, "systemPrompt"> & Partial<Pick<AgentState, "tools" | "thinkingLevel">>;
114
- };
115
- ```
109
+ import type {
110
+ AfterToolCallContext,
111
+ AfterToolCallResult,
112
+ AgentTool,
113
+ BeforeToolCallContext,
114
+ BeforeToolCallResult,
115
+ ModelId,
116
+ ShouldStopAfterTurnContext,
117
+ ThinkingLevel,
118
+ } from "@my-life-buddies/buddy-runtime";
119
+
120
+ interface BuddyOptions {
121
+ // 必填。这场会话的初始状态
122
+ initialState: {
123
+ // 必填。模型 id,ModelId 是 "kimi-k2.6" | "kimi-k3" | "deepseek-v4-flash" | "deepseek-v4-pro"。每场会话可以选不同的模型
124
+ model: ModelId;
125
+
126
+ // 必填。人设与做事规则。为保证模型缓存,只放不随回合变化的内容
127
+ systemPrompt: string;
128
+
129
+ // 可选。模型只看得到这里的工具;平台工具从 conversation.tools 里取来放进去,见第 3 节
130
+ tools?: AgentTool[];
131
+
132
+ // 可选,默认 "off"。ThinkingLevel 是 "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"。
133
+ // 模型不支持的级别在请求时就近换成支持的一档,建会话时打一条 warn
134
+ thinkingLevel?: ThinkingLevel;
135
+ };
116
136
 
117
- | 字段 | 说明 |
118
- |---|---|
119
- | `initialState.systemPrompt` | 必填,人设与做事规则,只放不随回合变化的内容。模型实际收到的系统提示见第 4 节 |
120
- | `initialState.tools` | 可选,pi 的 `AgentTool` 数组 |
121
- | `initialState.thinkingLevel` | 可选,默认 `off`。模型不支持的级别在请求时就近换成支持的一档,建会话时打一条 warn |
122
- | `platformTools` | 可选,给模型注册哪些平台工具,默认 `[]`,见第 3 节 |
123
- | `proactiveTools` | 可选,主动回合里还能用哪些工具,默认 `[]`(一个都不给)。写工具名,`platformTools` 选中的和 `initialState.tools` 里自己的都行;写了没注册的名字建会话时抛异常。见 3.6 |
124
- | 其余字段 | 原样交给 pi;`beforeToolCall` 抛异常时按放行处理 |
137
+ // 可选。模型出错或一句话没说时发的兜底回复,用户和模型之后都看得到;主动回合不发
138
+ errorReply?: string;
125
139
 
126
- 写工具时注意:
140
+ // 可选。工具执行前调用,context 里有这次的 toolCall、校验过的 args 和当前上下文。
141
+ // 返回 { block: true, reason } 时工具不执行,模型看到 reason;抛异常时工具也不执行,模型看到异常信息
142
+ beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
127
143
 
128
- - `label` 必填,是工具执行时用户看到的状态文案,空字符串表示不显示。
129
- - `execute` 的返回值必须带 `details`,可以是 `undefined`;失败直接抛异常,模型能看到异常信息。
130
- - 先单独声明为 `AgentTool<typeof 参数 schema>` 再放进数组,`execute` 的参数才有类型;直接写在数组里时参数类型是 `unknown`。
131
- - 工具名不能与平台工具或其他工具重复。
144
+ // 可选。工具执行完调用,context 里带着执行结果;返回的 content、details、isError 会替换结果里的同名字段,抛异常时结果换成异常信息
145
+ afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
132
146
 
133
- 模型、密钥与会话历史由运行时管理,不在配置里。
147
+ // 可选。模型说完一步(这一步的工具也跑完)后调用,返回 true 就不再请求模型。
148
+ // 这一步发出了权限请求卡或题卡时,这一轮直接停下,不调它
149
+ shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext, signal?: AbortSignal) => boolean | Promise<boolean>;
150
+ }
151
+ ```
134
152
 
135
153
  ### 2.3 `Conversation`
136
154
 
@@ -144,8 +162,23 @@ interface Conversation {
144
162
  // 日志,每行开头带搭子 id
145
163
  log: BuddyLogger;
146
164
 
165
+ // 平台工具,见第 3 节:运行时造好、绑好这场会话,挑要用的放进 initialState.tools
166
+ tools: {
167
+ resource_list: AgentTool;
168
+ resource_read: AgentTool;
169
+ ask_question: AgentTool;
170
+ data_access_query: AgentTool;
171
+ data_access_request: AgentTool;
172
+ data_access_read_result: AgentTool;
173
+ widget_create: AgentTool;
174
+ widget_write: AgentTool;
175
+ widget_read: AgentTool;
176
+ widget_delete: AgentTool;
177
+ widget_send: AgentTool;
178
+ };
179
+
147
180
  // 资料,见 3.1:resources/ 里的 .md,进程启动时读好,所有会话共用
148
- resources: {
181
+ resource: {
149
182
  list(): string[];
150
183
  read(file: string): string;
151
184
  };
@@ -160,10 +193,12 @@ interface Conversation {
160
193
  // 授权数据,见 3.4:私聊不传 memberId,群聊传消息前缀里的 m_…
161
194
  dataAccess: {
162
195
  query(request: { datasets: string[]; memberId?: string }): Promise<DatasetAccessResult[]>;
196
+ request(input: DataAccessRequestInput & { idempotencyKey: string }): Promise<void>;
197
+ readResult(requestId: string): Promise<DataAccessReadResult>;
163
198
  };
164
199
 
165
200
  // 小挂件,见 3.5
166
- widgets: {
201
+ widget: {
167
202
  list(): Promise<WidgetSummary[]>;
168
203
  create(input: { type: string; title: string; idempotencyKey?: string }): Promise<{ id: string }>;
169
204
  read(id: string): Promise<Widget>;
@@ -179,34 +214,44 @@ interface Conversation {
179
214
  send(id: string, options: { idempotencyKey: string }): void;
180
215
  };
181
216
 
182
- // 主动服务,见 3.6:提订阅建议,用户在 App 里确认后才生效
217
+ // 主动服务,见 3.6:propose 提订阅建议,用户在 App 里确认后才生效;isRunning 判断现在是不是主动回合
183
218
  proactive: {
184
219
  propose(request: ProactiveSubscriptionRequest): Promise<{ id: string; status: "pending" }>;
220
+ isRunning(): boolean;
185
221
  };
186
222
  }
187
223
  ```
188
224
 
189
- `memory`、`dataAccess`、`widgets`、`proactive` 已经绑好这场会话,直接调用,不用传令牌或拼接口路径。
225
+ `tools`、`memory`、`dataAccess`、`widget`、`proactive` 已经绑好这场会话,直接用,不用传令牌或拼接口路径。
190
226
 
191
227
  ## 3. 平台能力
192
228
 
193
- | 能力 | `conversation` 对象方法 | 默认注册成的模型工具 | 默认注册成模型工具的条件 |
194
- |---|---|---|---|
195
- | 资料 | `conversation.resources.list` / `conversation.resources.read` | `resource_read` | platformTools 声明 && `resources/` 下有 `.md` |
196
- | 提问 | 无 | `ask_question` | platformTools 声明 |
197
- | 记忆 | `conversation.memory` | 无,默认不注册成模型工具,开发者可以根据具体场景,注册成适合场景需要的模型工具 | — |
198
- | 授权数据 | `conversation.dataAccess.query` | `data_access_query` | platformTools 声明 && 声明过至少一个 Dataset |
199
- | 会话内权限 | `conversation.permissions.request` / `conversation.permissions.readResult` | `permission_request` / `permission_result_read` | platformTools 声明;第一期仅私聊 |
200
- | 小挂件 | `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 声明 && `widgets/` 下有小挂件 |
201
- | 主动服务 | `conversation.proactive.propose` | 无 | — |
229
+ | 能力 | `conversation` 对象方法 | 模型工具(`conversation.tools` 上) |
230
+ |---|---|---|
231
+ | 资料 | `conversation.resource.list` / `conversation.resource.read` | `resource_list` / `resource_read` |
232
+ | 提问 | 无 | `ask_question` |
233
+ | 记忆 | `conversation.memory` | 无,开发者可以根据具体场景,自己组装成适合的模型工具 |
234
+ | 用户授权数据 | `conversation.dataAccess.query` / `conversation.dataAccess.request` / `conversation.dataAccess.readResult` | `data_access_query` / `data_access_request` / `data_access_read_result` |
235
+ | 小挂件 | `conversation.widget.list` / `conversation.widget.create` / `conversation.widget.read` / `conversation.widget.readModule` / `conversation.widget.rename` / `conversation.widget.write` / `conversation.widget.deleteRecord` / `conversation.widget.delete` / `conversation.widget.setPresentation` / `conversation.widget.send` | `widget_create` / `widget_write` / `widget_read` / `widget_delete` / `widget_send` |
236
+ | 主动服务 | `conversation.proactive.propose` / `conversation.proactive.isRunning` | |
237
+
238
+ ```ts
239
+ buddy: (conversation) => ({
240
+ initialState: {
241
+ model: "kimi-k3",
242
+ systemPrompt: PERSONA,
243
+ tools: [conversation.tools.resource_list, conversation.tools.resource_read, conversation.tools.ask_question, ...myTools],
244
+ },
245
+ }),
246
+ ```
202
247
 
203
248
  ### 3.1 资料
204
249
 
205
- 把 `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,`platformTools` 写上 `resource_read`,模型就能按需读。
250
+ 把 `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,把 `conversation.tools.resource_list` `conversation.tools.resource_read` 放进 `initialState.tools`,模型先列出有哪些资料,再按需读。
206
251
 
207
252
  - 工程目录从入口脚本往上找,与启动时的工作目录无关。
208
253
  - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 与点开头的文件和目录;资料改了要重启进程。
209
- - 系统提示末尾追加资料清单,模型按清单里的相对路径读。没有资料或没选 `resource_read` 时,不注册工具,也不追加清单。
254
+ - `resource_list` 列出全部资料的相对路径,`resource_read` 按路径读一篇。没有资料时 `resource_list` 回「没有资料」。
210
255
 
211
256
  ```
212
257
  my-buddy/ 工程目录
@@ -219,7 +264,7 @@ my-buddy/ 工程目录
219
264
 
220
265
  **在自己的代码里用**
221
266
 
222
- 调用 `conversation.resources`,不需要写进 `platformTools`:
267
+ 调用 `conversation.resource`,不需要把这两个工具放进 `initialState.tools`:
223
268
 
224
269
  | 方法 | 做什么 |
225
270
  |---|---|
@@ -227,7 +272,7 @@ my-buddy/ 工程目录
227
272
  | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填;没有这篇时抛异常 |
228
273
 
229
274
  ```ts
230
- const notes = conversation.resources.read("research/01-writings.md");
275
+ const notes = conversation.resource.read("research/01-writings.md");
231
276
  ```
232
277
 
233
278
  - 两个方法都是同步的,读的是进程启动时读好的那份。
@@ -239,7 +284,7 @@ const notes = conversation.resources.read("research/01-writings.md");
239
284
 
240
285
  让模型发一张题卡请用户选:一个问题加 2 到 4 个可以直接点的选项。私聊、群聊都能用。
241
286
 
242
- 把 `ask_question` 写进 `platformTools`,模型就能发题卡。
287
+ 把 `conversation.tools.ask_question` 放进 `initialState.tools`,模型就能发题卡。
243
288
 
244
289
  - 参数是 `question`(题目,不超过 500 字)和 `options`(2 到 4 个,每个不超过 30 字,不能重复);不合规时模型会看到错误,改了再调。
245
290
  - 用户点选项,就是用选项原文发一条消息,跟自己打字一样;也可以不点,直接打字。
@@ -271,7 +316,6 @@ interface MemoryEntry {
271
316
  const REMEMBER_PARAMS = Type.Object({ topic: Type.String(), text: Type.String() });
272
317
 
273
318
  await BuddyServer.start({
274
- model: "kimi-k3",
275
319
  dataRequirements: [],
276
320
  buddy: (conversation) => {
277
321
  const remember: AgentTool<typeof REMEMBER_PARAMS, undefined> = {
@@ -284,58 +328,51 @@ await BuddyServer.start({
284
328
  return { content: [{ type: "text", text: "记下了。" }], details: undefined };
285
329
  },
286
330
  };
287
- return { initialState: { systemPrompt: PERSONA, tools: [remember] } };
331
+ return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [remember] } };
288
332
  },
289
333
  });
290
334
  ```
291
335
 
292
- ### 3.4 授权数据
336
+ ### 3.4 用户授权数据
293
337
 
294
338
  用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id。
295
339
 
296
340
  这里有三层不同概念:
297
341
 
298
- 1. **平台能力目录**:平台目前能提供哪些 Dataset,由 `GET /cli/data-access/capabilities` 返回。
342
+ 1. **Runtime 能力目录**:当前 SDK 支持哪些 Dataset,由包导出的 `DATA_ACCESS_CAPABILITIES` 提供。
299
343
  2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在 `BuddyServer.start.dataRequirements`。
300
344
  3. **用户 Grant**:用户是否同意这只搭子读取某个已声明 Dataset,由客户端授权流程管理,搭子不能代替用户授予。
301
345
 
302
- `platformTools` 只决定模型能不能调用 `data_access_query`、`permission_request` 等工具,不等于声明 Dataset,也不等于获得用户授权。
303
-
304
- 位置和日历是当前会话的一次性权限结果,通过 `permission_request` 请求,不属于 Dataset,也不写进 `dataRequirements`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在 `dataRequirements` 中声明。
346
+ `data_access_query`、`data_access_request` 等平台工具放进 `initialState.tools`,只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权。
305
347
 
306
- **查看平台能力目录**
348
+ 位置和日历是当前会话的一次性授权结果,通过 `data_access_request` 请求,不属于 Dataset,也不写进 `dataRequirements`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在 `dataRequirements` 中声明。
307
349
 
308
- 开发者凭 CLI 用户令牌请求:
350
+ **读取 Runtime 能力目录**
309
351
 
310
- ```http
311
- GET /cli/data-access/capabilities
312
- Authorization: Bearer <用户 token>
313
- ```
352
+ 开发者工具和项目代码直接从 Runtime 包导入,不需要网络请求:
314
353
 
315
- 响应示例:
316
-
317
- ```json
318
- {
319
- "datasets": [
320
- {
321
- "id": "health.sleep",
322
- "title": "睡眠",
323
- "source": "healthkit",
324
- "schemaVersion": 1
325
- }
326
- ]
354
+ ```ts
355
+ import {
356
+ DATA_ACCESS_CAPABILITIES,
357
+ dataAccessCapability,
358
+ type DataAccessDatasetId,
359
+ } from "@my-life-buddies/buddy-runtime";
360
+
361
+ for (const capability of DATA_ACCESS_CAPABILITIES) {
362
+ console.log(capability.id, capability.title, capability.source, capability.schemaVersion);
327
363
  }
364
+
365
+ const sleep = dataAccessCapability("health.sleep");
328
366
  ```
329
367
 
330
- 这份目录是可选 `id` 的权威来源。表格仅用于阅读,新增能力时以接口返回为准。
368
+ 这份导出是开发者可选 `id` 的权威来源。新增 Dataset 随 Runtime 版本发布;developer-platform、脚手架和预检都应读取该导出,不再维护一份手写枚举。
331
369
 
332
370
  **声明需要哪些数据**
333
371
 
334
- 声明写在 `BuddyServer.start` 的初始化参数里,Runtime 每次启动时整份上传平台:
372
+ 声明写在 `BuddyServer.start` 的初始化参数里,Runtime 在本地展开成带标题、来源和 Schema 版本的完整声明:
335
373
 
336
374
  ```ts
337
375
  await BuddyServer.start({
338
- model: "kimi-k3",
339
376
  dataRequirements: [
340
377
  { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
341
378
  ],
@@ -343,13 +380,11 @@ await BuddyServer.start({
343
380
  });
344
381
  ```
345
382
 
346
- - `id`:从下表选,也可以调用 `GET /cli/data-access/capabilities` 获取平台当前的完整能力目录。
383
+ - `id`:从 `DATA_ACCESS_CAPABILITIES` 中选,TypeScript 会收窄为 `DataAccessDatasetId`。
347
384
  - `purpose`:用途,1~200 字。用户看到这句话再决定是否同意。
348
- - 每次启动都是整份覆盖:新增项进入待授权,删除项立即停止访问并撤销对应 Grant。
349
- - `id` `purpose` 都没变化时保留声明版本和已有 Grant;修改 `purpose` 会提升声明版本,已有 Grant 变成 `reconsentRequired`,需要用户重新确认。
350
- - 声明不再通过 `PUT /cli/buddies/:id/data-requirements` 修改;改代码并重启或发布搭子即可生效。
351
- - 本地进程连接线上网关时使用 `<buddyId>-dev` 身份,其声明和用户授权与正式搭子隔离,不会覆盖线上版本。
352
- - 可用 `GET /cli/buddies/:id/data-requirements` 查看某搭子当前已经上传并生效的声明;这个接口只读。
385
+ - Runtime 不在启动时注册声明;查询、HealthKit 权限卡和主动服务请求会携带当前完整声明。
386
+ - Server 在实际请求时按声明用途和 Schema 计算版本;`id`、`purpose` Schema 都没变化时保留已有 Grant,变化后要求重新授权。
387
+ - 本地进程连接线上网关时使用 `<buddyId>-dev` 身份,实际使用形成的授权上下文与正式搭子隔离。
353
388
 
354
389
  | Dataset | 内容 | `data.items` 每条的字段 |
355
390
  |---|---|---|
@@ -364,11 +399,65 @@ await BuddyServer.start({
364
399
 
365
400
  部分字段可能缺失,取值前先判断。
366
401
 
367
- **查询**
402
+ **模型工具与代码接口**
403
+
404
+ - `data_access_query` / `conversation.dataAccess.query`:查询已授权 Dataset。
405
+ - `data_access_request` / `conversation.dataAccess.request`:在私聊中发数据授权卡;发卡后当前回合结束,等待用户操作。
406
+ - `data_access_read_result` / `conversation.dataAccess.readResult`:按授权卡的 `requestId` 读取一次性位置或日历结果;HealthKit 不走这个接口。
368
407
 
369
- - 让模型查:`platformTools` 写上 `data_access_query`,工具说明里会列出声明过的 Dataset 与用途。可以在系统提示里写明什么时候查,比如「聊到作息时,先用 data_access_query 查 health.sleep」。
370
- - 要在聊天中申请位置、日历或 HealthKit 权限,把 `permission_request` 加进 `platformTools`。HealthKit 先查询,收到 `notGranted` / `reconsentRequired` 后再发卡;位置和日历授权完成后,以卡片消息 ID 调 `permission_result_read` 读取一次性结果。用户拒绝后不要在同一任务里反复申请。
371
- - 在工具里查:调用 `conversation.dataAccess.query`。
408
+ 模型工具要从 `conversation.tools` 取来放进 `initialState.tools` 才能调用;没声明 Dataset 时照常注册,查询的每一项都回 `notDeclared`。开发者自己的工具直接调用 `conversation.dataAccess`,不用把这几个工具放进去。
409
+
410
+ **HealthKit:先查询,缺授权再发卡**
411
+
412
+ ```ts
413
+ await BuddyServer.start({
414
+ dataRequirements: [
415
+ { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
416
+ ],
417
+ buddy: (conversation) => ({
418
+ initialState: {
419
+ model: "kimi-k3",
420
+ systemPrompt: [
421
+ "聊到作息时,先用 data_access_query 查询 health.sleep。",
422
+ "返回 notGranted 时,说明当前问题为什么需要数据,再用 data_access_request 发 HealthKit 授权卡。",
423
+ "用户处理授权卡后会开启新回合;再次调用 data_access_query,不要调用 data_access_read_result。",
424
+ "用户拒绝后不要在同一任务里反复申请。",
425
+ ].join("\n"),
426
+ tools: [conversation.tools.data_access_query, conversation.tools.data_access_request],
427
+ },
428
+ }),
429
+ });
430
+ ```
431
+
432
+ 完整链路:
433
+
434
+ ```text
435
+ data_access_query
436
+ → notGranted
437
+ → data_access_request { kind: "healthkit", datasets: ["health.sleep"], reason: "..." }
438
+ → 用户在授权卡操作,平台更新 Grant 和 Snapshot
439
+ → 隐藏结果开启新回合
440
+ → data_access_query
441
+ → available + data
442
+ ```
443
+
444
+ 查询接口不会把“从未授权”和“用途变化后需重新确认”分成两种模型状态,都会返回 `notGranted`;重新确认原因由用户侧授权页面展示。
445
+
446
+ **位置和日历:读取一次性结果**
447
+
448
+ 位置和日历不属于 Dataset,不写进 `dataRequirements`。模型先用 `data_access_request` 发卡,用户处理后,隐藏结果会带回这张卡的 `requestId`;再用 `data_access_read_result` 读取。结果只在平台短期保存,读不到时返回 `unavailable`。
449
+
450
+ ```text
451
+ data_access_request { kind: "location", reason: "查找你附近的地点" }
452
+ → 用户在授权卡操作
453
+ → 隐藏结果开启新回合并带回 requestId
454
+ → data_access_read_result { requestId }
455
+ → available + data,或 unavailable
456
+ ```
457
+
458
+ `calendar` 还可传 `calendarDays`,范围为 1~31,默认 7。授权请求第一期只支持私聊;群聊需要引导目标成员去私聊操作。
459
+
460
+ **开发者代码查询 Dataset**
372
461
 
373
462
  ```ts
374
463
  const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
@@ -403,7 +492,7 @@ if (sleep.status === "available") {
403
492
 
404
493
  ### 3.5 小挂件
405
494
 
406
- 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widgets`,都能建小挂件、写记录、读记录、删记录,并把它发进聊天。
495
+ 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widget`,都能建小挂件、写记录、读记录、删记录,并把它发进聊天。
407
496
 
408
497
  **放进工程目录**
409
498
 
@@ -421,22 +510,22 @@ my-buddy/ 工程目录
421
510
 
422
511
  **让模型使用**
423
512
 
424
- 把要用的 `widget_*` 写进 `platformTools`。
513
+ 把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`。
425
514
 
426
515
  - 工具说明里附上各类型的模块与字段。改了 `widgets/` 要重启进程。
427
- - 这场会话里已有哪些小挂件,平台会在消息里告诉模型,见第 4 节。
516
+ - 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
428
517
  - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
429
518
  - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
430
519
 
431
520
  **在自己的代码里用**
432
521
 
433
- 调用 `conversation.widgets`,不需要写进 `platformTools`:
522
+ 调用 `conversation.widget`,不需要把 `widget_*` 放进工具:
434
523
 
435
524
  ```ts
436
525
  execute: async (toolCallId, params) => {
437
- const { id } = await conversation.widgets.create({ type: "menu", title: "本周菜单", idempotencyKey: `menu:${toolCallId}` });
438
- await conversation.widgets.write(id, { module: "菜单", recordId: "2026-09-14", data: { dishes: ["红烧肉", "清炒时蔬"] } });
439
- conversation.widgets.send(id, { idempotencyKey: `menu-send:${toolCallId}` });
526
+ const { id } = await conversation.widget.create({ type: "menu", title: "本周菜单", idempotencyKey: `menu:${toolCallId}` });
527
+ await conversation.widget.write(id, { module: "菜单", recordId: "2026-09-14", data: { dishes: ["红烧肉", "清炒时蔬"] } });
528
+ conversation.widget.send(id, { idempotencyKey: `menu-send:${toolCallId}` });
440
529
  return { content: [{ type: "text", text: "菜单建好了。" }], details: undefined };
441
530
  },
442
531
  ```
@@ -496,8 +585,17 @@ await conversation.proactive.propose({
496
585
 
497
586
  主动回合里:
498
587
 
499
- - **模型能用的工具只有 `proactiveTools` 里点名的那些,默认一个都没有。** 用户不在场,所以不默认信任任何工具——名字叫 `read_xxx` 的也可能在里面发请求、写外部系统。没点名的工具模型仍然看得见,调了会被挡回一条说明,工具本身不执行。`data_access_query` 另外只能读订阅的 `allowedDataScopes`。
500
- - 系统提示和普通回合一字不差,记忆不会被附上去。要让模型用记忆,自己把 `conversation.memory.list` 包成工具注册进来,再写进 `proactiveTools`。
501
- - 调用 `memory.write`、`memory.delete`、`proactive.propose`,以及 `widgets` 上除 `list`、`read`、`readModule` 以外的方法,会抛异常;开发者的钩子照常执行。
588
+ - **工具清单和普通回合一样,运行时不按工具名拦。** `widget_create`、`widget_write`、`widget_delete`、`widget_send`、`ask_question`、`data_access_request`、`data_access_read_result` 调了会抛异常,模型看到错误;`data_access_query` 只能读订阅的 `allowedDataScopes`。
589
+ - **开发者自己的工具,运行时不拦。** 用户不在场,工具里要是会发请求、写外部系统,就在 `beforeToolCall` 里按 `conversation.proactive.isRunning()` 挡掉:
590
+
591
+ ```ts
592
+ beforeToolCall: async ({ toolCall }) =>
593
+ conversation.proactive.isRunning() && toolCall.name !== "read_plan"
594
+ ? { block: true, reason: "这一轮用户不在场,这个工具用不了。" }
595
+ : undefined,
596
+ ```
597
+
598
+ - 系统提示和普通回合一字不差,记忆不会被附上去。要让模型用记忆,自己把 `conversation.memory.list` 包成工具放进 `initialState.tools`。
599
+ - 调用 `memory.write`、`memory.delete`、`proactive.propose`,以及 `widget` 上除 `list`、`read`、`readModule` 以外的方法,会抛异常;开发者的钩子照常执行。
502
600
  - 用户这时发来消息,主动回合中止,先回复用户;超过 4 分钟也中止。
503
- - 主动回合的过程不下发给用户:平台在会话流里记两条不可见的记录(叫你判断了什么、你判断的结果),中间的工具步照常写回但也不下发。**开口的那条消息才是用户看得到的**,之后作为搭子说过的话出现在会话历史里。
601
+ - 主动回合的过程不下发给用户:平台在会话流里记两条不可见的记录(叫你判断了什么、你判断的结果),中间的工具步照常写回但也不下发。**开口的那条消息才是用户看得到的**,之后作为搭子说过的话出现在会话历史里。
@@ -14,13 +14,14 @@ export interface AgentDeps {
14
14
  key: string;
15
15
  buddyId: string;
16
16
  client: MLBClient;
17
- model: Model<Api>;
17
+ /** 平台网关地址:按开发者的 initialState.model 造模型条目时,baseUrl 指到它的 /internal/v1 */
18
+ gatewayUrl: string;
18
19
  streamFn: StreamFn;
19
20
  /** 搭子令牌:经 Agent 的 getApiKey 交给模型通道,变成 Authorization: Bearer 头 */
20
21
  token: string;
21
- /** 小挂件类型目录(server 启动时上传平台的那些);空数组就不装小挂件工具 */
22
+ /** 小挂件类型目录(server 启动时上传平台的那些);空数组时 widget_* 照常注册,说明里的类型目录为空 */
22
23
  widgetTypes: WidgetType[];
23
- /** 搭子目录下 resources/ 里的资料;空数组就不装 resource_read、不接清单 */
24
+ /** 搭子目录下 resources/ 里的资料;空数组时 resource_list 回「没有资料」 */
24
25
  resources: Resource[];
25
26
  dataRequirements: readonly DataRequirement[];
26
27
  factory: BuddyFactory;
@@ -28,11 +29,16 @@ export interface AgentDeps {
28
29
  /** Agent 事件的去处。pi 会等它返回,所以只能同步做事 */
29
30
  onEvent: (agent: Agent, event: AgentEvent) => void;
30
31
  }
31
- /** 跑开发者工厂拿配置,叠上运行时预置,建 Agent。开发者的工具名跟运行时的工具(小挂件、resource_read)重名或互相重名 → throw。 */
32
+ /** createAgent 建好的:Agent、这场会话的模型条目、授权数据记账,以及开发者配的兜底话 */
32
33
  export interface CreatedAgent {
33
34
  agent: Agent;
35
+ /** 按开发者 initialState.model 造的模型条目:重建历史时抄身份、判断收不收图片都按它 */
36
+ model: Model<Api>;
34
37
  dataAccessUsage: DataAccessUsageTracker;
38
+ /** BuddyOptions.errorReply:普通回合出错或一句话没说时由 index.ts 的 runTurn 发;没配是 undefined */
39
+ errorReply: string | undefined;
35
40
  }
41
+ /** 跑开发者工厂拿配置,叠上运行时预置,建 Agent。模型 id 运行时不支持、工具清单重名、拉平台 system prompt 失败 → throw。 */
36
42
  export declare function createAgent(deps: AgentDeps): Promise<CreatedAgent>;
37
43
  /**
38
44
  * 每次请求模型时接在历史末尾那条 user 消息的正文;现在的时间由 convertToLlm 按这条消息的 timestamp 拼在开头。
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/conversation/agent.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAa,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAEnG,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE7H,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,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,sDAAsD;IACtD,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,kFAAkF;AAClF,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,eAAe,EAAE,sBAAsB,CAAC;CACxC;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAmKxE;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,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAUxD"}
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;AAIrD,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,gFAAgF;IAChF,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAED,kFAAkF;AAClF,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CA2LxE;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,0DAA0D;AAC1D,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAUxD"}