@my-life-buddies/buddy-runtime 0.8.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 (67) hide show
  1. package/README.md +242 -111
  2. package/dist/conversation/agent.d.ts +14 -6
  3. package/dist/conversation/agent.d.ts.map +1 -1
  4. package/dist/conversation/agent.js +121 -64
  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 +14 -12
  11. package/dist/conversation/index.d.ts.map +1 -1
  12. package/dist/conversation/index.js +23 -22
  13. package/dist/conversation/index.js.map +1 -1
  14. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +1 -1
  15. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
  16. package/dist/conversation/mlbMessageAgentMessageConverter.js +3 -2
  17. package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
  18. package/dist/dataAccessCatalog.d.ts +56 -0
  19. package/dist/dataAccessCatalog.d.ts.map +1 -0
  20. package/dist/dataAccessCatalog.js +18 -0
  21. package/dist/dataAccessCatalog.js.map +1 -0
  22. package/dist/index.d.ts +5 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/mlbClient.d.ts +6 -6
  27. package/dist/mlbClient.d.ts.map +1 -1
  28. package/dist/mlbClient.js +11 -16
  29. package/dist/mlbClient.js.map +1 -1
  30. package/dist/model.d.ts +8 -1
  31. package/dist/model.d.ts.map +1 -1
  32. package/dist/model.js +12 -6
  33. package/dist/model.js.map +1 -1
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +36 -24
  36. package/dist/server.js.map +1 -1
  37. package/dist/tools/askQuestion.d.ts +15 -0
  38. package/dist/tools/askQuestion.d.ts.map +1 -0
  39. package/dist/tools/askQuestion.js +47 -0
  40. package/dist/tools/askQuestion.js.map +1 -0
  41. package/dist/tools/dataAccess.d.ts +3 -2
  42. package/dist/tools/dataAccess.d.ts.map +1 -1
  43. package/dist/tools/dataAccess.js +7 -4
  44. package/dist/tools/dataAccess.js.map +1 -1
  45. package/dist/tools/dataAccessRequest.d.ts +7 -0
  46. package/dist/tools/dataAccessRequest.d.ts.map +1 -0
  47. package/dist/tools/dataAccessRequest.js +67 -0
  48. package/dist/tools/dataAccessRequest.js.map +1 -0
  49. package/dist/tools/registry.d.ts +1 -4
  50. package/dist/tools/registry.d.ts.map +1 -1
  51. package/dist/tools/registry.js +1 -15
  52. package/dist/tools/registry.js.map +1 -1
  53. package/dist/tools/resource.d.ts +4 -3
  54. package/dist/tools/resource.d.ts.map +1 -1
  55. package/dist/tools/resource.js +18 -13
  56. package/dist/tools/resource.js.map +1 -1
  57. package/dist/tools/{widgets.d.ts → widget.d.ts} +6 -4
  58. package/dist/tools/widget.d.ts.map +1 -0
  59. package/dist/tools/{widgets.js → widget.js} +13 -9
  60. package/dist/tools/widget.js.map +1 -0
  61. package/dist/types.d.ts +79 -23
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +5 -3
  64. package/dist/types.js.map +1 -1
  65. package/package.json +2 -2
  66. package/dist/tools/widgets.d.ts.map +0 -1
  67. 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`。
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,9 +44,10 @@ const planMenu: AgentTool<typeof PLAN_MENU_PARAMS, undefined> = {
44
44
  };
45
45
 
46
46
  await BuddyServer.start({
47
- model: "kimi-k3",
47
+ dataRequirements: [],
48
48
  buddy: () => ({
49
49
  initialState: {
50
+ model: "kimi-k3",
50
51
  systemPrompt: "你是「小涂阿姨」——一位管做饭的阿姨……",
51
52
  tools: [planMenu],
52
53
  },
@@ -74,7 +75,7 @@ await BuddyServer.start({
74
75
  - 在自己电脑上连测试网关时,平台把这个进程当作 `<id>-dev` 分身,会话与正式搭子分开。
75
76
  - 改完代码想自动重启,在启动命令里加 `--watch`。
76
77
 
77
- 完整示例:[examples/aunt-tu/](examples/aunt-tu/)(记忆、小挂件)、[examples/stacy-sims/](examples/stacy-sims/)(资料)。
78
+ 完整示例:[examples/aunt-tu/](examples/aunt-tu/)(提问、记忆、小挂件)、[examples/stacy-sims/](examples/stacy-sims/)(资料)。
78
79
 
79
80
  ## 2. 配置
80
81
 
@@ -82,51 +83,72 @@ await BuddyServer.start({
82
83
 
83
84
  连上平台开始处理消息,返回 `{ stop(): Promise<void> }`。
84
85
 
85
- | 参数 | 说明 |
86
- |---|---|
87
- | `model` | 必填,模型 id,取值见 `GET /cli/models`,进程内所有会话共用。没填或运行时不认识这个 id 时启动失败 |
88
- | `buddy` | 必填,`(conversation: Conversation) => BuddyOptions \| Promise<BuddyOptions>`,见 2.2、2.3 |
89
- | `errorReply` | 可选,模型出错或空回复时发的兜底回复 |
86
+ ```ts
87
+ interface StartOptions {
88
+ // 必填。这只搭子需要的用户数据及用途,见 3.4;不需要时传 []
89
+ dataRequirements: { id: DataAccessDatasetId; purpose: string }[];
90
+
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。
90
97
 
91
98
  用户新加搭子、新建群时,搭子会先开口打个招呼,跟平时回复一样调模型、逐字出现。开场白怎么说,写在 system prompt 里。
92
99
 
93
- `buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,要记住的东西写进记忆(3.2)。
100
+ `buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,要记住的东西写进记忆(3.3)。
94
101
 
95
102
  会话历史长到一定程度(超过 128K token,窗口小的模型按窗口的四分之一算)时,运行时在会话被回收后把早先的对话压成一份摘要写回平台,下次重建时模型看到的是「摘要 + 最近的原文」。开发者不用做任何事,也没有开关。
96
103
 
97
104
  ### 2.2 `BuddyOptions`
98
105
 
99
- `buddy` 的返回值。字段名和签名取自 pi-agent-core 的 `AgentOptions` / `AgentState`,详见 pi 的文档。
106
+ `buddy` 的返回值。
100
107
 
101
108
  ```ts
102
- type BuddyOptions = Pick<AgentOptions,
103
- | "beforeToolCall" | "afterToolCall"
104
- | "shouldStopAfterTurn" | "prepareNextTurnWithContext"
105
- | "thinkingBudgets" | "toolExecution"
106
- > & {
107
- platformTools?: readonly PlatformToolName[];
108
- proactiveTools?: readonly string[];
109
- initialState: Pick<AgentState, "systemPrompt"> & Partial<Pick<AgentState, "tools" | "thinkingLevel">>;
110
- };
111
- ```
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
+ };
112
136
 
113
- | 字段 | 说明 |
114
- |---|---|
115
- | `initialState.systemPrompt` | 必填,人设与做事规则,只放不随回合变化的内容。模型实际收到的系统提示见第 4 节 |
116
- | `initialState.tools` | 可选,pi 的 `AgentTool` 数组 |
117
- | `initialState.thinkingLevel` | 可选,默认 `off`。模型不支持的级别在请求时就近换成支持的一档,建会话时打一条 warn |
118
- | `platformTools` | 可选,给模型注册哪些平台工具,默认 `[]`,见第 3 节 |
119
- | `proactiveTools` | 可选,主动回合里还能用哪些工具,默认 `[]`(一个都不给)。写工具名,`platformTools` 选中的和 `initialState.tools` 里自己的都行;写了没注册的名字建会话时抛异常。见 3.5 |
120
- | 其余字段 | 原样交给 pi;`beforeToolCall` 抛异常时按放行处理 |
137
+ // 可选。模型出错或一句话没说时发的兜底回复,用户和模型之后都看得到;主动回合不发
138
+ errorReply?: string;
121
139
 
122
- 写工具时注意:
140
+ // 可选。工具执行前调用,context 里有这次的 toolCall、校验过的 args 和当前上下文。
141
+ // 返回 { block: true, reason } 时工具不执行,模型看到 reason;抛异常时工具也不执行,模型看到异常信息
142
+ beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise<BeforeToolCallResult | undefined>;
123
143
 
124
- - `label` 必填,是工具执行时用户看到的状态文案,空字符串表示不显示。
125
- - `execute` 的返回值必须带 `details`,可以是 `undefined`;失败直接抛异常,模型能看到异常信息。
126
- - 先单独声明为 `AgentTool<typeof 参数 schema>` 再放进数组,`execute` 的参数才有类型;直接写在数组里时参数类型是 `unknown`。
127
- - 工具名不能与平台工具或其他工具重复。
144
+ // 可选。工具执行完调用,context 里带着执行结果;返回的 content、details、isError 会替换结果里的同名字段,抛异常时结果换成异常信息
145
+ afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise<AfterToolCallResult | undefined>;
128
146
 
129
- 模型、密钥与会话历史由运行时管理,不在配置里。
147
+ // 可选。模型说完一步(这一步的工具也跑完)后调用,返回 true 就不再请求模型。
148
+ // 这一步发出了权限请求卡或题卡时,这一轮直接停下,不调它
149
+ shouldStopAfterTurn?: (context: ShouldStopAfterTurnContext, signal?: AbortSignal) => boolean | Promise<boolean>;
150
+ }
151
+ ```
130
152
 
131
153
  ### 2.3 `Conversation`
132
154
 
@@ -140,26 +162,43 @@ interface Conversation {
140
162
  // 日志,每行开头带搭子 id
141
163
  log: BuddyLogger;
142
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
+
143
180
  // 资料,见 3.1:resources/ 里的 .md,进程启动时读好,所有会话共用
144
- resources: {
181
+ resource: {
145
182
  list(): string[];
146
183
  read(file: string): string;
147
184
  };
148
185
 
149
- // 记忆,见 3.2:这场会话的长期记忆,存在平台上
186
+ // 记忆,见 3.3:这场会话的长期记忆,存在平台上
150
187
  memory: {
151
188
  list(): Promise<MemoryEntry[]>;
152
189
  write(key: string, text: string): Promise<void>;
153
190
  delete(key: string): Promise<void>;
154
191
  };
155
192
 
156
- // 授权数据,见 3.3:私聊不传 memberId,群聊传消息前缀里的 m_…
193
+ // 授权数据,见 3.4:私聊不传 memberId,群聊传消息前缀里的 m_…
157
194
  dataAccess: {
158
195
  query(request: { datasets: string[]; memberId?: string }): Promise<DatasetAccessResult[]>;
196
+ request(input: DataAccessRequestInput & { idempotencyKey: string }): Promise<void>;
197
+ readResult(requestId: string): Promise<DataAccessReadResult>;
159
198
  };
160
199
 
161
- // 小挂件,见 3.4
162
- widgets: {
200
+ // 小挂件,见 3.5
201
+ widget: {
163
202
  list(): Promise<WidgetSummary[]>;
164
203
  create(input: { type: string; title: string; idempotencyKey?: string }): Promise<{ id: string }>;
165
204
  read(id: string): Promise<Widget>;
@@ -175,32 +214,44 @@ interface Conversation {
175
214
  send(id: string, options: { idempotencyKey: string }): void;
176
215
  };
177
216
 
178
- // 主动服务,见 3.5:提订阅建议,用户在 App 里确认后才生效
217
+ // 主动服务,见 3.6:propose 提订阅建议,用户在 App 里确认后才生效;isRunning 判断现在是不是主动回合
179
218
  proactive: {
180
219
  propose(request: ProactiveSubscriptionRequest): Promise<{ id: string; status: "pending" }>;
220
+ isRunning(): boolean;
181
221
  };
182
222
  }
183
223
  ```
184
224
 
185
- `memory`、`dataAccess`、`widgets`、`proactive` 已经绑好这场会话,直接调用,不用传令牌或拼接口路径。
225
+ `tools`、`memory`、`dataAccess`、`widget`、`proactive` 已经绑好这场会话,直接用,不用传令牌或拼接口路径。
186
226
 
187
227
  ## 3. 平台能力
188
228
 
189
- | 能力 | `conversation` 对象方法 | 默认注册成的模型工具 | 默认注册成模型工具的条件 |
190
- |---|---|---|---|
191
- | 资料 | `conversation.resources.list` / `conversation.resources.read` | `resource_read` | platformTools 声明 && `resources/` 下有 `.md` |
192
- | 记忆 | `conversation.memory` | 无,默认不注册成模型工具,开发者可以根据具体场景,注册成适合场景需要的模型工具 | — |
193
- | 授权数据 | `conversation.dataAccess.query` | `data_access_query` | platformTools 声明 && 声明过至少一个 Dataset |
194
- | 小挂件 | `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/` 下有小挂件 |
195
- | 主动服务 | `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
+ ```
196
247
 
197
248
  ### 3.1 资料
198
249
 
199
- 把 `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,`platformTools` 写上 `resource_read`,模型就能按需读。
250
+ 把 `.md` 资料放进工程目录(`package.json` 所在目录)下的 `resources/`,把 `conversation.tools.resource_list` `conversation.tools.resource_read` 放进 `initialState.tools`,模型先列出有哪些资料,再按需读。
200
251
 
201
252
  - 工程目录从入口脚本往上找,与启动时的工作目录无关。
202
253
  - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 与点开头的文件和目录;资料改了要重启进程。
203
- - 系统提示末尾追加资料清单,模型按清单里的相对路径读。没有资料或没选 `resource_read` 时,不注册工具,也不追加清单。
254
+ - `resource_list` 列出全部资料的相对路径,`resource_read` 按路径读一篇。没有资料时 `resource_list` 回「没有资料」。
204
255
 
205
256
  ```
206
257
  my-buddy/ 工程目录
@@ -213,7 +264,7 @@ my-buddy/ 工程目录
213
264
 
214
265
  **在自己的代码里用**
215
266
 
216
- 调用 `conversation.resources`,不需要写进 `platformTools`:
267
+ 调用 `conversation.resource`,不需要把这两个工具放进 `initialState.tools`:
217
268
 
218
269
  | 方法 | 做什么 |
219
270
  |---|---|
@@ -221,7 +272,7 @@ my-buddy/ 工程目录
221
272
  | `read(file)` | 读一篇的正文,`file` 照 `list()` 给的路径原样填;没有这篇时抛异常 |
222
273
 
223
274
  ```ts
224
- const notes = conversation.resources.read("research/01-writings.md");
275
+ const notes = conversation.resource.read("research/01-writings.md");
225
276
  ```
226
277
 
227
278
  - 两个方法都是同步的,读的是进程启动时读好的那份。
@@ -229,7 +280,19 @@ const notes = conversation.resources.read("research/01-writings.md");
229
280
 
230
281
  完整示例见 [examples/stacy-sims/](examples/stacy-sims/)。
231
282
 
232
- ### 3.2 记忆
283
+ ### 3.2 提问
284
+
285
+ 让模型发一张题卡请用户选:一个问题加 2 到 4 个可以直接点的选项。私聊、群聊都能用。
286
+
287
+ 把 `conversation.tools.ask_question` 放进 `initialState.tools`,模型就能发题卡。
288
+
289
+ - 参数是 `question`(题目,不超过 500 字)和 `options`(2 到 4 个,每个不超过 30 字,不能重复);不合规时模型会看到错误,改了再调。
290
+ - 用户点选项,就是用选项原文发一条消息,跟自己打字一样;也可以不点,直接打字。
291
+ - `conversation` 对象不提供发题卡的方法,题卡只由模型发。
292
+
293
+ 用法见 [examples/aunt-tu/](examples/aunt-tu/)。
294
+
295
+ ### 3.3 记忆
233
296
 
234
297
  这场会话的长期记忆,存在平台上,只在本会话内可见,会话删除时一并清除。
235
298
 
@@ -253,7 +316,7 @@ interface MemoryEntry {
253
316
  const REMEMBER_PARAMS = Type.Object({ topic: Type.String(), text: Type.String() });
254
317
 
255
318
  await BuddyServer.start({
256
- model: "kimi-k3",
319
+ dataRequirements: [],
257
320
  buddy: (conversation) => {
258
321
  const remember: AgentTool<typeof REMEMBER_PARAMS, undefined> = {
259
322
  name: "remember",
@@ -265,31 +328,63 @@ await BuddyServer.start({
265
328
  return { content: [{ type: "text", text: "记下了。" }], details: undefined };
266
329
  },
267
330
  };
268
- return { initialState: { systemPrompt: PERSONA, tools: [remember] } };
331
+ return { initialState: { model: "kimi-k3", systemPrompt: PERSONA, tools: [remember] } };
269
332
  },
270
333
  });
271
334
  ```
272
335
 
273
- ### 3.3 授权数据
336
+ ### 3.4 用户授权数据
274
337
 
275
338
  用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id。
276
339
 
277
- **声明需要哪些数据**
340
+ 这里有三层不同概念:
278
341
 
279
- `PUT /cli/buddies/:id/data-requirements` 提交完整清单,每次覆盖上一次:
342
+ 1. **Runtime 能力目录**:当前 SDK 支持哪些 Dataset,由包导出的 `DATA_ACCESS_CAPABILITIES` 提供。
343
+ 2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在 `BuddyServer.start.dataRequirements`。
344
+ 3. **用户 Grant**:用户是否同意这只搭子读取某个已声明 Dataset,由客户端授权流程管理,搭子不能代替用户授予。
280
345
 
281
- ```json
282
- {
283
- "datasets": [
284
- { "id": "health.sleep", "purpose": "根据你最近的睡眠调整作息建议" }
285
- ]
346
+ 把 `data_access_query`、`data_access_request` 等平台工具放进 `initialState.tools`,只决定模型能不能调用它们,不等于声明 Dataset,也不等于获得用户授权。
347
+
348
+ 位置和日历是当前会话的一次性授权结果,通过 `data_access_request` 请求,不属于 Dataset,也不写进 `dataRequirements`。HealthKit 数据会形成可重复查询的 Dataset,因此必须先在 `dataRequirements` 中声明。
349
+
350
+ **读取 Runtime 能力目录**
351
+
352
+ 开发者工具和项目代码直接从 Runtime 包导入,不需要网络请求:
353
+
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);
286
363
  }
364
+
365
+ const sleep = dataAccessCapability("health.sleep");
287
366
  ```
288
367
 
289
- - `id`:从下表选。
368
+ 这份导出是开发者可选 `id` 的权威来源。新增 Dataset 随 Runtime 版本发布;developer-platform、脚手架和预检都应读取该导出,不再维护一份手写枚举。
369
+
370
+ **声明需要哪些数据**
371
+
372
+ 声明写在 `BuddyServer.start` 的初始化参数里,Runtime 在本地展开成带标题、来源和 Schema 版本的完整声明:
373
+
374
+ ```ts
375
+ await BuddyServer.start({
376
+ dataRequirements: [
377
+ { id: "health.sleep", purpose: "根据你最近的睡眠调整作息建议" },
378
+ ],
379
+ buddy: () => ({ /* ... */ }),
380
+ });
381
+ ```
382
+
383
+ - `id`:从 `DATA_ACCESS_CAPABILITIES` 中选,TypeScript 会收窄为 `DataAccessDatasetId`。
290
384
  - `purpose`:用途,1~200 字。用户看到这句话再决定是否同意。
291
- - 改了某项的 `purpose`,用户要重新同意;从清单里删掉的,搭子立即读不到。
292
- - 改完清单要重启搭子进程,模型才能看到新清单。
385
+ - Runtime 不在启动时注册声明;查询、HealthKit 权限卡和主动服务请求会携带当前完整声明。
386
+ - Server 在实际请求时按声明用途和 Schema 计算版本;`id`、`purpose` 与 Schema 都没变化时保留已有 Grant,变化后要求重新授权。
387
+ - 本地进程连接线上网关时使用 `<buddyId>-dev` 身份,实际使用形成的授权上下文与正式搭子隔离。
293
388
 
294
389
  | Dataset | 内容 | `data.items` 每条的字段 |
295
390
  |---|---|---|
@@ -304,10 +399,65 @@ await BuddyServer.start({
304
399
 
305
400
  部分字段可能缺失,取值前先判断。
306
401
 
307
- **查询**
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 不走这个接口。
407
+
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。授权请求第一期只支持私聊;群聊需要引导目标成员去私聊操作。
308
459
 
309
- - 让模型查:`platformTools` 写上 `data_access_query`,工具说明里会列出声明过的 Dataset 与用途。可以在系统提示里写明什么时候查,比如「聊到作息时,先用 data_access_query 查 health.sleep」。
310
- - 在工具里查:调用 `conversation.dataAccess.query`。
460
+ **开发者代码查询 Dataset**
311
461
 
312
462
  ```ts
313
463
  const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
@@ -340,9 +490,9 @@ if (sleep.status === "available") {
340
490
  - **想让某个结论以后还在场,就在回复里说出来**(用户也会看到)。模型对这些数据的记忆只能活在它说过的话里——下次它读到的是「查了 health.sleep,可用」,不是具体数字。
341
491
  - **别把查到的数据写进记忆、小挂件或别的工具的参数**。那些地方不脱敏,写进去就永久留下了。
342
492
 
343
- ### 3.4 小挂件
493
+ ### 3.5 小挂件
344
494
 
345
- 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widgets`,都能建小挂件、写记录、读记录、删记录,并把它发进聊天。
495
+ 小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widget`,都能建小挂件、写记录、读记录、删记录,并把它发进聊天。
346
496
 
347
497
  **放进工程目录**
348
498
 
@@ -360,22 +510,22 @@ my-buddy/ 工程目录
360
510
 
361
511
  **让模型使用**
362
512
 
363
- 把要用的 `widget_*` 写进 `platformTools`。
513
+ 把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`。
364
514
 
365
515
  - 工具说明里附上各类型的模块与字段。改了 `widgets/` 要重启进程。
366
- - 这场会话里已有哪些小挂件,平台会在消息里告诉模型,见第 4 节。
516
+ - 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
367
517
  - 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
368
518
  - `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
369
519
 
370
520
  **在自己的代码里用**
371
521
 
372
- 调用 `conversation.widgets`,不需要写进 `platformTools`:
522
+ 调用 `conversation.widget`,不需要把 `widget_*` 放进工具:
373
523
 
374
524
  ```ts
375
525
  execute: async (toolCallId, params) => {
376
- const { id } = await conversation.widgets.create({ type: "menu", title: "本周菜单", idempotencyKey: `menu:${toolCallId}` });
377
- await conversation.widgets.write(id, { module: "菜单", recordId: "2026-09-14", data: { dishes: ["红烧肉", "清炒时蔬"] } });
378
- 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}` });
379
529
  return { content: [{ type: "text", text: "菜单建好了。" }], details: undefined };
380
530
  },
381
531
  ```
@@ -396,11 +546,11 @@ execute: async (toolCallId, params) => {
396
546
  - `list` 和 `read` 回的 `canEdit` 为 `false` 的,是别的会话转发来的引用,只能读;改名、写记录、删除、设卡片时平台回 403。
397
547
  - `data` 要符合 `schema.json` 里这个模块的 `record`,不符合时平台回 400。
398
548
  - 平台回非 2xx、网络出错、超过 30 秒或响应超过 4 MiB 时抛异常,异常的 `status` 是 HTTP 状态码,不带平台给的原因。写操作不自动重试,失败后先读一遍真实状态再决定。
399
- - 主动服务回合里,除了 `list`、`read`、`readModule`,其余方法都会抛异常,见 3.5
549
+ - 主动服务回合里,除了 `list`、`read`、`readModule`,其余方法都会抛异常,见 3.6
400
550
 
401
551
  用法见 [examples/aunt-tu/](examples/aunt-tu/)。
402
552
 
403
- ### 3.5 主动服务
553
+ ### 3.6 主动服务
404
554
 
405
555
  让搭子在约定的时机主动找用户说话,比如每周一次复盘。只支持私聊。
406
556
 
@@ -435,36 +585,17 @@ await conversation.proactive.propose({
435
585
 
436
586
  主动回合里:
437
587
 
438
- - **模型能用的工具只有 `proactiveTools` 里点名的那些,默认一个都没有。** 用户不在场,所以不默认信任任何工具——名字叫 `read_xxx` 的也可能在里面发请求、写外部系统。没点名的工具模型仍然看得见,调了会被挡回一条说明,工具本身不执行。`data_access_query` 另外只能读订阅的 `allowedDataScopes`。
439
- - 系统提示和普通回合一字不差,记忆不会被附上去。要让模型用记忆,自己把 `conversation.memory.list` 包成工具注册进来,再写进 `proactiveTools`。
440
- - 调用 `memory.write`、`memory.delete`、`proactive.propose`,以及 `widgets` 上除 `list`、`read`、`readModule` 以外的方法,会抛异常;开发者的钩子照常执行。
441
- - 用户这时发来消息,主动回合中止,先回复用户;超过 4 分钟也中止。
442
- - 主动回合的过程不下发给用户:平台在会话流里记两条不可见的记录(叫你判断了什么、你判断的结果),中间的工具步照常写回但也不下发。**开口的那条消息才是用户看得到的**,之后作为搭子说过的话出现在会话历史里。
443
-
444
- ## 4. 模型收到什么
445
-
446
- 开发者不用保存会话历史,也不用自己拼模型消息。每场会话的消息存在平台上,进程重启后运行时从平台取回。每次请求模型时,运行时组出以下内容。
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()` 挡掉:
447
590
 
448
- **系统提示**,按顺序拼接,段间空一行:
449
-
450
- 1. 平台的系统提示:对每场会话都一样的规则,比如消息开头的时间怎么读、用户所在时区去哪看。
451
- 2. `initialState.systemPrompt`。
452
- 3. 资料清单:选了 `resource_read` 且有资料时才有。
453
-
454
- **工具**:选中的平台工具在前,`initialState.tools` 在后。
455
-
456
- **消息**:这场会话的完整历史,按时间先后排列。一轮回复里模型可能请求多次,比如调完工具接着说,每次都带上当时的完整历史。
457
-
458
- - 用户消息、工具结果的开头带 GMT 时间,如「[2026-09-10 14:00:30 GMT] 」;搭子自己说的话不带。
459
- - 历史末尾多一条「现在的时间(平台附注,不是谁说的话)」,时间是请求时刻,只在这一次请求里。
460
- - 群聊里成员说的话开头标「[名字 #m_…] 」。
461
- - 平台交代的情况(比如这场会话里已有哪些小挂件)是一条以「平台交代的情况:」开头的消息,情况有变化才追加新的一份。
462
- - 引用了别的消息时,开头加一行「[引用 发言人:原话]」,引用图片写「[引用 发言人的图片 #被引消息 id]」。
463
- - 转发的聊天记录卡按条目展开,每条先写「【名字】」再写正文。
464
- - 查过授权数据的那一轮,结束后按 3.3 替换。
465
-
466
- **图片**:用户发的图不用开发者处理。
591
+ ```ts
592
+ beforeToolCall: async ({ toolCall }) =>
593
+ conversation.proactive.isRunning() && toolCall.name !== "read_plan"
594
+ ? { block: true, reason: "这一轮用户不在场,这个工具用不了。" }
595
+ : undefined,
596
+ ```
467
597
 
468
- - 平台在上传时统一处理成 JPEG,最长边不超过 2048 px、不超过 5 MiB。
469
- - 图片消息是一条 user 消息:先是「[图片 #消息 id]」和用户配的文字,再跟图片本身;聊天记录卡里的图跟在所在条目的正文后面。
470
- - 图片读不到时换成「[图片无法读取]」;`model` 不支持看图时换成「[图片:当前模型不支持看图]」。
598
+ - 系统提示和普通回合一字不差,记忆不会被附上去。要让模型用记忆,自己把 `conversation.memory.list` 包成工具放进 `initialState.tools`。
599
+ - 调用 `memory.write`、`memory.delete`、`proactive.propose`,以及 `widget` 上除 `list`、`read`、`readModule` 以外的方法,会抛异常;开发者的钩子照常执行。
600
+ - 用户这时发来消息,主动回合中止,先回复用户;超过 4 分钟也中止。
601
+ - 主动回合的过程不下发给用户:平台在会话流里记两条不可见的记录(叫你判断了什么、你判断的结果),中间的工具步照常写回但也不下发。**开口的那条消息才是用户看得到的**,之后作为搭子说过的话出现在会话历史里。
@@ -6,31 +6,39 @@ import { DataAccessUsageTracker } from "../tools/dataAccess.ts";
6
6
  import type { Resource } from "../tools/resource.ts";
7
7
  import type { BuddyFactory, BuddyLogger, DataRequirement, MLBMessageToWrite } from "../types.ts";
8
8
  export interface AgentDeps {
9
- /** 这场会话的发送队列:widget_send 与 conversation.widgets.send 把小挂件引用放进来 */
9
+ /** 这场会话的发送队列:小挂件引用、权限请求卡、题卡都放进来 */
10
10
  enqueueMessage: (message: MLBMessageToWrite) => void;
11
+ /** 发了一张等用户操作的卡(权限请求卡、题卡):这张卡算这一轮说过的话 */
12
+ onWaitingForUser?: () => void;
11
13
  onPolicyStop?: () => void;
12
14
  key: string;
13
15
  buddyId: string;
14
16
  client: MLBClient;
15
- model: Model<Api>;
17
+ /** 平台网关地址:按开发者的 initialState.model 造模型条目时,baseUrl 指到它的 /internal/v1 */
18
+ gatewayUrl: string;
16
19
  streamFn: StreamFn;
17
20
  /** 搭子令牌:经 Agent 的 getApiKey 交给模型通道,变成 Authorization: Bearer 头 */
18
21
  token: string;
19
- /** 小挂件类型目录(server 启动时上传平台的那些);空数组就不装小挂件工具 */
22
+ /** 小挂件类型目录(server 启动时上传平台的那些);空数组时 widget_* 照常注册,说明里的类型目录为空 */
20
23
  widgetTypes: WidgetType[];
21
- /** 搭子目录下 resources/ 里的资料;空数组就不装 resource_read、不接清单 */
24
+ /** 搭子目录下 resources/ 里的资料;空数组时 resource_list 回「没有资料」 */
22
25
  resources: Resource[];
23
- dataRequirements: () => Promise<DataRequirement[]>;
26
+ dataRequirements: readonly DataRequirement[];
24
27
  factory: BuddyFactory;
25
28
  log: BuddyLogger;
26
29
  /** Agent 事件的去处。pi 会等它返回,所以只能同步做事 */
27
30
  onEvent: (agent: Agent, event: AgentEvent) => void;
28
31
  }
29
- /** 跑开发者工厂拿配置,叠上运行时预置,建 Agent。开发者的工具名跟运行时的工具(小挂件、resource_read)重名或互相重名 → throw。 */
32
+ /** createAgent 建好的:Agent、这场会话的模型条目、授权数据记账,以及开发者配的兜底话 */
30
33
  export interface CreatedAgent {
31
34
  agent: Agent;
35
+ /** 按开发者 initialState.model 造的模型条目:重建历史时抄身份、判断收不收图片都按它 */
36
+ model: Model<Api>;
32
37
  dataAccessUsage: DataAccessUsageTracker;
38
+ /** BuddyOptions.errorReply:普通回合出错或一句话没说时由 index.ts 的 runTurn 发;没配是 undefined */
39
+ errorReply: string | undefined;
33
40
  }
41
+ /** 跑开发者工厂拿配置,叠上运行时预置,建 Agent。模型 id 运行时不支持、工具清单重名、拉平台 system prompt 失败 → throw。 */
34
42
  export declare function createAgent(deps: AgentDeps): Promise<CreatedAgent>;
35
43
  /**
36
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;AAC7D,OAAO,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE7H,MAAM,WAAW,SAAS;IACzB,kEAAkE;IAClE,cAAc,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,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,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACnD,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,CAsIxE;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"}