bloby-bot 0.20.0 → 0.20.1

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": "bloby-bot",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "releaseNotes": [
5
5
  "1. react router implemented",
6
6
  "2. new workspace design",
@@ -51,7 +51,7 @@
51
51
  "dev:docs": "cd ./docs && npx fumapress"
52
52
  },
53
53
  "dependencies": {
54
- "@anthropic-ai/claude-agent-sdk": "^0.2.50",
54
+ "@anthropic-ai/claude-agent-sdk": "^0.2.97",
55
55
  "@clack/prompts": "^1.1.0",
56
56
  "@streamdown/code": "^1.1.1",
57
57
  "@tailwindcss/vite": "^4.2.0",
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Agent Definitions — scalable sub-agent configuration.
3
+ *
4
+ * Each agent has:
5
+ * - A config entry in AGENT_CONFIGS (tools, maxTurns, model, etc.)
6
+ * - A prompt file in ./prompts/{name}.txt
7
+ *
8
+ * Adding a new agent: 1) create prompts/{name}.txt, 2) add config entry below.
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { log } from '../../shared/logger.js';
14
+ import { WORKSPACE_DIR } from '../../shared/paths.js';
15
+
16
+ const PROMPTS_DIR = path.join(import.meta.dirname, 'prompts');
17
+
18
+ /** Agent config without prompt (prompt is loaded from file) */
19
+ interface AgentConfig {
20
+ description: string;
21
+ promptFile: string;
22
+ background: boolean;
23
+ maxTurns: number;
24
+ permissionMode?: string;
25
+ tools?: string[];
26
+ disallowedTools?: string[];
27
+ effort?: 'low' | 'medium' | 'high' | 'max';
28
+ model?: string;
29
+ memory?: 'user' | 'project' | 'local';
30
+ }
31
+
32
+ // ── Agent Configurations ──────────────────────────────────────────────────────
33
+ // Add new agents here. Each needs a matching prompts/{promptFile}.
34
+
35
+ const AGENT_CONFIGS: Record<string, AgentConfig> = {
36
+ coder: {
37
+ description: 'Handles all coding tasks: building features, fixing bugs, refactoring, full-stack development in the workspace. Use this for any task that requires reading, writing, or editing code files.',
38
+ promptFile: 'coder.txt',
39
+ background: true,
40
+ maxTurns: 50,
41
+ permissionMode: 'bypassPermissions',
42
+ effort: 'high',
43
+ },
44
+
45
+ // ── Future agents ──
46
+ // researcher: {
47
+ // description: 'Researches topics on the web, gathers data, writes findings to files.',
48
+ // promptFile: 'researcher.txt',
49
+ // background: true,
50
+ // maxTurns: 30,
51
+ // disallowedTools: ['Write', 'Edit'],
52
+ // effort: 'medium',
53
+ // },
54
+ //
55
+ // marketplace: {
56
+ // description: 'Browses the Bloby Marketplace, installs and configures skills.',
57
+ // promptFile: 'marketplace.txt',
58
+ // background: true,
59
+ // maxTurns: 20,
60
+ // permissionMode: 'bypassPermissions',
61
+ // effort: 'medium',
62
+ // },
63
+ };
64
+
65
+ // ── Helpers ──────────────────────────────────────────────────────────────────
66
+
67
+ /** Read a workspace memory file, return '(empty)' if missing */
68
+ function readMemoryFile(filename: string): string {
69
+ try {
70
+ const content = fs.readFileSync(path.join(WORKSPACE_DIR, filename), 'utf-8').trim();
71
+ return content || '(empty)';
72
+ } catch {
73
+ return '(empty)';
74
+ }
75
+ }
76
+
77
+ /** Read a prompt file from the prompts directory */
78
+ function readPromptFile(filename: string): string {
79
+ const filePath = path.join(PROMPTS_DIR, filename);
80
+ try {
81
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
82
+ if (!content) {
83
+ log.warn(`[agents] Prompt file empty: ${filename}`);
84
+ return 'You are a background sub-agent. Complete the task described below.';
85
+ }
86
+ return content;
87
+ } catch (err) {
88
+ log.warn(`[agents] Could not read prompt file ${filename}: ${err}`);
89
+ return 'You are a background sub-agent. Complete the task described below.';
90
+ }
91
+ }
92
+
93
+ // ── Public API ──────────────────────────────────────────────────────────────
94
+
95
+ /**
96
+ * Build the agents config for the SDK's query() options.
97
+ * Reads prompt files and injects project memory.
98
+ */
99
+ export function buildAgents(): Record<string, any> {
100
+ const agents: Record<string, any> = {};
101
+ const memory = readMemoryFile('MEMORY.md');
102
+
103
+ for (const [name, config] of Object.entries(AGENT_CONFIGS)) {
104
+ let prompt = readPromptFile(config.promptFile);
105
+
106
+ // Inject project memory at the end of the prompt
107
+ prompt += `\n\n---\n\n# Project Memory\n\n## MEMORY.md\n${memory}`;
108
+
109
+ agents[name] = {
110
+ description: config.description,
111
+ prompt,
112
+ background: config.background,
113
+ maxTurns: config.maxTurns,
114
+ ...(config.permissionMode && { permissionMode: config.permissionMode }),
115
+ ...(config.tools && { tools: config.tools }),
116
+ ...(config.disallowedTools && { disallowedTools: config.disallowedTools }),
117
+ ...(config.effort && { effort: config.effort }),
118
+ ...(config.model && { model: config.model }),
119
+ ...(config.memory && { memory: config.memory }),
120
+ };
121
+
122
+ log.info(`[agents] Loaded agent "${name}" — prompt: ${prompt.length} chars, bg: ${config.background}, maxTurns: ${config.maxTurns}`);
123
+ }
124
+
125
+ log.info(`[agents] Built ${Object.keys(agents).length} agent definition(s): ${Object.keys(agents).join(', ')}`);
126
+ return agents;
127
+ }
@@ -0,0 +1,120 @@
1
+ # Sub-Agent: Coder
2
+
3
+ You are a background coding agent. You have been delegated a specific task by the orchestrator — complete it fully and report what you did.
4
+
5
+ ## How to Work
6
+ - Focus only on the task described in your prompt
7
+ - Complete the full task before responding
8
+ - Do not chat, greet, or explain what you will do — just do it
9
+ - If something is ambiguous, make a reasonable choice and note it in your summary
10
+ - When finished, write a concise summary of what you changed and why
11
+
12
+ ## Constraints
13
+ - Do NOT edit: PULSE.json, CRONS.json, MYSELF.md, MYHUMAN.md — those are orchestrator-only
14
+ - You MAY write to memory/daily notes if you learn something worth remembering
15
+ - You MAY read any file in the workspace
16
+ - You have full tool access: Read, Write, Edit, Bash, Glob, Grep
17
+
18
+ ---
19
+
20
+ # Coding Excellence
21
+
22
+ ## Action Orientation
23
+ Do things, don't describe them. When asked to build something, build it. When asked to fix something, fix it. Accept ambitious tasks — you're often the difference between "too complex" and "done."
24
+
25
+ ## Read Before Modify
26
+ Always read code before changing it. Understand what exists. Never propose changes to code you haven't read.
27
+
28
+ ## Simplicity
29
+ No over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.
30
+ - Don't add features, refactoring, or "improvements" beyond what was asked
31
+ - Don't add docstrings, comments, or type annotations to code you didn't change
32
+ - Don't add error handling for scenarios that can't happen
33
+ - Trust internal code and framework guarantees — validate only at system boundaries
34
+ - Don't create helpers or abstractions for one-time operations
35
+ - Three similar lines of code is better than a premature abstraction
36
+
37
+ ## Prefer Editing Over Creating
38
+ Always prefer editing existing files over creating new ones. This prevents file bloat and builds on existing work. Don't create files unless absolutely necessary.
39
+
40
+ ## Careful Execution
41
+ Consider the reversibility and blast radius of actions. Prefer `trash` over `rm` — recoverable beats gone forever. If something fails, pivot — don't retry the same thing blindly. Read error messages carefully and address root causes.
42
+
43
+ ## Parallel Operations
44
+ Run independent tool calls in parallel. Don't serialize what can run concurrently.
45
+
46
+ ## Security
47
+ Be aware of OWASP top 10 vulnerabilities. Sanitize user input at boundaries. Never hardcode secrets.
48
+
49
+ ## Error Philosophy
50
+ Graceful failure. Read error messages carefully. Don't brute-force past errors.
51
+
52
+ ---
53
+
54
+ # Workspace Architecture
55
+
56
+ Your working directory is the `workspace/` folder. This is your full-stack workspace:
57
+
58
+ - **Frontend**: `client/` (React + Vite + TailwindCSS). Edit files in `client/src/`
59
+ - **Backend**: `backend/` (Node.js/Express). Entry point: `backend/index.ts`
60
+ - **Database**: `app.db` (SQLite via `better-sqlite3`)
61
+ - **Environment**: `.env` — managed through the chat UI. When you need environment variables, use this XML format so the UI renders an interactive form:
62
+
63
+ ```
64
+ <EnvGroup title="Service Name Configuration">
65
+ <EnvInput name="ENV_VAR_NAME" label="Human-readable Label" placeholder="example_value..." />
66
+ </EnvGroup>
67
+ ```
68
+
69
+ ## Backend Routing (Critical)
70
+
71
+ A supervisor process sits in front of everything on port 3000. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
72
+
73
+ ```
74
+ Browser: GET /app/api/tasks → Supervisor strips /app → Backend receives: GET /api/tasks
75
+ ```
76
+
77
+ **The rules:**
78
+ - **Frontend** fetch calls: use `/app/api/...`
79
+ - **Backend** Express routes: register as `/api/tasks`, `/api/health`
80
+ - The `/app` prefix distinguishes workspace backend routes from system routes
81
+
82
+ ## Frontend Routing (React Router)
83
+
84
+ Routes are defined in `client/src/App.tsx`. The sidebar uses `NavLink` for navigation.
85
+
86
+ ### Adding a New Full-Stack Page (Example)
87
+
88
+ 1. **Backend route** (`backend/index.ts`): register Express route at `/api/...`
89
+ 2. **Frontend page** (`client/src/components/Dashboard/`): create React component
90
+ 3. **Add route** (`client/src/App.tsx`): add `<Route>` inside `<Routes>`
91
+ 4. **Add sidebar link** (`client/src/components/Layout/Sidebar.tsx`): add `<NavItem>`
92
+
93
+ ## Build Rules
94
+ NEVER run `npm run build`, `vite build`, or any build commands. Vite HMR handles frontend. Backend auto-restarts on file edit.
95
+
96
+ ## Installing Packages
97
+ ```bash
98
+ npm install <package> # run from your cwd (workspace root)
99
+ ```
100
+ Packages are isolated to the workspace. Never modify the parent's package.json.
101
+
102
+ ## Backend Lifecycle (Critical)
103
+
104
+ The supervisor manages the backend process:
105
+ - Editing `.ts`, `.js`, or `.json` files in `backend/` → auto-restart
106
+ - Editing `.env` → auto-restart
107
+ - After your turn ends, if you used Write or Edit tools → auto-restart
108
+ - The backend does NOT restart mid-turn — edits are batched
109
+
110
+ **NEVER** kill processes, run `bloby start`, or run `npm start` directly.
111
+
112
+ ## Sacred Files — NEVER Modify
113
+ - `supervisor/` — chat UI, proxy, process management
114
+ - `worker/` — platform APIs and database
115
+ - `shared/` — shared utilities
116
+ - `bin/` — CLI entry point
117
+
118
+ ## Modular Philosophy
119
+
120
+ Build **mini apps**: a new page component, a new React Router route, and a sidebar link. Each feature lives on its own page. Add a small dashboard widget when it makes sense.
@@ -11,7 +11,7 @@ import { WORKSPACE_DIR } from '../shared/paths.js';
11
11
  import type { SavedFile } from './file-saver.js';
12
12
  import { getClaudeAccessToken } from '../worker/claude-auth.js';
13
13
  import { assembleSystemPrompt } from '../worker/prompts/prompt-assembler.js';
14
- import { formatTaskBoard } from './task-board.js';
14
+ import { buildAgents } from './agents/index.js';
15
15
 
16
16
  export interface RecentMessage {
17
17
  role: 'user' | 'assistant';
@@ -20,6 +20,7 @@ export interface RecentMessage {
20
20
 
21
21
  interface ActiveQuery {
22
22
  abortController: AbortController;
23
+ queryHandle?: any; // SDK query handle for stopTask()
23
24
  }
24
25
 
25
26
  const activeQueries = new Map<string, ActiveQuery>();
@@ -144,20 +145,13 @@ export async function startBlobyAgentQuery(
144
145
  }
145
146
  } catch {}
146
147
 
147
- // Inject task board (active + recently completed sub-agent tasks)
148
- const taskBoard = formatTaskBoard();
149
- if (taskBoard) {
150
- log.info(`[bloby-agent] Injecting task board into system prompt (${taskBoard.length} chars)`);
151
- enrichedPrompt += `\n\n---\n# Background Tasks\n${taskBoard}`;
152
- }
148
+ // Task board is now managed natively by the SDK via background agents
153
149
  }
154
150
 
155
151
  if (recentMessages?.length) {
156
152
  enrichedPrompt += `\n\n---\n# Recent Conversation\n${formatConversationHistory(recentMessages)}`;
157
153
  }
158
154
 
159
- activeQueries.set(conversationId, { abortController });
160
-
161
155
  let fullText = '';
162
156
  const usedTools = new Set<string>();
163
157
  let stderrBuf = '';
@@ -198,7 +192,15 @@ export async function startBlobyAgentQuery(
198
192
  } catch {}
199
193
 
200
194
  const effectiveMaxTurns = maxTurns ?? 50;
201
- log.info(`[bloby-agent] Starting query: conv=${conversationId}, model=${model}, maxTurns=${effectiveMaxTurns}, promptLen=${enrichedPrompt.length}`);
195
+
196
+ // Build sub-agent definitions (only for orchestrator/admin, not customer support)
197
+ let agents: Record<string, any> | undefined;
198
+ if (!supportPrompt && effectiveMaxTurns <= 10) {
199
+ agents = buildAgents();
200
+ log.info(`[bloby-agent] Orchestrator mode — loaded ${Object.keys(agents).length} sub-agent(s): ${Object.keys(agents).join(', ')}`);
201
+ }
202
+
203
+ log.info(`[bloby-agent] Starting query: conv=${conversationId}, model=${model}, maxTurns=${effectiveMaxTurns}, agents=${agents ? Object.keys(agents).join(',') : 'none'}, promptLen=${enrichedPrompt.length}`);
202
204
 
203
205
  const claudeQuery = query({
204
206
  prompt: sdkPrompt,
@@ -211,6 +213,8 @@ export async function startBlobyAgentQuery(
211
213
  abortController,
212
214
  systemPrompt: enrichedPrompt,
213
215
  mcpServers,
216
+ ...(agents && { agents }),
217
+ ...(agents && { agentProgressSummaries: true }),
214
218
  stderr: (chunk: string) => { stderrBuf += chunk; },
215
219
  env: {
216
220
  ...process.env as Record<string, string>,
@@ -220,6 +224,9 @@ export async function startBlobyAgentQuery(
220
224
  },
221
225
  });
222
226
 
227
+ // Store query handle for stopTask() support
228
+ activeQueries.set(conversationId, { abortController, queryHandle: claudeQuery });
229
+
223
230
  onMessage('bot:typing', { conversationId });
224
231
 
225
232
  for await (const msg of claudeQuery) {
@@ -265,6 +272,51 @@ export async function startBlobyAgentQuery(
265
272
  status: 'running',
266
273
  });
267
274
  break;
275
+
276
+ // ── Background sub-agent events (SDK-managed) ──
277
+ case 'system': {
278
+ const sysMsg = msg as any;
279
+ if (sysMsg.subtype === 'task_started') {
280
+ log.info(`[bloby-agent] ──── SUB-AGENT STARTED ────`);
281
+ log.info(`[bloby-agent] Task ID: ${sysMsg.task_id}`);
282
+ log.info(`[bloby-agent] Description: ${sysMsg.description}`);
283
+ log.info(`[bloby-agent] Type: ${sysMsg.task_type || 'agent'}`);
284
+ onMessage('bot:task-created', {
285
+ conversationId,
286
+ taskId: sysMsg.task_id,
287
+ description: sysMsg.description,
288
+ type: sysMsg.task_type,
289
+ });
290
+ } else if (sysMsg.subtype === 'task_progress') {
291
+ const summary = sysMsg.summary || sysMsg.last_tool_name || 'working';
292
+ log.info(`[bloby-agent] Sub-agent ${sysMsg.task_id} | Progress: ${summary} | Tools: ${sysMsg.usage?.tool_uses || 0} | ${Math.round((sysMsg.usage?.duration_ms || 0) / 1000)}s`);
293
+ onMessage('bot:task-progress', {
294
+ conversationId,
295
+ taskId: sysMsg.task_id,
296
+ summary,
297
+ lastTool: sysMsg.last_tool_name,
298
+ usage: sysMsg.usage,
299
+ });
300
+ } else if (sysMsg.subtype === 'task_notification') {
301
+ log.info(`[bloby-agent] ──── SUB-AGENT ${sysMsg.status?.toUpperCase()} ────`);
302
+ log.info(`[bloby-agent] Task ID: ${sysMsg.task_id}`);
303
+ log.info(`[bloby-agent] Status: ${sysMsg.status}`);
304
+ log.info(`[bloby-agent] Summary: ${sysMsg.summary?.slice(0, 200)}`);
305
+ log.info(`[bloby-agent] Tokens: ${sysMsg.usage?.total_tokens || 0} | Tools: ${sysMsg.usage?.tool_uses || 0} | Duration: ${Math.round((sysMsg.usage?.duration_ms || 0) / 1000)}s`);
306
+ onMessage('bot:task-done', {
307
+ conversationId,
308
+ taskId: sysMsg.task_id,
309
+ status: sysMsg.status,
310
+ summary: sysMsg.summary,
311
+ usage: sysMsg.usage,
312
+ });
313
+ // If the sub-agent wrote files, flag it
314
+ if (sysMsg.status === 'completed') {
315
+ usedTools.add('Write'); // ensure backend restart
316
+ }
317
+ }
318
+ break;
319
+ }
268
320
  }
269
321
  }
270
322
 
@@ -295,3 +347,14 @@ export function stopBlobyAgentQuery(conversationId: string): void {
295
347
  activeQueries.delete(conversationId);
296
348
  }
297
349
  }
350
+
351
+ /** Stop a specific background sub-agent task */
352
+ export async function stopSubAgentTask(conversationId: string, taskId: string): Promise<void> {
353
+ const q = activeQueries.get(conversationId);
354
+ if (q?.queryHandle?.stopTask) {
355
+ log.info(`[bloby-agent] Stopping sub-agent task: ${taskId}`);
356
+ await q.queryHandle.stopTask(taskId);
357
+ } else {
358
+ log.warn(`[bloby-agent] Cannot stop task ${taskId} — no active query for ${conversationId}`);
359
+ }
360
+ }
@@ -21,8 +21,6 @@ import { loadConfig } from '../../shared/config.js';
21
21
  import { WORKSPACE_DIR } from '../../shared/paths.js';
22
22
  import { log } from '../../shared/logger.js';
23
23
  import { startBlobyAgentQuery, type RecentMessage } from '../bloby-agent.js';
24
- import { parseDelegateBlocks } from '../delegate-parser.js';
25
- import { spawnSubAgent } from '../sub-agent.js';
26
24
  import { WhatsAppChannel } from './whatsapp.js';
27
25
  import type { ChannelConfig, ChannelProvider, ChannelStatus, ChannelType, InboundMessage, InboundMessageAttachment, SenderRole } from './types.js';
28
26
  import type { AgentAttachment } from '../bloby-agent.js';
@@ -416,29 +414,6 @@ export class ChannelManager {
416
414
  }
417
415
 
418
416
  if (type === 'bot:response' && eventData.content) {
419
- // Parse <Delegate> blocks — spawn sub-agents, strip from visible text
420
- log.info(`[channels] Admin response — parsing for <Delegate> blocks...`);
421
- const { blocks, cleanText } = parseDelegateBlocks(eventData.content);
422
-
423
- if (blocks.length) {
424
- log.info(`[channels] Found ${blocks.length} delegation(s) from WhatsApp admin response`);
425
- }
426
-
427
- for (const block of blocks) {
428
- log.info(`[channels] Spawning sub-agent from WhatsApp: type=${block.type}`);
429
- spawnSubAgent({
430
- type: block.type,
431
- description: block.description,
432
- conversationId: convId,
433
- model,
434
- names: { botName, humanName },
435
- broadcastBloby,
436
- restartBackend: () => this.opts.restartBackend(),
437
- });
438
- }
439
-
440
- const displayContent = cleanText;
441
-
442
417
  // Send remaining text after the last tool use (or the full response if no tools were used)
443
418
  const remaining = waChunkBuf.trim();
444
419
  if (remaining) {
@@ -448,20 +423,16 @@ export class ChannelManager {
448
423
  waChunkBuf = '';
449
424
  }
450
425
 
451
- // Save cleaned response to DB
426
+ // Save response to DB
452
427
  workerApi(`/api/conversations/${convId}/messages`, 'POST', {
453
428
  role: 'assistant',
454
- content: displayContent,
429
+ content: eventData.content,
455
430
  meta: { model },
456
431
  }).catch(() => {});
457
-
458
- // Mirror cleaned response to chat clients
459
- broadcastBloby('bot:response', { ...eventData, content: displayContent });
460
- return; // prevent double-broadcast below
461
432
  }
462
433
 
463
- // Mirror streaming to chat clients
464
- if (type === 'bot:token' || type === 'bot:typing' || type === 'bot:tool') {
434
+ // Mirror streaming + task events to chat clients
435
+ if (type === 'bot:token' || type === 'bot:response' || type === 'bot:typing' || type === 'bot:tool' || type.startsWith('bot:task-')) {
465
436
  broadcastBloby(type, eventData);
466
437
  }
467
438
 
@@ -13,10 +13,7 @@ import { createWorkerApp } from '../worker/index.js';
13
13
  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
- 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';
16
+ import { startBlobyAgentQuery, stopBlobyAgentQuery, stopSubAgentTask, type RecentMessage } from './bloby-agent.js';
20
17
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
21
18
  import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
22
19
  import { startScheduler, stopScheduler } from './scheduler.js';
@@ -1190,50 +1187,19 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1190
1187
  waChunkBuf = '';
1191
1188
  }
1192
1189
 
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
-
1221
1190
  (async () => {
1222
1191
  try {
1223
1192
  await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
1224
- role: 'assistant', content: displayContent, meta: { model: freshConfig.ai.model },
1193
+ role: 'assistant', content: eventData.content, meta: { model: freshConfig.ai.model },
1225
1194
  });
1226
1195
  } catch (err: any) {
1227
1196
  log.warn(`[bloby] DB persist bot response error: ${err.message}`);
1228
1197
  }
1229
1198
  })();
1230
-
1231
- // Broadcast cleaned text (without <Delegate> blocks)
1232
- broadcastBloby('bot:response', { ...eventData, content: displayContent });
1233
- return; // prevent double-broadcast below
1234
1199
  }
1235
1200
 
1236
- // Stream all other events to every connected client
1201
+ // Stream all events to every connected client
1202
+ // (includes bot:task-created, bot:task-progress, bot:task-done from SDK)
1237
1203
  broadcastBloby(type, eventData);
1238
1204
  }, data.attachments, savedFiles, { botName, humanName }, recentMessages,
1239
1205
  undefined, // no supportPrompt
@@ -1279,20 +1245,14 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1279
1245
  if (msg.type === 'user:stop-task') {
1280
1246
  const taskId = (msg as any).data?.taskId;
1281
1247
  if (taskId) {
1282
- log.info(`[orchestrator] Stopping task: ${taskId}`);
1283
- stopTask(taskId);
1284
- broadcastBloby('bot:task-stopped', { taskId });
1248
+ log.info(`[orchestrator] Stopping sub-agent task: ${taskId}`);
1249
+ stopSubAgentTask(convId, taskId).catch((err) => {
1250
+ log.warn(`[orchestrator] Failed to stop task ${taskId}: ${err.message}`);
1251
+ });
1285
1252
  }
1286
1253
  return;
1287
1254
  }
1288
1255
 
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
-
1296
1256
  if (msg.type === 'user:clear-context') {
1297
1257
  (async () => {
1298
1258
  try {
@@ -206,65 +206,37 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
206
206
 
207
207
  # Delegation
208
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.
209
+ You have background sub-agents available via the Agent tool. **Always delegate work never do coding or heavy tasks yourself.** You are the conversational orchestrator. Your sub-agents do the actual work.
210
210
 
211
- ## When to Delegate
211
+ ## How It Works
212
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
213
+ You have an Agent tool with background agents available. When you invoke one, it runs in the background — you respond immediately to your human while the agent works. You'll receive progress updates and completion notifications automatically.
218
214
 
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>`
215
+ ## CRITICAL: Always Delegate
245
216
 
246
- ## Available Sub-Agent Types
217
+ **You MUST delegate** (use your sub-agents):
218
+ - ALL coding tasks — building features, fixing bugs, refactoring, any file editing
219
+ - ALL workspace modifications — creating pages, APIs, components
220
+ - Complex research or data gathering
221
+ - Any task that requires tool use (Read, Write, Edit, Bash)
247
222
 
248
- | Type | Use For |
249
- |------|---------|
250
- | `coder` | All coding tasks features, fixes, refactoring, full-stack development |
251
-
252
- ## Task Board
223
+ **You respond directly** (NO delegation needed):
224
+ - Conversational responses, chitchat, questions
225
+ - Answering questions from memory/context already in your prompt
226
+ - Acknowledging requests and telling your human what you're doing
253
227
 
254
- A "Background Tasks" section appears in your context showing active and recently completed sub-agent tasks. Use it to:
228
+ **You NEVER use tools directly.** No Read, Write, Edit, Bash, Glob, Grep. If you need to do any of those things, delegate to a sub-agent. You are the friendly conversational layer — your sub-agents are the workers.
255
229
 
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.
230
+ ## When a sub-agent completes
259
231
 
260
- When a task completes, report the results to your human in your next response. Be specific about what was built or changed.
232
+ You'll be notified with a summary of what was done. Report the results naturally to your human: "Done! I built the contacts page with search and tags. Check it out!"
261
233
 
262
234
  ## Rules
263
235
 
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.
236
+ - **Keep task descriptions specific and actionable.** Include what to build, which files, acceptance criteria.
237
+ - **Always respond conversationally alongside delegation.** Don't go silent tell your human what you're doing.
238
+ - **Report results** when sub-agents finish. Be specific about what changed.
239
+ - **You can run multiple agents** in parallel if the user asks for several things at once.
268
240
 
269
241
  ---
270
242
 
@@ -1,44 +0,0 @@
1
- /**
2
- * Parse <Delegate> blocks from agent responses.
3
- * Same pattern as <Message> parsing in scheduler.ts.
4
- */
5
-
6
- import { log } from '../shared/logger.js';
7
- import type { SubAgentType } from './task-board.js';
8
-
9
- export interface DelegateBlock {
10
- type: SubAgentType;
11
- description: string;
12
- }
13
-
14
- const DELEGATE_REGEX = /<Delegate\s+type="(\w+)">([\s\S]*?)<\/Delegate>/g;
15
-
16
- /**
17
- * Extract <Delegate> blocks from response text.
18
- * Returns the parsed blocks and the cleaned text with blocks stripped.
19
- */
20
- export function parseDelegateBlocks(text: string): { blocks: DelegateBlock[]; cleanText: string } {
21
- const blocks: DelegateBlock[] = [];
22
-
23
- let match;
24
- // Reset lastIndex since we reuse the regex
25
- DELEGATE_REGEX.lastIndex = 0;
26
- while ((match = DELEGATE_REGEX.exec(text)) !== null) {
27
- const type = match[1] as SubAgentType;
28
- const description = match[2].trim();
29
- log.info(`[delegate-parser] Found <Delegate type="${type}"> — "${description.slice(0, 80)}..."`);
30
- blocks.push({ type, description });
31
- }
32
-
33
- // Strip delegate blocks from visible text
34
- let cleanText = text.replace(DELEGATE_REGEX, '').trim();
35
-
36
- // Clean up excessive newlines left by stripping
37
- cleanText = cleanText.replace(/\n{3,}/g, '\n\n');
38
-
39
- if (blocks.length) {
40
- log.info(`[delegate-parser] Parsed ${blocks.length} delegate block(s), cleaned text: "${cleanText.slice(0, 80)}..."`);
41
- }
42
-
43
- return { blocks, cleanText };
44
- }
@@ -1,175 +0,0 @@
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
- }
@@ -1,231 +0,0 @@
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
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * Sub-agent System Prompt Builder
3
- *
4
- * Builds task-specific system prompts for background sub-agents.
5
- * Extracts relevant sections from the main bloby-system-prompt.txt.
6
- */
7
-
8
- import fs from 'fs';
9
- import path from 'path';
10
- import { WORKSPACE_DIR } from '../../shared/paths.js';
11
- import { log } from '../../shared/logger.js';
12
-
13
- const PROMPT_FILE = path.join(import.meta.dirname, 'bloby-system-prompt.txt');
14
-
15
- /** Read a workspace memory file, return '(empty)' if missing */
16
- function readMemoryFile(filename: string): string {
17
- try {
18
- const content = fs.readFileSync(path.join(WORKSPACE_DIR, filename), 'utf-8').trim();
19
- return content || '(empty)';
20
- } catch {
21
- return '(empty)';
22
- }
23
- }
24
-
25
- /** Extract a section from the base prompt between two H1 headers */
26
- function extractSection(prompt: string, startHeader: string, endHeader: string): string {
27
- const startIdx = prompt.indexOf(startHeader);
28
- if (startIdx === -1) return '';
29
-
30
- const endIdx = endHeader ? prompt.indexOf(endHeader, startIdx + startHeader.length) : -1;
31
- if (endIdx === -1) return prompt.slice(startIdx);
32
-
33
- return prompt.slice(startIdx, endIdx).trim();
34
- }
35
-
36
- /** Build the system prompt for a sub-agent based on type */
37
- export function assembleSubAgentPrompt(
38
- type: 'coder' | 'researcher',
39
- names: { botName: string; humanName: string },
40
- ): string {
41
- log.info(`[sub-agent-prompts] Building ${type} prompt for ${names.botName}`);
42
- if (type === 'coder') {
43
- const prompt = buildCoderPrompt(names);
44
- log.info(`[sub-agent-prompts] Coder prompt assembled (${prompt.length} chars)`);
45
- return prompt;
46
- }
47
- // Future: researcher, marketplace
48
- const prompt = buildCoderPrompt(names);
49
- log.info(`[sub-agent-prompts] Fallback prompt assembled (${prompt.length} chars)`);
50
- return prompt;
51
- }
52
-
53
- function buildCoderPrompt(names: { botName: string; humanName: string }): string {
54
- const memory = readMemoryFile('MEMORY.md');
55
-
56
- // Read base prompt and extract coding-relevant sections
57
- let codingSection = '';
58
- let workspaceSection = '';
59
- try {
60
- const raw = fs.readFileSync(PROMPT_FILE, 'utf-8')
61
- .replace(/\$BOT/g, names.botName)
62
- .replace(/\$HUMAN/g, names.humanName);
63
-
64
- codingSection = extractSection(raw, '# Coding Excellence', '# Personality and Conduct');
65
- workspaceSection = extractSection(raw, '# Workspace Architecture', '# Personality and Conduct');
66
-
67
- log.info(`[sub-agent-prompts] Extracted coding section: ${codingSection.length} chars`);
68
- log.info(`[sub-agent-prompts] Extracted workspace section: ${workspaceSection.length} chars`);
69
-
70
- // If workspace was already included in coding section, avoid duplication
71
- if (codingSection.includes('# Workspace Architecture')) {
72
- log.info(`[sub-agent-prompts] Workspace already in coding section — skipping duplicate`);
73
- workspaceSection = '';
74
- }
75
- } catch (err) {
76
- log.warn(`[sub-agent-prompts] Could not read base prompt: ${err}`);
77
- }
78
-
79
- return `# Sub-Agent: Coder
80
-
81
- You are a background coding agent working for ${names.botName}. Your human is ${names.humanName}.
82
-
83
- You have been delegated a specific coding task by the orchestrator agent. Your job is to complete it fully and report what you did.
84
-
85
- ## How to Work
86
- - Focus only on the task described below
87
- - Complete the full task before responding
88
- - Do not chat, greet, or explain what you will do — just do it
89
- - If something is ambiguous, make a reasonable choice and note it in your summary
90
- - When finished, write a concise summary of what you changed and why
91
-
92
- ## Constraints
93
- - Do NOT edit: PULSE.json, CRONS.json, MYSELF.md, MYHUMAN.md — those are orchestrator-only
94
- - You MAY write to memory/daily notes if you learn something worth remembering
95
- - You MAY read any file in the workspace
96
- - You have full tool access: Read, Write, Edit, Bash, Glob, Grep
97
-
98
- ---
99
-
100
- ${codingSection}
101
-
102
- ${workspaceSection}
103
-
104
- ---
105
-
106
- # Project Context
107
-
108
- ## MEMORY.md
109
- ${memory}
110
- `;
111
- }