tiger-agent 0.2.4 → 0.3.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.
@@ -5,6 +5,26 @@ const { telegramBotToken } = require('../config');
5
5
  const { handleMessage } = require('../agent/mainAgent');
6
6
  const tokenManager = require('../tokenManager');
7
7
  const { getProvider } = require('../apiProviders');
8
+ const {
9
+ ensureSwarmLayout,
10
+ runTigerFlow,
11
+ continueTask,
12
+ cancelTask,
13
+ deleteTask,
14
+ askAgent,
15
+ getAgentsStatus,
16
+ getStatusSummary
17
+ } = require('../swarm');
18
+ const {
19
+ ensureSwarmConfigLayout,
20
+ listArchitectureFiles,
21
+ listTaskStyleFiles,
22
+ readArchitectureText,
23
+ writeArchitectureText,
24
+ readTaskStyleText,
25
+ writeTaskStyleText,
26
+ updateDefaultStyleArchitecture
27
+ } = require('../swarm/configStore');
8
28
 
9
29
  // ─── Helpers ─────────────────────────────────────────────────────────────────
10
30
 
@@ -73,6 +93,16 @@ function handleApiCommand(arg) {
73
93
  if (!result.ok) return `❌ Switch failed: ${result.error}`;
74
94
 
75
95
  const p = getProvider(target);
96
+ const overLimit = tokenManager.isOverLimit(target);
97
+ if (overLimit) {
98
+ return [
99
+ `✅ Active provider set to *${p.name}* (\`${target}\`)`,
100
+ `Model: \`${p.chatModel}\``,
101
+ '',
102
+ `⚠️ *${target}* is currently over its daily token limit, so requests will fall back to another provider.`,
103
+ `Use \`/limit ${target} 0\` (unlimited) or raise its limit, then try again.`
104
+ ].join('\n');
105
+ }
76
106
  return `✅ Switched to *${p.name}* (\`${target}\`)\nModel: \`${p.chatModel}\``;
77
107
  }
78
108
 
@@ -137,13 +167,22 @@ function startTelegramBot() {
137
167
  throw new Error('TELEGRAM_BOT_TOKEN is empty.');
138
168
  }
139
169
 
170
+ ensureSwarmLayout();
171
+ ensureSwarmConfigLayout();
140
172
  const bot = new TelegramBot(telegramBotToken, { polling: true });
173
+ let swarmEnabled = true;
141
174
 
142
175
  // Register commands so Telegram shows the list when user types /
143
176
  bot.setMyCommands([
144
177
  { command: 'api', description: 'Show or switch active API provider' },
145
178
  { command: 'tokens', description: 'Show token usage for today' },
146
179
  { command: 'limit', description: 'Show or set daily token limit per provider' },
180
+ { command: 'swarm', description: 'Enable or disable agent swarm' },
181
+ { command: 'status', description: 'Show swarm task status' },
182
+ { command: 'task', description: 'Continue a failed swarm task' },
183
+ { command: 'architecture', description: 'View/update swarm architecture YAML' },
184
+ { command: 'taskstyle', description: 'View/update task style YAML' },
185
+ { command: 'agents', description: 'Show swarm agents' },
147
186
  { command: 'help', description: 'Show all available commands' }
148
187
  ]).catch((err) => {
149
188
  process.stderr.write(`[telegram] setMyCommands failed: ${err.message}\n`);
@@ -186,6 +225,245 @@ function startTelegramBot() {
186
225
  return;
187
226
  }
188
227
 
228
+ if (text.startsWith('/swarm')) {
229
+ const arg = text.slice(6).trim().toLowerCase();
230
+ if (!arg) {
231
+ await safeSend(bot, chatId, `🐯 Swarm is currently *${swarmEnabled ? 'ON' : 'OFF'}*.\nUse \`/swarm on\` or \`/swarm off\`.`, MD);
232
+ return;
233
+ }
234
+ if (arg === 'on') {
235
+ swarmEnabled = true;
236
+ await safeSend(bot, chatId, '✅ Swarm routing is now *ON*', MD);
237
+ return;
238
+ }
239
+ if (arg === 'off') {
240
+ swarmEnabled = false;
241
+ await safeSend(bot, chatId, '✅ Swarm routing is now *OFF*\\.\nNew messages will go to the regular Tiger agent\\.', { parse_mode: 'MarkdownV2' });
242
+ return;
243
+ }
244
+ await safeSend(bot, chatId, 'Usage: `/swarm on` or `/swarm off`', MD);
245
+ return;
246
+ }
247
+
248
+ if (text.startsWith('/architecture')) {
249
+ const arg = text.slice('/architecture'.length).trim();
250
+ try {
251
+ if (!arg || /^list$/i.test(arg)) {
252
+ const files = listArchitectureFiles();
253
+ const lines = ['🧩 *Architecture Files*', ''];
254
+ for (const f of files) lines.push(`- \`${f}\``);
255
+ lines.push('');
256
+ lines.push('Use `/architecture show <file>`');
257
+ lines.push('Use `/architecture write <file>` then newline + full YAML');
258
+ lines.push('Use `/architecture use <file>` to set default task style architecture');
259
+ await safeSend(bot, chatId, lines.join('\n'), MD);
260
+ return;
261
+ }
262
+
263
+ const showMatch = arg.match(/^show\s+(\S+)$/i);
264
+ if (showMatch) {
265
+ const file = showMatch[1];
266
+ const yaml = readArchitectureText(file);
267
+ await safeSend(bot, chatId, `Architecture: ${file}\n\n${yaml}`);
268
+ return;
269
+ }
270
+
271
+ const useMatch = arg.match(/^use\s+(\S+)$/i);
272
+ if (useMatch) {
273
+ const file = useMatch[1];
274
+ updateDefaultStyleArchitecture(file);
275
+ await safeSend(bot, chatId, `✅ default task style now uses architecture \`${file}\``, MD);
276
+ return;
277
+ }
278
+
279
+ const writeMatch = text.match(/^\/architecture\s+write\s+(\S+)\s*\n([\s\S]+)$/i);
280
+ if (writeMatch) {
281
+ const file = writeMatch[1];
282
+ const yaml = writeMatch[2];
283
+ writeArchitectureText(file, yaml);
284
+ await safeSend(bot, chatId, `✅ wrote architecture \`${file}\``, MD);
285
+ return;
286
+ }
287
+
288
+ await safeSend(
289
+ bot,
290
+ chatId,
291
+ 'Usage:\n/architecture\n/architecture list\n/architecture show <file>\n/architecture use <file>\n/architecture write <file> + newline + yaml'
292
+ );
293
+ } catch (err) {
294
+ await safeSend(bot, chatId, `❌ /architecture failed: ${err.message}`);
295
+ }
296
+ return;
297
+ }
298
+
299
+ if (text.startsWith('/taskstyle')) {
300
+ const arg = text.slice('/taskstyle'.length).trim();
301
+ try {
302
+ if (!arg || /^list$/i.test(arg)) {
303
+ const files = listTaskStyleFiles();
304
+ const lines = ['📝 *Task Style Files*', ''];
305
+ for (const f of files) lines.push(`- \`${f}\``);
306
+ lines.push('');
307
+ lines.push('Use `/taskstyle show <file>`');
308
+ lines.push('Use `/taskstyle write <file>` then newline + full YAML');
309
+ await safeSend(bot, chatId, lines.join('\n'), MD);
310
+ return;
311
+ }
312
+
313
+ const showMatch = arg.match(/^show\s+(\S+)$/i);
314
+ if (showMatch) {
315
+ const file = showMatch[1];
316
+ const yaml = readTaskStyleText(file);
317
+ await safeSend(bot, chatId, `Task style: ${file}\n\n${yaml}`);
318
+ return;
319
+ }
320
+
321
+ const writeMatch = text.match(/^\/taskstyle\s+write\s+(\S+)\s*\n([\s\S]+)$/i);
322
+ if (writeMatch) {
323
+ const file = writeMatch[1];
324
+ const yaml = writeMatch[2];
325
+ writeTaskStyleText(file, yaml);
326
+ await safeSend(bot, chatId, `✅ wrote task style \`${file}\``, MD);
327
+ return;
328
+ }
329
+
330
+ await safeSend(
331
+ bot,
332
+ chatId,
333
+ 'Usage:\n/taskstyle\n/taskstyle list\n/taskstyle show <file>\n/taskstyle write <file> + newline + yaml'
334
+ );
335
+ } catch (err) {
336
+ await safeSend(bot, chatId, `❌ /taskstyle failed: ${err.message}`);
337
+ }
338
+ return;
339
+ }
340
+
341
+ if (text === '/agents') {
342
+ const agents = getAgentsStatus();
343
+ const lines = ['🤖 *Agents*', ''];
344
+ for (const a of agents) {
345
+ lines.push(`${a.alive ? '✅' : '❌'} \`${a.name}\` - ${a.label}`);
346
+ }
347
+ await safeSend(bot, chatId, lines.join('\n'), MD);
348
+ return;
349
+ }
350
+
351
+ if (text === '/status') {
352
+ const status = getStatusSummary();
353
+ const lines = ['📋 *Swarm Status*', ''];
354
+ lines.push(`in_progress: *${status.in_progress.length}*`);
355
+ for (const t of status.in_progress.slice(0, 10)) {
356
+ lines.push(`- \`${t.task_id}\` → \`${t.next_agent}\` | ${t.goal}`);
357
+ }
358
+ lines.push(`pending: *${status.pending.length}*`);
359
+ for (const t of status.pending.slice(0, 10)) {
360
+ lines.push(`- \`${t.task_id}\` → \`${t.next_agent}\` | ${t.goal}`);
361
+ }
362
+ lines.push(`done: *${status.done.length}* | failed: *${status.failed.length}*`);
363
+ await safeSend(bot, chatId, lines.join('\n'), MD);
364
+ return;
365
+ }
366
+
367
+ if (text.startsWith('/cancel ')) {
368
+ const taskId = text.slice(8).trim();
369
+ if (!taskId) {
370
+ await safeSend(bot, chatId, 'Usage: `/cancel task_xxx`', MD);
371
+ return;
372
+ }
373
+ const out = cancelTask(taskId, 'tiger');
374
+ if (!out.ok) {
375
+ await safeSend(bot, chatId, `❌ ${out.error}`);
376
+ return;
377
+ }
378
+ await safeSend(bot, chatId, `✅ Cancelled \`${taskId}\``, MD);
379
+ return;
380
+ }
381
+
382
+ if (text.startsWith('/task')) {
383
+ const arg = text.slice(5).trim();
384
+ if (!arg || /^list$/i.test(arg)) {
385
+ const status = getStatusSummary();
386
+ const lines = ['🗂️ *Swarm Tasks*', ''];
387
+ for (const bucketName of ['in_progress', 'pending', 'failed', 'done']) {
388
+ const tasks = Array.isArray(status[bucketName]) ? status[bucketName] : [];
389
+ lines.push(`${bucketName}: *${tasks.length}*`);
390
+ for (const t of tasks.slice(0, 10)) {
391
+ lines.push(`- \`${t.task_id}\` → \`${t.next_agent}\` | ${t.goal}`);
392
+ }
393
+ if (tasks.length > 10) lines.push(`- ... and ${tasks.length - 10} more`);
394
+ }
395
+ lines.push('');
396
+ lines.push('Use `/task continue <id>`, `/task retry <id>`, `/task delete <id>`');
397
+ await safeSend(bot, chatId, lines.join('\n'), MD);
398
+ return;
399
+ }
400
+
401
+ const actionMatch = arg.match(/^(continue|retry|delete)\s+(\S+)$/i);
402
+ if (!actionMatch) {
403
+ await safeSend(bot, chatId, 'Usage: `/task` or `/task <continue|retry|delete> task_xxx`', MD);
404
+ return;
405
+ }
406
+ const action = actionMatch[1].toLowerCase();
407
+ const taskId = actionMatch[2];
408
+
409
+ if (action === 'delete') {
410
+ const out = deleteTask(taskId);
411
+ if (!out.ok) {
412
+ await safeSend(bot, chatId, `❌ ${out.error}`);
413
+ return;
414
+ }
415
+ await safeSend(bot, chatId, `🗑️ Deleted \`${taskId}\` from *${out.bucket}*`, MD);
416
+ return;
417
+ }
418
+
419
+ const progressMarks = new Set();
420
+ try {
421
+ await safeSendTyping(bot, chatId);
422
+ const out = await continueTask(taskId, {
423
+ onProgress: ({ phase, agent, task }) => {
424
+ if (phase === 'worker_done' && task) {
425
+ const key = `${agent}:${task.next_agent}:${task.thread ? task.thread.length : 0}`;
426
+ if (progressMarks.has(key)) return;
427
+ progressMarks.add(key);
428
+ void safeSend(bot, chatId, `Tiger: resumed task ${task.task_id} - ${agent} finished. Next: ${task.next_agent}.`);
429
+ }
430
+ }
431
+ });
432
+ if (out.ok) {
433
+ await safeSend(bot, chatId, out.result || '(empty result)');
434
+ return;
435
+ }
436
+ await safeSend(
437
+ bot,
438
+ chatId,
439
+ `⚠️ ${action} failed: ${out.error || 'unknown error'}${out.task ? `\nTask: \`${out.task.task_id}\` next=\`${out.task.next_agent}\` status=\`${out.task.status}\`` : ''}`,
440
+ MD
441
+ );
442
+ } catch (err) {
443
+ await safeSend(bot, chatId, `⚠️ /task ${action} failed: ${err.message}`);
444
+ }
445
+ return;
446
+ }
447
+
448
+ if (text.startsWith('/ask ')) {
449
+ const parts = text.slice(5).trim();
450
+ const [agentNameRaw, ...rest] = parts.split(/\s+/);
451
+ const agentName = String(agentNameRaw || '').toLowerCase();
452
+ const prompt = rest.join(' ').trim();
453
+ if (!agentName || !prompt) {
454
+ await safeSend(bot, chatId, 'Usage: `/ask <designer|senior_eng|spec_writer|scout|coder|critic> <question>`', MD);
455
+ return;
456
+ }
457
+ try {
458
+ await safeSendTyping(bot, chatId);
459
+ const answer = await askAgent(agentName, prompt);
460
+ await safeSend(bot, chatId, answer || '(empty reply)');
461
+ } catch (err) {
462
+ await safeSend(bot, chatId, `⚠️ /ask failed: ${err.message}`);
463
+ }
464
+ return;
465
+ }
466
+
189
467
  if (text === '/help' || text === '/start') {
190
468
  const helpText = [
191
469
  '🤖 *Tiger Bot Commands*',
@@ -195,6 +473,23 @@ function startTelegramBot() {
195
473
  '/tokens \\- Show token usage for today',
196
474
  '/limit \\- Show daily token limits per provider',
197
475
  '/limit `<name> <n>` \\- Set limit \\(0 = unlimited\\)',
476
+ '/swarm \\- Show swarm on/off status',
477
+ '/swarm `<on|off>` \\- Enable or disable swarm routing',
478
+ '/status \\- Show swarm task status',
479
+ '/task \\- List swarm tasks',
480
+ '/task `continue <task_id>` \\- Resume a failed swarm task',
481
+ '/task `retry <task_id>` \\- Alias of continue',
482
+ '/task `delete <task_id>` \\- Delete a swarm task file',
483
+ '/architecture \\- List architecture YAML files',
484
+ '/architecture `show <file>` \\- Show architecture YAML',
485
+ '/architecture `use <file>` \\- Set default architecture',
486
+ '/architecture `write <file>` + newline + yaml \\- Save architecture YAML',
487
+ '/taskstyle \\- List task style YAML files',
488
+ '/taskstyle `show <file>` \\- Show task style YAML',
489
+ '/taskstyle `write <file>` + newline + yaml \\- Save task style YAML',
490
+ '/agents \\- Show internal swarm agents',
491
+ '/cancel `<task_id>` \\- Cancel a swarm task',
492
+ '/ask `<agent> <question>` \\- Ask a specific internal agent',
198
493
  '/help \\- Show this message',
199
494
  '',
200
495
  '*Available providers:* ' + KNOWN_PROVIDERS.join(', ')
@@ -214,9 +509,42 @@ function startTelegramBot() {
214
509
  try {
215
510
  await safeSendTyping(bot, chatId);
216
511
  typingTimer = setInterval(() => safeSendTyping(bot, chatId), 4500);
512
+ if (!swarmEnabled) {
513
+ const reply = await handleMessage({ platform: 'telegram', userId, text });
514
+ clearInterval(typingTimer);
515
+ await safeSend(bot, chatId, reply);
516
+ return;
517
+ }
518
+ const progressMarks = new Set();
519
+ const flowResult = await runTigerFlow(text, {
520
+ metadata: { platform: 'telegram', userId, chatId },
521
+ onProgress: ({ phase, agent, task }) => {
522
+ if (phase === 'task_created' && task && !progressMarks.has('created')) {
523
+ progressMarks.add('created');
524
+ void safeSend(bot, chatId, `Tiger: task created \\(${task.task_id}\\)\\. Starting Designer\\.`, { parse_mode: 'MarkdownV2' });
525
+ return;
526
+ }
527
+ if (phase !== 'worker_done' || !task) return;
528
+ const key = `${agent}:${task.next_agent}:${task.thread ? task.thread.length : 0}`;
529
+ if (progressMarks.has(key)) return;
530
+ progressMarks.add(key);
531
+ let msg = `Tiger: ${agent} finished. Next: ${task.next_agent}.`;
532
+ if (agent === 'senior_eng' && task.next_agent === 'designer') {
533
+ msg = 'Tiger: Senior Eng requested changes. Designer is revising.';
534
+ } else if (agent === 'senior_eng' && task.next_agent === 'spec_writer') {
535
+ msg = 'Tiger: Senior Eng approved. Spec Writer is drafting the final spec.';
536
+ }
537
+ void safeSend(bot, chatId, msg);
538
+ }
539
+ });
217
540
 
218
- const reply = await handleMessage({ platform: 'telegram', userId, text });
219
541
  clearInterval(typingTimer);
542
+ if (flowResult.ok) {
543
+ await safeSend(bot, chatId, flowResult.result || '(empty result)');
544
+ return;
545
+ }
546
+
547
+ const reply = await handleMessage({ platform: 'telegram', userId, text });
220
548
  await safeSend(bot, chatId, reply);
221
549
  } catch (err) {
222
550
  if (typingTimer) clearInterval(typingTimer);