@my-life-buddies/buddy-runtime 0.0.1

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 (58) hide show
  1. package/README.md +175 -0
  2. package/dist/conversation/agent.d.ts +24 -0
  3. package/dist/conversation/agent.d.ts.map +1 -0
  4. package/dist/conversation/agent.js +94 -0
  5. package/dist/conversation/agent.js.map +1 -0
  6. package/dist/conversation/convert.d.ts +46 -0
  7. package/dist/conversation/convert.d.ts.map +1 -0
  8. package/dist/conversation/convert.js +275 -0
  9. package/dist/conversation/convert.js.map +1 -0
  10. package/dist/conversation/history.d.ts +18 -0
  11. package/dist/conversation/history.d.ts.map +1 -0
  12. package/dist/conversation/history.js +28 -0
  13. package/dist/conversation/history.js.map +1 -0
  14. package/dist/conversation/index.d.ts +92 -0
  15. package/dist/conversation/index.d.ts.map +1 -0
  16. package/dist/conversation/index.js +322 -0
  17. package/dist/conversation/index.js.map +1 -0
  18. package/dist/conversation/presence.d.ts +24 -0
  19. package/dist/conversation/presence.d.ts.map +1 -0
  20. package/dist/conversation/presence.js +56 -0
  21. package/dist/conversation/presence.js.map +1 -0
  22. package/dist/conversation/writeBack.d.ts +36 -0
  23. package/dist/conversation/writeBack.d.ts.map +1 -0
  24. package/dist/conversation/writeBack.js +114 -0
  25. package/dist/conversation/writeBack.js.map +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +4 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/log.d.ts +5 -0
  31. package/dist/log.d.ts.map +1 -0
  32. package/dist/log.js +17 -0
  33. package/dist/log.js.map +1 -0
  34. package/dist/mlbClient.d.ts +118 -0
  35. package/dist/mlbClient.d.ts.map +1 -0
  36. package/dist/mlbClient.js +322 -0
  37. package/dist/mlbClient.js.map +1 -0
  38. package/dist/model.d.ts +24 -0
  39. package/dist/model.d.ts.map +1 -0
  40. package/dist/model.js +45 -0
  41. package/dist/model.js.map +1 -0
  42. package/dist/server.d.ts +6 -0
  43. package/dist/server.d.ts.map +1 -0
  44. package/dist/server.js +99 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/tools/resource.d.ts +18 -0
  47. package/dist/tools/resource.d.ts.map +1 -0
  48. package/dist/tools/resource.js +57 -0
  49. package/dist/tools/resource.js.map +1 -0
  50. package/dist/tools/widgets.d.ts +16 -0
  51. package/dist/tools/widgets.d.ts.map +1 -0
  52. package/dist/tools/widgets.js +205 -0
  53. package/dist/tools/widgets.js.map +1 -0
  54. package/dist/types.d.ts +137 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +8 -0
  57. package/dist/types.js.map +1 -0
  58. package/package.json +37 -0
@@ -0,0 +1,18 @@
1
+ import type { PlatformMessage } from "../types.ts";
2
+ /** 首次拉取:末尾一条若是对方说的,不灌历史,留给回合按正常规矩处理(该开口就开口) */
3
+ export declare function splitForRebuild(records: PlatformMessage[]): {
4
+ history: PlatformMessage[];
5
+ tail: PlatformMessage | undefined;
6
+ };
7
+ /** 一批记录里最大的 seq;空批回原游标 */
8
+ export declare function maxSeq(records: PlatformMessage[], from: number): number;
9
+ /**
10
+ * 一批增量:游标之后的、对方说的(author ≠ buddy)、有正文或是转发卡的,才是要处理的;
11
+ * 「该不该开口」只看平台下发的 shouldReply,运行时不判、不让模型猜。
12
+ */
13
+ export declare function incoming(batch: PlatformMessage[], pulledSeq: number): {
14
+ fresh: PlatformMessage[];
15
+ theirs: PlatformMessage[];
16
+ shouldReply: boolean;
17
+ };
18
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/conversation/history.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,+CAA+C;AAC/C,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG;IAAE,OAAO,EAAE,eAAe,EAAE,CAAC;IAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAAA;CAAE,CAI7H;AAED,0BAA0B;AAC1B,wBAAgB,MAAM,CAAC,OAAO,EAAE,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIvE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IAAC,MAAM,EAAE,eAAe,EAAE,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAInJ"}
@@ -0,0 +1,28 @@
1
+ // 拉回来的平台记录怎么分拣:哪些灌进历史、哪条留给回合、哪些是对方说的、要不要开口。纯函数,不碰 Agent。
2
+ // 管:首次拉取(重建)的切分;每批增量的过滤与判定。
3
+ // 不管:记录换成 pi 消息的形状(convert.ts)、分拣之后怎么处理(index.ts)。
4
+ /** 首次拉取:末尾一条若是对方说的,不灌历史,留给回合按正常规矩处理(该开口就开口) */
5
+ export function splitForRebuild(records) {
6
+ const last = records[records.length - 1];
7
+ if (last === undefined || last.author === "buddy")
8
+ return { history: records, tail: undefined };
9
+ return { history: records.slice(0, -1), tail: last };
10
+ }
11
+ /** 一批记录里最大的 seq;空批回原游标 */
12
+ export function maxSeq(records, from) {
13
+ let seq = from;
14
+ for (const m of records)
15
+ if (m.seq > seq)
16
+ seq = m.seq;
17
+ return seq;
18
+ }
19
+ /**
20
+ * 一批增量:游标之后的、对方说的(author ≠ buddy)、有正文或是转发卡的,才是要处理的;
21
+ * 「该不该开口」只看平台下发的 shouldReply,运行时不判、不让模型猜。
22
+ */
23
+ export function incoming(batch, pulledSeq) {
24
+ const fresh = batch.filter((m) => m.seq > pulledSeq);
25
+ const theirs = fresh.filter((m) => m.author !== "buddy" && ((m.text ?? "") !== "" || m.messagesCard !== undefined));
26
+ return { fresh, theirs, shouldReply: theirs.some((m) => m.shouldReply === true) };
27
+ }
28
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../../src/conversation/history.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,4BAA4B;AAC5B,mDAAmD;AAInD,+CAA+C;AAC/C,MAAM,UAAU,eAAe,CAAC,OAA0B;IACzD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,KAAK,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAChG,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACtD,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,MAAM,CAAC,OAA0B,EAAE,IAAY;IAC9D,IAAI,GAAG,GAAG,IAAI,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,OAAO;QAAE,IAAI,CAAC,CAAC,GAAG,GAAG,GAAG;YAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;IACtD,OAAO,GAAG,CAAC;AACZ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAwB,EAAE,SAAiB;IACnE,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,SAAS,CAAC,CAAC;IACrD,MAAM,MAAM,GAAG,KAAK,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,CAAC,CAAC,CAAC;IACpH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,IAAI,CAAC,EAAE,CAAC;AACnF,CAAC"}
@@ -0,0 +1,92 @@
1
+ import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import type { MLBClient, WidgetType } from "../mlbClient.ts";
4
+ import type { BuddyFactory, BuddyLogger } from "../types.ts";
5
+ export interface ConversationHandleDeps {
6
+ /** 平台会话键:p_ 私聊、g_ 群 */
7
+ key: string;
8
+ buddyId: string;
9
+ client: MLBClient;
10
+ model: Model<Api>;
11
+ streamFn: StreamFn;
12
+ /** 搭子令牌:每回合经 Agent 的 getApiKey 交给模型通道,变成 Authorization: Bearer 头 */
13
+ token: string;
14
+ /** 小挂件类型目录,server 懒拉一次后一直给同一个 promise */
15
+ widgetTypes: () => Promise<WidgetType[]>;
16
+ factory: BuddyFactory;
17
+ log: BuddyLogger;
18
+ /** 拉取响应没有 context 字段(键已作废)时通知 server 把这场会话从表里删掉 */
19
+ onInvalid: () => void;
20
+ }
21
+ export declare class ConversationHandle {
22
+ private readonly deps;
23
+ private readonly identity;
24
+ private readonly presence;
25
+ private readonly writeBack;
26
+ /** 懒建:第一次拉到消息时跑 factory 再 new */
27
+ private agent;
28
+ /** 同会话的拉取串行链:notify 只往链尾排,绝不并发拉 */
29
+ private pullChain;
30
+ /** 读方向游标:已经拉到并处理过的最大 seq(含自己写的) */
31
+ private pulledSeq;
32
+ /** 首次拉取(after=0 全量灌历史)做过没有 */
33
+ private rebuilt;
34
+ /** 平台随拉取下发的 context 段,每回合由 transformContext 插进历史末尾 */
35
+ private lastContext;
36
+ /** 忙时听见的、不用开口的旁听消息,回合收尾再进历史 */
37
+ private heard;
38
+ /** 当前回合的 promise,close 要等它 */
39
+ private turn;
40
+ /** 本回合有没有一条 assistant 消息带非空正文且正常收尾 */
41
+ private spoke;
42
+ /** 本回合有没有一条 assistant 消息是出错 / 被掐断收尾的——这种不补 NUDGE(半截已定格、错误已记,再驱动一轮只会在错误后面多说一句) */
43
+ private failed;
44
+ private _busy;
45
+ private _closed;
46
+ private _lastActivity;
47
+ constructor(deps: ConversationHandleDeps);
48
+ /** 正在跑回合(含收尾:等出站队列清空) */
49
+ get busy(): boolean;
50
+ /** 最近一次有消息进来或回合收尾的时刻,server 按它做空闲淘汰 */
51
+ get lastActivity(): number;
52
+ /** 已退役:不再拉、不再写、事件全丢 */
53
+ get closed(): boolean;
54
+ /** 测试探针:Agent 当前的历史;还没建 Agent 时是空数组 */
55
+ get transcript(): readonly AgentMessage[];
56
+ /** SSE message 到了:把一次拉取排进串行链。已退役的直接忽略 */
57
+ notify(): void;
58
+ /**
59
+ * SSE delete 到了(或 server 空闲淘汰 / 键作废):先标退役,再打断正在跑的回合,等回合收尾。可以重复调。
60
+ */
61
+ close(): Promise<void>;
62
+ /** 标退役:写回队列清空并打断重发等待(回合收尾才不会被它拖住)、攒着的 delta 扔掉 */
63
+ private retire;
64
+ /**
65
+ * 一次拉取:从 pulledSeq 往后翻到不满页。响应没有 context = 键已作废 → 退役、通知 server;
66
+ * 有 context 就存下来。第一次走重建,之后走 ingest。网络错误只记 warn,等下次通知再拉。
67
+ */
68
+ private pull;
69
+ /**
70
+ * 首次拉取:建 Agent,把全量历史灌进 state.messages(不经事件,天然不会被写回平台),不起回合。
71
+ * 末尾一条若是对方说的,不灌,留给 ingest 按正常规矩处理(该开口就开口)。
72
+ */
73
+ private rebuild;
74
+ /**
75
+ * 一批新记录:游标推到最大 seq(自己写的也算,下次不再拉到);只留对方说的、有正文(或是转发卡)的,换成 user 消息;
76
+ * 空闲 + 不用开口 → 直接进历史;空闲 + 要开口 → 起回合;忙 + 不用开口 → 攒进 heard;忙 + 要开口 → steer 进当前回合。
77
+ */
78
+ private ingest;
79
+ /**
80
+ * 跑一个回合。messages 有值就 prompt 它们;没值(收尾时发现队列里还有消息)就直接 continue。
81
+ * prompt 完把回合中 steer 进来的消化完;一句话没说就 followUp 一条 note 提醒再跑一次。
82
+ * 收尾:heard 进历史、delta 发完、replying 关掉、等出站队列清空,才算不忙。
83
+ * 收尾期间又有人 steer(busy 还是 true,走不了 prompt),就接着再跑一个回合把它消化掉。
84
+ */
85
+ private runTurn;
86
+ /**
87
+ * Agent 事件 → 写回队列 / 在线态。pi 会等这个函数返回再往下走,所以只做同步入队,网络在队列自己的链上;
88
+ * 抛错会让 pi 把整个回合判失败,所以整体 try/catch。退役后的事件直接丢。
89
+ */
90
+ private onEvent;
91
+ }
92
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/conversation/index.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAqB,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC/F,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAIxD,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAgC,MAAM,aAAa,CAAC;AAM3F,MAAM,WAAW,sBAAsB;IACtC,uBAAuB;IACvB,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,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,yCAAyC;IACzC,WAAW,EAAE,MAAM,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,GAAG,EAAE,WAAW,CAAC;IACjB,mDAAmD;IACnD,SAAS,EAAE,MAAM,IAAI,CAAC;CACtB;AAQD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IAEtC,iCAAiC;IACjC,OAAO,CAAC,KAAK,CAAoB;IACjC,mCAAmC;IACnC,OAAO,CAAC,SAAS,CAAoC;IACrD,mCAAmC;IACnC,OAAO,CAAC,SAAS,CAAK;IACtB,8BAA8B;IAC9B,OAAO,CAAC,OAAO,CAAS;IACxB,sDAAsD;IACtD,OAAO,CAAC,WAAW,CAAqB;IACxC,+BAA+B;IAC/B,OAAO,CAAC,KAAK,CAAsB;IACnC,8BAA8B;IAC9B,OAAO,CAAC,IAAI,CAA4B;IACxC,sCAAsC;IACtC,OAAO,CAAC,KAAK,CAAS;IACtB,iFAAiF;IACjF,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAc;gBAEvB,IAAI,EAAE,sBAAsB;IAcxC,yBAAyB;IACzB,IAAI,IAAI,IAAI,OAAO,CAElB;IAED,uCAAuC;IACvC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,uBAAuB;IACvB,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,uCAAuC;IACvC,IAAI,UAAU,IAAI,SAAS,YAAY,EAAE,CAExC;IAED,yCAAyC;IACzC,MAAM,IAAI,IAAI;IAQd;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,kDAAkD;IAClD,OAAO,CAAC,MAAM;IAQd;;;OAGG;YACW,IAAI;IAsClB;;;OAGG;YACW,OAAO;IAgBrB;;;OAGG;IACH,OAAO,CAAC,MAAM;IAqBd;;;;;OAKG;YACW,OAAO;IA6CrB;;;OAGG;IACH,OAAO,CAAC,OAAO;CAuDf"}
@@ -0,0 +1,322 @@
1
+ // 一场会话的纪律:什么时候拉、拉到怎么办、什么时候起回合、Agent 事件往哪送、怎么退役。
2
+ // 管:同会话拉取串行链与读方向游标;首次拉取把全量历史灌进 Agent(重建);之后每批按空闲 / 忙碌 × 要不要开口四路分发;
3
+ // 回合(prompt → 把回合中排进队列的消息消化完 → 一句话没说就补一句提醒再跑一次);Agent 事件分给写回队列与在线态;
4
+ // close(先标退役、再打断回合、等它收尾)。
5
+ // 不管:Agent 怎么建(agent.ts)、拉回来的记录怎么分拣(history.ts)、写回平台(writeBack.ts)、replying / delta 推送(presence.ts)、
6
+ // HTTP 请求本身(mlbClient.ts)、平台记录与 pi 消息的形状换算(convert.ts)、会话从哪来到哪去(server.ts 管 Map、空闲淘汰与 delete 事件)。
7
+ import { randomUUID } from "node:crypto";
8
+ import { outboundFromAssistant, outboundFromNote, outboundFromTurn, textOf, toAgentMessages } from "./convert.js";
9
+ import { errorText } from "../log.js";
10
+ import { modelIdentity } from "../model.js";
11
+ import { createAgent } from "./agent.js";
12
+ import { incoming, maxSeq, splitForRebuild } from "./history.js";
13
+ import { Presence } from "./presence.js";
14
+ import { WriteBack } from "./writeBack.js";
15
+ /** 每页拉多少条:平台钳在 1..200,翻页靠不满页 */
16
+ const PULL_LIMIT = 200;
17
+ /** 回合跑完一句话都没说时,以 note 的身份补给模型的一句提醒 */
18
+ const NUDGE = "你上一条回复正文是空的。把要说的话直接说出来。";
19
+ export class ConversationHandle {
20
+ deps;
21
+ identity;
22
+ presence;
23
+ writeBack;
24
+ /** 懒建:第一次拉到消息时跑 factory 再 new */
25
+ agent;
26
+ /** 同会话的拉取串行链:notify 只往链尾排,绝不并发拉 */
27
+ pullChain = Promise.resolve();
28
+ /** 读方向游标:已经拉到并处理过的最大 seq(含自己写的) */
29
+ pulledSeq = 0;
30
+ /** 首次拉取(after=0 全量灌历史)做过没有 */
31
+ rebuilt = false;
32
+ /** 平台随拉取下发的 context 段,每回合由 transformContext 插进历史末尾 */
33
+ lastContext;
34
+ /** 忙时听见的、不用开口的旁听消息,回合收尾再进历史 */
35
+ heard = [];
36
+ /** 当前回合的 promise,close 要等它 */
37
+ turn;
38
+ /** 本回合有没有一条 assistant 消息带非空正文且正常收尾 */
39
+ spoke = false;
40
+ /** 本回合有没有一条 assistant 消息是出错 / 被掐断收尾的——这种不补 NUDGE(半截已定格、错误已记,再驱动一轮只会在错误后面多说一句) */
41
+ failed = false;
42
+ _busy = false;
43
+ _closed = false;
44
+ _lastActivity = Date.now();
45
+ constructor(deps) {
46
+ this.deps = deps;
47
+ this.identity = modelIdentity(deps.model);
48
+ this.presence = new Presence(deps.client, deps.key);
49
+ this.writeBack = new WriteBack({
50
+ client: deps.client,
51
+ key: deps.key,
52
+ buddyId: deps.buddyId,
53
+ runId: randomUUID().slice(0, 8),
54
+ log: deps.log,
55
+ beforeSend: () => this.presence.flushDelta(),
56
+ });
57
+ }
58
+ /** 正在跑回合(含收尾:等出站队列清空) */
59
+ get busy() {
60
+ return this._busy;
61
+ }
62
+ /** 最近一次有消息进来或回合收尾的时刻,server 按它做空闲淘汰 */
63
+ get lastActivity() {
64
+ return this._lastActivity;
65
+ }
66
+ /** 已退役:不再拉、不再写、事件全丢 */
67
+ get closed() {
68
+ return this._closed;
69
+ }
70
+ /** 测试探针:Agent 当前的历史;还没建 Agent 时是空数组 */
71
+ get transcript() {
72
+ return this.agent?.state.messages ?? [];
73
+ }
74
+ /** SSE message 到了:把一次拉取排进串行链。已退役的直接忽略 */
75
+ notify() {
76
+ if (this._closed)
77
+ return;
78
+ this._lastActivity = Date.now();
79
+ this.pullChain = this.pullChain
80
+ .then(() => this.pull())
81
+ .catch((e) => this.deps.log.warn("处理拉取结果出错", { convoKey: this.deps.key, error: errorText(e) }));
82
+ }
83
+ /**
84
+ * SSE delete 到了(或 server 空闲淘汰 / 键作废):先标退役,再打断正在跑的回合,等回合收尾。可以重复调。
85
+ */
86
+ async close() {
87
+ this.retire();
88
+ if (this._busy)
89
+ this.agent?.abort();
90
+ await this.turn;
91
+ }
92
+ /** 标退役:写回队列清空并打断重发等待(回合收尾才不会被它拖住)、攒着的 delta 扔掉 */
93
+ retire() {
94
+ this._closed = true;
95
+ this.writeBack.close();
96
+ this.presence.close();
97
+ }
98
+ // —— 拉取
99
+ /**
100
+ * 一次拉取:从 pulledSeq 往后翻到不满页。响应没有 context = 键已作废 → 退役、通知 server;
101
+ * 有 context 就存下来。第一次走重建,之后走 ingest。网络错误只记 warn,等下次通知再拉。
102
+ */
103
+ async pull() {
104
+ if (this._closed)
105
+ return;
106
+ const batch = [];
107
+ let after = this.pulledSeq;
108
+ for (;;) {
109
+ let page;
110
+ try {
111
+ page = await this.deps.client.pullMessages(this.deps.key, after, PULL_LIMIT);
112
+ }
113
+ catch (e) {
114
+ this.deps.log.warn("拉消息失败,等下次通知再拉", { convoKey: this.deps.key, after, error: errorText(e) });
115
+ return;
116
+ }
117
+ if (this._closed)
118
+ return;
119
+ if (page.context === undefined) {
120
+ this.deps.log.info("会话键已作废,退役", { convoKey: this.deps.key });
121
+ this.retire();
122
+ this.deps.onInvalid();
123
+ return;
124
+ }
125
+ this.lastContext = page.context;
126
+ batch.push(...page.messages);
127
+ if (page.messages.length < PULL_LIMIT)
128
+ break;
129
+ after = page.messages[page.messages.length - 1].seq;
130
+ }
131
+ if (this.rebuilt) {
132
+ this.ingest(batch);
133
+ return;
134
+ }
135
+ try {
136
+ await this.rebuild(batch);
137
+ }
138
+ catch (e) {
139
+ // factory 抛错、工具重名之类:这场会话起不来。退役并让 server 删掉,下一条消息会重新起一场再试
140
+ this.deps.log.error("起会话失败", { convoKey: this.deps.key, error: errorText(e) });
141
+ this.retire();
142
+ this.deps.onInvalid();
143
+ }
144
+ }
145
+ /**
146
+ * 首次拉取:建 Agent,把全量历史灌进 state.messages(不经事件,天然不会被写回平台),不起回合。
147
+ * 末尾一条若是对方说的,不灌,留给 ingest 按正常规矩处理(该开口就开口)。
148
+ */
149
+ async rebuild(records) {
150
+ const agent = await createAgent({
151
+ ...this.deps,
152
+ contextOf: () => this.lastContext,
153
+ onEvent: (a, event) => this.onEvent(a, event),
154
+ });
155
+ if (this._closed)
156
+ return;
157
+ this.agent = agent;
158
+ const { history, tail } = splitForRebuild(records);
159
+ agent.state.messages = toAgentMessages(history, this.identity);
160
+ this.pulledSeq = maxSeq(history, this.pulledSeq);
161
+ this.rebuilt = true;
162
+ this.deps.log.info("历史已重建", { convoKey: this.deps.key, records: history.length, transcript: agent.state.messages.length });
163
+ if (tail !== undefined)
164
+ this.ingest([tail]);
165
+ }
166
+ /**
167
+ * 一批新记录:游标推到最大 seq(自己写的也算,下次不再拉到);只留对方说的、有正文(或是转发卡)的,换成 user 消息;
168
+ * 空闲 + 不用开口 → 直接进历史;空闲 + 要开口 → 起回合;忙 + 不用开口 → 攒进 heard;忙 + 要开口 → steer 进当前回合。
169
+ */
170
+ ingest(batch) {
171
+ const agent = this.agent;
172
+ if (agent === undefined || this._closed)
173
+ return;
174
+ const { fresh, theirs, shouldReply } = incoming(batch, this.pulledSeq);
175
+ if (fresh.length === 0)
176
+ return;
177
+ this.pulledSeq = maxSeq(fresh, this.pulledSeq);
178
+ this._lastActivity = Date.now();
179
+ if (theirs.length === 0)
180
+ return;
181
+ const messages = toAgentMessages(theirs, this.identity);
182
+ if (messages.length === 0)
183
+ return;
184
+ if (!this._busy) {
185
+ if (shouldReply)
186
+ this.turn = this.runTurn(messages);
187
+ else
188
+ agent.state.messages.push(...messages);
189
+ return;
190
+ }
191
+ if (shouldReply)
192
+ for (const m of messages)
193
+ agent.steer(m);
194
+ else
195
+ this.heard.push(...messages);
196
+ }
197
+ // —— 回合
198
+ /**
199
+ * 跑一个回合。messages 有值就 prompt 它们;没值(收尾时发现队列里还有消息)就直接 continue。
200
+ * prompt 完把回合中 steer 进来的消化完;一句话没说就 followUp 一条 note 提醒再跑一次。
201
+ * 收尾:heard 进历史、delta 发完、replying 关掉、等出站队列清空,才算不忙。
202
+ * 收尾期间又有人 steer(busy 还是 true,走不了 prompt),就接着再跑一个回合把它消化掉。
203
+ */
204
+ async runTurn(messages) {
205
+ const agent = this.agent;
206
+ if (agent === undefined || this._closed)
207
+ return;
208
+ this._busy = true;
209
+ this.spoke = false;
210
+ this.failed = false;
211
+ this.presence.replying({ on: true });
212
+ const startedAt = Date.now();
213
+ this.deps.log.info("回合开始", { convoKey: this.deps.key, prompt: messages?.length ?? 0, transcript: agent.state.messages.length });
214
+ try {
215
+ if (messages !== undefined)
216
+ await agent.prompt(messages);
217
+ this.deps.log.info("prompt 返回", { convoKey: this.deps.key, queued: agent.hasQueuedMessages(), spoke: this.spoke, failed: this.failed });
218
+ while (!this._closed && agent.hasQueuedMessages())
219
+ await agent.continue();
220
+ // 正常收尾却一句正文没有才补;出错 / 被掐断的不补
221
+ if (!this._closed && !this.spoke && !this.failed) {
222
+ this.deps.log.info("空回复,补一句", { convoKey: this.deps.key });
223
+ const nudge = { role: "note", text: NUDGE, timestamp: Date.now() };
224
+ agent.followUp(nudge);
225
+ await agent.continue();
226
+ }
227
+ }
228
+ catch (e) {
229
+ this.deps.log.warn("回合出错", { convoKey: this.deps.key, error: errorText(e) });
230
+ }
231
+ finally {
232
+ if (this.heard.length > 0) {
233
+ agent.state.messages.push(...this.heard);
234
+ this.heard = [];
235
+ }
236
+ this.presence.flushDelta();
237
+ this.presence.replying({ on: false });
238
+ await this.writeBack.flush();
239
+ this._busy = false;
240
+ this._lastActivity = Date.now();
241
+ this.deps.log.info("回合收尾", {
242
+ convoKey: this.deps.key,
243
+ ms: Date.now() - startedAt,
244
+ spoke: this.spoke,
245
+ failed: this.failed,
246
+ error: agent.state.errorMessage,
247
+ });
248
+ if (!this._closed && agent.hasQueuedMessages())
249
+ this.turn = this.runTurn(undefined);
250
+ }
251
+ }
252
+ // —— Agent 事件
253
+ /**
254
+ * Agent 事件 → 写回队列 / 在线态。pi 会等这个函数返回再往下走,所以只做同步入队,网络在队列自己的链上;
255
+ * 抛错会让 pi 把整个回合判失败,所以整体 try/catch。退役后的事件直接丢。
256
+ */
257
+ onEvent(agent, event) {
258
+ if (this._closed)
259
+ return;
260
+ try {
261
+ switch (event.type) {
262
+ case "agent_start":
263
+ this.deps.log.info("run 开始", { convoKey: this.deps.key });
264
+ break;
265
+ case "agent_end":
266
+ this.deps.log.info("run 结束", { convoKey: this.deps.key, newMessages: event.messages.length, queued: agent.hasQueuedMessages() });
267
+ break;
268
+ case "message_end": {
269
+ const m = event.message;
270
+ if (m.role === "assistant") {
271
+ this.deps.log.info("assistant 一段", {
272
+ convoKey: this.deps.key,
273
+ stopReason: m.stopReason,
274
+ chars: textOf(m).length,
275
+ toolCalls: m.content.filter((block) => block.type === "toolCall").length,
276
+ error: m.errorMessage,
277
+ });
278
+ if (m.stopReason === "error" || m.stopReason === "aborted")
279
+ this.failed = true;
280
+ else if (textOf(m) !== "")
281
+ this.spoke = true;
282
+ // 带工具调用的等 turn_end 连结果一起写成 tool 记录
283
+ if (!m.content.some((block) => block.type === "toolCall")) {
284
+ const out = outboundFromAssistant(m);
285
+ if (out !== null)
286
+ this.writeBack.enqueue(out);
287
+ }
288
+ }
289
+ else if (m.role === "note") {
290
+ this.writeBack.enqueue(outboundFromNote(m));
291
+ }
292
+ break;
293
+ }
294
+ case "turn_end": {
295
+ const m = event.message;
296
+ if (m.role === "assistant" && m.content.some((block) => block.type === "toolCall")) {
297
+ const out = outboundFromTurn(m, event.toolResults, agent.state.tools);
298
+ if (out !== null)
299
+ this.writeBack.enqueue(out);
300
+ }
301
+ break;
302
+ }
303
+ case "message_update":
304
+ if (event.assistantMessageEvent.type === "text_delta")
305
+ this.presence.delta(event.assistantMessageEvent.delta);
306
+ break;
307
+ case "tool_execution_start": {
308
+ const label = agent.state.tools.find((t) => t.name === event.toolName)?.label;
309
+ if (label)
310
+ this.presence.replying({ on: true, status: label });
311
+ break;
312
+ }
313
+ default:
314
+ break;
315
+ }
316
+ }
317
+ catch (e) {
318
+ this.deps.log.warn("处理 Agent 事件出错", { convoKey: this.deps.key, event: event.type, error: errorText(e) });
319
+ }
320
+ }
321
+ }
322
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/conversation/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { MLBClient } from "../mlbClient.ts";
2
+ export declare class Presence {
3
+ private readonly client;
4
+ private readonly key;
5
+ /** 推送串行链:只保证顺序,不等它 */
6
+ private chain;
7
+ private deltaBuffer;
8
+ private deltaTimer;
9
+ private closed;
10
+ constructor(client: MLBClient, key: string);
11
+ /** 回合开始 on、收尾 off;工具跑起来时带 status 更新状态行 */
12
+ replying(data: {
13
+ on: boolean;
14
+ status?: string;
15
+ }): void;
16
+ /** 正文碎片先攒着,120 毫秒内攒到的一起发 */
17
+ delta(text: string): void;
18
+ /** 把攒下的碎片立刻发出去;退役了就扔掉。写平台前、回合收尾前各叫一次,预览不落在正文后面 */
19
+ flushDelta(): void;
20
+ /** 退役:攒着的碎片扔掉,定时器清掉。replying off 仍然放行——收尾要把三个圆点熄掉 */
21
+ close(): void;
22
+ private clearTimer;
23
+ }
24
+ //# sourceMappingURL=presence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../src/conversation/presence.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKjD,qBAAa,QAAQ;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,sBAAsB;IACtB,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,UAAU,CAA4C;IAC9D,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM;IAK1C,0CAA0C;IAC1C,QAAQ,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAItD,4BAA4B;IAC5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAWzB,kDAAkD;IAClD,UAAU,IAAI,IAAI;IAQlB,qDAAqD;IACrD,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,UAAU;CAKlB"}
@@ -0,0 +1,56 @@
1
+ // 一场会话的在线态:replying 开关与工具状态行、生成中的正文碎片(delta)。
2
+ // 管:把 replying / delta 按顺序推给平台(POST /internal/v1/push),只保证顺序、不等结果;delta 每 120 毫秒攒一帧。
3
+ // 不管:什么时候该开关 replying、碎片从哪来(index.ts 按 Agent 事件调这里)。
4
+ /** 生成中的正文碎片攒多久发一帧 delta */
5
+ const DELTA_THROTTLE_MS = 120;
6
+ export class Presence {
7
+ client;
8
+ key;
9
+ /** 推送串行链:只保证顺序,不等它 */
10
+ chain = Promise.resolve();
11
+ deltaBuffer = "";
12
+ deltaTimer;
13
+ closed = false;
14
+ constructor(client, key) {
15
+ this.client = client;
16
+ this.key = key;
17
+ }
18
+ /** 回合开始 on、收尾 off;工具跑起来时带 status 更新状态行 */
19
+ replying(data) {
20
+ this.chain = this.chain.then(() => this.client.push(this.key, "replying", data)).catch(() => { });
21
+ }
22
+ /** 正文碎片先攒着,120 毫秒内攒到的一起发 */
23
+ delta(text) {
24
+ if (text === "" || this.closed)
25
+ return;
26
+ this.deltaBuffer += text;
27
+ if (this.deltaTimer === undefined) {
28
+ this.deltaTimer = setTimeout(() => {
29
+ this.deltaTimer = undefined;
30
+ this.flushDelta();
31
+ }, DELTA_THROTTLE_MS);
32
+ }
33
+ }
34
+ /** 把攒下的碎片立刻发出去;退役了就扔掉。写平台前、回合收尾前各叫一次,预览不落在正文后面 */
35
+ flushDelta() {
36
+ this.clearTimer();
37
+ const text = this.deltaBuffer;
38
+ this.deltaBuffer = "";
39
+ if (text === "" || this.closed)
40
+ return;
41
+ this.chain = this.chain.then(() => this.client.push(this.key, "delta", { text })).catch(() => { });
42
+ }
43
+ /** 退役:攒着的碎片扔掉,定时器清掉。replying off 仍然放行——收尾要把三个圆点熄掉 */
44
+ close() {
45
+ this.closed = true;
46
+ this.clearTimer();
47
+ this.deltaBuffer = "";
48
+ }
49
+ clearTimer() {
50
+ if (this.deltaTimer === undefined)
51
+ return;
52
+ clearTimeout(this.deltaTimer);
53
+ this.deltaTimer = undefined;
54
+ }
55
+ }
56
+ //# sourceMappingURL=presence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/conversation/presence.ts"],"names":[],"mappings":"AAAA,8CAA8C;AAC9C,qFAAqF;AACrF,qDAAqD;AAIrD,2BAA2B;AAC3B,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B,MAAM,OAAO,QAAQ;IACH,MAAM,CAAY;IAClB,GAAG,CAAS;IAC7B,sBAAsB;IACd,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IACzC,WAAW,GAAG,EAAE,CAAC;IACjB,UAAU,CAA4C;IACtD,MAAM,GAAG,KAAK,CAAC;IAEvB,YAAY,MAAiB,EAAE,GAAW;QACzC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IAChB,CAAC;IAED,0CAA0C;IAC1C,QAAQ,CAAC,IAAsC;QAC9C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClG,CAAC;IAED,4BAA4B;IAC5B,KAAK,CAAC,IAAY;QACjB,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACvC,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC;QACzB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACnC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE;gBACjC,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;gBAC5B,IAAI,CAAC,UAAU,EAAE,CAAC;YACnB,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACvB,CAAC;IACF,CAAC;IAED,kDAAkD;IAClD,UAAU;QACT,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;QACtB,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACvC,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACnG,CAAC;IAED,qDAAqD;IACrD,KAAK;QACJ,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;IACvB,CAAC;IAEO,UAAU;QACjB,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,OAAO;QAC1C,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC9B,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;IAC7B,CAAC;CACD"}
@@ -0,0 +1,36 @@
1
+ import type { MLBClient } from "../mlbClient.ts";
2
+ import type { BuddyLogger, OutgoingMessage } from "../types.ts";
3
+ export interface WriteBackDeps {
4
+ client: MLBClient;
5
+ key: string;
6
+ buddyId: string;
7
+ /** 本进程里这场会话的一次生命周期标识,进幂等键;重启后换一个,旧键不会撞新键 */
8
+ runId: string;
9
+ log: BuddyLogger;
10
+ /** 每次真正写平台之前先调一下(把攒着的 delta 发完,预览不落在正文后面) */
11
+ beforeSend: () => void;
12
+ }
13
+ export declare class WriteBack {
14
+ private readonly deps;
15
+ private queue;
16
+ /** 串行链正在跑 */
17
+ private sending;
18
+ /** 幂等键里的自增号 */
19
+ private sent;
20
+ private retryDelay;
21
+ /** close 时打断重发等待 */
22
+ private readonly abort;
23
+ /** 等队列清空的人 */
24
+ private waiters;
25
+ private closed;
26
+ constructor(deps: WriteBackDeps);
27
+ /** 入队即分配幂等键;退役后不收 */
28
+ enqueue(message: OutgoingMessage): void;
29
+ /** 等队列清空(或退役) */
30
+ flush(): Promise<void>;
31
+ /** 退役:正在退避等重发的立刻醒来并退出,队列清空 */
32
+ close(): void;
33
+ /** 一次发队头一条:delivered → 出队;rejected(4xx)→ warn 出队;throw(网络 / 5xx)→ 留队退避后带同一个键重发 */
34
+ private drain;
35
+ }
36
+ //# sourceMappingURL=writeBack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"writeBack.d.ts","sourceRoot":"","sources":["../../src/conversation/writeBack.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,iBAAiB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAMhE,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,SAAS,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,WAAW,CAAC;IACjB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,IAAI,CAAC;CACvB;AAED,qBAAa,SAAS;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAgB;IACrC,OAAO,CAAC,KAAK,CAAyB;IACtC,aAAa;IACb,OAAO,CAAC,OAAO,CAAS;IACxB,eAAe;IACf,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,UAAU,CAAsB;IACxC,oBAAoB;IACpB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyB;IAC/C,cAAc;IACd,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,MAAM,CAAS;gBAEX,IAAI,EAAE,aAAa;IAI/B,qBAAqB;IACrB,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAOvC,iBAAiB;IACjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,8BAA8B;IAC9B,KAAK,IAAI,IAAI;IAMb,kFAAkF;YACpE,KAAK;CA2CnB"}