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 +2 -2
- package/supervisor/agents/index.ts +127 -0
- package/supervisor/agents/prompts/coder.txt +120 -0
- package/supervisor/bloby-agent.ts +73 -10
- package/supervisor/channels/manager.ts +4 -33
- package/supervisor/index.ts +8 -48
- package/worker/prompts/bloby-system-prompt.txt +20 -48
- package/supervisor/delegate-parser.ts +0 -44
- package/supervisor/sub-agent.ts +0 -175
- package/supervisor/task-board.ts +0 -231
- package/worker/prompts/sub-agent-prompts.ts +0 -111
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bloby-bot",
|
|
3
|
-
"version": "0.20.
|
|
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.
|
|
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 {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
426
|
+
// Save response to DB
|
|
452
427
|
workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
|
453
428
|
role: 'assistant',
|
|
454
|
-
content:
|
|
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
|
|
package/supervisor/index.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
-
|
|
1284
|
-
|
|
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
|
|
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
|
-
##
|
|
211
|
+
## How It Works
|
|
212
212
|
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
265
|
-
- **
|
|
266
|
-
- **
|
|
267
|
-
- **
|
|
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
|
-
}
|
package/supervisor/sub-agent.ts
DELETED
|
@@ -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
|
-
}
|
package/supervisor/task-board.ts
DELETED
|
@@ -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
|
-
}
|