@my-life-buddies/buddy-runtime 0.4.2 → 0.6.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 +2 -0
- package/dist/conversation/agentMessageLLMMessageConverter.d.ts +2 -1
- package/dist/conversation/agentMessageLLMMessageConverter.d.ts.map +1 -1
- package/dist/conversation/agentMessageLLMMessageConverter.js +14 -3
- package/dist/conversation/agentMessageLLMMessageConverter.js.map +1 -1
- package/dist/conversation/compaction.d.ts +73 -0
- package/dist/conversation/compaction.d.ts.map +1 -0
- package/dist/conversation/compaction.js +178 -0
- package/dist/conversation/compaction.js.map +1 -0
- package/dist/conversation/history.d.ts +16 -2
- package/dist/conversation/history.d.ts.map +1 -1
- package/dist/conversation/history.js +26 -3
- package/dist/conversation/history.js.map +1 -1
- package/dist/conversation/index.d.ts +7 -1
- package/dist/conversation/index.d.ts.map +1 -1
- package/dist/conversation/index.js +32 -12
- package/dist/conversation/index.js.map +1 -1
- package/dist/conversation/mlbMessageAgentMessageConverter.d.ts +16 -5
- package/dist/conversation/mlbMessageAgentMessageConverter.d.ts.map +1 -1
- package/dist/conversation/mlbMessageAgentMessageConverter.js +36 -16
- package/dist/conversation/mlbMessageAgentMessageConverter.js.map +1 -1
- package/dist/conversation/readMLBMessages.d.ts +8 -1
- package/dist/conversation/readMLBMessages.d.ts.map +1 -1
- package/dist/conversation/readMLBMessages.js +55 -9
- package/dist/conversation/readMLBMessages.js.map +1 -1
- package/dist/conversation/writeMLBMessage.d.ts +2 -2
- package/dist/conversation/writeMLBMessage.d.ts.map +1 -1
- package/dist/conversation/writeMLBMessage.js +2 -2
- package/dist/conversation/writeMLBMessage.js.map +1 -1
- package/dist/mlbClient.d.ts +176 -1
- package/dist/mlbClient.d.ts.map +1 -1
- package/dist/mlbClient.js +535 -2
- package/dist/mlbClient.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -4
- package/dist/server.js.map +1 -1
- package/dist/tools/widgets.d.ts +1 -1
- package/dist/tools/widgets.d.ts.map +1 -1
- package/dist/tools/widgets.js +1 -1
- package/dist/tools/widgets.js.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/platform/mlbClient.d.ts +0 -172
- package/dist/platform/mlbClient.d.ts.map +0 -1
- package/dist/platform/mlbClient.js +0 -523
- package/dist/platform/mlbClient.js.map +0 -1
package/README.md
CHANGED
|
@@ -91,6 +91,8 @@ await BuddyServer.start({
|
|
|
91
91
|
|
|
92
92
|
`buddy` 在每场会话收到第一条消息时调用一次。会话空闲 30 分钟被回收、或进程重启后,下一条消息会重新调用,所以闭包里的变量不能当长期存储,要记住的东西写进记忆(3.2)。
|
|
93
93
|
|
|
94
|
+
会话历史长到一定程度(超过 128K token,窗口小的模型按窗口的四分之一算)时,运行时在会话被回收后把早先的对话压成一份摘要写回平台,下次重建时模型看到的是「摘要 + 最近的原文」。开发者不用做任何事,也没有开关。
|
|
95
|
+
|
|
94
96
|
### 2.2 `BuddyOptions`
|
|
95
97
|
|
|
96
98
|
`buddy` 的返回值。字段名和签名取自 pi-agent-core 的 `AgentOptions` / `AgentState`,详见 pi 的文档。
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { Message } from "@earendil-works/pi-ai";
|
|
3
3
|
/**
|
|
4
|
-
* user、toolResult 的正文开头拼上它的时间「[2026-09-10 14:00:30 GMT] 」;搭子自己说的(assistant
|
|
4
|
+
* user、toolResult 的正文开头拼上它的时间「[2026-09-10 14:00:30 GMT] 」;搭子自己说的(assistant)不拼,
|
|
5
5
|
* 拼了模型会照着历史里自己的样子,在回复开头也写上方括号时间。一条对一条、按原来的先后,相邻的 user 不合并。
|
|
6
6
|
* 时间统一用 GMT、只看消息自己的 timestamp,同一条消息每次拼出来一字不差,前缀缓存照样命中。
|
|
7
|
+
* 说到一半断掉的 assistant 换成只有正文的一条(见 stoppedAssistantToLLMMessage)。
|
|
7
8
|
* 返回的是新数组,不改传进来的消息。
|
|
8
9
|
*/
|
|
9
10
|
export declare const agentMessagesToLLMMessages: (messages: AgentMessage[]) => Message[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentMessageLLMMessageConverter.d.ts","sourceRoot":"","sources":["../../src/conversation/agentMessageLLMMessageConverter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"agentMessageLLMMessageConverter.d.ts","sourceRoot":"","sources":["../../src/conversation/agentMessageLLMMessageConverter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAClE,OAAO,KAAK,EAAkC,OAAO,EAAe,MAAM,uBAAuB,CAAC;AAGlG;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,GAAI,UAAU,YAAY,EAAE,KAAG,OAAO,EAI1E,CAAC;AA0BJ,kEAAkE;AAClE,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAElD"}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// AgentMessage → LLMMessage 的换算:交给 pi 的 convertToLlm,每次请求模型前把整段 Agent 历史换成模型收的消息(pi-ai 的 Message)。
|
|
2
2
|
// MLBMessage 与 AgentMessage 之间的换算不在这里,在 mlbMessageAgentMessageConverter.ts;图片块在那一步就定好了,这里原样交出去。
|
|
3
|
+
import { stoppedByErrorOrAbort } from "./mlbMessageAgentMessageConverter.js";
|
|
3
4
|
/**
|
|
4
|
-
* user、toolResult 的正文开头拼上它的时间「[2026-09-10 14:00:30 GMT] 」;搭子自己说的(assistant
|
|
5
|
+
* user、toolResult 的正文开头拼上它的时间「[2026-09-10 14:00:30 GMT] 」;搭子自己说的(assistant)不拼,
|
|
5
6
|
* 拼了模型会照着历史里自己的样子,在回复开头也写上方括号时间。一条对一条、按原来的先后,相邻的 user 不合并。
|
|
6
7
|
* 时间统一用 GMT、只看消息自己的 timestamp,同一条消息每次拼出来一字不差,前缀缓存照样命中。
|
|
8
|
+
* 说到一半断掉的 assistant 换成只有正文的一条(见 stoppedAssistantToLLMMessage)。
|
|
7
9
|
* 返回的是新数组,不改传进来的消息。
|
|
8
10
|
*/
|
|
9
11
|
export const agentMessagesToLLMMessages = (messages) => messages.flatMap((m) => {
|
|
10
12
|
const next = agentMessageToLLMMessage(m);
|
|
11
13
|
return next === undefined ? [] : [next];
|
|
12
14
|
});
|
|
13
|
-
/** 一条 AgentMessage
|
|
15
|
+
/** 一条 AgentMessage 到模型那边是什么:正常收尾的 assistant 原样;pi harness 自己并进来的那几种 role(bashExecution / custom / …)这个运行时不产生,照 pi 默认 convertToLlm 的做法不给模型 */
|
|
14
16
|
function agentMessageToLLMMessage(m) {
|
|
15
17
|
switch (m.role) {
|
|
16
18
|
case "user":
|
|
@@ -18,11 +20,20 @@ function agentMessageToLLMMessage(m) {
|
|
|
18
20
|
case "toolResult":
|
|
19
21
|
return { ...m, content: withTimeMark(m.content, m.timestamp) };
|
|
20
22
|
case "assistant":
|
|
21
|
-
return m;
|
|
23
|
+
return stoppedByErrorOrAbort(m) ? stoppedAssistantToLLMMessage(m) : m;
|
|
22
24
|
default:
|
|
23
25
|
return undefined;
|
|
24
26
|
}
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* 以 error / aborted 收尾的 assistant(这一步说到一半断了):pi-ai 发请求前会把它整条跳过,模型就不知道用户已经看见了这半句。
|
|
30
|
+
* 换成只留正文、stopReason 为 stop 的一条,跟用户屏上看到的、重建时这条记录还原出来的一样;
|
|
31
|
+
* 心声和没流完的工具调用不给(用户看不见,工具调用的参数也不完整、没有结果);没正文的不给模型
|
|
32
|
+
*/
|
|
33
|
+
function stoppedAssistantToLLMMessage(m) {
|
|
34
|
+
const content = m.content.filter((block) => block.type === "text" && block.text !== "");
|
|
35
|
+
return content.length === 0 ? undefined : { ...m, content, stopReason: "stop" };
|
|
36
|
+
}
|
|
26
37
|
/** 拼在正文开头的时间「[2026-09-10 14:00:30 GMT] 」:timestamp 换成 GMT,精确到秒 */
|
|
27
38
|
export function timeMark(timestamp) {
|
|
28
39
|
return `[${new Date(timestamp).toISOString().slice(0, 19).replace("T", " ")} GMT] `;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentMessageLLMMessageConverter.js","sourceRoot":"","sources":["../../src/conversation/agentMessageLLMMessageConverter.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,gGAAgG;
|
|
1
|
+
{"version":3,"file":"agentMessageLLMMessageConverter.js","sourceRoot":"","sources":["../../src/conversation/agentMessageLLMMessageConverter.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,gGAAgG;AAIhG,OAAO,EAAE,qBAAqB,EAAE,MAAM,sCAAsC,CAAC;AAE7E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,QAAwB,EAAa,EAAE,CACjF,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;IACtB,MAAM,IAAI,GAAG,wBAAwB,CAAC,CAAC,CAAC,CAAC;IACzC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC,CAAC,CAAC;AAEJ,6IAA6I;AAC7I,SAAS,wBAAwB,CAAC,CAAe;IAChD,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,MAAM;YACV,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;QACpI,KAAK,YAAY;YAChB,OAAO,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;QAChE,KAAK,WAAW;YACf,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,4BAA4B,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE;YACC,OAAO,SAAS,CAAC;IACnB,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,SAAS,4BAA4B,CAAC,CAAmB;IACxD,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC;IACxF,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,CAAC;AACjF,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,QAAQ,CAAC,SAAiB;IACzC,OAAO,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,CAAC;AACrF,CAAC;AAED,+CAA+C;AAC/C,SAAS,YAAY,CAAC,OAAuC,EAAE,SAAiB;IAC/E,MAAM,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC;IACjC,IAAI,KAAK,EAAE,IAAI,KAAK,MAAM;QAAE,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC;IACnG,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,CAAC;AAC5E,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AgentMessage, AgentTool, StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Api, Message, Model, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { MLBClient } from "../mlbClient.ts";
|
|
4
|
+
import type { BuddyLogger } from "../types.ts";
|
|
5
|
+
/** 触发线:min(TRIGGER_CAP_TOKENS, 模型窗口 × TRIGGER_WINDOW_RATIO)。1M 窗口是 128K,256K 窗口是 64K:一场从触发线起聊,一晚上也撑不到窗口 */
|
|
6
|
+
export declare const TRIGGER_CAP_TOKENS = 128000;
|
|
7
|
+
export declare const TRIGGER_WINDOW_RATIO = 0.25;
|
|
8
|
+
/** 保留尾部大约多少 token 的原文(最近两三天),再往前的靠摘要 */
|
|
9
|
+
export declare const KEEP_TOKENS = 32000;
|
|
10
|
+
/** 摘要本身的输出上限 */
|
|
11
|
+
export declare const SUMMARY_MAX_TOKENS = 4096;
|
|
12
|
+
/** 摘要记录正文的第一行;平台 system prompt 按这句指认它(my-life-buddies-server 的 src/messages/systemPrompt.ts),改这里那边一起改 */
|
|
13
|
+
export declare const COMPACTION_HEADER = "\u66F4\u65E9\u7684\u5BF9\u8BDD\u5DF2\u538B\u7F29\u6210\u6458\u8981\uFF08\u5E73\u53F0\u9644\u6CE8\uFF0C\u4E0D\u662F\u8C01\u8BF4\u7684\u8BDD\uFF09\uFF1A";
|
|
14
|
+
/** 平台场合交代记录的第一行(my-life-buddies-server 的 situationOf 写的);被盖住那段里最新的一条要原样接在摘要后面 */
|
|
15
|
+
export declare const SITUATION_HEADER = "\u5E73\u53F0\u4EA4\u4EE3\u7684\u60C5\u51B5\uFF1A";
|
|
16
|
+
/** 接在切点之前那段历史末尾的指令。固定分节,逼模型每次都过一遍每个格子,多轮压缩才不会越压越丢 */
|
|
17
|
+
export declare const SUMMARY_INSTRUCTION: string;
|
|
18
|
+
/** 会话被回收那一刻拍下的快照:摘要请求要跟平时的请求同一前缀,所以 system prompt 与工具清单一起带上 */
|
|
19
|
+
export interface CompactionSnapshot {
|
|
20
|
+
systemPrompt: string;
|
|
21
|
+
tools: AgentTool[];
|
|
22
|
+
messages: AgentMessage[];
|
|
23
|
+
}
|
|
24
|
+
/** 切点算出来的东西:切点之前的那段、它盖到哪一刻、要原样接在摘要后面的场合交代 */
|
|
25
|
+
export interface CompactionPlan {
|
|
26
|
+
toSummarize: AgentMessage[];
|
|
27
|
+
coversUntil: number;
|
|
28
|
+
situation: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** 这个模型的触发线 */
|
|
31
|
+
export declare function triggerTokens(model: Pick<Model<Api>, "contextWindow">): number;
|
|
32
|
+
/**
|
|
33
|
+
* 历史超没超触发线。上下文大小用 pi 的估法:最后一条真实回复带回的用量(输入 + 缓存命中 + 输出)就是那次请求的上下文,
|
|
34
|
+
* 再把它之后的消息按字符数除 4 估上;刚重建完还没回过话时所有回复都没有用量,全按字符估,偏保守
|
|
35
|
+
*/
|
|
36
|
+
export declare function overTrigger(messages: AgentMessage[], model: Pick<Model<Api>, "contextWindow">): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* 切点:从尾往前数够 keepTokens,停在哪条就再往前挪到最近一条 user 角色消息,保留的尾部从它开始——尾部只会比保留量多、不会少,
|
|
39
|
+
* 一步的 assistant 和它的工具结果永远不拆。切点之前的全部进摘要,包括开头那份旧摘要。
|
|
40
|
+
* coversUntil 取切点前那条记录的时间:工具结果跟着它那一步的 assistant,取那一步的时间。
|
|
41
|
+
* 历史不够长(数够保留量已经到头)、或者往前找不到 user、切点前没东西,都回 undefined
|
|
42
|
+
*/
|
|
43
|
+
export declare function planCompaction(messages: AgentMessage[], keepTokens?: number): CompactionPlan | undefined;
|
|
44
|
+
/** 摘要记录正文里带的那段场合交代(compactionText 接上去的);没带回 undefined */
|
|
45
|
+
export declare function embeddedSituation(text: string): string | undefined;
|
|
46
|
+
/** 摘要记录的正文:第一行固定说明,接摘要,再原样接上被盖住那段里最新一条场合交代 */
|
|
47
|
+
export declare function compactionText(summary: string, situation: string | undefined): string;
|
|
48
|
+
/**
|
|
49
|
+
* 摘要请求的 messages:切点之前那段照平时的换算(user、工具结果开头拼时间),末尾接一条指令。
|
|
50
|
+
* 前缀跟平时的请求逐字相同,上游的前缀缓存还在就整段命中
|
|
51
|
+
*/
|
|
52
|
+
export declare function summaryRequestMessages(plan: CompactionPlan, now: number): Message[];
|
|
53
|
+
/** 这个模型最低的思考档:关得掉就关(不带思考参数),关不掉(kimi-k3)就最低那档 */
|
|
54
|
+
export declare function lowestThinkingLevel(model: Model<Api>): ThinkingLevel | undefined;
|
|
55
|
+
export interface CompactionDeps {
|
|
56
|
+
client: MLBClient;
|
|
57
|
+
key: string;
|
|
58
|
+
buddyId: string;
|
|
59
|
+
model: Model<Api>;
|
|
60
|
+
streamFn: StreamFn;
|
|
61
|
+
/** 搭子令牌:摘要请求也经网关,同一把 */
|
|
62
|
+
token: string;
|
|
63
|
+
log: BuddyLogger;
|
|
64
|
+
/** 进程停止时打断:正在生成的请求取消,没写完的不写 */
|
|
65
|
+
signal?: AbortSignal;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 后台压一次:算切点 → 请求模型生成摘要 → 写回一条 compaction 记录。
|
|
69
|
+
* 回 true 表示摘要已交给发送队列并等到了队列清空;生成失败、模型没按要求输出、写被拒都回 false 且什么都不动。
|
|
70
|
+
* 幂等键按会话键加 coversUntil 拼,重发不会写重
|
|
71
|
+
*/
|
|
72
|
+
export declare function compactConversation(deps: CompactionDeps, snapshot: CompactionSnapshot): Promise<boolean>;
|
|
73
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/conversation/compaction.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAEvF,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAKhF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,2GAA2G;AAC3G,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,eAAO,MAAM,oBAAoB,OAAO,CAAC;AACzC,wCAAwC;AACxC,eAAO,MAAM,WAAW,QAAS,CAAC;AAClC,gBAAgB;AAChB,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AAIxC,yGAAyG;AACzG,eAAO,MAAM,iBAAiB,2JAA8B,CAAC;AAC7D,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,qDAAa,CAAC;AAE3C,qDAAqD;AACrD,eAAO,MAAM,mBAAmB,QAepB,CAAC;AAEb,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC9B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,eAAe;AACf,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,GAAG,MAAM,CAE9E;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,GAAG,OAAO,CAEvG;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,YAAY,EAAE,EAAE,UAAU,SAAc,GAAG,cAAc,GAAG,SAAS,CAe7G;AAcD,yDAAyD;AACzD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGlE;AAED,8CAA8C;AAC9C,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAGrF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,EAAE,CAEnF;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,aAAa,GAAG,SAAS,CAGhF;AAED,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,QAAQ,EAAE,QAAQ,CAAC;IACnB,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,WAAW,CAAC;IACjB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC,CAgD9G"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// 会话压缩:会话空闲被回收那一刻,历史超过触发线就拍一份快照,后台压成一份摘要写回平台,下次重建生效
|
|
2
|
+
//(REDoc《【技术设计】搭子会话压缩 Compaction》)。压缩从头到尾不碰活会话的内存历史。
|
|
3
|
+
// 管:触发线(min(128K, 模型窗口的 25%))与上下文大小的估算;切点(从尾数够保留量、再往前挪到最近一条 user,一步不拆)与 coversUntil;
|
|
4
|
+
// 被盖住那段里最新一条场合交代原样接在摘要后面;摘要请求(跟平时同一前缀 + 末尾一条指令,思考最低档);
|
|
5
|
+
// 摘要记录的正文;后台跑一次:生成 → 经 WriteMLBMessage 写回;生成失败、写被拒都什么都不动,下次回收再来。
|
|
6
|
+
// 不管:什么时候回收、后台任务怎么管(server.ts);重建时怎么用这条记录(readMLBMessages.ts / history.ts / mlbMessageAgentMessageConverter.ts)。
|
|
7
|
+
import { estimateContextTokens, estimateTokens } from "@earendil-works/pi-agent-core";
|
|
8
|
+
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
9
|
+
import { agentMessagesToLLMMessages } from "./agentMessageLLMMessageConverter.js";
|
|
10
|
+
import { textOf } from "./mlbMessageAgentMessageConverter.js";
|
|
11
|
+
import { WriteMLBMessage } from "./writeMLBMessage.js";
|
|
12
|
+
import { errorText } from "../log.js";
|
|
13
|
+
/** 触发线:min(TRIGGER_CAP_TOKENS, 模型窗口 × TRIGGER_WINDOW_RATIO)。1M 窗口是 128K,256K 窗口是 64K:一场从触发线起聊,一晚上也撑不到窗口 */
|
|
14
|
+
export const TRIGGER_CAP_TOKENS = 128_000;
|
|
15
|
+
export const TRIGGER_WINDOW_RATIO = 0.25;
|
|
16
|
+
/** 保留尾部大约多少 token 的原文(最近两三天),再往前的靠摘要 */
|
|
17
|
+
export const KEEP_TOKENS = 32_000;
|
|
18
|
+
/** 摘要本身的输出上限 */
|
|
19
|
+
export const SUMMARY_MAX_TOKENS = 4_096;
|
|
20
|
+
/** 写回平台最多等多久:网络一直不通就不等了,这次当没压过 */
|
|
21
|
+
const WRITE_WAIT_MS = 2 * 60_000;
|
|
22
|
+
/** 摘要记录正文的第一行;平台 system prompt 按这句指认它(my-life-buddies-server 的 src/messages/systemPrompt.ts),改这里那边一起改 */
|
|
23
|
+
export const COMPACTION_HEADER = "更早的对话已压缩成摘要(平台附注,不是谁说的话):";
|
|
24
|
+
/** 平台场合交代记录的第一行(my-life-buddies-server 的 situationOf 写的);被盖住那段里最新的一条要原样接在摘要后面 */
|
|
25
|
+
export const SITUATION_HEADER = "平台交代的情况:";
|
|
26
|
+
/** 接在切点之前那段历史末尾的指令。固定分节,逼模型每次都过一遍每个格子,多轮压缩才不会越压越丢 */
|
|
27
|
+
export const SUMMARY_INSTRUCTION = [
|
|
28
|
+
"把以上对话压成一份摘要,交给以后的你接着聊。只输出摘要本身:不回应上面任何人的话,不加开场白和结尾,不调用工具。",
|
|
29
|
+
`上面的对话开头如果已经有一段「${COMPACTION_HEADER.slice(0, -1)}」,把它的内容合并进来,任何时刻只有一份摘要。`,
|
|
30
|
+
"按下面九节写,每节写不出就写「无」,不要漏节,节名照抄:",
|
|
31
|
+
"## 用户的事实与身边的人",
|
|
32
|
+
"## 用户的偏好与明确要求",
|
|
33
|
+
"## 用户明确说过要记住的事",
|
|
34
|
+
"## 承诺过的、没做完的事",
|
|
35
|
+
"## 正在进行的话题和最近的结论",
|
|
36
|
+
"## 你自己表过的态、给过的建议",
|
|
37
|
+
"## 哪些事已经说过了",
|
|
38
|
+
"## 时间线",
|
|
39
|
+
"## 编号",
|
|
40
|
+
"要求:每条事实带上是哪天,消息开头方括号里是 GMT 时间,按它写日期;widget 的 id、图片的「#消息 id」、群成员的「#m_ 编号」原样抄,不要改写;" +
|
|
41
|
+
`以「${SITUATION_HEADER}」开头的那几段不用写进摘要;图片只记「[图片 #消息 id]」和当时说了什么;不写你在想什么,只写事实和结论;总长不超过两千字。`,
|
|
42
|
+
].join("\n");
|
|
43
|
+
/** 这个模型的触发线 */
|
|
44
|
+
export function triggerTokens(model) {
|
|
45
|
+
return Math.min(TRIGGER_CAP_TOKENS, Math.floor(model.contextWindow * TRIGGER_WINDOW_RATIO));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 历史超没超触发线。上下文大小用 pi 的估法:最后一条真实回复带回的用量(输入 + 缓存命中 + 输出)就是那次请求的上下文,
|
|
49
|
+
* 再把它之后的消息按字符数除 4 估上;刚重建完还没回过话时所有回复都没有用量,全按字符估,偏保守
|
|
50
|
+
*/
|
|
51
|
+
export function overTrigger(messages, model) {
|
|
52
|
+
return estimateContextTokens(messages).tokens > triggerTokens(model);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 切点:从尾往前数够 keepTokens,停在哪条就再往前挪到最近一条 user 角色消息,保留的尾部从它开始——尾部只会比保留量多、不会少,
|
|
56
|
+
* 一步的 assistant 和它的工具结果永远不拆。切点之前的全部进摘要,包括开头那份旧摘要。
|
|
57
|
+
* coversUntil 取切点前那条记录的时间:工具结果跟着它那一步的 assistant,取那一步的时间。
|
|
58
|
+
* 历史不够长(数够保留量已经到头)、或者往前找不到 user、切点前没东西,都回 undefined
|
|
59
|
+
*/
|
|
60
|
+
export function planCompaction(messages, keepTokens = KEEP_TOKENS) {
|
|
61
|
+
let kept = 0;
|
|
62
|
+
let i = messages.length;
|
|
63
|
+
while (i > 0 && kept < keepTokens) {
|
|
64
|
+
i -= 1;
|
|
65
|
+
kept += estimateTokens(messages[i]);
|
|
66
|
+
}
|
|
67
|
+
if (kept < keepTokens)
|
|
68
|
+
return undefined;
|
|
69
|
+
let cut = i;
|
|
70
|
+
while (cut > 0 && messages[cut].role !== "user")
|
|
71
|
+
cut -= 1;
|
|
72
|
+
if (cut <= 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
let head = cut - 1;
|
|
75
|
+
while (head > 0 && messages[head].role === "toolResult")
|
|
76
|
+
head -= 1;
|
|
77
|
+
const toSummarize = messages.slice(0, cut);
|
|
78
|
+
return { toSummarize, coversUntil: messages[head].timestamp, situation: latestSituationIn(toSummarize) };
|
|
79
|
+
}
|
|
80
|
+
/** 切点之前那段里最新一条场合交代的正文:先找 situation 记录换算出来的 user 消息,找不到再看开头那份旧摘要末尾带的那段 */
|
|
81
|
+
function latestSituationIn(messages) {
|
|
82
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
83
|
+
const m = messages[i];
|
|
84
|
+
if (m.role !== "user")
|
|
85
|
+
continue;
|
|
86
|
+
const text = userText(m);
|
|
87
|
+
if (text.startsWith(SITUATION_HEADER))
|
|
88
|
+
return text;
|
|
89
|
+
if (i === 0 && text.startsWith(COMPACTION_HEADER))
|
|
90
|
+
return embeddedSituation(text);
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
/** 摘要记录正文里带的那段场合交代(compactionText 接上去的);没带回 undefined */
|
|
95
|
+
export function embeddedSituation(text) {
|
|
96
|
+
const at = text.lastIndexOf(`\n\n${SITUATION_HEADER}`);
|
|
97
|
+
return at === -1 ? undefined : text.slice(at + 2);
|
|
98
|
+
}
|
|
99
|
+
/** 摘要记录的正文:第一行固定说明,接摘要,再原样接上被盖住那段里最新一条场合交代 */
|
|
100
|
+
export function compactionText(summary, situation) {
|
|
101
|
+
const body = `${COMPACTION_HEADER}\n${summary.trim()}`;
|
|
102
|
+
return situation === undefined ? body : `${body}\n\n${situation}`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 摘要请求的 messages:切点之前那段照平时的换算(user、工具结果开头拼时间),末尾接一条指令。
|
|
106
|
+
* 前缀跟平时的请求逐字相同,上游的前缀缓存还在就整段命中
|
|
107
|
+
*/
|
|
108
|
+
export function summaryRequestMessages(plan, now) {
|
|
109
|
+
return agentMessagesToLLMMessages([...plan.toSummarize, { role: "user", content: [{ type: "text", text: SUMMARY_INSTRUCTION }], timestamp: now }]);
|
|
110
|
+
}
|
|
111
|
+
/** 这个模型最低的思考档:关得掉就关(不带思考参数),关不掉(kimi-k3)就最低那档 */
|
|
112
|
+
export function lowestThinkingLevel(model) {
|
|
113
|
+
const lowest = getSupportedThinkingLevels(model)[0];
|
|
114
|
+
return lowest === undefined || lowest === "off" ? undefined : lowest;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 后台压一次:算切点 → 请求模型生成摘要 → 写回一条 compaction 记录。
|
|
118
|
+
* 回 true 表示摘要已交给发送队列并等到了队列清空;生成失败、模型没按要求输出、写被拒都回 false 且什么都不动。
|
|
119
|
+
* 幂等键按会话键加 coversUntil 拼,重发不会写重
|
|
120
|
+
*/
|
|
121
|
+
export async function compactConversation(deps, snapshot) {
|
|
122
|
+
const { log, key } = deps;
|
|
123
|
+
if (deps.signal?.aborted)
|
|
124
|
+
return false;
|
|
125
|
+
const plan = planCompaction(snapshot.messages);
|
|
126
|
+
if (plan === undefined) {
|
|
127
|
+
log.info("压缩:历史还不够长,没有可压的", { convoKey: key, messages: snapshot.messages.length });
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
const messages = summaryRequestMessages(plan, Date.now());
|
|
131
|
+
log.info("压缩:请求摘要", { convoKey: key, summarized: plan.toSummarize.length, kept: snapshot.messages.length - plan.toSummarize.length, coversUntil: plan.coversUntil });
|
|
132
|
+
let summary;
|
|
133
|
+
try {
|
|
134
|
+
const stream = await deps.streamFn(deps.model, { systemPrompt: snapshot.systemPrompt, messages, tools: snapshot.tools }, { apiKey: deps.token, maxTokens: SUMMARY_MAX_TOKENS, reasoning: lowestThinkingLevel(deps.model), signal: deps.signal });
|
|
135
|
+
const result = await stream.result();
|
|
136
|
+
if (result.stopReason !== "stop")
|
|
137
|
+
throw new Error(`模型以 ${result.stopReason} 收尾:${result.errorMessage ?? ""}`);
|
|
138
|
+
if (result.content.some((block) => block.type === "toolCall"))
|
|
139
|
+
throw new Error("模型没写摘要,调了工具");
|
|
140
|
+
summary = textOf(result).trim();
|
|
141
|
+
if (summary === "")
|
|
142
|
+
throw new Error("模型回了空正文");
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
log.warn("压缩:摘要没生成,这次不压,下次回收再试", { convoKey: key, error: errorText(e) });
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
// 进程正在停:没写的不写,这次当没压过
|
|
149
|
+
if (deps.signal?.aborted)
|
|
150
|
+
return false;
|
|
151
|
+
const writer = new WriteMLBMessage({ client: deps.client, key, buddyId: deps.buddyId, runId: "compaction", log });
|
|
152
|
+
const stop = () => writer.close();
|
|
153
|
+
deps.signal?.addEventListener("abort", stop, { once: true });
|
|
154
|
+
const timeout = setTimeout(stop, WRITE_WAIT_MS);
|
|
155
|
+
timeout.unref();
|
|
156
|
+
try {
|
|
157
|
+
writer.enqueueMessage({
|
|
158
|
+
type: "compaction",
|
|
159
|
+
text: compactionText(summary, plan.situation),
|
|
160
|
+
coversUntil: plan.coversUntil,
|
|
161
|
+
idempotencyKey: `compaction:${deps.buddyId}:${key}:${plan.coversUntil}`,
|
|
162
|
+
});
|
|
163
|
+
await writer.flush();
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
deps.signal?.removeEventListener("abort", stop);
|
|
168
|
+
writer.close();
|
|
169
|
+
}
|
|
170
|
+
log.info("压缩:摘要已写回", { convoKey: key, coversUntil: plan.coversUntil, chars: summary.length });
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
function userText(m) {
|
|
174
|
+
if (m.role !== "user")
|
|
175
|
+
return "";
|
|
176
|
+
return typeof m.content === "string" ? m.content : m.content.map((block) => (block.type === "text" ? block.text : "")).join("");
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=compaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../../src/conversation/compaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import type { MLBMessage } from "../types.ts";
|
|
2
|
+
/** 一条压缩记录(运行时自己写回的摘要,见 compaction.ts):type compaction、搭子写的、带 coversUntil */
|
|
3
|
+
export declare function isCompaction(record: MLBMessage): record is MLBMessage & {
|
|
4
|
+
coversUntil: number;
|
|
5
|
+
};
|
|
6
|
+
/** 这批记录里最新的一条压缩记录(按时间),没有回 undefined */
|
|
7
|
+
export declare function latestCompaction(records: MLBMessage[]): (MLBMessage & {
|
|
8
|
+
coversUntil: number;
|
|
9
|
+
}) | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* 重建时按压缩记录裁:只认最新一条,时间不晚于它 coversUntil 的记录一律扔掉,更早的压缩记录也扔掉;
|
|
12
|
+
* 那条记录本身留着,在流里的位置不动(换算时它变成一条时间为 coversUntil 的 user 消息,回合收尾那套按时间排会把它排到开头)。
|
|
13
|
+
* 没有压缩记录原样返回
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyCompaction(records: MLBMessage[]): MLBMessage[];
|
|
2
16
|
/**
|
|
3
17
|
* 首次拉取(重建):以搭子最后一次写的记录为界。之前的整段是历史,照原样灌;之后的是搭子还没回应过的,
|
|
4
|
-
* 交给回合按平时同一条规矩处理(wantsReply)。不看「最后一条」:拉取时平台追加的
|
|
18
|
+
* 交给回合按平时同一条规矩处理(wantsReply)。不看「最后一条」:拉取时平台追加的 situation 记录、
|
|
5
19
|
* 群里 @ 之后别人紧跟的闲聊,都可能排在该答的那句后面。搭子一句都没说过时,整段都算没回应过的
|
|
6
20
|
*/
|
|
7
21
|
export declare function splitForRebuild(records: MLBMessage[]): {
|
|
@@ -11,7 +25,7 @@ export declare function splitForRebuild(records: MLBMessage[]): {
|
|
|
11
25
|
/** 对方的记录里要处理的:不是搭子写的,有正文或是转发卡 */
|
|
12
26
|
export declare function theirsOf(records: MLBMessage[]): MLBMessage[];
|
|
13
27
|
/**
|
|
14
|
-
* 要不要开口:其中有一条是用户说的、平台标了该答。平台写的(
|
|
28
|
+
* 要不要开口:其中有一条是用户说的、平台标了该答。平台写的(situation、system)从不该答;
|
|
15
29
|
* 该不该答只看平台下发的 shouldReply(旧平台叫 reply,没有 shouldReply 时照认),运行时不判、不让模型猜
|
|
16
30
|
*/
|
|
17
31
|
export declare function wantsReply(records: MLBMessage[]): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/conversation/history.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG;IAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,CAIvG;AAED,iCAAiC;AACjC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAE5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAEzD"}
|
|
1
|
+
{"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/conversation/history.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,IAAI,UAAU,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CAE/F;AAED,wCAAwC;AACxC,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,GAAG;IAAE,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,SAAS,CAI1G;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAInE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG;IAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IAAC,OAAO,EAAE,UAAU,EAAE,CAAA;CAAE,CAIvG;AAED,iCAAiC;AACjC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAE5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAEzD"}
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
// 拉回来的平台记录怎么分拣:哪些灌进历史、哪些是搭子还没回应过的、哪些是对方的、要不要开口。纯函数,不碰 Agent。
|
|
2
|
-
//
|
|
2
|
+
// 管:压缩记录怎么认、盖住的记录怎么扔(只认最新一条);首次拉取(重建)的切分;挑出对方的记录;「要不要开口」的判定(重建与平时同一条)。
|
|
3
3
|
// 不管:拉取、游标与只交新消息(readMLBMessages.ts)、记录换成 pi 消息的形状(mlbMessageAgentMessageConverter.ts)、分拣之后怎么处理(index.ts)。
|
|
4
|
+
/** 一条压缩记录(运行时自己写回的摘要,见 compaction.ts):type compaction、搭子写的、带 coversUntil */
|
|
5
|
+
export function isCompaction(record) {
|
|
6
|
+
return record.type === "compaction" && record.author === "buddy" && typeof record.coversUntil === "number";
|
|
7
|
+
}
|
|
8
|
+
/** 这批记录里最新的一条压缩记录(按时间),没有回 undefined */
|
|
9
|
+
export function latestCompaction(records) {
|
|
10
|
+
let found;
|
|
11
|
+
for (const record of records)
|
|
12
|
+
if (isCompaction(record) && (found === undefined || record.timestamp > found.timestamp))
|
|
13
|
+
found = record;
|
|
14
|
+
return found;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 重建时按压缩记录裁:只认最新一条,时间不晚于它 coversUntil 的记录一律扔掉,更早的压缩记录也扔掉;
|
|
18
|
+
* 那条记录本身留着,在流里的位置不动(换算时它变成一条时间为 coversUntil 的 user 消息,回合收尾那套按时间排会把它排到开头)。
|
|
19
|
+
* 没有压缩记录原样返回
|
|
20
|
+
*/
|
|
21
|
+
export function applyCompaction(records) {
|
|
22
|
+
const latest = latestCompaction(records);
|
|
23
|
+
if (latest === undefined)
|
|
24
|
+
return records;
|
|
25
|
+
return records.filter((record) => record === latest || (!isCompaction(record) && record.timestamp > latest.coversUntil));
|
|
26
|
+
}
|
|
4
27
|
/**
|
|
5
28
|
* 首次拉取(重建):以搭子最后一次写的记录为界。之前的整段是历史,照原样灌;之后的是搭子还没回应过的,
|
|
6
|
-
* 交给回合按平时同一条规矩处理(wantsReply)。不看「最后一条」:拉取时平台追加的
|
|
29
|
+
* 交给回合按平时同一条规矩处理(wantsReply)。不看「最后一条」:拉取时平台追加的 situation 记录、
|
|
7
30
|
* 群里 @ 之后别人紧跟的闲聊,都可能排在该答的那句后面。搭子一句都没说过时,整段都算没回应过的
|
|
8
31
|
*/
|
|
9
32
|
export function splitForRebuild(records) {
|
|
@@ -17,7 +40,7 @@ export function theirsOf(records) {
|
|
|
17
40
|
return records.filter((m) => m.author !== "buddy" && ((m.text ?? "") !== "" || m.messagesCard !== undefined || (m.type === "image" && !!m.image?.mediaId)));
|
|
18
41
|
}
|
|
19
42
|
/**
|
|
20
|
-
* 要不要开口:其中有一条是用户说的、平台标了该答。平台写的(
|
|
43
|
+
* 要不要开口:其中有一条是用户说的、平台标了该答。平台写的(situation、system)从不该答;
|
|
21
44
|
* 该不该答只看平台下发的 shouldReply(旧平台叫 reply,没有 shouldReply 时照认),运行时不判、不让模型猜
|
|
22
45
|
*/
|
|
23
46
|
export function wantsReply(records) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/conversation/history.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/conversation/history.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,uEAAuE;AACvE,2GAA2G;AAI3G,4EAA4E;AAC5E,MAAM,UAAU,YAAY,CAAC,MAAkB;IAC9C,OAAO,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,IAAI,OAAO,MAAM,CAAC,WAAW,KAAK,QAAQ,CAAC;AAC5G,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACrD,IAAI,KAAyD,CAAC;IAC9D,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;YAAE,KAAK,GAAG,MAAM,CAAC;IACtI,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAqB;IACpD,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACzC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC;IACzC,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;AAC1H,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAqB;IACpD,IAAI,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IAC9B,OAAO,IAAI,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;QAAE,IAAI,EAAE,CAAC;IAC7D,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC;AAClF,CAAC;AAED,iCAAiC;AACjC,MAAM,UAAU,QAAQ,CAAC,OAAqB;IAC7C,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,YAAY,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;AAC7J,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,OAAqB;IAC/C,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,CAAC,WAAW,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,WAAW,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AAClI,CAAC"}
|
|
@@ -3,6 +3,7 @@ import type { Api, Model } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import type { MLBClient, WidgetType } from "../mlbClient.ts";
|
|
4
4
|
import type { Resource } from "../tools/resource.ts";
|
|
5
5
|
import type { BuddyFactory, BuddyLogger, DataRequirement, ProactiveDecision, ProactiveRun } from "../types.ts";
|
|
6
|
+
import type { CompactionSnapshot } from "./compaction.ts";
|
|
6
7
|
export interface ConversationHandleDeps {
|
|
7
8
|
welcome?: string;
|
|
8
9
|
errorReply?: string;
|
|
@@ -66,6 +67,11 @@ export declare class ConversationHandle {
|
|
|
66
67
|
get closed(): boolean;
|
|
67
68
|
/** 测试探针:Agent 当前的历史;还没建 Agent 时是空数组 */
|
|
68
69
|
get transcript(): readonly AgentMessage[];
|
|
70
|
+
/**
|
|
71
|
+
* 会话被回收前问一句:历史超过触发线了没(compaction.ts 的 overTrigger)。超过就交出一份快照——system prompt、工具清单、整段历史的副本——
|
|
72
|
+
* 回收后拿它在后台压缩;没超、还没建会话、正忙都回 undefined。压缩从不碰这场会话的内存历史
|
|
73
|
+
*/
|
|
74
|
+
snapshotForCompaction(): CompactionSnapshot | undefined;
|
|
69
75
|
/** SSE message 到了:让 reader 拉一次。已退役的直接忽略 */
|
|
70
76
|
notify(welcome?: boolean): void;
|
|
71
77
|
runProactive(run: ProactiveRun): Promise<ProactiveDecision>;
|
|
@@ -78,7 +84,7 @@ export declare class ConversationHandle {
|
|
|
78
84
|
private retire;
|
|
79
85
|
/**
|
|
80
86
|
* reader 交来一批:第一次走重建,之后走 ingest。
|
|
81
|
-
* 平台的场合交代(
|
|
87
|
+
* 平台的场合交代(situation)是流里的一条记录,跟别的记录一起拉到;会话没了只靠 SSE delete 事件收,这里不判。
|
|
82
88
|
*/
|
|
83
89
|
private onMessages;
|
|
84
90
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAqB,YAAY,EAAa,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC1G,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAc,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// 一场会话的纪律:拉到消息怎么办、什么时候起回合、Agent 事件往哪送、怎么退役。
|
|
2
2
|
// 管:首次拉到的整批灌进 Agent(重建),搭子最后一次说话之后有该答的就起回合;
|
|
3
3
|
// 之后每批按空闲 / 忙碌 × 要不要开口四路分发;
|
|
4
|
-
// 回合(prompt → 把回合中排进队列的消息消化完 → 出错或一句话没说就发 errorReply
|
|
4
|
+
// 回合(prompt → 把回合中排进队列的消息消化完 → 出错或一句话没说就发 errorReply 兜底、兜底话也进历史 → 收尾把这一轮的历史按消息时间排好);
|
|
5
5
|
// Agent 事件交给发平台的队列;close(先标退役、再打断回合、等它收尾)。
|
|
6
|
-
// 不管:什么时候拉、游标、翻页与重试(readMLBMessages.ts)、Agent 怎么建(agent.ts)、拉回来的记录怎么分拣(history.ts)、
|
|
6
|
+
// 不管:什么时候拉、游标、翻页与重试(readMLBMessages.ts)、Agent 怎么建(agent.ts)、拉回来的记录怎么分拣(history.ts)、压缩本身(compaction.ts)、
|
|
7
7
|
// replying / delta / 写回怎么发(writeMLBMessage.ts)、HTTP 请求本身(mlbClient.ts)、
|
|
8
8
|
// MLBMessage 与 AgentMessage 的换算(mlbMessageAgentMessageConverter.ts)、会话从哪来到哪去(server.ts 管 Map、空闲淘汰与 delete 事件)。
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import { mlbMessagesToAgentMessages, textOf } from "./mlbMessageAgentMessageConverter.js";
|
|
10
|
+
import { mlbMessagesToAgentMessages, stoppedByErrorOrAbort, textAssistantMessage, textOf } from "./mlbMessageAgentMessageConverter.js";
|
|
11
11
|
import { errorText } from "../log.js";
|
|
12
12
|
import { modelIdentity } from "../model.js";
|
|
13
13
|
import { redactSensitiveRun } from "../tools/dataAccess.js";
|
|
14
14
|
import { createAgent } from "./agent.js";
|
|
15
|
+
import { overTrigger } from "./compaction.js";
|
|
15
16
|
import { splitForRebuild, theirsOf, wantsReply } from "./history.js";
|
|
16
17
|
import { ReadMLBMessages } from "./readMLBMessages.js";
|
|
17
18
|
import { WriteMLBMessage } from "./writeMLBMessage.js";
|
|
@@ -81,6 +82,18 @@ export class ConversationHandle {
|
|
|
81
82
|
get transcript() {
|
|
82
83
|
return this.agent?.state.messages ?? [];
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* 会话被回收前问一句:历史超过触发线了没(compaction.ts 的 overTrigger)。超过就交出一份快照——system prompt、工具清单、整段历史的副本——
|
|
87
|
+
* 回收后拿它在后台压缩;没超、还没建会话、正忙都回 undefined。压缩从不碰这场会话的内存历史
|
|
88
|
+
*/
|
|
89
|
+
snapshotForCompaction() {
|
|
90
|
+
const agent = this.agent;
|
|
91
|
+
if (agent === undefined || this._busy || this._closed)
|
|
92
|
+
return undefined;
|
|
93
|
+
if (!overTrigger(agent.state.messages, this.deps.model))
|
|
94
|
+
return undefined;
|
|
95
|
+
return { systemPrompt: agent.state.systemPrompt, tools: agent.state.tools, messages: [...agent.state.messages] };
|
|
96
|
+
}
|
|
84
97
|
/** SSE message 到了:让 reader 拉一次。已退役的直接忽略 */
|
|
85
98
|
notify(welcome = false) {
|
|
86
99
|
if (this._closed)
|
|
@@ -195,7 +208,7 @@ export class ConversationHandle {
|
|
|
195
208
|
// —— 拉到消息
|
|
196
209
|
/**
|
|
197
210
|
* reader 交来一批:第一次走重建,之后走 ingest。
|
|
198
|
-
* 平台的场合交代(
|
|
211
|
+
* 平台的场合交代(situation)是流里的一条记录,跟别的记录一起拉到;会话没了只靠 SSE delete 事件收,这里不判。
|
|
199
212
|
*/
|
|
200
213
|
async onMessages(messages) {
|
|
201
214
|
if (this._closed)
|
|
@@ -210,7 +223,7 @@ export class ConversationHandle {
|
|
|
210
223
|
try {
|
|
211
224
|
if (this.welcomeRequested) {
|
|
212
225
|
this.welcomeRequested = false;
|
|
213
|
-
if (this.deps.welcome && messages.every(m => m.type === "
|
|
226
|
+
if (this.deps.welcome && messages.every(m => m.type === "situation" && m.author === "platform")) {
|
|
214
227
|
this.writer.enqueueMessage({ type: "message", text: this.deps.welcome, idempotencyKey: `welcome:${this.deps.key}` });
|
|
215
228
|
await this.writer.flush();
|
|
216
229
|
if (!this._closed) {
|
|
@@ -245,7 +258,8 @@ export class ConversationHandle {
|
|
|
245
258
|
this.agent = agent;
|
|
246
259
|
this.dataAccessUsage = created.dataAccessUsage;
|
|
247
260
|
const { history, pending } = splitForRebuild(records);
|
|
248
|
-
|
|
261
|
+
// 按时间排一遍:压缩记录换算出来的那条摘要时间是 coversUntil,排到开头,后面才是它盖住之后的原文
|
|
262
|
+
agent.state.messages = sortByTimestamp(mlbMessagesToAgentMessages(history, this.identity, this.imageInput));
|
|
249
263
|
this.rebuilt = true;
|
|
250
264
|
this.deps.log.info("历史已重建", { convoKey: this.deps.key, records: history.length, pending: pending.length, transcript: agent.state.messages.length });
|
|
251
265
|
this.admit(theirsOf(pending));
|
|
@@ -347,7 +361,11 @@ export class ConversationHandle {
|
|
|
347
361
|
}
|
|
348
362
|
finally {
|
|
349
363
|
if (!this._closed && this.deps.errorReply && (this.failed || !this.spoke)) {
|
|
350
|
-
|
|
364
|
+
// 兜底话用户屏上看得见,模型也要知道自己说过:进历史的是只有这句正文的 assistant,跟重建时这条记录还原出来的一样;
|
|
365
|
+
// 写回时以它为 target,平台回的时间写到它身上,下面按时间排
|
|
366
|
+
const fallback = textAssistantMessage(this.deps.errorReply, Date.now(), this.identity);
|
|
367
|
+
agent.state.messages.push(fallback);
|
|
368
|
+
this.writer.enqueueMessage({ type: "message", text: this.deps.errorReply, idempotencyKey: `fallback:${this.deps.key}:${this.replyTo}` }, fallback);
|
|
351
369
|
}
|
|
352
370
|
this.writer.replying({ on: false });
|
|
353
371
|
await this.writer.flush();
|
|
@@ -410,20 +428,22 @@ export class ConversationHandle {
|
|
|
410
428
|
toolCalls: m.content.filter((block) => block.type === "toolCall").length,
|
|
411
429
|
error: m.errorMessage,
|
|
412
430
|
});
|
|
413
|
-
|
|
431
|
+
const stopped = stoppedByErrorOrAbort(m);
|
|
432
|
+
if (stopped)
|
|
414
433
|
this.failed = true;
|
|
415
434
|
else if (textOf(m) !== "")
|
|
416
435
|
this.spoke = true;
|
|
417
|
-
//
|
|
418
|
-
if (!this.proactiveRun && !m.content.some((block) => block.type === "toolCall"))
|
|
436
|
+
// 正常收尾又带工具调用的,等 turn_end 连结果一起写成 tool 记录;说到一半断了的这一步没有结果,只写推过去的正文
|
|
437
|
+
if (!this.proactiveRun && (stopped || !m.content.some((block) => block.type === "toolCall")))
|
|
419
438
|
this.writer.assistant(m, this.dataAccessUsage?.hasUsage());
|
|
420
439
|
}
|
|
421
440
|
break;
|
|
422
441
|
}
|
|
423
442
|
case "turn_end": {
|
|
424
443
|
const m = event.message;
|
|
425
|
-
// 主动回合的工具步也记:带 proactive 标记,平台 ②
|
|
426
|
-
|
|
444
|
+
// 主动回合的工具步也记:带 proactive 标记,平台 ② 出口整条不下发,只给模型下次读历史时看。
|
|
445
|
+
// 说到一半断了的那一步不记:工具没执行、参数不完整,正文已在 message_end 写过
|
|
446
|
+
if (m.role === "assistant" && !stoppedByErrorOrAbort(m) && m.content.some((block) => block.type === "toolCall")) {
|
|
427
447
|
this.writer.toolStep(m, event.toolResults, agent.state.tools, (toolCallId) => this.dataAccessUsage?.details(toolCallId), { dataAccessTainted: this.dataAccessUsage?.hasUsage(), proactive: this.proactiveRun });
|
|
428
448
|
}
|
|
429
449
|
break;
|