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.
@@ -0,0 +1,699 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { chatCompletion } = require('../llmClient');
6
+ const {
7
+ swarmAgentTimeoutMs,
8
+ swarmRouteOnProviderError,
9
+ swarmDefaultFlow,
10
+ swarmFirstAgentPolicy,
11
+ swarmFirstAgent
12
+ } = require('../config');
13
+ const {
14
+ AGENTS_DIR,
15
+ ensureSwarmLayout,
16
+ createTask,
17
+ listInProgressTasks,
18
+ listTasks,
19
+ findTask,
20
+ saveTaskInPlace,
21
+ appendThread,
22
+ claimPendingTask,
23
+ releaseTask,
24
+ cancelTask,
25
+ deleteTask
26
+ } = require('./taskBus');
27
+ const {
28
+ ensureSwarmConfigLayout,
29
+ loadTaskStyle,
30
+ loadArchitecture
31
+ } = require('./configStore');
32
+
33
+ const AGENT_DEFS = {
34
+ tiger: { label: 'Tiger', kind: 'orchestrator' },
35
+ designer: { label: 'Designer', kind: 'worker' },
36
+ senior_eng: { label: 'Senior Eng', kind: 'worker' },
37
+ spec_writer: { label: 'Spec Writer', kind: 'worker' },
38
+ scout: { label: 'Scout', kind: 'worker' },
39
+ coder: { label: 'Coder', kind: 'worker' },
40
+ critic: { label: 'Critic', kind: 'worker' }
41
+ };
42
+ const WORKER_AGENT_NAMES = Object.keys(AGENT_DEFS).filter((n) => n !== 'tiger');
43
+
44
+ function safeJsonParse(text, fallback) {
45
+ try {
46
+ return JSON.parse(text);
47
+ } catch (err) {
48
+ return fallback;
49
+ }
50
+ }
51
+
52
+ function getAgentSoul(agentName) {
53
+ const full = path.join(AGENTS_DIR, agentName, 'soul.md');
54
+ if (!fs.existsSync(full)) return '';
55
+ try {
56
+ return fs.readFileSync(full, 'utf8');
57
+ } catch (err) {
58
+ return '';
59
+ }
60
+ }
61
+
62
+ function renderThread(task) {
63
+ return (Array.isArray(task.thread) ? task.thread : [])
64
+ .map((m) => `- [${m.at || ''}] ${m.by || 'unknown'}: ${m.msg || ''}`)
65
+ .join('\n');
66
+ }
67
+
68
+ function stageRef(id) {
69
+ return `stage:${id}`;
70
+ }
71
+
72
+ function isStageRef(value) {
73
+ return String(value || '').startsWith('stage:');
74
+ }
75
+
76
+ function stageIdFromRef(value) {
77
+ return String(value || '').replace(/^stage:/, '').trim();
78
+ }
79
+
80
+ function getTaskContext(task) {
81
+ if (!task.metadata || typeof task.metadata !== 'object') task.metadata = {};
82
+ if (!task.metadata.swarm_ctx || typeof task.metadata.swarm_ctx !== 'object') {
83
+ task.metadata.swarm_ctx = {};
84
+ }
85
+ return task.metadata.swarm_ctx;
86
+ }
87
+
88
+ function resolveRoleMap(architecture) {
89
+ const out = {};
90
+ const agents = Array.isArray(architecture && architecture.agents) ? architecture.agents : [];
91
+ for (const row of agents) {
92
+ const id = String(row && row.id ? row.id : '').trim();
93
+ if (!id) continue;
94
+ out[id] = {
95
+ id,
96
+ runtimeAgent: String(row.runtime_agent || id).trim(),
97
+ role: String(row.role || '').trim()
98
+ };
99
+ }
100
+ return out;
101
+ }
102
+
103
+ function getStageById(architecture, id) {
104
+ const stageId = String(id || '').trim();
105
+ const stages = Array.isArray(architecture && architecture.stages) ? architecture.stages : [];
106
+ return stages.find((s) => String(s && s.id ? s.id : '').trim() === stageId) || null;
107
+ }
108
+
109
+ function parseReviewerDecision(raw, fallbackRoleId) {
110
+ const parsed = safeJsonParse(raw, null);
111
+ if (parsed && typeof parsed === 'object') {
112
+ return {
113
+ approved: Boolean(parsed.approved),
114
+ selectedRole: String(parsed.selected_role || fallbackRoleId || '').trim(),
115
+ feedback: String(parsed.feedback || '').trim(),
116
+ reasoning: String(parsed.reasoning || '').trim(),
117
+ calculationReport: String(parsed.calculation_report || '').trim()
118
+ };
119
+ }
120
+ return {
121
+ approved: /approved\s*✅?|approve\b/i.test(raw) && !/reject/i.test(raw),
122
+ selectedRole: String(fallbackRoleId || '').trim(),
123
+ feedback: String(raw || '').trim(),
124
+ reasoning: '',
125
+ calculationReport: ''
126
+ };
127
+ }
128
+
129
+ async function llmText(system, user, stepLabel) {
130
+ // Step-level timeout: each LLM call gets its own fresh timer (hook reset)
131
+ const stepTimeoutMs = Number(process.env.SWARM_AGENT_TIMEOUT_MS || swarmAgentTimeoutMs || 720000);
132
+ const label = stepLabel || 'llmText';
133
+
134
+ const llmCall = chatCompletion([
135
+ { role: 'system', content: system },
136
+ { role: 'user', content: user }
137
+ ], {
138
+ fallbackOnAnyProviderError: swarmRouteOnProviderError
139
+ });
140
+
141
+ // Fresh timeout per LLM step — resets on every hook call
142
+ const out = await withTimeout(llmCall, stepTimeoutMs, `[step:${label}] LLM call`);
143
+ return String(out && out.content ? out.content : '').trim();
144
+ }
145
+
146
+ async function designerStep(task) {
147
+ const soul = getAgentSoul('designer');
148
+ const system = [
149
+ 'You are Designer in a multi-agent swarm.',
150
+ 'Propose or revise a solution.',
151
+ 'Be concrete. Mention architecture, flow, risks, and tradeoffs.',
152
+ 'Do not approve your own design.',
153
+ soul
154
+ ].join('\n\n');
155
+ const user = [
156
+ `Goal: ${task.goal}`,
157
+ `Flow: ${task.flow || 'design'}`,
158
+ 'Task thread so far:',
159
+ renderThread(task),
160
+ 'Return a revised design proposal for Senior Eng review.'
161
+ ].join('\n\n');
162
+ const text = await llmText(system, user, 'designer:propose');
163
+ appendThread(task, 'designer', text || 'proposed initial design');
164
+ task.next_agent = 'senior_eng';
165
+ task.status = 'pending';
166
+ return task;
167
+ }
168
+
169
+ async function seniorEngStep(task) {
170
+ const soul = getAgentSoul('senior_eng');
171
+ const system = [
172
+ 'You are Senior Engineer reviewer in a swarm.',
173
+ 'Review the latest proposed design critically.',
174
+ 'Return strict JSON only: {"approved":true|false,"feedback":"...","must_fix":["..."]}.',
175
+ 'If approving, feedback should summarize key safeguards.',
176
+ soul
177
+ ].join('\n\n');
178
+ const user = [
179
+ `Goal: ${task.goal}`,
180
+ 'Conversation thread:',
181
+ renderThread(task)
182
+ ].join('\n\n');
183
+ const raw = await llmText(system, user, 'senior_eng:review');
184
+ const parsed = safeJsonParse(raw, null);
185
+
186
+ let approved = false;
187
+ let feedback = raw || 'review completed';
188
+ let mustFix = [];
189
+
190
+ if (parsed && typeof parsed === 'object') {
191
+ approved = Boolean(parsed.approved);
192
+ feedback = String(parsed.feedback || feedback).trim();
193
+ mustFix = Array.isArray(parsed.must_fix) ? parsed.must_fix.map((x) => String(x).trim()).filter(Boolean) : [];
194
+ } else {
195
+ approved = /approved\s*✅?|approve\b/i.test(raw) && !/reject/i.test(raw);
196
+ }
197
+
198
+ const msg = approved
199
+ ? `approved ✅ ${feedback}`.trim()
200
+ : `rejected - ${feedback}${mustFix.length ? ` | must_fix: ${mustFix.join('; ')}` : ''}`.trim();
201
+
202
+ appendThread(task, 'senior_eng', msg);
203
+ task.next_agent = approved ? 'spec_writer' : 'designer';
204
+ task.status = 'pending';
205
+ return task;
206
+ }
207
+
208
+ async function specWriterStep(task) {
209
+ const soul = getAgentSoul('spec_writer');
210
+ const system = [
211
+ 'You are Spec Writer in a swarm.',
212
+ 'Write a clear formal implementation/design spec from the approved discussion.',
213
+ 'Use structured markdown with scope, architecture, flow, edge cases, and next steps.',
214
+ soul
215
+ ].join('\n\n');
216
+ const user = [
217
+ `Goal: ${task.goal}`,
218
+ 'Thread:',
219
+ renderThread(task),
220
+ 'Write the final spec now.'
221
+ ].join('\n\n');
222
+ const spec = await llmText(system, user, 'spec_writer:write');
223
+ appendThread(task, 'spec_writer', 'formal spec written');
224
+ task.result = spec || '(empty spec)';
225
+ task.next_agent = 'tiger';
226
+ task.status = 'pending';
227
+ return task;
228
+ }
229
+
230
+ async function genericWorkerStep(task, agentName, roleHint) {
231
+ const soul = getAgentSoul(agentName);
232
+ const text = await llmText(
233
+ [
234
+ `You are ${agentName} in Tiger swarm.`,
235
+ roleHint,
236
+ 'Respond concisely and practically.',
237
+ soul
238
+ ].join('\n\n'),
239
+ [`Goal: ${task.goal}`, 'Thread:', renderThread(task)].join('\n\n'),
240
+ `${agentName}:step`
241
+ );
242
+ appendThread(task, agentName, text || `${agentName} completed step`);
243
+ task.status = 'pending';
244
+ task.next_agent =
245
+ agentName === 'scout' ? 'coder' :
246
+ agentName === 'coder' ? 'critic' :
247
+ agentName === 'critic' ? 'tiger' : 'tiger';
248
+ if (agentName === 'critic') {
249
+ task.result = text || task.result || '';
250
+ }
251
+ return task;
252
+ }
253
+
254
+ async function runRoleStep(task, roleId, runtimeAgent, roleHint, promptLines, opts = {}) {
255
+ const soul = getAgentSoul(runtimeAgent);
256
+ const text = await llmText(
257
+ [
258
+ `You are ${roleId} in Tiger swarm (runtime agent: ${runtimeAgent}).`,
259
+ roleHint,
260
+ 'Respond concisely and practically.',
261
+ soul
262
+ ].join('\n\n'),
263
+ [
264
+ `Goal: ${task.goal}`,
265
+ 'Thread:',
266
+ renderThread(task),
267
+ ...(Array.isArray(promptLines) ? promptLines : [])
268
+ ].join('\n\n'),
269
+ `${roleId}:step`
270
+ );
271
+ if (opts.appendThread !== false) {
272
+ appendThread(task, roleId, text || `${roleId} completed step`);
273
+ }
274
+ return text || '';
275
+ }
276
+
277
+ async function runArchitectureStage(task, architecture, stage) {
278
+ const roleMap = resolveRoleMap(architecture);
279
+ const ctx = getTaskContext(task);
280
+ const stageType = String(stage.type || '').toLowerCase();
281
+
282
+ if (stageType === 'parallel') {
283
+ const roles = Array.isArray(stage.roles) ? stage.roles.map((x) => String(x || '').trim()).filter(Boolean) : [];
284
+ const outputs = await Promise.all(roles.map(async (roleId) => {
285
+ const role = roleMap[roleId] || { runtimeAgent: roleId };
286
+ const text = await runRoleStep(
287
+ task,
288
+ roleId,
289
+ role.runtimeAgent,
290
+ 'Create a concrete design candidate that fits the objective.',
291
+ ['Return one complete proposal.'],
292
+ { appendThread: false }
293
+ );
294
+ return { role: roleId, runtime_agent: role.runtimeAgent, text };
295
+ }));
296
+ for (const out of outputs) {
297
+ appendThread(task, out.role, out.text || `${out.role} completed step`);
298
+ }
299
+ const key = String(stage.store_as || `${stage.id}_outputs`).trim();
300
+ ctx[key] = outputs;
301
+ appendThread(task, 'tiger', `stage ${stage.id} completed with ${outputs.length} parallel outputs`);
302
+ task.next_agent = stage.next ? stageRef(stage.next) : 'tiger';
303
+ task.status = 'pending';
304
+ return task;
305
+ }
306
+
307
+ if (stageType === 'judge') {
308
+ const roleId = String(stage.role || '').trim();
309
+ const role = roleMap[roleId] || { runtimeAgent: roleId };
310
+ const candidateKey = String(stage.candidates_from || 'design_candidates').trim();
311
+ const candidates = Array.isArray(ctx[candidateKey]) ? ctx[candidateKey] : [];
312
+ const matrix = architecture && architecture.judgment_matrix ? architecture.judgment_matrix : {};
313
+ const criteria = Array.isArray(matrix.criteria) ? matrix.criteria : [];
314
+ const fallbackRole = candidates[0] && candidates[0].role ? candidates[0].role : '';
315
+
316
+ const decisionRaw = await runRoleStep(
317
+ task,
318
+ roleId,
319
+ role.runtimeAgent,
320
+ 'Evaluate candidates and select the best one using the judgment matrix. Build an explicit calculation report for the selected candidate.',
321
+ [
322
+ `Candidates JSON: ${JSON.stringify(candidates)}`,
323
+ `Judgment matrix: ${JSON.stringify(criteria)}`,
324
+ 'Return strict JSON only: {"approved":true|false,"selected_role":"...","feedback":"...","reasoning":"...","calculation_report":"..."}'
325
+ ]
326
+ );
327
+ const decision = parseReviewerDecision(decisionRaw, fallbackRole);
328
+ const selectedKey = String(stage.selected_role_key || 'selected_role').trim();
329
+ const feedbackKey = String(stage.feedback_key || 'reviewer_feedback').trim();
330
+ const calculationReportKey = String(stage.calculation_report_key || '').trim();
331
+ ctx[selectedKey] = decision.selectedRole || fallbackRole;
332
+ ctx[feedbackKey] = decision.feedback || '';
333
+ if (calculationReportKey) {
334
+ ctx[calculationReportKey] = decision.calculationReport || '';
335
+ }
336
+ appendThread(
337
+ task,
338
+ roleId,
339
+ `decision approved=${decision.approved} selected=${ctx[selectedKey]} feedback=${ctx[feedbackKey]}`
340
+ );
341
+ task.next_agent = stageRef(decision.approved ? stage.pass_next : stage.fail_next);
342
+ task.status = 'pending';
343
+ return task;
344
+ }
345
+
346
+ if (stageType === 'revise') {
347
+ const roleKey = String(stage.role_from_context || 'selected_role').trim();
348
+ const feedbackKey = String(stage.feedback_from_context || 'reviewer_feedback').trim();
349
+ const candidateKey = String(stage.candidates_from || 'design_candidates').trim();
350
+ const roleId = String(ctx[roleKey] || '').trim();
351
+ const role = roleMap[roleId] || { runtimeAgent: roleId || 'designer' };
352
+ const feedback = String(ctx[feedbackKey] || '').trim();
353
+ const revised = await runRoleStep(
354
+ task,
355
+ roleId || 'designer',
356
+ role.runtimeAgent,
357
+ 'Revise your selected proposal based on reviewer feedback.',
358
+ [`Reviewer feedback: ${feedback}`]
359
+ );
360
+
361
+ if (Array.isArray(ctx[candidateKey])) {
362
+ ctx[candidateKey] = ctx[candidateKey].map((c) => (
363
+ c && c.role === roleId ? { ...c, text: revised } : c
364
+ ));
365
+ }
366
+ const selectedRoleValue = String(ctx[roleKey] || '').trim();
367
+ const keysToUpdate = Array.isArray(stage.update_context_keys_from_revised)
368
+ ? stage.update_context_keys_from_revised.map((x) => String(x || '').trim()).filter(Boolean)
369
+ : [];
370
+ if (roleId && selectedRoleValue === roleId) {
371
+ for (const key of keysToUpdate) {
372
+ ctx[key] = revised;
373
+ }
374
+ }
375
+ appendThread(task, 'tiger', `revision completed by ${roleId || 'designer'}`);
376
+ task.next_agent = stage.next ? stageRef(stage.next) : 'tiger';
377
+ task.status = 'pending';
378
+ return task;
379
+ }
380
+
381
+ if (stageType === 'final') {
382
+ const roleId = String(stage.role || '').trim();
383
+ const role = roleMap[roleId] || { runtimeAgent: roleId };
384
+ const sourceKey = String(stage.source_from_context || 'design_candidates').trim();
385
+ const source = ctx[sourceKey];
386
+ const outputSections = Array.isArray(stage.output_sections)
387
+ ? stage.output_sections.map((x) => String(x || '').trim()).filter(Boolean)
388
+ : [];
389
+ const outputNotes = String(stage.output_notes || '').trim();
390
+ const finalPromptLines = [`Source context JSON: ${JSON.stringify(source || null)}`];
391
+ if (outputSections.length) {
392
+ finalPromptLines.push(`Required output sections: ${outputSections.join(', ')}`);
393
+ }
394
+ if (outputNotes) {
395
+ finalPromptLines.push(`Output requirements: ${outputNotes}`);
396
+ }
397
+ const finalText = await runRoleStep(
398
+ task,
399
+ roleId,
400
+ role.runtimeAgent,
401
+ 'Write the final polished specification from the selected and revised design.',
402
+ finalPromptLines
403
+ );
404
+ task.result = finalText;
405
+ task.next_agent = 'tiger';
406
+ task.status = 'pending';
407
+ return task;
408
+ }
409
+
410
+ throw new Error(`Unsupported stage type: ${stage.type}`);
411
+ }
412
+
413
+ async function processWorkerTask(agentName, task) {
414
+ if (agentName === 'designer') return designerStep(task);
415
+ if (agentName === 'senior_eng') return seniorEngStep(task);
416
+ if (agentName === 'spec_writer') return specWriterStep(task);
417
+ if (agentName === 'scout') return genericWorkerStep(task, 'scout', 'Research and verify from multiple angles.');
418
+ if (agentName === 'coder') return genericWorkerStep(task, 'coder', 'Propose implementation plan and code-level steps.');
419
+ if (agentName === 'critic') return genericWorkerStep(task, 'critic', 'Review for defects, risks, and regressions.');
420
+ throw new Error(`Unsupported worker: ${agentName}`);
421
+ }
422
+
423
+ function withTimeout(promise, timeoutMs, label) {
424
+ const ms = Number(timeoutMs || 0);
425
+ if (!Number.isFinite(ms) || ms <= 0) return promise;
426
+ return new Promise((resolve, reject) => {
427
+ const timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms);
428
+ Promise.resolve(promise).then(
429
+ (value) => {
430
+ clearTimeout(timer);
431
+ resolve(value);
432
+ },
433
+ (err) => {
434
+ clearTimeout(timer);
435
+ reject(err);
436
+ }
437
+ );
438
+ });
439
+ }
440
+
441
+ async function runWorkerTurn(agentName) {
442
+ ensureSwarmLayout();
443
+ const claim = claimPendingTask(agentName);
444
+ if (!claim) return { ok: true, idle: true, agent: agentName };
445
+
446
+ // ✅ Hook-based timeout: each LLM step inside processWorkerTask resets its own timer
447
+ // withTimeout is applied per-step inside llmText() — no outer wrap needed here
448
+ let { task, filePath } = claim;
449
+ try {
450
+ task = await processWorkerTask(agentName, task);
451
+ const out = releaseTask(task, filePath, task.status === 'failed' ? 'failed' : 'pending');
452
+ return { ok: true, idle: false, agent: agentName, task: out.task };
453
+ } catch (err) {
454
+ appendThread(task, agentName, `error: ${err.message}`);
455
+ if (!task.metadata || typeof task.metadata !== 'object') task.metadata = {};
456
+ task.metadata.last_failed_agent = agentName;
457
+ task.metadata.last_error = String(err && err.message ? err.message : 'unknown error');
458
+ task.status = 'failed';
459
+ task.next_agent = 'tiger';
460
+ const out = releaseTask(task, filePath, 'failed');
461
+ return { ok: false, idle: false, agent: agentName, error: err.message, task: out.task };
462
+ }
463
+ }
464
+
465
+ function pickFlowFirstAgent(flow) {
466
+ return flow === 'research_build' ? 'scout' : 'designer';
467
+ }
468
+
469
+ function pickAutoFirstAgent(goal, flow) {
470
+ const text = String(goal || '').toLowerCase();
471
+ if (flow === 'research_build') return 'scout';
472
+ if (/(research|investigate|compare|look up|search|verify|news|find out)/i.test(text)) return 'scout';
473
+ if (/(bug|fix|error|exception|stack trace|refactor|implement|write code|code change|patch)/i.test(text)) return 'coder';
474
+ if (/(review|audit|critique|risk check)/i.test(text)) return 'critic';
475
+ if (/(spec|prd|requirements|design doc|document)/i.test(text)) return 'designer';
476
+ return 'designer';
477
+ }
478
+
479
+ function resolveFirstAgent(goal, flow, opts = {}) {
480
+ const policy = String(opts.firstAgentPolicy || swarmFirstAgentPolicy || 'auto').toLowerCase();
481
+ const fixed = String(opts.firstAgent || swarmFirstAgent || '').toLowerCase();
482
+
483
+ if (WORKER_AGENT_NAMES.includes(policy)) return policy;
484
+ if (policy === 'fixed' && WORKER_AGENT_NAMES.includes(fixed)) return fixed;
485
+ if (policy === 'flow') return pickFlowFirstAgent(flow);
486
+ return pickAutoFirstAgent(goal, flow);
487
+ }
488
+
489
+ function extractTigerResult(taskId) {
490
+ const found = findTask(taskId);
491
+ if (!found) return { ok: false, error: 'Task not found' };
492
+ const { filePath, task, bucket } = found;
493
+ if (task.next_agent !== 'tiger') {
494
+ return { ok: false, error: `Task not ready for tiger (next_agent=${task.next_agent})`, task };
495
+ }
496
+ task.status = task.status === 'failed' ? 'failed' : 'done';
497
+ const targetBucket = task.status === 'failed' ? 'failed' : 'done';
498
+ const nextPath = path.join(path.dirname(filePath), '..', targetBucket, `${task.task_id}.json`);
499
+ void nextPath; // path computation not used directly; move handled via releaseTask.
500
+ const released = releaseTask(task, filePath, targetBucket);
501
+ return { ok: true, task: released.task, bucketFrom: bucket, bucketTo: targetBucket };
502
+ }
503
+
504
+ async function runTaskToTiger(taskId, opts = {}) {
505
+ const rawMaxTurns = Number(opts.maxTurns);
506
+ const maxTurns = Number.isFinite(rawMaxTurns) && rawMaxTurns > 0 ? Math.floor(rawMaxTurns) : null;
507
+ const onProgress = typeof opts.onProgress === 'function' ? opts.onProgress : null;
508
+
509
+ for (let i = 0; maxTurns == null || i < maxTurns; i += 1) {
510
+ const found = findTask(taskId);
511
+ if (!found) return { ok: false, error: 'Task disappeared' };
512
+ const { task, filePath } = found;
513
+ if (task.next_agent === 'tiger') {
514
+ return { ok: true, task, readyForTiger: true };
515
+ }
516
+ if (task.status === 'failed') {
517
+ return { ok: false, task, error: 'Task failed' };
518
+ }
519
+
520
+ const agentName = task.next_agent;
521
+ if (isStageRef(agentName)) {
522
+ const architectureFile = String(task.metadata && task.metadata.architecture_file ? task.metadata.architecture_file : '').trim();
523
+ const architecture = loadArchitecture(architectureFile);
524
+ const stageId = stageIdFromRef(agentName);
525
+ const stage = getStageById(architecture, stageId);
526
+ if (!stage) return { ok: false, task, error: `Unknown stage: ${stageId}` };
527
+ if (onProgress) onProgress({ phase: 'worker_start', agent: stage.id, task });
528
+ const updated = await runArchitectureStage(task, architecture, stage);
529
+ saveTaskInPlace(filePath, updated);
530
+ if (onProgress) onProgress({ phase: 'worker_done', agent: stage.id, task: updated, turn: { ok: true } });
531
+ continue;
532
+ }
533
+
534
+ if (!AGENT_DEFS[agentName]) {
535
+ return { ok: false, task, error: `Unknown next_agent: ${agentName}` };
536
+ }
537
+
538
+ if (onProgress) onProgress({ phase: 'worker_start', agent: agentName, task });
539
+ const turn = await runWorkerTurn(agentName);
540
+ const latest = findTask(taskId);
541
+ if (onProgress && latest) {
542
+ onProgress({ phase: 'worker_done', agent: agentName, task: latest.task, turn });
543
+ }
544
+ if (!turn.ok && !turn.idle) return { ok: false, task: turn.task, error: turn.error || 'Worker failed' };
545
+ }
546
+
547
+ const found = findTask(taskId);
548
+ return { ok: false, error: `Exceeded max turns (${maxTurns})`, task: found ? found.task : null };
549
+ }
550
+
551
+ async function runTigerFlow(goal, opts = {}) {
552
+ ensureSwarmLayout();
553
+ ensureSwarmConfigLayout();
554
+
555
+ const requestedStyle = String(opts.taskStyle || process.env.SWARM_TASK_STYLE || 'default.yaml').trim();
556
+ const taskStyle = loadTaskStyle(requestedStyle);
557
+ const architectureFile = String(taskStyle.architecture || '').trim();
558
+ const architecture = loadArchitecture(architectureFile);
559
+ const stages = Array.isArray(architecture.stages) ? architecture.stages : [];
560
+ const startStage = String(architecture.start_stage || (stages[0] && stages[0].id) || '').trim();
561
+ const flow = String(opts.flow || taskStyle.flow || swarmDefaultFlow || 'architecture').toLowerCase();
562
+ const objectivePrefix = String(taskStyle.objective_prefix || '').trim();
563
+ const normalizedGoal = objectivePrefix ? `${objectivePrefix} ${String(goal || '').trim()}` : String(goal || '').trim();
564
+ const firstAgent = startStage ? stageRef(startStage) : resolveFirstAgent(normalizedGoal, flow, opts);
565
+ const task = createTask({
566
+ from: 'tiger',
567
+ goal: normalizedGoal,
568
+ nextAgent: firstAgent,
569
+ flow,
570
+ metadata: {
571
+ ...(opts.metadata || {}),
572
+ first_agent_policy: String(opts.firstAgentPolicy || swarmFirstAgentPolicy || 'auto').toLowerCase(),
573
+ first_agent: firstAgent,
574
+ task_style_file: requestedStyle,
575
+ architecture_file: architectureFile
576
+ }
577
+ });
578
+
579
+ if (typeof opts.onProgress === 'function') {
580
+ opts.onProgress({ phase: 'task_created', task });
581
+ }
582
+
583
+ const progress = await runTaskToTiger(task.task_id, opts);
584
+ if (!progress.ok) return progress;
585
+
586
+ const final = extractTigerResult(task.task_id);
587
+ if (!final.ok) return final;
588
+
589
+ return { ok: true, task: final.task, result: final.task.result || '' };
590
+ }
591
+
592
+ function inferResumeAgent(task) {
593
+ const meta = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
594
+ const fromMeta = String(meta.last_failed_agent || '').trim();
595
+ if (fromMeta && AGENT_DEFS[fromMeta] && fromMeta !== 'tiger') return fromMeta;
596
+
597
+ const thread = Array.isArray(task && task.thread) ? task.thread : [];
598
+ for (let i = thread.length - 1; i >= 0; i -= 1) {
599
+ const m = thread[i] || {};
600
+ const by = String(m.by || '').trim();
601
+ const msg = String(m.msg || '').trim();
602
+ if (by && by !== 'tiger' && AGENT_DEFS[by] && /^error:/i.test(msg)) return by;
603
+ }
604
+
605
+ const next = String(task && task.next_agent || '').trim();
606
+ if (next && next !== 'tiger' && AGENT_DEFS[next]) return next;
607
+ return pickFlowFirstAgent(task && task.flow);
608
+ }
609
+
610
+ async function continueTask(taskId, opts = {}) {
611
+ ensureSwarmLayout();
612
+ const found = findTask(taskId);
613
+ if (!found) return { ok: false, error: 'Task not found' };
614
+
615
+ let { task, filePath, bucket } = found;
616
+ if (bucket === 'done') {
617
+ return { ok: false, error: 'Task is already done', task };
618
+ }
619
+
620
+ if (bucket === 'failed') {
621
+ const resumeAgent = isStageRef(task.next_agent) ? task.next_agent : inferResumeAgent(task);
622
+ task.status = 'pending';
623
+ task.next_agent = resumeAgent;
624
+ appendThread(task, 'tiger', `resume requested: continue from ${resumeAgent}`);
625
+ const released = releaseTask(task, filePath, 'pending');
626
+ task = released.task;
627
+ filePath = released.filePath;
628
+ bucket = released.bucket;
629
+ void filePath;
630
+ void bucket;
631
+ } else if (bucket === 'in_progress') {
632
+ // Recovery path for stale stuck tasks.
633
+ task.status = 'pending';
634
+ appendThread(task, 'tiger', 'resume requested: moved stale in_progress task back to pending');
635
+ const released = releaseTask(task, filePath, 'pending');
636
+ task = released.task;
637
+ }
638
+
639
+ const progress = await runTaskToTiger(taskId, opts);
640
+ if (!progress.ok) return progress;
641
+
642
+ const final = extractTigerResult(taskId);
643
+ if (!final.ok) return final;
644
+ return { ok: true, task: final.task, result: final.task.result || '' };
645
+ }
646
+
647
+ async function askAgent(agentName, prompt) {
648
+ ensureSwarmLayout();
649
+ if (!AGENT_DEFS[agentName] || agentName === 'tiger') {
650
+ throw new Error(`Unknown or unsupported /ask agent: ${agentName}`);
651
+ }
652
+ const soul = getAgentSoul(agentName);
653
+ const system = [
654
+ `You are ${agentName} in Tiger's internal swarm.`,
655
+ 'Answer as that role only.',
656
+ 'Be concise and practical.',
657
+ soul
658
+ ].join('\n\n');
659
+ return llmText(system, String(prompt || '').trim());
660
+ }
661
+
662
+ function getAgentsStatus() {
663
+ ensureSwarmLayout();
664
+ return Object.keys(AGENT_DEFS).map((name) => {
665
+ const dir = path.join(AGENTS_DIR, name);
666
+ return {
667
+ name,
668
+ label: AGENT_DEFS[name].label,
669
+ kind: AGENT_DEFS[name].kind,
670
+ alive: fs.existsSync(dir),
671
+ path: dir
672
+ };
673
+ });
674
+ }
675
+
676
+ function getStatusSummary() {
677
+ ensureSwarmLayout();
678
+ return {
679
+ in_progress: listInProgressTasks(),
680
+ pending: listTasks('pending'),
681
+ done: listTasks('done'),
682
+ failed: listTasks('failed')
683
+ };
684
+ }
685
+
686
+ module.exports = {
687
+ AGENT_DEFS,
688
+ ensureSwarmLayout,
689
+ runWorkerTurn,
690
+ runTaskToTiger,
691
+ runTigerFlow,
692
+ continueTask,
693
+ deleteTask,
694
+ extractTigerResult,
695
+ cancelTask,
696
+ askAgent,
697
+ getAgentsStatus,
698
+ getStatusSummary
699
+ };