kernelbot 1.0.26 → 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 -124
- package/bin/kernel.js +201 -4
- package/package.json +1 -1
- package/src/agent.js +397 -222
- 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 +667 -21
- package/src/conversation.js +33 -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 +59 -6
- 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 +3 -3
- 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 +50 -15
- 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,23 +1,75 @@
|
|
|
1
1
|
import { createProvider, PROVIDERS } from './providers/index.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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';
|
|
5
9
|
import { getLogger } from './utils/logger.js';
|
|
6
10
|
import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
|
|
7
11
|
|
|
8
12
|
const MAX_RESULT_LENGTH = 3000;
|
|
9
13
|
const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
|
|
10
14
|
|
|
11
|
-
export class
|
|
12
|
-
constructor({ config, conversationManager }) {
|
|
15
|
+
export class OrchestratorAgent {
|
|
16
|
+
constructor({ config, conversationManager, personaManager, jobManager, automationManager }) {
|
|
13
17
|
this.config = config;
|
|
14
18
|
this.conversationManager = conversationManager;
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
19
|
+
this.personaManager = personaManager;
|
|
20
|
+
this.jobManager = jobManager;
|
|
21
|
+
this.automationManager = automationManager || null;
|
|
17
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);
|
|
18
65
|
}
|
|
19
66
|
|
|
20
|
-
|
|
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. */
|
|
21
73
|
getBrainInfo() {
|
|
22
74
|
const { provider, model } = this.config.brain;
|
|
23
75
|
const providerDef = PROVIDERS[provider];
|
|
@@ -27,64 +79,65 @@ export class Agent {
|
|
|
27
79
|
return { provider, providerName, model, modelLabel };
|
|
28
80
|
}
|
|
29
81
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
* Resolves the API key from process.env automatically.
|
|
33
|
-
* Returns null on success, or an error string if the key is missing.
|
|
34
|
-
*/
|
|
35
|
-
switchBrain(providerKey, modelId) {
|
|
82
|
+
/** Switch worker brain provider/model at runtime. */
|
|
83
|
+
async switchBrain(providerKey, modelId) {
|
|
36
84
|
const logger = getLogger();
|
|
37
85
|
const providerDef = PROVIDERS[providerKey];
|
|
38
86
|
if (!providerDef) return `Unknown provider: ${providerKey}`;
|
|
39
87
|
|
|
40
88
|
const envKey = providerDef.envKey;
|
|
41
89
|
const apiKey = process.env[envKey];
|
|
42
|
-
if (!apiKey)
|
|
43
|
-
|
|
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 };
|
|
44
108
|
}
|
|
45
|
-
|
|
46
|
-
this.config.brain.provider = providerKey;
|
|
47
|
-
this.config.brain.model = modelId;
|
|
48
|
-
this.config.brain.api_key = apiKey;
|
|
49
|
-
|
|
50
|
-
// Recreate the provider instance
|
|
51
|
-
this.provider = createProvider(this.config);
|
|
52
|
-
|
|
53
|
-
// Persist to config.yaml
|
|
54
|
-
saveProviderToYaml(providerKey, modelId);
|
|
55
|
-
|
|
56
|
-
logger.info(`Brain switched to ${providerDef.name} / ${modelId}`);
|
|
57
|
-
return null;
|
|
58
109
|
}
|
|
59
110
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
switchBrainWithKey(providerKey, modelId, apiKey) {
|
|
111
|
+
/** Finalize brain switch after API key was provided via chat. */
|
|
112
|
+
async switchBrainWithKey(providerKey, modelId, apiKey) {
|
|
64
113
|
const logger = getLogger();
|
|
65
114
|
const providerDef = PROVIDERS[providerKey];
|
|
66
115
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|
|
78
134
|
}
|
|
79
135
|
|
|
80
|
-
/**
|
|
81
|
-
* Truncate a tool result to stay within token budget.
|
|
82
|
-
*/
|
|
136
|
+
/** Truncate a tool result. */
|
|
83
137
|
_truncateResult(name, result) {
|
|
84
138
|
let str = JSON.stringify(result);
|
|
85
139
|
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
86
140
|
|
|
87
|
-
// Try truncating known large fields first
|
|
88
141
|
if (result && typeof result === 'object') {
|
|
89
142
|
const truncated = { ...result };
|
|
90
143
|
for (const field of LARGE_FIELDS) {
|
|
@@ -96,217 +149,278 @@ export class Agent {
|
|
|
96
149
|
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
97
150
|
}
|
|
98
151
|
|
|
99
|
-
// Hard truncate
|
|
100
152
|
return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
|
|
101
153
|
}
|
|
102
154
|
|
|
103
155
|
async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
|
|
104
156
|
const logger = getLogger();
|
|
105
157
|
|
|
106
|
-
|
|
107
|
-
|
|
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 });
|
|
108
162
|
|
|
109
163
|
// Handle pending responses (confirmation or credential)
|
|
110
164
|
const pending = this._pending.get(chatId);
|
|
111
165
|
if (pending) {
|
|
112
166
|
this._pending.delete(chatId);
|
|
167
|
+
logger.debug(`Orchestrator handling pending ${pending.type} response for chat ${chatId}`);
|
|
113
168
|
|
|
114
169
|
if (pending.type === 'credential') {
|
|
115
|
-
return await this._handleCredentialResponse(chatId, userMessage, user, pending);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (pending.type === 'confirmation') {
|
|
119
|
-
return await this._handleConfirmationResponse(chatId, userMessage, user, pending);
|
|
170
|
+
return await this._handleCredentialResponse(chatId, userMessage, user, pending, onUpdate);
|
|
120
171
|
}
|
|
121
172
|
}
|
|
122
173
|
|
|
123
|
-
const { max_tool_depth } = this.config.
|
|
174
|
+
const { max_tool_depth } = this.config.orchestrator;
|
|
124
175
|
|
|
125
176
|
// Add user message to persistent history
|
|
126
177
|
this.conversationManager.addMessage(chatId, 'user', userMessage);
|
|
127
178
|
|
|
128
179
|
// Build working messages from compressed history
|
|
129
180
|
const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
|
|
181
|
+
logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
|
|
130
182
|
|
|
131
|
-
|
|
132
|
-
const tools = selectToolsForMessage(userMessage, toolDefinitions);
|
|
133
|
-
logger.debug(`Selected ${tools.length}/${toolDefinitions.length} tools for message`);
|
|
183
|
+
const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
134
184
|
|
|
135
|
-
|
|
136
|
-
}
|
|
185
|
+
logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
|
|
137
186
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
write_file: 'path',
|
|
143
|
-
list_directory: 'path',
|
|
144
|
-
git_clone: 'repo',
|
|
145
|
-
git_checkout: 'branch',
|
|
146
|
-
git_commit: 'message',
|
|
147
|
-
git_push: 'dir',
|
|
148
|
-
git_diff: 'dir',
|
|
149
|
-
github_create_pr: 'title',
|
|
150
|
-
github_create_repo: 'name',
|
|
151
|
-
github_list_prs: 'repo',
|
|
152
|
-
github_get_pr_diff: 'repo',
|
|
153
|
-
github_post_review: 'repo',
|
|
154
|
-
spawn_claude_code: 'prompt',
|
|
155
|
-
kill_process: 'pid',
|
|
156
|
-
docker_exec: 'container',
|
|
157
|
-
docker_logs: 'container',
|
|
158
|
-
docker_compose: 'action',
|
|
159
|
-
curl_url: 'url',
|
|
160
|
-
check_port: 'port',
|
|
161
|
-
screenshot_website: 'url',
|
|
162
|
-
send_image: 'file_path',
|
|
163
|
-
browse_website: 'url',
|
|
164
|
-
extract_content: 'url',
|
|
165
|
-
interact_with_page: 'url',
|
|
166
|
-
}[name];
|
|
167
|
-
const val = key && input[key] ? String(input[key]).slice(0, 120) : JSON.stringify(input).slice(0, 120);
|
|
168
|
-
return `${name}: ${val}`;
|
|
187
|
+
// Background persona extraction
|
|
188
|
+
this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
|
|
189
|
+
|
|
190
|
+
return reply;
|
|
169
191
|
}
|
|
170
192
|
|
|
171
|
-
async _sendUpdate(text) {
|
|
172
|
-
|
|
173
|
-
|
|
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 {}
|
|
174
197
|
}
|
|
198
|
+
return null;
|
|
175
199
|
}
|
|
176
200
|
|
|
177
|
-
async _handleCredentialResponse(chatId, userMessage, user, pending) {
|
|
201
|
+
async _handleCredentialResponse(chatId, userMessage, user, pending, onUpdate) {
|
|
178
202
|
const logger = getLogger();
|
|
179
203
|
const value = userMessage.trim();
|
|
180
204
|
|
|
181
205
|
if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
|
|
182
206
|
logger.info(`User skipped credential: ${pending.credential.envKey}`);
|
|
183
|
-
|
|
184
|
-
type: 'tool_result',
|
|
185
|
-
tool_use_id: pending.block.id,
|
|
186
|
-
content: this._truncateResult(pending.block.name, { error: `${pending.credential.label} not provided. Operation skipped.` }),
|
|
187
|
-
});
|
|
188
|
-
return await this._resumeAfterPause(chatId, user, pending);
|
|
207
|
+
return 'Credential skipped. You can provide it later.';
|
|
189
208
|
}
|
|
190
209
|
|
|
191
|
-
// Save the credential
|
|
192
210
|
saveCredential(this.config, pending.credential.envKey, value);
|
|
193
211
|
logger.info(`Saved credential: ${pending.credential.envKey}`);
|
|
212
|
+
return `Saved ${pending.credential.label}. You can now try the task again.`;
|
|
213
|
+
}
|
|
194
214
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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`);
|
|
202
225
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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}`);
|
|
232
|
+
|
|
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
|
+
}
|
|
207
247
|
});
|
|
208
248
|
|
|
209
|
-
|
|
210
|
-
|
|
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;
|
|
211
253
|
|
|
212
|
-
|
|
213
|
-
const logger = getLogger();
|
|
214
|
-
const lower = userMessage.toLowerCase().trim();
|
|
254
|
+
logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
|
|
215
255
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
|
|
257
|
+
this.conversationManager.addMessage(chatId, 'assistant', msg);
|
|
258
|
+
this._sendUpdate(chatId, msg);
|
|
259
|
+
});
|
|
219
260
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
logger.info(`User denied dangerous tool: ${pending.block.name}`);
|
|
227
|
-
pending.toolResults.push({
|
|
228
|
-
type: 'tool_result',
|
|
229
|
-
tool_use_id: pending.block.id,
|
|
230
|
-
content: this._truncateResult(pending.block.name, { error: 'User denied this operation.' }),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
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;
|
|
265
|
+
|
|
266
|
+
logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
|
|
233
267
|
|
|
234
|
-
|
|
268
|
+
const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
|
|
269
|
+
this._sendUpdate(chatId, msg);
|
|
270
|
+
});
|
|
235
271
|
}
|
|
236
272
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
});
|
|
241
298
|
|
|
242
|
-
|
|
243
|
-
|
|
299
|
+
const summary = response.text || '';
|
|
300
|
+
logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
|
|
244
301
|
|
|
245
|
-
|
|
246
|
-
pending.toolResults.push({
|
|
247
|
-
type: 'tool_result',
|
|
248
|
-
tool_use_id: block.id,
|
|
249
|
-
content: this._truncateResult(block.name, r),
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
pending.messages.push({ role: 'user', content: pending.toolResults });
|
|
254
|
-
const { max_tool_depth } = this.config.brain;
|
|
255
|
-
return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth, pending.tools || toolDefinitions);
|
|
302
|
+
return summary || null;
|
|
256
303
|
}
|
|
257
304
|
|
|
258
|
-
|
|
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) {
|
|
259
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
|
+
};
|
|
260
360
|
|
|
261
|
-
//
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
this._pending.set(chatId, {
|
|
266
|
-
type: 'credential',
|
|
267
|
-
block,
|
|
268
|
-
credential: missing,
|
|
269
|
-
context: { config: this.config, user },
|
|
270
|
-
toolResults,
|
|
271
|
-
remainingBlocks,
|
|
272
|
-
messages,
|
|
273
|
-
});
|
|
274
|
-
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.`;
|
|
275
|
-
}
|
|
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}`);
|
|
276
365
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
});
|
|
291
396
|
|
|
292
|
-
|
|
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);
|
|
293
405
|
}
|
|
294
406
|
|
|
295
|
-
async _runLoop(chatId, messages, user, startDepth, maxDepth
|
|
407
|
+
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
296
408
|
const logger = getLogger();
|
|
297
|
-
let currentTools = tools || toolDefinitions;
|
|
298
409
|
|
|
299
410
|
for (let depth = startDepth; depth < maxDepth; depth++) {
|
|
300
|
-
logger.
|
|
411
|
+
logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
|
|
301
412
|
|
|
302
|
-
const response = await this.
|
|
303
|
-
system: this.
|
|
413
|
+
const response = await this.orchestratorProvider.chat({
|
|
414
|
+
system: this._getSystemPrompt(chatId, user),
|
|
304
415
|
messages,
|
|
305
|
-
tools:
|
|
416
|
+
tools: orchestratorToolDefinitions,
|
|
306
417
|
});
|
|
307
418
|
|
|
419
|
+
logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
|
|
420
|
+
|
|
308
421
|
if (response.stopReason === 'end_turn') {
|
|
309
422
|
const reply = response.text || '';
|
|
423
|
+
logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
|
|
310
424
|
this.conversationManager.addMessage(chatId, 'assistant', reply);
|
|
311
425
|
return reply;
|
|
312
426
|
}
|
|
@@ -314,40 +428,27 @@ export class Agent {
|
|
|
314
428
|
if (response.stopReason === 'tool_use') {
|
|
315
429
|
messages.push({ role: 'assistant', content: response.rawContent });
|
|
316
430
|
|
|
317
|
-
// Send thinking text to the user
|
|
318
431
|
if (response.text && response.text.trim()) {
|
|
319
|
-
logger.info(`
|
|
320
|
-
await this._sendUpdate(`💭 ${response.text}`);
|
|
432
|
+
logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
|
|
321
433
|
}
|
|
322
434
|
|
|
323
435
|
const toolResults = [];
|
|
324
|
-
const usedToolNames = [];
|
|
325
|
-
|
|
326
|
-
for (let i = 0; i < response.toolCalls.length; i++) {
|
|
327
|
-
const block = response.toolCalls[i];
|
|
328
|
-
|
|
329
|
-
// Build a block-like object for _checkPause (needs .type for remainingBlocks filter)
|
|
330
|
-
const blockObj = { type: 'tool_use', id: block.id, name: block.name, input: block.input };
|
|
331
|
-
|
|
332
|
-
// Check if we need to pause (missing cred or dangerous action)
|
|
333
|
-
const remaining = response.toolCalls.slice(i + 1).map((tc) => ({
|
|
334
|
-
type: 'tool_use', id: tc.id, name: tc.name, input: tc.input,
|
|
335
|
-
}));
|
|
336
|
-
const pauseMsg = this._checkPause(chatId, blockObj, user, toolResults, remaining, messages);
|
|
337
|
-
if (pauseMsg) return pauseMsg;
|
|
338
436
|
|
|
437
|
+
for (const block of response.toolCalls) {
|
|
339
438
|
const summary = this._formatToolSummary(block.name, block.input);
|
|
340
|
-
logger.info(`
|
|
341
|
-
|
|
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}`);
|
|
342
442
|
|
|
343
|
-
const result = await
|
|
443
|
+
const result = await executeOrchestratorTool(block.name, block.input, {
|
|
444
|
+
chatId,
|
|
445
|
+
jobManager: this.jobManager,
|
|
344
446
|
config: this.config,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
sendPhoto: this._sendPhoto,
|
|
447
|
+
spawnWorker: (job) => this._spawnWorker(job),
|
|
448
|
+
automationManager: this.automationManager,
|
|
348
449
|
});
|
|
349
450
|
|
|
350
|
-
|
|
451
|
+
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
351
452
|
|
|
352
453
|
toolResults.push({
|
|
353
454
|
type: 'tool_result',
|
|
@@ -356,15 +457,12 @@ export class Agent {
|
|
|
356
457
|
});
|
|
357
458
|
}
|
|
358
459
|
|
|
359
|
-
// Expand tools based on what was actually used
|
|
360
|
-
currentTools = expandToolsForUsed(usedToolNames, currentTools, toolDefinitions);
|
|
361
|
-
|
|
362
460
|
messages.push({ role: 'user', content: toolResults });
|
|
363
461
|
continue;
|
|
364
462
|
}
|
|
365
463
|
|
|
366
464
|
// Unexpected stop reason
|
|
367
|
-
logger.warn(`Unexpected stopReason: ${response.stopReason}`);
|
|
465
|
+
logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
|
|
368
466
|
if (response.text) {
|
|
369
467
|
this.conversationManager.addMessage(chatId, 'assistant', response.text);
|
|
370
468
|
return response.text;
|
|
@@ -372,10 +470,87 @@ export class Agent {
|
|
|
372
470
|
return 'Something went wrong — unexpected response from the model.';
|
|
373
471
|
}
|
|
374
472
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
`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}).`;
|
|
378
475
|
this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
|
|
379
476
|
return depthWarning;
|
|
380
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
|
+
}
|
|
381
553
|
}
|
|
554
|
+
|
|
555
|
+
// Re-export as Agent for backward compatibility with bin/kernel.js import
|
|
556
|
+
export { OrchestratorAgent as Agent };
|