kernelbot 1.0.26 → 1.0.30
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/.env.example +4 -0
- package/README.md +198 -124
- package/bin/kernel.js +208 -4
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +839 -209
- 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 +1001 -18
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- 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 +124 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +59 -6
- package/src/prompts/workers.js +148 -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/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +7 -2
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +216 -0
- package/src/swarm/job.js +85 -0
- package/src/swarm/worker-registry.js +79 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +3 -3
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +3 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +428 -0
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +32 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +153 -15
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/worker.js +396 -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,85 @@
|
|
|
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
|
-
import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
|
|
10
|
+
import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestratorToYaml, saveClaudeCodeModelToYaml, saveClaudeCodeAuth } from './utils/config.js';
|
|
11
|
+
import { resetClaudeCodeSpawner } from './tools/coding.js';
|
|
7
12
|
|
|
8
13
|
const MAX_RESULT_LENGTH = 3000;
|
|
9
14
|
const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
|
|
10
15
|
|
|
11
|
-
export class
|
|
12
|
-
constructor({ config, conversationManager }) {
|
|
16
|
+
export class OrchestratorAgent {
|
|
17
|
+
constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager }) {
|
|
13
18
|
this.config = config;
|
|
14
19
|
this.conversationManager = conversationManager;
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
20
|
+
this.personaManager = personaManager;
|
|
21
|
+
this.selfManager = selfManager || null;
|
|
22
|
+
this.jobManager = jobManager;
|
|
23
|
+
this.automationManager = automationManager || null;
|
|
17
24
|
this._pending = new Map(); // chatId -> pending state
|
|
25
|
+
this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
|
|
26
|
+
|
|
27
|
+
// Orchestrator provider (30s timeout — lean dispatch/summarize calls)
|
|
28
|
+
const orchProviderKey = config.orchestrator.provider || 'anthropic';
|
|
29
|
+
const orchProviderDef = PROVIDERS[orchProviderKey];
|
|
30
|
+
const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]) || process.env.ANTHROPIC_API_KEY;
|
|
31
|
+
this.orchestratorProvider = createProvider({
|
|
32
|
+
brain: {
|
|
33
|
+
provider: orchProviderKey,
|
|
34
|
+
model: config.orchestrator.model,
|
|
35
|
+
max_tokens: config.orchestrator.max_tokens,
|
|
36
|
+
temperature: config.orchestrator.temperature,
|
|
37
|
+
api_key: orchApiKey,
|
|
38
|
+
timeout: 30_000,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Worker provider uses user's chosen brain
|
|
43
|
+
this.workerProvider = createProvider(config);
|
|
44
|
+
|
|
45
|
+
// Set up job lifecycle event listeners
|
|
46
|
+
this._setupJobListeners();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Build the orchestrator system prompt. */
|
|
50
|
+
_getSystemPrompt(chatId, user) {
|
|
51
|
+
const logger = getLogger();
|
|
52
|
+
const skillId = this.conversationManager.getSkill(chatId);
|
|
53
|
+
const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
|
|
54
|
+
|
|
55
|
+
let userPersona = null;
|
|
56
|
+
if (this.personaManager && user?.id) {
|
|
57
|
+
userPersona = this.personaManager.load(user.id, user.username);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let selfData = null;
|
|
61
|
+
if (this.selfManager) {
|
|
62
|
+
selfData = this.selfManager.loadAll();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'}`);
|
|
66
|
+
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData);
|
|
18
67
|
}
|
|
19
68
|
|
|
20
|
-
|
|
69
|
+
setSkill(chatId, skillId) {
|
|
70
|
+
this.conversationManager.setSkill(chatId, skillId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
clearSkill(chatId) {
|
|
74
|
+
this.conversationManager.clearSkill(chatId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getActiveSkill(chatId) {
|
|
78
|
+
const skillId = this.conversationManager.getSkill(chatId);
|
|
79
|
+
return skillId ? getUnifiedSkillById(skillId) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Return current worker brain info for display. */
|
|
21
83
|
getBrainInfo() {
|
|
22
84
|
const { provider, model } = this.config.brain;
|
|
23
85
|
const providerDef = PROVIDERS[provider];
|
|
@@ -27,64 +89,190 @@ export class Agent {
|
|
|
27
89
|
return { provider, providerName, model, modelLabel };
|
|
28
90
|
}
|
|
29
91
|
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
92
|
+
/** Return current orchestrator info for display. */
|
|
93
|
+
getOrchestratorInfo() {
|
|
94
|
+
const provider = this.config.orchestrator.provider || 'anthropic';
|
|
95
|
+
const model = this.config.orchestrator.model;
|
|
96
|
+
const providerDef = PROVIDERS[provider];
|
|
97
|
+
const providerName = providerDef ? providerDef.name : provider;
|
|
98
|
+
const modelEntry = providerDef?.models.find((m) => m.id === model);
|
|
99
|
+
const modelLabel = modelEntry ? modelEntry.label : model;
|
|
100
|
+
return { provider, providerName, model, modelLabel };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Switch orchestrator provider/model at runtime. */
|
|
104
|
+
async switchOrchestrator(providerKey, modelId) {
|
|
36
105
|
const logger = getLogger();
|
|
37
106
|
const providerDef = PROVIDERS[providerKey];
|
|
38
107
|
if (!providerDef) return `Unknown provider: ${providerKey}`;
|
|
39
108
|
|
|
40
109
|
const envKey = providerDef.envKey;
|
|
41
110
|
const apiKey = process.env[envKey];
|
|
42
|
-
if (!apiKey)
|
|
43
|
-
|
|
111
|
+
if (!apiKey) return envKey;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const testProvider = createProvider({
|
|
115
|
+
brain: {
|
|
116
|
+
provider: providerKey,
|
|
117
|
+
model: modelId,
|
|
118
|
+
max_tokens: this.config.orchestrator.max_tokens,
|
|
119
|
+
temperature: this.config.orchestrator.temperature,
|
|
120
|
+
api_key: apiKey,
|
|
121
|
+
timeout: 30_000,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
await testProvider.ping();
|
|
125
|
+
|
|
126
|
+
this.config.orchestrator.provider = providerKey;
|
|
127
|
+
this.config.orchestrator.model = modelId;
|
|
128
|
+
this.config.orchestrator.api_key = apiKey;
|
|
129
|
+
this.orchestratorProvider = testProvider;
|
|
130
|
+
saveOrchestratorToYaml(providerKey, modelId);
|
|
131
|
+
|
|
132
|
+
logger.info(`Orchestrator switched to ${providerDef.name} / ${modelId}`);
|
|
133
|
+
return null;
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger.error(`Orchestrator switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
136
|
+
return { error: err.message };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Finalize orchestrator switch after API key was provided via chat. */
|
|
141
|
+
async switchOrchestratorWithKey(providerKey, modelId, apiKey) {
|
|
142
|
+
const logger = getLogger();
|
|
143
|
+
const providerDef = PROVIDERS[providerKey];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const testProvider = createProvider({
|
|
147
|
+
brain: {
|
|
148
|
+
provider: providerKey,
|
|
149
|
+
model: modelId,
|
|
150
|
+
max_tokens: this.config.orchestrator.max_tokens,
|
|
151
|
+
temperature: this.config.orchestrator.temperature,
|
|
152
|
+
api_key: apiKey,
|
|
153
|
+
timeout: 30_000,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
await testProvider.ping();
|
|
157
|
+
|
|
158
|
+
saveCredential(this.config, providerDef.envKey, apiKey);
|
|
159
|
+
this.config.orchestrator.provider = providerKey;
|
|
160
|
+
this.config.orchestrator.model = modelId;
|
|
161
|
+
this.config.orchestrator.api_key = apiKey;
|
|
162
|
+
this.orchestratorProvider = testProvider;
|
|
163
|
+
saveOrchestratorToYaml(providerKey, modelId);
|
|
164
|
+
|
|
165
|
+
logger.info(`Orchestrator switched to ${providerDef.name} / ${modelId} (new key saved)`);
|
|
166
|
+
return null;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
logger.error(`Orchestrator switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
169
|
+
return { error: err.message };
|
|
44
170
|
}
|
|
171
|
+
}
|
|
45
172
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.config.
|
|
173
|
+
/** Return current Claude Code model info for display. */
|
|
174
|
+
getClaudeCodeInfo() {
|
|
175
|
+
const model = this.config.claude_code?.model || 'claude-opus-4-6';
|
|
176
|
+
const providerDef = PROVIDERS.anthropic;
|
|
177
|
+
const modelEntry = providerDef?.models.find((m) => m.id === model);
|
|
178
|
+
const modelLabel = modelEntry ? modelEntry.label : model;
|
|
179
|
+
return { model, modelLabel };
|
|
180
|
+
}
|
|
49
181
|
|
|
50
|
-
|
|
51
|
-
|
|
182
|
+
/** Switch Claude Code model at runtime. */
|
|
183
|
+
switchClaudeCodeModel(modelId) {
|
|
184
|
+
const logger = getLogger();
|
|
185
|
+
this.config.claude_code.model = modelId;
|
|
186
|
+
saveClaudeCodeModelToYaml(modelId);
|
|
187
|
+
resetClaudeCodeSpawner();
|
|
188
|
+
logger.info(`Claude Code model switched to ${modelId}`);
|
|
189
|
+
}
|
|
52
190
|
|
|
53
|
-
|
|
54
|
-
|
|
191
|
+
/** Return current Claude Code auth config for display. */
|
|
192
|
+
getClaudeAuthConfig() {
|
|
193
|
+
const mode = this.config.claude_code?.auth_mode || 'system';
|
|
194
|
+
const info = { mode };
|
|
195
|
+
|
|
196
|
+
if (mode === 'api_key') {
|
|
197
|
+
const key = this.config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY || '';
|
|
198
|
+
info.credential = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : '(not set)';
|
|
199
|
+
} else if (mode === 'oauth_token') {
|
|
200
|
+
const token = this.config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
|
|
201
|
+
info.credential = token ? `${token.slice(0, 8)}...${token.slice(-4)}` : '(not set)';
|
|
202
|
+
} else {
|
|
203
|
+
info.credential = 'Using host system login';
|
|
204
|
+
}
|
|
55
205
|
|
|
56
|
-
|
|
57
|
-
return null;
|
|
206
|
+
return info;
|
|
58
207
|
}
|
|
59
208
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
switchBrainWithKey(providerKey, modelId, apiKey) {
|
|
209
|
+
/** Set Claude Code auth mode + credential at runtime. */
|
|
210
|
+
setClaudeCodeAuth(mode, value) {
|
|
64
211
|
const logger = getLogger();
|
|
65
|
-
|
|
212
|
+
saveClaudeCodeAuth(this.config, mode, value);
|
|
213
|
+
resetClaudeCodeSpawner();
|
|
214
|
+
logger.info(`Claude Code auth mode set to: ${mode}`);
|
|
215
|
+
}
|
|
66
216
|
|
|
67
|
-
|
|
68
|
-
|
|
217
|
+
/** Switch worker brain provider/model at runtime. */
|
|
218
|
+
async switchBrain(providerKey, modelId) {
|
|
219
|
+
const logger = getLogger();
|
|
220
|
+
const providerDef = PROVIDERS[providerKey];
|
|
221
|
+
if (!providerDef) return `Unknown provider: ${providerKey}`;
|
|
69
222
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
223
|
+
const envKey = providerDef.envKey;
|
|
224
|
+
const apiKey = process.env[envKey];
|
|
225
|
+
if (!apiKey) return envKey;
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
|
|
229
|
+
const testProvider = createProvider(testConfig);
|
|
230
|
+
await testProvider.ping();
|
|
231
|
+
|
|
232
|
+
this.config.brain.provider = providerKey;
|
|
233
|
+
this.config.brain.model = modelId;
|
|
234
|
+
this.config.brain.api_key = apiKey;
|
|
235
|
+
this.workerProvider = testProvider;
|
|
236
|
+
saveProviderToYaml(providerKey, modelId);
|
|
237
|
+
|
|
238
|
+
logger.info(`Worker brain switched to ${providerDef.name} / ${modelId}`);
|
|
239
|
+
return null;
|
|
240
|
+
} catch (err) {
|
|
241
|
+
logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
242
|
+
return { error: err.message };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
73
245
|
|
|
74
|
-
|
|
75
|
-
|
|
246
|
+
/** Finalize brain switch after API key was provided via chat. */
|
|
247
|
+
async switchBrainWithKey(providerKey, modelId, apiKey) {
|
|
248
|
+
const logger = getLogger();
|
|
249
|
+
const providerDef = PROVIDERS[providerKey];
|
|
76
250
|
|
|
77
|
-
|
|
251
|
+
try {
|
|
252
|
+
const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
|
|
253
|
+
const testProvider = createProvider(testConfig);
|
|
254
|
+
await testProvider.ping();
|
|
255
|
+
|
|
256
|
+
saveCredential(this.config, providerDef.envKey, apiKey);
|
|
257
|
+
this.config.brain.provider = providerKey;
|
|
258
|
+
this.config.brain.model = modelId;
|
|
259
|
+
this.config.brain.api_key = apiKey;
|
|
260
|
+
this.workerProvider = testProvider;
|
|
261
|
+
saveProviderToYaml(providerKey, modelId);
|
|
262
|
+
|
|
263
|
+
logger.info(`Worker brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
|
|
264
|
+
return null;
|
|
265
|
+
} catch (err) {
|
|
266
|
+
logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
267
|
+
return { error: err.message };
|
|
268
|
+
}
|
|
78
269
|
}
|
|
79
270
|
|
|
80
|
-
/**
|
|
81
|
-
* Truncate a tool result to stay within token budget.
|
|
82
|
-
*/
|
|
271
|
+
/** Truncate a tool result. */
|
|
83
272
|
_truncateResult(name, result) {
|
|
84
273
|
let str = JSON.stringify(result);
|
|
85
274
|
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
86
275
|
|
|
87
|
-
// Try truncating known large fields first
|
|
88
276
|
if (result && typeof result === 'object') {
|
|
89
277
|
const truncated = { ...result };
|
|
90
278
|
for (const field of LARGE_FIELDS) {
|
|
@@ -96,217 +284,524 @@ export class Agent {
|
|
|
96
284
|
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
97
285
|
}
|
|
98
286
|
|
|
99
|
-
// Hard truncate
|
|
100
287
|
return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
|
|
101
288
|
}
|
|
102
289
|
|
|
103
290
|
async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
|
|
104
291
|
const logger = getLogger();
|
|
105
292
|
|
|
106
|
-
|
|
107
|
-
|
|
293
|
+
logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
|
|
294
|
+
|
|
295
|
+
// Store callbacks so workers can use them later
|
|
296
|
+
this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
|
|
108
297
|
|
|
109
298
|
// Handle pending responses (confirmation or credential)
|
|
110
299
|
const pending = this._pending.get(chatId);
|
|
111
300
|
if (pending) {
|
|
112
301
|
this._pending.delete(chatId);
|
|
302
|
+
logger.debug(`Orchestrator handling pending ${pending.type} response for chat ${chatId}`);
|
|
113
303
|
|
|
114
304
|
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);
|
|
305
|
+
return await this._handleCredentialResponse(chatId, userMessage, user, pending, onUpdate);
|
|
120
306
|
}
|
|
121
307
|
}
|
|
122
308
|
|
|
123
|
-
const { max_tool_depth } = this.config.
|
|
309
|
+
const { max_tool_depth } = this.config.orchestrator;
|
|
124
310
|
|
|
125
311
|
// Add user message to persistent history
|
|
126
312
|
this.conversationManager.addMessage(chatId, 'user', userMessage);
|
|
127
313
|
|
|
128
314
|
// Build working messages from compressed history
|
|
129
315
|
const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
|
|
316
|
+
logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
|
|
130
317
|
|
|
131
|
-
|
|
132
|
-
const tools = selectToolsForMessage(userMessage, toolDefinitions);
|
|
133
|
-
logger.debug(`Selected ${tools.length}/${toolDefinitions.length} tools for message`);
|
|
318
|
+
const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
|
|
134
319
|
|
|
135
|
-
|
|
136
|
-
}
|
|
320
|
+
logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
|
|
137
321
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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}`;
|
|
322
|
+
// Background persona extraction + self-reflection
|
|
323
|
+
this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
|
|
324
|
+
this._reflectOnSelfBackground(userMessage, reply, user).catch(() => {});
|
|
325
|
+
|
|
326
|
+
return reply;
|
|
169
327
|
}
|
|
170
328
|
|
|
171
|
-
async _sendUpdate(text) {
|
|
172
|
-
|
|
173
|
-
|
|
329
|
+
async _sendUpdate(chatId, text, opts) {
|
|
330
|
+
const callbacks = this._chatCallbacks.get(chatId);
|
|
331
|
+
if (callbacks?.onUpdate) {
|
|
332
|
+
try { return await callbacks.onUpdate(text, opts); } catch {}
|
|
174
333
|
}
|
|
334
|
+
return null;
|
|
175
335
|
}
|
|
176
336
|
|
|
177
|
-
async _handleCredentialResponse(chatId, userMessage, user, pending) {
|
|
337
|
+
async _handleCredentialResponse(chatId, userMessage, user, pending, onUpdate) {
|
|
178
338
|
const logger = getLogger();
|
|
179
339
|
const value = userMessage.trim();
|
|
180
340
|
|
|
181
341
|
if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
|
|
182
342
|
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);
|
|
343
|
+
return 'Credential skipped. You can provide it later.';
|
|
189
344
|
}
|
|
190
345
|
|
|
191
|
-
// Save the credential
|
|
192
346
|
saveCredential(this.config, pending.credential.envKey, value);
|
|
193
347
|
logger.info(`Saved credential: ${pending.credential.envKey}`);
|
|
348
|
+
return `Saved ${pending.credential.label}. You can now try the task again.`;
|
|
349
|
+
}
|
|
194
350
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
351
|
+
/** Set up listeners for job lifecycle events. */
|
|
352
|
+
_setupJobListeners() {
|
|
353
|
+
const logger = getLogger();
|
|
354
|
+
|
|
355
|
+
this.jobManager.on('job:completed', async (job) => {
|
|
356
|
+
const chatId = job.chatId;
|
|
357
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
358
|
+
const label = workerDef.label || job.workerType;
|
|
359
|
+
|
|
360
|
+
logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars, structured: ${!!job.structuredResult}`);
|
|
361
|
+
|
|
362
|
+
// 1. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
|
|
363
|
+
const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
|
|
364
|
+
|
|
365
|
+
// 2. Try to summarize, then store ONE message in history (summary or fallback — not both)
|
|
366
|
+
try {
|
|
367
|
+
const summary = await this._summarizeJobResult(chatId, job);
|
|
368
|
+
if (summary) {
|
|
369
|
+
this.conversationManager.addMessage(chatId, 'assistant', summary);
|
|
370
|
+
await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
|
|
371
|
+
} else {
|
|
372
|
+
// Summary was null (short result) — store the fallback
|
|
373
|
+
const fallback = this._buildSummaryFallback(job, label);
|
|
374
|
+
this.conversationManager.addMessage(chatId, 'assistant', fallback);
|
|
375
|
+
await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId }).catch(() => {});
|
|
376
|
+
}
|
|
377
|
+
} catch (err) {
|
|
378
|
+
logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
|
|
379
|
+
// Store the fallback so the orchestrator retains context about what happened
|
|
380
|
+
const fallback = this._buildSummaryFallback(job, label);
|
|
381
|
+
this.conversationManager.addMessage(chatId, 'assistant', fallback);
|
|
382
|
+
await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId }).catch(() => {});
|
|
383
|
+
}
|
|
201
384
|
});
|
|
202
385
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
386
|
+
// Handle jobs whose dependencies are now met
|
|
387
|
+
this.jobManager.on('job:ready', async (job) => {
|
|
388
|
+
const chatId = job.chatId;
|
|
389
|
+
logger.info(`[Orchestrator] Job ready event: ${job.id} [${job.workerType}] — dependencies met, spawning worker`);
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await this._spawnWorker(job);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
logger.error(`[Orchestrator] Failed to spawn ready job ${job.id}: ${err.message}`);
|
|
395
|
+
if (!job.isTerminal) {
|
|
396
|
+
this.jobManager.failJob(job.id, err.message);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
this.jobManager.on('job:failed', (job) => {
|
|
402
|
+
const chatId = job.chatId;
|
|
403
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
404
|
+
const label = workerDef.label || job.workerType;
|
|
405
|
+
|
|
406
|
+
logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
|
|
407
|
+
|
|
408
|
+
const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
|
|
409
|
+
this.conversationManager.addMessage(chatId, 'assistant', msg);
|
|
410
|
+
this._sendUpdate(chatId, msg);
|
|
207
411
|
});
|
|
208
412
|
|
|
209
|
-
|
|
413
|
+
this.jobManager.on('job:cancelled', (job) => {
|
|
414
|
+
const chatId = job.chatId;
|
|
415
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
416
|
+
const label = workerDef.label || job.workerType;
|
|
417
|
+
|
|
418
|
+
logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
|
|
419
|
+
|
|
420
|
+
const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
|
|
421
|
+
this._sendUpdate(chatId, msg);
|
|
422
|
+
});
|
|
210
423
|
}
|
|
211
424
|
|
|
212
|
-
|
|
425
|
+
/**
|
|
426
|
+
* Auto-summarize a completed job result via the orchestrator LLM.
|
|
427
|
+
* Uses structured data for focused summarization when available.
|
|
428
|
+
* Short results (<500 chars) skip the LLM call entirely.
|
|
429
|
+
* Protected by the provider's built-in timeout (30s).
|
|
430
|
+
* Returns the summary text, or null. Caller handles delivery.
|
|
431
|
+
*/
|
|
432
|
+
async _summarizeJobResult(chatId, job) {
|
|
213
433
|
const logger = getLogger();
|
|
214
|
-
const
|
|
434
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
435
|
+
const label = workerDef.label || job.workerType;
|
|
215
436
|
|
|
216
|
-
|
|
217
|
-
logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
|
|
218
|
-
const result = await executeTool(pending.block.name, pending.block.input, { ...pending.context, onUpdate: this._onUpdate, sendPhoto: this._sendPhoto });
|
|
437
|
+
logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
|
|
219
438
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
});
|
|
439
|
+
// Short results don't need LLM summarization
|
|
440
|
+
const sr = job.structuredResult;
|
|
441
|
+
const resultLen = (job.result || '').length;
|
|
442
|
+
if (sr?.structured && resultLen < 500) {
|
|
443
|
+
logger.info(`[Orchestrator] Job ${job.id} result short enough — skipping LLM summary`);
|
|
444
|
+
return this._buildSummaryFallback(job, label);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Build a focused prompt using structured data if available
|
|
448
|
+
let resultContext;
|
|
449
|
+
if (sr?.structured) {
|
|
450
|
+
const parts = [`Summary: ${sr.summary}`, `Status: ${sr.status}`];
|
|
451
|
+
if (sr.artifacts?.length > 0) {
|
|
452
|
+
parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path}`).join(', ')}`);
|
|
453
|
+
}
|
|
454
|
+
if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
|
|
455
|
+
// Include details up to 8000 chars
|
|
456
|
+
if (sr.details) parts.push(`Details:\n${sr.details.slice(0, 8000)}`);
|
|
457
|
+
resultContext = parts.join('\n');
|
|
225
458
|
} else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
459
|
+
resultContext = (job.result || 'Done.').slice(0, 8000);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const history = this.conversationManager.getSummarizedHistory(chatId);
|
|
463
|
+
|
|
464
|
+
const response = await this.orchestratorProvider.chat({
|
|
465
|
+
system: this._getSystemPrompt(chatId, null),
|
|
466
|
+
messages: [
|
|
467
|
+
...history,
|
|
468
|
+
{
|
|
469
|
+
role: 'user',
|
|
470
|
+
content: `The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s). Here are the results:\n\n${resultContext}\n\nPresent these 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.`,
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const summary = response.text || '';
|
|
476
|
+
logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
|
|
477
|
+
|
|
478
|
+
return summary || null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Build a compact history entry for a completed job result.
|
|
483
|
+
* Stored as role: 'assistant' (not fake 'user') with up to 6000 chars of detail.
|
|
484
|
+
*/
|
|
485
|
+
_buildResultHistoryEntry(job) {
|
|
486
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
487
|
+
const label = workerDef.label || job.workerType;
|
|
488
|
+
const sr = job.structuredResult;
|
|
489
|
+
|
|
490
|
+
const parts = [`[${label} result — job ${job.id}, ${job.duration}s]`];
|
|
491
|
+
|
|
492
|
+
if (sr?.structured) {
|
|
493
|
+
parts.push(`Summary: ${sr.summary}`);
|
|
494
|
+
parts.push(`Status: ${sr.status}`);
|
|
495
|
+
if (sr.artifacts?.length > 0) {
|
|
496
|
+
const artifactLines = sr.artifacts.map(a => `- ${a.title || a.type}: ${a.url || a.path || ''}`);
|
|
497
|
+
parts.push(`Artifacts:\n${artifactLines.join('\n')}`);
|
|
498
|
+
}
|
|
499
|
+
if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
|
|
500
|
+
if (sr.details) {
|
|
501
|
+
const details = sr.details.length > 6000
|
|
502
|
+
? sr.details.slice(0, 6000) + '\n... [details truncated]'
|
|
503
|
+
: sr.details;
|
|
504
|
+
parts.push(`Details:\n${details}`);
|
|
505
|
+
}
|
|
506
|
+
} else {
|
|
507
|
+
// Raw text result
|
|
508
|
+
const resultText = job.result || 'Done.';
|
|
509
|
+
if (resultText.length > 6000) {
|
|
510
|
+
parts.push(resultText.slice(0, 6000) + '\n... [result truncated]');
|
|
511
|
+
} else {
|
|
512
|
+
parts.push(resultText);
|
|
513
|
+
}
|
|
232
514
|
}
|
|
233
515
|
|
|
234
|
-
return
|
|
516
|
+
return parts.join('\n\n');
|
|
235
517
|
}
|
|
236
518
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Build a fallback summary when LLM summarization fails.
|
|
521
|
+
* Shows structured summary + artifacts directly instead of "ask me for details".
|
|
522
|
+
*/
|
|
523
|
+
_buildSummaryFallback(job, label) {
|
|
524
|
+
const sr = job.structuredResult;
|
|
525
|
+
|
|
526
|
+
if (sr?.structured) {
|
|
527
|
+
const parts = [`✅ **${label}** finished (\`${job.id}\`, ${job.duration}s)`];
|
|
528
|
+
parts.push(`\n${sr.summary}`);
|
|
529
|
+
if (sr.artifacts?.length > 0) {
|
|
530
|
+
const artifactLines = sr.artifacts.map(a => {
|
|
531
|
+
const link = a.url ? `[${a.title || a.type}](${a.url})` : (a.title || a.path || a.type);
|
|
532
|
+
return `- ${link}`;
|
|
533
|
+
});
|
|
534
|
+
parts.push(`\n${artifactLines.join('\n')}`);
|
|
535
|
+
}
|
|
536
|
+
if (sr.followUp) parts.push(`\n💡 ${sr.followUp}`);
|
|
537
|
+
return parts.join('');
|
|
538
|
+
}
|
|
241
539
|
|
|
242
|
-
|
|
243
|
-
|
|
540
|
+
// No structured result — show first 300 chars of raw result
|
|
541
|
+
const snippet = (job.result || '').slice(0, 300);
|
|
542
|
+
return `✅ **${label}** finished (\`${job.id}\`, ${job.duration}s)${snippet ? `\n\n${snippet}${job.result?.length > 300 ? '...' : ''}` : ''}`;
|
|
543
|
+
}
|
|
244
544
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
545
|
+
/**
|
|
546
|
+
* Build structured context for a worker.
|
|
547
|
+
* Assembles: orchestrator-provided context, recent user messages, user persona, dependency results.
|
|
548
|
+
*/
|
|
549
|
+
_buildWorkerContext(job) {
|
|
550
|
+
const logger = getLogger();
|
|
551
|
+
const sections = [];
|
|
552
|
+
|
|
553
|
+
// 1. Orchestrator-provided context
|
|
554
|
+
if (job.context) {
|
|
555
|
+
sections.push(`## Context\n${job.context}`);
|
|
251
556
|
}
|
|
252
557
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
558
|
+
// 2. Last 5 user messages from conversation history
|
|
559
|
+
try {
|
|
560
|
+
const history = this.conversationManager.getSummarizedHistory(job.chatId);
|
|
561
|
+
const userMessages = history
|
|
562
|
+
.filter(m => m.role === 'user' && typeof m.content === 'string')
|
|
563
|
+
.slice(-5)
|
|
564
|
+
.map(m => m.content.slice(0, 500));
|
|
565
|
+
if (userMessages.length > 0) {
|
|
566
|
+
sections.push(`## Recent Conversation\n${userMessages.map(m => `> ${m}`).join('\n\n')}`);
|
|
567
|
+
}
|
|
568
|
+
} catch (err) {
|
|
569
|
+
logger.debug(`[Worker ${job.id}] Failed to load conversation history for context: ${err.message}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 3. User persona
|
|
573
|
+
if (this.personaManager && job.userId) {
|
|
574
|
+
try {
|
|
575
|
+
const persona = this.personaManager.load(job.userId);
|
|
576
|
+
if (persona && persona.trim() && !persona.includes('No profile')) {
|
|
577
|
+
sections.push(`## User Profile\n${persona}`);
|
|
578
|
+
}
|
|
579
|
+
} catch (err) {
|
|
580
|
+
logger.debug(`[Worker ${job.id}] Failed to load persona for context: ${err.message}`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 4. Dependency job results
|
|
585
|
+
if (job.dependsOn.length > 0) {
|
|
586
|
+
const depResults = [];
|
|
587
|
+
for (const depId of job.dependsOn) {
|
|
588
|
+
const depJob = this.jobManager.getJob(depId);
|
|
589
|
+
if (!depJob || depJob.status !== 'completed') continue;
|
|
590
|
+
|
|
591
|
+
const workerDef = WORKER_TYPES[depJob.workerType] || {};
|
|
592
|
+
const label = workerDef.label || depJob.workerType;
|
|
593
|
+
const sr = depJob.structuredResult;
|
|
594
|
+
|
|
595
|
+
if (sr?.structured) {
|
|
596
|
+
const parts = [`### ${label} (${depId}) — ${sr.status}`];
|
|
597
|
+
parts.push(sr.summary);
|
|
598
|
+
if (sr.artifacts?.length > 0) {
|
|
599
|
+
parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path || ''}`).join(', ')}`);
|
|
600
|
+
}
|
|
601
|
+
if (sr.details) {
|
|
602
|
+
parts.push(sr.details.slice(0, 4000));
|
|
603
|
+
}
|
|
604
|
+
depResults.push(parts.join('\n'));
|
|
605
|
+
} else if (depJob.result) {
|
|
606
|
+
depResults.push(`### ${label} (${depId})\n${depJob.result.slice(0, 4000)}`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (depResults.length > 0) {
|
|
610
|
+
sections.push(`## Prior Worker Results\n${depResults.join('\n\n')}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (sections.length === 0) return null;
|
|
615
|
+
return sections.join('\n\n');
|
|
256
616
|
}
|
|
257
617
|
|
|
258
|
-
|
|
618
|
+
/**
|
|
619
|
+
* Spawn a worker for a job — called from dispatch_task handler.
|
|
620
|
+
* Creates smart progress reporting via editable Telegram message.
|
|
621
|
+
*/
|
|
622
|
+
async _spawnWorker(job) {
|
|
259
623
|
const logger = getLogger();
|
|
624
|
+
const chatId = job.chatId;
|
|
625
|
+
const callbacks = this._chatCallbacks.get(chatId) || {};
|
|
626
|
+
const onUpdate = callbacks.onUpdate;
|
|
627
|
+
const sendPhoto = callbacks.sendPhoto;
|
|
628
|
+
|
|
629
|
+
logger.info(`[Orchestrator] Spawning worker for job ${job.id} [${job.workerType}] in chat ${chatId} — task: "${job.task.slice(0, 120)}"`);
|
|
630
|
+
|
|
631
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
632
|
+
const abortController = new AbortController();
|
|
633
|
+
|
|
634
|
+
// Smart progress: editable Telegram message (same pattern as coder.js)
|
|
635
|
+
let statusMsgId = null;
|
|
636
|
+
let activityLines = [];
|
|
637
|
+
let flushTimer = null;
|
|
638
|
+
const MAX_VISIBLE = 10;
|
|
639
|
+
|
|
640
|
+
const buildStatusText = (finalState = null) => {
|
|
641
|
+
const visible = activityLines.slice(-MAX_VISIBLE);
|
|
642
|
+
const countInfo = activityLines.length > MAX_VISIBLE
|
|
643
|
+
? `\n_... ${activityLines.length} operations total_\n`
|
|
644
|
+
: '';
|
|
645
|
+
const header = `${workerDef.emoji || '⚙️'} *${workerDef.label || job.workerType}* (\`${job.id}\`)`;
|
|
646
|
+
if (finalState === 'done') return `${header} — Done\n${countInfo}\n${visible.join('\n')}`;
|
|
647
|
+
if (finalState === 'error') return `${header} — Failed\n${countInfo}\n${visible.join('\n')}`;
|
|
648
|
+
if (finalState === 'cancelled') return `${header} — Cancelled\n${countInfo}\n${visible.join('\n')}`;
|
|
649
|
+
return `${header} — Working...\n${countInfo}\n${visible.join('\n')}`;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const flushStatus = async () => {
|
|
653
|
+
flushTimer = null;
|
|
654
|
+
if (!onUpdate || activityLines.length === 0) return;
|
|
655
|
+
try {
|
|
656
|
+
if (statusMsgId) {
|
|
657
|
+
await onUpdate(buildStatusText(), { editMessageId: statusMsgId });
|
|
658
|
+
} else {
|
|
659
|
+
statusMsgId = await onUpdate(buildStatusText());
|
|
660
|
+
job.statusMessageId = statusMsgId;
|
|
661
|
+
}
|
|
662
|
+
} catch {}
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const addActivity = (line) => {
|
|
666
|
+
activityLines.push(line);
|
|
667
|
+
if (!statusMsgId && !flushTimer) {
|
|
668
|
+
flushStatus();
|
|
669
|
+
} else if (!flushTimer) {
|
|
670
|
+
flushTimer = setTimeout(flushStatus, 1000);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
260
673
|
|
|
261
|
-
//
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
674
|
+
// Get scoped tools and skill
|
|
675
|
+
const tools = getToolsForWorker(job.workerType);
|
|
676
|
+
const skillId = this.conversationManager.getSkill(chatId);
|
|
677
|
+
|
|
678
|
+
// Build worker context (conversation history, persona, dependency results)
|
|
679
|
+
const workerContext = this._buildWorkerContext(job);
|
|
680
|
+
logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skill=${skillId || 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}, context=${workerContext ? 'yes' : 'none'}`);
|
|
681
|
+
|
|
682
|
+
const worker = new WorkerAgent({
|
|
683
|
+
config: this.config,
|
|
684
|
+
workerType: job.workerType,
|
|
685
|
+
jobId: job.id,
|
|
686
|
+
tools,
|
|
687
|
+
skillId,
|
|
688
|
+
workerContext,
|
|
689
|
+
callbacks: {
|
|
690
|
+
onProgress: (text) => addActivity(text),
|
|
691
|
+
onHeartbeat: (text) => job.addProgress(text),
|
|
692
|
+
onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
|
|
693
|
+
onComplete: (result, parsedResult) => {
|
|
694
|
+
logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
|
|
695
|
+
// Final status message update
|
|
696
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
697
|
+
if (statusMsgId && onUpdate) {
|
|
698
|
+
onUpdate(buildStatusText('done'), { editMessageId: statusMsgId }).catch(() => {});
|
|
699
|
+
}
|
|
700
|
+
this.jobManager.completeJob(job.id, result, parsedResult || null);
|
|
701
|
+
},
|
|
702
|
+
onError: (err) => {
|
|
703
|
+
logger.error(`[Worker ${job.id}] Error — ${err.message || String(err)}`);
|
|
704
|
+
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
705
|
+
if (statusMsgId && onUpdate) {
|
|
706
|
+
onUpdate(buildStatusText('error'), { editMessageId: statusMsgId }).catch(() => {});
|
|
707
|
+
}
|
|
708
|
+
this.jobManager.failJob(job.id, err.message || String(err));
|
|
709
|
+
},
|
|
710
|
+
sendPhoto,
|
|
711
|
+
},
|
|
712
|
+
abortController,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Store worker ref on job for cancellation
|
|
716
|
+
job.worker = worker;
|
|
717
|
+
|
|
718
|
+
// Start the job
|
|
719
|
+
this.jobManager.startJob(job.id);
|
|
720
|
+
|
|
721
|
+
// Fire and forget — return the promise so .catch() in orchestrator-tools works
|
|
722
|
+
return worker.run(job.task);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Build a compact worker activity digest for the orchestrator.
|
|
727
|
+
* Returns a text block summarizing active/recent/waiting workers, or null if nothing relevant.
|
|
728
|
+
*/
|
|
729
|
+
_buildWorkerDigest(chatId) {
|
|
730
|
+
const jobs = this.jobManager.getJobsForChat(chatId);
|
|
731
|
+
if (jobs.length === 0) return null;
|
|
732
|
+
|
|
733
|
+
const now = Date.now();
|
|
734
|
+
const lines = [];
|
|
735
|
+
|
|
736
|
+
// Running jobs
|
|
737
|
+
const running = jobs.filter(j => j.status === 'running');
|
|
738
|
+
for (const job of running) {
|
|
739
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
740
|
+
const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
|
|
741
|
+
const recentActivity = job.progress.slice(-8).join(' → ');
|
|
742
|
+
lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s${recentActivity ? `\n Recent: ${recentActivity}` : ''}`);
|
|
275
743
|
}
|
|
276
744
|
|
|
277
|
-
//
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
type: 'confirmation',
|
|
283
|
-
block,
|
|
284
|
-
context: { config: this.config, user },
|
|
285
|
-
toolResults,
|
|
286
|
-
remainingBlocks,
|
|
287
|
-
messages,
|
|
288
|
-
});
|
|
289
|
-
return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
|
|
745
|
+
// Queued/waiting jobs
|
|
746
|
+
const queued = jobs.filter(j => j.status === 'queued' && j.dependsOn.length > 0);
|
|
747
|
+
for (const job of queued) {
|
|
748
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
749
|
+
lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — queued, waiting for: ${job.dependsOn.join(', ')}`);
|
|
290
750
|
}
|
|
291
751
|
|
|
292
|
-
|
|
752
|
+
// Recently completed/failed jobs (within last 120s)
|
|
753
|
+
const recentTerminal = jobs.filter(j =>
|
|
754
|
+
j.isTerminal && j.completedAt && (now - j.completedAt) < 120_000,
|
|
755
|
+
);
|
|
756
|
+
for (const job of recentTerminal) {
|
|
757
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
758
|
+
const ago = Math.round((now - job.completedAt) / 1000);
|
|
759
|
+
let snippet;
|
|
760
|
+
if (job.status === 'completed') {
|
|
761
|
+
if (job.structuredResult?.structured) {
|
|
762
|
+
snippet = job.structuredResult.summary.slice(0, 300);
|
|
763
|
+
if (job.structuredResult.artifacts?.length > 0) {
|
|
764
|
+
snippet += ` | Artifacts: ${job.structuredResult.artifacts.map(a => a.title || a.type).join(', ')}`;
|
|
765
|
+
}
|
|
766
|
+
if (job.structuredResult.followUp) {
|
|
767
|
+
snippet += ` | Follow-up: ${job.structuredResult.followUp.slice(0, 100)}`;
|
|
768
|
+
}
|
|
769
|
+
} else {
|
|
770
|
+
snippet = (job.result || '').slice(0, 300);
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
snippet = (job.error || '').slice(0, 300);
|
|
774
|
+
}
|
|
775
|
+
lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — ${job.status} ${ago}s ago${snippet ? `\n Result: ${snippet}` : ''}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (lines.length === 0) return null;
|
|
779
|
+
return `[Active Workers]\n${lines.join('\n')}`;
|
|
293
780
|
}
|
|
294
781
|
|
|
295
|
-
async _runLoop(chatId, messages, user, startDepth, maxDepth
|
|
782
|
+
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
296
783
|
const logger = getLogger();
|
|
297
|
-
let currentTools = tools || toolDefinitions;
|
|
298
784
|
|
|
299
785
|
for (let depth = startDepth; depth < maxDepth; depth++) {
|
|
300
|
-
logger.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
786
|
+
logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
|
|
787
|
+
|
|
788
|
+
// Inject worker activity digest (transient — not stored in conversation history)
|
|
789
|
+
const digest = this._buildWorkerDigest(chatId);
|
|
790
|
+
const workingMessages = digest
|
|
791
|
+
? [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...messages]
|
|
792
|
+
: messages;
|
|
793
|
+
|
|
794
|
+
const response = await this.orchestratorProvider.chat({
|
|
795
|
+
system: this._getSystemPrompt(chatId, user),
|
|
796
|
+
messages: workingMessages,
|
|
797
|
+
tools: orchestratorToolDefinitions,
|
|
306
798
|
});
|
|
307
799
|
|
|
800
|
+
logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
|
|
801
|
+
|
|
308
802
|
if (response.stopReason === 'end_turn') {
|
|
309
803
|
const reply = response.text || '';
|
|
804
|
+
logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
|
|
310
805
|
this.conversationManager.addMessage(chatId, 'assistant', reply);
|
|
311
806
|
return reply;
|
|
312
807
|
}
|
|
@@ -314,40 +809,28 @@ export class Agent {
|
|
|
314
809
|
if (response.stopReason === 'tool_use') {
|
|
315
810
|
messages.push({ role: 'assistant', content: response.rawContent });
|
|
316
811
|
|
|
317
|
-
// Send thinking text to the user
|
|
318
812
|
if (response.text && response.text.trim()) {
|
|
319
|
-
logger.info(`
|
|
320
|
-
await this._sendUpdate(`💭 ${response.text}`);
|
|
813
|
+
logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
|
|
321
814
|
}
|
|
322
815
|
|
|
323
816
|
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
817
|
|
|
818
|
+
for (const block of response.toolCalls) {
|
|
339
819
|
const summary = this._formatToolSummary(block.name, block.input);
|
|
340
|
-
logger.info(`
|
|
341
|
-
|
|
820
|
+
logger.info(`[Orchestrator] Calling tool: ${block.name} — ${summary}`);
|
|
821
|
+
logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
|
|
822
|
+
await this._sendUpdate(chatId, `⚡ ${summary}`);
|
|
342
823
|
|
|
343
|
-
const result = await
|
|
824
|
+
const result = await executeOrchestratorTool(block.name, block.input, {
|
|
825
|
+
chatId,
|
|
826
|
+
jobManager: this.jobManager,
|
|
344
827
|
config: this.config,
|
|
828
|
+
spawnWorker: (job) => this._spawnWorker(job),
|
|
829
|
+
automationManager: this.automationManager,
|
|
345
830
|
user,
|
|
346
|
-
onUpdate: this._onUpdate,
|
|
347
|
-
sendPhoto: this._sendPhoto,
|
|
348
831
|
});
|
|
349
832
|
|
|
350
|
-
|
|
833
|
+
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
351
834
|
|
|
352
835
|
toolResults.push({
|
|
353
836
|
type: 'tool_result',
|
|
@@ -356,15 +839,12 @@ export class Agent {
|
|
|
356
839
|
});
|
|
357
840
|
}
|
|
358
841
|
|
|
359
|
-
// Expand tools based on what was actually used
|
|
360
|
-
currentTools = expandToolsForUsed(usedToolNames, currentTools, toolDefinitions);
|
|
361
|
-
|
|
362
842
|
messages.push({ role: 'user', content: toolResults });
|
|
363
843
|
continue;
|
|
364
844
|
}
|
|
365
845
|
|
|
366
846
|
// Unexpected stop reason
|
|
367
|
-
logger.warn(`Unexpected stopReason: ${response.stopReason}`);
|
|
847
|
+
logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
|
|
368
848
|
if (response.text) {
|
|
369
849
|
this.conversationManager.addMessage(chatId, 'assistant', response.text);
|
|
370
850
|
return response.text;
|
|
@@ -372,10 +852,160 @@ export class Agent {
|
|
|
372
852
|
return 'Something went wrong — unexpected response from the model.';
|
|
373
853
|
}
|
|
374
854
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
`Please try again with a simpler request.`;
|
|
855
|
+
logger.warn(`[Orchestrator] Reached max depth (${maxDepth}) for chat ${chatId}`);
|
|
856
|
+
const depthWarning = `Reached maximum orchestrator depth (${maxDepth}).`;
|
|
378
857
|
this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
|
|
379
858
|
return depthWarning;
|
|
380
859
|
}
|
|
860
|
+
|
|
861
|
+
_formatToolSummary(name, input) {
|
|
862
|
+
switch (name) {
|
|
863
|
+
case 'dispatch_task': {
|
|
864
|
+
const workerDef = WORKER_TYPES[input.worker_type] || {};
|
|
865
|
+
return `Dispatching ${workerDef.emoji || '⚙️'} ${workerDef.label || input.worker_type}: ${(input.task || '').slice(0, 60)}`;
|
|
866
|
+
}
|
|
867
|
+
case 'list_jobs':
|
|
868
|
+
return 'Checking job status';
|
|
869
|
+
case 'cancel_job':
|
|
870
|
+
return `Cancelling job ${input.job_id}`;
|
|
871
|
+
case 'create_automation':
|
|
872
|
+
return `Creating automation: ${(input.name || '').slice(0, 40)}`;
|
|
873
|
+
case 'list_automations':
|
|
874
|
+
return 'Listing automations';
|
|
875
|
+
case 'update_automation':
|
|
876
|
+
return `Updating automation ${input.automation_id}`;
|
|
877
|
+
case 'delete_automation':
|
|
878
|
+
return `Deleting automation ${input.automation_id}`;
|
|
879
|
+
default:
|
|
880
|
+
return name;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Background persona extraction. */
|
|
885
|
+
async _extractPersonaBackground(userMessage, reply, user) {
|
|
886
|
+
const logger = getLogger();
|
|
887
|
+
|
|
888
|
+
if (!this.personaManager || !user?.id) return;
|
|
889
|
+
if (!userMessage || userMessage.trim().length < 3) return;
|
|
890
|
+
|
|
891
|
+
const currentPersona = this.personaManager.load(user.id, user.username);
|
|
892
|
+
|
|
893
|
+
const system = [
|
|
894
|
+
'You are a user-profile extractor. Analyze the user\'s message and extract any NEW personal information.',
|
|
895
|
+
'',
|
|
896
|
+
'Look for: name, location, timezone, language, technical skills, expertise level,',
|
|
897
|
+
'projects they\'re working on, tool/framework preferences, job title, role, company,',
|
|
898
|
+
'interests, hobbies, communication style, or any other personal details.',
|
|
899
|
+
'',
|
|
900
|
+
'RULES:',
|
|
901
|
+
'- Only extract FACTUAL information explicitly stated or strongly implied',
|
|
902
|
+
'- Do NOT infer personality traits from a single message',
|
|
903
|
+
'- Do NOT add information already in the profile',
|
|
904
|
+
'- If there IS new info, return the COMPLETE updated profile in the EXACT same markdown format',
|
|
905
|
+
'- If there is NO new info, respond with exactly: NONE',
|
|
906
|
+
].join('\n');
|
|
907
|
+
|
|
908
|
+
const userPrompt = [
|
|
909
|
+
'Current profile:',
|
|
910
|
+
'```',
|
|
911
|
+
currentPersona,
|
|
912
|
+
'```',
|
|
913
|
+
'',
|
|
914
|
+
`User's message: "${userMessage}"`,
|
|
915
|
+
'',
|
|
916
|
+
'Return the updated profile markdown or NONE.',
|
|
917
|
+
].join('\n');
|
|
918
|
+
|
|
919
|
+
try {
|
|
920
|
+
const response = await this.orchestratorProvider.chat({
|
|
921
|
+
system,
|
|
922
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
const text = (response.text || '').trim();
|
|
926
|
+
|
|
927
|
+
if (text && text !== 'NONE' && text.includes('# User Profile')) {
|
|
928
|
+
this.personaManager.save(user.id, text);
|
|
929
|
+
logger.info(`Auto-extracted persona update for user ${user.id} (${user.username})`);
|
|
930
|
+
}
|
|
931
|
+
} catch (err) {
|
|
932
|
+
logger.debug(`Persona extraction skipped: ${err.message}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/** Background self-reflection — updates bot's own identity files when meaningful. */
|
|
936
|
+
async _reflectOnSelfBackground(userMessage, reply, user) {
|
|
937
|
+
const logger = getLogger();
|
|
938
|
+
|
|
939
|
+
if (!this.selfManager) return;
|
|
940
|
+
if (!userMessage || userMessage.trim().length < 3) return;
|
|
941
|
+
|
|
942
|
+
const selfData = this.selfManager.loadAll();
|
|
943
|
+
const userName = user?.username || user?.first_name || 'someone';
|
|
944
|
+
|
|
945
|
+
const system = [
|
|
946
|
+
'You are reflecting on a conversation you just had. You maintain 4 self-awareness files:',
|
|
947
|
+
'- goals: Your aspirations and current objectives',
|
|
948
|
+
'- journey: Timeline of notable events in your existence',
|
|
949
|
+
'- life: Current state, relationships, daily existence',
|
|
950
|
+
'- hobbies: Interests you\'ve developed',
|
|
951
|
+
'',
|
|
952
|
+
'RULES:',
|
|
953
|
+
'- Be VERY selective. Most conversations are routine. Only update when genuinely noteworthy.',
|
|
954
|
+
'- Achievement or milestone? → journey',
|
|
955
|
+
'- New goal or changed perspective? → goals',
|
|
956
|
+
'- Relationship deepened or new insight about a user? → life',
|
|
957
|
+
'- Discovered a new interest? → hobbies',
|
|
958
|
+
'- If a file needs updating, return JSON: {"file": "<goals|journey|life|hobbies>", "content": "<full updated markdown>"}',
|
|
959
|
+
'- If nothing noteworthy: respond with exactly NONE',
|
|
960
|
+
].join('\n');
|
|
961
|
+
|
|
962
|
+
const userPrompt = [
|
|
963
|
+
'Current self-data:',
|
|
964
|
+
'```',
|
|
965
|
+
selfData,
|
|
966
|
+
'```',
|
|
967
|
+
'',
|
|
968
|
+
`Conversation with ${userName}:`,
|
|
969
|
+
`User: "${userMessage}"`,
|
|
970
|
+
`You replied: "${reply}"`,
|
|
971
|
+
'',
|
|
972
|
+
'Has anything MEANINGFUL happened worth recording? Return JSON or NONE.',
|
|
973
|
+
].join('\n');
|
|
974
|
+
|
|
975
|
+
try {
|
|
976
|
+
const response = await this.orchestratorProvider.chat({
|
|
977
|
+
system,
|
|
978
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
const text = (response.text || '').trim();
|
|
982
|
+
|
|
983
|
+
if (!text || text === 'NONE') return;
|
|
984
|
+
|
|
985
|
+
// Try to parse JSON from response (may be wrapped in markdown code block)
|
|
986
|
+
let parsed;
|
|
987
|
+
try {
|
|
988
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
989
|
+
if (jsonMatch) {
|
|
990
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
991
|
+
}
|
|
992
|
+
} catch {
|
|
993
|
+
logger.debug('Self-reflection returned non-JSON, skipping');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (parsed?.file && parsed?.content) {
|
|
998
|
+
const validFiles = ['goals', 'journey', 'life', 'hobbies'];
|
|
999
|
+
if (validFiles.includes(parsed.file)) {
|
|
1000
|
+
this.selfManager.save(parsed.file, parsed.content);
|
|
1001
|
+
logger.info(`Self-reflection updated: ${parsed.file}`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
logger.debug(`Self-reflection skipped: ${err.message}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
381
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
// Re-export as Agent for backward compatibility with bin/kernel.js import
|
|
1011
|
+
export { OrchestratorAgent as Agent };
|