@arka-labs/nemesis 1.2.0
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/LICENSE +201 -0
- package/README.md +668 -0
- package/lib/core/agent-launcher.js +193 -0
- package/lib/core/audit.js +210 -0
- package/lib/core/connexions.js +80 -0
- package/lib/core/flowmap/api.js +111 -0
- package/lib/core/flowmap/cli-helpers.js +80 -0
- package/lib/core/flowmap/machine.js +281 -0
- package/lib/core/flowmap/persistence.js +83 -0
- package/lib/core/generators.js +183 -0
- package/lib/core/inbox.js +275 -0
- package/lib/core/logger.js +20 -0
- package/lib/core/mission.js +109 -0
- package/lib/core/notewriter/config.js +36 -0
- package/lib/core/notewriter/cr.js +237 -0
- package/lib/core/notewriter/log.js +112 -0
- package/lib/core/notewriter/notes.js +168 -0
- package/lib/core/notewriter/paths.js +45 -0
- package/lib/core/notewriter/reader.js +121 -0
- package/lib/core/notewriter/registry.js +80 -0
- package/lib/core/odm.js +191 -0
- package/lib/core/profile-picker.js +323 -0
- package/lib/core/project.js +287 -0
- package/lib/core/registry.js +129 -0
- package/lib/core/secrets.js +137 -0
- package/lib/core/services.js +45 -0
- package/lib/core/team.js +287 -0
- package/lib/core/templates.js +80 -0
- package/lib/kairos/agent-runner.js +261 -0
- package/lib/kairos/claude-invoker.js +90 -0
- package/lib/kairos/context-injector.js +331 -0
- package/lib/kairos/context-loader.js +108 -0
- package/lib/kairos/context-writer.js +45 -0
- package/lib/kairos/dispatcher-router.js +173 -0
- package/lib/kairos/dispatcher.js +139 -0
- package/lib/kairos/event-bus.js +287 -0
- package/lib/kairos/event-router.js +131 -0
- package/lib/kairos/flowmap-bridge.js +120 -0
- package/lib/kairos/hook-handlers.js +351 -0
- package/lib/kairos/hook-installer.js +207 -0
- package/lib/kairos/hook-prompts.js +54 -0
- package/lib/kairos/leader-rules.js +94 -0
- package/lib/kairos/pid-checker.js +108 -0
- package/lib/kairos/situation-detector.js +123 -0
- package/lib/sync/fallback-engine.js +97 -0
- package/lib/sync/hcm-client.js +170 -0
- package/lib/sync/health.js +47 -0
- package/lib/sync/llm-client.js +387 -0
- package/lib/sync/nemesis-client.js +379 -0
- package/lib/sync/service-session.js +74 -0
- package/lib/sync/sync-engine.js +178 -0
- package/lib/ui/box.js +104 -0
- package/lib/ui/brand.js +42 -0
- package/lib/ui/colors.js +57 -0
- package/lib/ui/dashboard.js +580 -0
- package/lib/ui/error-hints.js +49 -0
- package/lib/ui/format.js +61 -0
- package/lib/ui/menu.js +306 -0
- package/lib/ui/note-card.js +198 -0
- package/lib/ui/note-colors.js +26 -0
- package/lib/ui/note-detail.js +297 -0
- package/lib/ui/note-filters.js +252 -0
- package/lib/ui/note-views.js +283 -0
- package/lib/ui/prompt.js +81 -0
- package/lib/ui/spinner.js +139 -0
- package/lib/ui/streambox.js +46 -0
- package/lib/ui/table.js +42 -0
- package/lib/ui/tree.js +33 -0
- package/package.json +53 -0
- package/src/cli.js +457 -0
- package/src/commands/_helpers.js +119 -0
- package/src/commands/audit.js +187 -0
- package/src/commands/auth.js +316 -0
- package/src/commands/doctor.js +243 -0
- package/src/commands/hcm.js +147 -0
- package/src/commands/inbox.js +333 -0
- package/src/commands/init.js +160 -0
- package/src/commands/kairos.js +216 -0
- package/src/commands/kars.js +134 -0
- package/src/commands/mission.js +275 -0
- package/src/commands/notes.js +316 -0
- package/src/commands/notewriter.js +296 -0
- package/src/commands/odm.js +329 -0
- package/src/commands/orch.js +68 -0
- package/src/commands/project.js +123 -0
- package/src/commands/run.js +123 -0
- package/src/commands/services.js +705 -0
- package/src/commands/status.js +231 -0
- package/src/commands/team.js +572 -0
- package/src/config.js +84 -0
- package/src/index.js +5 -0
- package/templates/project-context.json +10 -0
- package/templates/template_CONTRIB-NAME.json +22 -0
- package/templates/template_CR-ODM-NAME-000.exemple.json +32 -0
- package/templates/template_DEC-NAME-000.json +18 -0
- package/templates/template_INTV-NAME-000.json +15 -0
- package/templates/template_MISSION_CONTRACT.json +46 -0
- package/templates/template_ODM-NAME-000.json +89 -0
- package/templates/template_REGISTRY-PROJECT.json +26 -0
- package/templates/template_TXN-NAME-000.json +24 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher Router — execute routing decisions post-classification.
|
|
3
|
+
* V1: terminal display only. V2 will add webhook HCM + event-bus.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { loadTemplate, mergeTemplate } from '../core/templates.js';
|
|
9
|
+
import { titledBox } from '../ui/box.js';
|
|
10
|
+
import { style } from '../ui/colors.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Display routing decisions in terminal.
|
|
14
|
+
* LEADER = green box, HUMAN = yellow box, NOISE = dim line.
|
|
15
|
+
* @param {Array} routing — array of { target, reason, priority, summary }
|
|
16
|
+
* @param {object} context — { agentName, odmId }
|
|
17
|
+
* @returns {{ displayed: string[], archived: string[], escalated: string[] }}
|
|
18
|
+
*/
|
|
19
|
+
export function routeToTerminal(routing, _context = {}) {
|
|
20
|
+
const displayed = [];
|
|
21
|
+
const archived = [];
|
|
22
|
+
const escalated = [];
|
|
23
|
+
|
|
24
|
+
for (const entry of routing) {
|
|
25
|
+
const line = formatSingleRouting(entry);
|
|
26
|
+
|
|
27
|
+
switch (entry.target) {
|
|
28
|
+
case 'LEADER':
|
|
29
|
+
console.log('');
|
|
30
|
+
console.log(titledBox('Routing LEADER', [line], { border: style.green }));
|
|
31
|
+
displayed.push(entry.summary || entry.reason);
|
|
32
|
+
break;
|
|
33
|
+
case 'HUMAN':
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(titledBox('Routing HUMAN', [line], { border: style.yellow }));
|
|
36
|
+
escalated.push(entry.summary || entry.reason);
|
|
37
|
+
break;
|
|
38
|
+
case 'NOISE':
|
|
39
|
+
console.log(` ${style.dim(`NOISE ○ [${entry.priority}] ${entry.summary || entry.reason}`)}`);
|
|
40
|
+
archived.push(entry.summary || entry.reason);
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// V2: webhook HCM (Signal 2)
|
|
46
|
+
// V2: event-bus push vers Leader
|
|
47
|
+
// V2: notifications Slack/email pour HUMAN
|
|
48
|
+
|
|
49
|
+
return { displayed, archived, escalated };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create a dispatch Transaction in .nemesis/HCM/transactions/.
|
|
54
|
+
* @param {object} opts — { from, to, odmId, projectId, root }
|
|
55
|
+
* @returns {{ filepath: string, txnId: string }}
|
|
56
|
+
*/
|
|
57
|
+
export function createDispatchTransaction(opts = {}) {
|
|
58
|
+
const { from = 'PM', to, odmId = '', missionId = '', projectId = '', root = process.cwd() } = opts;
|
|
59
|
+
|
|
60
|
+
const txnId = `TXN-DISP-${Date.now()}`;
|
|
61
|
+
const txnDir = join(root, '.nemesis', 'HCM', 'transactions');
|
|
62
|
+
|
|
63
|
+
if (!existsSync(txnDir)) {
|
|
64
|
+
mkdirSync(txnDir, { recursive: true });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const refs = [];
|
|
68
|
+
if (odmId) refs.push(odmId);
|
|
69
|
+
if (missionId) refs.push(missionId);
|
|
70
|
+
|
|
71
|
+
const template = loadTemplate('transaction', root);
|
|
72
|
+
const txn = mergeTemplate(template, {
|
|
73
|
+
transaction_meta: {
|
|
74
|
+
txn_id: txnId,
|
|
75
|
+
project_id: projectId,
|
|
76
|
+
type: 'OdM_DISPATCH',
|
|
77
|
+
status: 'SENT',
|
|
78
|
+
created_at: new Date().toISOString(),
|
|
79
|
+
mission_id: missionId || undefined,
|
|
80
|
+
},
|
|
81
|
+
transaction_payload: {
|
|
82
|
+
from: { actor_type: 'human', actor_id: from },
|
|
83
|
+
to: { actor_type: 'agent', actor_id: to },
|
|
84
|
+
subject: `Dispatch OdM ${odmId}`,
|
|
85
|
+
refs,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const filepath = join(txnDir, `${txnId}.json`);
|
|
90
|
+
writeFileSync(filepath, JSON.stringify(txn, null, 2) + '\n', 'utf-8');
|
|
91
|
+
|
|
92
|
+
return { filepath, txnId };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Format routing decisions as display lines.
|
|
97
|
+
* @param {Array} routing
|
|
98
|
+
* @param {object} context
|
|
99
|
+
* @returns {string[]}
|
|
100
|
+
*/
|
|
101
|
+
export function formatRoutingDisplay(routing, _context = {}) {
|
|
102
|
+
return routing.map(entry => formatSingleRouting(entry));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatSingleRouting(entry) {
|
|
106
|
+
const icons = { LEADER: '★', HUMAN: '⚠', NOISE: '○' };
|
|
107
|
+
const colors = { LEADER: style.green, HUMAN: style.yellow, NOISE: style.dim };
|
|
108
|
+
const icon = icons[entry.target] || '?';
|
|
109
|
+
const colorFn = colors[entry.target] || (v => v);
|
|
110
|
+
return colorFn(`${entry.target} ${icon} [${entry.priority}] ${entry.reason} — ${entry.summary}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Route classified dispatch results to files for inter-process consumption.
|
|
115
|
+
* LEADER → .nemesis/kairos/events/
|
|
116
|
+
* HUMAN → .nemesis/kairos/escalations/
|
|
117
|
+
* NOISE → .nemesis/kairos/noise/
|
|
118
|
+
*
|
|
119
|
+
* @param {Array} routing - array of { target, reason, priority, summary }
|
|
120
|
+
* @param {object} context - { agentName, odmId, projectId }
|
|
121
|
+
* @param {string} projectRoot
|
|
122
|
+
* @returns {{ events: string[], escalations: string[], noise: string[] }}
|
|
123
|
+
*/
|
|
124
|
+
export function routeToFile(routing, context = {}, projectRoot = process.cwd()) {
|
|
125
|
+
const kairosDir = join(projectRoot, '.nemesis', 'kairos');
|
|
126
|
+
const events = [];
|
|
127
|
+
const escalations = [];
|
|
128
|
+
const noise = [];
|
|
129
|
+
|
|
130
|
+
for (const entry of routing) {
|
|
131
|
+
const ts = Date.now();
|
|
132
|
+
const agentId = context.agentName || 'unknown';
|
|
133
|
+
const payload = {
|
|
134
|
+
timestamp: new Date().toISOString(),
|
|
135
|
+
agentName: agentId,
|
|
136
|
+
odmId: context.odmId || '',
|
|
137
|
+
projectId: context.projectId || '',
|
|
138
|
+
target: entry.target,
|
|
139
|
+
priority: entry.priority,
|
|
140
|
+
reason: entry.reason,
|
|
141
|
+
summary: entry.summary,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
switch (entry.target) {
|
|
145
|
+
case 'LEADER': {
|
|
146
|
+
const dir = join(kairosDir, 'events');
|
|
147
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
148
|
+
const filepath = join(dir, `${ts}-${agentId}.json`);
|
|
149
|
+
writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
150
|
+
events.push(filepath);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 'HUMAN': {
|
|
154
|
+
const dir = join(kairosDir, 'escalations');
|
|
155
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
156
|
+
const filepath = join(dir, `${ts}.json`);
|
|
157
|
+
writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
158
|
+
escalations.push(filepath);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
case 'NOISE': {
|
|
162
|
+
const dir = join(kairosDir, 'noise');
|
|
163
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
164
|
+
const filepath = join(dir, `${ts}.json`);
|
|
165
|
+
writeFileSync(filepath, JSON.stringify(payload, null, 2) + '\n', 'utf-8');
|
|
166
|
+
noise.push(filepath);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { events, escalations, noise };
|
|
173
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatcher — LLM classification of agent responses.
|
|
3
|
+
* Routes to LEADER, HUMAN, or NOISE.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { debug } from '../core/logger.js';
|
|
7
|
+
|
|
8
|
+
export const ROUTING_TARGETS = { LEADER: 'LEADER', HUMAN: 'HUMAN', NOISE: 'NOISE' };
|
|
9
|
+
|
|
10
|
+
const VALID_TARGETS = new Set(Object.values(ROUTING_TARGETS));
|
|
11
|
+
const VALID_PRIORITIES = new Set(['HIGH', 'MEDIUM', 'LOW']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Build the system prompt for the Dispatcher LLM.
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function buildDispatcherSystemPrompt() {
|
|
18
|
+
return `Tu es le Kairos Dispatcher. Classe la reponse d'un agent en 3 categories.
|
|
19
|
+
|
|
20
|
+
## LEADER
|
|
21
|
+
Contient un livrable, CR, demande de validation, rapport d'avancement,
|
|
22
|
+
ou information que le Leader doit examiner.
|
|
23
|
+
Indices : fichiers deposes, references OdM, "voici le livrable", "CR depose".
|
|
24
|
+
|
|
25
|
+
## HUMAN
|
|
26
|
+
Demande d'intervention humaine : decision hors perimetre, blocage non
|
|
27
|
+
resolvable, clarification PM, alerte securite.
|
|
28
|
+
Indices : "besoin d'arbitrage PM", "blocage", "question pour le PM".
|
|
29
|
+
|
|
30
|
+
## NOISE
|
|
31
|
+
Accuse de reception, log intermediaire, confirmation standard, contenu vide.
|
|
32
|
+
Indices : reponse < 50 mots sans contenu technique, "OK", "compris".
|
|
33
|
+
|
|
34
|
+
## Regles
|
|
35
|
+
1. Doute LEADER/NOISE → LEADER
|
|
36
|
+
2. Doute LEADER/HUMAN → HUMAN
|
|
37
|
+
3. Un CR n'est JAMAIS NOISE
|
|
38
|
+
4. Message > 200 mots technique = rarement NOISE
|
|
39
|
+
|
|
40
|
+
## Format JSON strict
|
|
41
|
+
{ "routing": [{ "target": "LEADER"|"HUMAN"|"NOISE", "reason": "...", "priority": "HIGH"|"MEDIUM"|"LOW", "summary": "..." }] }`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build the user prompt containing the agent response to classify.
|
|
46
|
+
* @param {string} agentResponse
|
|
47
|
+
* @param {object} context — { agentName, odmId, projectId }
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
export function buildDispatcherUserPrompt(agentResponse, context = {}) {
|
|
51
|
+
const parts = ['# Reponse agent a classifier'];
|
|
52
|
+
if (context.agentName) parts.push(`Agent : ${context.agentName}`);
|
|
53
|
+
if (context.odmId) parts.push(`OdM : ${context.odmId}`);
|
|
54
|
+
if (context.projectId) parts.push(`Projet : ${context.projectId}`);
|
|
55
|
+
parts.push('', '## Reponse', '', agentResponse);
|
|
56
|
+
return parts.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Classify an agent response via LLM.
|
|
61
|
+
* @param {string} agentResponse
|
|
62
|
+
* @param {object} context — { agentName, odmId, projectId }
|
|
63
|
+
* @param {function} callLlm — async (systemPrompt, userPrompt) => string
|
|
64
|
+
* @returns {Promise<{ routing: Array, raw: string }>}
|
|
65
|
+
*/
|
|
66
|
+
export async function classifyResponse(agentResponse, context, callLlm) {
|
|
67
|
+
const systemPrompt = buildDispatcherSystemPrompt();
|
|
68
|
+
const userPrompt = buildDispatcherUserPrompt(agentResponse, context);
|
|
69
|
+
|
|
70
|
+
let raw;
|
|
71
|
+
try {
|
|
72
|
+
raw = await callLlm(systemPrompt, userPrompt);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
debug(`classifyResponse LLM: ${e.message}`);
|
|
75
|
+
return classifyFallback(agentResponse, context);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parsed = parseJsonResponse(raw);
|
|
79
|
+
if (!parsed) return classifyFallback(agentResponse, context);
|
|
80
|
+
|
|
81
|
+
return { routing: parsed.routing, raw };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Graceful degradation: route everything to LEADER.
|
|
86
|
+
* @param {string} agentResponse
|
|
87
|
+
* @param {object} context
|
|
88
|
+
* @returns {{ routing: Array, raw: string }}
|
|
89
|
+
*/
|
|
90
|
+
export function classifyFallback(agentResponse, _context = {}) {
|
|
91
|
+
const maxLen = 200;
|
|
92
|
+
const summary = agentResponse.length > maxLen
|
|
93
|
+
? agentResponse.slice(0, maxLen) + '...'
|
|
94
|
+
: agentResponse;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
routing: [{
|
|
98
|
+
target: 'LEADER',
|
|
99
|
+
reason: 'Classification automatique (fallback)',
|
|
100
|
+
priority: 'MEDIUM',
|
|
101
|
+
summary,
|
|
102
|
+
}],
|
|
103
|
+
raw: '',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parse JSON from LLM response — direct or from markdown code block.
|
|
109
|
+
* @param {string} raw
|
|
110
|
+
* @returns {{ routing: Array }|null}
|
|
111
|
+
*/
|
|
112
|
+
function parseJsonResponse(raw) {
|
|
113
|
+
// Try direct parse
|
|
114
|
+
let obj = tryParse(raw);
|
|
115
|
+
if (obj && validateRouting(obj)) return obj;
|
|
116
|
+
|
|
117
|
+
// Try extracting from markdown code block
|
|
118
|
+
const match = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
119
|
+
if (match) {
|
|
120
|
+
obj = tryParse(match[1].trim());
|
|
121
|
+
if (obj && validateRouting(obj)) return obj;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function tryParse(str) {
|
|
128
|
+
try { return JSON.parse(str); } catch (e) { debug(`tryParse: ${e.message}`); return null; }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateRouting(obj) {
|
|
132
|
+
if (!obj || !Array.isArray(obj.routing) || obj.routing.length === 0) return false;
|
|
133
|
+
return obj.routing.every(entry =>
|
|
134
|
+
VALID_TARGETS.has(entry.target) &&
|
|
135
|
+
typeof entry.reason === 'string' &&
|
|
136
|
+
VALID_PRIORITIES.has(entry.priority) &&
|
|
137
|
+
typeof entry.summary === 'string'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { debug } from '../core/logger.js';
|
|
5
|
+
|
|
6
|
+
const EVENT_BUS_DIR = '.nemesis/kairos/event-bus';
|
|
7
|
+
|
|
8
|
+
export const EVENT_TYPES = Object.freeze({
|
|
9
|
+
CR_DEPOSITED: 'cr_deposited',
|
|
10
|
+
TASK_ASSIGNED: 'task_assigned',
|
|
11
|
+
VALIDATION_REQUIRED: 'validation_required',
|
|
12
|
+
DISPATCH_RESULT: 'dispatch_result',
|
|
13
|
+
MESSAGE: 'message',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const PRIORITIES = Object.freeze({
|
|
17
|
+
HIGH: 'HIGH',
|
|
18
|
+
MEDIUM: 'MEDIUM',
|
|
19
|
+
LOW: 'LOW',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const DEFAULT_TTL = 300; // seconds
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve queue directory for an agent.
|
|
26
|
+
*/
|
|
27
|
+
function queueDir(projectRoot, agentId) {
|
|
28
|
+
return join(projectRoot, EVENT_BUS_DIR, agentId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a unique event ID.
|
|
33
|
+
*/
|
|
34
|
+
function generateEventId() {
|
|
35
|
+
const ts = Date.now();
|
|
36
|
+
const rand = randomBytes(3).toString('hex');
|
|
37
|
+
return `EVT-${ts}-${rand}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Push an event to an agent's inbox queue.
|
|
42
|
+
* @param {string} projectRoot
|
|
43
|
+
* @param {object} opts
|
|
44
|
+
* @param {string} opts.target_agent_id
|
|
45
|
+
* @param {string} opts.event_type
|
|
46
|
+
* @param {object} [opts.payload]
|
|
47
|
+
* @param {string} [opts.priority]
|
|
48
|
+
* @param {number} [opts.ttl] - TTL in seconds
|
|
49
|
+
* @param {string} [opts.source_agent_id]
|
|
50
|
+
* @returns {{ eventId: string, filepath: string }}
|
|
51
|
+
*/
|
|
52
|
+
export function pushEvent(projectRoot, opts) {
|
|
53
|
+
const {
|
|
54
|
+
target_agent_id,
|
|
55
|
+
event_type,
|
|
56
|
+
payload = {},
|
|
57
|
+
priority = PRIORITIES.MEDIUM,
|
|
58
|
+
ttl = DEFAULT_TTL,
|
|
59
|
+
source_agent_id = 'system',
|
|
60
|
+
} = opts;
|
|
61
|
+
|
|
62
|
+
if (!target_agent_id) throw new Error('target_agent_id requis');
|
|
63
|
+
if (!event_type) throw new Error('event_type requis');
|
|
64
|
+
|
|
65
|
+
const dir = queueDir(projectRoot, target_agent_id);
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
const eventId = generateEventId();
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const expiresAt = new Date(now.getTime() + ttl * 1000);
|
|
71
|
+
|
|
72
|
+
const event = {
|
|
73
|
+
event_id: eventId,
|
|
74
|
+
event_type,
|
|
75
|
+
source_agent_id,
|
|
76
|
+
target_agent_id,
|
|
77
|
+
priority,
|
|
78
|
+
ttl,
|
|
79
|
+
created_at: now.toISOString(),
|
|
80
|
+
expires_at: expiresAt.toISOString(),
|
|
81
|
+
payload,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const filepath = join(dir, `${eventId}.json`);
|
|
85
|
+
writeFileSync(filepath, JSON.stringify(event, null, 2) + '\n', 'utf-8');
|
|
86
|
+
|
|
87
|
+
return { eventId, filepath };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Poll pending events for an agent (non-consuming read).
|
|
92
|
+
* @param {string} projectRoot
|
|
93
|
+
* @param {string} agentId
|
|
94
|
+
* @returns {Array<{ eventId: string, event: object, filepath: string }>}
|
|
95
|
+
*/
|
|
96
|
+
export function pollEvents(projectRoot, agentId) {
|
|
97
|
+
const dir = queueDir(projectRoot, agentId);
|
|
98
|
+
if (!existsSync(dir)) return [];
|
|
99
|
+
|
|
100
|
+
const files = readdirSync(dir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
|
|
101
|
+
const results = [];
|
|
102
|
+
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const filepath = join(dir, file);
|
|
105
|
+
try {
|
|
106
|
+
const event = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
107
|
+
results.push({
|
|
108
|
+
eventId: event.event_id || file.replace('.json', ''),
|
|
109
|
+
event,
|
|
110
|
+
filepath,
|
|
111
|
+
});
|
|
112
|
+
} catch (e) { debug(`pollEvents ${file}: ${e.message}`); }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Sort by priority (HIGH first) then by creation time
|
|
116
|
+
const priorityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
117
|
+
results.sort((a, b) => {
|
|
118
|
+
const pa = priorityOrder[a.event.priority] ?? 1;
|
|
119
|
+
const pb = priorityOrder[b.event.priority] ?? 1;
|
|
120
|
+
if (pa !== pb) return pa - pb;
|
|
121
|
+
return (a.event.created_at || '').localeCompare(b.event.created_at || '');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Consume (delete) a single event by ID.
|
|
129
|
+
* @param {string} projectRoot
|
|
130
|
+
* @param {string} agentId
|
|
131
|
+
* @param {string} eventId
|
|
132
|
+
*/
|
|
133
|
+
export function consumeEvent(projectRoot, agentId, eventId) {
|
|
134
|
+
const dir = queueDir(projectRoot, agentId);
|
|
135
|
+
if (!existsSync(dir)) return;
|
|
136
|
+
|
|
137
|
+
const filepath = join(dir, `${eventId}.json`);
|
|
138
|
+
try { unlinkSync(filepath); } catch (e) { debug(`consumeEvent: ${e.message}`); }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Atomically consume all events for an agent — read + delete.
|
|
143
|
+
* @param {string} projectRoot
|
|
144
|
+
* @param {string} agentId
|
|
145
|
+
* @returns {Array<{ eventId: string, event: object }>}
|
|
146
|
+
*/
|
|
147
|
+
export function consumeAllEvents(projectRoot, agentId) {
|
|
148
|
+
const dir = queueDir(projectRoot, agentId);
|
|
149
|
+
if (!existsSync(dir)) return [];
|
|
150
|
+
|
|
151
|
+
const files = readdirSync(dir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
|
|
152
|
+
const results = [];
|
|
153
|
+
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
const filepath = join(dir, file);
|
|
156
|
+
try {
|
|
157
|
+
const event = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
158
|
+
results.push({
|
|
159
|
+
eventId: event.event_id || file.replace('.json', ''),
|
|
160
|
+
event,
|
|
161
|
+
});
|
|
162
|
+
unlinkSync(filepath);
|
|
163
|
+
} catch (e) { debug(`consumeAllEvents ${file}: ${e.message}`); }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sort by priority then time
|
|
167
|
+
const priorityOrder = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
168
|
+
results.sort((a, b) => {
|
|
169
|
+
const pa = priorityOrder[a.event.priority] ?? 1;
|
|
170
|
+
const pb = priorityOrder[b.event.priority] ?? 1;
|
|
171
|
+
if (pa !== pb) return pa - pb;
|
|
172
|
+
return (a.event.created_at || '').localeCompare(b.event.created_at || '');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* List all queues with event counts.
|
|
180
|
+
* @param {string} projectRoot
|
|
181
|
+
* @returns {Array<{ agentId: string, count: number, oldest: string|null }>}
|
|
182
|
+
*/
|
|
183
|
+
export function listQueues(projectRoot) {
|
|
184
|
+
const busDir = join(projectRoot, EVENT_BUS_DIR);
|
|
185
|
+
if (!existsSync(busDir)) return [];
|
|
186
|
+
|
|
187
|
+
const dirs = readdirSync(busDir, { withFileTypes: true })
|
|
188
|
+
.filter(d => d.isDirectory());
|
|
189
|
+
|
|
190
|
+
return dirs.map(d => {
|
|
191
|
+
const agentDir = join(busDir, d.name);
|
|
192
|
+
const files = readdirSync(agentDir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
|
|
193
|
+
let oldest = null;
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
try {
|
|
196
|
+
const evt = JSON.parse(readFileSync(join(agentDir, file), 'utf-8'));
|
|
197
|
+
if (!oldest || evt.created_at < oldest) oldest = evt.created_at;
|
|
198
|
+
} catch (e) { debug(`listQueues ${file}: ${e.message}`); }
|
|
199
|
+
}
|
|
200
|
+
return { agentId: d.name, count: files.length, oldest };
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Clear all events in an agent's queue.
|
|
206
|
+
* @param {string} projectRoot
|
|
207
|
+
* @param {string} agentId
|
|
208
|
+
* @returns {number} Number of events cleared
|
|
209
|
+
*/
|
|
210
|
+
export function clearQueue(projectRoot, agentId) {
|
|
211
|
+
const dir = queueDir(projectRoot, agentId);
|
|
212
|
+
if (!existsSync(dir)) return 0;
|
|
213
|
+
|
|
214
|
+
const files = readdirSync(dir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
|
|
215
|
+
let count = 0;
|
|
216
|
+
for (const file of files) {
|
|
217
|
+
try { unlinkSync(join(dir, file)); count++; } catch (e) { debug(`clearQueue: ${e.message}`); }
|
|
218
|
+
}
|
|
219
|
+
return count;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Format events as markdown for stdout injection in hooks.
|
|
224
|
+
* @param {Array<{ eventId: string, event: object }>} events
|
|
225
|
+
* @returns {string}
|
|
226
|
+
*/
|
|
227
|
+
export function formatEventsForInjection(events) {
|
|
228
|
+
if (!events || events.length === 0) return '';
|
|
229
|
+
|
|
230
|
+
const lines = [
|
|
231
|
+
'',
|
|
232
|
+
`# KAIROS EVENT BUS — ${events.length} evenement(s) en attente`,
|
|
233
|
+
'',
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
for (const { event } of events) {
|
|
237
|
+
lines.push(`## [${event.priority || 'MEDIUM'}] ${event.event_type}`);
|
|
238
|
+
lines.push(`De: ${event.source_agent_id || 'inconnu'} — ${event.created_at || ''}`);
|
|
239
|
+
|
|
240
|
+
if (event.payload) {
|
|
241
|
+
if (event.payload.summary) lines.push(`Resume: ${event.payload.summary}`);
|
|
242
|
+
if (event.payload.odmId) lines.push(`OdM: ${event.payload.odmId}`);
|
|
243
|
+
if (event.payload.cr_id) lines.push(`CR: ${event.payload.cr_id}`);
|
|
244
|
+
if (event.payload.message) lines.push(`Message: ${event.payload.message}`);
|
|
245
|
+
}
|
|
246
|
+
lines.push('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
lines.push('---');
|
|
250
|
+
lines.push('Traite ces evenements selon leur priorite.');
|
|
251
|
+
lines.push('');
|
|
252
|
+
|
|
253
|
+
return lines.join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Purge expired events across all queues.
|
|
258
|
+
* @param {string} projectRoot
|
|
259
|
+
* @returns {number} Number of events purged
|
|
260
|
+
*/
|
|
261
|
+
export function purgeExpired(projectRoot) {
|
|
262
|
+
const busDir = join(projectRoot, EVENT_BUS_DIR);
|
|
263
|
+
if (!existsSync(busDir)) return 0;
|
|
264
|
+
|
|
265
|
+
const now = new Date();
|
|
266
|
+
let purged = 0;
|
|
267
|
+
|
|
268
|
+
const dirs = readdirSync(busDir, { withFileTypes: true })
|
|
269
|
+
.filter(d => d.isDirectory());
|
|
270
|
+
|
|
271
|
+
for (const d of dirs) {
|
|
272
|
+
const agentDir = join(busDir, d.name);
|
|
273
|
+
const files = readdirSync(agentDir).filter(f => f.startsWith('EVT-') && f.endsWith('.json'));
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
const filepath = join(agentDir, file);
|
|
276
|
+
try {
|
|
277
|
+
const evt = JSON.parse(readFileSync(filepath, 'utf-8'));
|
|
278
|
+
if (evt.expires_at && new Date(evt.expires_at) < now) {
|
|
279
|
+
unlinkSync(filepath);
|
|
280
|
+
purged++;
|
|
281
|
+
}
|
|
282
|
+
} catch (e) { debug(`purgeExpired ${file}: ${e.message}`); }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return purged;
|
|
287
|
+
}
|