dsh-galatea 0.3.5 → 0.3.6
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/index.js +28 -1
- package/lib/beat-tool.js +16 -0
- package/lib/heartbeat-runner.js +26 -14
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import z from '@deepseek-ai/schemastery';
|
|
|
16
16
|
import { renderIndex, dataRoot, listEntries } from './lib/store.js';
|
|
17
17
|
import { remember, recall } from './lib/tools.js';
|
|
18
18
|
import { makeBeatTool, snapshotAgents } from './lib/beat-tool.js';
|
|
19
|
-
import { loadState } from './lib/heartbeat-runner.js';
|
|
19
|
+
import { loadState, noteUserMessage, stateFile } from './lib/heartbeat-runner.js';
|
|
20
20
|
import { DEFAULTS } from './lib/heartbeat.js';
|
|
21
21
|
|
|
22
22
|
/** Cordis 插件名。 */
|
|
@@ -155,4 +155,31 @@ export function apply(ctx, config) {
|
|
|
155
155
|
} else {
|
|
156
156
|
console.warn('[galatea] 宿主没有 tools 服务,记忆只能读不能写。');
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
// ── 用户说话的时间点 ────────────────────────────────────
|
|
160
|
+
// cooldown(「你刚说完话,至少安静 N 分钟」)读的是 state.lastUserAt,
|
|
161
|
+
// 而它只能由「有人说了一句话」这件事更新。不接这条线,cooldown 就是死配置 ——
|
|
162
|
+
// 表现是用户刚说完话它就插嘴,而且没人知道为什么。
|
|
163
|
+
//
|
|
164
|
+
// 用 agent/pre-step:它的 payload.messages 是这一步真正要消费的消息。
|
|
165
|
+
// 判据是 source.kind === 'user' —— 那是真人发的。
|
|
166
|
+
// cron / heartbeat 这些生产者也走同一条通道,但 kind 不同,必须排除,
|
|
167
|
+
// 否则自主轮会把自己发的消息记成「用户说话了」,cooldown 永远不生效。
|
|
168
|
+
//
|
|
169
|
+
// 它是 waterfall:不调 next() 会把这一步卡住,所以无论记录成没成都要放行。
|
|
170
|
+
if (ctx && typeof ctx.on === 'function') {
|
|
171
|
+
ctx.on('agent/pre-step', (payload, next) => {
|
|
172
|
+
try {
|
|
173
|
+
const msgs = payload && Array.isArray(payload.messages) ? payload.messages : [];
|
|
174
|
+
if (msgs.some((m) => m && m.source && m.source.kind === 'user')) {
|
|
175
|
+
noteUserMessage(new Date(), stateFile());
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
console.warn('[galatea] 记录用户发言时间失败:', err && err.message);
|
|
179
|
+
}
|
|
180
|
+
return next();
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
console.warn('[galatea] 宿主没有 ctx.on,cooldown 不会生效。');
|
|
184
|
+
}
|
|
158
185
|
}
|
package/lib/beat-tool.js
CHANGED
|
@@ -88,6 +88,19 @@ export function makeBeatTool(deps) {
|
|
|
88
88
|
// 给人看的那几行:把它自己最近的决定翻成人话。
|
|
89
89
|
const journal = renderJournal(state, { quietRounds: liveCfg.quietRounds });
|
|
90
90
|
|
|
91
|
+
// 手动档也写账本 —— 这不是副作用,是这个工具该做的事。
|
|
92
|
+
// 不写的话:state 永远不存在,每次都是「第一次」,
|
|
93
|
+
// 连跑十次结果一模一样,而 cooldown / nothingNew / floor 一次都触发不了,
|
|
94
|
+
// 用户会得出「它每次都说话」的错误印象。
|
|
95
|
+
// 写进去之后,手动跑一次就真的等于走了一次心跳。
|
|
96
|
+
let committed = null;
|
|
97
|
+
try {
|
|
98
|
+
committed = commitDecision(state, { speak: r.speak, reason: r.reason, detail: r.detail }, new Date());
|
|
99
|
+
saveState(committed, stateFile());
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn('[galatea] 心跳账本落盘失败:', err && err.message);
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
const lines = [];
|
|
92
105
|
lines.push(r.speak ? '现在会说话。' : '现在不会说话。');
|
|
93
106
|
lines.push(`理由:${r.reason} —— ${r.detail}`);
|
|
@@ -95,6 +108,9 @@ export function makeBeatTool(deps) {
|
|
|
95
108
|
if (r.switched) lines.push(`⚠ 换了目标会话:${r.previous} → ${r.sessionId}(连续在这里断过一次)`);
|
|
96
109
|
if (picked) lines.push(`由头:${picked.id} ${picked.title}`);
|
|
97
110
|
if (r.prompt) lines.push(`会带着这段由头开口:\n${r.prompt}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push('—— 最近几次 ——');
|
|
113
|
+
lines.push(journal);
|
|
98
114
|
|
|
99
115
|
return {
|
|
100
116
|
speak: r.speak,
|
package/lib/heartbeat-runner.js
CHANGED
|
@@ -51,6 +51,31 @@ export function saveState(state, file = stateFile()) {
|
|
|
51
51
|
*
|
|
52
52
|
* @returns {{ decision: object, state: object, shouldSpeak: boolean }}
|
|
53
53
|
*/
|
|
54
|
+
/**
|
|
55
|
+
* 把一次决定写进状态。**纯函数**(不落盘),落盘由调用方决定。
|
|
56
|
+
*
|
|
57
|
+
* 抽出来是因为有两个调用点:tick(完整跑一轮)和手动档工具(只算不发)。
|
|
58
|
+
* 如果各写一份,两份必然漂移 —— 而漂移的方向永远是「手动档那份忘了更新」,
|
|
59
|
+
* 表现就是「手动跑十次结果一样」,今晚外面的人就是这么发现的。
|
|
60
|
+
*/
|
|
61
|
+
export function commitDecision(state, decision, now = new Date(), opts = {}) {
|
|
62
|
+
const at = (now instanceof Date ? now : new Date(now)).toISOString();
|
|
63
|
+
const entry = {
|
|
64
|
+
at,
|
|
65
|
+
speak: decision.speak,
|
|
66
|
+
reason: decision.reason,
|
|
67
|
+
detail: decision.detail,
|
|
68
|
+
};
|
|
69
|
+
const next = {
|
|
70
|
+
...state,
|
|
71
|
+
lastBeatAt: at,
|
|
72
|
+
quietStreak: decision.speak ? 0 : (state.quietStreak ?? 0) + 1,
|
|
73
|
+
decisions: [...(state.decisions ?? []), entry].slice(-(opts.keepDecisions ?? 50)),
|
|
74
|
+
};
|
|
75
|
+
if (decision.speak) next.lastSpokeAt = at;
|
|
76
|
+
return next;
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
export function tick(opts = {}) {
|
|
55
80
|
const now = opts.now instanceof Date ? opts.now : new Date(opts.now ?? Date.now());
|
|
56
81
|
const file = opts.file ?? stateFile();
|
|
@@ -68,20 +93,7 @@ export function tick(opts = {}) {
|
|
|
68
93
|
random: opts.random,
|
|
69
94
|
});
|
|
70
95
|
|
|
71
|
-
const
|
|
72
|
-
at: now.toISOString(),
|
|
73
|
-
speak: decision.speak,
|
|
74
|
-
reason: decision.reason,
|
|
75
|
-
detail: decision.detail,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const next = {
|
|
79
|
-
...prev,
|
|
80
|
-
lastBeatAt: now.toISOString(),
|
|
81
|
-
quietStreak: decision.speak ? 0 : (prev.quietStreak ?? 0) + 1,
|
|
82
|
-
decisions: [...(prev.decisions ?? []), entry].slice(-(opts.keepDecisions ?? 50)),
|
|
83
|
-
};
|
|
84
|
-
if (decision.speak) next.lastSpokeAt = now.toISOString();
|
|
96
|
+
const next = commitDecision(prev, decision, now, opts);
|
|
85
97
|
|
|
86
98
|
if (opts.dryRun !== true) saveState(next, file);
|
|
87
99
|
|