@yemi33/minions 0.1.1710 → 0.1.1711
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/CHANGELOG.md +5 -0
- package/engine/copilot-models.json +1 -1
- package/engine/lifecycle.js +8 -1
- package/engine/meeting.js +146 -64
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/engine/lifecycle.js
CHANGED
|
@@ -2662,7 +2662,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
2662
2662
|
if (type === WORK_TYPE.MEETING && meta?.meetingId) {
|
|
2663
2663
|
try {
|
|
2664
2664
|
const { collectMeetingFindings } = require('./meeting');
|
|
2665
|
-
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion, meta.round
|
|
2665
|
+
collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion, meta.round, {
|
|
2666
|
+
success: effectiveSuccess,
|
|
2667
|
+
result,
|
|
2668
|
+
code,
|
|
2669
|
+
completionStatus,
|
|
2670
|
+
agentReportedFailure,
|
|
2671
|
+
summary: resultSummary,
|
|
2672
|
+
});
|
|
2666
2673
|
} catch (err) { log('warn', `Meeting collect: ${err.message}`); }
|
|
2667
2674
|
}
|
|
2668
2675
|
|
package/engine/meeting.js
CHANGED
|
@@ -26,6 +26,7 @@ const ROUND_STATUS_BY_NAME = {
|
|
|
26
26
|
debate: 'debating',
|
|
27
27
|
conclude: 'concluding',
|
|
28
28
|
};
|
|
29
|
+
const ROUND_NUMBER_BY_NAME = { investigate: 1, debate: 2, conclude: 3 };
|
|
29
30
|
const ACTIVE_MEETING_STATUSES = new Set(Object.values(ROUND_STATUS_BY_NAME));
|
|
30
31
|
|
|
31
32
|
function isTerminalMeetingStatus(status) {
|
|
@@ -36,6 +37,90 @@ function expectedMeetingStatusForRound(roundName) {
|
|
|
36
37
|
return ROUND_STATUS_BY_NAME[String(roundName || '').toLowerCase()] || null;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function roundKeyFor(roundName, round) {
|
|
41
|
+
const numeric = Number(round);
|
|
42
|
+
if (Number.isFinite(numeric) && numeric > 0) return String(numeric);
|
|
43
|
+
return String(ROUND_NUMBER_BY_NAME[String(roundName || '').toLowerCase()] || 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getRoundFailures(meeting, roundName, round, create = false) {
|
|
47
|
+
if (!meeting.roundFailures || typeof meeting.roundFailures !== 'object') {
|
|
48
|
+
if (!create) return {};
|
|
49
|
+
meeting.roundFailures = {};
|
|
50
|
+
}
|
|
51
|
+
const key = roundKeyFor(roundName, round);
|
|
52
|
+
if (!meeting.roundFailures[key] || typeof meeting.roundFailures[key] !== 'object') {
|
|
53
|
+
if (!create) return {};
|
|
54
|
+
meeting.roundFailures[key] = {};
|
|
55
|
+
}
|
|
56
|
+
return meeting.roundFailures[key];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasRoundFailure(meeting, roundName, agentId, round = meeting.round) {
|
|
60
|
+
return Boolean(getRoundFailures(meeting, roundName, round, false)[agentId]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function hasRoundSuccess(meeting, roundName, agentId) {
|
|
64
|
+
if (roundName === 'investigate') return Boolean(meeting.findings?.[agentId]);
|
|
65
|
+
if (roundName === 'debate') return Boolean(meeting.debate?.[agentId]);
|
|
66
|
+
return Boolean(meeting.conclusion && meeting.conclusion.agent === agentId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasRoundTerminalOutcome(meeting, roundName, agentId, round = meeting.round) {
|
|
70
|
+
return hasRoundSuccess(meeting, roundName, agentId) || hasRoundFailure(meeting, roundName, agentId, round);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function allParticipantsFinishedRound(meeting, roundName, round = meeting.round) {
|
|
74
|
+
const participants = Array.isArray(meeting.participants) ? meeting.participants : [];
|
|
75
|
+
return participants.length > 0 && participants.every(agentId =>
|
|
76
|
+
hasRoundTerminalOutcome(meeting, roundName, agentId, round)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatRoundFailuresForConclusion(meeting, roundName, round = meeting.round) {
|
|
81
|
+
const failures = getRoundFailures(meeting, roundName, round, false);
|
|
82
|
+
return Object.entries(failures)
|
|
83
|
+
.map(([agent, failure]) => `- **${agent}**: ${failure.reason || 'Agent failed before producing a meeting contribution.'}`)
|
|
84
|
+
.join('\n') || '- No structured failure details were captured.';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildFailedMeetingConclusion(meeting, agents, reason) {
|
|
88
|
+
const base = buildTimedOutMeetingConclusion(meeting, agents)
|
|
89
|
+
.replace('*Auto-generated — conclusion round timed out.*', '*Auto-generated — conclusion round failed.*');
|
|
90
|
+
return `${base}\n\n## Conclusion Failure\n${reason || formatRoundFailuresForConclusion(meeting, 'conclude', meeting.round)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function advanceMeetingIfRoundComplete(meeting, roundName, meetingId, config = null) {
|
|
94
|
+
if (roundName === 'investigate') {
|
|
95
|
+
if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
|
|
96
|
+
meeting.status = 'debating';
|
|
97
|
+
meeting.round = 2;
|
|
98
|
+
meeting.roundStartedAt = ts();
|
|
99
|
+
log('info', `Meeting ${meetingId}: all findings finished — advancing to debate`);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (roundName === 'debate') {
|
|
103
|
+
if (!allParticipantsFinishedRound(meeting, roundName, meeting.round)) return false;
|
|
104
|
+
meeting.status = 'concluding';
|
|
105
|
+
meeting.round = 3;
|
|
106
|
+
meeting.roundStartedAt = ts();
|
|
107
|
+
log('info', `Meeting ${meetingId}: all debate responses finished — advancing to conclusion`);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (roundName === 'conclude' && !meeting.conclusion) {
|
|
111
|
+
const agents = (config || queries.getConfig()).agents || {};
|
|
112
|
+
const autoConclusion = buildFailedMeetingConclusion(meeting, agents);
|
|
113
|
+
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
|
|
114
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
|
|
115
|
+
meeting.status = 'completed';
|
|
116
|
+
meeting.completedAt = ts();
|
|
117
|
+
writeMeetingTranscriptToInbox(meeting, meetingId, agents);
|
|
118
|
+
log('warn', `Meeting ${meetingId}: conclusion failed — auto-generated fallback conclusion`);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
39
124
|
function isEmptyMeetingContent(text) {
|
|
40
125
|
const value = String(text || '').trim();
|
|
41
126
|
return !value || EMPTY_OUTPUT_PATTERNS.includes(value);
|
|
@@ -233,6 +318,15 @@ function buildTimedOutMeetingConclusion(meeting, agents) {
|
|
|
233
318
|
return `*Auto-generated — conclusion round timed out.*\n\nThis summary is based on ${findingsCount} finding${findingsCount === 1 ? '' : 's'} and ${debateCount} debate response${debateCount === 1 ? '' : 's'}.\n\n## Findings Highlights\n${findingsHighlights.join('\n')}\n\n## Debate Takeaways\n${(debateTakeaways.length ? debateTakeaways : fallbackDebate).join('\n')}\n\n## Recommended Next Steps\n${nextSteps.join('\n')}`;
|
|
234
319
|
}
|
|
235
320
|
|
|
321
|
+
function writeMeetingTranscriptToInbox(meeting, meetingId, agents) {
|
|
322
|
+
try {
|
|
323
|
+
const transcript = meeting.transcript.map(t =>
|
|
324
|
+
`### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
|
|
325
|
+
).join('\n\n---\n\n');
|
|
326
|
+
shared.writeToInbox('meeting', meetingId, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
|
|
327
|
+
} catch (e) { log('warn', `Meeting ${meetingId} inbox write: ${e.message}`); }
|
|
328
|
+
}
|
|
329
|
+
|
|
236
330
|
function getMeetings() {
|
|
237
331
|
if (!fs.existsSync(MEETINGS_DIR)) return [];
|
|
238
332
|
return fs.readdirSync(MEETINGS_DIR)
|
|
@@ -249,6 +343,8 @@ function getMeeting(id) {
|
|
|
249
343
|
if (!m.debate) m.debate = {};
|
|
250
344
|
if (!m.humanNotes) m.humanNotes = [];
|
|
251
345
|
if (!m.participants) m.participants = [];
|
|
346
|
+
if (!m.transcript) m.transcript = [];
|
|
347
|
+
if (!m.roundFailures || typeof m.roundFailures !== 'object') m.roundFailures = {};
|
|
252
348
|
}
|
|
253
349
|
return m;
|
|
254
350
|
}
|
|
@@ -272,6 +368,7 @@ function createMeeting({ title, agenda, participants }) {
|
|
|
272
368
|
debate: {},
|
|
273
369
|
conclusion: null,
|
|
274
370
|
humanNotes: [],
|
|
371
|
+
roundFailures: {},
|
|
275
372
|
transcript: [],
|
|
276
373
|
};
|
|
277
374
|
saveMeeting(meeting);
|
|
@@ -373,6 +470,8 @@ function discoverMeetingWork(config) {
|
|
|
373
470
|
// Skip if already submitted for this round
|
|
374
471
|
if (roundName === 'investigating' && meeting.findings?.[agentId]) continue;
|
|
375
472
|
if (roundName === 'debating' && meeting.debate?.[agentId]) continue;
|
|
473
|
+
const dispatchRoundName = roundName === 'investigating' ? 'investigate' : 'debate';
|
|
474
|
+
if (hasRoundFailure(meeting, dispatchRoundName, agentId, round)) continue;
|
|
376
475
|
|
|
377
476
|
const key = `meeting-${meeting.id}-r${round}-${agentId}`;
|
|
378
477
|
if (activeKeys.has(key)) continue;
|
|
@@ -417,7 +516,7 @@ function discoverMeetingWork(config) {
|
|
|
417
516
|
source: 'meeting',
|
|
418
517
|
meetingId: meeting.id,
|
|
419
518
|
round,
|
|
420
|
-
roundName:
|
|
519
|
+
roundName: dispatchRoundName,
|
|
421
520
|
}
|
|
422
521
|
});
|
|
423
522
|
}
|
|
@@ -429,7 +528,7 @@ function discoverMeetingWork(config) {
|
|
|
429
528
|
* Collect findings from a completed meeting agent.
|
|
430
529
|
* Called from runPostCompletionHooks when type === 'meeting'.
|
|
431
530
|
*/
|
|
432
|
-
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null, expectedRound = null) {
|
|
531
|
+
function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null, expectedRound = null, completionInfo = {}) {
|
|
433
532
|
const meeting = getMeeting(meetingId);
|
|
434
533
|
if (!meeting) return;
|
|
435
534
|
if (isTerminalMeetingStatus(meeting.status)) {
|
|
@@ -450,13 +549,33 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
450
549
|
log('info', `Ignoring stale round ${expectedRound} output from ${agentId} for meeting ${meetingId} currently on round ${meeting.round || 1}`);
|
|
451
550
|
return;
|
|
452
551
|
}
|
|
552
|
+
if (hasRoundTerminalOutcome(meeting, roundName, agentId, meeting.round)) {
|
|
553
|
+
log('info', `Ignoring duplicate ${roundName} output from ${agentId} for meeting ${meetingId}`);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
453
556
|
|
|
454
557
|
const content = resolveMeetingContributionContent(output, structuredCompletion);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (isEmptyMeetingContent(content)) {
|
|
458
|
-
|
|
459
|
-
|
|
558
|
+
const completionSucceeded = completionInfo?.success !== false;
|
|
559
|
+
|
|
560
|
+
if (!completionSucceeded || isEmptyMeetingContent(content)) {
|
|
561
|
+
const failures = getRoundFailures(meeting, roundName, meeting.round, true);
|
|
562
|
+
const reason = !completionSucceeded
|
|
563
|
+
? (completionInfo?.reason || completionInfo?.completionStatus || 'Agent failed before completing the meeting round')
|
|
564
|
+
: 'Agent produced empty meeting output';
|
|
565
|
+
failures[agentId] = {
|
|
566
|
+
reason,
|
|
567
|
+
content: content || completionInfo?.summary || '',
|
|
568
|
+
submittedAt: ts(),
|
|
569
|
+
};
|
|
570
|
+
meeting.transcript.push({
|
|
571
|
+
round: meeting.round,
|
|
572
|
+
agent: agentId,
|
|
573
|
+
type: 'failure',
|
|
574
|
+
content: reason,
|
|
575
|
+
at: ts(),
|
|
576
|
+
});
|
|
577
|
+
log('warn', `Meeting ${meetingId}: agent ${agentId} failed ${roundName} — ${reason}`);
|
|
578
|
+
advanceMeetingIfRoundComplete(meeting, roundName, meetingId);
|
|
460
579
|
saveMeeting(meeting);
|
|
461
580
|
return;
|
|
462
581
|
}
|
|
@@ -476,11 +595,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
476
595
|
// Write transcript to inbox so agents learn from it (slug-based dedup)
|
|
477
596
|
try {
|
|
478
597
|
const config = queries.getConfig();
|
|
479
|
-
|
|
480
|
-
const transcript = meeting.transcript.map(t =>
|
|
481
|
-
`### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
|
|
482
|
-
).join('\n\n---\n\n');
|
|
483
|
-
shared.writeToInbox('meeting', meetingId, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
|
|
598
|
+
writeMeetingTranscriptToInbox(meeting, meetingId, config.agents || {});
|
|
484
599
|
} catch (e) { log('warn', `Meeting ${meetingId} inbox write: ${e.message}`); }
|
|
485
600
|
|
|
486
601
|
log('info', `Meeting ${meetingId} completed — transcript written to inbox`);
|
|
@@ -488,26 +603,7 @@ function collectMeetingFindings(meetingId, agentId, roundName, output, structure
|
|
|
488
603
|
return;
|
|
489
604
|
}
|
|
490
605
|
|
|
491
|
-
|
|
492
|
-
const participantCount = meeting.participants.length;
|
|
493
|
-
const allSubmitted =
|
|
494
|
-
(meeting.status === 'investigating' && Object.keys(meeting.findings || {}).length >= participantCount) ||
|
|
495
|
-
(meeting.status === 'debating' && Object.keys(meeting.debate || {}).length >= participantCount);
|
|
496
|
-
|
|
497
|
-
if (allSubmitted) {
|
|
498
|
-
// Advance to next round
|
|
499
|
-
if (meeting.status === 'investigating') {
|
|
500
|
-
meeting.status = 'debating';
|
|
501
|
-
meeting.round = 2;
|
|
502
|
-
meeting.roundStartedAt = ts();
|
|
503
|
-
log('info', `Meeting ${meetingId}: all findings in — advancing to debate`);
|
|
504
|
-
} else if (meeting.status === 'debating') {
|
|
505
|
-
meeting.status = 'concluding';
|
|
506
|
-
meeting.round = 3;
|
|
507
|
-
meeting.roundStartedAt = ts();
|
|
508
|
-
log('info', `Meeting ${meetingId}: all debate responses in — advancing to conclusion`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
606
|
+
advanceMeetingIfRoundComplete(meeting, roundName, meetingId);
|
|
511
607
|
|
|
512
608
|
saveMeeting(meeting);
|
|
513
609
|
}
|
|
@@ -626,7 +722,8 @@ function deleteMeeting(id) {
|
|
|
626
722
|
|
|
627
723
|
/**
|
|
628
724
|
* Check for meeting rounds that have exceeded the timeout.
|
|
629
|
-
*
|
|
725
|
+
* Timeout is observational: rounds advance only after every participant has
|
|
726
|
+
* succeeded or failed, and conclusion waits for the conclusion agent outcome.
|
|
630
727
|
* Called from engine.js tick cycle.
|
|
631
728
|
*/
|
|
632
729
|
function checkMeetingTimeouts(config) {
|
|
@@ -651,38 +748,23 @@ function checkMeetingTimeouts(config) {
|
|
|
651
748
|
: 0;
|
|
652
749
|
const totalCount = meeting.participants.length;
|
|
653
750
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
751
|
+
const roundName = meeting.status === 'investigating'
|
|
752
|
+
? 'investigate'
|
|
753
|
+
: meeting.status === 'debating'
|
|
754
|
+
? 'debate'
|
|
755
|
+
: 'conclude';
|
|
756
|
+
|
|
757
|
+
if (roundName !== 'conclude') {
|
|
758
|
+
if (allParticipantsFinishedRound(meeting, roundName, meeting.round)) {
|
|
759
|
+
log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min but all participants are terminal — advancing`);
|
|
760
|
+
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'timeout', content: `Round ${meeting.round} timed out after all participants finished`, at: ts() });
|
|
761
|
+
advanceMeetingIfRoundComplete(meeting, roundName, meeting.id, config);
|
|
762
|
+
saveMeeting(meeting);
|
|
763
|
+
} else {
|
|
764
|
+
log('warn', `Meeting ${meeting.id}: round ${meeting.round} timed out after ${Math.round(elapsed / 60000)}min — waiting for all participants to finish (${respondedCount}/${totalCount} succeeded)`);
|
|
765
|
+
}
|
|
668
766
|
} else if (meeting.status === 'concluding') {
|
|
669
|
-
log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min —
|
|
670
|
-
const autoConclusion = buildTimedOutMeetingConclusion(meeting, config.agents || {});
|
|
671
|
-
meeting.conclusion = { content: autoConclusion, agent: 'system', submittedAt: ts() };
|
|
672
|
-
meeting.transcript.push({ round: meeting.round, agent: 'system', type: 'conclusion', content: autoConclusion, at: ts() });
|
|
673
|
-
meeting.status = 'completed';
|
|
674
|
-
meeting.completedAt = ts();
|
|
675
|
-
|
|
676
|
-
// Write transcript to inbox (same as normal conclusion path)
|
|
677
|
-
try {
|
|
678
|
-
const agents = config.agents || {};
|
|
679
|
-
const transcript = meeting.transcript.map(t =>
|
|
680
|
-
`### ${agents[t.agent]?.name || t.agent} (${t.type}, Round ${t.round})\n\n${t.content}`
|
|
681
|
-
).join('\n\n---\n\n');
|
|
682
|
-
shared.writeToInbox('meeting', meeting.id, `# Meeting Transcript: ${meeting.title}\n\n${transcript}`);
|
|
683
|
-
} catch (e) { log('warn', `Meeting ${meeting.id} inbox write: ${e.message}`); }
|
|
684
|
-
|
|
685
|
-
saveMeeting(meeting);
|
|
767
|
+
log('warn', `Meeting ${meeting.id}: conclusion round timed out after ${Math.round(elapsed / 60000)}min — waiting for the conclusion agent to finish`);
|
|
686
768
|
}
|
|
687
769
|
}
|
|
688
770
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1711",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|