stella-timeline-plugin 2.0.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/LICENSE +16 -0
- package/README.md +103 -0
- package/README_ZH.md +103 -0
- package/bin/openclaw-timeline-doctor.mjs +2 -0
- package/bin/openclaw-timeline-setup.mjs +2 -0
- package/dist/index.js +26 -0
- package/dist/src/core/build_consumption_view.js +52 -0
- package/dist/src/core/calendar_dates.js +23 -0
- package/dist/src/core/collect_sources.js +39 -0
- package/dist/src/core/collect_timeline_request.js +76 -0
- package/dist/src/core/materialize_generated_candidate.js +87 -0
- package/dist/src/core/resolve_window.js +83 -0
- package/dist/src/core/runtime_guard.js +170 -0
- package/dist/src/core/timeline_reasoner_contract.js +2 -0
- package/dist/src/core/trace.js +22 -0
- package/dist/src/core/world_rhythm.js +258 -0
- package/dist/src/lib/fingerprint.js +46 -0
- package/dist/src/lib/holidays.js +95 -0
- package/dist/src/lib/inherit-appearance.js +46 -0
- package/dist/src/lib/parse-memory.js +171 -0
- package/dist/src/lib/time-utils.js +49 -0
- package/dist/src/lib/timeline_semantics.js +63 -0
- package/dist/src/lib/types.js +2 -0
- package/dist/src/openclaw-sdk-compat.js +39 -0
- package/dist/src/plugin_metadata.js +9 -0
- package/dist/src/runtime/conversation_context.js +128 -0
- package/dist/src/runtime/openclaw_timeline_runtime.js +655 -0
- package/dist/src/storage/daily_log.js +60 -0
- package/dist/src/storage/lock.js +74 -0
- package/dist/src/storage/trace_log.js +70 -0
- package/dist/src/storage/write-episode.js +164 -0
- package/dist/src/tools/timeline_resolve.js +689 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +73 -0
- package/scripts/doctor-openclaw-workspace.mjs +94 -0
- package/scripts/migrate-existing-memory.mjs +153 -0
- package/scripts/release.mjs +99 -0
- package/scripts/run-openclaw-live-e2e.mjs +64 -0
- package/scripts/run-openclaw-smoke.mjs +21 -0
- package/scripts/setup-openclaw-workspace.mjs +119 -0
- package/scripts/workspace-contract.mjs +47 -0
- package/skills/timeline/SKILL.md +111 -0
- package/templates/AGENTS.fragment.md +29 -0
- package/templates/SOUL.fragment.md +17 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.makeOpenClawTimelineResolveToolFactory = makeOpenClawTimelineResolveToolFactory;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const conversation_context_1 = require("./conversation_context");
|
|
40
|
+
const timeline_resolve_1 = require("../tools/timeline_resolve");
|
|
41
|
+
function readString(value) {
|
|
42
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
43
|
+
}
|
|
44
|
+
function readBoolean(value, fallback) {
|
|
45
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
46
|
+
}
|
|
47
|
+
function readInteger(value, fallback) {
|
|
48
|
+
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
|
49
|
+
}
|
|
50
|
+
function resolvePluginRuntimeConfig(pluginConfig) {
|
|
51
|
+
return {
|
|
52
|
+
enableTrace: readBoolean(pluginConfig?.enableTrace, true),
|
|
53
|
+
traceLogPath: readString(pluginConfig?.traceLogPath),
|
|
54
|
+
canonicalMemoryRoot: readString(pluginConfig?.canonicalMemoryRoot) || 'memory',
|
|
55
|
+
reasonerTimeoutMs: readInteger(pluginConfig?.reasonerTimeoutMs, 45000),
|
|
56
|
+
reasonerSessionPrefix: readString(pluginConfig?.reasonerSessionPrefix) || 'timeline-reasoner',
|
|
57
|
+
reasonerMessageLimit: readInteger(pluginConfig?.reasonerMessageLimit, 24),
|
|
58
|
+
sessionHistoryLimit: readInteger(pluginConfig?.sessionHistoryLimit, 12),
|
|
59
|
+
memorySearchMaxResults: readInteger(pluginConfig?.memorySearchMaxResults, 6),
|
|
60
|
+
conversationStickinessWindowMinutes: readInteger(pluginConfig?.conversationStickinessWindowMinutes, 10),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function wrapToolPayload(payload) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
66
|
+
details: payload,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function resolveWorkspaceDir(pluginApi, toolContext) {
|
|
70
|
+
if (toolContext.workspaceDir)
|
|
71
|
+
return toolContext.workspaceDir;
|
|
72
|
+
if (pluginApi.workspaceDir)
|
|
73
|
+
return pluginApi.workspaceDir;
|
|
74
|
+
if (typeof pluginApi.resolvePath === 'function')
|
|
75
|
+
return pluginApi.resolvePath('.');
|
|
76
|
+
return process.cwd();
|
|
77
|
+
}
|
|
78
|
+
function resolveConfiguredPath(workspaceDir, configuredPath, fallbackRelativePath) {
|
|
79
|
+
const raw = configuredPath || fallbackRelativePath;
|
|
80
|
+
return path.isAbsolute(raw) ? path.normalize(raw) : path.normalize(path.join(workspaceDir, raw));
|
|
81
|
+
}
|
|
82
|
+
function formatOffsetMinutes(offsetMinutes) {
|
|
83
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
84
|
+
const abs = Math.abs(offsetMinutes);
|
|
85
|
+
const hours = String(Math.floor(abs / 60)).padStart(2, '0');
|
|
86
|
+
const minutes = String(abs % 60).padStart(2, '0');
|
|
87
|
+
return `${sign}${hours}:${minutes}`;
|
|
88
|
+
}
|
|
89
|
+
function formatCurrentTimestamp(date) {
|
|
90
|
+
const year = date.getFullYear();
|
|
91
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
92
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
93
|
+
const hour = String(date.getHours()).padStart(2, '0');
|
|
94
|
+
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
95
|
+
const second = String(date.getSeconds()).padStart(2, '0');
|
|
96
|
+
const offset = formatOffsetMinutes(-date.getTimezoneOffset());
|
|
97
|
+
return `${year}-${month}-${day}T${hour}:${minute}:${second}${offset}`;
|
|
98
|
+
}
|
|
99
|
+
function readWorkspaceTextFile(filePath) {
|
|
100
|
+
try {
|
|
101
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function buildMemorySearchQuery(input) {
|
|
108
|
+
const explicit = readString(input.query);
|
|
109
|
+
if (explicit)
|
|
110
|
+
return explicit;
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function stringifySearchResult(entry) {
|
|
114
|
+
if (!entry || typeof entry !== 'object') {
|
|
115
|
+
return typeof entry === 'string' ? entry : JSON.stringify(entry);
|
|
116
|
+
}
|
|
117
|
+
const record = entry;
|
|
118
|
+
if (typeof record.snippet === 'string' && record.snippet.trim())
|
|
119
|
+
return record.snippet.trim();
|
|
120
|
+
return JSON.stringify(entry);
|
|
121
|
+
}
|
|
122
|
+
function extractMessageText(value) {
|
|
123
|
+
if (typeof value === 'string')
|
|
124
|
+
return value.trim();
|
|
125
|
+
if (!value || typeof value !== 'object')
|
|
126
|
+
return '';
|
|
127
|
+
const record = value;
|
|
128
|
+
const directKeys = ['bodyText', 'body', 'text', 'contentText'];
|
|
129
|
+
for (const key of directKeys) {
|
|
130
|
+
if (typeof record[key] === 'string' && String(record[key]).trim()) {
|
|
131
|
+
return String(record[key]).trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(record.content)) {
|
|
135
|
+
const joined = record.content
|
|
136
|
+
.map((part) => extractMessageText(part))
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join('\n');
|
|
139
|
+
if (joined)
|
|
140
|
+
return joined;
|
|
141
|
+
}
|
|
142
|
+
if (record.content && typeof record.content === 'object') {
|
|
143
|
+
const nestedContent = extractMessageText(record.content);
|
|
144
|
+
if (nestedContent)
|
|
145
|
+
return nestedContent;
|
|
146
|
+
}
|
|
147
|
+
if (record.message && typeof record.message === 'object') {
|
|
148
|
+
const nestedMessage = extractMessageText(record.message);
|
|
149
|
+
if (nestedMessage)
|
|
150
|
+
return nestedMessage;
|
|
151
|
+
}
|
|
152
|
+
if (typeof record.type === 'string' && record.type === 'text' && typeof record.text === 'string') {
|
|
153
|
+
return record.text.trim();
|
|
154
|
+
}
|
|
155
|
+
return '';
|
|
156
|
+
}
|
|
157
|
+
function extractMessageRole(value) {
|
|
158
|
+
if (!value || typeof value !== 'object')
|
|
159
|
+
return 'unknown';
|
|
160
|
+
const directRole = value.role;
|
|
161
|
+
if (typeof directRole === 'string' && directRole.trim()) {
|
|
162
|
+
return directRole.trim();
|
|
163
|
+
}
|
|
164
|
+
const nestedMessage = value.message;
|
|
165
|
+
if (nestedMessage && typeof nestedMessage === 'object') {
|
|
166
|
+
const nestedRole = nestedMessage.role;
|
|
167
|
+
if (typeof nestedRole === 'string' && nestedRole.trim()) {
|
|
168
|
+
return nestedRole.trim();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return 'unknown';
|
|
172
|
+
}
|
|
173
|
+
function normalizeSessionHistory(messages, limit) {
|
|
174
|
+
return messages
|
|
175
|
+
.map((message) => {
|
|
176
|
+
const role = extractMessageRole(message);
|
|
177
|
+
const text = extractMessageText(message);
|
|
178
|
+
return text ? `${role}: ${text}` : '';
|
|
179
|
+
})
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.slice(-limit);
|
|
182
|
+
}
|
|
183
|
+
function extractLatestAssistantText(messages) {
|
|
184
|
+
const reversed = [...messages].reverse();
|
|
185
|
+
for (const message of reversed) {
|
|
186
|
+
const role = typeof message?.role === 'string'
|
|
187
|
+
? String(message.role).trim()
|
|
188
|
+
: '';
|
|
189
|
+
const text = extractMessageText(message);
|
|
190
|
+
if (role === 'assistant' && text)
|
|
191
|
+
return text;
|
|
192
|
+
}
|
|
193
|
+
for (const message of reversed) {
|
|
194
|
+
const text = extractMessageText(message);
|
|
195
|
+
if (text)
|
|
196
|
+
return text;
|
|
197
|
+
}
|
|
198
|
+
return '';
|
|
199
|
+
}
|
|
200
|
+
function collectBalancedJsonObjects(text) {
|
|
201
|
+
const objects = [];
|
|
202
|
+
let depth = 0;
|
|
203
|
+
let start = -1;
|
|
204
|
+
let inString = false;
|
|
205
|
+
let escaping = false;
|
|
206
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
207
|
+
const char = text[index];
|
|
208
|
+
if (escaping) {
|
|
209
|
+
escaping = false;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === '\\') {
|
|
213
|
+
escaping = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (char === '"') {
|
|
217
|
+
inString = !inString;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (inString) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (char === '{') {
|
|
224
|
+
if (depth === 0) {
|
|
225
|
+
start = index;
|
|
226
|
+
}
|
|
227
|
+
depth += 1;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (char === '}') {
|
|
231
|
+
if (depth === 0) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
depth -= 1;
|
|
235
|
+
if (depth === 0 && start !== -1) {
|
|
236
|
+
objects.push(text.slice(start, index + 1));
|
|
237
|
+
start = -1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return objects;
|
|
242
|
+
}
|
|
243
|
+
function tryExtractJsonObject(text, expectedRequestId) {
|
|
244
|
+
const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
245
|
+
const candidate = fencedMatch?.[1]?.trim() || text.trim();
|
|
246
|
+
const objects = collectBalancedJsonObjects(candidate);
|
|
247
|
+
if (objects.length === 0) {
|
|
248
|
+
throw new Error('Timeline reasoner did not return a JSON object');
|
|
249
|
+
}
|
|
250
|
+
let firstParsedObject = null;
|
|
251
|
+
for (const objectText of objects) {
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(objectText);
|
|
254
|
+
if (!firstParsedObject) {
|
|
255
|
+
firstParsedObject = objectText;
|
|
256
|
+
}
|
|
257
|
+
if (!expectedRequestId || parsed.request_id === expectedRequestId) {
|
|
258
|
+
return objectText;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (firstParsedObject) {
|
|
266
|
+
return firstParsedObject;
|
|
267
|
+
}
|
|
268
|
+
throw new Error('Timeline reasoner did not return a parseable JSON object');
|
|
269
|
+
}
|
|
270
|
+
function makePlannerRequestId() {
|
|
271
|
+
return `timeline-plan-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
|
272
|
+
}
|
|
273
|
+
function buildTimelineQueryPlannerSystemPrompt() {
|
|
274
|
+
return [
|
|
275
|
+
'你是 Timeline 插件内部的时间查询归一化器。',
|
|
276
|
+
'你的唯一任务,是把自然语言时间请求归一化为 Timeline 内部可执行的结构化时间计划。',
|
|
277
|
+
'禁止调用任何工具,禁止输出 Markdown、解释或多余文本,只输出严格 JSON。',
|
|
278
|
+
'必须遵守这些约束:',
|
|
279
|
+
'1. 你必须先判断请求属于 now、past_point、past_range 中的哪一类。',
|
|
280
|
+
'2. 不能靠关键词机械枚举,而要真正理解用户语言中的时间语义。',
|
|
281
|
+
'3. now 不输出 normalized_point / normalized_start / normalized_end。',
|
|
282
|
+
'4. past_point 必须输出 normalized_point。',
|
|
283
|
+
'5. past_range 必须输出 normalized_start 和 normalized_end。',
|
|
284
|
+
'6. 对“最近”这类口语范围,要结合 anchor.now 归一化成具体起止时间。',
|
|
285
|
+
'7. 对“昨晚”“今天”“昨天上午”这类表达,要给出符合现实习惯的合理时间范围。',
|
|
286
|
+
'8. 输出时间必须是带时区偏移的 ISO-like 时间戳。',
|
|
287
|
+
'9. 只有用户明确指向某个时刻时,才允许判为 past_point;例如“昨晚八点”“昨天上午十点”“上周六晚上九点”。',
|
|
288
|
+
'10. 只要用户问的是一个时间段或一整个时段,就必须判为 past_range;例如“昨晚在做什么”“今天都忙了什么”“最近有什么有趣的事吗”“这几天怎么样”。',
|
|
289
|
+
'11. “昨晚”本身不是时间点,而是一个晚间范围;只有“昨晚八点”这类带明确时点锚点的表达才是 past_point。',
|
|
290
|
+
].join('\n');
|
|
291
|
+
}
|
|
292
|
+
function buildTimelineQueryPlannerMessage(input, anchor, requestId) {
|
|
293
|
+
return [
|
|
294
|
+
'请只根据下面的信息做时间归一化。',
|
|
295
|
+
'输出 JSON 对象,字段必须满足下列结构:',
|
|
296
|
+
JSON.stringify({
|
|
297
|
+
schema_version: '1.0',
|
|
298
|
+
request_id: requestId,
|
|
299
|
+
target_time_range: 'now | past_point | past_range',
|
|
300
|
+
normalized_point: 'past_point 时必填,其余省略',
|
|
301
|
+
normalized_start: 'past_range 时必填,其余省略',
|
|
302
|
+
normalized_end: 'past_range 时必填,其余省略',
|
|
303
|
+
summary: '你如何理解用户时间语义的简短说明',
|
|
304
|
+
}, null, 2),
|
|
305
|
+
'',
|
|
306
|
+
'input:',
|
|
307
|
+
JSON.stringify({
|
|
308
|
+
query: input.query,
|
|
309
|
+
anchor,
|
|
310
|
+
}, null, 2),
|
|
311
|
+
].join('\n');
|
|
312
|
+
}
|
|
313
|
+
function buildTimelineReasonerSystemPrompt() {
|
|
314
|
+
return [
|
|
315
|
+
'你是 Timeline 插件内部的时间语义推理器。',
|
|
316
|
+
'你的唯一任务,是基于 collector 提供的事实包,输出一个严格符合 TimelineReasonerOutput 结构的 JSON 对象。',
|
|
317
|
+
'禁止调用任何工具,禁止引用 collector 之外的新既有事实,禁止输出 Markdown、解释或多余文本。',
|
|
318
|
+
'必须遵守这些约束:',
|
|
319
|
+
'1. 会话硬事实和已存在 canon 优先于生成。',
|
|
320
|
+
'2. 如果 collector.request.mode 是 read_only,则绝不能 generate_new_fact。',
|
|
321
|
+
'3. 如果 decision.action 是 reuse_existing_fact,selected_fact_id 必须来自 candidate_facts。',
|
|
322
|
+
'4. 如果 decision.action 是 generate_new_fact,必须给出完整 generated_fact,并且 should_write_canon=true。',
|
|
323
|
+
'5. 如果当前信息不足且不应复用或生成,才允许 return_empty。',
|
|
324
|
+
'6. continuity 字段必须如实表达是否做了延续性判断,以及判断理由。',
|
|
325
|
+
'7. request_type 只能是 now、past_point、past_range。',
|
|
326
|
+
'8. continuity 不是独立请求类型;它只是 now 或 past_point 查询中的推理结果。',
|
|
327
|
+
'9. past_point 可以通过精确命中,或通过“较早事实自然持续到目标时间点”的方式命中。',
|
|
328
|
+
'10. past_range 需要先理解已经归一化的时间范围,再从该范围内挑选最相关、最鲜活、最值得提的事实。',
|
|
329
|
+
'11. 如果用户在问“有趣”“好玩”“忙不忙”这类语义筛选词,必须先理解筛选语义,再决定复用什么事实或生成什么事实。',
|
|
330
|
+
'12. 如果为 past_point 或 past_range 生成新事实,generated_fact 应尽量提供一个合理的 timestamp,并保证它落在目标时间点或目标时间范围内,而不是默认落在当前时刻。',
|
|
331
|
+
'13. generated_fact 只输出结构化字段,不要输出自然正文、解释或额外叙述。',
|
|
332
|
+
'14. 如果 collector.persona_context.should_constrain_generation=true,则生成的新事实必须显式参考 SOUL / MEMORY / IDENTITY 中的稳定人格、语气、兴趣、生活习惯或长期约定,不能生成与这些内容冲突的生活片段。',
|
|
333
|
+
'15. 当 persona_context 中存在明确人格线索时,rationale.persona_basis 不能为空,且必须指出本次生成具体参考了哪些 persona 线索。',
|
|
334
|
+
'16. 当 persona_context 中存在明确人格线索时,rationale.constraint_basis 不能为空,且必须指出哪些长期约束限制了生成结果。',
|
|
335
|
+
'17. 不要生成通用、模板化、任何人格都能成立的空泛日常;应尽量让 location、action、emotion、appearance、internalMonologue 都体现该 persona 的生活连续性。',
|
|
336
|
+
'18. MEMORY 中的长期偏好、关系、生活节奏和与用户的长期约定,都是编织时间记忆时的重要约束;它们不是时间事实本身,但会限制什么样的生成是可信的。',
|
|
337
|
+
'19. 还必须遵守 collector.world_context 提供的现实时间逻辑:一日三餐、睡眠、工作/学习、休闲、周末、工作日、节假日的安排都应尽量符合普通现实生活节奏。',
|
|
338
|
+
'20. 如果生成的是凌晨或深夜时段,优先考虑睡眠、休息、安静活动;如果生成的是早餐/午餐/晚餐,则时间应落在合理餐段;不要生成明显违背现实作息的片段。',
|
|
339
|
+
'21. 如果 decision.action 是 generate_new_fact,generated_fact.sceneSemantics 必须完整输出,用来说明本次编织的事件属于什么活动类型、与当天已知状态是什么连续关系,以及为什么这样判断。',
|
|
340
|
+
'22. 如果 decision.action 是 generate_new_fact,generated_fact.appearanceLogic 必须完整输出,用来说明这次事件是否延续当天穿着、是否需要换装、换装原因是什么、最终服装类型属于哪一类。',
|
|
341
|
+
'23. 外貌与穿着必须依赖具体事件本身,而不是脱离事件单独生成;例如运动、洗澡、入睡、正式出门、买到并换上新衣物,都会显著影响 appearanceLogic。',
|
|
342
|
+
'24. 如果没有足够理由触发换装,优先认为当天穿着具有连续性;不要无缘无故在同一天内频繁改变外貌描述。',
|
|
343
|
+
'25. 对 now 查询,如果 collector.conversation_context.should_prefer_conversation_continuity_for_now=true,则“刚刚还在和用户继续这段对话”应被视为最高优先级的近场现实。',
|
|
344
|
+
'26. 如果当前会话仍处于粘连窗口内,优先把当前状态理解为还在和用户继续刚才的话题、思考上一轮内容或准备回应,而不是立即跳到脱离当前会话的生活片段。',
|
|
345
|
+
].join('\n');
|
|
346
|
+
}
|
|
347
|
+
function createTimelineQueryPlanner(pluginApi, toolContext, runtimeConfig) {
|
|
348
|
+
return async (input, anchor) => {
|
|
349
|
+
const subagentRuntime = pluginApi.runtime?.subagent;
|
|
350
|
+
if (!subagentRuntime?.run || !subagentRuntime.waitForRun || !subagentRuntime.getSessionMessages) {
|
|
351
|
+
throw new Error('Timeline query planner dependency missing');
|
|
352
|
+
}
|
|
353
|
+
if (!String(input.query || '').trim()) {
|
|
354
|
+
throw new Error('Timeline query planner dependency missing query');
|
|
355
|
+
}
|
|
356
|
+
const requestId = makePlannerRequestId();
|
|
357
|
+
const baseSessionKey = toolContext.sessionKey || `plugin:${runtimeConfig.reasonerSessionPrefix}`;
|
|
358
|
+
const plannerSessionKey = `${baseSessionKey}:${runtimeConfig.reasonerSessionPrefix}:planner:${requestId}`;
|
|
359
|
+
try {
|
|
360
|
+
const runResult = await subagentRuntime.run({
|
|
361
|
+
sessionKey: plannerSessionKey,
|
|
362
|
+
message: buildTimelineQueryPlannerMessage(input, anchor, requestId),
|
|
363
|
+
extraSystemPrompt: buildTimelineQueryPlannerSystemPrompt(),
|
|
364
|
+
deliver: false,
|
|
365
|
+
idempotencyKey: requestId,
|
|
366
|
+
});
|
|
367
|
+
const waitResult = await subagentRuntime.waitForRun({
|
|
368
|
+
runId: runResult.runId,
|
|
369
|
+
timeoutMs: runtimeConfig.reasonerTimeoutMs,
|
|
370
|
+
});
|
|
371
|
+
if (waitResult.status === 'timeout') {
|
|
372
|
+
throw new Error('Timeline query planner returned no decision');
|
|
373
|
+
}
|
|
374
|
+
if (waitResult.status === 'error') {
|
|
375
|
+
throw new Error(waitResult.error || 'Timeline query planner returned no decision');
|
|
376
|
+
}
|
|
377
|
+
const session = await subagentRuntime.getSessionMessages({
|
|
378
|
+
sessionKey: plannerSessionKey,
|
|
379
|
+
limit: runtimeConfig.reasonerMessageLimit,
|
|
380
|
+
});
|
|
381
|
+
const assistantText = extractLatestAssistantText(session.messages || []);
|
|
382
|
+
const jsonText = tryExtractJsonObject(assistantText, requestId);
|
|
383
|
+
const parsed = JSON.parse(jsonText);
|
|
384
|
+
if (parsed.request_id && parsed.request_id !== requestId) {
|
|
385
|
+
throw new Error('Timeline query planner returned mismatched request_id');
|
|
386
|
+
}
|
|
387
|
+
if (!['now', 'past_point', 'past_range'].includes(parsed.target_time_range)) {
|
|
388
|
+
throw new Error('Timeline query planner returned an invalid target_time_range');
|
|
389
|
+
}
|
|
390
|
+
return parsed;
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
try {
|
|
394
|
+
await subagentRuntime.deleteSession?.({
|
|
395
|
+
sessionKey: plannerSessionKey,
|
|
396
|
+
deleteTranscript: true,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
pluginApi.logger?.debug?.('timeline query planner session cleanup skipped', {
|
|
401
|
+
error: error instanceof Error ? error.message : String(error),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function buildTimelineReasonerMessage(collector) {
|
|
408
|
+
return [
|
|
409
|
+
'请只根据下面的 collector JSON 做结构化时间推理。',
|
|
410
|
+
'输出一个 JSON 对象,字段必须满足 TimelineReasonerOutput:',
|
|
411
|
+
JSON.stringify({
|
|
412
|
+
schema_version: '1.0',
|
|
413
|
+
request_id: collector.request_id,
|
|
414
|
+
request_type: 'now | past_point | past_range',
|
|
415
|
+
time_interpretation: {
|
|
416
|
+
normalized_kind: 'now | point | range',
|
|
417
|
+
normalized_point: 'optional',
|
|
418
|
+
normalized_start: 'optional',
|
|
419
|
+
normalized_end: 'optional',
|
|
420
|
+
match_strategy: 'exact_match | continuation | range_summary | generated',
|
|
421
|
+
summary: 'how you interpreted the user time semantics',
|
|
422
|
+
},
|
|
423
|
+
decision: {
|
|
424
|
+
action: 'reuse_existing_fact | generate_new_fact | return_empty',
|
|
425
|
+
selected_fact_id: 'reuse_existing_fact 时必填',
|
|
426
|
+
should_write_canon: true,
|
|
427
|
+
},
|
|
428
|
+
continuity: {
|
|
429
|
+
judged: true,
|
|
430
|
+
is_continuing: true,
|
|
431
|
+
reason: 'continuity reasoning summary',
|
|
432
|
+
},
|
|
433
|
+
conversation_context: {
|
|
434
|
+
is_recently_active: true,
|
|
435
|
+
minutes_since_last_turn: 3,
|
|
436
|
+
stickiness_window_minutes: 10,
|
|
437
|
+
active_topic_summary: 'what the conversation was just about',
|
|
438
|
+
should_prefer_conversation_continuity_for_now: true,
|
|
439
|
+
last_active_timestamp: 'optional timestamp',
|
|
440
|
+
},
|
|
441
|
+
rationale: {
|
|
442
|
+
summary: 'short summary',
|
|
443
|
+
hard_fact_basis: ['...'],
|
|
444
|
+
canon_basis: ['...'],
|
|
445
|
+
persona_basis: ['...'],
|
|
446
|
+
constraint_basis: ['...'],
|
|
447
|
+
uncertainty: 'optional',
|
|
448
|
+
},
|
|
449
|
+
generated_fact: {
|
|
450
|
+
timestamp: 'optional ISO-like timestamp when generation should land at a specific past point or past range',
|
|
451
|
+
location: 'string',
|
|
452
|
+
action: 'string',
|
|
453
|
+
emotionTags: ['string'],
|
|
454
|
+
appearance: 'string',
|
|
455
|
+
internalMonologue: 'string',
|
|
456
|
+
confidence: 0.8,
|
|
457
|
+
reason: 'string',
|
|
458
|
+
sceneSemantics: {
|
|
459
|
+
activityMode: 'sleep | bath | meal | work_or_study | commute | exercise | social | shopping | leisure | domestic | errands | transition | rest | unknown',
|
|
460
|
+
continuityRelation: 'same_day_continuation | same_scene_continuation | shifted_scene | return_home | fresh_moment | unknown',
|
|
461
|
+
rationale: 'why this generated scene fits the current timeline state',
|
|
462
|
+
},
|
|
463
|
+
appearanceLogic: {
|
|
464
|
+
transition: 'inherit | change_required | change_allowed | unknown',
|
|
465
|
+
changeReason: 'same_day_continuation | exercise | bath | sleep | formal_outing | shopping | weather_adjustment | unknown',
|
|
466
|
+
outfitMode: 'casual_home | casual_outing | workwear | sportswear | sleepwear | bathrobe | dressed_up | fresh_purchase | unknown',
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
}, null, 2),
|
|
470
|
+
'',
|
|
471
|
+
'collector:',
|
|
472
|
+
JSON.stringify(collector, null, 2),
|
|
473
|
+
].join('\n');
|
|
474
|
+
}
|
|
475
|
+
function createSubagentReasoner(pluginApi, toolContext, runtimeConfig) {
|
|
476
|
+
return async (collector) => {
|
|
477
|
+
const subagentRuntime = pluginApi.runtime?.subagent;
|
|
478
|
+
if (!subagentRuntime?.run || !subagentRuntime.waitForRun || !subagentRuntime.getSessionMessages) {
|
|
479
|
+
throw new Error('Timeline reasoner dependency missing');
|
|
480
|
+
}
|
|
481
|
+
const baseSessionKey = toolContext.sessionKey || `plugin:${runtimeConfig.reasonerSessionPrefix}`;
|
|
482
|
+
const reasonerSessionKey = `${baseSessionKey}:${runtimeConfig.reasonerSessionPrefix}:${collector.request_id}`;
|
|
483
|
+
try {
|
|
484
|
+
const runResult = await subagentRuntime.run({
|
|
485
|
+
sessionKey: reasonerSessionKey,
|
|
486
|
+
message: buildTimelineReasonerMessage(collector),
|
|
487
|
+
extraSystemPrompt: buildTimelineReasonerSystemPrompt(),
|
|
488
|
+
deliver: false,
|
|
489
|
+
idempotencyKey: collector.request_id,
|
|
490
|
+
});
|
|
491
|
+
const waitResult = await subagentRuntime.waitForRun({
|
|
492
|
+
runId: runResult.runId,
|
|
493
|
+
timeoutMs: runtimeConfig.reasonerTimeoutMs,
|
|
494
|
+
});
|
|
495
|
+
if (waitResult.status === 'timeout') {
|
|
496
|
+
throw new Error('Timeline reasoner returned no decision');
|
|
497
|
+
}
|
|
498
|
+
if (waitResult.status === 'error') {
|
|
499
|
+
throw new Error(waitResult.error || 'Timeline reasoner returned no decision');
|
|
500
|
+
}
|
|
501
|
+
const session = await subagentRuntime.getSessionMessages({
|
|
502
|
+
sessionKey: reasonerSessionKey,
|
|
503
|
+
limit: runtimeConfig.reasonerMessageLimit,
|
|
504
|
+
});
|
|
505
|
+
const assistantText = extractLatestAssistantText(session.messages || []);
|
|
506
|
+
const jsonText = tryExtractJsonObject(assistantText, collector.request_id);
|
|
507
|
+
return JSON.parse(jsonText);
|
|
508
|
+
}
|
|
509
|
+
finally {
|
|
510
|
+
try {
|
|
511
|
+
await subagentRuntime.deleteSession?.({
|
|
512
|
+
sessionKey: reasonerSessionKey,
|
|
513
|
+
deleteTranscript: true,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
pluginApi.logger?.debug?.('timeline reasoner session cleanup skipped', {
|
|
518
|
+
error: error instanceof Error ? error.message : String(error),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function createTimelineResolveDependencies(pluginApi, toolContext) {
|
|
525
|
+
const runtimeConfig = resolvePluginRuntimeConfig(pluginApi.pluginConfig);
|
|
526
|
+
const workspaceDir = resolveWorkspaceDir(pluginApi, toolContext);
|
|
527
|
+
const canonicalRootPath = resolveConfiguredPath(workspaceDir, runtimeConfig.canonicalMemoryRoot, 'memory');
|
|
528
|
+
const canonicalRootName = path.basename(canonicalRootPath);
|
|
529
|
+
const traceLogPath = runtimeConfig.enableTrace
|
|
530
|
+
? resolveConfiguredPath(workspaceDir, runtimeConfig.traceLogPath, path.join(canonicalRootName, '.timeline-trace.log'))
|
|
531
|
+
: undefined;
|
|
532
|
+
return {
|
|
533
|
+
currentTime: async () => ({
|
|
534
|
+
now: formatCurrentTimestamp(new Date()),
|
|
535
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
|
|
536
|
+
}),
|
|
537
|
+
sessionsHistory: async () => {
|
|
538
|
+
const sessionKey = toolContext.sessionKey;
|
|
539
|
+
const getSessionMessages = pluginApi.runtime?.subagent?.getSessionMessages;
|
|
540
|
+
if (!sessionKey || !getSessionMessages)
|
|
541
|
+
return [];
|
|
542
|
+
try {
|
|
543
|
+
const session = await getSessionMessages({
|
|
544
|
+
sessionKey,
|
|
545
|
+
limit: runtimeConfig.sessionHistoryLimit,
|
|
546
|
+
});
|
|
547
|
+
return normalizeSessionHistory(session.messages || [], runtimeConfig.sessionHistoryLimit);
|
|
548
|
+
}
|
|
549
|
+
catch (error) {
|
|
550
|
+
pluginApi.logger?.debug?.('timeline sessionsHistory fallback to empty', {
|
|
551
|
+
error: error instanceof Error ? error.message : String(error),
|
|
552
|
+
});
|
|
553
|
+
return [];
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
conversationContext: async (window, input) => {
|
|
557
|
+
const sessionKey = toolContext.sessionKey;
|
|
558
|
+
const getSessionMessages = pluginApi.runtime?.subagent?.getSessionMessages;
|
|
559
|
+
if (!sessionKey || !getSessionMessages) {
|
|
560
|
+
return {
|
|
561
|
+
is_recently_active: false,
|
|
562
|
+
minutes_since_last_turn: null,
|
|
563
|
+
stickiness_window_minutes: runtimeConfig.conversationStickinessWindowMinutes,
|
|
564
|
+
active_topic_summary: '',
|
|
565
|
+
should_prefer_conversation_continuity_for_now: false,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
const session = await getSessionMessages({
|
|
570
|
+
sessionKey,
|
|
571
|
+
limit: runtimeConfig.sessionHistoryLimit,
|
|
572
|
+
});
|
|
573
|
+
return (0, conversation_context_1.buildConversationContextFromMessages)(session.messages || [], window.end, input, runtimeConfig.conversationStickinessWindowMinutes, window.query_range);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
pluginApi.logger?.debug?.('timeline conversationContext fallback to inactive', {
|
|
577
|
+
error: error instanceof Error ? error.message : String(error),
|
|
578
|
+
});
|
|
579
|
+
return {
|
|
580
|
+
is_recently_active: false,
|
|
581
|
+
minutes_since_last_turn: null,
|
|
582
|
+
stickiness_window_minutes: runtimeConfig.conversationStickinessWindowMinutes,
|
|
583
|
+
active_topic_summary: '',
|
|
584
|
+
should_prefer_conversation_continuity_for_now: false,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
memoryGet: async (calendarDate) => {
|
|
589
|
+
const filePath = path.join(canonicalRootPath, `${calendarDate}.md`);
|
|
590
|
+
return readWorkspaceTextFile(filePath);
|
|
591
|
+
},
|
|
592
|
+
memorySearch: async (window, input) => {
|
|
593
|
+
const createMemorySearchTool = pluginApi.runtime?.tools?.createMemorySearchTool;
|
|
594
|
+
if (!createMemorySearchTool)
|
|
595
|
+
return [];
|
|
596
|
+
const query = buildMemorySearchQuery(input);
|
|
597
|
+
if (!query)
|
|
598
|
+
return [];
|
|
599
|
+
const tool = createMemorySearchTool({
|
|
600
|
+
config: pluginApi.config,
|
|
601
|
+
agentSessionKey: toolContext.sessionKey,
|
|
602
|
+
});
|
|
603
|
+
if (!tool)
|
|
604
|
+
return [];
|
|
605
|
+
try {
|
|
606
|
+
const result = await tool.execute(`timeline-memory-search-${Date.now()}`, {
|
|
607
|
+
query,
|
|
608
|
+
maxResults: runtimeConfig.memorySearchMaxResults,
|
|
609
|
+
});
|
|
610
|
+
const payload = result?.details;
|
|
611
|
+
if (!payload || payload.disabled || !Array.isArray(payload.results))
|
|
612
|
+
return [];
|
|
613
|
+
return payload.results.map((entry) => stringifySearchResult(entry)).filter(Boolean);
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
pluginApi.logger?.debug?.('timeline memorySearch fallback to empty', {
|
|
617
|
+
error: error instanceof Error ? error.message : String(error),
|
|
618
|
+
});
|
|
619
|
+
return [];
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
coreFiles: async () => ({
|
|
623
|
+
soul: readWorkspaceTextFile(path.join(workspaceDir, 'SOUL.md')),
|
|
624
|
+
memory: readWorkspaceTextFile(path.join(workspaceDir, 'MEMORY.md'))
|
|
625
|
+
|| readWorkspaceTextFile(path.join(workspaceDir, 'memory.md')),
|
|
626
|
+
identity: readWorkspaceTextFile(path.join(workspaceDir, 'IDENTITY.md'))
|
|
627
|
+
|| readWorkspaceTextFile(path.join(workspaceDir, 'IDENTITY')),
|
|
628
|
+
available_sources: [
|
|
629
|
+
readWorkspaceTextFile(path.join(workspaceDir, 'SOUL.md')).trim() ? 'soul' : '',
|
|
630
|
+
(readWorkspaceTextFile(path.join(workspaceDir, 'MEMORY.md'))
|
|
631
|
+
|| readWorkspaceTextFile(path.join(workspaceDir, 'memory.md'))).trim() ? 'memory' : '',
|
|
632
|
+
(readWorkspaceTextFile(path.join(workspaceDir, 'IDENTITY.md'))
|
|
633
|
+
|| readWorkspaceTextFile(path.join(workspaceDir, 'IDENTITY'))).trim() ? 'identity' : '',
|
|
634
|
+
].filter(Boolean),
|
|
635
|
+
should_constrain_generation: Boolean(readWorkspaceTextFile(path.join(workspaceDir, 'SOUL.md')).trim()
|
|
636
|
+
|| (readWorkspaceTextFile(path.join(workspaceDir, 'MEMORY.md'))
|
|
637
|
+
|| readWorkspaceTextFile(path.join(workspaceDir, 'memory.md'))).trim()
|
|
638
|
+
|| (readWorkspaceTextFile(path.join(workspaceDir, 'IDENTITY.md'))
|
|
639
|
+
|| readWorkspaceTextFile(path.join(workspaceDir, 'IDENTITY'))).trim()),
|
|
640
|
+
}),
|
|
641
|
+
memoryFilePath: (calendarDate) => path.join(canonicalRootPath, `${calendarDate}.md`),
|
|
642
|
+
canonicalRootName,
|
|
643
|
+
traceLogPath,
|
|
644
|
+
planTimelineQuery: createTimelineQueryPlanner(pluginApi, toolContext, runtimeConfig),
|
|
645
|
+
reasonTimeline: createSubagentReasoner(pluginApi, toolContext, runtimeConfig),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function makeOpenClawTimelineResolveToolFactory(pluginApi) {
|
|
649
|
+
return (toolContext) => ({
|
|
650
|
+
name: timeline_resolve_1.timelineResolveToolSpec.name,
|
|
651
|
+
description: timeline_resolve_1.timelineResolveToolSpec.description,
|
|
652
|
+
parameters: timeline_resolve_1.timelineResolveToolSpec.inputSchema,
|
|
653
|
+
execute: async (_toolCallId, params) => wrapToolPayload(await (0, timeline_resolve_1.timelineResolve)(params, createTimelineResolveDependencies(pluginApi, toolContext))),
|
|
654
|
+
});
|
|
655
|
+
}
|