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 +1 -1
- package/src/agent.js +52 -14
- package/src/bot.js +99 -2
- package/src/conversation.js +51 -5
- package/src/prompts/orchestrator.js +44 -2
- package/src/prompts/persona.md +7 -0
- package/src/swarm/job.js +11 -0
- package/src/tools/orchestrator-tools.js +42 -0
- package/src/worker.js +17 -0
package/package.json
CHANGED
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
|
|
844
|
-
|
|
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
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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, {
|
|
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
|
});
|
package/src/conversation.js
CHANGED
|
@@ -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
|
|
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, ...
|
|
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}`;
|
package/src/prompts/persona.md
CHANGED
|
@@ -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;
|