@yemi33/minions 0.1.59 → 0.1.60
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 +11 -0
- package/engine/cooldown.js +117 -0
- package/engine/playbook.js +479 -0
- package/engine/routing.js +163 -0
- package/engine.js +14 -641
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/routing.js — Agent routing, budget checks, and routing table parsing.
|
|
3
|
+
* Extracted from engine.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const shared = require('./shared');
|
|
9
|
+
const queries = require('./queries');
|
|
10
|
+
|
|
11
|
+
const { safeJson, safeRead } = shared;
|
|
12
|
+
const { ENGINE_DIR, DISPATCH_PATH } = queries;
|
|
13
|
+
|
|
14
|
+
const MINIONS_DIR = path.resolve(__dirname, '..');
|
|
15
|
+
const ROUTING_PATH = path.join(MINIONS_DIR, 'routing.md');
|
|
16
|
+
|
|
17
|
+
// Lazy require to avoid circular dependency with engine.js
|
|
18
|
+
let _engine = null;
|
|
19
|
+
function engine() { if (!_engine) _engine = require('../engine'); return _engine; }
|
|
20
|
+
|
|
21
|
+
// ─── Temp Agents ─────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const tempAgents = new Map(); // tempAgentId → { name, role, createdAt }
|
|
24
|
+
|
|
25
|
+
// ─── Routing Parser ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function getRouting() {
|
|
28
|
+
return safeRead(ROUTING_PATH);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let _routingCache = null;
|
|
32
|
+
let _routingCacheMtime = 0;
|
|
33
|
+
|
|
34
|
+
function parseRoutingTable() {
|
|
35
|
+
const content = getRouting();
|
|
36
|
+
const routes = {};
|
|
37
|
+
const lines = content.split('\n');
|
|
38
|
+
let inTable = false;
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (line.startsWith('| Work Type')) { inTable = true; continue; }
|
|
42
|
+
if (line.startsWith('|---')) continue;
|
|
43
|
+
if (!inTable || !line.startsWith('|')) {
|
|
44
|
+
if (inTable && !line.startsWith('|')) inTable = false;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
48
|
+
if (cells.length >= 3) {
|
|
49
|
+
routes[cells[0].toLowerCase()] = {
|
|
50
|
+
preferred: cells[1].toLowerCase(),
|
|
51
|
+
fallback: cells[2].toLowerCase()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return routes;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getRoutingTableCached() {
|
|
59
|
+
let mtime = 0;
|
|
60
|
+
try { mtime = fs.statSync(ROUTING_PATH).mtimeMs; } catch { /* optional */ }
|
|
61
|
+
if (_routingCache && _routingCacheMtime === mtime) return _routingCache;
|
|
62
|
+
_routingCache = parseRoutingTable();
|
|
63
|
+
_routingCacheMtime = mtime;
|
|
64
|
+
return _routingCache;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Budget ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function getMonthlySpend(agentId) {
|
|
70
|
+
const metrics = safeJson(path.join(ENGINE_DIR, 'metrics.json')) || {};
|
|
71
|
+
const daily = metrics._daily || {};
|
|
72
|
+
const now = new Date();
|
|
73
|
+
const monthPrefix = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
|
|
74
|
+
let total = 0;
|
|
75
|
+
for (const [date, data] of Object.entries(daily)) {
|
|
76
|
+
if (date.startsWith(monthPrefix)) {
|
|
77
|
+
total += (data.perAgent?.[agentId]?.costUsd || 0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Fallback: if no per-agent daily data, use cumulative (less accurate for monthly)
|
|
81
|
+
if (total === 0 && metrics[agentId]?.totalCostUsd) {
|
|
82
|
+
// Can't distinguish monthly from cumulative — treat as monthly estimate
|
|
83
|
+
// This path is for backward compat before per-agent daily tracking was added
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getAgentErrorRate(agentId) {
|
|
89
|
+
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
90
|
+
const metrics = safeJson(metricsPath) || {};
|
|
91
|
+
const m = metrics[agentId];
|
|
92
|
+
if (!m) return 0;
|
|
93
|
+
const total = m.tasksCompleted + m.tasksErrored;
|
|
94
|
+
return total > 0 ? m.tasksErrored / total : 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isAgentIdle(agentId) {
|
|
98
|
+
// Dispatch queue is the single source of truth for agent availability
|
|
99
|
+
const dispatch = safeJson(DISPATCH_PATH) || {};
|
|
100
|
+
return !(dispatch.active || []).some(d => d.agent === agentId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Agent Resolution ────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
// Track agents claimed during a single discovery pass to distribute work
|
|
106
|
+
const _claimedAgents = new Set();
|
|
107
|
+
function resetClaimedAgents() { _claimedAgents.clear(); }
|
|
108
|
+
|
|
109
|
+
function resolveAgent(workType, config, authorAgent = null) {
|
|
110
|
+
const routes = getRoutingTableCached();
|
|
111
|
+
const route = routes[workType] || routes['implement'];
|
|
112
|
+
const agents = config.agents || {};
|
|
113
|
+
|
|
114
|
+
// Resolve _author_ token
|
|
115
|
+
let preferred = route.preferred === '_author_' ? authorAgent : route.preferred;
|
|
116
|
+
let fallback = route.fallback === '_author_' ? authorAgent : route.fallback;
|
|
117
|
+
|
|
118
|
+
const isAvailable = (id) => {
|
|
119
|
+
if (!agents[id] || !isAgentIdle(id) || _claimedAgents.has(id)) return false;
|
|
120
|
+
// Budget check — no budget means infinite (no limit)
|
|
121
|
+
const budget = agents[id].monthlyBudgetUsd;
|
|
122
|
+
if (budget && budget > 0) {
|
|
123
|
+
if (getMonthlySpend(id) >= budget) return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Check preferred and fallback first (routing table order)
|
|
129
|
+
if (preferred && isAvailable(preferred)) { _claimedAgents.add(preferred); return preferred; }
|
|
130
|
+
if (fallback && isAvailable(fallback)) { _claimedAgents.add(fallback); return fallback; }
|
|
131
|
+
|
|
132
|
+
// Fall back to any idle agent, preferring lower error rates
|
|
133
|
+
const idle = Object.keys(agents)
|
|
134
|
+
.filter(id => id !== preferred && id !== fallback && isAvailable(id))
|
|
135
|
+
.sort((a, b) => getAgentErrorRate(a) - getAgentErrorRate(b));
|
|
136
|
+
|
|
137
|
+
if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
|
|
138
|
+
|
|
139
|
+
// No idle configured agent — try temp agent if enabled
|
|
140
|
+
if (config.engine?.allowTempAgents) {
|
|
141
|
+
const tempId = `temp-${shared.uid()}`;
|
|
142
|
+
_claimedAgents.add(tempId);
|
|
143
|
+
tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: engine().ts() });
|
|
144
|
+
engine().log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
|
|
145
|
+
return tempId;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// No idle agent available — return null, item stays pending until next tick
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
tempAgents,
|
|
154
|
+
getRouting,
|
|
155
|
+
parseRoutingTable,
|
|
156
|
+
getRoutingTableCached,
|
|
157
|
+
getMonthlySpend,
|
|
158
|
+
getAgentErrorRate,
|
|
159
|
+
isAgentIdle,
|
|
160
|
+
_claimedAgents,
|
|
161
|
+
resetClaimedAgents,
|
|
162
|
+
resolveAgent,
|
|
163
|
+
};
|