kernelbot 1.0.25 → 1.0.28
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 +198 -123
- package/bin/kernel.js +201 -4
- package/package.json +1 -1
- package/src/agent.js +447 -174
- package/src/automation/automation-manager.js +377 -0
- package/src/automation/automation.js +79 -0
- package/src/automation/index.js +2 -0
- package/src/automation/scheduler.js +141 -0
- package/src/bot.js +908 -69
- package/src/conversation.js +69 -0
- package/src/intents/detector.js +50 -0
- package/src/intents/index.js +2 -0
- package/src/intents/planner.js +58 -0
- package/src/persona.js +68 -0
- package/src/prompts/orchestrator.js +76 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +74 -35
- package/src/prompts/workers.js +89 -0
- package/src/providers/anthropic.js +23 -16
- package/src/providers/base.js +76 -2
- package/src/providers/index.js +1 -0
- package/src/providers/models.js +2 -1
- package/src/providers/openai-compat.js +5 -3
- package/src/security/confirm.js +7 -2
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +169 -0
- package/src/swarm/job.js +67 -0
- package/src/swarm/worker-registry.js +74 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +101 -0
- package/src/tools/index.js +3 -0
- package/src/tools/orchestrator-tools.js +371 -0
- package/src/tools/persona.js +32 -0
- package/src/utils/config.js +53 -16
- package/src/worker.js +305 -0
- package/.agents/skills/interface-design/SKILL.md +0 -391
- package/.agents/skills/interface-design/references/critique.md +0 -67
- package/.agents/skills/interface-design/references/example.md +0 -86
- package/.agents/skills/interface-design/references/principles.md +0 -235
- package/.agents/skills/interface-design/references/validation.md +0 -48
package/src/agent.js
CHANGED
|
@@ -1,220 +1,426 @@
|
|
|
1
|
-
import { createProvider } from './providers/index.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createProvider, PROVIDERS } from './providers/index.js';
|
|
2
|
+
import { orchestratorToolDefinitions, executeOrchestratorTool } from './tools/orchestrator-tools.js';
|
|
3
|
+
import { getToolsForWorker } from './swarm/worker-registry.js';
|
|
4
|
+
import { WORKER_TYPES } from './swarm/worker-registry.js';
|
|
5
|
+
import { getOrchestratorPrompt } from './prompts/orchestrator.js';
|
|
6
|
+
import { getWorkerPrompt } from './prompts/workers.js';
|
|
7
|
+
import { getUnifiedSkillById } from './skills/custom.js';
|
|
8
|
+
import { WorkerAgent } from './worker.js';
|
|
4
9
|
import { getLogger } from './utils/logger.js';
|
|
5
|
-
import { getMissingCredential, saveCredential } from './utils/config.js';
|
|
10
|
+
import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
const MAX_RESULT_LENGTH = 3000;
|
|
13
|
+
const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
|
|
14
|
+
|
|
15
|
+
export class OrchestratorAgent {
|
|
16
|
+
constructor({ config, conversationManager, personaManager, jobManager, automationManager }) {
|
|
9
17
|
this.config = config;
|
|
10
18
|
this.conversationManager = conversationManager;
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
19
|
+
this.personaManager = personaManager;
|
|
20
|
+
this.jobManager = jobManager;
|
|
21
|
+
this.automationManager = automationManager || null;
|
|
13
22
|
this._pending = new Map(); // chatId -> pending state
|
|
23
|
+
this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
|
|
24
|
+
|
|
25
|
+
// Orchestrator always uses Anthropic (30s timeout — lean dispatch/summarize calls)
|
|
26
|
+
this.orchestratorProvider = createProvider({
|
|
27
|
+
brain: {
|
|
28
|
+
provider: 'anthropic',
|
|
29
|
+
model: config.orchestrator.model,
|
|
30
|
+
max_tokens: config.orchestrator.max_tokens,
|
|
31
|
+
temperature: config.orchestrator.temperature,
|
|
32
|
+
api_key: config.orchestrator.api_key || process.env.ANTHROPIC_API_KEY,
|
|
33
|
+
timeout: 30_000,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Worker provider uses user's chosen brain
|
|
38
|
+
this.workerProvider = createProvider(config);
|
|
39
|
+
|
|
40
|
+
// Set up job lifecycle event listeners
|
|
41
|
+
this._setupJobListeners();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build the orchestrator system prompt. */
|
|
45
|
+
_getSystemPrompt(chatId, user) {
|
|
46
|
+
const logger = getLogger();
|
|
47
|
+
const skillId = this.conversationManager.getSkill(chatId);
|
|
48
|
+
const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
|
|
49
|
+
|
|
50
|
+
let userPersona = null;
|
|
51
|
+
if (this.personaManager && user?.id) {
|
|
52
|
+
userPersona = this.personaManager.load(user.id, user.username);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'}`);
|
|
56
|
+
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setSkill(chatId, skillId) {
|
|
60
|
+
this.conversationManager.setSkill(chatId, skillId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clearSkill(chatId) {
|
|
64
|
+
this.conversationManager.clearSkill(chatId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getActiveSkill(chatId) {
|
|
68
|
+
const skillId = this.conversationManager.getSkill(chatId);
|
|
69
|
+
return skillId ? getUnifiedSkillById(skillId) : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Return current worker brain info for display. */
|
|
73
|
+
getBrainInfo() {
|
|
74
|
+
const { provider, model } = this.config.brain;
|
|
75
|
+
const providerDef = PROVIDERS[provider];
|
|
76
|
+
const providerName = providerDef ? providerDef.name : provider;
|
|
77
|
+
const modelEntry = providerDef?.models.find((m) => m.id === model);
|
|
78
|
+
const modelLabel = modelEntry ? modelEntry.label : model;
|
|
79
|
+
return { provider, providerName, model, modelLabel };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Switch worker brain provider/model at runtime. */
|
|
83
|
+
async switchBrain(providerKey, modelId) {
|
|
84
|
+
const logger = getLogger();
|
|
85
|
+
const providerDef = PROVIDERS[providerKey];
|
|
86
|
+
if (!providerDef) return `Unknown provider: ${providerKey}`;
|
|
87
|
+
|
|
88
|
+
const envKey = providerDef.envKey;
|
|
89
|
+
const apiKey = process.env[envKey];
|
|
90
|
+
if (!apiKey) return envKey;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
|
|
94
|
+
const testProvider = createProvider(testConfig);
|
|
95
|
+
await testProvider.ping();
|
|
96
|
+
|
|
97
|
+
this.config.brain.provider = providerKey;
|
|
98
|
+
this.config.brain.model = modelId;
|
|
99
|
+
this.config.brain.api_key = apiKey;
|
|
100
|
+
this.workerProvider = testProvider;
|
|
101
|
+
saveProviderToYaml(providerKey, modelId);
|
|
102
|
+
|
|
103
|
+
logger.info(`Worker brain switched to ${providerDef.name} / ${modelId}`);
|
|
104
|
+
return null;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
107
|
+
return { error: err.message };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Finalize brain switch after API key was provided via chat. */
|
|
112
|
+
async switchBrainWithKey(providerKey, modelId, apiKey) {
|
|
113
|
+
const logger = getLogger();
|
|
114
|
+
const providerDef = PROVIDERS[providerKey];
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
|
|
118
|
+
const testProvider = createProvider(testConfig);
|
|
119
|
+
await testProvider.ping();
|
|
120
|
+
|
|
121
|
+
saveCredential(this.config, providerDef.envKey, apiKey);
|
|
122
|
+
this.config.brain.provider = providerKey;
|
|
123
|
+
this.config.brain.model = modelId;
|
|
124
|
+
this.config.brain.api_key = apiKey;
|
|
125
|
+
this.workerProvider = testProvider;
|
|
126
|
+
saveProviderToYaml(providerKey, modelId);
|
|
127
|
+
|
|
128
|
+
logger.info(`Worker brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
|
|
129
|
+
return null;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
132
|
+
return { error: err.message };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Truncate a tool result. */
|
|
137
|
+
_truncateResult(name, result) {
|
|
138
|
+
let str = JSON.stringify(result);
|
|
139
|
+
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
140
|
+
|
|
141
|
+
if (result && typeof result === 'object') {
|
|
142
|
+
const truncated = { ...result };
|
|
143
|
+
for (const field of LARGE_FIELDS) {
|
|
144
|
+
if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
|
|
145
|
+
truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
str = JSON.stringify(truncated);
|
|
149
|
+
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
|
|
14
153
|
}
|
|
15
154
|
|
|
16
155
|
async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
|
|
17
156
|
const logger = getLogger();
|
|
18
157
|
|
|
19
|
-
|
|
20
|
-
|
|
158
|
+
logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
|
|
159
|
+
|
|
160
|
+
// Store callbacks so workers can use them later
|
|
161
|
+
this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
|
|
21
162
|
|
|
22
163
|
// Handle pending responses (confirmation or credential)
|
|
23
164
|
const pending = this._pending.get(chatId);
|
|
24
165
|
if (pending) {
|
|
25
166
|
this._pending.delete(chatId);
|
|
167
|
+
logger.debug(`Orchestrator handling pending ${pending.type} response for chat ${chatId}`);
|
|
26
168
|
|
|
27
169
|
if (pending.type === 'credential') {
|
|
28
|
-
return await this._handleCredentialResponse(chatId, userMessage, user, pending);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (pending.type === 'confirmation') {
|
|
32
|
-
return await this._handleConfirmationResponse(chatId, userMessage, user, pending);
|
|
170
|
+
return await this._handleCredentialResponse(chatId, userMessage, user, pending, onUpdate);
|
|
33
171
|
}
|
|
34
172
|
}
|
|
35
173
|
|
|
36
|
-
const { max_tool_depth } = this.config.
|
|
174
|
+
const { max_tool_depth } = this.config.orchestrator;
|
|
37
175
|
|
|
38
176
|
// Add user message to persistent history
|
|
39
177
|
this.conversationManager.addMessage(chatId, 'user', userMessage);
|
|
40
178
|
|
|
41
|
-
// Build working messages from history
|
|
42
|
-
const messages = [...this.conversationManager.
|
|
179
|
+
// Build working messages from compressed history
|
|
180
|
+
const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
|
|
181
|
+
logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
|
|
43
182
|
|
|
44
|
-
|
|
45
|
-
}
|
|
183
|
+
const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
46
184
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
git_clone: 'repo',
|
|
54
|
-
git_checkout: 'branch',
|
|
55
|
-
git_commit: 'message',
|
|
56
|
-
git_push: 'dir',
|
|
57
|
-
git_diff: 'dir',
|
|
58
|
-
github_create_pr: 'title',
|
|
59
|
-
github_create_repo: 'name',
|
|
60
|
-
github_list_prs: 'repo',
|
|
61
|
-
github_get_pr_diff: 'repo',
|
|
62
|
-
github_post_review: 'repo',
|
|
63
|
-
spawn_claude_code: 'prompt',
|
|
64
|
-
kill_process: 'pid',
|
|
65
|
-
docker_exec: 'container',
|
|
66
|
-
docker_logs: 'container',
|
|
67
|
-
docker_compose: 'action',
|
|
68
|
-
curl_url: 'url',
|
|
69
|
-
check_port: 'port',
|
|
70
|
-
screenshot_website: 'url',
|
|
71
|
-
send_image: 'file_path',
|
|
72
|
-
browse_website: 'url',
|
|
73
|
-
extract_content: 'url',
|
|
74
|
-
interact_with_page: 'url',
|
|
75
|
-
}[name];
|
|
76
|
-
const val = key && input[key] ? String(input[key]).slice(0, 120) : JSON.stringify(input).slice(0, 120);
|
|
77
|
-
return `${name}: ${val}`;
|
|
185
|
+
logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
|
|
186
|
+
|
|
187
|
+
// Background persona extraction
|
|
188
|
+
this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
|
|
189
|
+
|
|
190
|
+
return reply;
|
|
78
191
|
}
|
|
79
192
|
|
|
80
|
-
async _sendUpdate(text) {
|
|
81
|
-
|
|
82
|
-
|
|
193
|
+
async _sendUpdate(chatId, text, opts) {
|
|
194
|
+
const callbacks = this._chatCallbacks.get(chatId);
|
|
195
|
+
if (callbacks?.onUpdate) {
|
|
196
|
+
try { return await callbacks.onUpdate(text, opts); } catch {}
|
|
83
197
|
}
|
|
198
|
+
return null;
|
|
84
199
|
}
|
|
85
200
|
|
|
86
|
-
async _handleCredentialResponse(chatId, userMessage, user, pending) {
|
|
201
|
+
async _handleCredentialResponse(chatId, userMessage, user, pending, onUpdate) {
|
|
87
202
|
const logger = getLogger();
|
|
88
203
|
const value = userMessage.trim();
|
|
89
204
|
|
|
90
205
|
if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
|
|
91
206
|
logger.info(`User skipped credential: ${pending.credential.envKey}`);
|
|
92
|
-
|
|
93
|
-
type: 'tool_result',
|
|
94
|
-
tool_use_id: pending.block.id,
|
|
95
|
-
content: JSON.stringify({ error: `${pending.credential.label} not provided. Operation skipped.` }),
|
|
96
|
-
});
|
|
97
|
-
return await this._resumeAfterPause(chatId, user, pending);
|
|
207
|
+
return 'Credential skipped. You can provide it later.';
|
|
98
208
|
}
|
|
99
209
|
|
|
100
|
-
// Save the credential
|
|
101
210
|
saveCredential(this.config, pending.credential.envKey, value);
|
|
102
211
|
logger.info(`Saved credential: ${pending.credential.envKey}`);
|
|
212
|
+
return `Saved ${pending.credential.label}. You can now try the task again.`;
|
|
213
|
+
}
|
|
103
214
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
215
|
+
/** Set up listeners for job lifecycle events. */
|
|
216
|
+
_setupJobListeners() {
|
|
217
|
+
const logger = getLogger();
|
|
218
|
+
|
|
219
|
+
this.jobManager.on('job:completed', async (job) => {
|
|
220
|
+
const chatId = job.chatId;
|
|
221
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
222
|
+
const label = workerDef.label || job.workerType;
|
|
223
|
+
|
|
224
|
+
logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars`);
|
|
225
|
+
|
|
226
|
+
// 1. Store raw result in conversation history so orchestrator has full context
|
|
227
|
+
let resultText = job.result || 'Done.';
|
|
228
|
+
if (resultText.length > 3000) {
|
|
229
|
+
resultText = resultText.slice(0, 3000) + '\n\n... [result truncated]';
|
|
230
|
+
}
|
|
231
|
+
this.conversationManager.addMessage(chatId, 'user', `[Worker result: ${label} (${job.id}, ${job.duration}s)]\n\n${resultText}`);
|
|
111
232
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
233
|
+
// 2. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
|
|
234
|
+
const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
|
|
235
|
+
|
|
236
|
+
// 3. Try to summarize (provider timeout protects against hangs)
|
|
237
|
+
try {
|
|
238
|
+
const summary = await this._summarizeJobResult(chatId, job);
|
|
239
|
+
if (summary) {
|
|
240
|
+
this.conversationManager.addMessage(chatId, 'assistant', summary);
|
|
241
|
+
await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
|
|
245
|
+
await this._sendUpdate(chatId, `✅ ${label} finished (\`${job.id}\`, ${job.duration}s)! Ask me for the details.`, { editMessageId: notifyMsgId }).catch(() => {});
|
|
246
|
+
}
|
|
116
247
|
});
|
|
117
248
|
|
|
118
|
-
|
|
119
|
-
|
|
249
|
+
this.jobManager.on('job:failed', (job) => {
|
|
250
|
+
const chatId = job.chatId;
|
|
251
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
252
|
+
const label = workerDef.label || job.workerType;
|
|
120
253
|
|
|
121
|
-
|
|
122
|
-
const logger = getLogger();
|
|
123
|
-
const lower = userMessage.toLowerCase().trim();
|
|
254
|
+
logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
|
|
124
255
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
256
|
+
const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
|
|
257
|
+
this.conversationManager.addMessage(chatId, 'assistant', msg);
|
|
258
|
+
this._sendUpdate(chatId, msg);
|
|
259
|
+
});
|
|
128
260
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
} else {
|
|
135
|
-
logger.info(`User denied dangerous tool: ${pending.block.name}`);
|
|
136
|
-
pending.toolResults.push({
|
|
137
|
-
type: 'tool_result',
|
|
138
|
-
tool_use_id: pending.block.id,
|
|
139
|
-
content: JSON.stringify({ error: 'User denied this operation.' }),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
261
|
+
this.jobManager.on('job:cancelled', (job) => {
|
|
262
|
+
const chatId = job.chatId;
|
|
263
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
264
|
+
const label = workerDef.label || job.workerType;
|
|
142
265
|
|
|
143
|
-
|
|
144
|
-
}
|
|
266
|
+
logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
|
|
145
267
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
268
|
+
const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
|
|
269
|
+
this._sendUpdate(chatId, msg);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
150
272
|
|
|
151
|
-
|
|
152
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Auto-summarize a completed job result via the orchestrator LLM.
|
|
275
|
+
* The orchestrator reads the worker's raw result and presents a clean summary.
|
|
276
|
+
* Protected by the provider's built-in timeout (30s) — no manual Promise.race needed.
|
|
277
|
+
* Returns the summary text, or null. Caller handles delivery.
|
|
278
|
+
*/
|
|
279
|
+
async _summarizeJobResult(chatId, job) {
|
|
280
|
+
const logger = getLogger();
|
|
281
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
282
|
+
const label = workerDef.label || job.workerType;
|
|
283
|
+
|
|
284
|
+
logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
|
|
285
|
+
|
|
286
|
+
const history = this.conversationManager.getSummarizedHistory(chatId);
|
|
287
|
+
|
|
288
|
+
const response = await this.orchestratorProvider.chat({
|
|
289
|
+
system: this._getSystemPrompt(chatId, null),
|
|
290
|
+
messages: [
|
|
291
|
+
...history,
|
|
292
|
+
{
|
|
293
|
+
role: 'user',
|
|
294
|
+
content: `The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s). Present the results to the user in a clean, well-formatted way. Don't mention "worker" or technical job details — just present the findings naturally as if you did the work yourself.`,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
});
|
|
153
298
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
type: 'tool_result',
|
|
157
|
-
tool_use_id: block.id,
|
|
158
|
-
content: JSON.stringify(r),
|
|
159
|
-
});
|
|
160
|
-
}
|
|
299
|
+
const summary = response.text || '';
|
|
300
|
+
logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
|
|
161
301
|
|
|
162
|
-
|
|
163
|
-
const { max_tool_depth } = this.config.brain;
|
|
164
|
-
return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth);
|
|
302
|
+
return summary || null;
|
|
165
303
|
}
|
|
166
304
|
|
|
167
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Spawn a worker for a job — called from dispatch_task handler.
|
|
307
|
+
* Creates smart progress reporting via editable Telegram message.
|
|
308
|
+
*/
|
|
309
|
+
async _spawnWorker(job) {
|
|
168
310
|
const logger = getLogger();
|
|
311
|
+
const chatId = job.chatId;
|
|
312
|
+
const callbacks = this._chatCallbacks.get(chatId) || {};
|
|
313
|
+
const onUpdate = callbacks.onUpdate;
|
|
314
|
+
const sendPhoto = callbacks.sendPhoto;
|
|
315
|
+
|
|
316
|
+
logger.info(`[Orchestrator] Spawning worker for job ${job.id} [${job.workerType}] in chat ${chatId} — task: "${job.task.slice(0, 120)}"`);
|
|
317
|
+
|
|
318
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
319
|
+
const abortController = new AbortController();
|
|
320
|
+
|
|
321
|
+
// Smart progress: editable Telegram message (same pattern as coder.js)
|
|
322
|
+
let statusMsgId = null;
|
|
323
|
+
let activityLines = [];
|
|
324
|
+
let flushTimer = null;
|
|
325
|
+
const MAX_VISIBLE = 10;
|
|
326
|
+
|
|
327
|
+
const buildStatusText = (finalState = null) => {
|
|
328
|
+
const visible = activityLines.slice(-MAX_VISIBLE);
|
|
329
|
+
const countInfo = activityLines.length > MAX_VISIBLE
|
|
330
|
+
? `\n_... ${activityLines.length} operations total_\n`
|
|
331
|
+
: '';
|
|
332
|
+
const header = `${workerDef.emoji || '⚙️'} *${workerDef.label || job.workerType}* (\`${job.id}\`)`;
|
|
333
|
+
if (finalState === 'done') return `${header} — Done\n${countInfo}\n${visible.join('\n')}`;
|
|
334
|
+
if (finalState === 'error') return `${header} — Failed\n${countInfo}\n${visible.join('\n')}`;
|
|
335
|
+
if (finalState === 'cancelled') return `${header} — Cancelled\n${countInfo}\n${visible.join('\n')}`;
|
|
336
|
+
return `${header} — Working...\n${countInfo}\n${visible.join('\n')}`;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const flushStatus = async () => {
|
|
340
|
+
flushTimer = null;
|
|
341
|
+
if (!onUpdate || activityLines.length === 0) return;
|
|
342
|
+
try {
|
|
343
|
+
if (statusMsgId) {
|
|
344
|
+
await onUpdate(buildStatusText(), { editMessageId: statusMsgId });
|
|
345
|
+
} else {
|
|
346
|
+
statusMsgId = await onUpdate(buildStatusText());
|
|
347
|
+
job.statusMessageId = statusMsgId;
|
|
348
|
+
}
|
|
349
|
+
} catch {}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const addActivity = (line) => {
|
|
353
|
+
activityLines.push(line);
|
|
354
|
+
if (!statusMsgId && !flushTimer) {
|
|
355
|
+
flushStatus();
|
|
356
|
+
} else if (!flushTimer) {
|
|
357
|
+
flushTimer = setTimeout(flushStatus, 1000);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
169
360
|
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
this._pending.set(chatId, {
|
|
175
|
-
type: 'credential',
|
|
176
|
-
block,
|
|
177
|
-
credential: missing,
|
|
178
|
-
context: { config: this.config, user },
|
|
179
|
-
toolResults,
|
|
180
|
-
remainingBlocks,
|
|
181
|
-
messages,
|
|
182
|
-
});
|
|
183
|
-
return `🔑 **${missing.label}** is required for this action.\n\nPlease send your token now (it will be saved to \`~/.kernelbot/.env\`).\n\nOr reply **skip** to cancel.`;
|
|
184
|
-
}
|
|
361
|
+
// Get scoped tools and skill
|
|
362
|
+
const tools = getToolsForWorker(job.workerType);
|
|
363
|
+
const skillId = this.conversationManager.getSkill(chatId);
|
|
364
|
+
logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skill=${skillId || 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}`);
|
|
185
365
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
366
|
+
const worker = new WorkerAgent({
|
|
367
|
+
config: this.config,
|
|
368
|
+
workerType: job.workerType,
|
|
369
|
+
jobId: job.id,
|
|
370
|
+
tools,
|
|
371
|
+
skillId,
|
|
372
|
+
callbacks: {
|
|
373
|
+
onProgress: (text) => addActivity(text),
|
|
374
|
+
onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
|
|
375
|
+
onComplete: (result) => {
|
|
376
|
+
logger.info(`[Worker ${job.id}] Completed — result: "${(result || '').slice(0, 150)}"`);
|
|
377
|
+
// Final status message update
|
|
378
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
379
|
+
if (statusMsgId && onUpdate) {
|
|
380
|
+
onUpdate(buildStatusText('done'), { editMessageId: statusMsgId }).catch(() => {});
|
|
381
|
+
}
|
|
382
|
+
this.jobManager.completeJob(job.id, result);
|
|
383
|
+
},
|
|
384
|
+
onError: (err) => {
|
|
385
|
+
logger.error(`[Worker ${job.id}] Error — ${err.message || String(err)}`);
|
|
386
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
387
|
+
if (statusMsgId && onUpdate) {
|
|
388
|
+
onUpdate(buildStatusText('error'), { editMessageId: statusMsgId }).catch(() => {});
|
|
389
|
+
}
|
|
390
|
+
this.jobManager.failJob(job.id, err.message || String(err));
|
|
391
|
+
},
|
|
392
|
+
sendPhoto,
|
|
393
|
+
},
|
|
394
|
+
abortController,
|
|
395
|
+
});
|
|
200
396
|
|
|
201
|
-
|
|
397
|
+
// Store worker ref on job for cancellation
|
|
398
|
+
job.worker = worker;
|
|
399
|
+
|
|
400
|
+
// Start the job
|
|
401
|
+
this.jobManager.startJob(job.id);
|
|
402
|
+
|
|
403
|
+
// Fire and forget — return the promise so .catch() in orchestrator-tools works
|
|
404
|
+
return worker.run(job.task);
|
|
202
405
|
}
|
|
203
406
|
|
|
204
407
|
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
205
408
|
const logger = getLogger();
|
|
206
409
|
|
|
207
410
|
for (let depth = startDepth; depth < maxDepth; depth++) {
|
|
208
|
-
logger.
|
|
411
|
+
logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
|
|
209
412
|
|
|
210
|
-
const response = await this.
|
|
211
|
-
system: this.
|
|
413
|
+
const response = await this.orchestratorProvider.chat({
|
|
414
|
+
system: this._getSystemPrompt(chatId, user),
|
|
212
415
|
messages,
|
|
213
|
-
tools:
|
|
416
|
+
tools: orchestratorToolDefinitions,
|
|
214
417
|
});
|
|
215
418
|
|
|
419
|
+
logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
|
|
420
|
+
|
|
216
421
|
if (response.stopReason === 'end_turn') {
|
|
217
422
|
const reply = response.text || '';
|
|
423
|
+
logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
|
|
218
424
|
this.conversationManager.addMessage(chatId, 'assistant', reply);
|
|
219
425
|
return reply;
|
|
220
426
|
}
|
|
@@ -222,42 +428,32 @@ export class Agent {
|
|
|
222
428
|
if (response.stopReason === 'tool_use') {
|
|
223
429
|
messages.push({ role: 'assistant', content: response.rawContent });
|
|
224
430
|
|
|
225
|
-
// Send thinking text to the user
|
|
226
431
|
if (response.text && response.text.trim()) {
|
|
227
|
-
logger.info(`
|
|
228
|
-
await this._sendUpdate(`💭 ${response.text}`);
|
|
432
|
+
logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
|
|
229
433
|
}
|
|
230
434
|
|
|
231
435
|
const toolResults = [];
|
|
232
436
|
|
|
233
|
-
for (
|
|
234
|
-
const block = response.toolCalls[i];
|
|
235
|
-
|
|
236
|
-
// Build a block-like object for _checkPause (needs .type for remainingBlocks filter)
|
|
237
|
-
const blockObj = { type: 'tool_use', id: block.id, name: block.name, input: block.input };
|
|
238
|
-
|
|
239
|
-
// Check if we need to pause (missing cred or dangerous action)
|
|
240
|
-
const remaining = response.toolCalls.slice(i + 1).map((tc) => ({
|
|
241
|
-
type: 'tool_use', id: tc.id, name: tc.name, input: tc.input,
|
|
242
|
-
}));
|
|
243
|
-
const pauseMsg = this._checkPause(chatId, blockObj, user, toolResults, remaining, messages);
|
|
244
|
-
if (pauseMsg) return pauseMsg;
|
|
245
|
-
|
|
437
|
+
for (const block of response.toolCalls) {
|
|
246
438
|
const summary = this._formatToolSummary(block.name, block.input);
|
|
247
|
-
logger.info(`
|
|
248
|
-
|
|
439
|
+
logger.info(`[Orchestrator] Calling tool: ${block.name} — ${summary}`);
|
|
440
|
+
logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
|
|
441
|
+
await this._sendUpdate(chatId, `⚡ ${summary}`);
|
|
249
442
|
|
|
250
|
-
const result = await
|
|
443
|
+
const result = await executeOrchestratorTool(block.name, block.input, {
|
|
444
|
+
chatId,
|
|
445
|
+
jobManager: this.jobManager,
|
|
251
446
|
config: this.config,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
sendPhoto: this._sendPhoto,
|
|
447
|
+
spawnWorker: (job) => this._spawnWorker(job),
|
|
448
|
+
automationManager: this.automationManager,
|
|
255
449
|
});
|
|
256
450
|
|
|
451
|
+
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
452
|
+
|
|
257
453
|
toolResults.push({
|
|
258
454
|
type: 'tool_result',
|
|
259
455
|
tool_use_id: block.id,
|
|
260
|
-
content:
|
|
456
|
+
content: this._truncateResult(block.name, result),
|
|
261
457
|
});
|
|
262
458
|
}
|
|
263
459
|
|
|
@@ -266,7 +462,7 @@ export class Agent {
|
|
|
266
462
|
}
|
|
267
463
|
|
|
268
464
|
// Unexpected stop reason
|
|
269
|
-
logger.warn(`Unexpected stopReason: ${response.stopReason}`);
|
|
465
|
+
logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
|
|
270
466
|
if (response.text) {
|
|
271
467
|
this.conversationManager.addMessage(chatId, 'assistant', response.text);
|
|
272
468
|
return response.text;
|
|
@@ -274,10 +470,87 @@ export class Agent {
|
|
|
274
470
|
return 'Something went wrong — unexpected response from the model.';
|
|
275
471
|
}
|
|
276
472
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
`Please try again with a simpler request.`;
|
|
473
|
+
logger.warn(`[Orchestrator] Reached max depth (${maxDepth}) for chat ${chatId}`);
|
|
474
|
+
const depthWarning = `Reached maximum orchestrator depth (${maxDepth}).`;
|
|
280
475
|
this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
|
|
281
476
|
return depthWarning;
|
|
282
477
|
}
|
|
478
|
+
|
|
479
|
+
_formatToolSummary(name, input) {
|
|
480
|
+
switch (name) {
|
|
481
|
+
case 'dispatch_task': {
|
|
482
|
+
const workerDef = WORKER_TYPES[input.worker_type] || {};
|
|
483
|
+
return `Dispatching ${workerDef.emoji || '⚙️'} ${workerDef.label || input.worker_type}: ${(input.task || '').slice(0, 60)}`;
|
|
484
|
+
}
|
|
485
|
+
case 'list_jobs':
|
|
486
|
+
return 'Checking job status';
|
|
487
|
+
case 'cancel_job':
|
|
488
|
+
return `Cancelling job ${input.job_id}`;
|
|
489
|
+
case 'create_automation':
|
|
490
|
+
return `Creating automation: ${(input.name || '').slice(0, 40)}`;
|
|
491
|
+
case 'list_automations':
|
|
492
|
+
return 'Listing automations';
|
|
493
|
+
case 'update_automation':
|
|
494
|
+
return `Updating automation ${input.automation_id}`;
|
|
495
|
+
case 'delete_automation':
|
|
496
|
+
return `Deleting automation ${input.automation_id}`;
|
|
497
|
+
default:
|
|
498
|
+
return name;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Background persona extraction. */
|
|
503
|
+
async _extractPersonaBackground(userMessage, reply, user) {
|
|
504
|
+
const logger = getLogger();
|
|
505
|
+
|
|
506
|
+
if (!this.personaManager || !user?.id) return;
|
|
507
|
+
if (!userMessage || userMessage.trim().length < 3) return;
|
|
508
|
+
|
|
509
|
+
const currentPersona = this.personaManager.load(user.id, user.username);
|
|
510
|
+
|
|
511
|
+
const system = [
|
|
512
|
+
'You are a user-profile extractor. Analyze the user\'s message and extract any NEW personal information.',
|
|
513
|
+
'',
|
|
514
|
+
'Look for: name, location, timezone, language, technical skills, expertise level,',
|
|
515
|
+
'projects they\'re working on, tool/framework preferences, job title, role, company,',
|
|
516
|
+
'interests, hobbies, communication style, or any other personal details.',
|
|
517
|
+
'',
|
|
518
|
+
'RULES:',
|
|
519
|
+
'- Only extract FACTUAL information explicitly stated or strongly implied',
|
|
520
|
+
'- Do NOT infer personality traits from a single message',
|
|
521
|
+
'- Do NOT add information already in the profile',
|
|
522
|
+
'- If there IS new info, return the COMPLETE updated profile in the EXACT same markdown format',
|
|
523
|
+
'- If there is NO new info, respond with exactly: NONE',
|
|
524
|
+
].join('\n');
|
|
525
|
+
|
|
526
|
+
const userPrompt = [
|
|
527
|
+
'Current profile:',
|
|
528
|
+
'```',
|
|
529
|
+
currentPersona,
|
|
530
|
+
'```',
|
|
531
|
+
'',
|
|
532
|
+
`User's message: "${userMessage}"`,
|
|
533
|
+
'',
|
|
534
|
+
'Return the updated profile markdown or NONE.',
|
|
535
|
+
].join('\n');
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const response = await this.orchestratorProvider.chat({
|
|
539
|
+
system,
|
|
540
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
const text = (response.text || '').trim();
|
|
544
|
+
|
|
545
|
+
if (text && text !== 'NONE' && text.includes('# User Profile')) {
|
|
546
|
+
this.personaManager.save(user.id, text);
|
|
547
|
+
logger.info(`Auto-extracted persona update for user ${user.id} (${user.username})`);
|
|
548
|
+
}
|
|
549
|
+
} catch (err) {
|
|
550
|
+
logger.debug(`Persona extraction skipped: ${err.message}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
283
553
|
}
|
|
554
|
+
|
|
555
|
+
// Re-export as Agent for backward compatibility with bin/kernel.js import
|
|
556
|
+
export { OrchestratorAgent as Agent };
|