kiku-agent 1.0.0 → 1.0.3
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 +293 -122
- package/package.json +60 -61
- package/server/agent/agentCore.js +892 -782
- package/server/agent/appPaths.js +21 -0
- package/server/agent/audioPlayer.js +739 -673
- package/server/agent/cli.js +1414 -1111
- package/server/agent/commandRegistry.js +74 -58
- package/server/agent/commands.js +1694 -788
- package/server/agent/config.js +264 -90
- package/server/agent/contextCompactor.js +79 -0
- package/server/agent/doctor.js +69 -0
- package/server/agent/eventBus.js +107 -103
- package/server/agent/gpuMetrics.js +189 -0
- package/server/agent/llmClient.js +369 -200
- package/server/agent/mcpClient.js +364 -0
- package/server/agent/memory.js +670 -566
- package/server/agent/modelManager.js +77 -0
- package/server/agent/research.js +202 -0
- package/server/agent/session.js +244 -239
- package/server/agent/subagent.js +116 -0
- package/server/agent/theme.js +125 -124
- package/server/agent/toolRouter.js +201 -0
- package/server/agent/tools.js +1679 -1528
- package/server/agent/ui.js +760 -469
- package/server/mcp/claude-desktop-config.example.json +1 -1
- package/server/mcp/data/discography.js +36 -36
- package/server/mcp/dataAdapter.js +102 -0
- package/server/mcp/examples/claude-code.mcp.example.json +19 -0
- package/server/mcp/examples/claude-desktop.example.json +11 -0
- package/server/mcp/examples/cline.example.json +13 -0
- package/server/mcp/examples/cursor.example.json +14 -0
- package/server/mcp/examples/gemini-cli.example.json +17 -0
- package/server/mcp/examples/opencode.example.json +21 -0
- package/server/mcp/examples/vscode.example.json +19 -0
- package/server/mcp/examples/windsurf.example.json +14 -0
- package/server/mcp/handler.js +85 -159
- package/server/mcp/security.js +140 -138
- package/server/mcp/server.js +129 -100
- package/server/mcp/stdio.js +4 -2
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const NON_CHAT_MODEL = /(embed|embedding|rerank|whisper|tts|clip)/i;
|
|
2
|
+
|
|
3
|
+
export class ModelManager {
|
|
4
|
+
constructor(client, options = {}) {
|
|
5
|
+
if (!client) throw new Error('ModelManager requires an LLM client.');
|
|
6
|
+
this.client = client;
|
|
7
|
+
this.requestedModel = options.requestedModel || client.model;
|
|
8
|
+
this.fallbackModel = options.fallbackModel || null;
|
|
9
|
+
this.lastResolution = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async inspect() {
|
|
13
|
+
const health = await this.client.checkConnection();
|
|
14
|
+
const models = health.models || [];
|
|
15
|
+
return {
|
|
16
|
+
...health,
|
|
17
|
+
models,
|
|
18
|
+
selectedModel: this.client.model,
|
|
19
|
+
state: !health.connected
|
|
20
|
+
? 'server_offline'
|
|
21
|
+
: models.length === 0
|
|
22
|
+
? 'server_online_no_models'
|
|
23
|
+
: models.includes(this.client.model)
|
|
24
|
+
? 'ready'
|
|
25
|
+
: 'requested_model_missing',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
select(model, availableModels = []) {
|
|
30
|
+
const requested = String(model || '').trim();
|
|
31
|
+
if (!requested) throw new Error('A model ID is required.');
|
|
32
|
+
if (availableModels.length > 0 && !availableModels.includes(requested)) {
|
|
33
|
+
throw new Error(`Model is not available: ${requested}`);
|
|
34
|
+
}
|
|
35
|
+
if (NON_CHAT_MODEL.test(requested)) {
|
|
36
|
+
throw new Error(`Model does not appear to support chat: ${requested}`);
|
|
37
|
+
}
|
|
38
|
+
this.client.model = requested;
|
|
39
|
+
this.requestedModel = requested;
|
|
40
|
+
return requested;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async resolve(options = {}) {
|
|
44
|
+
const health = await this.client.checkConnection();
|
|
45
|
+
if (!health.connected) {
|
|
46
|
+
return this._record({ state: 'server_offline', connected: false, models: [], error: health.error });
|
|
47
|
+
}
|
|
48
|
+
const models = health.models || [];
|
|
49
|
+
if (models.length === 0) {
|
|
50
|
+
return this._record({ state: 'server_online_no_models', connected: true, models, error: `${this.client.providerName || 'LLM server'} is online but no model is loaded.` });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const requested = options.requestedModel || this.requestedModel;
|
|
54
|
+
const exact = models.find(id => id === requested);
|
|
55
|
+
const configuredFallback = models.find(id => id === (options.fallbackModel || this.fallbackModel));
|
|
56
|
+
const gemma = models.find(id => /gemma/i.test(id) && !NON_CHAT_MODEL.test(id));
|
|
57
|
+
const chatFallback = models.find(id => !NON_CHAT_MODEL.test(id));
|
|
58
|
+
const selected = exact || configuredFallback || gemma || chatFallback;
|
|
59
|
+
if (!selected) {
|
|
60
|
+
return this._record({ state: 'no_chat_model', connected: true, models, error: 'No chat-capable model was found.' });
|
|
61
|
+
}
|
|
62
|
+
this.client.model = selected;
|
|
63
|
+
return this._record({
|
|
64
|
+
state: exact ? 'ready' : 'fallback_selected',
|
|
65
|
+
connected: true,
|
|
66
|
+
models,
|
|
67
|
+
selectedModel: selected,
|
|
68
|
+
requestedModel: requested,
|
|
69
|
+
usedFallback: !exact,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
_record(result) {
|
|
74
|
+
this.lastResolution = { ...result, checkedAt: new Date().toISOString() };
|
|
75
|
+
return this.lastResolution;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { ResearchSubagent } from './subagent.js';
|
|
2
|
+
import { executeTool } from './tools.js';
|
|
3
|
+
import { eventBus, Events } from './eventBus.js';
|
|
4
|
+
|
|
5
|
+
const CATALOG_TOOLS = Object.freeze([
|
|
6
|
+
'global_search', 'get_profile_and_fandom', 'search_works', 'get_work_detail',
|
|
7
|
+
'get_character_roles', 'search_discography', 'get_music_videos',
|
|
8
|
+
'get_media_links', 'get_brand_endorsements', 'get_career_timeline',
|
|
9
|
+
'get_awards', 'get_social_feed',
|
|
10
|
+
]);
|
|
11
|
+
const WEB_TOOLS = Object.freeze(['web_search', 'web_extract', 'fetch_webpage']);
|
|
12
|
+
const VERIFY_TOOLS = Object.freeze([...CATALOG_TOOLS, ...WEB_TOOLS]);
|
|
13
|
+
|
|
14
|
+
function uniqueUrls(text) {
|
|
15
|
+
const matches = String(text || '').match(/https?:\/\/[^\s<>()\]]+/g) || [];
|
|
16
|
+
return [...new Set(matches.map(url => url.replace(/[.,;:!?]+$/, '')))];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stableSignature(name, args) {
|
|
20
|
+
const sort = value => {
|
|
21
|
+
if (Array.isArray(value)) return value.map(sort);
|
|
22
|
+
if (value && typeof value === 'object') {
|
|
23
|
+
return Object.fromEntries(Object.keys(value).sort().map(key => [key, sort(value[key])]));
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
return `${name}:${JSON.stringify(sort(args || {}))}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function taskPlan(question, mode) {
|
|
31
|
+
const verifyInstruction = mode === 'verify'
|
|
32
|
+
? 'Classify the claim as verified, unverified, conflicting, or false. Do not infer confirmation from silence.'
|
|
33
|
+
: 'Identify corroboration, contradictions, and missing evidence.';
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
id: 'catalog',
|
|
37
|
+
label: 'Kiku catalog',
|
|
38
|
+
tools: CATALOG_TOOLS,
|
|
39
|
+
objective: `Search the verified local Kiku catalog for this question: ${question}`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'web',
|
|
43
|
+
label: 'Current web sources',
|
|
44
|
+
tools: WEB_TOOLS,
|
|
45
|
+
objective: `Find current primary or authoritative web sources for this Ju Jingyi question: ${question}`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'verify',
|
|
49
|
+
label: 'Independent verifier',
|
|
50
|
+
tools: VERIFY_TOOLS,
|
|
51
|
+
objective: `${verifyInstruction}\nQuestion or claim: ${question}`,
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class ResearchOrchestrator {
|
|
57
|
+
constructor(options = {}) {
|
|
58
|
+
if (!options.client) throw new Error('ResearchOrchestrator requires an LLM client.');
|
|
59
|
+
this.client = options.client;
|
|
60
|
+
this.executeTool = options.executeTool;
|
|
61
|
+
this.maxWorkers = Math.min(Math.max(options.maxWorkers || 3, 1), 3);
|
|
62
|
+
this.maxIterations = Math.min(Math.max(options.maxIterations || 3, 1), 5);
|
|
63
|
+
this.active = null;
|
|
64
|
+
this.lastResult = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getStatus() {
|
|
68
|
+
if (!this.active) return { state: 'idle', lastResult: this.lastResult };
|
|
69
|
+
return {
|
|
70
|
+
state: 'running',
|
|
71
|
+
id: this.active.id,
|
|
72
|
+
question: this.active.question,
|
|
73
|
+
startedAt: this.active.startedAt,
|
|
74
|
+
workers: [...this.active.workers.values()],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cancel() {
|
|
79
|
+
if (!this.active) return false;
|
|
80
|
+
this.active.controller.abort();
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async run(question, options = {}) {
|
|
85
|
+
const query = String(question || '').trim();
|
|
86
|
+
if (!query) throw new Error('A research question is required.');
|
|
87
|
+
if (this.active) throw new Error('A research job is already running.');
|
|
88
|
+
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const abortParent = () => controller.abort(options.signal?.reason);
|
|
91
|
+
options.signal?.addEventListener('abort', abortParent, { once: true });
|
|
92
|
+
const plan = taskPlan(query, options.mode || 'research').slice(0, this.maxWorkers);
|
|
93
|
+
const job = {
|
|
94
|
+
id: `research_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
95
|
+
question: query,
|
|
96
|
+
startedAt: new Date().toISOString(),
|
|
97
|
+
controller,
|
|
98
|
+
workers: new Map(plan.map(task => [task.id, { id: task.id, label: task.label, state: 'pending', toolCalls: 0 }])),
|
|
99
|
+
};
|
|
100
|
+
const sharedToolCache = new Map();
|
|
101
|
+
const cachedExecute = async (name, args) => {
|
|
102
|
+
const signature = stableSignature(name, args);
|
|
103
|
+
if (!sharedToolCache.has(signature)) {
|
|
104
|
+
sharedToolCache.set(signature, Promise.resolve((this.executeTool || executeTool)(name, args)));
|
|
105
|
+
}
|
|
106
|
+
return sharedToolCache.get(signature);
|
|
107
|
+
};
|
|
108
|
+
this.active = job;
|
|
109
|
+
options.onProgress?.({ type: 'start', id: job.id, workers: plan.length });
|
|
110
|
+
eventBus.emit(Events.AGENT_PLAN_START, {
|
|
111
|
+
planId: job.id,
|
|
112
|
+
question: query,
|
|
113
|
+
steps: plan.map(p => ({ id: p.id, label: p.label, state: 'pending', objective: p.objective }))
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const settled = await Promise.allSettled(plan.map(async task => {
|
|
118
|
+
const status = job.workers.get(task.id);
|
|
119
|
+
status.state = 'running';
|
|
120
|
+
options.onProgress?.({ type: 'worker-start', id: task.id, label: task.label });
|
|
121
|
+
eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
|
|
122
|
+
planId: job.id,
|
|
123
|
+
stepId: task.id,
|
|
124
|
+
state: 'running',
|
|
125
|
+
label: task.label,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const worker = new ResearchSubagent({
|
|
129
|
+
client: this.client,
|
|
130
|
+
executeTool: cachedExecute,
|
|
131
|
+
allowedTools: task.tools,
|
|
132
|
+
maxIterations: this.maxIterations,
|
|
133
|
+
});
|
|
134
|
+
const result = await worker.run(task.objective, {
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
skillContext: options.skillContext,
|
|
137
|
+
onProgress: event => {
|
|
138
|
+
if (event.type === 'tool') status.toolCalls += 1;
|
|
139
|
+
options.onProgress?.({ ...event, workerId: task.id, label: task.label });
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
status.state = 'complete';
|
|
143
|
+
options.onProgress?.({ type: 'worker-complete', id: task.id, label: task.label });
|
|
144
|
+
eventBus.emit(Events.AGENT_PLAN_STEP_UPDATE, {
|
|
145
|
+
planId: job.id,
|
|
146
|
+
stepId: task.id,
|
|
147
|
+
state: 'complete',
|
|
148
|
+
label: task.label,
|
|
149
|
+
});
|
|
150
|
+
return { id: task.id, label: task.label, ...result };
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
const reports = settled.map((entry, index) => {
|
|
154
|
+
if (entry.status === 'fulfilled') return entry.value;
|
|
155
|
+
const task = plan[index];
|
|
156
|
+
const status = job.workers.get(task.id);
|
|
157
|
+
status.state = controller.signal.aborted ? 'cancelled' : 'failed';
|
|
158
|
+
status.error = entry.reason?.message || 'Unknown worker error';
|
|
159
|
+
return { id: task.id, label: task.label, answer: '', error: status.error, toolCalls: [] };
|
|
160
|
+
});
|
|
161
|
+
if (controller.signal.aborted) throw controller.signal.reason || new DOMException('Aborted', 'AbortError');
|
|
162
|
+
|
|
163
|
+
const usable = reports.filter(report => report.answer);
|
|
164
|
+
if (usable.length === 0) throw new Error('All research workers failed.');
|
|
165
|
+
const synthesisPrompt = options.mode === 'verify'
|
|
166
|
+
? 'Return a clear verdict: VERIFIED, UNVERIFIED, CONFLICTING, or FALSE. Explain briefly and cite only URLs present in the reports.'
|
|
167
|
+
: 'Synthesize a direct, polished answer. Separate verified facts, conflicts, and missing evidence. Cite only URLs present in the reports.';
|
|
168
|
+
const synthesis = await this.client.chat({
|
|
169
|
+
messages: [
|
|
170
|
+
{ role: 'system', content: `You are the Kiku research editor. ${synthesisPrompt} Never invent a source, date, title, or URL. Match the language of the question. Do not reveal hidden reasoning.` },
|
|
171
|
+
{ role: 'user', content: JSON.stringify({ question: query, reports: usable.map(({ label, answer }) => ({ label, answer })) }) },
|
|
172
|
+
],
|
|
173
|
+
temperature: 0.1,
|
|
174
|
+
stream: false,
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
});
|
|
177
|
+
const answer = synthesis?.content || usable.map(report => `### ${report.label}\n${report.answer}`).join('\n\n');
|
|
178
|
+
const sources = uniqueUrls(usable.map(report => report.answer).join('\n'));
|
|
179
|
+
const result = {
|
|
180
|
+
id: job.id,
|
|
181
|
+
mode: options.mode || 'research',
|
|
182
|
+
question: query,
|
|
183
|
+
answer,
|
|
184
|
+
sources,
|
|
185
|
+
reports,
|
|
186
|
+
startedAt: job.startedAt,
|
|
187
|
+
completedAt: new Date().toISOString(),
|
|
188
|
+
};
|
|
189
|
+
this.lastResult = result;
|
|
190
|
+
options.onProgress?.({ type: 'complete', id: job.id, sources: sources.length });
|
|
191
|
+
eventBus.emit(Events.AGENT_PLAN_COMPLETE, {
|
|
192
|
+
planId: job.id,
|
|
193
|
+
sourcesCount: sources.length,
|
|
194
|
+
reportsCount: reports.length,
|
|
195
|
+
});
|
|
196
|
+
return result;
|
|
197
|
+
} finally {
|
|
198
|
+
options.signal?.removeEventListener('abort', abortParent);
|
|
199
|
+
this.active = null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|