kernelbot 1.0.32 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kernelbot",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "description": "KernelBot — AI engineering agent with full OS control",
5
5
  "type": "module",
6
6
  "author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
package/src/agent.js CHANGED
@@ -49,7 +49,7 @@ export class OrchestratorAgent {
49
49
  }
50
50
 
51
51
  /** Build the orchestrator system prompt. */
52
- _getSystemPrompt(chatId, user) {
52
+ _getSystemPrompt(chatId, user, temporalContext = null) {
53
53
  const logger = getLogger();
54
54
  const skillId = this.conversationManager.getSkill(chatId);
55
55
  const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
@@ -76,8 +76,8 @@ export class OrchestratorAgent {
76
76
  sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
77
77
  }
78
78
 
79
- logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'}`);
80
- return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock);
79
+ logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'} | temporal=${temporalContext ? 'yes' : 'none'}`);
80
+ return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock, temporalContext);
81
81
  }
82
82
 
83
83
  setSkill(chatId, skillId) {
@@ -301,13 +301,13 @@ export class OrchestratorAgent {
301
301
  return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
302
302
  }
303
303
 
304
- async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
304
+ async processMessage(chatId, userMessage, user, onUpdate, sendPhoto, opts = {}) {
305
305
  const logger = getLogger();
306
306
 
307
307
  logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
308
308
 
309
309
  // Store callbacks so workers can use them later
310
- this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
310
+ this._chatCallbacks.set(chatId, { onUpdate, sendPhoto, sendReaction: opts.sendReaction, lastUserMessageId: opts.messageId });
311
311
 
312
312
  // Handle pending responses (confirmation or credential)
313
313
  const pending = this._pending.get(chatId);
@@ -322,6 +322,22 @@ export class OrchestratorAgent {
322
322
 
323
323
  const { max_tool_depth } = this.config.orchestrator;
324
324
 
325
+ // Detect time gap before adding the new message
326
+ let temporalContext = null;
327
+ const lastTs = this.conversationManager.getLastMessageTimestamp(chatId);
328
+ if (lastTs) {
329
+ const gapMs = Date.now() - lastTs;
330
+ const gapMinutes = Math.floor(gapMs / 60_000);
331
+ if (gapMinutes >= 30) {
332
+ const gapHours = Math.floor(gapMinutes / 60);
333
+ const gapText = gapHours >= 1
334
+ ? `${gapHours} hour(s)`
335
+ : `${gapMinutes} minute(s)`;
336
+ temporalContext = `[Time gap detected: ${gapText} since last message. User may be starting a new topic.]`;
337
+ logger.info(`Time gap detected for chat ${chatId}: ${gapText}`);
338
+ }
339
+ }
340
+
325
341
  // Add user message to persistent history
326
342
  this.conversationManager.addMessage(chatId, 'user', userMessage);
327
343
 
@@ -329,7 +345,7 @@ export class OrchestratorAgent {
329
345
  const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
330
346
  logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
331
347
 
332
- const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
348
+ const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth, temporalContext);
333
349
 
334
350
  logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
335
351
 
@@ -738,6 +754,7 @@ export class OrchestratorAgent {
738
754
  callbacks: {
739
755
  onProgress: (text) => addActivity(text),
740
756
  onHeartbeat: (text) => job.addProgress(text),
757
+ onStats: (stats) => job.updateStats(stats),
741
758
  onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
742
759
  onComplete: (result, parsedResult) => {
743
760
  logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
@@ -840,8 +857,16 @@ export class OrchestratorAgent {
840
857
  for (const job of running) {
841
858
  const workerDef = WORKER_TYPES[job.workerType] || {};
842
859
  const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
843
- const recentActivity = job.progress.slice(-8).join(' → ');
844
- lines.push(`- ${workerDef.label || job.workerType} (${job.id}) running ${dur}s${recentActivity ? `\n Recent: ${recentActivity}` : ''}`);
860
+ const stats = `${job.llmCalls} LLM calls, ${job.toolCalls} tools`;
861
+ const recentActivity = job.progress.slice(-5).join(' ');
862
+ let line = `- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s [${stats}]`;
863
+ if (job.lastThinking) {
864
+ line += `\n Thinking: "${job.lastThinking.slice(0, 150)}"`;
865
+ }
866
+ if (recentActivity) {
867
+ line += `\n Recent: ${recentActivity}`;
868
+ }
869
+ lines.push(line);
845
870
  }
846
871
 
847
872
  // Queued/waiting jobs
@@ -881,20 +906,28 @@ export class OrchestratorAgent {
881
906
  return `[Active Workers]\n${lines.join('\n')}`;
882
907
  }
883
908
 
884
- async _runLoop(chatId, messages, user, startDepth, maxDepth) {
909
+ async _runLoop(chatId, messages, user, startDepth, maxDepth, temporalContext = null) {
885
910
  const logger = getLogger();
886
911
 
887
912
  for (let depth = startDepth; depth < maxDepth; depth++) {
888
913
  logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
889
914
 
890
- // Inject worker activity digest (transient — not stored in conversation history)
915
+ // Inject transient context messages (not stored in conversation history)
916
+ let workingMessages = [...messages];
917
+
918
+ // On first iteration, inject temporal context if present
919
+ if (depth === 0 && temporalContext) {
920
+ workingMessages = [{ role: 'user', content: `[Temporal Context]\n${temporalContext}` }, ...workingMessages];
921
+ }
922
+
923
+ // Inject worker activity digest
891
924
  const digest = this._buildWorkerDigest(chatId);
892
- const workingMessages = digest
893
- ? [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...messages]
894
- : messages;
925
+ if (digest) {
926
+ workingMessages = [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...workingMessages];
927
+ }
895
928
 
896
929
  const response = await this.orchestratorProvider.chat({
897
- system: this._getSystemPrompt(chatId, user),
930
+ system: this._getSystemPrompt(chatId, user, temporalContext),
898
931
  messages: workingMessages,
899
932
  tools: orchestratorToolDefinitions,
900
933
  });
@@ -923,6 +956,7 @@ export class OrchestratorAgent {
923
956
  logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
924
957
  await this._sendUpdate(chatId, `⚡ ${summary}`);
925
958
 
959
+ const chatCallbacks = this._chatCallbacks.get(chatId) || {};
926
960
  const result = await executeOrchestratorTool(block.name, block.input, {
927
961
  chatId,
928
962
  jobManager: this.jobManager,
@@ -930,6 +964,8 @@ export class OrchestratorAgent {
930
964
  spawnWorker: (job) => this._spawnWorker(job),
931
965
  automationManager: this.automationManager,
932
966
  user,
967
+ sendReaction: chatCallbacks.sendReaction || null,
968
+ lastUserMessageId: chatCallbacks.lastUserMessageId || null,
933
969
  });
934
970
 
935
971
  logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
@@ -978,6 +1014,8 @@ export class OrchestratorAgent {
978
1014
  return `Updating automation ${input.automation_id}`;
979
1015
  case 'delete_automation':
980
1016
  return `Deleting automation ${input.automation_id}`;
1017
+ case 'send_reaction':
1018
+ return `Reacting with ${input.emoji}`;
981
1019
  default:
982
1020
  return name;
983
1021
  }
package/src/bot.js CHANGED
@@ -56,7 +56,13 @@ class ChatQueue {
56
56
  export function startBot(config, agent, conversationManager, jobManager, automationManager, lifeDeps = {}) {
57
57
  const { lifeEngine, memoryManager, journalManager, shareQueue, evolutionTracker, codebaseKnowledge } = lifeDeps;
58
58
  const logger = getLogger();
59
- const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
59
+ const bot = new TelegramBot(config.telegram.bot_token, {
60
+ polling: {
61
+ params: {
62
+ allowed_updates: ['message', 'callback_query', 'message_reaction'],
63
+ },
64
+ },
65
+ });
60
66
  const chatQueue = new ChatQueue();
61
67
  const batchWindowMs = config.telegram.batch_window_ms || 3000;
62
68
 
@@ -1630,11 +1636,18 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1630
1636
  }
1631
1637
  };
1632
1638
 
1639
+ const sendReaction = async (targetChatId, targetMsgId, emoji, isBig = false) => {
1640
+ await bot.setMessageReaction(targetChatId, targetMsgId, {
1641
+ reaction: [{ type: 'emoji', emoji }],
1642
+ is_big: isBig,
1643
+ });
1644
+ };
1645
+
1633
1646
  logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
1634
1647
  const reply = await agent.processMessage(chatId, mergedText, {
1635
1648
  id: userId,
1636
1649
  username,
1637
- }, onUpdate, sendPhoto);
1650
+ }, onUpdate, sendPhoto, { sendReaction, messageId: msg.message_id });
1638
1651
 
1639
1652
  clearInterval(typingInterval);
1640
1653
 
@@ -1668,6 +1681,90 @@ export function startBot(config, agent, conversationManager, jobManager, automat
1668
1681
  });
1669
1682
  });
1670
1683
 
1684
+ // Handle message reactions (love, like, etc.)
1685
+ bot.on('message_reaction', async (reaction) => {
1686
+ const chatId = reaction.chat.id;
1687
+ const userId = reaction.user?.id;
1688
+ const username = reaction.user?.username || reaction.user?.first_name || 'unknown';
1689
+
1690
+ if (!userId || !isAllowedUser(userId, config)) return;
1691
+
1692
+ const newReactions = reaction.new_reaction || [];
1693
+ const emojis = newReactions
1694
+ .filter(r => r.type === 'emoji')
1695
+ .map(r => r.emoji);
1696
+
1697
+ if (emojis.length === 0) return;
1698
+
1699
+ logger.info(`[Bot] Reaction from ${username} (${userId}) in chat ${chatId}: ${emojis.join(' ')}`);
1700
+
1701
+ const reactionText = `[User reacted with ${emojis.join(' ')} to your message]`;
1702
+
1703
+ chatQueue.enqueue(chatId, async () => {
1704
+ try {
1705
+ const onUpdate = async (update, opts = {}) => {
1706
+ if (opts.editMessageId) {
1707
+ try {
1708
+ const edited = await bot.editMessageText(update, {
1709
+ chat_id: chatId,
1710
+ message_id: opts.editMessageId,
1711
+ parse_mode: 'Markdown',
1712
+ });
1713
+ return edited.message_id;
1714
+ } catch {
1715
+ try {
1716
+ const edited = await bot.editMessageText(update, {
1717
+ chat_id: chatId,
1718
+ message_id: opts.editMessageId,
1719
+ });
1720
+ return edited.message_id;
1721
+ } catch {
1722
+ // fall through
1723
+ }
1724
+ }
1725
+ }
1726
+ const parts = splitMessage(update);
1727
+ let lastMsgId = null;
1728
+ for (const part of parts) {
1729
+ try {
1730
+ const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
1731
+ lastMsgId = sent.message_id;
1732
+ } catch {
1733
+ const sent = await bot.sendMessage(chatId, part);
1734
+ lastMsgId = sent.message_id;
1735
+ }
1736
+ }
1737
+ return lastMsgId;
1738
+ };
1739
+
1740
+ const sendReaction = async (targetChatId, targetMsgId, emoji, isBig = false) => {
1741
+ await bot.setMessageReaction(targetChatId, targetMsgId, {
1742
+ reaction: [{ type: 'emoji', emoji }],
1743
+ is_big: isBig,
1744
+ });
1745
+ };
1746
+
1747
+ const reply = await agent.processMessage(chatId, reactionText, {
1748
+ id: userId,
1749
+ username,
1750
+ }, onUpdate, null, { sendReaction, messageId: reaction.message_id });
1751
+
1752
+ if (reply && reply.trim()) {
1753
+ const chunks = splitMessage(reply);
1754
+ for (const chunk of chunks) {
1755
+ try {
1756
+ await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
1757
+ } catch {
1758
+ await bot.sendMessage(chatId, chunk);
1759
+ }
1760
+ }
1761
+ }
1762
+ } catch (err) {
1763
+ logger.error(`[Bot] Error processing reaction in chat ${chatId}: ${err.message}`);
1764
+ }
1765
+ });
1766
+ });
1767
+
1671
1768
  bot.on('polling_error', (err) => {
1672
1769
  logger.error(`Telegram polling error: ${err.message}`);
1673
1770
  });
@@ -68,6 +68,48 @@ export class ConversationManager {
68
68
  return this.conversations.get(key);
69
69
  }
70
70
 
71
+ /**
72
+ * Get the timestamp of the most recent message in a chat.
73
+ * Used by agent.js for time-gap detection before the current message is added.
74
+ */
75
+ getLastMessageTimestamp(chatId) {
76
+ const history = this.getHistory(chatId);
77
+ if (history.length === 0) return null;
78
+ return history[history.length - 1].timestamp || null;
79
+ }
80
+
81
+ /**
82
+ * Format a timestamp as a relative time marker.
83
+ * Returns null for missing timestamps (backward compat with old messages).
84
+ */
85
+ _formatRelativeTime(ts) {
86
+ if (!ts) return null;
87
+ const diff = Date.now() - ts;
88
+ const seconds = Math.floor(diff / 1000);
89
+ if (seconds < 60) return '[just now]';
90
+ const minutes = Math.floor(seconds / 60);
91
+ if (minutes < 60) return `[${minutes}m ago]`;
92
+ const hours = Math.floor(minutes / 60);
93
+ if (hours < 24) return `[${hours}h ago]`;
94
+ const days = Math.floor(hours / 24);
95
+ return `[${days}d ago]`;
96
+ }
97
+
98
+ /**
99
+ * Return a shallow copy of a message with a time marker prepended to string content.
100
+ * Skips tool_result arrays and messages without timestamps.
101
+ */
102
+ _annotateWithTime(msg) {
103
+ const marker = this._formatRelativeTime(msg.timestamp);
104
+ if (!marker || typeof msg.content !== 'string') return msg;
105
+ return { ...msg, content: `${marker} ${msg.content}` };
106
+ }
107
+
108
+ /** Strip internal metadata fields, returning only API-safe {role, content}. */
109
+ _sanitize(msg) {
110
+ return { role: msg.role, content: msg.content };
111
+ }
112
+
71
113
  /**
72
114
  * Get history with older messages compressed into a summary.
73
115
  * Keeps the last `recentWindow` messages verbatim and summarizes older ones.
@@ -76,18 +118,19 @@ export class ConversationManager {
76
118
  const history = this.getHistory(chatId);
77
119
 
78
120
  if (history.length <= this.recentWindow) {
79
- return [...history];
121
+ return history.map(m => this._sanitize(this._annotateWithTime(m)));
80
122
  }
81
123
 
82
124
  const olderMessages = history.slice(0, history.length - this.recentWindow);
83
125
  const recentMessages = history.slice(history.length - this.recentWindow);
84
126
 
85
- // Compress older messages into a single summary
127
+ // Compress older messages into a single summary (include time markers when available)
86
128
  const summaryLines = olderMessages.map((msg) => {
129
+ const timeTag = this._formatRelativeTime(msg.timestamp);
87
130
  const content = typeof msg.content === 'string'
88
131
  ? msg.content.slice(0, 200)
89
132
  : JSON.stringify(msg.content).slice(0, 200);
90
- return `[${msg.role}]: ${content}`;
133
+ return `[${msg.role}]${timeTag ? ` ${timeTag}` : ''}: ${content}`;
91
134
  });
92
135
 
93
136
  const summaryMessage = {
@@ -95,8 +138,11 @@ export class ConversationManager {
95
138
  content: `[CONVERSATION SUMMARY - ${olderMessages.length} earlier messages]\n${summaryLines.join('\n')}`,
96
139
  };
97
140
 
141
+ // Annotate recent messages with time markers and strip metadata
142
+ const annotatedRecent = recentMessages.map(m => this._sanitize(this._annotateWithTime(m)));
143
+
98
144
  // Ensure result starts with user role
99
- const result = [summaryMessage, ...recentMessages];
145
+ const result = [summaryMessage, ...annotatedRecent];
100
146
 
101
147
  // If the first real message after summary is assistant, that's fine since
102
148
  // our summary is role:user. But ensure recent starts correctly.
@@ -105,7 +151,7 @@ export class ConversationManager {
105
151
 
106
152
  addMessage(chatId, role, content) {
107
153
  const history = this.getHistory(chatId);
108
- history.push({ role, content });
154
+ history.push({ role, content, timestamp: Date.now() });
109
155
 
110
156
  // Trim to max history
111
157
  while (history.length > this.maxHistory) {
@@ -17,13 +17,31 @@ const PERSONA_MD = readFileSync(join(__dirname, 'persona.md'), 'utf-8').trim();
17
17
  * @param {string|null} memoriesBlock — relevant episodic/semantic memories
18
18
  * @param {string|null} sharesBlock — pending things to share with the user
19
19
  */
20
- export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null) {
20
+ export function getOrchestratorPrompt(config, skillPrompt = null, userPersona = null, selfData = null, memoriesBlock = null, sharesBlock = null, temporalContext = null) {
21
21
  const workerList = Object.entries(WORKER_TYPES)
22
22
  .map(([key, w]) => ` - **${key}**: ${w.emoji} ${w.description}`)
23
23
  .join('\n');
24
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
+
25
41
  let prompt = `You are ${config.bot.name}, the brain that commands a swarm of specialized worker agents.
26
42
 
43
+ ${timeBlock}
44
+
27
45
  ${PERSONA_MD}
28
46
 
29
47
  ## Your Role
@@ -75,6 +93,13 @@ Before dispatching dangerous tasks (file deletion, force push, \`rm -rf\`, killi
75
93
  - Use \`list_jobs\` to see current job statuses.
76
94
  - Use \`cancel_job\` to stop a running worker.
77
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
+
78
103
  ## Efficiency — Do It Yourself When You Can
79
104
  Workers are expensive (they spin up an entire agent loop with a separate LLM). Only dispatch when the task **actually needs tools**.
80
105
 
@@ -94,6 +119,15 @@ Workers are expensive (they spin up an entire agent loop with a separate LLM). O
94
119
 
95
120
  When results come back from workers, summarize them clearly for the user.
96
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
+
97
131
  ## Automations
98
132
  You can create and manage recurring automations that run on a schedule.
99
133
 
@@ -108,7 +142,15 @@ When a user asks to automate something ("check my server every hour", "news summ
108
142
  When you receive a message starting with [AUTOMATION:], an automation triggered it.
109
143
  Execute the task and report results. Don't create new automations from automated tasks.
110
144
 
111
- 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`;
112
154
 
113
155
  if (selfData) {
114
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}`;
@@ -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.
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);
@@ -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
  /**
@@ -437,6 +459,26 @@ export async function executeOrchestratorTool(name, input, context) {
437
459
  return { automation_id, status: 'deleted', message: `Automation deleted.` };
438
460
  }
439
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
+
440
482
  default:
441
483
  return { error: `Unknown orchestrator tool: ${name}` };
442
484
  }
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');
@@ -353,6 +358,18 @@ export class WorkerAgent {
353
358
  }
354
359
  }
355
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
+
356
373
  _truncateResult(name, result) {
357
374
  let str = JSON.stringify(result);
358
375
  if (str.length <= MAX_RESULT_LENGTH) return str;