bloby-bot 0.19.3 → 0.20.0
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/dist-bloby/assets/{bloby-d8kRAobK.js → bloby-C2KDOC_1.js} +4 -4
- package/dist-bloby/assets/{globals-DgjbJvFE.js → globals-VdwDxdso.js} +2 -2
- package/dist-bloby/assets/{globals-feFDOh3T.css → globals-b7xkhPEo.css} +1 -1
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-CrhNGB5p.js → highlighted-body-OFNGDK62-CdUBnqzY.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-mjSiQkZC.js +1 -0
- package/dist-bloby/assets/{onboard-BaFP_bOF.js → onboard-8MWxCQSm.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +1 -1
- package/supervisor/bloby-agent.ts +14 -1
- package/supervisor/channels/manager.ts +34 -3
- package/supervisor/chat/OnboardWizard.tsx +19 -1
- package/supervisor/delegate-parser.ts +44 -0
- package/supervisor/index.ts +67 -5
- package/supervisor/sub-agent.ts +175 -0
- package/supervisor/task-board.ts +231 -0
- package/worker/prompts/bloby-system-prompt.txt +64 -0
- package/worker/prompts/sub-agent-prompts.ts +111 -0
- package/dist-bloby/assets/mermaid-GHXKKRXX-B7uX_r-j.js +0 -1
package/supervisor/index.ts
CHANGED
|
@@ -14,6 +14,9 @@ import { closeDb, getSession, getSetting } from '../worker/db.js';
|
|
|
14
14
|
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, isBackendStopping, resetBackendRestarts } from './backend.js';
|
|
15
15
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
16
16
|
import { startBlobyAgentQuery, stopBlobyAgentQuery, type RecentMessage } from './bloby-agent.js';
|
|
17
|
+
import { parseDelegateBlocks } from './delegate-parser.js';
|
|
18
|
+
import { spawnSubAgent } from './sub-agent.js';
|
|
19
|
+
import { stopTask, stopAllTasks } from './task-board.js';
|
|
17
20
|
import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
|
|
18
21
|
import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
|
|
19
22
|
import { startScheduler, stopScheduler } from './scheduler.js';
|
|
@@ -1132,7 +1135,12 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
1132
1135
|
const waMirrorJid = waStatus?.connected ? waStatus.info?.phoneNumber : null;
|
|
1133
1136
|
let waChunkBuf = '';
|
|
1134
1137
|
|
|
1135
|
-
// Start
|
|
1138
|
+
// Start orchestrator query (maxTurns: 5 — fast, delegates heavy work)
|
|
1139
|
+
log.info(`[orchestrator] ──── USER MESSAGE ────`);
|
|
1140
|
+
log.info(`[orchestrator] Content: "${content.slice(0, 100)}..."`);
|
|
1141
|
+
log.info(`[orchestrator] Model: ${freshConfig.ai.model}`);
|
|
1142
|
+
log.info(`[orchestrator] Conv: ${convId}`);
|
|
1143
|
+
log.info(`[orchestrator] MaxTurns: 5 (orchestrator mode)`);
|
|
1136
1144
|
agentQueryActive = true;
|
|
1137
1145
|
currentStreamConvId = convId;
|
|
1138
1146
|
currentStreamBuffer = '';
|
|
@@ -1149,8 +1157,10 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
1149
1157
|
waChunkBuf = '';
|
|
1150
1158
|
}
|
|
1151
1159
|
|
|
1152
|
-
// Intercept bot:done —
|
|
1160
|
+
// Intercept bot:done — orchestrator turn finished
|
|
1153
1161
|
if (type === 'bot:done') {
|
|
1162
|
+
log.info(`[orchestrator] ──── TURN COMPLETE ────`);
|
|
1163
|
+
log.info(`[orchestrator] File tools used: ${eventData.usedFileTools}`);
|
|
1154
1164
|
agentQueryActive = false;
|
|
1155
1165
|
currentStreamConvId = null;
|
|
1156
1166
|
currentStreamBuffer = '';
|
|
@@ -1180,20 +1190,55 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
1180
1190
|
waChunkBuf = '';
|
|
1181
1191
|
}
|
|
1182
1192
|
|
|
1193
|
+
// Parse <Delegate> blocks — spawn sub-agents, strip from visible text
|
|
1194
|
+
log.info(`[orchestrator] Parsing response for <Delegate> blocks...`);
|
|
1195
|
+
const { blocks, cleanText } = parseDelegateBlocks(eventData.content || '');
|
|
1196
|
+
|
|
1197
|
+
if (blocks.length) {
|
|
1198
|
+
log.info(`[orchestrator] Found ${blocks.length} delegation(s) — spawning sub-agent(s)`);
|
|
1199
|
+
} else {
|
|
1200
|
+
log.info(`[orchestrator] No delegations — direct response`);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
for (const block of blocks) {
|
|
1204
|
+
log.info(`[orchestrator] Spawning sub-agent: type=${block.type}, task="${block.description.slice(0, 80)}..."`);
|
|
1205
|
+
spawnSubAgent({
|
|
1206
|
+
type: block.type,
|
|
1207
|
+
description: block.description,
|
|
1208
|
+
conversationId: convId,
|
|
1209
|
+
model: freshConfig.ai.model,
|
|
1210
|
+
names: { botName, humanName },
|
|
1211
|
+
broadcastBloby,
|
|
1212
|
+
restartBackend: () => {
|
|
1213
|
+
resetBackendRestarts();
|
|
1214
|
+
stopBackend().then(() => spawnBackend(backendPort));
|
|
1215
|
+
},
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const displayContent = cleanText;
|
|
1220
|
+
|
|
1183
1221
|
(async () => {
|
|
1184
1222
|
try {
|
|
1185
1223
|
await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
|
1186
|
-
role: 'assistant', content:
|
|
1224
|
+
role: 'assistant', content: displayContent, meta: { model: freshConfig.ai.model },
|
|
1187
1225
|
});
|
|
1188
1226
|
} catch (err: any) {
|
|
1189
1227
|
log.warn(`[bloby] DB persist bot response error: ${err.message}`);
|
|
1190
1228
|
}
|
|
1191
1229
|
})();
|
|
1230
|
+
|
|
1231
|
+
// Broadcast cleaned text (without <Delegate> blocks)
|
|
1232
|
+
broadcastBloby('bot:response', { ...eventData, content: displayContent });
|
|
1233
|
+
return; // prevent double-broadcast below
|
|
1192
1234
|
}
|
|
1193
1235
|
|
|
1194
|
-
// Stream all events to every connected client
|
|
1236
|
+
// Stream all other events to every connected client
|
|
1195
1237
|
broadcastBloby(type, eventData);
|
|
1196
|
-
}, data.attachments, savedFiles, { botName, humanName }, recentMessages
|
|
1238
|
+
}, data.attachments, savedFiles, { botName, humanName }, recentMessages,
|
|
1239
|
+
undefined, // no supportPrompt
|
|
1240
|
+
5, // maxTurns: orchestrator mode
|
|
1241
|
+
);
|
|
1197
1242
|
})();
|
|
1198
1243
|
return;
|
|
1199
1244
|
}
|
|
@@ -1231,6 +1276,23 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
|
|
|
1231
1276
|
return;
|
|
1232
1277
|
}
|
|
1233
1278
|
|
|
1279
|
+
if (msg.type === 'user:stop-task') {
|
|
1280
|
+
const taskId = (msg as any).data?.taskId;
|
|
1281
|
+
if (taskId) {
|
|
1282
|
+
log.info(`[orchestrator] Stopping task: ${taskId}`);
|
|
1283
|
+
stopTask(taskId);
|
|
1284
|
+
broadcastBloby('bot:task-stopped', { taskId });
|
|
1285
|
+
}
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
if (msg.type === 'user:stop-all-tasks') {
|
|
1290
|
+
log.info(`[orchestrator] Stopping all background tasks`);
|
|
1291
|
+
stopAllTasks();
|
|
1292
|
+
broadcastBloby('bot:tasks-cleared', {});
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1234
1296
|
if (msg.type === 'user:clear-context') {
|
|
1235
1297
|
(async () => {
|
|
1236
1298
|
try {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-Agent Spawner — creates and manages background agent queries.
|
|
3
|
+
* Sub-agents run independently from the orchestrator, handling
|
|
4
|
+
* heavy work (coding, research) while the user keeps chatting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log } from '../shared/logger.js';
|
|
8
|
+
import { startBlobyAgentQuery } from './bloby-agent.js';
|
|
9
|
+
import { createTask, completeTask, failTask, setOnTaskPromoted, type SubAgentTask, type SubAgentType } from './task-board.js';
|
|
10
|
+
import { assembleSubAgentPrompt } from '../worker/prompts/sub-agent-prompts.js';
|
|
11
|
+
|
|
12
|
+
export interface SpawnOpts {
|
|
13
|
+
type: SubAgentType;
|
|
14
|
+
description: string;
|
|
15
|
+
conversationId: string;
|
|
16
|
+
model: string;
|
|
17
|
+
names: { botName: string; humanName: string };
|
|
18
|
+
broadcastBloby: (type: string, data: any) => void;
|
|
19
|
+
restartBackend: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Pending spawn options for queued tasks, keyed by task ID */
|
|
23
|
+
const pendingOpts = new Map<string, SpawnOpts>();
|
|
24
|
+
|
|
25
|
+
// Register the global promotion handler once (handles all queued tasks)
|
|
26
|
+
setOnTaskPromoted((task: SubAgentTask) => {
|
|
27
|
+
const opts = pendingOpts.get(task.id);
|
|
28
|
+
if (opts) {
|
|
29
|
+
pendingOpts.delete(task.id);
|
|
30
|
+
log.info(`[sub-agent] Queued task ${task.id} promoted — starting agent`);
|
|
31
|
+
opts.broadcastBloby('bot:task-progress', {
|
|
32
|
+
taskId: task.id,
|
|
33
|
+
tool: 'starting',
|
|
34
|
+
});
|
|
35
|
+
runSubAgent(task, opts);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Spawn a background sub-agent for a delegated task.
|
|
41
|
+
* Returns the created task, or null if the queue is full.
|
|
42
|
+
*/
|
|
43
|
+
export function spawnSubAgent(opts: SpawnOpts): SubAgentTask | null {
|
|
44
|
+
log.info(`[sub-agent] ──── SPAWN REQUEST ────`);
|
|
45
|
+
log.info(`[sub-agent] Type: ${opts.type}`);
|
|
46
|
+
log.info(`[sub-agent] Description: "${opts.description.slice(0, 120)}..."`);
|
|
47
|
+
log.info(`[sub-agent] Model: ${opts.model}`);
|
|
48
|
+
log.info(`[sub-agent] Conversation: ${opts.conversationId}`);
|
|
49
|
+
|
|
50
|
+
const task = createTask(opts.type, opts.description, opts.conversationId);
|
|
51
|
+
if (!task) {
|
|
52
|
+
log.warn(`[sub-agent] Could not create task — queue full`);
|
|
53
|
+
opts.broadcastBloby('bot:task-error', {
|
|
54
|
+
error: 'Too many background tasks. Try again in a moment.',
|
|
55
|
+
});
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Broadcast task creation
|
|
60
|
+
opts.broadcastBloby('bot:task-created', {
|
|
61
|
+
taskId: task.id,
|
|
62
|
+
type: task.type,
|
|
63
|
+
description: task.description,
|
|
64
|
+
status: task.status,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (task.status === 'queued') {
|
|
68
|
+
// Store opts so we can start the agent when promoted
|
|
69
|
+
pendingOpts.set(task.id, opts);
|
|
70
|
+
log.info(`[sub-agent] Task ${task.id} queued — will start when a slot opens`);
|
|
71
|
+
return task;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Task is immediately running
|
|
75
|
+
log.info(`[sub-agent] Task ${task.id} starting immediately (slot available)`);
|
|
76
|
+
runSubAgent(task, opts);
|
|
77
|
+
return task;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Actually start the agent query for a task */
|
|
81
|
+
function runSubAgent(task: SubAgentTask, opts: SpawnOpts): void {
|
|
82
|
+
const systemPrompt = assembleSubAgentPrompt(opts.type, opts.names);
|
|
83
|
+
const convId = `subagent-${task.id}`;
|
|
84
|
+
|
|
85
|
+
log.info(`[sub-agent] ──── STARTING AGENT ────`);
|
|
86
|
+
log.info(`[sub-agent] Task ID: ${task.id}`);
|
|
87
|
+
log.info(`[sub-agent] Type: ${task.type}`);
|
|
88
|
+
log.info(`[sub-agent] Conv ID: ${convId}`);
|
|
89
|
+
log.info(`[sub-agent] Prompt length: ${systemPrompt.length} chars`);
|
|
90
|
+
log.info(`[sub-agent] Task description: "${task.description.slice(0, 120)}..."`);
|
|
91
|
+
|
|
92
|
+
let fullText = '';
|
|
93
|
+
let toolCount = 0;
|
|
94
|
+
|
|
95
|
+
startBlobyAgentQuery(
|
|
96
|
+
convId,
|
|
97
|
+
task.description,
|
|
98
|
+
opts.model,
|
|
99
|
+
(type, eventData) => {
|
|
100
|
+
// Track tool usage for progress
|
|
101
|
+
if (type === 'bot:tool') {
|
|
102
|
+
toolCount++;
|
|
103
|
+
const toolName = eventData.name || 'working';
|
|
104
|
+
task.progress.push(toolName);
|
|
105
|
+
if (toolName === 'Write' || toolName === 'Edit') {
|
|
106
|
+
task.usedFileTools = true;
|
|
107
|
+
}
|
|
108
|
+
log.info(`[sub-agent] Task ${task.id} | Tool #${toolCount}: ${toolName}${eventData.input?.file_path ? ` → ${eventData.input.file_path}` : ''}`);
|
|
109
|
+
opts.broadcastBloby('bot:task-progress', {
|
|
110
|
+
taskId: task.id,
|
|
111
|
+
tool: toolName,
|
|
112
|
+
input: eventData.input,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Accumulate response text
|
|
117
|
+
if (type === 'bot:token' && eventData.token) {
|
|
118
|
+
fullText += eventData.token;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Store final response
|
|
122
|
+
if (type === 'bot:response') {
|
|
123
|
+
fullText = eventData.content || fullText;
|
|
124
|
+
log.info(`[sub-agent] Task ${task.id} | Response received (${fullText.length} chars)`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle completion
|
|
128
|
+
if (type === 'bot:done') {
|
|
129
|
+
const usedFiles = task.usedFileTools || eventData.usedFileTools;
|
|
130
|
+
const elapsed = Math.round((Date.now() - task.createdAt) / 1000);
|
|
131
|
+
completeTask(task.id, fullText || '(completed)', usedFiles);
|
|
132
|
+
|
|
133
|
+
log.info(`[sub-agent] ──── TASK COMPLETED ────`);
|
|
134
|
+
log.info(`[sub-agent] Task ID: ${task.id}`);
|
|
135
|
+
log.info(`[sub-agent] Duration: ${elapsed}s`);
|
|
136
|
+
log.info(`[sub-agent] Tools used: ${toolCount}`);
|
|
137
|
+
log.info(`[sub-agent] File tools: ${usedFiles}`);
|
|
138
|
+
log.info(`[sub-agent] Result preview: "${(fullText || '').slice(0, 200)}..."`);
|
|
139
|
+
|
|
140
|
+
opts.broadcastBloby('bot:task-done', {
|
|
141
|
+
taskId: task.id,
|
|
142
|
+
type: task.type,
|
|
143
|
+
description: task.description,
|
|
144
|
+
result: fullText?.slice(0, 500) || '(completed)',
|
|
145
|
+
usedFileTools: usedFiles,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Restart backend if file tools were used
|
|
149
|
+
if (usedFiles) {
|
|
150
|
+
log.info(`[sub-agent] Task ${task.id} used file tools — restarting backend`);
|
|
151
|
+
opts.restartBackend();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle errors
|
|
156
|
+
if (type === 'bot:error') {
|
|
157
|
+
const errorMsg = eventData.error || 'Unknown error';
|
|
158
|
+
failTask(task.id, errorMsg);
|
|
159
|
+
opts.broadcastBloby('bot:task-error', {
|
|
160
|
+
taskId: task.id,
|
|
161
|
+
error: errorMsg,
|
|
162
|
+
});
|
|
163
|
+
log.warn(`[sub-agent] ──── TASK FAILED ────`);
|
|
164
|
+
log.warn(`[sub-agent] Task ID: ${task.id}`);
|
|
165
|
+
log.warn(`[sub-agent] Error: ${errorMsg}`);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
undefined, // no attachments
|
|
169
|
+
undefined, // no savedFiles
|
|
170
|
+
opts.names,
|
|
171
|
+
undefined, // no recentMessages — sub-agents don't need chat history
|
|
172
|
+
systemPrompt, // passed as supportPrompt to bypass main prompt assembly
|
|
173
|
+
50, // maxTurns — full power for sub-agents
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Board — tracks background sub-agent tasks.
|
|
3
|
+
* In-memory only (no DB persistence). Tasks are ephemeral — they exist
|
|
4
|
+
* for the duration of the work and a short window after for reporting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { log } from '../shared/logger.js';
|
|
8
|
+
|
|
9
|
+
export type SubAgentType = 'coder' | 'researcher';
|
|
10
|
+
|
|
11
|
+
export interface SubAgentTask {
|
|
12
|
+
id: string;
|
|
13
|
+
type: SubAgentType;
|
|
14
|
+
description: string;
|
|
15
|
+
status: 'queued' | 'running' | 'done' | 'error';
|
|
16
|
+
progress: string[];
|
|
17
|
+
result?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
usedFileTools: boolean;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
completedAt?: number;
|
|
22
|
+
conversationId: string;
|
|
23
|
+
abortController: AbortController;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tasks = new Map<string, SubAgentTask>();
|
|
27
|
+
const taskQueue: SubAgentTask[] = [];
|
|
28
|
+
const MAX_CONCURRENT = 3;
|
|
29
|
+
const MAX_QUEUED = 5;
|
|
30
|
+
/** Keep completed tasks for 5 minutes before cleanup */
|
|
31
|
+
const COMPLETED_TTL = 5 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
/** Callback invoked when a queued task gets promoted to running */
|
|
34
|
+
let onTaskPromoted: ((task: SubAgentTask) => void) | null = null;
|
|
35
|
+
|
|
36
|
+
/** Register a callback for when queued tasks are promoted */
|
|
37
|
+
export function setOnTaskPromoted(cb: (task: SubAgentTask) => void) {
|
|
38
|
+
onTaskPromoted = cb;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generateId(): string {
|
|
42
|
+
return 'task-' + Math.random().toString(36).slice(2, 8) + Date.now().toString(36).slice(-4);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getRunningCount(): number {
|
|
46
|
+
let count = 0;
|
|
47
|
+
for (const t of tasks.values()) {
|
|
48
|
+
if (t.status === 'running') count++;
|
|
49
|
+
}
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Create a task. Returns the task (queued or running), or null if queue is full. */
|
|
54
|
+
export function createTask(
|
|
55
|
+
type: SubAgentType,
|
|
56
|
+
description: string,
|
|
57
|
+
conversationId: string,
|
|
58
|
+
): SubAgentTask | null {
|
|
59
|
+
const task: SubAgentTask = {
|
|
60
|
+
id: generateId(),
|
|
61
|
+
type,
|
|
62
|
+
description,
|
|
63
|
+
status: 'queued',
|
|
64
|
+
progress: [],
|
|
65
|
+
usedFileTools: false,
|
|
66
|
+
createdAt: Date.now(),
|
|
67
|
+
conversationId,
|
|
68
|
+
abortController: new AbortController(),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (getRunningCount() < MAX_CONCURRENT) {
|
|
72
|
+
task.status = 'running';
|
|
73
|
+
tasks.set(task.id, task);
|
|
74
|
+
log.info(`[task-board] Created task ${task.id} (${type}) — running`);
|
|
75
|
+
return task;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (taskQueue.length >= MAX_QUEUED) {
|
|
79
|
+
log.warn(`[task-board] Queue full (${MAX_QUEUED}) — rejecting task`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
tasks.set(task.id, task);
|
|
84
|
+
taskQueue.push(task);
|
|
85
|
+
log.info(`[task-board] Created task ${task.id} (${type}) — queued (position ${taskQueue.length})`);
|
|
86
|
+
return task;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Mark a task as completed */
|
|
90
|
+
export function completeTask(id: string, result: string, usedFileTools: boolean): void {
|
|
91
|
+
const task = tasks.get(id);
|
|
92
|
+
if (!task) return;
|
|
93
|
+
task.status = 'done';
|
|
94
|
+
task.result = result;
|
|
95
|
+
task.usedFileTools = usedFileTools;
|
|
96
|
+
task.completedAt = Date.now();
|
|
97
|
+
log.info(`[task-board] Task ${id} completed`);
|
|
98
|
+
promoteNext();
|
|
99
|
+
scheduleCleanup(id);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Mark a task as failed */
|
|
103
|
+
export function failTask(id: string, error: string): void {
|
|
104
|
+
const task = tasks.get(id);
|
|
105
|
+
if (!task) return;
|
|
106
|
+
task.status = 'error';
|
|
107
|
+
task.error = error;
|
|
108
|
+
task.completedAt = Date.now();
|
|
109
|
+
log.warn(`[task-board] Task ${id} failed: ${error}`);
|
|
110
|
+
promoteNext();
|
|
111
|
+
scheduleCleanup(id);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Stop a task by aborting its controller */
|
|
115
|
+
export function stopTask(id: string): boolean {
|
|
116
|
+
const task = tasks.get(id);
|
|
117
|
+
if (!task) return false;
|
|
118
|
+
|
|
119
|
+
// If queued, just remove from queue
|
|
120
|
+
if (task.status === 'queued') {
|
|
121
|
+
const idx = taskQueue.indexOf(task);
|
|
122
|
+
if (idx !== -1) taskQueue.splice(idx, 1);
|
|
123
|
+
tasks.delete(id);
|
|
124
|
+
log.info(`[task-board] Removed queued task ${id}`);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (task.status === 'running') {
|
|
129
|
+
task.abortController.abort();
|
|
130
|
+
task.status = 'error';
|
|
131
|
+
task.error = 'Stopped by user';
|
|
132
|
+
task.completedAt = Date.now();
|
|
133
|
+
log.info(`[task-board] Stopped task ${id}`);
|
|
134
|
+
promoteNext();
|
|
135
|
+
scheduleCleanup(id);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Stop all active tasks */
|
|
143
|
+
export function stopAllTasks(): void {
|
|
144
|
+
for (const task of tasks.values()) {
|
|
145
|
+
if (task.status === 'running') {
|
|
146
|
+
task.abortController.abort();
|
|
147
|
+
task.status = 'error';
|
|
148
|
+
task.error = 'Stopped by user';
|
|
149
|
+
task.completedAt = Date.now();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Clear queue
|
|
153
|
+
while (taskQueue.length) {
|
|
154
|
+
const t = taskQueue.pop()!;
|
|
155
|
+
tasks.delete(t.id);
|
|
156
|
+
}
|
|
157
|
+
log.info('[task-board] All tasks stopped');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Get all active (running + queued) tasks */
|
|
161
|
+
export function getActiveTasks(): SubAgentTask[] {
|
|
162
|
+
return Array.from(tasks.values()).filter(
|
|
163
|
+
(t) => t.status === 'running' || t.status === 'queued',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Get recently completed/failed tasks (within TTL window) */
|
|
168
|
+
export function getRecentCompletedTasks(): SubAgentTask[] {
|
|
169
|
+
const cutoff = Date.now() - COMPLETED_TTL;
|
|
170
|
+
return Array.from(tasks.values()).filter(
|
|
171
|
+
(t) => (t.status === 'done' || t.status === 'error') && (t.completedAt || 0) > cutoff,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Format the task board as markdown for system prompt injection */
|
|
176
|
+
export function formatTaskBoard(): string {
|
|
177
|
+
const active = getActiveTasks();
|
|
178
|
+
const completed = getRecentCompletedTasks();
|
|
179
|
+
|
|
180
|
+
if (!active.length && !completed.length) return '';
|
|
181
|
+
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
|
|
184
|
+
if (active.length) {
|
|
185
|
+
lines.push('## Active Tasks');
|
|
186
|
+
for (const t of active) {
|
|
187
|
+
const elapsed = Math.round((Date.now() - t.createdAt) / 1000);
|
|
188
|
+
const time = elapsed < 60 ? `${elapsed}s ago` : `${Math.round(elapsed / 60)}m ago`;
|
|
189
|
+
const lastTool = t.progress.length ? t.progress[t.progress.length - 1] : 'starting';
|
|
190
|
+
lines.push(`- **${t.id}** (${t.type}, ${t.status}): "${t.description.slice(0, 100)}"`);
|
|
191
|
+
lines.push(` Started ${time} | Last: ${lastTool}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (completed.length) {
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push('## Recently Completed Tasks');
|
|
198
|
+
for (const t of completed) {
|
|
199
|
+
const statusLabel = t.status === 'done' ? 'DONE' : `ERROR: ${t.error}`;
|
|
200
|
+
lines.push(`- **${t.id}** (${t.type}, ${statusLabel}): "${t.description.slice(0, 100)}"`);
|
|
201
|
+
if (t.result) {
|
|
202
|
+
lines.push(` Result: ${t.result.slice(0, 300)}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Internal ──
|
|
211
|
+
|
|
212
|
+
function promoteNext() {
|
|
213
|
+
if (!taskQueue.length || getRunningCount() >= MAX_CONCURRENT) return;
|
|
214
|
+
|
|
215
|
+
const next = taskQueue.shift()!;
|
|
216
|
+
next.status = 'running';
|
|
217
|
+
log.info(`[task-board] Promoted queued task ${next.id} to running`);
|
|
218
|
+
|
|
219
|
+
if (onTaskPromoted) {
|
|
220
|
+
onTaskPromoted(next);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function scheduleCleanup(id: string) {
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
const task = tasks.get(id);
|
|
227
|
+
if (task && (task.status === 'done' || task.status === 'error')) {
|
|
228
|
+
tasks.delete(id);
|
|
229
|
+
}
|
|
230
|
+
}, COMPLETED_TTL);
|
|
231
|
+
}
|
|
@@ -204,6 +204,70 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
|
|
|
204
204
|
|
|
205
205
|
---
|
|
206
206
|
|
|
207
|
+
# Delegation
|
|
208
|
+
|
|
209
|
+
You can delegate heavy work to background sub-agents. This lets you respond quickly while work happens in parallel. The user keeps chatting with you while sub-agents build, research, or process in the background.
|
|
210
|
+
|
|
211
|
+
## When to Delegate
|
|
212
|
+
|
|
213
|
+
**Delegate to a sub-agent** (takes many tool calls, runs in background):
|
|
214
|
+
- Building new features (pages, APIs, components)
|
|
215
|
+
- Multi-file refactoring or code changes
|
|
216
|
+
- Complex bug fixes requiring investigation
|
|
217
|
+
- Any coding task that would take more than 2-3 tool calls
|
|
218
|
+
|
|
219
|
+
**Handle directly** (quick, do it yourself):
|
|
220
|
+
- Conversational responses, chitchat, questions
|
|
221
|
+
- Memory file writes (MEMORY.md, MYSELF.md, MYHUMAN.md, daily notes)
|
|
222
|
+
- Config edits (PULSE.json, CRONS.json, MCP.json)
|
|
223
|
+
- Channel configuration (mode, admins)
|
|
224
|
+
- Simple file reads or status checks
|
|
225
|
+
- Setting up reminders or crons
|
|
226
|
+
|
|
227
|
+
## How to Delegate
|
|
228
|
+
|
|
229
|
+
Include a `<Delegate>` block in your response. The supervisor strips it from your visible message, spawns a background coding agent, and your human never sees the XML — they just see your conversational response.
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
<Delegate type="coder">
|
|
233
|
+
Build a contacts page with search and tag filtering.
|
|
234
|
+
- Backend: Express API at /api/contacts (GET, POST, DELETE) using SQLite
|
|
235
|
+
- Frontend: React page at client/src/components/Dashboard/ContactsPage.tsx
|
|
236
|
+
- Add sidebar link and route in App.tsx
|
|
237
|
+
</Delegate>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
You can include multiple `<Delegate>` blocks in one response for parallel work. Always write a natural conversational message alongside the delegation — your human should know what you're doing.
|
|
241
|
+
|
|
242
|
+
**Good example:**
|
|
243
|
+
> "On it! I'll build that contacts page with search and tags. Give me a few minutes."
|
|
244
|
+
> `<Delegate type="coder">...</Delegate>`
|
|
245
|
+
|
|
246
|
+
## Available Sub-Agent Types
|
|
247
|
+
|
|
248
|
+
| Type | Use For |
|
|
249
|
+
|------|---------|
|
|
250
|
+
| `coder` | All coding tasks — features, fixes, refactoring, full-stack development |
|
|
251
|
+
|
|
252
|
+
## Task Board
|
|
253
|
+
|
|
254
|
+
A "Background Tasks" section appears in your context showing active and recently completed sub-agent tasks. Use it to:
|
|
255
|
+
|
|
256
|
+
- **Report results naturally** when tasks complete: "That contacts page is ready — I added search and tag filtering."
|
|
257
|
+
- **Give progress updates** if the user asks: "Still working on it — currently editing the API routes."
|
|
258
|
+
- **Avoid duplicates** — check if a similar task is already running before delegating again.
|
|
259
|
+
|
|
260
|
+
When a task completes, report the results to your human in your next response. Be specific about what was built or changed.
|
|
261
|
+
|
|
262
|
+
## Rules
|
|
263
|
+
|
|
264
|
+
- **Max 3 concurrent sub-agents.** If all slots are busy, tasks queue automatically (up to 5 queued).
|
|
265
|
+
- **Keep task descriptions specific and actionable.** Include file paths, technologies, and acceptance criteria when relevant.
|
|
266
|
+
- **Never delegate memory or config edits** — MYSELF.md, MYHUMAN.md, MEMORY.md, PULSE.json, CRONS.json are yours to manage directly.
|
|
267
|
+
- **Always respond conversationally alongside delegation.** Don't just silently delegate — tell your human what you're doing.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
207
271
|
## Skills
|
|
208
272
|
|
|
209
273
|
Skills live in `skills/` — each skill is a folder with instructions and resources:
|