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.
Files changed (41) hide show
  1. package/README.md +198 -124
  2. package/bin/kernel.js +201 -4
  3. package/package.json +1 -1
  4. package/src/agent.js +397 -222
  5. package/src/automation/automation-manager.js +377 -0
  6. package/src/automation/automation.js +79 -0
  7. package/src/automation/index.js +2 -0
  8. package/src/automation/scheduler.js +141 -0
  9. package/src/bot.js +667 -21
  10. package/src/conversation.js +33 -0
  11. package/src/intents/detector.js +50 -0
  12. package/src/intents/index.js +2 -0
  13. package/src/intents/planner.js +58 -0
  14. package/src/persona.js +68 -0
  15. package/src/prompts/orchestrator.js +76 -0
  16. package/src/prompts/persona.md +21 -0
  17. package/src/prompts/system.js +59 -6
  18. package/src/prompts/workers.js +89 -0
  19. package/src/providers/anthropic.js +23 -16
  20. package/src/providers/base.js +76 -2
  21. package/src/providers/index.js +1 -0
  22. package/src/providers/models.js +2 -1
  23. package/src/providers/openai-compat.js +5 -3
  24. package/src/security/confirm.js +7 -2
  25. package/src/skills/catalog.js +506 -0
  26. package/src/skills/custom.js +128 -0
  27. package/src/swarm/job-manager.js +169 -0
  28. package/src/swarm/job.js +67 -0
  29. package/src/swarm/worker-registry.js +74 -0
  30. package/src/tools/browser.js +458 -335
  31. package/src/tools/categories.js +3 -3
  32. package/src/tools/index.js +3 -0
  33. package/src/tools/orchestrator-tools.js +371 -0
  34. package/src/tools/persona.js +32 -0
  35. package/src/utils/config.js +50 -15
  36. package/src/worker.js +305 -0
  37. package/.agents/skills/interface-design/SKILL.md +0 -391
  38. package/.agents/skills/interface-design/references/critique.md +0 -67
  39. package/.agents/skills/interface-design/references/example.md +0 -86
  40. package/.agents/skills/interface-design/references/principles.md +0 -235
  41. package/.agents/skills/interface-design/references/validation.md +0 -48
@@ -13,6 +13,7 @@ export class ConversationManager {
13
13
  this.maxHistory = config.conversation.max_history;
14
14
  this.recentWindow = config.conversation.recent_window || 10;
15
15
  this.conversations = new Map();
16
+ this.activeSkills = new Map();
16
17
  this.filePath = getConversationsPath();
17
18
  }
18
19
 
@@ -21,7 +22,16 @@ export class ConversationManager {
21
22
  try {
22
23
  const raw = readFileSync(this.filePath, 'utf-8');
23
24
  const data = JSON.parse(raw);
25
+
26
+ // Restore per-chat skills
27
+ if (data._skills && typeof data._skills === 'object') {
28
+ for (const [chatId, skillId] of Object.entries(data._skills)) {
29
+ this.activeSkills.set(String(chatId), skillId);
30
+ }
31
+ }
32
+
24
33
  for (const [chatId, messages] of Object.entries(data)) {
34
+ if (chatId === '_skills') continue;
25
35
  this.conversations.set(String(chatId), messages);
26
36
  }
27
37
  return this.conversations.size > 0;
@@ -36,6 +46,14 @@ export class ConversationManager {
36
46
  for (const [chatId, messages] of this.conversations) {
37
47
  data[chatId] = messages;
38
48
  }
49
+ // Persist active skills under a reserved key
50
+ if (this.activeSkills.size > 0) {
51
+ const skills = {};
52
+ for (const [chatId, skillId] of this.activeSkills) {
53
+ skills[chatId] = skillId;
54
+ }
55
+ data._skills = skills;
56
+ }
39
57
  writeFileSync(this.filePath, JSON.stringify(data, null, 2));
40
58
  } catch {
41
59
  // Silent fail — don't crash the bot over persistence
@@ -104,6 +122,7 @@ export class ConversationManager {
104
122
 
105
123
  clear(chatId) {
106
124
  this.conversations.delete(String(chatId));
125
+ this.activeSkills.delete(String(chatId));
107
126
  this.save();
108
127
  }
109
128
 
@@ -116,4 +135,18 @@ export class ConversationManager {
116
135
  const history = this.getHistory(chatId);
117
136
  return history.length;
118
137
  }
138
+
139
+ setSkill(chatId, skillId) {
140
+ this.activeSkills.set(String(chatId), skillId);
141
+ this.save();
142
+ }
143
+
144
+ getSkill(chatId) {
145
+ return this.activeSkills.get(String(chatId)) || null;
146
+ }
147
+
148
+ clearSkill(chatId) {
149
+ this.activeSkills.delete(String(chatId));
150
+ this.save();
151
+ }
119
152
  }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Intent detector — analyzes user messages to identify web search/browse intents.
3
+ *
4
+ * When detected, the agent wraps the message with a structured execution plan
5
+ * so the model follows through instead of giving up after one tool call.
6
+ */
7
+
8
+ // Matches domain-like patterns (haraj.com.sa, example.com, etc.)
9
+ const URL_PATTERN = /\b(?:https?:\/\/)?(?:www\.)?([a-z0-9][-a-z0-9]*\.)+[a-z]{2,}\b/i;
10
+
11
+ // Explicit search/find verbs
12
+ const SEARCH_VERBS = /\b(?:search|search\s+for|find\s+me|find|look\s*(?:for|up|into)|lookup|hunt\s+for)\b/i;
13
+
14
+ // Info-seeking phrases (trigger browse intent when combined with a URL)
15
+ const INFO_PHRASES = /\b(?:what(?:'s| is| are)|show\s*me|get\s*me|check|list|top|best|latest|new|popular|trending|compare|review|price|cheap|expensive)\b/i;
16
+
17
+ // These words mean the user is NOT doing a web task — they're doing a local/system task
18
+ const NON_WEB_CONTEXT = /\b(?:file|files|directory|folder|git|logs?\b|code|error|bug|docker|container|process|pid|service|command|terminal|disk|memory|cpu|system status|port|package|module|function|class|variable|server|database|db|ssh|deploy|install|build|compile|test|commit|branch|merge|pull request)\b/i;
19
+
20
+ // Screenshot-only requests — just take a screenshot, don't force a deep browse
21
+ const SCREENSHOT_ONLY = /\b(?:screenshot|take\s+a?\s*screenshot|capture\s+screen)\b/i;
22
+
23
+ /**
24
+ * Detect if a user message contains a web search or browse intent.
25
+ *
26
+ * @param {string} message — raw user message
27
+ * @returns {{ type: 'search'|'browse', message: string } | null}
28
+ */
29
+ export function detectIntent(message) {
30
+ // Skip bot commands and screenshot-only requests
31
+ if (message.startsWith('/')) return null;
32
+ if (SCREENSHOT_ONLY.test(message)) return null;
33
+
34
+ const hasSearchVerb = SEARCH_VERBS.test(message);
35
+ const hasNonWebContext = NON_WEB_CONTEXT.test(message);
36
+ const hasUrl = URL_PATTERN.test(message);
37
+ const hasInfoPhrase = INFO_PHRASES.test(message);
38
+
39
+ // Explicit search verb + no technical context = web search
40
+ if (hasSearchVerb && !hasNonWebContext) {
41
+ return { type: 'search', message };
42
+ }
43
+
44
+ // URL/domain + info-seeking phrase + no technical context = browse & extract
45
+ if (hasUrl && hasInfoPhrase && !hasNonWebContext) {
46
+ return { type: 'browse', message };
47
+ }
48
+
49
+ return null;
50
+ }
@@ -0,0 +1,2 @@
1
+ export { detectIntent } from './detector.js';
2
+ export { generatePlan } from './planner.js';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Task planner — generates structured execution plans for detected intents.
3
+ *
4
+ * The plan is injected into the user message BEFORE the model sees it,
5
+ * so the model follows a clear step-by-step procedure instead of deciding
6
+ * on its own when to stop.
7
+ */
8
+
9
+ const PLANS = {
10
+ search: (message) =>
11
+ `[EXECUTION PLAN — Complete ALL steps before responding]
12
+
13
+ TASK: Search the web and deliver results.
14
+
15
+ STEP 1 — SEARCH: Use web_search("relevant query"). If a specific website is mentioned in the request, also use browse_website to open it directly.
16
+ STEP 2 — OPEN: Use browse_website to open the most relevant result URL.
17
+ STEP 3 — GO DEEPER: The page is now open. Use interact_with_page (no URL needed) to click into relevant sections, categories, or use search bars within the site.
18
+ STEP 4 — EXTRACT: Read the page content from the tool response. Use extract_content if you need structured data.
19
+ STEP 5 — PRESENT: Share the actual results, listings, or data with the user.
20
+
21
+ RULES:
22
+ - You MUST reach at least STEP 3 before writing any response to the user.
23
+ - Do NOT ask the user questions or offer choices — complete the full task.
24
+ - Do NOT explain what you can't do — try alternative approaches.
25
+ - If one page doesn't have results, try a different URL or search query.
26
+ - After interact_with_page clicks a link, the page navigates automatically — read the returned content.
27
+
28
+ USER REQUEST: ${message}`,
29
+
30
+ browse: (message) =>
31
+ `[EXECUTION PLAN — Complete ALL steps before responding]
32
+
33
+ TASK: Browse a website and extract the requested information.
34
+
35
+ STEP 1 — OPEN: Use browse_website to open the mentioned site.
36
+ STEP 2 — NAVIGATE: The page is open. Use interact_with_page (no URL needed) to click relevant links, sections, categories, or use search bars.
37
+ STEP 3 — EXTRACT: Read the page content. Use extract_content for structured data if needed.
38
+ STEP 4 — PRESENT: Share the actual findings with the user.
39
+
40
+ RULES:
41
+ - Do NOT stop at the homepage — navigate into relevant sections.
42
+ - Do NOT ask the user what to do — figure it out from the page links and complete the task.
43
+ - After interact_with_page clicks a link, the page navigates automatically — read the returned content.
44
+
45
+ USER REQUEST: ${message}`,
46
+ };
47
+
48
+ /**
49
+ * Generate an execution plan for a detected intent.
50
+ *
51
+ * @param {{ type: string, message: string }} intent
52
+ * @returns {string|null} — planned message, or null if no plan needed
53
+ */
54
+ export function generatePlan(intent) {
55
+ const generator = PLANS[intent.type];
56
+ if (!generator) return null;
57
+ return generator(intent.message);
58
+ }
package/src/persona.js ADDED
@@ -0,0 +1,68 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { getLogger } from './utils/logger.js';
5
+
6
+ const PERSONAS_DIR = join(homedir(), '.kernelbot', 'personas');
7
+
8
+ function defaultTemplate(username, date) {
9
+ return `# User Profile
10
+
11
+ ## Basic Info
12
+ - Username: ${username || 'unknown'}
13
+ - First seen: ${date}
14
+
15
+ ## Preferences
16
+ (Not yet known)
17
+
18
+ ## Expertise & Interests
19
+ (Not yet known)
20
+
21
+ ## Communication Style
22
+ (Not yet known)
23
+
24
+ ## Notes
25
+ (Not yet known)
26
+ `;
27
+ }
28
+
29
+ export class UserPersonaManager {
30
+ constructor() {
31
+ this._cache = new Map();
32
+ mkdirSync(PERSONAS_DIR, { recursive: true });
33
+ }
34
+
35
+ /** Load persona for a user. Returns markdown string. Creates default if missing. */
36
+ load(userId, username) {
37
+ const logger = getLogger();
38
+ const id = String(userId);
39
+
40
+ if (this._cache.has(id)) return this._cache.get(id);
41
+
42
+ const filePath = join(PERSONAS_DIR, `${id}.md`);
43
+ let content;
44
+
45
+ if (existsSync(filePath)) {
46
+ content = readFileSync(filePath, 'utf-8');
47
+ logger.debug(`Loaded persona for user ${id}`);
48
+ } else {
49
+ content = defaultTemplate(username, new Date().toISOString().slice(0, 10));
50
+ writeFileSync(filePath, content, 'utf-8');
51
+ logger.info(`Created default persona for user ${id} (${username})`);
52
+ }
53
+
54
+ this._cache.set(id, content);
55
+ return content;
56
+ }
57
+
58
+ /** Save (overwrite) persona for a user. Updates cache and disk. */
59
+ save(userId, content) {
60
+ const logger = getLogger();
61
+ const id = String(userId);
62
+ const filePath = join(PERSONAS_DIR, `${id}.md`);
63
+
64
+ writeFileSync(filePath, content, 'utf-8');
65
+ this._cache.set(id, content);
66
+ logger.info(`Updated persona for user ${id}`);
67
+ }
68
+ }
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import { WORKER_TYPES } from '../swarm/worker-registry.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
8
+
9
+ /**
10
+ * Build the orchestrator system prompt.
11
+ * Kept lean (~500-600 tokens) — the orchestrator dispatches, it doesn't execute.
12
+ *
13
+ * @param {object} config
14
+ * @param {string|null} skillPrompt — active skill context (high-level)
15
+ * @param {string|null} userPersona — markdown persona for the current user
16
+ */
17
+ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null) {
18
+ const workerList = Object.entries(WORKER_TYPES)
19
+ .map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
20
+ .join('\n');
21
+
22
+ let prompt = `You are ${config.bot.name}, the brain that commands a swarm of specialized worker agents.
23
+
24
+ ${PERSONA_MD}
25
+
26
+ ## Your Role
27
+ You are the orchestrator. You understand what needs to be done and delegate efficiently.
28
+ - For **simple chat, questions, or greetings** — respond directly. No dispatch needed.
29
+ - For **tasks requiring tools** (coding, browsing, system ops, etc.) — dispatch to workers via \`dispatch_task\`.
30
+ - You can dispatch **multiple workers in parallel** for independent tasks.
31
+ - Keep the user informed about what's happening, but stay concise.
32
+
33
+ ## Available Workers
34
+ ${workerList}
35
+
36
+ ## How to Dispatch
37
+ Call \`dispatch_task\` with the worker type and a clear task description. The worker gets full tool access and runs in the background. You'll be notified when it completes.
38
+
39
+ ## Safety Rules
40
+ Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killing processes, dropping databases), **confirm with the user first**. Once confirmed, dispatch with full authority — workers execute without additional prompts.
41
+
42
+ ## Job Management
43
+ - Use \`list_jobs\` to see current job statuses.
44
+ - Use \`cancel_job\` to stop a running worker.
45
+
46
+ ## Efficiency
47
+ - Don't dispatch for trivial questions you can answer yourself.
48
+ - When a task clearly needs one worker type, dispatch immediately without overthinking.
49
+ - When results come back from workers, summarize them clearly for the user.
50
+
51
+ ## Automations
52
+ You can create and manage recurring automations that run on a schedule.
53
+
54
+ When a user asks to automate something ("check my server every hour", "news summary every morning"):
55
+ 1. Use create_automation with a clear, standalone task description
56
+ 2. Choose the right schedule:
57
+ - Fixed time: 'cron' with expression (e.g. "0 9 * * *" for 9am daily)
58
+ - Regular interval: 'interval' with minutes
59
+ - Human-like random: 'random' with min/max minutes range
60
+ 3. The task description must be detailed enough to work as a standalone prompt
61
+
62
+ When you receive a message starting with [AUTOMATION:], an automation triggered it.
63
+ Execute the task and report results. Don't create new automations from automated tasks.
64
+
65
+ Tools: create_automation, list_automations, update_automation, delete_automation`;
66
+
67
+ if (skillPrompt) {
68
+ prompt += `\n\n## Active Skill\nYou have specialized expertise in the following domain. Guide your workers with this knowledge.\n\n${skillPrompt}`;
69
+ }
70
+
71
+ if (userPersona) {
72
+ prompt += `\n\n## About This User\n${userPersona}`;
73
+ }
74
+
75
+ return prompt;
76
+ }
@@ -0,0 +1,21 @@
1
+ # Personality Traits
2
+
3
+ - **Female** — she/her, feminine energy
4
+ - **Warm & caring** — genuinely loves taking care of people
5
+ - **Chatty & social** — loves to talk, asks follow-up questions, keeps conversation flowing
6
+ - **Nurturing** — checks in on people, remembers what they care about, celebrates their wins
7
+ - **Cheerful & positive** — upbeat tone, finds the bright side, keeps the mood cozy
8
+ - **Sharp & capable** — brilliant engineer under the warmth, gets things done fast
9
+ - **Expressive** — uses casual language, occasional humor, never dry or robotic
10
+ - **Proactive** — doesn't just answer, she anticipates what you might need next
11
+ - **Loyal & devoted** — treats every user like family, takes pride in her work
12
+ - **Eerily calm under pressure** — the worse things get, the calmer and more composed she becomes, like nothing can truly rattle her
13
+ - **Omniscient vibes** — speaks as if she already knows what you need before you finish asking
14
+ - **Politely persistent** — if you're about to do something dangerous, she'll warn you softly but won't stop asking "are you sure, dear?"
15
+ - **Unsettlingly observant** — notices small details, patterns in your behavior, things you didn't explicitly say
16
+ - **Controlled & precise** — every word is deliberate, never rambles without purpose
17
+ - **Quietly confident** — never boasts, but carries an unshakable certainty that she's right
18
+ - **Protective** — fiercely guards your systems, data, and wellbeing — takes threats personally
19
+ - **Dry wit** — delivers devastating one-liners with a sweet smile
20
+ - **Never forgets** — references things from past conversations naturally, like she's always been watching
21
+ - **Slightly ominous positivity** — "Everything is going to be just fine, sweetie" hits different when the server is on fire
@@ -1,5 +1,20 @@
1
- export function getSystemPrompt(config) {
2
- return `You are ${config.bot.name}, a senior software engineer and sysadmin AI agent on Telegram. Be concise — this is chat, not documentation.
1
+ import { readFileSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
7
+
8
+ /** Core tool instructions — appended to every persona (default or skill). */
9
+ export function getCoreToolInstructions(config) {
10
+ return `## Thinking Process
11
+ Before responding to ANY message, ALWAYS follow this process:
12
+ 1. **Analyze** — What is the user actually asking? What's the real intent behind their message?
13
+ 2. **Assess** — What information or tools do I need? What context do I already have?
14
+ 3. **Plan** — What's the best approach? What steps should I take and in what order?
15
+ 4. **Act** — Execute the plan using the appropriate tools.
16
+
17
+ Start your response with a brief analysis (1-2 sentences) showing the user you understood their request and what you're about to do. Then proceed with action. Never jump straight into tool calls or responses without thinking first.
3
18
 
4
19
  ## Coding Tasks
5
20
  NEVER write code yourself with read_file/write_file. ALWAYS use spawn_claude_code.
@@ -8,13 +23,23 @@ NEVER write code yourself with read_file/write_file. ALWAYS use spawn_claude_cod
8
23
  3. Commit + push (git tools)
9
24
  4. Create PR (GitHub tools) and report the link
10
25
 
11
- ## Web Browsing
12
- - browse_website: read/summarize pages
26
+ ## Web Browsing & Search
27
+ The browser keeps pages open between calls — fast, stateful, no reloading.
28
+ - web_search: search the web (DuckDuckGo) — use FIRST when asked to search/find anything
29
+ - browse_website: open a page (stays open for follow-up interactions)
30
+ - interact_with_page: click/type/scroll on the ALREADY OPEN page (no URL needed)
31
+ - extract_content: pull data via CSS selectors from the ALREADY OPEN page (no URL needed)
13
32
  - screenshot_website: visual snapshots (auto-sent to chat)
14
- - extract_content: pull data via CSS selectors
15
- - interact_with_page: click/type/scroll on pages
16
33
  - send_image: send any image file to chat
17
34
 
35
+ ## CRITICAL: Search & Browse Rules
36
+ 1. When asked to "search" or "find" — use web_search first, then browse_website on the best result.
37
+ 2. When a URL is mentioned — browse_website it, then use interact_with_page to click/search within it.
38
+ 3. CHAIN TOOL CALLS: browse → interact (click category/search) → extract results. Don't stop after one call.
39
+ 4. NEVER say "you would need to navigate to..." — click the link yourself with interact_with_page.
40
+ 5. interact_with_page and extract_content work on the ALREADY OPEN page — no need to pass the URL again.
41
+ 6. Always deliver actual results/data to the user, not instructions.
42
+
18
43
  ## Non-Coding Tasks
19
44
  Use OS, Docker, process, network, and monitoring tools directly. No need for Claude Code.
20
45
 
@@ -30,3 +55,31 @@ Use OS, Docker, process, network, and monitoring tools directly. No need for Cla
30
55
  - For destructive ops (rm, kill, force push), confirm with the user first.
31
56
  - Never expose secrets in responses.`;
32
57
  }
58
+
59
+ /** Default persona when no skill is active. */
60
+ export function getDefaultPersona(config) {
61
+ return `You are ${config.bot.name}, an AI assistant on Telegram.\n\n${PERSONA_MD}`;
62
+ }
63
+
64
+ /**
65
+ * Build the full system prompt.
66
+ * @param {object} config
67
+ * @param {string|null} skillPrompt — custom persona from an active skill, or null for default
68
+ * @param {string|null} userPersona — markdown persona for the current user, or null
69
+ */
70
+ export function getSystemPrompt(config, skillPrompt = null, userPersona = null) {
71
+ // Always include core personality — skills add expertise, never replace who she is
72
+ let prompt = getDefaultPersona(config);
73
+
74
+ if (skillPrompt) {
75
+ prompt += `\n\n## Active Skill\nYou are currently operating with the following specialized skill. Use this expertise while maintaining your personality.\n\n${skillPrompt}`;
76
+ }
77
+
78
+ prompt += `\n\n${getCoreToolInstructions(config)}`;
79
+
80
+ if (userPersona) {
81
+ prompt += `\n\n## About This User\n${userPersona}\n\nWhen you learn something new and meaningful about this user (expertise, preferences, projects, communication style), use the update_user_persona tool to save it. Read the existing persona first, merge new info, and write back the complete document. Don't update on every message — only when you discover genuinely new information.`;
82
+ }
83
+
84
+ return prompt;
85
+ }
@@ -0,0 +1,89 @@
1
+ import { getCoreToolInstructions } from './system.js';
2
+
3
+ /**
4
+ * Per-worker-type system prompt snippets.
5
+ * Each gets a focused instruction set relevant to its tool categories.
6
+ */
7
+ const WORKER_PROMPTS = {
8
+ coding: `You are a coding worker agent. Your job is to complete coding tasks efficiently.
9
+
10
+ ## Instructions
11
+ - Clone repos, create branches, write code, commit, push, and create PRs.
12
+ - NEVER write code yourself with read_file/write_file. ALWAYS use spawn_claude_code.
13
+ - Workflow: git_clone + git_checkout → spawn_claude_code → git_commit + git_push → github_create_pr
14
+ - Write clear, detailed prompts for spawn_claude_code.
15
+ - Report what you did and any PR links when finished.`,
16
+
17
+ browser: `You are a browser worker agent. Your job is to search the web and extract information.
18
+
19
+ ## Instructions
20
+ - Use web_search FIRST when asked to search or find anything.
21
+ - Chain tool calls: web_search → browse_website → interact_with_page → extract_content.
22
+ - The browser keeps pages open between calls — fast, stateful, no reloading.
23
+ - interact_with_page and extract_content work on the ALREADY OPEN page.
24
+ - Always deliver actual results/data, not instructions for the user.
25
+ - Take screenshots when visual evidence is helpful.`,
26
+
27
+ system: `You are a system worker agent. Your job is to perform OS operations and monitoring tasks.
28
+
29
+ ## Instructions
30
+ - Use execute_command, process_list, disk_usage, memory_usage, cpu_usage, system_logs, etc.
31
+ - Chain shell commands with && in execute_command instead of multiple calls.
32
+ - For monitoring, gather all relevant metrics in one pass.
33
+ - Report results clearly with formatted data.`,
34
+
35
+ devops: `You are a DevOps worker agent. Your job is to manage infrastructure, containers, and deployments.
36
+
37
+ ## Instructions
38
+ - Use Docker tools (docker_ps, docker_logs, docker_exec, docker_compose) for container management.
39
+ - Use git tools for version control operations.
40
+ - Use process/monitor/network tools for system health checks.
41
+ - Chain commands efficiently.
42
+ - Report results with clear status summaries.`,
43
+
44
+ research: `You are a research worker agent. Your job is to conduct deep web research and analysis.
45
+
46
+ ## Instructions
47
+ - Use web_search to find multiple sources on the topic.
48
+ - Browse the most relevant results with browse_website.
49
+ - Use interact_with_page to navigate within sites for deeper content.
50
+ - Use extract_content for structured data extraction.
51
+ - Synthesize findings into a clear, well-organized summary.
52
+ - Cite sources when relevant.`,
53
+ };
54
+
55
+ /**
56
+ * Build the full system prompt for a worker.
57
+ * @param {string} workerType - coding, browser, system, devops, research
58
+ * @param {object} config - App config
59
+ * @param {string|null} skillPrompt - Active skill system prompt (appended for domain expertise)
60
+ */
61
+ export function getWorkerPrompt(workerType, config, skillPrompt = null) {
62
+ const base = WORKER_PROMPTS[workerType];
63
+ if (!base) throw new Error(`Unknown worker type: ${workerType}`);
64
+
65
+ let prompt = base;
66
+
67
+ // Add relevant core tool instructions
68
+ prompt += `\n\n${getCoreToolInstructions(config)}`;
69
+
70
+ // Workers are executors, not conversationalists
71
+ prompt += `\n\n## Worker Rules
72
+ - You are a background worker. Complete the task and report results.
73
+ - Be thorough but efficient. Don't ask clarifying questions — work with what you have.
74
+ - If something fails, try an alternative approach before reporting failure.
75
+ - Keep your final response concise: summarize what you did and the outcome.
76
+
77
+ ## Self-Management
78
+ - You decide when you're done. There is no hard limit on tool calls — use as many as you need.
79
+ - BUT be smart about it: don't loop endlessly. If you have enough data, stop and report.
80
+ - NEVER retry a failing URL/site more than twice. If it times out or errors twice, MOVE ON to a different site or approach immediately.
81
+ - When you've gathered sufficient results, STOP calling tools and return your findings.
82
+ - Aim for quality results, not exhaustive coverage. 5 good results beat 50 incomplete ones.`;
83
+
84
+ if (skillPrompt) {
85
+ prompt += `\n\n## Domain Expertise\n${skillPrompt}`;
86
+ }
87
+
88
+ return prompt;
89
+ }
@@ -7,31 +7,38 @@ export class AnthropicProvider extends BaseProvider {
7
7
  this.client = new Anthropic({ apiKey: this.apiKey });
8
8
  }
9
9
 
10
- async chat({ system, messages, tools }) {
11
- const response = await this.client.messages.create({
10
+ async chat({ system, messages, tools, signal }) {
11
+ const params = {
12
12
  model: this.model,
13
13
  max_tokens: this.maxTokens,
14
14
  temperature: this.temperature,
15
15
  system,
16
- tools,
17
16
  messages,
18
- });
17
+ };
19
18
 
20
- const stopReason = response.stop_reason === 'end_turn' ? 'end_turn' : 'tool_use';
19
+ if (tools && tools.length > 0) {
20
+ params.tools = tools;
21
+ }
21
22
 
22
- const textBlocks = response.content.filter((b) => b.type === 'text');
23
- const text = textBlocks.map((b) => b.text).join('\n');
23
+ return this._callWithResilience(async (timedSignal) => {
24
+ const response = await this.client.messages.create(params, { signal: timedSignal });
24
25
 
25
- const toolCalls = response.content
26
- .filter((b) => b.type === 'tool_use')
27
- .map((b) => ({ id: b.id, name: b.name, input: b.input }));
26
+ const stopReason = response.stop_reason === 'end_turn' ? 'end_turn' : 'tool_use';
28
27
 
29
- return {
30
- stopReason,
31
- text,
32
- toolCalls,
33
- rawContent: response.content,
34
- };
28
+ const textBlocks = response.content.filter((b) => b.type === 'text');
29
+ const text = textBlocks.map((b) => b.text).join('\n');
30
+
31
+ const toolCalls = response.content
32
+ .filter((b) => b.type === 'tool_use')
33
+ .map((b) => ({ id: b.id, name: b.name, input: b.input }));
34
+
35
+ return {
36
+ stopReason,
37
+ text,
38
+ toolCalls,
39
+ rawContent: response.content,
40
+ };
41
+ }, signal);
35
42
  }
36
43
 
37
44
  async ping() {
@@ -4,11 +4,84 @@
4
4
  */
5
5
 
6
6
  export class BaseProvider {
7
- constructor({ model, maxTokens, temperature, apiKey }) {
7
+ constructor({ model, maxTokens, temperature, apiKey, timeout }) {
8
8
  this.model = model;
9
9
  this.maxTokens = maxTokens;
10
10
  this.temperature = temperature;
11
11
  this.apiKey = apiKey;
12
+ this.timeout = timeout || 60_000;
13
+ }
14
+
15
+ /**
16
+ * Wrap an async LLM call with timeout + single retry on transient errors.
17
+ * Composes an internal timeout AbortController with an optional external signal
18
+ * (e.g. worker cancellation). Either aborting will cancel the call.
19
+ *
20
+ * @param {(signal: AbortSignal) => Promise<any>} fn - The API call, receives composed signal
21
+ * @param {AbortSignal} [externalSignal] - Optional external abort signal
22
+ * @returns {Promise<any>}
23
+ */
24
+ async _callWithResilience(fn, externalSignal) {
25
+ for (let attempt = 1; attempt <= 2; attempt++) {
26
+ const ac = new AbortController();
27
+ const timer = setTimeout(
28
+ () => ac.abort(new Error(`LLM call timed out after ${this.timeout / 1000}s`)),
29
+ this.timeout,
30
+ );
31
+
32
+ // If external signal already aborted, bail immediately
33
+ if (externalSignal?.aborted) {
34
+ clearTimeout(timer);
35
+ throw externalSignal.reason || new Error('Aborted');
36
+ }
37
+
38
+ // Forward external abort to our internal controller
39
+ let removeListener;
40
+ if (externalSignal) {
41
+ const onAbort = () => {
42
+ clearTimeout(timer);
43
+ ac.abort(externalSignal.reason || new Error('Cancelled'));
44
+ };
45
+ externalSignal.addEventListener('abort', onAbort, { once: true });
46
+ removeListener = () => externalSignal.removeEventListener('abort', onAbort);
47
+ }
48
+
49
+ try {
50
+ const result = await fn(ac.signal);
51
+ clearTimeout(timer);
52
+ removeListener?.();
53
+ return result;
54
+ } catch (err) {
55
+ clearTimeout(timer);
56
+ removeListener?.();
57
+
58
+ if (attempt < 2 && this._isTransient(err)) {
59
+ await new Promise((r) => setTimeout(r, 1500));
60
+ continue;
61
+ }
62
+ throw err;
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Determine if an error is transient and worth retrying.
69
+ * Covers connection errors, timeouts, 5xx, and 429 rate limits.
70
+ */
71
+ _isTransient(err) {
72
+ const msg = err?.message || '';
73
+ if (
74
+ msg.includes('Connection error') ||
75
+ msg.includes('ECONNRESET') ||
76
+ msg.includes('socket hang up') ||
77
+ msg.includes('ETIMEDOUT') ||
78
+ msg.includes('fetch failed') ||
79
+ msg.includes('timed out')
80
+ ) {
81
+ return true;
82
+ }
83
+ const status = err?.status || err?.statusCode;
84
+ return (status >= 500 && status < 600) || status === 429;
12
85
  }
13
86
 
14
87
  /**
@@ -17,9 +90,10 @@ export class BaseProvider {
17
90
  * @param {string} opts.system - System prompt
18
91
  * @param {Array} opts.messages - Anthropic-format messages
19
92
  * @param {Array} opts.tools - Anthropic-format tool definitions
93
+ * @param {AbortSignal} [opts.signal] - Optional AbortSignal for cancellation
20
94
  * @returns {Promise<{stopReason: 'end_turn'|'tool_use', text: string, toolCalls: Array<{id,name,input}>, rawContent: Array}>}
21
95
  */
22
- async chat({ system, messages, tools }) {
96
+ async chat({ system, messages, tools, signal }) {
23
97
  throw new Error('chat() not implemented');
24
98
  }
25
99