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
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Job } from './job.js';
|
|
3
|
+
import { getLogger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Manages all jobs across all chats. Emits lifecycle events
|
|
7
|
+
* so the orchestrator can react when workers finish.
|
|
8
|
+
*
|
|
9
|
+
* Events:
|
|
10
|
+
* job:completed (job)
|
|
11
|
+
* job:failed (job)
|
|
12
|
+
* job:cancelled (job)
|
|
13
|
+
*/
|
|
14
|
+
export class JobManager extends EventEmitter {
|
|
15
|
+
constructor({ jobTimeoutSeconds = 300, cleanupIntervalMinutes = 30 } = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.jobs = new Map(); // id -> Job
|
|
18
|
+
this.jobTimeoutMs = jobTimeoutSeconds * 1000;
|
|
19
|
+
this.cleanupMaxAge = cleanupIntervalMinutes * 60 * 1000;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Create a new job (status: queued). */
|
|
23
|
+
createJob(chatId, workerType, task) {
|
|
24
|
+
const job = new Job({ chatId, workerType, task });
|
|
25
|
+
this.jobs.set(job.id, job);
|
|
26
|
+
const logger = getLogger();
|
|
27
|
+
logger.info(`[JobManager] Job created: ${job.id} [${workerType}] for chat ${chatId} — "${task.slice(0, 100)}"`);
|
|
28
|
+
logger.debug(`[JobManager] Total jobs tracked: ${this.jobs.size}`);
|
|
29
|
+
return job;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Move a job to running. */
|
|
33
|
+
startJob(jobId) {
|
|
34
|
+
const job = this._get(jobId);
|
|
35
|
+
job.transition('running');
|
|
36
|
+
getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → running`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Move a job to completed with a result. */
|
|
40
|
+
completeJob(jobId, result) {
|
|
41
|
+
const job = this._get(jobId);
|
|
42
|
+
job.transition('completed');
|
|
43
|
+
job.result = result;
|
|
44
|
+
getLogger().info(`[JobManager] Job ${job.id} [${job.workerType}] → completed (${job.duration}s) — result: ${(result || '').length} chars`);
|
|
45
|
+
this.emit('job:completed', job);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Move a job to failed with an error message. */
|
|
49
|
+
failJob(jobId, error) {
|
|
50
|
+
const job = this._get(jobId);
|
|
51
|
+
job.transition('failed');
|
|
52
|
+
job.error = typeof error === 'string' ? error : error?.message || String(error);
|
|
53
|
+
getLogger().error(`[JobManager] Job ${job.id} [${job.workerType}] → failed (${job.duration}s) — ${job.error}`);
|
|
54
|
+
this.emit('job:failed', job);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Cancel a specific job. Returns the job or null if not found / already terminal. */
|
|
58
|
+
cancelJob(jobId) {
|
|
59
|
+
const logger = getLogger();
|
|
60
|
+
const job = this.jobs.get(jobId);
|
|
61
|
+
if (!job) {
|
|
62
|
+
logger.warn(`[JobManager] Cancel: job ${jobId} not found`);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (job.isTerminal) {
|
|
66
|
+
logger.warn(`[JobManager] Cancel: job ${jobId} already in terminal state (${job.status})`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const prevStatus = job.status;
|
|
71
|
+
job.transition('cancelled');
|
|
72
|
+
// Abort the worker if it's running
|
|
73
|
+
if (job.worker && typeof job.worker.cancel === 'function') {
|
|
74
|
+
logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus}) — sending cancel to worker`);
|
|
75
|
+
job.worker.cancel();
|
|
76
|
+
} else {
|
|
77
|
+
logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus}) — no active worker to abort`);
|
|
78
|
+
}
|
|
79
|
+
this.emit('job:cancelled', job);
|
|
80
|
+
return job;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Cancel all non-terminal jobs for a chat. Returns array of cancelled jobs. */
|
|
84
|
+
cancelAllForChat(chatId) {
|
|
85
|
+
const logger = getLogger();
|
|
86
|
+
const allForChat = [...this.jobs.values()].filter(j => j.chatId === chatId && !j.isTerminal);
|
|
87
|
+
logger.info(`[JobManager] Cancel all for chat ${chatId} — ${allForChat.length} active jobs found`);
|
|
88
|
+
|
|
89
|
+
const cancelled = [];
|
|
90
|
+
for (const job of allForChat) {
|
|
91
|
+
const prevStatus = job.status;
|
|
92
|
+
job.transition('cancelled');
|
|
93
|
+
if (job.worker && typeof job.worker.cancel === 'function') {
|
|
94
|
+
job.worker.cancel();
|
|
95
|
+
}
|
|
96
|
+
logger.info(`[JobManager] Job ${job.id} [${job.workerType}] → cancelled (was: ${prevStatus})`);
|
|
97
|
+
this.emit('job:cancelled', job);
|
|
98
|
+
cancelled.push(job);
|
|
99
|
+
}
|
|
100
|
+
return cancelled;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Get all jobs for a chat (most recent first). */
|
|
104
|
+
getJobsForChat(chatId) {
|
|
105
|
+
const jobs = [...this.jobs.values()]
|
|
106
|
+
.filter((j) => j.chatId === chatId)
|
|
107
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
108
|
+
getLogger().debug(`[JobManager] getJobsForChat(${chatId}): ${jobs.length} jobs`);
|
|
109
|
+
return jobs;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Get only running jobs for a chat. */
|
|
113
|
+
getRunningJobsForChat(chatId) {
|
|
114
|
+
const running = [...this.jobs.values()]
|
|
115
|
+
.filter((j) => j.chatId === chatId && j.status === 'running');
|
|
116
|
+
getLogger().debug(`[JobManager] getRunningJobsForChat(${chatId}): ${running.length} running`);
|
|
117
|
+
return running;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Get a job by id. */
|
|
121
|
+
getJob(jobId) {
|
|
122
|
+
return this.jobs.get(jobId) || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Garbage-collect old terminal jobs. */
|
|
126
|
+
cleanup() {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
let removed = 0;
|
|
129
|
+
for (const [id, job] of this.jobs) {
|
|
130
|
+
if (job.isTerminal && job.completedAt && now - job.completedAt > this.cleanupMaxAge) {
|
|
131
|
+
this.jobs.delete(id);
|
|
132
|
+
removed++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (removed) {
|
|
136
|
+
getLogger().info(`[JobManager] Cleanup: removed ${removed} old jobs — ${this.jobs.size} remaining`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Enforce timeout on running jobs. Called periodically. */
|
|
141
|
+
enforceTimeouts() {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
let checkedCount = 0;
|
|
144
|
+
for (const job of this.jobs.values()) {
|
|
145
|
+
if (job.status === 'running' && job.startedAt) {
|
|
146
|
+
checkedCount++;
|
|
147
|
+
const elapsed = now - job.startedAt;
|
|
148
|
+
if (elapsed > this.jobTimeoutMs) {
|
|
149
|
+
getLogger().warn(`[JobManager] Job ${job.id} [${job.workerType}] timed out — elapsed: ${Math.round(elapsed / 1000)}s, limit: ${this.jobTimeoutMs / 1000}s`);
|
|
150
|
+
// Cancel the worker first so it stops executing & frees resources
|
|
151
|
+
if (job.worker && typeof job.worker.cancel === 'function') {
|
|
152
|
+
job.worker.cancel();
|
|
153
|
+
}
|
|
154
|
+
this.failJob(job.id, `Timed out after ${this.jobTimeoutMs / 1000}s`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (checkedCount > 0) {
|
|
159
|
+
getLogger().debug(`[JobManager] Timeout check: ${checkedCount} running jobs checked`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Internal: get job or throw. */
|
|
164
|
+
_get(jobId) {
|
|
165
|
+
const job = this.jobs.get(jobId);
|
|
166
|
+
if (!job) throw new Error(`Job not found: ${jobId}`);
|
|
167
|
+
return job;
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/swarm/job.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
|
|
3
|
+
/** Valid job statuses and allowed transitions. */
|
|
4
|
+
const TRANSITIONS = {
|
|
5
|
+
queued: ['running', 'cancelled'],
|
|
6
|
+
running: ['completed', 'failed', 'cancelled'],
|
|
7
|
+
completed: [],
|
|
8
|
+
failed: [],
|
|
9
|
+
cancelled: [],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A single unit of work dispatched from the orchestrator to a worker.
|
|
14
|
+
*/
|
|
15
|
+
export class Job {
|
|
16
|
+
constructor({ chatId, workerType, task }) {
|
|
17
|
+
this.id = randomBytes(4).toString('hex'); // short 8-char hex id
|
|
18
|
+
this.chatId = chatId;
|
|
19
|
+
this.workerType = workerType;
|
|
20
|
+
this.task = task;
|
|
21
|
+
this.status = 'queued';
|
|
22
|
+
this.result = null;
|
|
23
|
+
this.error = null;
|
|
24
|
+
this.worker = null; // WorkerAgent ref
|
|
25
|
+
this.statusMessageId = null; // Telegram message for progress edits
|
|
26
|
+
this.createdAt = Date.now();
|
|
27
|
+
this.startedAt = null;
|
|
28
|
+
this.completedAt = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Transition to a new status. Throws if the transition is invalid. */
|
|
32
|
+
transition(newStatus) {
|
|
33
|
+
const allowed = TRANSITIONS[this.status];
|
|
34
|
+
if (!allowed || !allowed.includes(newStatus)) {
|
|
35
|
+
throw new Error(`Invalid job transition: ${this.status} -> ${newStatus}`);
|
|
36
|
+
}
|
|
37
|
+
this.status = newStatus;
|
|
38
|
+
if (newStatus === 'running') this.startedAt = Date.now();
|
|
39
|
+
if (['completed', 'failed', 'cancelled'].includes(newStatus)) this.completedAt = Date.now();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Duration in seconds (or null if not started). */
|
|
43
|
+
get duration() {
|
|
44
|
+
if (!this.startedAt) return null;
|
|
45
|
+
const end = this.completedAt || Date.now();
|
|
46
|
+
return Math.round((end - this.startedAt) / 1000);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether this job is in a terminal state. */
|
|
50
|
+
get isTerminal() {
|
|
51
|
+
return ['completed', 'failed', 'cancelled'].includes(this.status);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Human-readable one-line summary. */
|
|
55
|
+
toSummary() {
|
|
56
|
+
const statusEmoji = {
|
|
57
|
+
queued: '🔜',
|
|
58
|
+
running: '⚙️',
|
|
59
|
+
completed: '✅',
|
|
60
|
+
failed: '❌',
|
|
61
|
+
cancelled: '🚫',
|
|
62
|
+
};
|
|
63
|
+
const emoji = statusEmoji[this.status] || '❓';
|
|
64
|
+
const dur = this.duration != null ? ` (${this.duration}s)` : '';
|
|
65
|
+
return `${emoji} \`${this.id}\` [${this.workerType}] ${this.task.slice(0, 60)}${this.task.length > 60 ? '...' : ''}${dur}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { TOOL_CATEGORIES } from '../tools/categories.js';
|
|
2
|
+
import { toolDefinitions } from '../tools/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Worker type definitions — maps each worker type to the tool categories it needs.
|
|
6
|
+
*/
|
|
7
|
+
export const WORKER_TYPES = {
|
|
8
|
+
coding: {
|
|
9
|
+
label: 'Coding Worker',
|
|
10
|
+
emoji: '💻',
|
|
11
|
+
categories: ['core', 'coding', 'git', 'github'],
|
|
12
|
+
description: 'Write code, fix bugs, create PRs',
|
|
13
|
+
},
|
|
14
|
+
browser: {
|
|
15
|
+
label: 'Browser Worker',
|
|
16
|
+
emoji: '🌐',
|
|
17
|
+
categories: ['browser'],
|
|
18
|
+
description: 'Web search, scraping, screenshots',
|
|
19
|
+
},
|
|
20
|
+
system: {
|
|
21
|
+
label: 'System Worker',
|
|
22
|
+
emoji: '🖥️',
|
|
23
|
+
categories: ['core', 'process', 'monitor', 'network'],
|
|
24
|
+
description: 'OS operations, monitoring, network',
|
|
25
|
+
},
|
|
26
|
+
devops: {
|
|
27
|
+
label: 'DevOps Worker',
|
|
28
|
+
emoji: '🚀',
|
|
29
|
+
categories: ['core', 'docker', 'process', 'monitor', 'network', 'git'],
|
|
30
|
+
description: 'Docker, deploy, infrastructure',
|
|
31
|
+
},
|
|
32
|
+
research: {
|
|
33
|
+
label: 'Research Worker',
|
|
34
|
+
emoji: '🔍',
|
|
35
|
+
categories: ['browser', 'core'],
|
|
36
|
+
description: 'Deep web research and analysis',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the tool name set for a given worker type.
|
|
42
|
+
* @param {string} workerType
|
|
43
|
+
* @returns {Set<string>}
|
|
44
|
+
*/
|
|
45
|
+
function getToolNamesForWorker(workerType) {
|
|
46
|
+
const config = WORKER_TYPES[workerType];
|
|
47
|
+
if (!config) throw new Error(`Unknown worker type: ${workerType}`);
|
|
48
|
+
|
|
49
|
+
const names = new Set();
|
|
50
|
+
for (const cat of config.categories) {
|
|
51
|
+
const tools = TOOL_CATEGORIES[cat];
|
|
52
|
+
if (tools) tools.forEach((t) => names.add(t));
|
|
53
|
+
}
|
|
54
|
+
return names;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get Anthropic-format tool definitions scoped to a worker type.
|
|
59
|
+
* @param {string} workerType
|
|
60
|
+
* @returns {Array} filtered tool definitions
|
|
61
|
+
*/
|
|
62
|
+
export function getToolsForWorker(workerType) {
|
|
63
|
+
const names = getToolNamesForWorker(workerType);
|
|
64
|
+
return toolDefinitions.filter((t) => names.has(t.name));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get all tool names for a worker type (for credential checking).
|
|
69
|
+
* @param {string} workerType
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
export function getToolNamesForWorkerType(workerType) {
|
|
73
|
+
return [...getToolNamesForWorker(workerType)];
|
|
74
|
+
}
|