@my-life-buddies/buddy-runtime 0.1.0 → 0.3.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 (79) hide show
  1. package/README.md +173 -73
  2. package/dist/conversation/agent.d.ts +16 -2
  3. package/dist/conversation/agent.d.ts.map +1 -1
  4. package/dist/conversation/agent.js +83 -54
  5. package/dist/conversation/agent.js.map +1 -1
  6. package/dist/conversation/agentMessageLLMMessageConverter.d.ts +5 -2
  7. package/dist/conversation/agentMessageLLMMessageConverter.d.ts.map +1 -1
  8. package/dist/conversation/agentMessageLLMMessageConverter.js +33 -33
  9. package/dist/conversation/agentMessageLLMMessageConverter.js.map +1 -1
  10. package/dist/conversation/history.js +1 -1
  11. package/dist/conversation/history.js.map +1 -1
  12. package/dist/conversation/index.d.ts +19 -2
  13. package/dist/conversation/index.d.ts.map +1 -1
  14. package/dist/conversation/index.js +183 -13
  15. package/dist/conversation/index.js.map +1 -1
  16. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +6 -3
  17. package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
  18. package/dist/conversation/mlbMessageAgentMessageConverter.js +78 -23
  19. package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
  20. package/dist/conversation/readMLBMessages.d.ts +5 -1
  21. package/dist/conversation/readMLBMessages.d.ts.map +1 -1
  22. package/dist/conversation/readMLBMessages.js +24 -10
  23. package/dist/conversation/readMLBMessages.js.map +1 -1
  24. package/dist/conversation/writeMLBMessage.d.ts +3 -1
  25. package/dist/conversation/writeMLBMessage.d.ts.map +1 -1
  26. package/dist/conversation/writeMLBMessage.js +5 -3
  27. package/dist/conversation/writeMLBMessage.js.map +1 -1
  28. package/dist/index.d.ts +6 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -0
  31. package/dist/index.js.map +1 -1
  32. package/dist/mlbClient.d.ts +1 -128
  33. package/dist/mlbClient.d.ts.map +1 -1
  34. package/dist/mlbClient.js +2 -375
  35. package/dist/mlbClient.js.map +1 -1
  36. package/dist/platform/client.d.ts +48 -0
  37. package/dist/platform/client.d.ts.map +1 -0
  38. package/dist/platform/client.js +24 -0
  39. package/dist/platform/client.js.map +1 -0
  40. package/dist/platform/mlbClient.d.ts +146 -0
  41. package/dist/platform/mlbClient.d.ts.map +1 -0
  42. package/dist/platform/mlbClient.js +476 -0
  43. package/dist/platform/mlbClient.js.map +1 -0
  44. package/dist/platform/widgets.d.ts +75 -0
  45. package/dist/platform/widgets.d.ts.map +1 -0
  46. package/dist/platform/widgets.js +88 -0
  47. package/dist/platform/widgets.js.map +1 -0
  48. package/dist/server.d.ts.map +1 -1
  49. package/dist/server.js +36 -4
  50. package/dist/server.js.map +1 -1
  51. package/dist/tools/dataAccess.d.ts +7 -2
  52. package/dist/tools/dataAccess.d.ts.map +1 -1
  53. package/dist/tools/dataAccess.js +14 -4
  54. package/dist/tools/dataAccess.js.map +1 -1
  55. package/dist/tools/registry.d.ts +6 -0
  56. package/dist/tools/registry.d.ts.map +1 -0
  57. package/dist/tools/registry.js +23 -0
  58. package/dist/tools/registry.js.map +1 -0
  59. package/dist/tools/widgets.d.ts +3 -2
  60. package/dist/tools/widgets.d.ts.map +1 -1
  61. package/dist/tools/widgets.js +22 -53
  62. package/dist/tools/widgets.js.map +1 -1
  63. package/dist/types.d.ts +92 -24
  64. package/dist/types.d.ts.map +1 -1
  65. package/dist/types.js +3 -3
  66. package/dist/types.js.map +1 -1
  67. package/package.json +4 -3
  68. package/dist/conversation/convert.d.ts +0 -46
  69. package/dist/conversation/convert.d.ts.map +0 -1
  70. package/dist/conversation/convert.js +0 -297
  71. package/dist/conversation/convert.js.map +0 -1
  72. package/dist/conversation/presence.d.ts +0 -24
  73. package/dist/conversation/presence.d.ts.map +0 -1
  74. package/dist/conversation/presence.js +0 -56
  75. package/dist/conversation/presence.js.map +0 -1
  76. package/dist/conversation/writeBack.d.ts +0 -36
  77. package/dist/conversation/writeBack.d.ts.map +0 -1
  78. package/dist/conversation/writeBack.js +0 -114
  79. package/dist/conversation/writeBack.js.map +0 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@ MLB 搭子运行时。基于 [`@earendil-works/pi-agent-core`](https://www.npmjs
5
5
  ## 1. 安装
6
6
 
7
7
  ```bash
8
- npm install @my-life-buddies/buddy-runtime
8
+ npm install @my-life-buddies/buddy-runtime typebox
9
9
  ```
10
10
 
11
11
  要求 Node.js ≥ 22.19。
@@ -67,6 +67,7 @@ await BuddyServer.start({
67
67
  | 参数 | 类型 | 说明 |
68
68
  |---|---|---|
69
69
  | `options.model` | `string`,必填 | 模型 id,取值见平台 `GET /cli/models`。进程内所有会话共用。没填,或 id 在运行时内置的模型目录中不存在时,启动失败 |
70
+ | `options.welcome` / `options.errorReply` | `string`,可选 | 应用提供的静态问候,以及模型出错、空回复时发的兜底回复 |
70
71
  | `options.buddy` | `(conversation: Conversation) => BuddyOptions \| Promise<BuddyOptions>` | 搭子配置工厂。每场会话在第一条消息到达时调用一次 |
71
72
 
72
73
  ### 3.2 `BuddyOptions`
@@ -76,40 +77,74 @@ await BuddyServer.start({
76
77
  ```ts
77
78
  type BuddyOptions =
78
79
  Pick<AgentOptions,
79
- | "transformContext" | "beforeToolCall" | "afterToolCall"
80
+ | "beforeToolCall" | "afterToolCall"
80
81
  | "shouldStopAfterTurn" | "prepareNextTurnWithContext"
81
82
  | "thinkingBudgets" | "toolExecution">
82
- & { initialState: Pick<AgentState, "systemPrompt"> & Partial<Pick<AgentState, "tools" | "thinkingLevel">> };
83
+ & { platformTools?: readonly PlatformToolName[]; initialState: Pick<AgentState, "systemPrompt"> & Partial<Pick<AgentState, "tools" | "thinkingLevel">> };
83
84
  ```
84
85
 
85
86
  | 字段 | 说明 |
86
87
  |---|---|
87
- | `initialState.systemPrompt` | 系统提示。静态内容;每回合变化的信息不应写在这里。工程目录下有 `resources/` 时,运行时在末尾追加资料清单 |
88
- | `initialState.tools` | 工具列表,类型为 pi 的 `AgentTool`。`label` 为必填,用作用户可见的状态行文案(空字符串表示不显示);`execute` 返回值的 `details` 为必填(可为 `undefined`);执行失败应直接抛出异常。工具名不得与内置工具或其他工具重复。先单独声明为 `AgentTool<typeof 参数 schema>` 再放入数组,`execute` 的参数才有类型;直接写在数组里时参数类型为 `unknown` |
88
+ | `platformTools` | 要注册给模型的平台工具名单,默认 `[]`,见 3.3 |
89
+ | `initialState.systemPrompt` | 系统提示。静态内容;每回合变化的信息不应写在这里。运行时在前面拼上平台的系统提示(平台对每场会话都一样的规则,比如消息开头的时间怎么读),两者之间空一行;`platformTools` 里有 `read_resource` 且有 `resources/` 时,运行时在末尾追加资料清单 |
90
+ | `initialState.tools` | 工具列表,类型为 pi 的 `AgentTool`。`label` 为必填,用作用户可见的状态行文案(空字符串表示不显示);`execute` 返回值的 `details` 为必填(可为 `undefined`);执行失败应直接抛出异常。工具名不得与平台工具或其他工具重复。先单独声明为 `AgentTool<typeof 参数 schema>` 再放入数组,`execute` 的参数才有类型;直接写在数组里时参数类型为 `unknown` |
89
91
  | `initialState.thinkingLevel` | 思考级别,默认 `off`。模型不支持的级别在发请求前就近换成支持的一档。显式设了模型不支持的级别时,每场会话建立时打一条 warn |
90
- | `transformContext` | 历史裁剪。入参是这场会话完整的 Agent 历史,平台交代的场合也在其中,消息从哪来、长什么样见下方「4. 会话历史」一节;返回值只用于这一次模型请求,不改动历史;抛出异常时使用原历史 |
91
92
  | `beforeToolCall` / `afterToolCall` / `shouldStopAfterTurn` / `prepareNextTurnWithContext` / `thinkingBudgets` / `toolExecution` | 原样传递给 pi |
92
93
 
93
- 密钥、会话历史、会话标识与插话策略由运行时统一管理,不对开发者开放。模型通过 `BuddyServer.start` 的 `model` 选择。
94
+ 密钥、会话历史与插话策略由运行时统一管理;工厂提供当前会话键与已绑定的平台能力,不传递 Token。模型通过 `BuddyServer.start` 的 `model` 选择。
94
95
 
95
- ### 3.3 `Conversation`:给开发者调用的方法
96
+ ### 3.3 `Conversation` 与平台能力
96
97
 
97
- 工厂拿到的对象。上面的属性和方法由开发者在代码里调用,不会注册给模型;要让模型用到,开发者自己包成工具放进 `initialState.tools`,见下方 `remember` 的例子。
98
+ 工厂拿到的对象,可在工具闭包中使用。会话之间的数据应以 `key` 隔离。
98
99
 
99
100
  ```ts
100
101
  interface Conversation {
101
- readonly key: string; // 会话键:私聊为 p_ 前缀,群聊为 g_ 前缀
102
+ readonly key: string; // 会话键:私聊为 p_ 前缀,群聊为 g_ 前缀
102
103
  readonly log: BuddyLogger;
104
+ readonly signal: AbortSignal; // 会话删除、空闲淘汰、stop 时中止
105
+ readonly platform: PlatformClient; // 已绑好这场会话的平台能力
106
+ }
107
+
108
+ interface PlatformClient {
103
109
  readonly memory: {
104
- list(): Promise<MemoryEntry[]>; // 全量列出,按 key 字典序
105
- write(key: string, text: string): Promise<void>; // 覆盖写一条
106
- delete(key: string): Promise<void>; // 删一条,删不存在的也算成功
107
- };
108
- readonly dataAccess: {
109
- query(request: { datasets: string[]; memberId?: string }): Promise<DatasetAccessResult[]>; // 读用户授权的数据,见 3.4.2
110
+ list(): Promise<MemoryEntry[]>;
111
+ write(key: string, text: string): Promise<void>;
112
+ delete(key: string): Promise<void>;
110
113
  };
114
+ readonly dataAccess: { query(request: { datasets: string[]; memberId?: string }): Promise<DatasetAccessResult[]> };
115
+ readonly proactive: { propose(request: ProactiveSubscriptionRequest): Promise<{ id: string; status: "pending" }> };
116
+ readonly widgets: WidgetClient;
117
+ readonly messages: { send(message: MLBMessageToWrite & { idempotencyKey: string }): void };
111
118
  }
119
+ ```
120
+
121
+ `platform` 下的方法由开发者在代码里调用,会话键已经绑好,不需要传 Token 或拼 HTTP 路径。其中授权数据、小挂件、资料三项,运行时还提供了给模型用的工具,把工具名写进 `BuddyOptions.platformTools` 才注册:
112
122
 
123
+ | 能力 | 代码里调用 | 给模型的工具 | 工具注册还要满足 |
124
+ |---|---|---|---|
125
+ | 记忆 | `platform.memory` | 无,要给模型用就自己包成工具,见 3.3.1 | 无 |
126
+ | 授权数据 | `platform.dataAccess.query` | `data_access_query` | 声明过至少一个 Dataset |
127
+ | 小挂件 | `platform.widgets` | `widget_create` / `widget_write` / `widget_read` / `widget_delete` / `widget_send` | 登记过至少一个小挂件类型 |
128
+ | 资料 | 无 | `read_resource` | `resources/` 下有 `.md` |
129
+ | 发消息 | `platform.messages.send` | 无 | 无 |
130
+ | 主动服务建议 | `platform.proactive.propose`,见第 7 节 | 无 | 用户在 App 确认后生效 |
131
+
132
+ - `platformTools` 默认 `[]`,一个平台工具都不注册。名单里有不认识或重复的名字时,这场会话起不来。
133
+ - 注册的平台工具排在 `initialState.tools` 前面;和开发者的工具重名时,这场会话起不来。
134
+ - 没写进名单的,照样可以在代码里调用 `platform`。
135
+
136
+ ```ts
137
+ return {
138
+ platformTools: ["data_access_query", "widget_read"],
139
+ initialState: { systemPrompt: PERSONA, tools: [remember] },
140
+ };
141
+ ```
142
+
143
+ #### 3.3.1 记忆
144
+
145
+ `platform.memory` 是这场会话的长期记忆,由平台保存,只在本会话内可见,会话删除时平台一并清除。`key` 为主题名(非空、不含空白与 `/`,最长 64 字),`text` 单条不超过 16KB;每场会话最多 64 条、合计不超过 256KB。`list` 全量列出,按 `key` 字典序;`write` 覆盖写一条;`delete` 删一条,删不存在的也算成功。超出配额、`key` 不合法或网络出错时直接抛出异常,异常信息为平台给出的原因;在工具的 `execute` 中调用时,模型能看到这个原因。
146
+
147
+ ```ts
113
148
  interface MemoryEntry {
114
149
  key: string; // 主题名
115
150
  text: string;
@@ -117,9 +152,7 @@ interface MemoryEntry {
117
152
  }
118
153
  ```
119
154
 
120
- 传入配置工厂,可在工具闭包中使用。会话之间的数据应以 `key` 隔离。
121
-
122
- `memory` 是这场会话的长期记忆,由平台保存,只在本会话内可见,会话删除时平台一并清除;会话键已经绑好,调用时无需传入。`key` 为主题名(非空、不含空白与 `/`,最长 64 字),`text` 单条不超过 16KB;每场会话最多 64 条、合计不超过 256KB。超出配额、`key` 不合法或网络出错时直接抛出异常,异常信息为平台给出的原因;在工具的 `execute` 中调用时,模型能看到这个原因。
155
+ 运行时不提供记忆工具,要让模型记东西,开发者自己包成工具:
123
156
 
124
157
  ```ts
125
158
  const REMEMBER_PARAMS = Type.Object({ topic: Type.String(), text: Type.String() });
@@ -133,7 +166,7 @@ await BuddyServer.start({
133
166
  description: "把一条关于对方的印象记下来,同一主题覆盖旧的。",
134
167
  parameters: REMEMBER_PARAMS,
135
168
  execute: async (_id, { topic, text }) => {
136
- await conversation.memory.write(topic, text);
169
+ await conversation.platform.memory.write(topic, text);
137
170
  return { content: [{ type: "text", text: "记下了。" }], details: undefined };
138
171
  },
139
172
  };
@@ -142,42 +175,11 @@ await BuddyServer.start({
142
175
  });
143
176
  ```
144
177
 
145
- 完整示例见 [examples/aunt-tu/](examples/aunt-tu/):`taste_read` / `taste_write` 两个工具用 `memory` 记每个人爱吃什么、不爱吃什么。
146
-
147
- `dataAccess.query` 读取用户授权给搭子的数据,用法见 3.4.2。
148
-
149
- ### 3.4 内置工具:运行时注册给模型
150
-
151
- 以下工具由运行时注册到 Agent,排在开发者工具之前,由模型调用。开发者不需要注册,也不能再注册同名工具;系统提示中可按名称引用。
152
-
153
- | 工具 | 何时注册 | 做什么 |
154
- |---|---|---|
155
- | `read_resource` | 工程目录下有 `resources/`,且其中有 `.md` | 按资料清单里的相对路径读一篇资料 |
156
- | `data_access_query` | 搭子声明过至少一个 Dataset | 按需读取当前会话用户授权的数据;单次最多 8 个 Dataset,响应上限 512 KiB |
157
- | `widget_create` / `widget_write` / `widget_read` / `widget_delete` / `widget_send` | 搭子在平台上登记了小挂件类型 | 建小挂件、写 / 读 / 删记录、把小挂件发进聊天 |
158
-
159
- #### 3.4.1 资料
160
-
161
- 资料放在工程目录下的 `resources/` 里,是一篇篇 `.md` 文件。运行时自动让模型按需读取,开发者不需要写代码。
162
-
163
- - 读哪些文件:进程启动时读一次 `resources/` 下全部 `.md`,包括子目录;跳过以 `.` 开头的文件与目录,以及 `node_modules`、`venv`、`.venv`、`__pycache__`;其他扩展名的文件忽略。资料改动后需重启进程。
164
- - 模型看到什么:系统提示末尾追加一份资料清单,列出每篇资料相对 `resources/` 的路径;模型调用 `read_resource` 并传入清单中的路径,读到该篇全文。
165
- - 没有资料时:`resources/` 不存在或其中没有 `.md`,不注册 `read_resource`,也不追加清单。
166
-
167
- ```
168
- my-buddy/ 工程目录
169
- ├─ package.json
170
- ├─ resources/ 资料目录
171
- │ └─ research/
172
- │ └─ 01-writings.md 清单中写作 research/01-writings.md
173
- └─ src/main.ts 入口脚本
174
- ```
175
-
176
- 完整示例见 [examples/stacy-sims/](examples/stacy-sims/)。
178
+ 完整示例见 [examples/aunt-tu/](examples/aunt-tu/):`taste_read` / `taste_write` 两个工具用 `platform.memory` 记每个人爱吃什么、不爱吃什么。
177
179
 
178
- #### 3.4.2 授权数据
180
+ #### 3.3.2 授权数据
179
181
 
180
- 经用户同意,搭子可以读取用户授权数据。数据按 Dataset 分项,每项内容与字段固定,用户逐项授权。开发者要做的是声明需要哪些 Dataset;查询工具由运行时注册给模型,开发者的工具里也能直接查。
182
+ 经用户同意,搭子可以读取用户授权数据。数据按 Dataset 分项,每项内容与字段固定,用户逐项授权。开发者要做的是声明需要哪些 Dataset,然后让模型用 `data_access_query` 查,或者在自己的工具里查。会话与真实用户身份由平台绑定,搭子拿不到真实用户 id。
181
183
 
182
184
  **声明需要哪些数据**
183
185
 
@@ -213,20 +215,20 @@ my-buddy/ 工程目录
213
215
 
214
216
  **让模型去查**
215
217
 
216
- 声明过至少一项数据,模型就有了 `data_access_query` 工具,工具说明里列着开发者声明的数据与用途。开发者可以在系统提示里写明什么时候查,例如「聊到作息时,先用 data_access_query 查 health.sleep」。
218
+ `platformTools` 里有 `data_access_query`,且声明过至少一项数据,模型就有了这个工具,工具说明里列着开发者声明的数据与用途。开发者可以在系统提示里写明什么时候查,例如「聊到作息时,先用 data_access_query 查 health.sleep」。
217
219
 
218
220
  **在自己的工具里查**
219
221
 
220
- 调用 `conversation.dataAccess.query`:
222
+ 调用 `conversation.platform.dataAccess.query`,不需要写进 `platformTools`:
221
223
 
222
224
  ```ts
223
- const [sleep] = await conversation.dataAccess.query({ datasets: ["health.sleep"] });
225
+ const [sleep] = await conversation.platform.dataAccess.query({ datasets: ["health.sleep"] });
224
226
  if (sleep.status === "available") {
225
227
  // sleep.data.items 是每天一条的睡眠记录
226
228
  }
227
229
  ```
228
230
 
229
- - `datasets`:一次最多 8 个。
231
+ - `datasets`:一次最多 8 个;响应超过 512 KiB 时抛出异常,减少 Dataset 后重试。
230
232
  - `memberId`:私聊不传;群聊必须传,即群消息前缀 `[名字 #m_…]` 里的 `m_…`,查的是这位成员的数据。传错时直接抛出异常。
231
233
 
232
234
  **查询结果**
@@ -243,33 +245,109 @@ if (sleep.status === "available") {
243
245
 
244
246
  **数据只在这一轮回复里可见**
245
247
 
246
- 一轮回复里只要查过授权数据,这一轮结束后,聊天记录和会话历史里这一轮的工具参数、工具结果与 thinking,都会换成「查了哪些数据、各是什么状态」,给用户看的回复照常保留。开发者自己写进 `memory` 或小挂件的内容不会被替换,不要把查到的数据写进去。
248
+ 一轮回复里只要查过授权数据,这一轮结束后,聊天记录和会话历史里这一轮的工具参数、工具结果与 thinking,都会换成「查了哪些数据、各是什么状态」,给用户看的回复照常保留。开发者自己写进记忆或小挂件的内容不会被替换,不要把查到的数据写进去。
247
249
 
248
- #### 3.4.3 小挂件
250
+ #### 3.3.3 小挂件
251
+
252
+ **登记类型**
249
253
 
250
254
  开发者通过平台 CLI 接口给搭子登记小挂件类型:`PUT /cli/buddies/:id/widget-types/:typeId/schema` 交 schema,每个模块写一句 `description` 给模型读,每条记录的形状是一份 JSON Schema;`PUT /cli/buddies/:id/widget-types/:typeId/page` 交展示页面,单个 HTML 文件。
251
255
 
252
- 运行时在第一场会话建立时拉一次 `GET /internal/v1/widget-types`,按拉到的类型注册五个 `widget_*` 工具,工具说明里附上各类型的模块与字段;一个类型都没登记时不注册。这场会话里已有哪些小挂件,由平台写进消息流的会话上下文告诉模型。
256
+ **让模型去用**
257
+
258
+ `platformTools` 里写了哪几个 `widget_*`,运行时就注册哪几个:进程里第一次要用时拉一次 `GET /internal/v1/widget-types`,之后共用;工具说明里附上各类型的模块与字段;一个类型都没登记时一个也不注册。这场会话里已有哪些小挂件,由平台消息流里的 `context` 记录告诉模型。
259
+
260
+ - 平台请求失败时工具抛出异常,模型看到的是「平台请求失败(HTTP 409),请查询最新状态后再操作」这样的提示,不带平台返回的具体原因。
261
+ - `widget_create` 用 `widget-create:<toolCallId>` 作幂等键,同一次工具调用重发不会建出两个。
262
+ - `widget_send` 把小挂件放进这场会话的发送队列就返回,不等平台确认送达。
263
+
264
+ **在自己的工具里用**
265
+
266
+ 调用 `conversation.platform.widgets`,不需要写进 `platformTools`:
267
+
268
+ ```ts
269
+ execute: async (toolCallId, params, signal) => {
270
+ const widgets = conversation.platform.widgets.withSignal(signal);
271
+ const { id } = await widgets.create({ type: "itinerary", title: "杭州行程", idempotencyKey: `itinerary:${toolCallId}` });
272
+ await widgets.write(id, { module: "days", recordId: "day-1", data: { city: "杭州" } });
273
+ widgets.send(id, { idempotencyKey: `itinerary-send:${toolCallId}` });
274
+ return { content: [{ type: "text", text: "行程建好了。" }], details: undefined };
275
+ },
276
+ ```
277
+
278
+ | 方法 | 做什么 | 平台接口 |
279
+ |---|---|---|
280
+ | `list()` | 列出这场会话里的小挂件,不带记录 | `GET /internal/v1/widgets` |
281
+ | `create({ type, title, idempotencyKey? })` | 新建一个,回 `{ id }`;同一个 `idempotencyKey` 重复建回同一个 id | `POST /internal/v1/widgets` |
282
+ | `read(id)` | 读整个小挂件,含各模块的记录 | `GET /internal/v1/widgets/:widgetId` |
283
+ | `readModule(id, module)` | 读一个模块,回 `{ records }` | `GET /internal/v1/widgets/:widgetId/:module` |
284
+ | `rename(id, title)` | 改标题,id 不变 | `PUT /internal/v1/widgets/:widgetId` |
285
+ | `write(id, { module, recordId, data, expectedRevision? })` | 写一条记录,已存在就整条覆盖;带 `expectedRevision` 时版本号对不上,平台回 409 | `PUT /internal/v1/widgets/:widgetId/:module/:recordId` |
286
+ | `deleteRecord(id, module, recordId)` | 删一条记录 | `DELETE /internal/v1/widgets/:widgetId/:module/:recordId` |
287
+ | `setPresentation(id, { summary, imageMediaId })` | 设置聊天流里小挂件卡片的摘要(不超过 500 字)和封面图,`imageMediaId` 传空串去掉封面 | `PUT /internal/v1/widgets/:widgetId/presentation` |
288
+ | `send(id, { idempotencyKey })` | 放进发送队列,按顺序发一条 `type: "widget"` 的消息 | `POST /internal/v1/messages` |
289
+ | `withSignal(signal)` | 回一个同时受这个 signal 与会话 `signal` 控制的 Client | 无 |
290
+
291
+ - 每个方法最后都可以传 `{ signal }`。
292
+ - `data` 要符合登记的 schema,不符合时平台回 400。
293
+ - 平台回非 2xx、网络出错、超过 30 秒或响应超过 4 MiB 时抛出 `PlatformRequestError`,异常只带 HTTP 状态码。写操作不自动重试,失败后先读一遍真实状态再决定。
253
294
 
254
295
  用法见 [examples/aunt-tu/](examples/aunt-tu/)。
255
296
 
297
+ #### 3.3.4 资料
298
+
299
+ 资料放在工程目录(`package.json` 所在目录)下的 `resources/`。工程目录由入口脚本向上定位,与启动进程的工作目录无关。
300
+
301
+ - 启动时递归读取 `.md`,跳过 `node_modules`、`venv`、`.venv`、`__pycache__` 及点开头的目录/文件;资料变更后需重启进程。
302
+ - `platformTools` 里有 `read_resource` 时,系统提示末尾追加资料清单;模型通过清单中的相对路径读取内容。
303
+ - 没有资料或名单里没有 `read_resource` 时,不注册 `read_resource`,也不追加资料清单。
304
+
305
+ ```
306
+ my-buddy/ 工程目录
307
+ ├─ package.json
308
+ ├─ resources/ 资料目录
309
+ │ └─ research/
310
+ │ └─ 01-writings.md 清单中写作 research/01-writings.md
311
+ └─ src/main.ts 入口脚本
312
+ ```
313
+
314
+ 完整示例见 [examples/stacy-sims/](examples/stacy-sims/)。
315
+
316
+ #### 3.3.5 发消息与图片
317
+
318
+ **发消息**
319
+
320
+ `platform.messages.send(message)` 把一条消息放进这场会话的发送队列,和模型的回复排在同一条队列里按顺序发(`POST /internal/v1/messages`),调用后立即返回,不等平台确认。
321
+
322
+ - `message.type` 取 `message` / `tool` / `note` / `widget`;`idempotencyKey` 必填。
323
+ - 会话已经删除或淘汰时直接抛出异常。
324
+
325
+ **图片**
326
+
327
+ 用户发的图不需要开发者处理。平台在拉取消息时把图片内容一起给运行时,运行时把它放进会话历史,之后每次请求模型原样带上:
328
+
329
+ - 图片在用户上传时由平台统一处理成 JPEG,最长边不超过 2048 px、不超过 5 MiB。
330
+ - 图片消息交给模型时是一条 user 消息:正文前标着「[图片 #消息 id]」,用户配了文字接在后面,再跟图片本身。
331
+ - 引用了别的消息的,开头加一行:引用文字写「[引用 发言人:原话]」,引用图片写「[引用 发言人的图片 #被引消息 id]」,模型按 id 对上是哪一张。
332
+ - 转发进聊天记录卡的图,跟在卡里那条正文后面。
333
+ - 图片已经读不到时换成「[图片无法读取]」;`model` 选的模型不支持看图时换成「[图片:当前模型不支持看图]」。
334
+
256
335
  ## 4. 会话历史:MLBMessage → AgentMessage → LLMMessage
257
336
 
258
337
  开发者不需要保存会话历史,也不需要把聊天里的消息换成模型消息,这些由运行时完成。每场会话的消息都保存在平台上,搭子进程重启后历史照样在。从平台上的 `MLBMessage` 到模型收到的 `LLMMessage`:
259
338
 
260
339
  ```
261
- MLBMessage[] → AgentMessage[] → transformContext() → AgentMessage[] → convertToLlm() → LLMMessage[] → LLM
262
- ▲ (1) │ ▲ (2) (3)
263
- └─────(5)──────┘ └──────────────────────────────────────(4)───────────────────────────────────────┘
340
+ MLBMessage[] → AgentMessage[] → convertToLlm() → LLMMessage[] → LLM
341
+ ▲ (1) │ ▲ (2) │
342
+ └─────(4)──────┘ └──────────────────(3)─────────────────────┘
264
343
  ```
265
344
 
266
- 1. 平台上的 `MLBMessage` 换成 pi 的 `AgentMessage`,追加进会话历史。一条消息只换一次:会话起来时换一遍平台上已有的全部消息,之后来一条换一条。什么消息换成什么,见 4.1
267
- 2. `transformContext()`:开发者可选。入参是完整的会话历史,返回值只用于这一次请求,不改动历史。
268
- 3. `convertToLlm()`:运行时实现,开发者不能替换,做什么见 4.3。换出来的 `LLMMessage`(即 pi-ai 的 `Message`)连同 `systemPrompt` 与 `tools` 发给模型。
269
- 4. 模型的回复,以及它调用工具得到的结果,直接追加进会话历史,不经过 (1)
270
- 5. 经 (4) 追加进历史的消息,运行时再换成 `MLBMessage` 写回平台;经 (1) 从平台来的不会写回。用户在聊天里看到的回复就是这一步写上去的。用过授权数据的那一轮,写回前先脱敏,见 3.4.2。搭子重启后,这些消息经 (1) 取回。
345
+ 1. 平台上的 `MLBMessage` 换成 pi 的 `AgentMessage`,追加进会话历史。一条消息只换一次:会话起来时换一遍平台上已有的全部消息,之后来一条换一条。图片、引用和聊天记录卡怎么换,见 3.3.5
346
+ 2. `convertToLlm()`:运行时实现,开发者不能替换。每次请求模型都拿完整的会话历史来换:每条消息正文开头拼上它的时间,统一用 GMT、精确到秒,如「[2026-09-10 14:00:30 GMT] 」;运行时补的提醒(note)换成 user,其余原样,按原来的先后一条对一条,相邻的 user 不合并。换之前,运行时先在历史末尾接一条提醒,开头的时间就是现在,正文是「现在的时间(平台附注,不是谁说的话)」;这条只进这一次请求,不进会话历史。这些时间怎么读、用户所在时区去哪看,写在平台的系统提示里。换出来的 `LLMMessage`(即 pi-ai 的 `Message`)连同 `systemPrompt` 与 `tools` 发给模型。
347
+ 3. 模型的回复,以及它调用工具得到的结果,直接追加进会话历史,不经过 (1)
348
+ 4. (3) 追加进历史的消息,运行时再换成 `MLBMessage` 写回平台;经 (1) 从平台来的不会写回。用户在聊天里看到的回复就是这一步写上去的。用过授权数据的那一轮,写回前先脱敏,见 3.3.2。搭子重启后,这些消息经 (1) 取回。
271
349
 
272
- (1) 在消息到来时发生;(2)、(3) 在每次请求模型前都完整跑一遍,一轮回复里模型可能请求好几次,比如调完工具接着说,每次都从当时完整的会话历史重新算;(4)、(5) 在模型每说完一段、每跑完一步工具时发生。
350
+ (1) 在消息到来时发生;(2) 在每次请求模型前都完整跑一遍,一轮回复里模型可能请求好几次,比如调完工具接着说,每次都从当时完整的会话历史重新算;(3)、(4) 在模型每说完一段、每跑完一步工具时发生。
273
351
 
274
352
  ## 5. 环境变量
275
353
 
@@ -278,12 +356,34 @@ MLBMessage[] → AgentMessage[] → transformContext() → AgentMessage[] → co
278
356
  | `MLB_BUDDY_ID` | 搭子 id,注册时由平台分配 |
279
357
  | `MLB_BUDDY_TOKEN` | 搭子令牌,注册时由平台分配 |
280
358
 
281
- 网关地址无需配置,搭子连接测试平台 ecs-test(`http://47.116.168.81:8788`)。
359
+ `MLB_GATEWAY_URL` 可覆盖平台网关。当前缺省为 ecs-test(`http://47.116.168.81:8788`);本地测试必须显式配置回环地址及虚拟凭据,不能用正式搭子 Token 启动第二个进程。
282
360
 
283
361
  搭子的档案(名称、头像、简介)由平台保存:注册时通过 `POST /cli/buddies` 提交,修改通过 `PUT /cli/buddies/:id/meta`。搭子进程不监听端口、不接收入站请求。从非回环地址连接网关时,平台将其识别为 `<id>-dev` 分身,会话与正式身份隔离。
284
362
 
285
363
  ## 6. 开发
286
364
 
287
- - `npm run dev`:以 `node --watch` 启动,文件变更后自动重启。停止时需终止整个进程树(`pkill -f "node --watch .* src/main.ts"`),仅终止子进程会被父进程重新拉起。
365
+ - 改完代码需要自动重启时,自行在 `package.json` 里用 `node --watch` 启动。
288
366
  - 会话消息拉取遇到临时网络错误时会主动指数退避重试(1 秒起步、最高 30 秒);新的 SSE 通知会取消等待并立即重拉。
289
- - 运行时仓库:`npm test`、`npm run typecheck`、`npm run build`。
367
+
368
+ ## 7. 主动服务
369
+
370
+ Runtime 消费平台 SSE `proactive {runId}`,领取 Run 后恢复对话、临时召回会话 Memory,并让模型按需查询授权 Dataset。数据查询携带短期执行令牌;最终 send/skip 结果由平台复核和落库,不经过普通消息写口。
371
+
372
+ 主动回合仅开放 `platformTools` 已选择的 `data_access_query` 和 `read_resource`,不会自动增加工具。临时 Trigger、Memory 注入和工具数据在结束后移除;普通用户消息到达会中止主动生成并优先回复。执行重试归平台,Runtime 不自行维护调度器。
373
+
374
+ 普通聊天中的开发者工具可提出订阅建议:
375
+
376
+ ```ts
377
+ await conversation.platform.proactive.propose({
378
+ title: "每周复盘",
379
+ reason: "在约定时间结合最近聊天做一次简短复盘",
380
+ sources: ["schedule"],
381
+ eventTypes: ["schedule.due"],
382
+ allowedDataScopes: [],
383
+ schedule: { at: firstReviewAt, intervalMinutes: 7 * 24 * 60 },
384
+ });
385
+ ```
386
+
387
+ 返回的订阅始终是 pending,用户在 App 确认后才能触发。`at` 为毫秒 UTC 时间戳;固定间隔不承诺夏令时地区的墙上时间不变。
388
+
389
+ 此能力要求匹配的 Proactive Server 版本。旧 Server 不支持这些端点,不会自动降级为直接主动写消息。
@@ -1,11 +1,14 @@
1
1
  import { Agent } from "@earendil-works/pi-agent-core";
2
- import type { AgentEvent, StreamFn } from "@earendil-works/pi-agent-core";
2
+ import type { AgentEvent, AgentMessage, AgentTool, StreamFn } from "@earendil-works/pi-agent-core";
3
3
  import type { Api, Model } from "@earendil-works/pi-ai";
4
4
  import type { MLBClient, WidgetType } from "../mlbClient.ts";
5
5
  import { DataAccessUsageTracker } from "../tools/dataAccess.ts";
6
6
  import type { Resource } from "../tools/resource.ts";
7
- import type { BuddyFactory, BuddyLogger, DataRequirement } from "../types.ts";
7
+ import type { BuddyFactory, BuddyLogger, DataRequirement, MLBMessageToWrite } from "../types.ts";
8
8
  export interface AgentDeps {
9
+ signal?: AbortSignal;
10
+ enqueueMessage?: (message: MLBMessageToWrite) => void;
11
+ onPolicyStop?: () => void;
9
12
  key: string;
10
13
  buddyId: string;
11
14
  client: MLBClient;
@@ -26,6 +29,17 @@ export interface AgentDeps {
26
29
  export interface CreatedAgent {
27
30
  agent: Agent;
28
31
  dataAccessUsage: DataAccessUsageTracker;
32
+ proactiveReadTools: AgentTool[];
29
33
  }
30
34
  export declare function createAgent(deps: AgentDeps): Promise<CreatedAgent>;
35
+ /**
36
+ * 每次请求模型时接在历史末尾那条 note 的正文;现在的时间由 convertToLlm 按这条 note 的 timestamp 拼在开头。
37
+ * 方括号时间怎么读、回复里别写,交代在平台的 system prompt 里(my-life-buddies-server 的 src/messages/systemPrompt.ts),
38
+ * 那边按「现在的时间」这几个字指认这条;改这里的正文或时间格式,那边一起改。
39
+ */
40
+ export declare const CURRENT_TIME_NOTE = "\u73B0\u5728\u7684\u65F6\u95F4\uFF08\u5E73\u53F0\u9644\u6CE8\uFF0C\u4E0D\u662F\u8C01\u8BF4\u7684\u8BDD\uFF09";
41
+ /** 历史副本:末尾接一条 now 时刻的 note,传进来的数组不改 */
42
+ export declare function withCurrentTime(messages: AgentMessage[], now: number): AgentMessage[];
43
+ /** 打日志用的副本:图片块的 data、请求体里 base64 的 data URL 换成字节数,其余原样 */
44
+ export declare function withoutImageData(value: unknown): unknown;
31
45
  //# sourceMappingURL=agent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/conversation/agent.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,EAAE,MAAM,+BAA+B,CAAC;AACtD,OAAO,KAAK,EAAE,UAAU,EAAa,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAErF,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;AAErD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1G,MAAM,WAAW,SAAS;IACzB,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,WAAW,EAAE,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACzC,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,CA2GxE"}
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,EAAE,SAAS,EAAE,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;AAIrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAA8B,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE7H,MAAM,WAAW,SAAS;IACzB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACtD,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,WAAW,EAAE,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACzC,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;IACxC,kBAAkB,EAAE,SAAS,EAAE,CAAC;CAChC;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAqGxE;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iHAAuB,CAAC;AAEtD,uCAAuC;AACvC,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,9 +1,11 @@
1
1
  // 一场会话的 Agent 怎么建:运行时先把该填的填好,开发者的配置叠上去。
2
- // 管:拼好交给开发者工厂的 Conversation(会话键、日志、绑好会话键的 memory / DataAccess),跑工厂拿配置;
3
- // 运行时工具(DataAccess、小挂件、有资料时的 read_resource)排在开发者工具前面并查重名;有资料时把资料清单接在 systemPrompt 末尾;
2
+ // 管:拼好交给开发者工厂的 Conversation(会话键、日志、绑好会话键的平台能力),跑工厂拿配置;
3
+ // systemPrompt 平台的(GET /internal/v1/system-prompt,每场会话建 Agent 时拉一次)→ 开发者的 → 资料清单 拼,段间空一行;
4
+ // 显式选中的平台工具排在开发者工具前面并查重名;选择 read_resource 时才追加资料清单;
4
5
  // 开发者显式设的 thinkingLevel 这个模型不支持时打一条 warn,写明支持哪几档、实际会怎么发;
5
- // 钩子叠加——开发者的 transformContext 包 try/catch 抛了用原历史;开发者的 beforeToolCall 包 try/catch 抛了按放行;其余直通;
6
- // 模型调用日志只记规模,不打印消息、Tool 结果和请求体,避免用户对话与授权数据进入日志。
6
+ // 钩子叠加——开发者的 beforeToolCall 包 try/catch 抛了按放行;其余直通;
7
+ // transformContext 由运行时自己装、不开放给开发者:每次请求模型在历史末尾接一条写当前时间的 note,不进会话历史;
8
+ // 普通回合沿用模型输入日志,图片换成字节数;主动回合只记规模,临时 Trigger、Memory 和查询结果不入日志。
7
9
  // 不管:Agent 建好之后的事(回合、事件分发在 index.ts)。
8
10
  import { Agent } from "@earendil-works/pi-agent-core";
9
11
  import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
@@ -11,36 +13,39 @@ import { agentMessagesToLLMMessages } from "./agentMessageLLMMessageConverter.js
11
13
  import { errorText } from "../log.js";
12
14
  import { dataAccessTool, DataAccessUsageTracker } from "../tools/dataAccess.js";
13
15
  import { readResourceTool, resourceCatalog } from "../tools/resource.js";
16
+ import { createPlatformClient } from "../platform/client.js";
17
+ import { validatePlatformTools, registerTools } from "../tools/registry.js";
14
18
  import { widgetTools } from "../tools/widgets.js";
15
19
  export async function createAgent(deps) {
16
20
  const dataAccessUsage = new DataAccessUsageTracker();
21
+ const lifetimeSignal = deps.signal ?? new AbortController().signal;
22
+ const platform = createPlatformClient({
23
+ client: deps.client, key: deps.key, signal: lifetimeSignal,
24
+ assertWritable: () => dataAccessUsage.assertWritable(),
25
+ enqueueMessage: message => { if (!deps.enqueueMessage)
26
+ throw new Error("消息队列不可用"); deps.enqueueMessage(message); },
27
+ queryDataAccess: ({ datasets, memberId }) => dataAccessUsage.query(deps.client, deps.key, datasets, memberId, lifetimeSignal),
28
+ });
17
29
  const conversation = {
18
- key: deps.key,
19
- log: deps.log,
20
- memory: {
21
- list: () => deps.client.memoryList(deps.key),
22
- write: (key, text) => deps.client.memoryWrite(deps.key, key, text),
23
- delete: (key) => deps.client.memoryDelete(deps.key, key),
24
- },
25
- dataAccess: {
26
- query: ({ datasets, memberId }) => dataAccessUsage.query(deps.client, deps.key, datasets, memberId),
27
- },
30
+ signal: lifetimeSignal, platform,
31
+ key: deps.key, log: deps.log,
32
+ proactive: platform.proactive,
28
33
  };
29
34
  const options = await deps.factory(conversation);
30
- const [types, requirements] = await Promise.all([deps.widgetTypes(), deps.dataRequirements()]);
31
- const dataTools = requirements.length === 0 ? [] : [dataAccessTool(deps.client, deps.key, dataAccessUsage, requirements)];
32
- const tools = [
33
- ...dataTools,
34
- ...widgetTools(deps.client, deps.key, types),
35
- ...(deps.resources.length > 0 ? [readResourceTool(deps.resources)] : []),
36
- ...(options.initialState.tools ?? []),
37
- ].map((tool) => dataAccessUsage.track(tool));
38
- const seen = new Set();
39
- for (const tool of tools) {
40
- if (seen.has(tool.name))
41
- throw new Error(`工具重名:${tool.name}`);
42
- seen.add(tool.name);
43
- }
35
+ const selected = validatePlatformTools(options.platformTools);
36
+ // 平台的 system prompt 拉失败就 throw:这场会话起不来,等下一条消息再重建
37
+ const [platformPrompt, types, requirements] = await Promise.all([
38
+ deps.client.systemPrompt(deps.key),
39
+ [...selected].some(name => name.startsWith("widget_")) ? deps.widgetTypes() : [],
40
+ selected.has("data_access_query") ? deps.dataRequirements() : [],
41
+ ]);
42
+ const resources = selected.has("read_resource") ? deps.resources : [];
43
+ const readTools = [
44
+ ...(requirements.length ? [dataAccessTool(deps.client, deps.key, dataAccessUsage, requirements, lifetimeSignal)] : []),
45
+ ...(resources.length ? [readResourceTool(resources)] : []),
46
+ ];
47
+ const available = [...readTools, ...(types.length ? widgetTools(platform.widgets, types) : [])];
48
+ const tools = registerTools(selected, available, options.initialState.tools ?? []).map(tool => dataAccessUsage.track(tool));
44
49
  const { log, key } = deps;
45
50
  const thinkingLevel = options.initialState.thinkingLevel;
46
51
  if (thinkingLevel !== undefined) {
@@ -55,8 +60,7 @@ export async function createAgent(deps) {
55
60
  });
56
61
  }
57
62
  }
58
- const catalog = resourceCatalog(deps.resources);
59
- const devTransform = options.transformContext;
63
+ const catalog = resourceCatalog(resources);
60
64
  const devBefore = options.beforeToolCall;
61
65
  const devAfter = options.afterToolCall;
62
66
  const agent = new Agent({
@@ -65,38 +69,30 @@ export async function createAgent(deps) {
65
69
  sessionId: `buddy:${deps.buddyId}:${key}`,
66
70
  initialState: {
67
71
  model: deps.model,
68
- // 资料清单是静态的,接在开发者人设末尾;没资料时 resourceCatalog 回空串,人设原样
69
- systemPrompt: catalog === "" ? options.initialState.systemPrompt : `${options.initialState.systemPrompt}\n\n${catalog}`,
72
+ // 平台的规矩在最前,开发者人设在中间,资料清单是静态的接在末尾;没资料时 resourceCatalog 回空串,不接
73
+ systemPrompt: [platformPrompt, options.initialState.systemPrompt, ...(catalog === "" ? [] : [catalog])].join("\n\n"),
70
74
  tools,
71
75
  thinkingLevel: options.initialState.thinkingLevel ?? "off",
72
76
  },
73
77
  steeringMode: "all",
74
78
  followUpMode: "all",
75
- // 下面三个钩子按 pi 每次请求模型时的调用顺序排;只记录规模,不记录正文或请求体。
76
- // 平台的场合交代是历史里的记录,不在这里插;这里只跑开发者的裁剪
77
- transformContext: async (messages, signal) => {
78
- log.info("transformContext 入参", { convoKey: key, messageCount: messages.length });
79
- if (devTransform === undefined)
80
- return messages;
81
- let base = messages;
82
- try {
83
- base = await devTransform(messages, signal);
84
- }
85
- catch (e) {
86
- log.warn("开发者的 transformContext 抛错,用原历史", { convoKey: key, error: errorText(e) });
87
- base = messages;
88
- }
89
- log.info("开发者 transformContext 出参", { convoKey: key, messageCount: base.length });
90
- return base;
91
- },
79
+ // 下面三个钩子按 pi 每次请求模型时的调用顺序排:transformContext → convertToLlm → onPayload;普通回合出参和请求体全量打日志,主动回合只记规模。
80
+ // transformContext 的返回值只交给这一次请求,不写回会话历史;场合交代和图片本来就是历史里的记录,不在这里加
81
+ transformContext: async (messages) => withCurrentTime(messages, Date.now()),
92
82
  convertToLlm: (messages) => {
93
83
  const llmMessages = agentMessagesToLLMMessages(messages);
94
- log.info("convertToLlm 出参", { convoKey: key, messageCount: llmMessages.length });
84
+ if (dataAccessUsage.isProactive())
85
+ log.info("Proactive 模型输入已构建", { convoKey: key, messageCount: llmMessages.length });
86
+ else
87
+ log.info("convertToLlm 出参", { convoKey: key, messages: withoutImageData(llmMessages) });
95
88
  return llmMessages;
96
89
  },
97
90
  // 只打日志、返回 undefined:返回别的值 pi-ai 会拿它顶替请求体发出去
98
- onPayload: () => {
99
- log.info("模型请求已构建", { convoKey: key });
91
+ onPayload: (payload) => {
92
+ if (dataAccessUsage.isProactive())
93
+ log.info("Proactive 模型请求已构建", { convoKey: key });
94
+ else
95
+ log.info("模型请求体", { convoKey: key, payload: withoutImageData(payload) });
100
96
  return undefined;
101
97
  },
102
98
  beforeToolCall: devBefore === undefined
@@ -113,12 +109,45 @@ export async function createAgent(deps) {
113
109
  afterToolCall: devAfter === undefined
114
110
  ? undefined
115
111
  : (context, signal) => dataAccessUsage.runForTool(context.toolCall.id, () => devAfter(context, signal)),
116
- shouldStopAfterTurn: options.shouldStopAfterTurn,
112
+ shouldStopAfterTurn: options.shouldStopAfterTurn ? async (context, signal) => {
113
+ const stop = await options.shouldStopAfterTurn(context, signal);
114
+ if (stop)
115
+ deps.onPolicyStop?.();
116
+ return stop;
117
+ } : undefined,
117
118
  prepareNextTurnWithContext: options.prepareNextTurnWithContext,
118
119
  thinkingBudgets: options.thinkingBudgets,
119
120
  toolExecution: options.toolExecution,
120
121
  });
121
122
  agent.subscribe((event) => deps.onEvent(agent, event));
122
- return { agent, dataAccessUsage };
123
+ return { agent, dataAccessUsage, proactiveReadTools: tools.filter((tool) => readTools.some((read) => read.name === tool.name)) };
124
+ }
125
+ /**
126
+ * 每次请求模型时接在历史末尾那条 note 的正文;现在的时间由 convertToLlm 按这条 note 的 timestamp 拼在开头。
127
+ * 方括号时间怎么读、回复里别写,交代在平台的 system prompt 里(my-life-buddies-server 的 src/messages/systemPrompt.ts),
128
+ * 那边按「现在的时间」这几个字指认这条;改这里的正文或时间格式,那边一起改。
129
+ */
130
+ export const CURRENT_TIME_NOTE = "现在的时间(平台附注,不是谁说的话)";
131
+ /** 历史副本:末尾接一条 now 时刻的 note,传进来的数组不改 */
132
+ export function withCurrentTime(messages, now) {
133
+ return [...messages, { role: "note", text: CURRENT_TIME_NOTE, timestamp: now }];
134
+ }
135
+ /** 打日志用的副本:图片块的 data、请求体里 base64 的 data URL 换成字节数,其余原样 */
136
+ export function withoutImageData(value) {
137
+ if (typeof value === "string") {
138
+ const marker = value.startsWith("data:") ? value.indexOf(";base64,") : -1;
139
+ return marker === -1 ? value : `${value.slice(0, marker)};base64,[${base64Bytes(value.slice(marker + 8))} 字节]`;
140
+ }
141
+ if (Array.isArray(value))
142
+ return value.map(withoutImageData);
143
+ if (typeof value !== "object" || value === null)
144
+ return value;
145
+ const record = value;
146
+ if (record.type === "image" && typeof record.data === "string")
147
+ return { ...record, data: `[${base64Bytes(record.data)} 字节]` };
148
+ return Object.fromEntries(Object.entries(record).map(([name, field]) => [name, withoutImageData(field)]));
149
+ }
150
+ function base64Bytes(data) {
151
+ return Math.floor((data.length * 3) / 4) - (data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0);
123
152
  }
124
153
  //# sourceMappingURL=agent.js.map