kiku-agent 1.3.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 +7 -7
- package/package.json +6 -1
- package/server/agent/agentCore.js +2 -2
- package/server/agent/catalogEntityResolver.js +8 -0
- package/server/agent/cli.js +4 -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/memory/contextAllocator.js +214 -0
- package/server/agent/memory/hierarchicalStore.js +523 -0
- package/server/agent/memory/reflectionWorker.js +110 -0
- package/server/agent/prompts.js +1 -1
- 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/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/apiRouter.js +7 -11
- package/server/routes/agent.js +55 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import '../youtubeAuth.js';
|
|
4
|
+
import { App } from './App.jsx';
|
|
5
|
+
import { KikuAgent } from '../agentCore.js';
|
|
6
|
+
import { LMStudioClient } from '../llmClient.js';
|
|
7
|
+
import { toolsSchema } from '../tools.js';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { COMMAND_DEFINITIONS } from '../commandRegistry.js';
|
|
10
|
+
import { C, UI_ICONS } from '../ui.js';
|
|
11
|
+
import { existsSync } from 'node:fs';
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
14
|
+
|
|
15
|
+
function parseCliArgs() {
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const options = {
|
|
18
|
+
query: null,
|
|
19
|
+
model: 'google/gemma-4-e4b',
|
|
20
|
+
url: null,
|
|
21
|
+
showTools: false,
|
|
22
|
+
showStats: false,
|
|
23
|
+
thinking: 'show',
|
|
24
|
+
verbose: false,
|
|
25
|
+
help: false,
|
|
26
|
+
version: false
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === '--help' || arg === '-h') options.help = true;
|
|
32
|
+
else if (arg === '--version' || arg === '-V') options.version = true;
|
|
33
|
+
else if (arg === '--tools' || arg === '-t') options.showTools = true;
|
|
34
|
+
else if (arg === '--stats' || arg === '-s') options.showStats = true;
|
|
35
|
+
else if (arg === '--verbose' || arg === '-v') { options.verbose = true; options.thinking = 'show'; }
|
|
36
|
+
else if (arg === '--thinking') options.thinking = args[++i] || 'show';
|
|
37
|
+
else if (arg === '--model' || arg === '-m') options.model = args[++i] || options.model;
|
|
38
|
+
else if (arg === '--url' || arg === '-u') options.url = args[++i];
|
|
39
|
+
else if (arg === '--query' || arg === '-q') options.query = args[++i];
|
|
40
|
+
else if (!arg.startsWith('-') && !options.query) { options.query = args.slice(i).join(' '); break; }
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function waitForLMStudio(baseUrl, timeoutMs = 30000) {
|
|
46
|
+
const client = new LMStudioClient({ baseUrl });
|
|
47
|
+
const start = Date.now();
|
|
48
|
+
while (Date.now() - start < timeoutMs) {
|
|
49
|
+
const health = await client.checkConnection();
|
|
50
|
+
if (health.connected) return health;
|
|
51
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function attemptAutoStartLMStudio(baseUrl, model) {
|
|
57
|
+
const isWin = process.platform === 'win32';
|
|
58
|
+
const isMac = process.platform === 'darwin';
|
|
59
|
+
|
|
60
|
+
const candidates = isWin
|
|
61
|
+
? [
|
|
62
|
+
`${process.env.LOCALAPPDATA}\\LM Studio\\LM Studio.exe`,
|
|
63
|
+
`${process.env.LOCALAPPDATA}\\Programs\\LM Studio\\LM Studio.exe`,
|
|
64
|
+
'C:\\Program Files\\LM Studio\\LM Studio.exe'
|
|
65
|
+
]
|
|
66
|
+
: isMac
|
|
67
|
+
? ['/Applications/LM Studio.app/Contents/MacOS/LM Studio']
|
|
68
|
+
: ['lmstudio', '/usr/local/bin/lmstudio', '/opt/lmstudio/bin/lmstudio'];
|
|
69
|
+
|
|
70
|
+
const exe = candidates.find(p => {
|
|
71
|
+
if (p.includes('/') || p.includes('\\')) {
|
|
72
|
+
try { return existsSync(p); } catch { return false; }
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!exe) return null;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
let port = '1234';
|
|
81
|
+
try { const parsedUrl = new URL(baseUrl); if (parsedUrl.port) port = parsedUrl.port; } catch {}
|
|
82
|
+
|
|
83
|
+
const child = spawn(exe, isWin
|
|
84
|
+
? ['--model', model, '--serve', '--port', port]
|
|
85
|
+
: ['serve', '--model', model, '--port', port],
|
|
86
|
+
{ detached: true, stdio: 'ignore', windowsHide: true }
|
|
87
|
+
);
|
|
88
|
+
child.unref();
|
|
89
|
+
|
|
90
|
+
return await waitForLMStudio(baseUrl, 45000);
|
|
91
|
+
} catch { return null; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printHelpText() {
|
|
95
|
+
const commands = COMMAND_DEFINITIONS
|
|
96
|
+
.map(command => ` ${C.emerald}${command.usage.padEnd(30)}${C.reset} ${command.desc}`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
console.log(`\n ${C.bold}${C.gold}${UI_ICONS.help} Kiku AI Agent — ${COMMAND_DEFINITIONS.length} Commands${C.reset}\n${commands}\n ${C.dim}Tip: Press [Tab] for auto-completion.${C.reset}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function printStatsText() {
|
|
102
|
+
const { executeTool } = await import('../tools.js');
|
|
103
|
+
const result = await executeTool('get_system_stats', {});
|
|
104
|
+
const s = result.stats;
|
|
105
|
+
console.log(JSON.stringify(s, null, 2));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function main() {
|
|
109
|
+
const opts = parseCliArgs();
|
|
110
|
+
|
|
111
|
+
if (opts.version) { console.log(PACKAGE_VERSION); process.exit(0); }
|
|
112
|
+
if (opts.help) { printHelpText(); process.exit(0); }
|
|
113
|
+
if (opts.showStats) { await printStatsText(); process.exit(0); }
|
|
114
|
+
|
|
115
|
+
if (opts.showTools) {
|
|
116
|
+
console.log(`\n ${C.bold}${C.sakura}Tools (${toolsSchema.length})${C.reset}\n`);
|
|
117
|
+
for (const [i, t] of toolsSchema.entries()) {
|
|
118
|
+
console.log(` ${C.gold}${String(i + 1).padStart(2)}${C.reset} ${C.cyberCyan}${t.function.name}${C.reset} — ${C.dim}${t.function.description.slice(0, 60)}${C.reset}`);
|
|
119
|
+
}
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const baseUrl = opts.url || config.lmStudio.baseUrl;
|
|
124
|
+
const client = new LMStudioClient({ baseUrl, model: opts.model });
|
|
125
|
+
const health = await client.checkConnection();
|
|
126
|
+
|
|
127
|
+
if (!health.connected) {
|
|
128
|
+
console.log(` ${C.amber}!! LM Studio offline${C.reset}`);
|
|
129
|
+
console.log(` ${C.dim}• ${health.error}${C.reset}\n`);
|
|
130
|
+
|
|
131
|
+
const autoHealth = await attemptAutoStartLMStudio(baseUrl, opts.model || config.lmStudio.model);
|
|
132
|
+
if (autoHealth) {
|
|
133
|
+
console.log(` ${C.emerald}[OK] LM Studio started.${C.reset}\n`);
|
|
134
|
+
} else {
|
|
135
|
+
console.log(` ${C.sakura}Setup: Open LM Studio → load google/gemma-4-e4b → Start Server :1234${C.reset}\n`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const agent = new KikuAgent({
|
|
141
|
+
baseUrl,
|
|
142
|
+
model: health.activeModel || opts.model,
|
|
143
|
+
thinkingMode: opts.thinking,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await agent._ready;
|
|
147
|
+
|
|
148
|
+
if (opts.query) {
|
|
149
|
+
const { executeTool: execTool } = await import('../tools.js');
|
|
150
|
+
const { sanitizeQuery } = await import('../sanitizer.js');
|
|
151
|
+
const { sanitized } = sanitizeQuery(opts.query);
|
|
152
|
+
const prompt = sanitized || opts.query;
|
|
153
|
+
|
|
154
|
+
console.log(` ${C.bold}${C.gold}QUERY${C.reset} ${prompt}\n`);
|
|
155
|
+
|
|
156
|
+
await agent.ask(prompt, {
|
|
157
|
+
onReasoningStart: () => process.stdout.write(' THINKING '),
|
|
158
|
+
onReasoningDelta: (chunk) => process.stdout.write(chunk),
|
|
159
|
+
onReasoningEnd: () => process.stdout.write('\n'),
|
|
160
|
+
onToolStart: (name) => process.stdout.write(` [RUNNING] ${name}... `),
|
|
161
|
+
onToolResult: () => process.stdout.write(`[OK]\n`),
|
|
162
|
+
onTokenDelta: (chunk) => process.stdout.write(chunk),
|
|
163
|
+
onFinalAnswer: () => {},
|
|
164
|
+
onError: (err) => console.error(`\n [ERROR] ${err.message}`),
|
|
165
|
+
});
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const helpFn = (sub) => { printHelpText(); };
|
|
170
|
+
const adFn = () => { console.log(` ${UI_ICONS.sponsor} SPONSOR DashSecurity — https://dashsecurity.kpltgroup.com/`); };
|
|
171
|
+
|
|
172
|
+
const { waitUntilExit } = render(
|
|
173
|
+
<App
|
|
174
|
+
agent={agent}
|
|
175
|
+
client={client}
|
|
176
|
+
baseUrl={baseUrl}
|
|
177
|
+
printHelp={helpFn}
|
|
178
|
+
printAd={adFn}
|
|
179
|
+
/>,
|
|
180
|
+
{
|
|
181
|
+
exitOnCtrlC: true,
|
|
182
|
+
patchConsole: false,
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await waitUntilExit;
|
|
187
|
+
|
|
188
|
+
try { await agent.endSession(); } catch {}
|
|
189
|
+
console.log('\n Session archived. Goodbye. (鞠躬 ~ 谢谢!)\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch(err => {
|
|
193
|
+
console.error(`\n ${C.red}Fatal: ${err.message}${C.reset}\n`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { eventBus } from '../eventBus.js';
|
|
3
|
+
|
|
4
|
+
export function useEvent(eventName, initialValue = null) {
|
|
5
|
+
const [value, setValue] = useState(initialValue);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const unsub = eventBus.on(eventName, (data) => {
|
|
9
|
+
setValue(data);
|
|
10
|
+
});
|
|
11
|
+
return unsub;
|
|
12
|
+
}, [eventName]);
|
|
13
|
+
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useEvents(eventMap) {
|
|
18
|
+
const [state, setState] = useState(() => {
|
|
19
|
+
const init = {};
|
|
20
|
+
for (const [key] of Object.entries(eventMap)) {
|
|
21
|
+
init[key] = null;
|
|
22
|
+
}
|
|
23
|
+
return init;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const unsubs = [];
|
|
28
|
+
for (const [key, eventName] of Object.entries(eventMap)) {
|
|
29
|
+
const unsub = eventBus.on(eventName, (data) => {
|
|
30
|
+
setState(prev => ({ ...prev, [key]: data }));
|
|
31
|
+
});
|
|
32
|
+
unsubs.push(unsub);
|
|
33
|
+
}
|
|
34
|
+
return () => unsubs.forEach(u => u());
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useEmit() {
|
|
41
|
+
return useCallback((eventName, data) => {
|
|
42
|
+
eventBus.emit(eventName, data);
|
|
43
|
+
}, []);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function useAgentState() {
|
|
47
|
+
const [agentState, setAgentState] = useState({
|
|
48
|
+
state: 'IDLE',
|
|
49
|
+
label: 'Idle',
|
|
50
|
+
toolName: null,
|
|
51
|
+
elapsedMs: 0,
|
|
52
|
+
isProcessing: false,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const unsubs = [
|
|
57
|
+
eventBus.on('agent:state:change', (data) => {
|
|
58
|
+
setAgentState({
|
|
59
|
+
state: data.state || 'IDLE',
|
|
60
|
+
label: data.label || 'Idle',
|
|
61
|
+
toolName: data.state === 'FETCHING_TOOL' ? data.label?.replace('Tool: ', '') : null,
|
|
62
|
+
elapsedMs: 0,
|
|
63
|
+
isProcessing: data.state !== 'IDLE' && data.state !== 'COMPLETE',
|
|
64
|
+
});
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
return () => unsubs.forEach(u => u());
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return agentState;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useStreamingText() {
|
|
74
|
+
const [text, setText] = useState('');
|
|
75
|
+
const [isStreaming, setIsStreaming] = useState(false);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const unsubs = [
|
|
79
|
+
eventBus.on('agent:token:start', () => {
|
|
80
|
+
setIsStreaming(true);
|
|
81
|
+
setText('');
|
|
82
|
+
}),
|
|
83
|
+
eventBus.on('agent:token:delta', (data) => {
|
|
84
|
+
setText(prev => prev + (data?.chunk || ''));
|
|
85
|
+
}),
|
|
86
|
+
eventBus.on('agent:final:answer', () => {
|
|
87
|
+
setIsStreaming(false);
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
return () => unsubs.forEach(u => u());
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
return { text, isStreaming, reset: () => setText('') };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useThinking() {
|
|
97
|
+
const [thinking, setThinking] = useState('');
|
|
98
|
+
const [duration, setDuration] = useState(0);
|
|
99
|
+
const [isActive, setIsActive] = useState(false);
|
|
100
|
+
const startTimeRef = useRef(null);
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const unsubs = [
|
|
104
|
+
eventBus.on('agent:reasoning:start', () => {
|
|
105
|
+
setIsActive(true);
|
|
106
|
+
setThinking('');
|
|
107
|
+
startTimeRef.current = Date.now();
|
|
108
|
+
}),
|
|
109
|
+
eventBus.on('agent:reasoning:delta', (data) => {
|
|
110
|
+
setThinking(prev => prev + (data?.chunk || ''));
|
|
111
|
+
}),
|
|
112
|
+
eventBus.on('agent:reasoning:end', () => {
|
|
113
|
+
setIsActive(false);
|
|
114
|
+
if (startTimeRef.current) {
|
|
115
|
+
setDuration(((Date.now() - startTimeRef.current) / 1000).toFixed(1));
|
|
116
|
+
}
|
|
117
|
+
}),
|
|
118
|
+
];
|
|
119
|
+
return () => unsubs.forEach(u => u());
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
return { thinking, duration, isActive };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useToolResults() {
|
|
126
|
+
const [tools, setTools] = useState([]);
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const onToolStart = (data) => {
|
|
130
|
+
const toolName = data && data.toolName ? data.toolName : '?';
|
|
131
|
+
const toolArgs = data && data.args ? data.args : {};
|
|
132
|
+
setTools((prev) => [...prev, {
|
|
133
|
+
name: toolName,
|
|
134
|
+
args: toolArgs,
|
|
135
|
+
result: null,
|
|
136
|
+
duration: 0,
|
|
137
|
+
status: 'running',
|
|
138
|
+
id: Date.now(),
|
|
139
|
+
}]);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const onToolResult = (data) => {
|
|
143
|
+
const toolName = data && data.toolName ? data.toolName : '';
|
|
144
|
+
const result = data && data.result ? data.result : null;
|
|
145
|
+
const durationMs = data && data.durationMs ? data.durationMs : 0;
|
|
146
|
+
const isError = result && result.status === 'error';
|
|
147
|
+
setTools((prev) => prev.map((t) => {
|
|
148
|
+
if (t.status === 'running' && t.name === toolName) {
|
|
149
|
+
return Object.assign({}, t, {
|
|
150
|
+
result: result,
|
|
151
|
+
duration: durationMs,
|
|
152
|
+
status: isError ? 'error' : 'done',
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return t;
|
|
156
|
+
}));
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const unsub1 = eventBus.on('agent:tool:start', onToolStart);
|
|
160
|
+
const unsub2 = eventBus.on('agent:tool:result', onToolResult);
|
|
161
|
+
return function cleanup() {
|
|
162
|
+
unsub1();
|
|
163
|
+
unsub2();
|
|
164
|
+
};
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const clearTools = useCallback(() => setTools([]), []);
|
|
168
|
+
|
|
169
|
+
return { tools: tools, clearTools: clearTools };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function useTaskPlan() {
|
|
173
|
+
const [plan, setPlan] = useState(null);
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const onStart = (data) => {
|
|
177
|
+
if (data?.steps) {
|
|
178
|
+
setPlan({
|
|
179
|
+
planId: data.planId,
|
|
180
|
+
question: data.question,
|
|
181
|
+
steps: data.steps,
|
|
182
|
+
isActive: true,
|
|
183
|
+
startTime: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const onStepUpdate = (data) => {
|
|
189
|
+
setPlan(prev => {
|
|
190
|
+
if (!prev) return prev;
|
|
191
|
+
const updatedSteps = prev.steps.map(s => {
|
|
192
|
+
if (s.id === data.stepId) {
|
|
193
|
+
return { ...s, state: data.state, label: data.label || s.label };
|
|
194
|
+
}
|
|
195
|
+
return s;
|
|
196
|
+
});
|
|
197
|
+
return { ...prev, steps: updatedSteps };
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const onComplete = () => {
|
|
202
|
+
setPlan(prev => prev ? { ...prev, isActive: false } : null);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const unsubs = [
|
|
206
|
+
eventBus.on('agent:plan:start', onStart),
|
|
207
|
+
eventBus.on('agent:plan:step:update', onStepUpdate),
|
|
208
|
+
eventBus.on('agent:plan:complete', onComplete),
|
|
209
|
+
eventBus.on('agent:final:answer', () => setPlan(null)),
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
return () => unsubs.forEach(u => u());
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
const clearPlan = useCallback(() => setPlan(null), []);
|
|
216
|
+
|
|
217
|
+
return { plan, clearPlan };
|
|
218
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
export function extractWordTokens(text) {
|
|
2
|
+
if (!text || typeof text !== 'string') return [];
|
|
3
|
+
return text
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^\p{L}\p{N}\s]+/gu, ' ')
|
|
6
|
+
.split(/\s+/)
|
|
7
|
+
.filter(t => t.length >= 2);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function buildTermFrequencyVector(tokens) {
|
|
11
|
+
const vec = {};
|
|
12
|
+
for (const t of tokens) {
|
|
13
|
+
vec[t] = (vec[t] || 0) + 1;
|
|
14
|
+
}
|
|
15
|
+
return vec;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function computeCosineSimilarity(vecA, vecB) {
|
|
19
|
+
let dotProduct = 0;
|
|
20
|
+
let normA = 0;
|
|
21
|
+
let normB = 0;
|
|
22
|
+
|
|
23
|
+
for (const [key, val] of Object.entries(vecA)) {
|
|
24
|
+
normA += val * val;
|
|
25
|
+
if (vecB[key]) {
|
|
26
|
+
dotProduct += val * vecB[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const val of Object.values(vecB)) {
|
|
31
|
+
normB += val * val;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (normA === 0 || normB === 0) return 0;
|
|
35
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ContextAllocator {
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.store = options.store;
|
|
41
|
+
this.defaultMaxTokens = options.maxTokens || 2000;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
estimateTokens(text) {
|
|
45
|
+
if (!text) return 0;
|
|
46
|
+
|
|
47
|
+
return Math.ceil(text.length / 3.5);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
truncateToTokenLimit(text, tokenLimit) {
|
|
51
|
+
if (!text) return '';
|
|
52
|
+
const charLimit = Math.floor(tokenLimit * 3.5);
|
|
53
|
+
if (text.length <= charLimit) return text;
|
|
54
|
+
return text.slice(0, charLimit - 15) + '\n...[truncated]';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async allocateContext({ currentQuery = '', activeRunId = null, memory = null, maxTokens = null } = {}) {
|
|
59
|
+
const budgetTokens = maxTokens || this.defaultMaxTokens;
|
|
60
|
+
const quota = {
|
|
61
|
+
user: Math.floor(budgetTokens * 0.20),
|
|
62
|
+
run: Math.floor(budgetTokens * 0.40),
|
|
63
|
+
reflections: Math.floor(budgetTokens * 0.20),
|
|
64
|
+
general: Math.floor(budgetTokens * 0.20),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const sections = [];
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const userParts = [];
|
|
71
|
+
if (this.store) {
|
|
72
|
+
const guidelineNodes = await this.store.listNodes('kiku/guidelines/');
|
|
73
|
+
for (const node of guidelineNodes) {
|
|
74
|
+
const domain = node.domain || 'general';
|
|
75
|
+
const content = typeof node.content === 'string' ? node.content : JSON.stringify(node.content);
|
|
76
|
+
userParts.push(`[Guideline: ${domain}]\n${content}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (memory?.preferences?.language) {
|
|
81
|
+
userParts.push(`User preferred language: ${memory.preferences.language}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const userSkillContext = memory?.buildUserSkillContext ? memory.buildUserSkillContext() : '';
|
|
85
|
+
if (userSkillContext) {
|
|
86
|
+
userParts.push(`User-taught skills:\n${userSkillContext}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const feedbackStyleContext = memory?.buildFeedbackStyleContext ? memory.buildFeedbackStyleContext() : '';
|
|
90
|
+
if (feedbackStyleContext) {
|
|
91
|
+
userParts.push(`Learned answer-style preferences:\n${feedbackStyleContext}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (userParts.length > 0) {
|
|
95
|
+
const userText = this.truncateToTokenLimit(userParts.join('\n\n'), quota.user);
|
|
96
|
+
sections.push(`--- USER GUIDELINES & PREFERENCES (20% Quota) ---\n${userText}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if (activeRunId && this.store) {
|
|
101
|
+
const runData = await this.store.loadActiveRun(activeRunId);
|
|
102
|
+
if (runData.exists) {
|
|
103
|
+
const runParts = [];
|
|
104
|
+
runParts.push(`Active Run ID: ${activeRunId}`);
|
|
105
|
+
|
|
106
|
+
if (runData.state) {
|
|
107
|
+
runParts.push(`Status: ${runData.state.status || 'IN_PROGRESS'}`);
|
|
108
|
+
if (runData.state.currentStep) {
|
|
109
|
+
runParts.push(`Current Step: ${runData.state.currentStep}`);
|
|
110
|
+
}
|
|
111
|
+
if (runData.state.lastSummary) {
|
|
112
|
+
runParts.push(`Latest Step Summary: ${runData.state.lastSummary}`);
|
|
113
|
+
}
|
|
114
|
+
if (runData.state.lastFailure) {
|
|
115
|
+
runParts.push(`Recent Failure Alert: ${runData.state.lastFailure.errorTrace} -> Recovery: ${runData.state.lastFailure.recoveryAction}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (runData.plan && Array.isArray(runData.plan.subtasks) && runData.plan.subtasks.length > 0) {
|
|
120
|
+
const checklist = runData.plan.subtasks.map(t => {
|
|
121
|
+
const marker = t.status === 'completed' ? '[x]' : (t.status === 'in_progress' ? '[>]' : (t.status === 'failed' ? '[!]' : '[ ]'));
|
|
122
|
+
const summaryText = t.summary ? ` - ${t.summary}` : '';
|
|
123
|
+
return `${marker} ${t.name} (${t.status})${summaryText}`;
|
|
124
|
+
}).join('\n');
|
|
125
|
+
runParts.push(`Execution Plan Checklist:\n${checklist}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (runData.artifacts && Array.isArray(runData.artifacts.items) && runData.artifacts.items.length > 0) {
|
|
129
|
+
const artifactList = runData.artifacts.items.map(a => `- ${a.name}: ${a.uri}`).join('\n');
|
|
130
|
+
runParts.push(`Artifact Pointers:\n${artifactList}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const runText = this.truncateToTokenLimit(runParts.join('\n\n'), quota.run);
|
|
134
|
+
sections.push(`--- ACTIVE TASK RUN STATE (40% Quota) ---\n${runText}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if (this.store) {
|
|
140
|
+
const reflectionNodes = await this.store.listNodes('kiku/reflections/');
|
|
141
|
+
if (reflectionNodes.length > 0) {
|
|
142
|
+
const queryTokens = extractWordTokens(currentQuery);
|
|
143
|
+
const queryVector = buildTermFrequencyVector(queryTokens);
|
|
144
|
+
|
|
145
|
+
const scored = reflectionNodes.map(node => {
|
|
146
|
+
const content = node.content || {};
|
|
147
|
+
const textToMatch = [
|
|
148
|
+
node.domain || '',
|
|
149
|
+
content.issuePattern || '',
|
|
150
|
+
content.errorTrace || '',
|
|
151
|
+
content.recoveryAction || '',
|
|
152
|
+
content.lesson || '',
|
|
153
|
+
content.summary || ''
|
|
154
|
+
].join(' ');
|
|
155
|
+
|
|
156
|
+
const nodeTokens = extractWordTokens(textToMatch);
|
|
157
|
+
const nodeVector = buildTermFrequencyVector(nodeTokens);
|
|
158
|
+
const similarity = computeCosineSimilarity(queryVector, nodeVector);
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
node,
|
|
162
|
+
similarity,
|
|
163
|
+
confidence: Number(content.confidence) || 1.0,
|
|
164
|
+
score: similarity * (Number(content.confidence) || 1.0)
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
scored.sort((a, b) => b.score - a.score);
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
const positiveScored = scored.filter(s => s.score > 0.05).slice(0, 4);
|
|
172
|
+
const topReflections = positiveScored.length > 0 ? positiveScored : scored.slice(0, 2);
|
|
173
|
+
|
|
174
|
+
const refLines = topReflections.map(item => {
|
|
175
|
+
const c = item.node.content || {};
|
|
176
|
+
const title = item.node.domain ? `[Reflection: ${item.node.domain}]` : '[Reflection]';
|
|
177
|
+
const issue = c.issuePattern ? `Problem: ${c.issuePattern}` : (c.errorTrace ? `Error: ${c.errorTrace}` : '');
|
|
178
|
+
const solution = c.recoveryAction ? `Solution: ${c.recoveryAction}` : (c.lesson ? `Lesson: ${c.lesson}` : (c.summary || ''));
|
|
179
|
+
return `${title}\n${issue}\n${solution}`.trim();
|
|
180
|
+
}).filter(Boolean);
|
|
181
|
+
|
|
182
|
+
if (refLines.length > 0) {
|
|
183
|
+
const refText = this.truncateToTokenLimit(refLines.join('\n\n'), quota.reflections);
|
|
184
|
+
sections.push(`--- EPISODIC REFLECTIONS & GOTCHAS (20% Quota) ---\n${refText}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
const generalParts = [];
|
|
191
|
+
if (memory?.getRelevantFacts) {
|
|
192
|
+
const facts = currentQuery ? memory.getRelevantFacts(currentQuery, 6) : (memory.facts || []).slice(-6);
|
|
193
|
+
if (facts.length > 0) {
|
|
194
|
+
generalParts.push('Known facts:\n' + facts.map(f => `- ${f.content}`).join('\n'));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (memory?.getTopTopics) {
|
|
199
|
+
const topTopics = memory.getTopTopics(3);
|
|
200
|
+
if (topTopics.length > 0) {
|
|
201
|
+
generalParts.push('Frequent topics: ' + topTopics.map(t => `${t.topic} (${t.count}x)`).join(', '));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (generalParts.length > 0) {
|
|
206
|
+
const genText = this.truncateToTokenLimit(generalParts.join('\n\n'), quota.general);
|
|
207
|
+
sections.push(`--- GENERAL KNOWLEDGE & CONTEXT (20% Quota) ---\n${genText}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (sections.length === 0) return '';
|
|
211
|
+
|
|
212
|
+
return `\n\n=== STATEFUL & SELF-REFLECTIVE MEMORY CONTEXT ===\n${sections.join('\n\n')}\n=== END MEMORY CONTEXT ===`;
|
|
213
|
+
}
|
|
214
|
+
}
|