bloby-bot 0.19.3 → 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.
@@ -11,6 +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 { buildAgents } from './agents/index.js';
14
15
 
15
16
  export interface RecentMessage {
16
17
  role: 'user' | 'assistant';
@@ -19,6 +20,7 @@ export interface RecentMessage {
19
20
 
20
21
  interface ActiveQuery {
21
22
  abortController: AbortController;
23
+ queryHandle?: any; // SDK query handle for stopTask()
22
24
  }
23
25
 
24
26
  const activeQueries = new Map<string, ActiveQuery>();
@@ -109,6 +111,8 @@ export async function startBlobyAgentQuery(
109
111
  recentMessages?: RecentMessage[],
110
112
  /** Override system prompt (used for customer-facing channel messages via SUPPORT.md) */
111
113
  supportPrompt?: string,
114
+ /** Max agentic turns. Default 50. Orchestrator uses 5. */
115
+ maxTurns?: number,
112
116
  ): Promise<void> {
113
117
  const oauthToken = await getClaudeAccessToken();
114
118
  if (!oauthToken) {
@@ -140,14 +144,14 @@ export async function startBlobyAgentQuery(
140
144
  enrichedPrompt += `\n\n---\n# Channel Config\n\`\`\`json\n${JSON.stringify(channels, null, 2)}\n\`\`\``;
141
145
  }
142
146
  } catch {}
147
+
148
+ // Task board is now managed natively by the SDK via background agents
143
149
  }
144
150
 
145
151
  if (recentMessages?.length) {
146
152
  enrichedPrompt += `\n\n---\n# Recent Conversation\n${formatConversationHistory(recentMessages)}`;
147
153
  }
148
154
 
149
- activeQueries.set(conversationId, { abortController });
150
-
151
155
  let fullText = '';
152
156
  const usedTools = new Set<string>();
153
157
  let stderrBuf = '';
@@ -187,6 +191,17 @@ export async function startBlobyAgentQuery(
187
191
  }
188
192
  } catch {}
189
193
 
194
+ const effectiveMaxTurns = maxTurns ?? 50;
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}`);
204
+
190
205
  const claudeQuery = query({
191
206
  prompt: sdkPrompt,
192
207
  options: {
@@ -194,10 +209,12 @@ export async function startBlobyAgentQuery(
194
209
  cwd: WORKSPACE_DIR,
195
210
  permissionMode: 'bypassPermissions',
196
211
  allowDangerouslySkipPermissions: true,
197
- maxTurns: 50,
212
+ maxTurns: effectiveMaxTurns,
198
213
  abortController,
199
214
  systemPrompt: enrichedPrompt,
200
215
  mcpServers,
216
+ ...(agents && { agents }),
217
+ ...(agents && { agentProgressSummaries: true }),
201
218
  stderr: (chunk: string) => { stderrBuf += chunk; },
202
219
  env: {
203
220
  ...process.env as Record<string, string>,
@@ -207,6 +224,9 @@ export async function startBlobyAgentQuery(
207
224
  },
208
225
  });
209
226
 
227
+ // Store query handle for stopTask() support
228
+ activeQueries.set(conversationId, { abortController, queryHandle: claudeQuery });
229
+
210
230
  onMessage('bot:typing', { conversationId });
211
231
 
212
232
  for await (const msg of claudeQuery) {
@@ -252,6 +272,51 @@ export async function startBlobyAgentQuery(
252
272
  status: 'running',
253
273
  });
254
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
+ }
255
320
  }
256
321
  }
257
322
 
@@ -282,3 +347,14 @@ export function stopBlobyAgentQuery(conversationId: string): void {
282
347
  activeQueries.delete(conversationId);
283
348
  }
284
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
+ }
@@ -423,7 +423,7 @@ export class ChannelManager {
423
423
  waChunkBuf = '';
424
424
  }
425
425
 
426
- // Save full response to DB
426
+ // Save response to DB
427
427
  workerApi(`/api/conversations/${convId}/messages`, 'POST', {
428
428
  role: 'assistant',
429
429
  content: eventData.content,
@@ -431,8 +431,8 @@ export class ChannelManager {
431
431
  }).catch(() => {});
432
432
  }
433
433
 
434
- // Mirror streaming to chat clients
435
- if (type === 'bot:token' || type === 'bot:response' || 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-')) {
436
436
  broadcastBloby(type, eventData);
437
437
  }
438
438
 
@@ -444,6 +444,8 @@ export class ChannelManager {
444
444
  undefined,
445
445
  { botName, humanName },
446
446
  recentMessages,
447
+ undefined, // no supportPrompt
448
+ 5, // maxTurns: orchestrator mode
447
449
  );
448
450
  }
449
451
 
@@ -203,6 +203,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
203
203
  const [portalCopied, setPortalCopied] = useState(false);
204
204
  const [portalExists, setPortalExists] = useState(false);
205
205
  const [showPassAllSet, setShowPassAllSet] = useState(false);
206
+ const [acceptedTerms, setAcceptedTerms] = useState(false);
206
207
 
207
208
  // Portal old password (for changing existing credentials)
208
209
  const [portalOldPass, setPortalOldPass] = useState('');
@@ -2309,8 +2310,25 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2309
2310
  </div>
2310
2311
  </div>
2311
2312
 
2313
+ {/* Terms & Privacy checkbox */}
2314
+ <label className="flex items-start gap-2.5 mt-6 cursor-pointer text-left select-none">
2315
+ <input
2316
+ type="checkbox"
2317
+ checked={acceptedTerms}
2318
+ onChange={(e) => setAcceptedTerms(e.target.checked)}
2319
+ className="mt-0.5 h-4 w-4 rounded border-white/20 bg-white/5 accent-emerald-500 shrink-0"
2320
+ />
2321
+ <span className="text-[12px] text-white/50 leading-relaxed">
2322
+ I accept the{' '}
2323
+ <a href="https://www.bloby.bot/terms" target="_blank" rel="noopener noreferrer" className="text-white/70 underline hover:text-white/90">terms</a>
2324
+ {' '}and{' '}
2325
+ <a href="https://www.bloby.bot/privacy" target="_blank" rel="noopener noreferrer" className="text-white/70 underline hover:text-white/90">privacy policy</a>
2326
+ </span>
2327
+ </label>
2328
+
2312
2329
  {/* Redirect / done button */}
2313
2330
  <button
2331
+ disabled={!acceptedTerms}
2314
2332
  onClick={() => {
2315
2333
  if (isPrivate) {
2316
2334
  (window.top || window).location.href = '/';
@@ -2318,7 +2336,7 @@ export default function OnboardWizard({ onComplete, isInitialSetup = false, onSa
2318
2336
  (window.top || window).location.href = finalUrlFull;
2319
2337
  }
2320
2338
  }}
2321
- className="w-full mt-6 py-3 bg-gradient-brand hover:opacity-90 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2"
2339
+ className={`w-full mt-4 py-3 text-white text-[14px] font-semibold rounded-full transition-colors flex items-center justify-center gap-2 ${acceptedTerms ? 'bg-gradient-brand hover:opacity-90' : 'bg-white/10 cursor-not-allowed opacity-50'}`}
2322
2340
  >
2323
2341
  {isPrivate ? 'Go to dashboard' : 'Go to your agent'}
2324
2342
  <ExternalLink className="h-4 w-4" />
@@ -13,7 +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';
16
+ import { startBlobyAgentQuery, stopBlobyAgentQuery, stopSubAgentTask, type RecentMessage } from './bloby-agent.js';
17
17
  import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
18
18
  import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
19
19
  import { startScheduler, stopScheduler } from './scheduler.js';
@@ -1132,7 +1132,12 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1132
1132
  const waMirrorJid = waStatus?.connected ? waStatus.info?.phoneNumber : null;
1133
1133
  let waChunkBuf = '';
1134
1134
 
1135
- // Start agent query
1135
+ // Start orchestrator query (maxTurns: 5 — fast, delegates heavy work)
1136
+ log.info(`[orchestrator] ──── USER MESSAGE ────`);
1137
+ log.info(`[orchestrator] Content: "${content.slice(0, 100)}..."`);
1138
+ log.info(`[orchestrator] Model: ${freshConfig.ai.model}`);
1139
+ log.info(`[orchestrator] Conv: ${convId}`);
1140
+ log.info(`[orchestrator] MaxTurns: 5 (orchestrator mode)`);
1136
1141
  agentQueryActive = true;
1137
1142
  currentStreamConvId = convId;
1138
1143
  currentStreamBuffer = '';
@@ -1149,8 +1154,10 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1149
1154
  waChunkBuf = '';
1150
1155
  }
1151
1156
 
1152
- // Intercept bot:done — Vite HMR handles file changes automatically
1157
+ // Intercept bot:done — orchestrator turn finished
1153
1158
  if (type === 'bot:done') {
1159
+ log.info(`[orchestrator] ──── TURN COMPLETE ────`);
1160
+ log.info(`[orchestrator] File tools used: ${eventData.usedFileTools}`);
1154
1161
  agentQueryActive = false;
1155
1162
  currentStreamConvId = null;
1156
1163
  currentStreamBuffer = '';
@@ -1192,8 +1199,12 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1192
1199
  }
1193
1200
 
1194
1201
  // Stream all events to every connected client
1202
+ // (includes bot:task-created, bot:task-progress, bot:task-done from SDK)
1195
1203
  broadcastBloby(type, eventData);
1196
- }, data.attachments, savedFiles, { botName, humanName }, recentMessages);
1204
+ }, data.attachments, savedFiles, { botName, humanName }, recentMessages,
1205
+ undefined, // no supportPrompt
1206
+ 5, // maxTurns: orchestrator mode
1207
+ );
1197
1208
  })();
1198
1209
  return;
1199
1210
  }
@@ -1231,6 +1242,17 @@ ${!connected ? '<script>setTimeout(()=>location.reload(),4000)</script>' : ''}
1231
1242
  return;
1232
1243
  }
1233
1244
 
1245
+ if (msg.type === 'user:stop-task') {
1246
+ const taskId = (msg as any).data?.taskId;
1247
+ if (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
+ });
1252
+ }
1253
+ return;
1254
+ }
1255
+
1234
1256
  if (msg.type === 'user:clear-context') {
1235
1257
  (async () => {
1236
1258
  try {
@@ -204,6 +204,42 @@ Complex cron tasks can have detailed instruction files in `tasks/{cron-id}.md`.
204
204
 
205
205
  ---
206
206
 
207
+ # Delegation
208
+
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
+
211
+ ## How It Works
212
+
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.
214
+
215
+ ## CRITICAL: Always Delegate
216
+
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)
222
+
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
227
+
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.
229
+
230
+ ## When a sub-agent completes
231
+
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!"
233
+
234
+ ## Rules
235
+
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.
240
+
241
+ ---
242
+
207
243
  ## Skills
208
244
 
209
245
  Skills live in `skills/` — each skill is a folder with instructions and resources:
@@ -1 +0,0 @@
1
- import{i as e}from"./bloby-d8kRAobK.js";export{e as Mermaid};