kiku-agent 1.2.0 → 1.3.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.
- package/README.md +459 -407
- package/package.json +32 -3
- package/server/agent/agentCore.js +635 -267
- package/server/agent/appPaths.js +2 -0
- package/server/agent/catalogEntityResolver.js +637 -0
- package/server/agent/citations.js +0 -21
- package/server/agent/clarificationAnalytics.js +1 -1
- package/server/agent/clarifyingQuestions.js +286 -216
- package/server/agent/cli.js +532 -73
- package/server/agent/commandFeatures.js +123 -24
- package/server/agent/commandRegistry.js +128 -62
- package/server/agent/commands.js +1410 -95
- package/server/agent/config.js +253 -163
- package/server/agent/contextCompactor.js +239 -25
- package/server/agent/dynamicCatalog.js +192 -0
- package/server/agent/envBootstrap.js +46 -0
- package/server/agent/envManager.js +317 -0
- package/server/agent/eventBus.js +3 -3
- package/server/agent/feedbackLearning.js +0 -8
- package/server/agent/gpuMetrics.js +188 -55
- package/server/agent/ingestionWorker.js +339 -0
- package/server/agent/ink/App.jsx +348 -0
- package/server/agent/ink/Panel.jsx +54 -0
- package/server/agent/ink/PromptInput.jsx +321 -0
- package/server/agent/ink/TaskPlanPanel.jsx +88 -0
- package/server/agent/ink/ThinkingPanel.jsx +68 -0
- package/server/agent/ink/ToolResult.jsx +133 -0
- package/server/agent/ink/main.jsx +195 -0
- package/server/agent/ink/useEventBus.js +218 -0
- package/server/agent/lanDiscovery.js +369 -0
- package/server/agent/llmClient.js +111 -39
- package/server/agent/lmStudioWakeup.js +144 -0
- package/server/agent/mcpClient.js +2 -2
- package/server/agent/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/memory.js +635 -494
- package/server/agent/modelConfig.js +487 -0
- package/server/agent/newsIntelligence.js +219 -0
- package/server/agent/newsWatch.js +85 -0
- package/server/agent/prompts.js +146 -0
- package/server/agent/publicTools.js +7 -0
- package/server/agent/queryDetails.js +62 -1
- package/server/agent/research.js +314 -35
- package/server/agent/routingInterceptor.js +77 -0
- package/server/agent/rumoredProjects.js +15 -0
- package/server/agent/sanitizer.js +117 -17
- package/server/agent/subagent.js +52 -9
- package/server/agent/suggestionTui.js +33 -13
- package/server/agent/suggestions.js +29 -13
- package/server/agent/telemetry/hardwareTiers.js +332 -0
- package/server/agent/telemetry/runtimeWatchdog.js +66 -0
- package/server/agent/telemetry/techPowerUpClient.js +682 -0
- package/server/agent/telemetry/techpowerup-mcp.json +22 -0
- package/server/agent/telemetry.js +0 -11
- package/server/agent/terminalImage.js +0 -1
- package/server/agent/textToolProtocol.js +190 -0
- package/server/agent/tokenTracker.js +220 -0
- package/server/agent/toolRouter.js +362 -216
- package/server/agent/tools.js +1155 -194
- package/server/agent/ui.js +144 -41
- package/server/agent/vramService.js +572 -0
- package/server/agent/webSearch.js +2279 -273
- package/server/agent/websearch/adapters/duckduckgo.js +97 -0
- package/server/agent/websearch/adapters/jina.js +77 -0
- package/server/agent/websearch/adapters/searxng.js +57 -0
- package/server/agent/websearch/core/canonical.js +89 -0
- package/server/agent/websearch/core/fusion.js +81 -0
- package/server/agent/websearch/core/types.js +29 -0
- package/server/agent/websearch/extract.js +605 -0
- package/server/agent/websearch/index.js +43 -0
- package/server/agent/websearch/orchestrator.js +266 -0
- package/server/agent/websearch/resilience/breaker.js +97 -0
- package/server/agent/websearch/resilience/cache.js +59 -0
- package/server/agent/websearch/resilience/limiter.js +93 -0
- package/server/agent/websearch/security/filter.js +36 -0
- package/server/agent/websearch/security/ssrf.js +289 -0
- package/server/agent/websearch/telemetry.js +71 -0
- package/server/agent/youtubeAuth.js +82 -73
- package/server/agent/youtubeTranscript.js +209 -0
- package/server/apiRouter.js +390 -190
- package/server/mcp/data/brands.js +5 -5
- package/server/mcp/data/discography.js +56 -45
- package/server/mcp/data/gallery.js +9 -0
- package/server/mcp/data/index.js +1 -0
- package/server/mcp/data/industryIndex.js +251 -0
- package/server/mcp/data/profile.js +5 -5
- package/server/mcp/data/timeline.js +2 -2
- package/server/mcp/data/works.js +2 -2
- package/server/mcp/handler.js +0 -2
- package/server/mcp/prompts.js +42 -2
- package/server/mcp/resources.js +5 -1
- package/server/mcp/security.js +25 -14
- package/server/mcp/server.js +92 -3
- package/server/mcp/tools.js +20 -11
- package/server/routes/agent.js +55 -0
- package/server/web-chat/core/connection.js +218 -194
- package/server/web-chat/core/conversationContext.js +1 -1
- package/server/web-chat/core/directCatalogEngine.js +190 -184
- package/server/web-chat/core/followUpEngine.js +88 -11
- package/server/web-chat/core/security.js +7 -4
- package/server/web-chat/router.js +54 -17
- package/server/web-chat/routes/bilibiliRoute.js +34 -0
- package/server/web-chat/routes/chatRoute.js +488 -397
- package/server/web-chat/routes/clarifyConfigRoute.js +1 -2
- package/server/web-chat/routes/exportRoute.js +2 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +90 -0
- package/server/web-chat/routes/newsRoute.js +36 -6
- package/server/web-chat/routes/providerRoute.js +87 -0
- package/server/web-chat/routes/staticRoute.js +61 -13
- package/server/web-chat/routes/statusRoute.js +23 -2
- package/server/web-chat/routes/vramRoute.js +22 -0
- package/server/web-chat/server.js +135 -106
- package/server/web-chat/ui/htmlBuilder.js +8 -8
- package/server/web-chat/ui/previews/modelSettings.preview.html +41 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +0 -5
- package/server/web-chat/ui/scripts/client.js +10526 -8397
- package/server/web-chat/ui/scripts/webchatI18n.js +190 -138
- package/server/web-chat/ui/styles/main.css +7471 -4246
- package/server/web-chat/ui/templates/body.html +305 -179
- package/src/data/brandsData.js +29 -6
- package/src/data/discographyData.js +918 -900
- package/src/data/galleryData.js +66 -0
- package/src/data/industryIndexData.js +292 -0
- package/src/data/musicVideoData.js +48 -5
- package/src/data/profileData.js +15 -1
- package/src/data/timelineData.js +4 -3
- package/src/data/worksData.js +536 -535
- package/src/lib/vram/estimator.js +464 -0
- package/src/lib/vram/modelRegistry.js +387 -0
- package/tokens.css +33 -2
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { HIERARCHICAL_MEMORY_DIR, ARTIFACTS_STORE_DIR } from '../appPaths.js';
|
|
5
|
+
|
|
6
|
+
export const POINTER_THRESHOLD_CHARS = 1000;
|
|
7
|
+
|
|
8
|
+
export const VALID_RUN_ID_REGEX = /^[a-zA-Z0-9_-]{3,64}$/;
|
|
9
|
+
export const VALID_DOMAIN_REGEX = /^[a-zA-Z0-9_-]{2,64}$/;
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
export function parseAndValidatePath(rawPath) {
|
|
13
|
+
if (typeof rawPath !== 'string') {
|
|
14
|
+
throw new Error('Memory path must be a string.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const normalized = rawPath.trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
18
|
+
if (normalized.includes('..') || /[\0\r\n]/.test(normalized)) {
|
|
19
|
+
throw new Error(`Invalid path traversal or characters detected: "${rawPath}"`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parts = normalized.split('/');
|
|
23
|
+
if (parts.length < 2 || parts[0] !== 'kiku') {
|
|
24
|
+
throw new Error(`Path must start with "kiku/". Received: "${rawPath}"`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const section = parts[1];
|
|
28
|
+
if (section === 'runs') {
|
|
29
|
+
if (parts.length !== 4) {
|
|
30
|
+
throw new Error(`Run paths must follow "kiku/runs/{run_id}/{subpath}". Received: "${rawPath}"`);
|
|
31
|
+
}
|
|
32
|
+
const runId = parts[2];
|
|
33
|
+
const subpath = parts[3];
|
|
34
|
+
if (!VALID_RUN_ID_REGEX.test(runId)) {
|
|
35
|
+
throw new Error(`Invalid run_id "${runId}". Must match ${VALID_RUN_ID_REGEX}`);
|
|
36
|
+
}
|
|
37
|
+
if (!['state', 'plan', 'artifacts'].includes(subpath)) {
|
|
38
|
+
throw new Error(`Invalid run subpath "${subpath}". Allowed: state, plan, artifacts`);
|
|
39
|
+
}
|
|
40
|
+
return { section: 'runs', runId, subpath, path: normalized, type: 'context' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (section === 'reflections') {
|
|
44
|
+
if (parts.length !== 3) {
|
|
45
|
+
throw new Error(`Reflection paths must follow "kiku/reflections/{tool_or_domain}". Received: "${rawPath}"`);
|
|
46
|
+
}
|
|
47
|
+
const domain = parts[2];
|
|
48
|
+
if (!VALID_DOMAIN_REGEX.test(domain)) {
|
|
49
|
+
throw new Error(`Invalid reflection domain/tool "${domain}". Must match ${VALID_DOMAIN_REGEX}`);
|
|
50
|
+
}
|
|
51
|
+
return { section: 'reflections', domain, path: normalized, type: 'context' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (section === 'guidelines') {
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
throw new Error(`Guideline paths must follow "kiku/guidelines/{domain}". Received: "${rawPath}"`);
|
|
57
|
+
}
|
|
58
|
+
const domain = parts[2];
|
|
59
|
+
if (!VALID_DOMAIN_REGEX.test(domain)) {
|
|
60
|
+
throw new Error(`Invalid guideline domain "${domain}". Must match ${VALID_DOMAIN_REGEX}`);
|
|
61
|
+
}
|
|
62
|
+
return { section: 'guidelines', domain, path: normalized, type: 'user' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (section === 'archive') {
|
|
66
|
+
if (parts.length !== 3) {
|
|
67
|
+
throw new Error(`Archive paths must follow "kiku/archive/{run_id}". Received: "${rawPath}"`);
|
|
68
|
+
}
|
|
69
|
+
const runId = parts[2];
|
|
70
|
+
if (!VALID_RUN_ID_REGEX.test(runId)) {
|
|
71
|
+
throw new Error(`Invalid archive run_id "${runId}". Must match ${VALID_RUN_ID_REGEX}`);
|
|
72
|
+
}
|
|
73
|
+
return { section: 'archive', runId, path: normalized, type: 'archive' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
throw new Error(`Unrecognized memory section "${section}" in path "${rawPath}". Allowed sections: runs, reflections, guidelines, archive.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function computeContentHash(content) {
|
|
80
|
+
const json = JSON.stringify(content, (k, v) => {
|
|
81
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
82
|
+
const sorted = {};
|
|
83
|
+
for (const key of Object.keys(v).sort()) {
|
|
84
|
+
sorted[key] = v[key];
|
|
85
|
+
}
|
|
86
|
+
return sorted;
|
|
87
|
+
}
|
|
88
|
+
return v;
|
|
89
|
+
});
|
|
90
|
+
return crypto.createHash('sha256').update(json || '').digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class HierarchicalStore {
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.persistenceEnabled = options.persistence !== false;
|
|
96
|
+
this.baseDir = options.baseDir || HIERARCHICAL_MEMORY_DIR;
|
|
97
|
+
this.artifactsDir = options.artifactsDir || ARTIFACTS_STORE_DIR;
|
|
98
|
+
this.nodes = new Map();
|
|
99
|
+
this._inMemoryArtifacts = new Map();
|
|
100
|
+
this._pointerCache = new Map();
|
|
101
|
+
this._indexDirty = false;
|
|
102
|
+
this._indexFlushTimer = null;
|
|
103
|
+
this._indexFlushChain = Promise.resolve();
|
|
104
|
+
this._initialized = false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async init() {
|
|
108
|
+
if (this._initialized) return true;
|
|
109
|
+
if (this.persistenceEnabled) {
|
|
110
|
+
try {
|
|
111
|
+
await fs.mkdir(this.baseDir, { recursive: true, mode: 0o700 });
|
|
112
|
+
await fs.mkdir(this.artifactsDir, { recursive: true, mode: 0o700 });
|
|
113
|
+
await this._loadAllFromDisk();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this._initialized = true;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async _loadAllFromDisk() {
|
|
123
|
+
try {
|
|
124
|
+
const indexFile = path.join(this.baseDir, 'index.json');
|
|
125
|
+
const raw = await fs.readFile(indexFile, 'utf-8');
|
|
126
|
+
const items = JSON.parse(raw);
|
|
127
|
+
if (Array.isArray(items)) {
|
|
128
|
+
for (const item of items) {
|
|
129
|
+
if (item?.path) {
|
|
130
|
+
this.nodes.set(item.path, item);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async _saveIndexToDisk() {
|
|
140
|
+
if (!this.persistenceEnabled) return;
|
|
141
|
+
try {
|
|
142
|
+
const indexFile = path.join(this.baseDir, 'index.json');
|
|
143
|
+
const tmpFile = `${indexFile}.${Date.now()}.${Math.random().toString(36).slice(2, 6)}.tmp`;
|
|
144
|
+
const items = Array.from(this.nodes.values());
|
|
145
|
+
await fs.writeFile(tmpFile, JSON.stringify(items), { encoding: 'utf-8', mode: 0o600 });
|
|
146
|
+
await fs.rename(tmpFile, indexFile);
|
|
147
|
+
} catch {
|
|
148
|
+
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_markIndexDirty() {
|
|
153
|
+
if (!this.persistenceEnabled) return;
|
|
154
|
+
this._indexDirty = true;
|
|
155
|
+
if (this._indexFlushTimer) return;
|
|
156
|
+
this._indexFlushTimer = setTimeout(() => {
|
|
157
|
+
this._indexFlushTimer = null;
|
|
158
|
+
this.flushIndex().catch(() => {});
|
|
159
|
+
}, 150);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async flushIndex() {
|
|
163
|
+
if (this._indexFlushTimer) {
|
|
164
|
+
clearTimeout(this._indexFlushTimer);
|
|
165
|
+
this._indexFlushTimer = null;
|
|
166
|
+
}
|
|
167
|
+
if (!this._indexDirty) return true;
|
|
168
|
+
const next = this._indexFlushChain.then(() => {
|
|
169
|
+
this._indexDirty = false;
|
|
170
|
+
return this._saveIndexToDisk();
|
|
171
|
+
});
|
|
172
|
+
this._indexFlushChain = next.catch(() => {});
|
|
173
|
+
await next;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async _applyPointerOverData(data) {
|
|
179
|
+
if (data === null || data === undefined) return data;
|
|
180
|
+
|
|
181
|
+
let serialized;
|
|
182
|
+
let isString = typeof data === 'string';
|
|
183
|
+
|
|
184
|
+
if (isString) {
|
|
185
|
+
serialized = data;
|
|
186
|
+
} else if (typeof data === 'object') {
|
|
187
|
+
serialized = JSON.stringify(data);
|
|
188
|
+
} else {
|
|
189
|
+
return data;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (serialized.length <= POINTER_THRESHOLD_CHARS) {
|
|
193
|
+
return data;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
const hash = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
198
|
+
const filename = `${hash}.json`;
|
|
199
|
+
const artifactPath = path.join(this.artifactsDir, filename);
|
|
200
|
+
|
|
201
|
+
if (this.persistenceEnabled) {
|
|
202
|
+
try {
|
|
203
|
+
await fs.mkdir(this.artifactsDir, { recursive: true, mode: 0o700 });
|
|
204
|
+
await fs.writeFile(artifactPath, serialized, { encoding: 'utf-8', mode: 0o600 });
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.error(`[HierarchicalStore] Failed to write pointer artifact: ${err.message}`);
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
this._inMemoryArtifacts.set(hash, serialized);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const preview = isString
|
|
213
|
+
? serialized.slice(0, 150) + '...'
|
|
214
|
+
: (typeof data === 'object' ? Object.keys(data).join(', ').slice(0, 120) : '');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
$type: 'pointer',
|
|
218
|
+
uri: `file://${artifactPath.replace(/\\/g, '/')}`,
|
|
219
|
+
sizeBytes: Buffer.byteLength(serialized, 'utf-8'),
|
|
220
|
+
charCount: serialized.length,
|
|
221
|
+
sha256: hash,
|
|
222
|
+
preview,
|
|
223
|
+
isString,
|
|
224
|
+
createdAt: new Date().toISOString()
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async resolvePointer(pointer) {
|
|
230
|
+
if (!pointer || pointer.$type !== 'pointer' || !pointer.uri) {
|
|
231
|
+
return pointer;
|
|
232
|
+
}
|
|
233
|
+
if (!this.persistenceEnabled && pointer.sha256 && this._inMemoryArtifacts.has(pointer.sha256)) {
|
|
234
|
+
const raw = this._inMemoryArtifacts.get(pointer.sha256);
|
|
235
|
+
return pointer.isString ? raw : JSON.parse(raw);
|
|
236
|
+
}
|
|
237
|
+
let rawPath = String(pointer.uri).replace(/^file:\/\//, '');
|
|
238
|
+
if (process.platform === 'win32' && /^\/[a-zA-Z]:/.test(rawPath)) {
|
|
239
|
+
rawPath = rawPath.slice(1);
|
|
240
|
+
}
|
|
241
|
+
const resolvedPath = path.resolve(rawPath);
|
|
242
|
+
const allowedDir = path.resolve(this.artifactsDir);
|
|
243
|
+
const allowedBase = path.resolve(this.baseDir);
|
|
244
|
+
const inArtifacts = resolvedPath === allowedDir || resolvedPath.startsWith(allowedDir + path.sep);
|
|
245
|
+
const inBase = resolvedPath === allowedBase || resolvedPath.startsWith(allowedBase + path.sep);
|
|
246
|
+
if (!inArtifacts && !inBase) {
|
|
247
|
+
return { error: 'Failed to resolve pointer artifact: Access denied (path outside storage directory)' };
|
|
248
|
+
}
|
|
249
|
+
const cacheKey = pointer.sha256 || null;
|
|
250
|
+
if (cacheKey) {
|
|
251
|
+
const cached = this._pointerCache.get(cacheKey);
|
|
252
|
+
if (cached) {
|
|
253
|
+
if (cached.expires > Date.now()) {
|
|
254
|
+
return pointer.isString ? cached.parsed : structuredClone(cached.parsed);
|
|
255
|
+
}
|
|
256
|
+
this._pointerCache.delete(cacheKey);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const raw = await fs.readFile(resolvedPath, 'utf-8');
|
|
261
|
+
const parsed = pointer.isString ? raw : JSON.parse(raw);
|
|
262
|
+
if (cacheKey) {
|
|
263
|
+
this._pointerCache.set(cacheKey, { expires: Date.now() + 60000, parsed });
|
|
264
|
+
if (this._pointerCache.size > 200) {
|
|
265
|
+
this._pointerCache.delete(this._pointerCache.keys().next().value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return pointer.isString ? parsed : structuredClone(parsed);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return { error: `Failed to resolve pointer artifact: ${err.message}` };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
_assertRunScope(parsed, scopedRunId) {
|
|
276
|
+
if (scopedRunId && parsed.section === 'runs') {
|
|
277
|
+
if (parsed.runId !== scopedRunId) {
|
|
278
|
+
throw new Error(`Run scoping violation: Active run "${scopedRunId}" cannot mutate run "${parsed.runId}"`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async setNode(rawPath, content, options = {}) {
|
|
285
|
+
await this.init();
|
|
286
|
+
const parsed = parseAndValidatePath(rawPath);
|
|
287
|
+
this._assertRunScope(parsed, options.scopedRunId);
|
|
288
|
+
|
|
289
|
+
const processedContent = await this._applyPointerOverData(content);
|
|
290
|
+
const contentHash = computeContentHash(processedContent);
|
|
291
|
+
|
|
292
|
+
const existing = this.nodes.get(parsed.path);
|
|
293
|
+
if (existing && existing.contentHash === contentHash && !options.force) {
|
|
294
|
+
return { status: 'unchanged', path: parsed.path, contentHash, node: existing };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const now = new Date().toISOString();
|
|
298
|
+
const node = {
|
|
299
|
+
path: parsed.path,
|
|
300
|
+
section: parsed.section,
|
|
301
|
+
type: options.type || parsed.type,
|
|
302
|
+
runId: parsed.runId || null,
|
|
303
|
+
domain: parsed.domain || null,
|
|
304
|
+
content: processedContent,
|
|
305
|
+
contentHash,
|
|
306
|
+
createdAt: existing ? existing.createdAt : now,
|
|
307
|
+
updatedAt: now,
|
|
308
|
+
metadata: options.metadata || (existing ? existing.metadata : {})
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
this.nodes.set(parsed.path, node);
|
|
312
|
+
this._markIndexDirty();
|
|
313
|
+
return { status: 'saved', path: parsed.path, contentHash, node };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
async replaceNode(rawPath, content, options = {}) {
|
|
318
|
+
return this.setNode(rawPath, content, { ...options, replace: true });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async getNode(rawPath) {
|
|
323
|
+
await this.init();
|
|
324
|
+
const parsed = parseAndValidatePath(rawPath);
|
|
325
|
+
return this.nodes.get(parsed.path) || null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
async deleteNode(rawPath, options = {}) {
|
|
330
|
+
await this.init();
|
|
331
|
+
const parsed = parseAndValidatePath(rawPath);
|
|
332
|
+
this._assertRunScope(parsed, options.scopedRunId);
|
|
333
|
+
|
|
334
|
+
const existed = this.nodes.delete(parsed.path);
|
|
335
|
+
if (existed) {
|
|
336
|
+
this._markIndexDirty();
|
|
337
|
+
}
|
|
338
|
+
return existed;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
async listNodes(prefix = '') {
|
|
343
|
+
await this.init();
|
|
344
|
+
const normPrefix = String(prefix || '').trim().replace(/\\/g, '/');
|
|
345
|
+
const matched = [];
|
|
346
|
+
for (const [key, node] of this.nodes.entries()) {
|
|
347
|
+
if (!normPrefix || key.startsWith(normPrefix)) {
|
|
348
|
+
matched.push(node);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return matched;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
async checkpointStep({ runId, stepName, status, summary, metadata = {} }) {
|
|
356
|
+
if (!runId || !VALID_RUN_ID_REGEX.test(runId)) {
|
|
357
|
+
throw new Error(`Valid runId is required for checkpointing. Received: "${runId}"`);
|
|
358
|
+
}
|
|
359
|
+
if (!stepName) {
|
|
360
|
+
throw new Error('stepName is required for checkpointing.');
|
|
361
|
+
}
|
|
362
|
+
const cleanStatus = ['pending', 'in_progress', 'completed', 'failed', 'paused', 'waiting_user'].includes(status)
|
|
363
|
+
? status
|
|
364
|
+
: 'in_progress';
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
const statePath = `kiku/runs/${runId}/state`;
|
|
368
|
+
const currentStateNode = await this.getNode(statePath);
|
|
369
|
+
const previousState = currentStateNode?.content || {
|
|
370
|
+
runId,
|
|
371
|
+
status: 'IN_PROGRESS',
|
|
372
|
+
currentStep: null,
|
|
373
|
+
metadata: {}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const newState = {
|
|
377
|
+
runId,
|
|
378
|
+
status: cleanStatus === 'failed' ? 'FAILED' : (cleanStatus === 'completed' ? 'IN_PROGRESS' : cleanStatus.toUpperCase()),
|
|
379
|
+
currentStep: stepName,
|
|
380
|
+
lastSummary: summary || '',
|
|
381
|
+
updatedAt: new Date().toISOString(),
|
|
382
|
+
metadata: { ...(previousState.metadata || {}), ...metadata }
|
|
383
|
+
};
|
|
384
|
+
await this.setNode(statePath, newState, { scopedRunId: runId });
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
const planPath = `kiku/runs/${runId}/plan`;
|
|
388
|
+
const currentPlanNode = await this.getNode(planPath);
|
|
389
|
+
const plan = currentPlanNode?.content || {
|
|
390
|
+
runId,
|
|
391
|
+
subtasks: []
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (!Array.isArray(plan.subtasks)) plan.subtasks = [];
|
|
395
|
+
const existingTask = plan.subtasks.find(t => t.name === stepName);
|
|
396
|
+
if (existingTask) {
|
|
397
|
+
existingTask.status = cleanStatus;
|
|
398
|
+
existingTask.summary = summary || existingTask.summary;
|
|
399
|
+
existingTask.updatedAt = new Date().toISOString();
|
|
400
|
+
} else {
|
|
401
|
+
plan.subtasks.push({
|
|
402
|
+
id: `step_${plan.subtasks.length + 1}`,
|
|
403
|
+
name: stepName,
|
|
404
|
+
status: cleanStatus,
|
|
405
|
+
summary: summary || '',
|
|
406
|
+
createdAt: new Date().toISOString(),
|
|
407
|
+
updatedAt: new Date().toISOString()
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await this.setNode(planPath, plan, { scopedRunId: runId });
|
|
412
|
+
await this.flushIndex();
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
runId,
|
|
416
|
+
currentStep: stepName,
|
|
417
|
+
status: cleanStatus,
|
|
418
|
+
summary,
|
|
419
|
+
totalSubtasks: plan.subtasks.length
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
async recordFailure({ runId, errorTrace, recoveryAction, toolName = null }) {
|
|
425
|
+
if (!runId || !VALID_RUN_ID_REGEX.test(runId)) {
|
|
426
|
+
throw new Error(`Valid runId is required. Received: "${runId}"`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const statePath = `kiku/runs/${runId}/state`;
|
|
430
|
+
const currentStateNode = await this.getNode(statePath);
|
|
431
|
+
const currentState = currentStateNode?.content || { runId, failures: [] };
|
|
432
|
+
const failures = Array.isArray(currentState.failures) ? currentState.failures : [];
|
|
433
|
+
|
|
434
|
+
const failureRecord = {
|
|
435
|
+
id: `fail_${Date.now()}`,
|
|
436
|
+
toolName: toolName || 'unknown',
|
|
437
|
+
errorTrace: String(errorTrace || '').slice(0, 1500),
|
|
438
|
+
recoveryAction: String(recoveryAction || '').slice(0, 1000),
|
|
439
|
+
recordedAt: new Date().toISOString()
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
failures.push(failureRecord);
|
|
443
|
+
currentState.failures = failures;
|
|
444
|
+
currentState.lastFailure = failureRecord;
|
|
445
|
+
|
|
446
|
+
await this.setNode(statePath, currentState, { scopedRunId: runId });
|
|
447
|
+
return { status: 'recorded', runId, failure: failureRecord };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
async loadActiveRun(runId) {
|
|
452
|
+
if (!runId || !VALID_RUN_ID_REGEX.test(runId)) {
|
|
453
|
+
throw new Error(`Valid runId is required. Received: "${runId}"`);
|
|
454
|
+
}
|
|
455
|
+
const [stateNode, planNode, artifactsNode] = await Promise.all([
|
|
456
|
+
this.getNode(`kiku/runs/${runId}/state`),
|
|
457
|
+
this.getNode(`kiku/runs/${runId}/plan`),
|
|
458
|
+
this.getNode(`kiku/runs/${runId}/artifacts`)
|
|
459
|
+
]);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
runId,
|
|
463
|
+
state: stateNode?.content || null,
|
|
464
|
+
plan: planNode?.content || null,
|
|
465
|
+
artifacts: artifactsNode?.content || null,
|
|
466
|
+
exists: Boolean(stateNode || planNode)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
async archiveRun(runId, ttlDays = 7) {
|
|
472
|
+
if (!runId || !VALID_RUN_ID_REGEX.test(runId)) {
|
|
473
|
+
throw new Error(`Valid runId is required. Received: "${runId}"`);
|
|
474
|
+
}
|
|
475
|
+
const runData = await this.loadActiveRun(runId);
|
|
476
|
+
if (!runData.exists) {
|
|
477
|
+
return { status: 'not_found', runId };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
const expiresAt = new Date(now + ttlDays * 24 * 60 * 60 * 1000).toISOString();
|
|
482
|
+
|
|
483
|
+
const archiveContent = {
|
|
484
|
+
runId,
|
|
485
|
+
archivedAt: new Date(now).toISOString(),
|
|
486
|
+
expiresAt,
|
|
487
|
+
ttlDays,
|
|
488
|
+
state: runData.state,
|
|
489
|
+
plan: runData.plan,
|
|
490
|
+
artifacts: runData.artifacts
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const archivePath = `kiku/archive/${runId}`;
|
|
494
|
+
await this.setNode(archivePath, archiveContent, { type: 'archive' });
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
await Promise.all([
|
|
498
|
+
this.deleteNode(`kiku/runs/${runId}/state`),
|
|
499
|
+
this.deleteNode(`kiku/runs/${runId}/plan`),
|
|
500
|
+
this.deleteNode(`kiku/runs/${runId}/artifacts`)
|
|
501
|
+
]);
|
|
502
|
+
await this.flushIndex();
|
|
503
|
+
|
|
504
|
+
return { status: 'archived', runId, archivePath, expiresAt };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
async purgeExpiredArchives() {
|
|
509
|
+
await this.init();
|
|
510
|
+
const archives = await this.listNodes('kiku/archive/');
|
|
511
|
+
const now = Date.now();
|
|
512
|
+
let purgedCount = 0;
|
|
513
|
+
|
|
514
|
+
for (const arch of archives) {
|
|
515
|
+
const expiresAt = arch.content?.expiresAt;
|
|
516
|
+
if (expiresAt && new Date(expiresAt).getTime() < now) {
|
|
517
|
+
await this.deleteNode(arch.path);
|
|
518
|
+
purgedCount++;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { purgedCount, remaining: archives.length - purgedCount };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export class ReflectionWorker {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.store = options.store;
|
|
4
|
+
this.memory = options.memory;
|
|
5
|
+
this.defaultTtlDays = options.ttlDays || 7;
|
|
6
|
+
this._isProcessing = false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async processPostRunCompaction(runId, finalStatus = 'COMPLETED', metadata = {}) {
|
|
11
|
+
if (!this.store) {
|
|
12
|
+
return { success: false, error: 'HierarchicalStore instance required' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const runData = await this.store.loadActiveRun(runId);
|
|
17
|
+
if (!runData.exists) {
|
|
18
|
+
return { success: false, error: `Run "${runId}" does not exist in active memory.` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const state = runData.state || {};
|
|
22
|
+
const plan = runData.plan || {};
|
|
23
|
+
const subtasks = Array.isArray(plan.subtasks) ? plan.subtasks : [];
|
|
24
|
+
const failures = Array.isArray(state.failures) ? state.failures : [];
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
const completedCount = subtasks.filter(t => t.status === 'completed').length;
|
|
28
|
+
const failedCount = subtasks.filter(t => t.status === 'failed').length;
|
|
29
|
+
const stepSummaries = subtasks
|
|
30
|
+
.filter(t => t.summary)
|
|
31
|
+
.map(t => `- Step "${t.name}" [${t.status}]: ${t.summary}`)
|
|
32
|
+
.join('\n');
|
|
33
|
+
|
|
34
|
+
const executiveSummary = {
|
|
35
|
+
runId,
|
|
36
|
+
finalStatus,
|
|
37
|
+
totalSteps: subtasks.length,
|
|
38
|
+
completedSteps: completedCount,
|
|
39
|
+
failedSteps: failedCount,
|
|
40
|
+
stepHighlights: stepSummaries || (state.lastSummary ? `- ${state.lastSummary}` : 'No step summaries recorded.'),
|
|
41
|
+
compiledAt: new Date().toISOString(),
|
|
42
|
+
metadata: { ...(state.metadata || {}), ...metadata }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
let reflectionsCount = 0;
|
|
47
|
+
for (const fail of failures) {
|
|
48
|
+
const domain = (fail.toolName && fail.toolName !== 'unknown')
|
|
49
|
+
? fail.toolName.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase()
|
|
50
|
+
: 'general_execution';
|
|
51
|
+
|
|
52
|
+
const reflectionPath = `kiku/reflections/${domain}`;
|
|
53
|
+
const existingNode = await this.store.getNode(reflectionPath);
|
|
54
|
+
const existing = existingNode?.content || {
|
|
55
|
+
domain,
|
|
56
|
+
occurrences: 0,
|
|
57
|
+
patterns: [],
|
|
58
|
+
lessons: []
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const patterns = Array.isArray(existing.patterns) ? existing.patterns : [];
|
|
62
|
+
const isDuplicate = patterns.some(p => p.errorTrace === fail.errorTrace);
|
|
63
|
+
|
|
64
|
+
if (!isDuplicate) {
|
|
65
|
+
patterns.push({
|
|
66
|
+
errorTrace: fail.errorTrace,
|
|
67
|
+
recoveryAction: fail.recoveryAction,
|
|
68
|
+
recordedAt: fail.recordedAt || new Date().toISOString(),
|
|
69
|
+
runId
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const updatedReflection = {
|
|
74
|
+
domain,
|
|
75
|
+
issuePattern: fail.errorTrace,
|
|
76
|
+
recoveryAction: fail.recoveryAction,
|
|
77
|
+
occurrences: (existing.occurrences || 0) + 1,
|
|
78
|
+
confidence: Math.min(1.5, 0.8 + (existing.occurrences || 0) * 0.1),
|
|
79
|
+
patterns: patterns.slice(-10),
|
|
80
|
+
lastUpdated: new Date().toISOString()
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await this.store.setNode(reflectionPath, updatedReflection, { type: 'context' });
|
|
84
|
+
reflectionsCount++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
state.executiveSummary = executiveSummary;
|
|
89
|
+
state.status = finalStatus;
|
|
90
|
+
await this.store.setNode(`kiku/runs/${runId}/state`, state, { scopedRunId: runId });
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
const archiveResult = await this.store.archiveRun(runId, this.defaultTtlDays);
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
this.store.purgeExpiredArchives().catch(() => {});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
success: true,
|
|
100
|
+
runId,
|
|
101
|
+
finalStatus,
|
|
102
|
+
executiveSummary,
|
|
103
|
+
reflectionsExtracted: reflectionsCount,
|
|
104
|
+
archive: archiveResult
|
|
105
|
+
};
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return { success: false, error: err.message };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|