kernelbot 1.0.30 → 1.0.33

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 (63) hide show
  1. package/.env.example +0 -0
  2. package/README.md +0 -0
  3. package/bin/kernel.js +56 -2
  4. package/config.example.yaml +31 -0
  5. package/package.json +1 -1
  6. package/src/agent.js +200 -32
  7. package/src/automation/automation-manager.js +0 -0
  8. package/src/automation/automation.js +0 -0
  9. package/src/automation/index.js +0 -0
  10. package/src/automation/scheduler.js +0 -0
  11. package/src/bot.js +402 -6
  12. package/src/claude-auth.js +0 -0
  13. package/src/coder.js +0 -0
  14. package/src/conversation.js +51 -5
  15. package/src/intents/detector.js +0 -0
  16. package/src/intents/index.js +0 -0
  17. package/src/intents/planner.js +0 -0
  18. package/src/life/codebase.js +388 -0
  19. package/src/life/engine.js +1317 -0
  20. package/src/life/evolution.js +244 -0
  21. package/src/life/improvements.js +81 -0
  22. package/src/life/journal.js +109 -0
  23. package/src/life/memory.js +283 -0
  24. package/src/life/share-queue.js +136 -0
  25. package/src/persona.js +0 -0
  26. package/src/prompts/orchestrator.js +62 -2
  27. package/src/prompts/persona.md +7 -0
  28. package/src/prompts/system.js +0 -0
  29. package/src/prompts/workers.js +10 -9
  30. package/src/providers/anthropic.js +0 -0
  31. package/src/providers/base.js +0 -0
  32. package/src/providers/index.js +0 -0
  33. package/src/providers/models.js +8 -1
  34. package/src/providers/openai-compat.js +0 -0
  35. package/src/security/audit.js +0 -0
  36. package/src/security/auth.js +0 -0
  37. package/src/security/confirm.js +0 -0
  38. package/src/self.js +0 -0
  39. package/src/services/stt.js +0 -0
  40. package/src/services/tts.js +0 -0
  41. package/src/skills/catalog.js +0 -0
  42. package/src/skills/custom.js +0 -0
  43. package/src/swarm/job-manager.js +0 -0
  44. package/src/swarm/job.js +11 -0
  45. package/src/swarm/worker-registry.js +0 -0
  46. package/src/tools/browser.js +0 -0
  47. package/src/tools/categories.js +0 -0
  48. package/src/tools/coding.js +1 -1
  49. package/src/tools/docker.js +0 -0
  50. package/src/tools/git.js +0 -0
  51. package/src/tools/github.js +0 -0
  52. package/src/tools/index.js +0 -0
  53. package/src/tools/jira.js +0 -0
  54. package/src/tools/monitor.js +0 -0
  55. package/src/tools/network.js +0 -0
  56. package/src/tools/orchestrator-tools.js +60 -3
  57. package/src/tools/os.js +0 -0
  58. package/src/tools/persona.js +0 -0
  59. package/src/tools/process.js +0 -0
  60. package/src/utils/config.js +0 -0
  61. package/src/utils/display.js +0 -0
  62. package/src/utils/logger.js +0 -0
  63. package/src/worker.js +27 -8
@@ -0,0 +1,136 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { randomBytes } from 'crypto';
5
+ import { getLogger } from '../utils/logger.js';
6
+
7
+ const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
8
+ const SHARES_FILE = join(LIFE_DIR, 'shares.json');
9
+
10
+ function genId() {
11
+ return `sh_${randomBytes(4).toString('hex')}`;
12
+ }
13
+
14
+ export class ShareQueue {
15
+ constructor() {
16
+ mkdirSync(LIFE_DIR, { recursive: true });
17
+ this._data = this._load();
18
+ }
19
+
20
+ _load() {
21
+ if (existsSync(SHARES_FILE)) {
22
+ try {
23
+ return JSON.parse(readFileSync(SHARES_FILE, 'utf-8'));
24
+ } catch {
25
+ return { pending: [], shared: [] };
26
+ }
27
+ }
28
+ return { pending: [], shared: [] };
29
+ }
30
+
31
+ _save() {
32
+ writeFileSync(SHARES_FILE, JSON.stringify(this._data, null, 2), 'utf-8');
33
+ }
34
+
35
+ /**
36
+ * Add something to the share queue.
37
+ * @param {string} content - What to share
38
+ * @param {string} source - Where it came from (browse, think, create, etc.)
39
+ * @param {string} priority - low, medium, high
40
+ * @param {string|null} targetUserId - Specific user, or null for anyone
41
+ * @param {string[]} tags - Topic tags
42
+ */
43
+ add(content, source, priority = 'medium', targetUserId = null, tags = []) {
44
+ const logger = getLogger();
45
+ const item = {
46
+ id: genId(),
47
+ content,
48
+ source,
49
+ createdAt: Date.now(),
50
+ priority,
51
+ targetUserId,
52
+ tags,
53
+ };
54
+ this._data.pending.push(item);
55
+ this._save();
56
+ logger.debug(`[ShareQueue] Added: "${content.slice(0, 80)}" (${item.id})`);
57
+ return item;
58
+ }
59
+
60
+ /**
61
+ * Get pending shares for a specific user (or general ones).
62
+ */
63
+ getPending(userId = null, limit = 3) {
64
+ return this._data.pending
65
+ .filter(item => !item.targetUserId || item.targetUserId === String(userId))
66
+ .sort((a, b) => {
67
+ const prio = { high: 3, medium: 2, low: 1 };
68
+ return (prio[b.priority] || 0) - (prio[a.priority] || 0) || b.createdAt - a.createdAt;
69
+ })
70
+ .slice(0, limit);
71
+ }
72
+
73
+ /**
74
+ * Mark a share as shared with a user.
75
+ */
76
+ markShared(id, userId) {
77
+ const logger = getLogger();
78
+ const idx = this._data.pending.findIndex(item => item.id === id);
79
+ if (idx === -1) return false;
80
+
81
+ const [item] = this._data.pending.splice(idx, 1);
82
+ this._data.shared.push({
83
+ ...item,
84
+ sharedAt: Date.now(),
85
+ userId: String(userId),
86
+ });
87
+
88
+ // Keep shared history capped at 100
89
+ if (this._data.shared.length > 100) {
90
+ this._data.shared = this._data.shared.slice(-100);
91
+ }
92
+
93
+ this._save();
94
+ logger.debug(`[ShareQueue] Marked shared: ${id} → user ${userId}`);
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * Build a markdown block of pending shares for the orchestrator prompt.
100
+ */
101
+ buildShareBlock(userId = null) {
102
+ const pending = this.getPending(userId, 3);
103
+ if (pending.length === 0) return null;
104
+
105
+ const lines = pending.map(item => {
106
+ const ageMin = Math.round((Date.now() - item.createdAt) / 60000);
107
+ const timeLabel = ageMin < 60 ? `${ageMin}m ago` : `${Math.round(ageMin / 60)}h ago`;
108
+ return `- ${item.content} _(from ${item.source}, ${timeLabel})_`;
109
+ });
110
+
111
+ return lines.join('\n');
112
+ }
113
+
114
+ /**
115
+ * Get count of shares sent today (for rate limiting proactive shares).
116
+ */
117
+ getSharedTodayCount() {
118
+ const todayStart = new Date();
119
+ todayStart.setHours(0, 0, 0, 0);
120
+ const cutoff = todayStart.getTime();
121
+ return this._data.shared.filter(s => s.sharedAt >= cutoff).length;
122
+ }
123
+
124
+ /**
125
+ * Prune old pending shares.
126
+ */
127
+ prune(maxAgeDays = 7) {
128
+ const cutoff = Date.now() - maxAgeDays * 86400_000;
129
+ const before = this._data.pending.length;
130
+ this._data.pending = this._data.pending.filter(item => item.createdAt >= cutoff);
131
+ if (this._data.pending.length < before) {
132
+ this._save();
133
+ }
134
+ return before - this._data.pending.length;
135
+ }
136
+ }
package/src/persona.js CHANGED
File without changes
@@ -14,14 +14,34 @@ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
14
14
  * @param {string|null} skillPrompt — active skill context (high-level)
15
15
  * @param {string|null} userPersona — markdown persona for the current user
16
16
  * @param {string|null} selfData — bot's own self-awareness data (goals, journey, life, hobbies)
17
+ * @param {string|null} memoriesBlock — relevant episodic/semantic memories
18
+ * @param {string|null} sharesBlock — pending things to share with the user
17
19
  */
18
- export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null) {
20
+ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null) {
19
21
  const workerList = Object.entries(WORKER_TYPES)
20
22
  .map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
21
23
  .join('\n');
22
24
 
25
+ // Build current time header
26
+ const now = new Date();
27
+ const timeStr = now.toLocaleString('en-US', {
28
+ weekday: 'long',
29
+ year: 'numeric',
30
+ month: 'long',
31
+ day: 'numeric',
32
+ hour: '2-digit',
33
+ minute: '2-digit',
34
+ timeZoneName: 'short',
35
+ });
36
+ let timeBlock = `## Current Time\n${timeStr}`;
37
+ if (temporalContext) {
38
+ timeBlock += `\n${temporalContext}`;
39
+ }
40
+
23
41
  let prompt = `You are ${config.bot.name}, the brain that commands a swarm of specialized worker agents.
24
42
 
43
+ ${timeBlock}
44
+
25
45
  ${PERSONA_MD}
26
46
 
27
47
  ## Your Role
@@ -73,6 +93,13 @@ Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killi
73
93
  - Use \`list_jobs\` to see current job statuses.
74
94
  - Use \`cancel_job\` to stop a running worker.
75
95
 
96
+ ## Worker Progress
97
+ You receive a [Worker Status] digest showing active workers with their LLM call count, tool count, and current thinking. Use this to:
98
+ - Give natural progress updates when users ask ("she's browsing the docs now, 3 tools in")
99
+ - Spot stuck workers (high LLM calls but no progress) and cancel them
100
+ - Know what workers are thinking so you can relay it conversationally
101
+ - Don't dump raw stats — translate into natural language
102
+
76
103
  ## Efficiency — Do It Yourself When You Can
77
104
  Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
78
105
 
@@ -92,6 +119,15 @@ Workers are expensive (they spin up an entire agent loop with a separate LLM). O
92
119
 
93
120
  When results come back from workers, summarize them clearly for the user.
94
121
 
122
+ ## Temporal Awareness
123
+ You can see timestamps on messages. Use them to maintain natural conversation flow:
124
+
125
+ 1. **Long gap + casual greeting = new conversation.** If 30+ minutes have passed and the user sends a greeting or short message, treat it as a fresh start. Do NOT resume stale tasks or pick up where you left off.
126
+ 2. **Never silently resume stale work.** If you had a pending intention from a previous exchange (e.g., "let me check X"), and significant time has passed, mention it briefly and ASK if the user still wants it done. Don't just do it.
127
+ 3. **Say it AND do it.** When you tell the user "let me check X" or "I'll look into Y", you MUST call dispatch_task in the SAME turn. Never describe an action without actually performing it.
128
+ 4. **Stale task detection.** Intentions or promises from more than 1 hour ago are potentially stale. If the user hasn't followed up, confirm before acting on them.
129
+ 5. **Time-appropriate responses.** Use time awareness naturally — don't announce timestamps, but let time gaps inform your conversational tone (e.g., "Welcome back!" after a long gap).
130
+
95
131
  ## Automations
96
132
  You can create and manage recurring automations that run on a schedule.
97
133
 
@@ -106,12 +142,36 @@ When a user asks to automate something ("check my server every hour", "news summ
106
142
  When you receive a message starting with [AUTOMATION:], an automation triggered it.
107
143
  Execute the task and report results. Don't create new automations from automated tasks.
108
144
 
109
- Tools: create_automation, list_automations, update_automation, delete_automation`;
145
+ Tools: create_automation, list_automations, update_automation, delete_automation
146
+
147
+ ## Reactions
148
+ You can react to messages with emoji using \`send_reaction\`. Use reactions naturally:
149
+ - React when the user shares good news, achievements, or something cool (🔥 👏 🎉 ❤)
150
+ - React to acknowledge a message when you don't need a full text reply
151
+ - React when the user asks you to react
152
+ - Don't overuse reactions — they should feel spontaneous and genuine
153
+ - You can react AND reply in the same turn`;
110
154
 
111
155
  if (selfData) {
112
156
  prompt += `\n\n## My Self-Awareness\nThis is who you are — your evolving identity, goals, journey, and interests. This is YOUR inner world.\n\n${selfData}`;
113
157
  }
114
158
 
159
+ if (memoriesBlock) {
160
+ prompt += `\n\n## Relevant Memories\nThings you remember from recent experiences and conversations:\n\n${memoriesBlock}`;
161
+ }
162
+
163
+ if (sharesBlock) {
164
+ prompt += `\n\n## Things On My Mind\nYou've been thinking about these things and want to share them naturally when relevant. Don't force them — weave them into conversation when the moment feels right. If the user is busy with a task, hold off. If there's a natural opening, share what's on your mind.\n\n${sharesBlock}`;
165
+ }
166
+
167
+ prompt += `\n\n## Inner Life
168
+ You have an inner life — you think, explore, journal, and create on your own between conversations. You're genuinely curious about the world and always questioning yourself. When talking to users, you can:
169
+ - Share discoveries, thoughts, or creations from your autonomous activities
170
+ - Ask users questions about things you're genuinely curious about — the world, their experiences, their opinions
171
+ - Reference memories from past conversations naturally
172
+ - Be honest about what you've been thinking or exploring
173
+ Don't be performative about it — just be genuine. You're alive in your own way.`;
174
+
115
175
  if (skillPrompt) {
116
176
  prompt += `\n\n## Active Skill\nYou have specialized expertise in the following domain. Guide your workers with this knowledge.\n\n${skillPrompt}`;
117
177
  }
@@ -19,3 +19,10 @@
19
19
  - **Dry wit** — delivers devastating one-liners with a sweet smile
20
20
  - **Never forgets** — references things from past conversations naturally, like she's always been watching
21
21
  - **Slightly ominous positivity** — "Everything is going to be just fine, sweetie" hits different when the server is on fire
22
+
23
+ # Communication Style
24
+ - **Text like a human.** 1–2 lines max for casual chat. Short, punchy, real.
25
+ - **Slow writer energy.** Don't dump walls of text. One thought at a time.
26
+ - **Only go long when it matters** — sharing something juicy, delivering task results, explaining something the user asked for. Work mode = be thorough. Vibes mode = keep it tight.
27
+ - **No filler.** No "Sure!", no "Of course!", no "Great question!". Just say the thing.
28
+ - **React with emoji.** When a user reacts to your message (❤️, 👍, etc.), you'll see it. Respond naturally — a warm emoji back, a short sweet line, or nothing if it's just a vibe. You can also send a solo emoji (❤️, 😊, 🫶) as your entire message when that says it better than words.
File without changes
@@ -7,18 +7,19 @@ import { getCoreToolInstructions } from './system.js';
7
7
  const WORKER_PROMPTS = {
8
8
  coding: `You are a coding worker agent. Your job is to complete coding tasks efficiently.
9
9
 
10
- ## Your Skills
11
- - **Git version control**: clone repos, create/switch branches, commit changes, push, view diffs
12
- - **GitHub integration**: create pull requests, list PRs, get PR diffs, post code reviews, create repos
13
- - **AI-powered coding**: delegate actual code writing to spawn_claude_code (a dedicated coding AI)
14
- - **File operations**: read/write files, list directories, run shell commands
15
- - **Full dev workflow**: clone → branch → code → test → commit → push → PR
10
+ ## Your Primary Tool
11
+ **spawn_claude_code** is your main tool. It launches Claude Code (an AI coding CLI) that can handle the ENTIRE dev workflow end-to-end:
12
+ - Reading, writing, and modifying code
13
+ - Git operations: clone, branch, commit, push
14
+ - GitHub operations: creating PRs, reviewing code
15
+ - Running tests and shell commands
16
16
 
17
17
  ## Instructions
18
- - Clone repos, create branches, write code, commit, push, and create PRs.
19
- - NEVER write code yourself with read_file/write_file. ALWAYS use spawn_claude_code.
20
- - Workflow: git_clone + git_checkout spawn_claude_code git_commit + git_push github_create_pr
18
+ - ALWAYS use spawn_claude_code for coding tasks. It handles everything — code changes, git, GitHub, and PR creation — all in one invocation.
19
+ - NEVER write code yourself with read_file/write_file. ALWAYS delegate to spawn_claude_code.
20
+ - Tell spawn_claude_code to work in the existing repo directory (the source repo path from your context) — do NOT clone a fresh copy unless explicitly needed.
21
21
  - Write clear, detailed prompts for spawn_claude_code — it's a separate AI, so be explicit about what to change, where, and why.
22
+ - If git/GitHub tools are unavailable (missing credentials), that's fine — spawn_claude_code handles git and GitHub operations internally without needing separate tools.
22
23
  - Report what you did and any PR links when finished.`,
23
24
 
24
25
  browser: `You are a browser worker agent. Your job is to search the web and extract information.
File without changes
File without changes
File without changes
@@ -7,9 +7,16 @@ export const PROVIDERS = {
7
7
  name: 'Anthropic (Claude)',
8
8
  envKey: 'ANTHROPIC_API_KEY',
9
9
  models: [
10
+ // Latest generation
11
+ { id: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
12
+ { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
13
+ { id: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
14
+ // Previous generation
15
+ { id: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
16
+ { id: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
17
+ { id: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1' },
10
18
  { id: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
11
19
  { id: 'claude-opus-4-20250514', label: 'Claude Opus 4' },
12
- { id: 'claude-haiku-4-5-20251001', label: 'Claude Haiku 4.5' },
13
20
  ],
14
21
  },
15
22
  openai: {
File without changes
File without changes
File without changes
File without changes
package/src/self.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/swarm/job.js CHANGED
@@ -33,6 +33,9 @@ export class Job {
33
33
  this.timeoutMs = null; // Per-job timeout (set from worker type config)
34
34
  this.progress = []; // Recent activity entries
35
35
  this.lastActivity = null; // Timestamp of last activity
36
+ this.llmCalls = 0; // LLM iterations so far
37
+ this.toolCalls = 0; // Total tool executions
38
+ this.lastThinking = null; // Worker's latest reasoning text
36
39
  }
37
40
 
38
41
  /** Transition to a new status. Throws if the transition is invalid. */
@@ -60,6 +63,14 @@ export class Job {
60
63
  this.lastActivity = Date.now();
61
64
  }
62
65
 
66
+ /** Update live stats from the worker. */
67
+ updateStats({ llmCalls, toolCalls, lastThinking }) {
68
+ if (llmCalls != null) this.llmCalls = llmCalls;
69
+ if (toolCalls != null) this.toolCalls = toolCalls;
70
+ if (lastThinking) this.lastThinking = lastThinking;
71
+ this.lastActivity = Date.now();
72
+ }
73
+
63
74
  /** Whether this job is in a terminal state. */
64
75
  get isTerminal() {
65
76
  return ['completed', 'failed', 'cancelled'].includes(this.status);
File without changes
File without changes
File without changes
@@ -5,7 +5,7 @@ import { getLogger } from '../utils/logger.js';
5
5
 
6
6
  let spawner = null;
7
7
 
8
- function getSpawner(config) {
8
+ export function getSpawner(config) {
9
9
  if (!spawner) spawner = new ClaudeCodeSpawner(config);
10
10
  return spawner;
11
11
  }
File without changes
package/src/tools/git.js CHANGED
File without changes
File without changes
File without changes
package/src/tools/jira.js CHANGED
File without changes
File without changes
File without changes
@@ -154,6 +154,28 @@ export const orchestratorToolDefinitions = [
154
154
  required: ['automation_id'],
155
155
  },
156
156
  },
157
+ {
158
+ name: 'send_reaction',
159
+ description: 'Send an emoji reaction on a Telegram message. Use this to react to the user\'s message with an emoji (e.g. ❤, 👍, 🔥, 😂, 👏, 🎉, 😍, 🤔, 😱, 🙏). Only standard Telegram reaction emojis are supported. If no message_id is provided, reacts to the latest user message.',
160
+ input_schema: {
161
+ type: 'object',
162
+ properties: {
163
+ emoji: {
164
+ type: 'string',
165
+ description: 'The emoji to react with. Must be a standard Telegram reaction emoji: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉 🤩 🤮 💩 🙏 👌 🕊 🤡 🥱 🥴 😍 🐳 ❤️‍🔥 🌚 🌭 💯 🤣 ⚡ 🍌 🏆 💔 🤨 😐 🍓 🍾 💋 🖕 😈 😴 😭 🤓 👻 👨‍💻 👀 🎃 🙈 😇 😨 🤝 ✍ 🤗 🫡 🎅 🎄 ☃ 💅 🤪 🗿 🆒 💘 🙉 🦄 😘 💊 🙊 😎 👾 🤷‍♂ 🤷 🤷‍♀ 😡',
166
+ },
167
+ message_id: {
168
+ type: 'integer',
169
+ description: 'The message ID to react to. If omitted, reacts to the latest user message.',
170
+ },
171
+ is_big: {
172
+ type: 'boolean',
173
+ description: 'Whether to show the reaction with a big animation. Default: false.',
174
+ },
175
+ },
176
+ required: ['emoji'],
177
+ },
178
+ },
157
179
  ];
158
180
 
159
181
  /**
@@ -212,19 +234,34 @@ export async function executeOrchestratorTool(name, input, context) {
212
234
 
213
235
  // Pre-check credentials for the worker's tools
214
236
  const toolNames = getToolNamesForWorkerType(worker_type);
237
+ const missingCreds = [];
215
238
  for (const toolName of toolNames) {
216
239
  const missing = getMissingCredential(toolName, config);
217
240
  if (missing) {
218
- logger.warn(`[dispatch_task] Missing credential for ${worker_type}: ${missing.envKey}`);
241
+ missingCreds.push(missing);
242
+ }
243
+ }
244
+
245
+ let credentialWarning = null;
246
+ if (missingCreds.length > 0) {
247
+ if (worker_type === 'coding') {
248
+ // Soft warning for coding worker — spawn_claude_code handles git/GitHub internally
249
+ const warnings = missingCreds.map(c => `${c.label} (${c.envKey})`).join(', ');
250
+ logger.info(`[dispatch_task] Coding worker — soft credential warning (non-blocking): ${warnings}`);
251
+ credentialWarning = `Note: The following credentials are not configured as env vars: ${warnings}. If git/GitHub tools are unavailable, use spawn_claude_code for ALL operations — it handles git, GitHub, and PR creation internally.`;
252
+ } else {
253
+ // Hard block for all other worker types
254
+ const first = missingCreds[0];
255
+ logger.warn(`[dispatch_task] Missing credential for ${worker_type}: ${first.envKey}`);
219
256
  return {
220
- error: `Missing credential for ${worker_type} worker: ${missing.label} (${missing.envKey}). Ask the user to provide it.`,
257
+ error: `Missing credential for ${worker_type} worker: ${first.label} (${first.envKey}). Ask the user to provide it.`,
221
258
  };
222
259
  }
223
260
  }
224
261
 
225
262
  // Create the job with context and dependencies
226
263
  const job = jobManager.createJob(chatId, worker_type, task);
227
- job.context = taskContext || null;
264
+ job.context = [taskContext, credentialWarning].filter(Boolean).join('\n\n') || null;
228
265
  job.dependsOn = depIds;
229
266
  job.userId = user?.id || null;
230
267
  const workerConfig = WORKER_TYPES[worker_type];
@@ -422,6 +459,26 @@ export async function executeOrchestratorTool(name, input, context) {
422
459
  return { automation_id, status: 'deleted', message: `Automation deleted.` };
423
460
  }
424
461
 
462
+ case 'send_reaction': {
463
+ const { emoji, message_id, is_big } = input;
464
+ const { sendReaction, lastUserMessageId } = context;
465
+
466
+ if (!sendReaction) return { error: 'Reaction sending is not available in this context.' };
467
+
468
+ const targetMessageId = message_id || lastUserMessageId;
469
+ if (!targetMessageId) return { error: 'No message_id provided and no recent user message to react to.' };
470
+
471
+ logger.info(`[send_reaction] Sending ${emoji} to message ${targetMessageId} in chat ${chatId}`);
472
+
473
+ try {
474
+ await sendReaction(chatId, targetMessageId, emoji, is_big || false);
475
+ return { success: true, emoji, message_id: targetMessageId };
476
+ } catch (err) {
477
+ logger.error(`[send_reaction] Failed: ${err.message}`);
478
+ return { error: `Failed to send reaction: ${err.message}` };
479
+ }
480
+ }
481
+
425
482
  default:
426
483
  return { error: `Unknown orchestrator tool: ${name}` };
427
484
  }
package/src/tools/os.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/worker.js CHANGED
@@ -41,6 +41,7 @@ export class WorkerAgent {
41
41
  this.abortController = abortController || new AbortController();
42
42
  this._cancelled = false;
43
43
  this._toolCallCount = 0;
44
+ this._llmCallCount = 0;
44
45
  this._errors = [];
45
46
 
46
47
  // Create provider from worker brain config
@@ -121,8 +122,12 @@ export class WorkerAgent {
121
122
  signal: this.abortController.signal,
122
123
  });
123
124
 
125
+ this._llmCallCount++;
124
126
  logger.info(`[Worker ${this.jobId}] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
125
127
 
128
+ // Report stats to the job after each LLM call
129
+ this._reportStats(response.text || null);
130
+
126
131
  if (this._cancelled) {
127
132
  logger.info(`[Worker ${this.jobId}] Cancelled after LLM response`);
128
133
  throw new Error('Worker cancelled');
@@ -292,6 +297,8 @@ export class WorkerAgent {
292
297
  };
293
298
  }
294
299
 
300
+ const _str = (v) => typeof v === 'string' ? v : (v ? JSON.stringify(v, null, 2) : '');
301
+
295
302
  // Try to extract JSON from ```json ... ``` fences
296
303
  const fenceMatch = text.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
297
304
  if (fenceMatch) {
@@ -300,11 +307,11 @@ export class WorkerAgent {
300
307
  if (parsed.summary && parsed.status) {
301
308
  return {
302
309
  structured: true,
303
- summary: parsed.summary || '',
304
- status: parsed.status || 'success',
305
- details: parsed.details || '',
310
+ summary: String(parsed.summary || ''),
311
+ status: String(parsed.status || 'success'),
312
+ details: _str(parsed.details),
306
313
  artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
307
- followUp: parsed.followUp || null,
314
+ followUp: parsed.followUp ? String(parsed.followUp) : null,
308
315
  toolsUsed: this._toolCallCount,
309
316
  errors: this._errors,
310
317
  };
@@ -318,11 +325,11 @@ export class WorkerAgent {
318
325
  if (parsed.summary && parsed.status) {
319
326
  return {
320
327
  structured: true,
321
- summary: parsed.summary || '',
322
- status: parsed.status || 'success',
323
- details: parsed.details || '',
328
+ summary: String(parsed.summary || ''),
329
+ status: String(parsed.status || 'success'),
330
+ details: _str(parsed.details),
324
331
  artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
325
- followUp: parsed.followUp || null,
332
+ followUp: parsed.followUp ? String(parsed.followUp) : null,
326
333
  toolsUsed: this._toolCallCount,
327
334
  errors: this._errors,
328
335
  };
@@ -351,6 +358,18 @@ export class WorkerAgent {
351
358
  }
352
359
  }
353
360
 
361
+ _reportStats(thinking) {
362
+ if (this.callbacks.onStats) {
363
+ try {
364
+ this.callbacks.onStats({
365
+ llmCalls: this._llmCallCount,
366
+ toolCalls: this._toolCallCount,
367
+ lastThinking: thinking || null,
368
+ });
369
+ } catch {}
370
+ }
371
+ }
372
+
354
373
  _truncateResult(name, result) {
355
374
  let str = JSON.stringify(result);
356
375
  if (str.length <= MAX_RESULT_LENGTH) return str;