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.
- package/.env.example +0 -0
- package/README.md +0 -0
- package/bin/kernel.js +56 -2
- package/config.example.yaml +31 -0
- package/package.json +1 -1
- package/src/agent.js +200 -32
- package/src/automation/automation-manager.js +0 -0
- package/src/automation/automation.js +0 -0
- package/src/automation/index.js +0 -0
- package/src/automation/scheduler.js +0 -0
- package/src/bot.js +402 -6
- package/src/claude-auth.js +0 -0
- package/src/coder.js +0 -0
- package/src/conversation.js +51 -5
- package/src/intents/detector.js +0 -0
- package/src/intents/index.js +0 -0
- package/src/intents/planner.js +0 -0
- package/src/life/codebase.js +388 -0
- package/src/life/engine.js +1317 -0
- package/src/life/evolution.js +244 -0
- package/src/life/improvements.js +81 -0
- package/src/life/journal.js +109 -0
- package/src/life/memory.js +283 -0
- package/src/life/share-queue.js +136 -0
- package/src/persona.js +0 -0
- package/src/prompts/orchestrator.js +62 -2
- package/src/prompts/persona.md +7 -0
- package/src/prompts/system.js +0 -0
- package/src/prompts/workers.js +10 -9
- package/src/providers/anthropic.js +0 -0
- package/src/providers/base.js +0 -0
- package/src/providers/index.js +0 -0
- package/src/providers/models.js +8 -1
- package/src/providers/openai-compat.js +0 -0
- package/src/security/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +0 -0
- package/src/self.js +0 -0
- package/src/services/stt.js +0 -0
- package/src/services/tts.js +0 -0
- package/src/skills/catalog.js +0 -0
- package/src/skills/custom.js +0 -0
- package/src/swarm/job-manager.js +0 -0
- package/src/swarm/job.js +11 -0
- package/src/swarm/worker-registry.js +0 -0
- package/src/tools/browser.js +0 -0
- package/src/tools/categories.js +0 -0
- package/src/tools/coding.js +1 -1
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +0 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +60 -3
- package/src/tools/os.js +0 -0
- package/src/tools/persona.js +0 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +0 -0
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- 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
|
}
|
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/prompts/system.js
CHANGED
|
File without changes
|
package/src/prompts/workers.js
CHANGED
|
@@ -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
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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
|
-
-
|
|
19
|
-
- NEVER write code yourself with read_file/write_file. ALWAYS
|
|
20
|
-
-
|
|
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
|
package/src/providers/base.js
CHANGED
|
File without changes
|
package/src/providers/index.js
CHANGED
|
File without changes
|
package/src/providers/models.js
CHANGED
|
@@ -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
|
package/src/security/audit.js
CHANGED
|
File without changes
|
package/src/security/auth.js
CHANGED
|
File without changes
|
package/src/security/confirm.js
CHANGED
|
File without changes
|
package/src/self.js
CHANGED
|
File without changes
|
package/src/services/stt.js
CHANGED
|
File without changes
|
package/src/services/tts.js
CHANGED
|
File without changes
|
package/src/skills/catalog.js
CHANGED
|
File without changes
|
package/src/skills/custom.js
CHANGED
|
File without changes
|
package/src/swarm/job-manager.js
CHANGED
|
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
|
package/src/tools/browser.js
CHANGED
|
File without changes
|
package/src/tools/categories.js
CHANGED
|
File without changes
|
package/src/tools/coding.js
CHANGED
package/src/tools/docker.js
CHANGED
|
File without changes
|
package/src/tools/git.js
CHANGED
|
File without changes
|
package/src/tools/github.js
CHANGED
|
File without changes
|
package/src/tools/index.js
CHANGED
|
File without changes
|
package/src/tools/jira.js
CHANGED
|
File without changes
|
package/src/tools/monitor.js
CHANGED
|
File without changes
|
package/src/tools/network.js
CHANGED
|
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
|
-
|
|
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: ${
|
|
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
|
package/src/tools/persona.js
CHANGED
|
File without changes
|
package/src/tools/process.js
CHANGED
|
File without changes
|
package/src/utils/config.js
CHANGED
|
File without changes
|
package/src/utils/display.js
CHANGED
|
File without changes
|
package/src/utils/logger.js
CHANGED
|
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
|
|
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
|
|
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;
|