@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.
- package/README.md +242 -111
- package/dist/conversation/agent.d.ts +14 -6
- package/dist/conversation/agent.d.ts.map +1 -1
- package/dist/conversation/agent.js +121 -64
- package/dist/conversation/agent.js.map +1 -1
- package/dist/conversation/compaction.d.ts +2 -2
- package/dist/conversation/compaction.d.ts.map +1 -1
- package/dist/conversation/compaction.js +1 -1
- package/dist/conversation/compaction.js.map +1 -1
- package/dist/conversation/index.d.ts +14 -12
- package/dist/conversation/index.d.ts.map +1 -1
- package/dist/conversation/index.js +23 -22
- package/dist/conversation/index.js.map +1 -1
- package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +1 -1
- package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
- package/dist/conversation/mlbMessageAgentMessageConverter.js +3 -2
- package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
- package/dist/dataAccessCatalog.d.ts +56 -0
- package/dist/dataAccessCatalog.d.ts.map +1 -0
- package/dist/dataAccessCatalog.js +18 -0
- package/dist/dataAccessCatalog.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mlbClient.d.ts +6 -6
- package/dist/mlbClient.d.ts.map +1 -1
- package/dist/mlbClient.js +11 -16
- package/dist/mlbClient.js.map +1 -1
- package/dist/model.d.ts +8 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +12 -6
- package/dist/model.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +36 -24
- package/dist/server.js.map +1 -1
- package/dist/tools/askQuestion.d.ts +15 -0
- package/dist/tools/askQuestion.d.ts.map +1 -0
- package/dist/tools/askQuestion.js +47 -0
- package/dist/tools/askQuestion.js.map +1 -0
- package/dist/tools/dataAccess.d.ts +3 -2
- package/dist/tools/dataAccess.d.ts.map +1 -1
- package/dist/tools/dataAccess.js +7 -4
- package/dist/tools/dataAccess.js.map +1 -1
- package/dist/tools/dataAccessRequest.d.ts +7 -0
- package/dist/tools/dataAccessRequest.d.ts.map +1 -0
- package/dist/tools/dataAccessRequest.js +67 -0
- package/dist/tools/dataAccessRequest.js.map +1 -0
- package/dist/tools/registry.d.ts +1 -4
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +1 -15
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/resource.d.ts +4 -3
- package/dist/tools/resource.d.ts.map +1 -1
- package/dist/tools/resource.js +18 -13
- package/dist/tools/resource.js.map +1 -1
- package/dist/tools/{widgets.d.ts → widget.d.ts} +6 -4
- package/dist/tools/widget.d.ts.map +1 -0
- package/dist/tools/{widgets.js → widget.js} +13 -9
- package/dist/tools/widget.js.map +1 -0
- package/dist/types.d.ts +79 -23
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -3
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/dist/tools/widgets.d.ts.map +0 -1
- 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
|
|
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
|
-
|
|
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/)
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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.
|
|
100
|
+
`buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,要记住的东西写进记忆(3.3)。
|
|
94
101
|
|
|
95
102
|
会话历史长到一定程度(超过 128K token,窗口小的模型按窗口的四分之一算)时,运行时在会话被回收后把早先的对话压成一份摘要写回平台,下次重建时模型看到的是「摘要 + 最近的原文」。开发者不用做任何事,也没有开关。
|
|
96
103
|
|
|
97
104
|
### 2.2 `BuddyOptions`
|
|
98
105
|
|
|
99
|
-
`buddy`
|
|
106
|
+
`buddy` 的返回值。
|
|
100
107
|
|
|
101
108
|
```ts
|
|
102
|
-
type
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
181
|
+
resource: {
|
|
145
182
|
list(): string[];
|
|
146
183
|
read(file: string): string;
|
|
147
184
|
};
|
|
148
185
|
|
|
149
|
-
// 记忆,见 3.
|
|
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.
|
|
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.
|
|
162
|
-
|
|
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.
|
|
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`、`
|
|
225
|
+
`tools`、`memory`、`dataAccess`、`widget`、`proactive` 已经绑好这场会话,直接用,不用传令牌或拼接口路径。
|
|
186
226
|
|
|
187
227
|
## 3. 平台能力
|
|
188
228
|
|
|
189
|
-
| 能力 | `conversation` 对象方法 |
|
|
190
|
-
|
|
191
|
-
| 资料 | `conversation.
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
336
|
+
### 3.4 用户授权数据
|
|
274
337
|
|
|
275
338
|
用户同意后,搭子可以读用户的睡眠、运动等数据。数据按 Dataset 分项,用户逐项同意。搭子拿不到用户的真实 id。
|
|
276
339
|
|
|
277
|
-
|
|
340
|
+
这里有三层不同概念:
|
|
278
341
|
|
|
279
|
-
|
|
342
|
+
1. **Runtime 能力目录**:当前 SDK 支持哪些 Dataset,由包导出的 `DATA_ACCESS_CAPABILITIES` 提供。
|
|
343
|
+
2. **搭子数据声明**:这只搭子实际需要其中哪些 Dataset,以及各自用途,写在 `BuddyServer.start.dataRequirements`。
|
|
344
|
+
3. **用户 Grant**:用户是否同意这只搭子读取某个已声明 Dataset,由客户端授权流程管理,搭子不能代替用户授予。
|
|
280
345
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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.
|
|
493
|
+
### 3.5 小挂件
|
|
344
494
|
|
|
345
|
-
小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.
|
|
495
|
+
小挂件是会话里一块给用户看的结构化面板,比如一份菜单。模型通过平台工具、开发者通过 `conversation.widget`,都能建小挂件、写记录、读记录、删记录,并把它发进聊天。
|
|
346
496
|
|
|
347
497
|
**放进工程目录**
|
|
348
498
|
|
|
@@ -360,22 +510,22 @@ my-buddy/ 工程目录
|
|
|
360
510
|
|
|
361
511
|
**让模型使用**
|
|
362
512
|
|
|
363
|
-
把要用的 `widget_*`
|
|
513
|
+
把要用的 `conversation.tools.widget_*` 放进 `initialState.tools`。
|
|
364
514
|
|
|
365
515
|
- 工具说明里附上各类型的模块与字段。改了 `widgets/` 要重启进程。
|
|
366
|
-
-
|
|
516
|
+
- 这场会话里已有哪些小挂件,平台会在消息里告诉模型。
|
|
367
517
|
- 平台请求失败时,模型只看到「平台请求失败(HTTP 409),请查询最新状态后再操作」这类提示,不带具体原因。
|
|
368
518
|
- `widget_send` 把小挂件放进发送队列就返回,不等平台确认送达。
|
|
369
519
|
|
|
370
520
|
**在自己的代码里用**
|
|
371
521
|
|
|
372
|
-
调用 `conversation.
|
|
522
|
+
调用 `conversation.widget`,不需要把 `widget_*` 放进工具:
|
|
373
523
|
|
|
374
524
|
```ts
|
|
375
525
|
execute: async (toolCallId, params) => {
|
|
376
|
-
const { id } = await conversation.
|
|
377
|
-
await conversation.
|
|
378
|
-
conversation.
|
|
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.
|
|
549
|
+
- 主动服务回合里,除了 `list`、`read`、`readModule`,其余方法都会抛异常,见 3.6。
|
|
400
550
|
|
|
401
551
|
用法见 [examples/aunt-tu/](examples/aunt-tu/)。
|
|
402
552
|
|
|
403
|
-
### 3.
|
|
553
|
+
### 3.6 主动服务
|
|
404
554
|
|
|
405
555
|
让搭子在约定的时机主动找用户说话,比如每周一次复盘。只支持私聊。
|
|
406
556
|
|
|
@@ -435,36 +585,17 @@ await conversation.proactive.propose({
|
|
|
435
585
|
|
|
436
586
|
主动回合里:
|
|
437
587
|
|
|
438
|
-
-
|
|
439
|
-
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
-
|
|
469
|
-
-
|
|
470
|
-
-
|
|
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
|
-
/**
|
|
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
|
|
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/
|
|
24
|
+
/** 搭子目录下 resources/ 里的资料;空数组时 resource_list 回「没有资料」 */
|
|
22
25
|
resources: Resource[];
|
|
23
|
-
dataRequirements:
|
|
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
|
-
/**
|
|
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":"
|
|
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"}
|