@intent-systems/nexus 2026.1.5-3 → 2026.1.5-5
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/dist/agents/agent-id.js +41 -0
- package/dist/agents/auth-profiles.js +114 -25
- package/dist/agents/identity-state.js +79 -0
- package/dist/agents/model-auth.js +1 -0
- package/dist/agents/model-fallback.js +15 -9
- package/dist/agents/model-selection.js +1 -1
- package/dist/agents/models-config.js +17 -11
- package/dist/agents/pi-embedded-runner.js +101 -9
- package/dist/agents/sandbox.js +12 -3
- package/dist/agents/skill-runner.js +29 -4
- package/dist/agents/skill-usage.js +114 -11
- package/dist/agents/skills-status.js +4 -4
- package/dist/agents/skills.js +18 -7
- package/dist/agents/subagent-registry.js +25 -11
- package/dist/agents/system-prompt.js +16 -0
- package/dist/agents/tool-policy.js +19 -3
- package/dist/agents/tools/browser-tool.js +5 -2
- package/dist/agents/tools/image-tool.js +93 -8
- package/dist/agents/tools/sessions-announce-target.js +5 -1
- package/dist/agents/workspace.js +55 -46
- package/dist/auto-reply/command-detection.js +2 -1
- package/dist/auto-reply/reply/directive-handling.js +153 -28
- package/dist/auto-reply/reply/directives.js +17 -2
- package/dist/auto-reply/reply/model-selection.js +8 -3
- package/dist/auto-reply/reply/queue.js +2 -2
- package/dist/auto-reply/reply.js +1 -1
- package/dist/auto-reply/thinking.js +15 -0
- package/dist/browser/chrome.js +1 -1
- package/dist/browser/client.js +2 -0
- package/dist/browser/config.js +6 -2
- package/dist/browser/pw-tools-core.js +3 -0
- package/dist/browser/routes/agent.js +14 -0
- package/dist/canvas-host/server.js +1 -1
- package/dist/capabilities/detector.js +245 -0
- package/dist/capabilities/registry.js +99 -0
- package/dist/channels/location.js +44 -0
- package/dist/channels/web/index.js +2 -0
- package/dist/cli/cloud-cli.js +12 -7
- package/dist/cli/credential-cli.js +139 -17
- package/dist/cli/gateway-cli.js +1 -1
- package/dist/cli/log-cli.js +25 -0
- package/dist/cli/pairing-cli.js +1 -1
- package/dist/cli/program.js +58 -6
- package/dist/cli/run-main.js +1 -1
- package/dist/cli/skills-cli.js +144 -21
- package/dist/cli/skills-hub-cli.js +59 -29
- package/dist/cli/tool-connector-cli.js +99 -24
- package/dist/cli/upstream-sync-cli.js +253 -96
- package/dist/cli/usage-cli.js +14 -0
- package/dist/commands/auth-choice-options.js +6 -1
- package/dist/commands/auth-choice.js +157 -5
- package/dist/commands/bootstrap-preset.js +10 -6
- package/dist/commands/capabilities.js +33 -6
- package/dist/commands/claude-md.js +3 -2
- package/dist/commands/config-view.js +1 -1
- package/dist/commands/configure.js +4 -4
- package/dist/commands/credential.js +497 -36
- package/dist/commands/cursor-rules.js +39 -19
- package/dist/commands/doctor.js +5 -4
- package/dist/commands/identity.js +28 -31
- package/dist/commands/init.js +15 -18
- package/dist/commands/log.js +134 -0
- package/dist/commands/models/fallbacks.js +1 -1
- package/dist/commands/models/image-fallbacks.js +1 -1
- package/dist/commands/models/list.js +1 -1
- package/dist/commands/models/scan.js +1 -1
- package/dist/commands/onboard-auth.js +27 -2
- package/dist/commands/onboard-eve-identity.js +7 -8
- package/dist/commands/onboard-non-interactive.js +4 -2
- package/dist/commands/onboard-quickstart.js +18 -11
- package/dist/commands/quest-state.js +271 -0
- package/dist/commands/quest.js +53 -13
- package/dist/commands/reset.js +1 -1
- package/dist/commands/sessions-ingest.js +5 -4
- package/dist/commands/setup.js +4 -2
- package/dist/commands/skills-manifest.js +2 -2
- package/dist/commands/status.js +179 -61
- package/dist/commands/suggestions.js +1 -1
- package/dist/commands/usage-tracking.js +32 -0
- package/dist/commands/usage-upload.js +6 -1
- package/dist/config/defaults.js +1 -3
- package/dist/config/includes.js +5 -7
- package/dist/config/io.js +88 -16
- package/dist/config/legacy.js +4 -2
- package/dist/config/paths.js +16 -0
- package/dist/config/sessions.js +9 -5
- package/dist/config/zod-schema.js +4 -3
- package/dist/control-plane/broker/broker.js +1022 -0
- package/dist/control-plane/compaction.js +282 -0
- package/dist/control-plane/factory.js +31 -0
- package/dist/control-plane/index.js +10 -0
- package/dist/control-plane/odu/agents.js +192 -0
- package/dist/control-plane/odu/interaction-tools.js +208 -0
- package/dist/control-plane/odu/prompt-loader.js +95 -0
- package/dist/control-plane/odu/runtime.js +479 -0
- package/dist/control-plane/odu/types.js +6 -0
- package/dist/control-plane/odu-control-plane.js +316 -0
- package/dist/control-plane/single-agent.js +249 -0
- package/dist/control-plane/types.js +11 -0
- package/dist/credentials/store.js +449 -0
- package/dist/gateway/server-browser.js +5 -4
- package/dist/gateway/server-methods/cron.js +11 -1
- package/dist/gateway/server.js +14 -7
- package/dist/infra/bonjour.js +1 -1
- package/dist/infra/event-log.js +8 -2
- package/dist/infra/path-env.js +1 -2
- package/dist/infra/provider-usage.auth.js +5 -3
- package/dist/infra/provider-usage.fetch.claude.js +16 -6
- package/dist/infra/provider-usage.fetch.minimax.js +8 -3
- package/dist/infra/provider-usage.js +9 -5
- package/dist/infra/restart.js +2 -2
- package/dist/infra/usage-settings.js +78 -0
- package/dist/infra/usage-suggestions.js +17 -5
- package/dist/infra/usage-upload.js +38 -1
- package/dist/infra/voicewake.js +2 -2
- package/dist/logging/redact.js +109 -0
- package/dist/markdown/fences.js +58 -0
- package/dist/media/image-ops.js +3 -1
- package/dist/memory/embeddings.js +146 -0
- package/dist/memory/index.js +3 -0
- package/dist/memory/internal.js +163 -0
- package/dist/pairing/pairing-store.js +218 -0
- package/dist/plugins/cli.js +42 -0
- package/dist/plugins/discovery.js +253 -0
- package/dist/plugins/install.js +181 -0
- package/dist/plugins/loader.js +290 -0
- package/dist/plugins/registry.js +105 -0
- package/dist/plugins/status.js +29 -0
- package/dist/plugins/tools.js +39 -0
- package/dist/plugins/types.js +1 -0
- package/dist/providers/github-copilot-auth.js +1 -1
- package/dist/routing/resolve-route.js +144 -0
- package/dist/routing/session-key.js +65 -0
- package/dist/sessions/send-policy.js +5 -5
- package/dist/slack/monitor.js +22 -1
- package/dist/telegram/reaction-level.js +2 -1
- package/dist/utils/provider-utils.js +28 -0
- package/dist/utils.js +4 -3
- package/dist/wizard/onboarding.js +29 -7
- package/package.json +4 -29
- package/patches/@mariozechner__pi-ai.patch +215 -0
- package/patches/playwright-core@1.57.0.patch +13 -0
- package/patches/qrcode-terminal.patch +12 -0
- package/scripts/postinstall.js +202 -0
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Active Message Broker - Agent Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* The broker is responsible for:
|
|
5
|
+
* - Receiving messages for agents
|
|
6
|
+
* - Starting agents when they have work
|
|
7
|
+
* - Restarting agents when new work arrives
|
|
8
|
+
* - Interrupting agents if needed
|
|
9
|
+
* - Managing agent lifecycle
|
|
10
|
+
*
|
|
11
|
+
* Agents are pure workers - they don't check queues or manage themselves.
|
|
12
|
+
*
|
|
13
|
+
* Ported from magic-toolbox and adapted for Nexus:
|
|
14
|
+
* - Uses Nexus session storage (sessions.json + transcript JSONL) instead of SQLite
|
|
15
|
+
* - Uses Nexus logging system (createSubsystemLogger)
|
|
16
|
+
* - Removed instance manager (sharding not yet supported)
|
|
17
|
+
* - Simplified for single ODU initially
|
|
18
|
+
*/
|
|
19
|
+
import crypto from "node:crypto";
|
|
20
|
+
import { loadSession, writeSessionMetadata, } from "../../config/sessions.js";
|
|
21
|
+
import { createSubsystemLogger } from "../../logging.js";
|
|
22
|
+
import { DEFAULT_AGENT_ID, normalizeAgentId, } from "../../routing/session-key.js";
|
|
23
|
+
/**
|
|
24
|
+
* Active Message Broker - manages agent lifecycle
|
|
25
|
+
*/
|
|
26
|
+
export class ActiveMessageBroker {
|
|
27
|
+
// Message queues per agent
|
|
28
|
+
queues = new Map();
|
|
29
|
+
// Currently running agents
|
|
30
|
+
runningAgents = new Map();
|
|
31
|
+
// Agents currently being started (to prevent race conditions)
|
|
32
|
+
startingAgents = new Set();
|
|
33
|
+
// Session status tracking (in-memory only, resets on server restart)
|
|
34
|
+
agentStatus = new Map();
|
|
35
|
+
// Registered IAs (always-on singletons)
|
|
36
|
+
registeredIAs = new Map();
|
|
37
|
+
// Track external callers per agent (who has sent messages to this agent)
|
|
38
|
+
// Format: agentId -> Set<senderAgentId>
|
|
39
|
+
externalCallers = new Map();
|
|
40
|
+
// Delivery mode per agent (programmatic override)
|
|
41
|
+
deliveryModes = new Map();
|
|
42
|
+
// Agent factories per ODU (registered by each ODU)
|
|
43
|
+
agentFactories = new Map();
|
|
44
|
+
// Session store paths per ODU
|
|
45
|
+
sessionStorePaths = new Map();
|
|
46
|
+
// Completion callbacks for async waiting
|
|
47
|
+
completionCallbacks = new Map();
|
|
48
|
+
// Collection support for 'collect' mode
|
|
49
|
+
collectionTimers = new Map();
|
|
50
|
+
collectionBuffers = new Map();
|
|
51
|
+
collectDebounceMs = 500; // Default debounce delay
|
|
52
|
+
collectMaxMessages = 10; // Default max messages before auto-flush
|
|
53
|
+
// Logger
|
|
54
|
+
logger;
|
|
55
|
+
config;
|
|
56
|
+
constructor(config) {
|
|
57
|
+
this.logger = createSubsystemLogger("broker");
|
|
58
|
+
this.config = config;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Register an ODU with the broker
|
|
62
|
+
* Each ODU registers its agent factory and session store path
|
|
63
|
+
*/
|
|
64
|
+
registerODU(oduName, sessionStorePath, agentFactory) {
|
|
65
|
+
this.sessionStorePaths.set(oduName, sessionStorePath);
|
|
66
|
+
this.agentFactories.set(oduName, agentFactory);
|
|
67
|
+
this.logger.info(`ODU registered: ${oduName}`, {
|
|
68
|
+
oduName,
|
|
69
|
+
sessionStorePath,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Register an IA with the broker
|
|
74
|
+
* IAs are singleton, always-on agents that can receive messages
|
|
75
|
+
*/
|
|
76
|
+
registerIA(iaId, instance) {
|
|
77
|
+
this.registeredIAs.set(iaId, instance);
|
|
78
|
+
this.logger.info(`IA registered: ${iaId}`, { iaId });
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Set delivery mode for an agent (programmatic only, for tests)
|
|
82
|
+
*/
|
|
83
|
+
setDeliveryMode(agentId, mode) {
|
|
84
|
+
this.deliveryModes.set(agentId, mode);
|
|
85
|
+
this.logger.debug(`Delivery mode set for ${agentId}: ${mode}`, {
|
|
86
|
+
agentId,
|
|
87
|
+
mode,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Set collection parameters (for 'collect' mode)
|
|
92
|
+
*/
|
|
93
|
+
setCollectionParams(debounceMs, maxMessages) {
|
|
94
|
+
if (debounceMs !== undefined)
|
|
95
|
+
this.collectDebounceMs = debounceMs;
|
|
96
|
+
if (maxMessages !== undefined)
|
|
97
|
+
this.collectMaxMessages = maxMessages;
|
|
98
|
+
this.logger.debug("Collection params updated", {
|
|
99
|
+
debounceMs: this.collectDebounceMs,
|
|
100
|
+
maxMessages: this.collectMaxMessages,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Handle collect mode: buffer messages and debounce delivery
|
|
105
|
+
*/
|
|
106
|
+
handleCollectMode(message) {
|
|
107
|
+
const agentId = message.to;
|
|
108
|
+
// Add to collection buffer
|
|
109
|
+
if (!this.collectionBuffers.has(agentId)) {
|
|
110
|
+
this.collectionBuffers.set(agentId, []);
|
|
111
|
+
}
|
|
112
|
+
this.collectionBuffers.get(agentId)?.push(message);
|
|
113
|
+
// Clear existing timer
|
|
114
|
+
const existingTimer = this.collectionTimers.get(agentId);
|
|
115
|
+
if (existingTimer) {
|
|
116
|
+
clearTimeout(existingTimer);
|
|
117
|
+
}
|
|
118
|
+
// Check if we've hit max messages
|
|
119
|
+
const buffer = this.collectionBuffers.get(agentId);
|
|
120
|
+
if (!buffer)
|
|
121
|
+
return;
|
|
122
|
+
if (buffer.length >= this.collectMaxMessages) {
|
|
123
|
+
// Flush immediately
|
|
124
|
+
this.flushCollectionBuffer(agentId);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Set new debounce timer
|
|
128
|
+
const timer = setTimeout(() => {
|
|
129
|
+
this.flushCollectionBuffer(agentId);
|
|
130
|
+
}, this.collectDebounceMs);
|
|
131
|
+
this.collectionTimers.set(agentId, timer);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Flush collection buffer to queue
|
|
135
|
+
*/
|
|
136
|
+
flushCollectionBuffer(agentId) {
|
|
137
|
+
const buffer = this.collectionBuffers.get(agentId);
|
|
138
|
+
if (!buffer || buffer.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
this.logger.debug(`Flushing collection buffer for ${agentId}`, {
|
|
141
|
+
agentId,
|
|
142
|
+
messageCount: buffer.length,
|
|
143
|
+
});
|
|
144
|
+
// Clear timer and buffer
|
|
145
|
+
const timer = this.collectionTimers.get(agentId);
|
|
146
|
+
if (timer) {
|
|
147
|
+
clearTimeout(timer);
|
|
148
|
+
this.collectionTimers.delete(agentId);
|
|
149
|
+
}
|
|
150
|
+
this.collectionBuffers.delete(agentId);
|
|
151
|
+
// Enqueue all buffered messages
|
|
152
|
+
for (const message of buffer) {
|
|
153
|
+
this.enqueue(message);
|
|
154
|
+
}
|
|
155
|
+
// Trigger agent execution if not running
|
|
156
|
+
if (!this.runningAgents.has(agentId) && !this.registeredIAs.has(agentId)) {
|
|
157
|
+
const queue = this.queues.get(agentId) || [];
|
|
158
|
+
if (queue.length > 0) {
|
|
159
|
+
void this.startAgentWithBatch(agentId, queue);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Send message and wait for synchronous acknowledgment from IA
|
|
165
|
+
* Used for cross-ODU calls where caller needs immediate confirmation
|
|
166
|
+
*
|
|
167
|
+
* @param message - The message to send
|
|
168
|
+
* @returns Promise that resolves with acknowledgment string
|
|
169
|
+
*/
|
|
170
|
+
async sendAndWaitForAck(message) {
|
|
171
|
+
this.logger.debug(`Sending with ack wait: ${message.from} → ${message.to}`, { message });
|
|
172
|
+
// Route message (resolve short names to full IDs)
|
|
173
|
+
const resolvedTo = this.routeMessage(message.from, message.to);
|
|
174
|
+
message.to = resolvedTo;
|
|
175
|
+
// Validate target is an IA
|
|
176
|
+
const ia = this.registeredIAs.get(message.to);
|
|
177
|
+
if (!ia) {
|
|
178
|
+
throw new Error(`Cannot wait for ack: ${message.to} is not a registered IA`);
|
|
179
|
+
}
|
|
180
|
+
// Track external caller if needed
|
|
181
|
+
if (message.from !== message.to &&
|
|
182
|
+
message.from !== "user" &&
|
|
183
|
+
message.from !== "system") {
|
|
184
|
+
if (!this.externalCallers.has(message.to)) {
|
|
185
|
+
this.externalCallers.set(message.to, new Set());
|
|
186
|
+
}
|
|
187
|
+
this.externalCallers.get(message.to)?.add(message.from);
|
|
188
|
+
}
|
|
189
|
+
// Add to queue for tracking
|
|
190
|
+
this.enqueue(message);
|
|
191
|
+
// Queue message at IA
|
|
192
|
+
if (ia.queueMessage) {
|
|
193
|
+
ia.queueMessage(message.content, message.priority || "normal", message.from);
|
|
194
|
+
}
|
|
195
|
+
this.logger.debug(`Waiting for ack from ${message.to}...`, {
|
|
196
|
+
to: message.to,
|
|
197
|
+
});
|
|
198
|
+
// Kick off processing and wait for acknowledgment
|
|
199
|
+
let ack = "";
|
|
200
|
+
if (ia.processQueue) {
|
|
201
|
+
ack = await ia.processQueue();
|
|
202
|
+
}
|
|
203
|
+
this.logger.debug(`Received ack from ${message.to}`, {
|
|
204
|
+
to: message.to,
|
|
205
|
+
ackLength: ack.length,
|
|
206
|
+
});
|
|
207
|
+
return ack;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Send a message to an agent
|
|
211
|
+
*
|
|
212
|
+
* The broker decides everything:
|
|
213
|
+
* - Should we interrupt?
|
|
214
|
+
* - Should we batch?
|
|
215
|
+
* - Should we start the agent?
|
|
216
|
+
*/
|
|
217
|
+
async send(message) {
|
|
218
|
+
this.logger.debug(`Message from ${message.from} to ${message.to}`, {
|
|
219
|
+
from: message.from,
|
|
220
|
+
to: message.to,
|
|
221
|
+
priority: message.priority,
|
|
222
|
+
contentPreview: message.content.substring(0, 100),
|
|
223
|
+
});
|
|
224
|
+
// Route message (resolve short names to full IDs)
|
|
225
|
+
const resolvedTo = this.routeMessage(message.from, message.to);
|
|
226
|
+
message.to = resolvedTo;
|
|
227
|
+
this.logger.debug(`Routed to ${resolvedTo}`, { resolvedTo });
|
|
228
|
+
// 1. Track external caller (if not self-message)
|
|
229
|
+
if (message.from !== message.to &&
|
|
230
|
+
message.from !== "user" &&
|
|
231
|
+
message.from !== "system") {
|
|
232
|
+
if (!this.externalCallers.has(message.to)) {
|
|
233
|
+
this.externalCallers.set(message.to, new Set());
|
|
234
|
+
}
|
|
235
|
+
this.externalCallers.get(message.to)?.add(message.from);
|
|
236
|
+
}
|
|
237
|
+
// 2. Handle collect mode (buffer and debounce)
|
|
238
|
+
if (message.deliveryMode === "collect") {
|
|
239
|
+
this.handleCollectMode(message);
|
|
240
|
+
return; // Don't enqueue immediately
|
|
241
|
+
}
|
|
242
|
+
// 3. Handle steer mode (interrupt like urgent)
|
|
243
|
+
if (message.deliveryMode === "steer") {
|
|
244
|
+
// Steer mode interrupts current work
|
|
245
|
+
message.priority = "urgent";
|
|
246
|
+
message.deliveryMode = "interrupt";
|
|
247
|
+
}
|
|
248
|
+
// 4. Handle followup mode (queue without interrupting)
|
|
249
|
+
if (message.deliveryMode === "followup") {
|
|
250
|
+
// Followup doesn't interrupt - just queues normally
|
|
251
|
+
// No special handling needed, just enqueue
|
|
252
|
+
}
|
|
253
|
+
// 5. Add to queue
|
|
254
|
+
this.enqueue(message);
|
|
255
|
+
// 3. Decide what to do
|
|
256
|
+
// Check if target is a registered IA
|
|
257
|
+
const ia = this.registeredIAs.get(message.to);
|
|
258
|
+
if (ia) {
|
|
259
|
+
// IA is always running - queue message and trigger processing
|
|
260
|
+
this.logger.debug(`Delivering to IA: ${message.to}`, {
|
|
261
|
+
to: message.to,
|
|
262
|
+
contentPreview: message.content.substring(0, 100),
|
|
263
|
+
});
|
|
264
|
+
// Queue the message with sender information
|
|
265
|
+
if (ia.queueMessage) {
|
|
266
|
+
ia.queueMessage(message.content, message.priority || "normal", message.from);
|
|
267
|
+
}
|
|
268
|
+
// Trigger processing by calling processQueue() if it exists, otherwise use chatSync
|
|
269
|
+
if (ia.processQueue) {
|
|
270
|
+
this.logger.debug(`Calling processQueue() for ${message.to}`, {
|
|
271
|
+
to: message.to,
|
|
272
|
+
});
|
|
273
|
+
ia.processQueue().catch((error) => {
|
|
274
|
+
this.logger.error(`IA ${message.to} processQueue error: ${error.message}`, { to: message.to, error: error.message });
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
else if (ia.chatSync) {
|
|
278
|
+
// Fallback: Call chatSync with empty string
|
|
279
|
+
this.logger.debug(`Calling chatSync('') for ${message.to} (fallback)`, {
|
|
280
|
+
to: message.to,
|
|
281
|
+
});
|
|
282
|
+
setImmediate(() => {
|
|
283
|
+
ia.chatSync?.("").catch((error) => {
|
|
284
|
+
this.logger.error(`IA ${message.to} chatSync error: ${error.message}`, { to: message.to, error: error.message });
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
// Target is an EA - handle normally
|
|
291
|
+
const shouldInterrupt = this.shouldInterrupt(message);
|
|
292
|
+
const running = this.runningAgents.get(message.to);
|
|
293
|
+
if (shouldInterrupt && running) {
|
|
294
|
+
// Interrupt and restart immediately
|
|
295
|
+
this.logger.info(`Interrupting ${message.to} for urgent message`, {
|
|
296
|
+
agentId: message.to,
|
|
297
|
+
});
|
|
298
|
+
await this.interruptAndRestart(message.to);
|
|
299
|
+
}
|
|
300
|
+
else if (!running && !this.startingAgents.has(message.to)) {
|
|
301
|
+
// Agent not running and not being started - mark as starting and process
|
|
302
|
+
this.startingAgents.add(message.to);
|
|
303
|
+
this.logger.info(`Processing batch for ${message.to}`, {
|
|
304
|
+
agentId: message.to,
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
await this.processNextBatch(message.to);
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
this.startingAgents.delete(message.to);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
// Agent running or being started - let it finish
|
|
315
|
+
this.logger.debug(`Message queued for ${message.to}, will process after current session`, { agentId: message.to });
|
|
316
|
+
// When it completes, broker will process next batch
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Route message to correct agent
|
|
321
|
+
* Resolves short names to fully-qualified agent IDs
|
|
322
|
+
*
|
|
323
|
+
* Rules:
|
|
324
|
+
* 1. Fully-qualified name (e.g., "toolbox-ea-worktrees") → Route directly
|
|
325
|
+
* 2. Short name (e.g., "worktrees") → Expand to caller's ODU EA
|
|
326
|
+
*/
|
|
327
|
+
routeMessage(from, to) {
|
|
328
|
+
// Rule 1: If fully-qualified, route directly
|
|
329
|
+
if (this.isFullyQualified(to)) {
|
|
330
|
+
// Validate agent exists or can be created
|
|
331
|
+
if (!this.validateAgentExists(to)) {
|
|
332
|
+
throw new Error(`Unknown agent: ${to}. Agent does not exist in session store or running agents.`);
|
|
333
|
+
}
|
|
334
|
+
return to;
|
|
335
|
+
}
|
|
336
|
+
// Rule 2: Short name - expand to caller's ODU EA
|
|
337
|
+
// Special case: 'user' and 'system' default to primary ODU
|
|
338
|
+
let callerODU;
|
|
339
|
+
if (from === "user" || from === "system") {
|
|
340
|
+
// Get first registered ODU (primary)
|
|
341
|
+
const odus = Array.from(this.agentFactories.keys());
|
|
342
|
+
callerODU = odus[0] || "nexus";
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
callerODU = this.getODUName(from);
|
|
346
|
+
}
|
|
347
|
+
const expandedId = this.expandAgentName(callerODU, to);
|
|
348
|
+
this.logger.debug(`Expanded "${to}" to "${expandedId}" for caller ${from}`, { from, to, expandedId });
|
|
349
|
+
return expandedId;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Check if agent name is fully-qualified
|
|
353
|
+
* Fully-qualified format: {oduName}-{ia|ea}-{identifier} OR {oduName}-ia
|
|
354
|
+
* Examples: "toolbox-ea-worktrees", "meta-ia"
|
|
355
|
+
*/
|
|
356
|
+
isFullyQualified(name) {
|
|
357
|
+
const parts = name.split("-");
|
|
358
|
+
// Must have at least 2 parts (oduName-ia) or 3+ parts (oduName-ea-identifier)
|
|
359
|
+
if (parts.length < 2) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
// Second part must be 'ia' or 'ea'
|
|
363
|
+
const agentType = parts[1];
|
|
364
|
+
return agentType === "ia" || agentType === "ea";
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Expand short agent name to fully-qualified ID
|
|
368
|
+
* Short name "worktrees" → "toolbox-ea-worktrees"
|
|
369
|
+
*/
|
|
370
|
+
expandAgentName(callerODU, shortName) {
|
|
371
|
+
return `${callerODU}-ea-${shortName}`;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Validate that agent exists or can be created
|
|
375
|
+
* Checks: registered IAs, running agents, or if ODU is registered
|
|
376
|
+
*/
|
|
377
|
+
validateAgentExists(agentId) {
|
|
378
|
+
// Check if it's a registered IA
|
|
379
|
+
if (this.registeredIAs.has(agentId)) {
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
// Check if agent is currently running
|
|
383
|
+
if (this.runningAgents.has(agentId)) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
// Check if ODU is registered (can create agent)
|
|
387
|
+
try {
|
|
388
|
+
const oduName = this.getODUName(agentId);
|
|
389
|
+
const storePath = this.sessionStorePaths.get(oduName);
|
|
390
|
+
if (!storePath) {
|
|
391
|
+
// ODU not registered - can't create agent
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
// ODU is registered, so we can create the agent if needed
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
this.logger.error(`Error validating agent ${agentId}`, {
|
|
399
|
+
agentId,
|
|
400
|
+
error: error instanceof Error ? error.message : String(error),
|
|
401
|
+
});
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Check if agent has pending messages
|
|
407
|
+
*/
|
|
408
|
+
hasPending(agentId) {
|
|
409
|
+
const queue = this.queues.get(agentId);
|
|
410
|
+
return queue ? queue.length > 0 : false;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Get current session status for an agent
|
|
414
|
+
* Returns 'idle' if agent never started or completed
|
|
415
|
+
*/
|
|
416
|
+
getAgentStatus(agentId) {
|
|
417
|
+
return this.agentStatus.get(agentId) || "idle";
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Set session status for an agent
|
|
421
|
+
* Called internally during agent lifecycle
|
|
422
|
+
*/
|
|
423
|
+
setAgentStatus(agentId, status) {
|
|
424
|
+
const oldStatus = this.agentStatus.get(agentId);
|
|
425
|
+
this.agentStatus.set(agentId, status);
|
|
426
|
+
this.logger.debug(`Agent ${agentId} status: ${status}`, {
|
|
427
|
+
agentId,
|
|
428
|
+
status,
|
|
429
|
+
});
|
|
430
|
+
// Emit status change event
|
|
431
|
+
if (oldStatus !== status) {
|
|
432
|
+
this.emit("agent_status_changed", {
|
|
433
|
+
agentId,
|
|
434
|
+
oldStatus,
|
|
435
|
+
newStatus: status,
|
|
436
|
+
timestamp: Date.now(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Check if agent is currently active (processing messages)
|
|
442
|
+
*/
|
|
443
|
+
isAgentActive(agentId) {
|
|
444
|
+
return this.getAgentStatus(agentId) === "active";
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Get list of external agents that have sent messages to this agent
|
|
448
|
+
* Returns fully-qualified agent IDs
|
|
449
|
+
*/
|
|
450
|
+
getExternalCallers(agentId) {
|
|
451
|
+
const callers = this.externalCallers.get(agentId);
|
|
452
|
+
return callers ? Array.from(callers) : [];
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Get queue size for agent (for debugging/monitoring)
|
|
456
|
+
*/
|
|
457
|
+
getQueueSize(agentId) {
|
|
458
|
+
return this.queues.get(agentId)?.length || 0;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Wait for specific agent to complete
|
|
462
|
+
* Returns Promise that resolves when agent finishes (success or error)
|
|
463
|
+
*
|
|
464
|
+
* Usage:
|
|
465
|
+
* const result = await broker.onceAgentCompletes('toolbox-ea-worktrees');
|
|
466
|
+
* if (result.success) { ... }
|
|
467
|
+
*/
|
|
468
|
+
onceAgentCompletes(agentId) {
|
|
469
|
+
return new Promise((resolve) => {
|
|
470
|
+
if (!this.completionCallbacks.has(agentId)) {
|
|
471
|
+
this.completionCallbacks.set(agentId, []);
|
|
472
|
+
}
|
|
473
|
+
this.completionCallbacks.get(agentId)?.push(resolve);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Decide if we should interrupt based on context-aware rules
|
|
478
|
+
*/
|
|
479
|
+
shouldInterrupt(message) {
|
|
480
|
+
// Rule 1: External → IA always interrupts
|
|
481
|
+
if (message.to.endsWith("-ia") && message.metadata?.source === "user") {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
// Rule 2: EA → parent IA never interrupts
|
|
485
|
+
if (message.to.endsWith("-ia") && message.metadata?.source === "ea") {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
// Rule 3: Explicit interrupt mode
|
|
489
|
+
if (message.deliveryMode === "interrupt") {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
// Rule 4: Priority-based
|
|
493
|
+
if (message.priority === "urgent") {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
if (message.priority === "high") {
|
|
497
|
+
const running = this.runningAgents.get(message.to);
|
|
498
|
+
if (running && Date.now() - running.startedAt > 30000) {
|
|
499
|
+
return true; // Running > 30s, interrupt
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return false; // Default: don't interrupt
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Process next batch of messages for an agent
|
|
506
|
+
* Batches consecutive messages from the same sender
|
|
507
|
+
*
|
|
508
|
+
* NOTE: Caller should add agent to startingAgents before calling this
|
|
509
|
+
*/
|
|
510
|
+
async processNextBatch(agentId) {
|
|
511
|
+
const queue = this.queues.get(agentId);
|
|
512
|
+
if (!queue || queue.length === 0) {
|
|
513
|
+
this.logger.debug(`No messages for ${agentId}`, { agentId });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
// Get consecutive messages from same sender
|
|
517
|
+
const firstSender = queue[0].from;
|
|
518
|
+
const batch = [];
|
|
519
|
+
// Take all consecutive messages from the same sender
|
|
520
|
+
while (queue.length > 0 && queue[0].from === firstSender) {
|
|
521
|
+
const nextMessage = queue.shift();
|
|
522
|
+
if (!nextMessage)
|
|
523
|
+
break;
|
|
524
|
+
batch.push(nextMessage);
|
|
525
|
+
}
|
|
526
|
+
this.logger.info(`Batched ${batch.length} messages from ${firstSender} for ${agentId}`, {
|
|
527
|
+
agentId,
|
|
528
|
+
batchSize: batch.length,
|
|
529
|
+
sender: firstSender,
|
|
530
|
+
});
|
|
531
|
+
// Start agent with this batch
|
|
532
|
+
await this.startAgentWithBatch(agentId, batch);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Start an agent with a specific batch of messages
|
|
536
|
+
*
|
|
537
|
+
* Steps:
|
|
538
|
+
* 1. Parse agent ID to find ODU
|
|
539
|
+
* 2. Load session history from store
|
|
540
|
+
* 3. Format batch messages
|
|
541
|
+
* 4. Create agent via factory
|
|
542
|
+
* 5. Start agent.execute()
|
|
543
|
+
* 6. Monitor completion
|
|
544
|
+
*/
|
|
545
|
+
async startAgentWithBatch(agentId, batch) {
|
|
546
|
+
try {
|
|
547
|
+
// 1. Parse agent ID to find ODU
|
|
548
|
+
const oduName = this.getODUName(agentId);
|
|
549
|
+
const factory = this.agentFactories.get(oduName);
|
|
550
|
+
const storePath = this.sessionStorePaths.get(oduName);
|
|
551
|
+
if (!factory || !storePath) {
|
|
552
|
+
throw new Error(`ODU not registered: ${oduName} (for agent ${agentId})`);
|
|
553
|
+
}
|
|
554
|
+
// 2. Register EA (creates if new, updates if exists)
|
|
555
|
+
const displayName = this.getDisplayName(agentId);
|
|
556
|
+
await this.registerEA(storePath, agentId, displayName);
|
|
557
|
+
// 3. Load session history from store
|
|
558
|
+
const session = await this.loadSessionFromStore(storePath, agentId);
|
|
559
|
+
const history = session?.history || [];
|
|
560
|
+
this.logger.debug(`Loaded session for ${agentId}`, {
|
|
561
|
+
agentId,
|
|
562
|
+
historyLength: history.length,
|
|
563
|
+
displayName,
|
|
564
|
+
});
|
|
565
|
+
// 4. Format batch messages
|
|
566
|
+
let taskDescription;
|
|
567
|
+
const deliveryMode = this.deliveryModes.get(agentId) || "batch";
|
|
568
|
+
if (deliveryMode === "single" || batch.length === 1) {
|
|
569
|
+
// Single message
|
|
570
|
+
taskDescription = batch[0].content;
|
|
571
|
+
this.logger.debug(`Processing single message from ${this.getDisplayName(batch[0].from)}`, {
|
|
572
|
+
agentId,
|
|
573
|
+
from: batch[0].from,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
// Multiple messages from same sender - batch them
|
|
578
|
+
if (batch.length === 1) {
|
|
579
|
+
taskDescription = batch[0].content;
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
taskDescription = batch
|
|
583
|
+
.map((m, i) => `Message ${i + 1}:\n${m.content}`)
|
|
584
|
+
.join("\n\n---\n\n");
|
|
585
|
+
}
|
|
586
|
+
this.logger.debug(`Batched ${batch.length} messages into prompt`, {
|
|
587
|
+
agentId,
|
|
588
|
+
batchSize: batch.length,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
// 5. Create agent via factory
|
|
592
|
+
const agent = factory(agentId, taskDescription, history);
|
|
593
|
+
// 6. Start and track
|
|
594
|
+
const promise = agent.execute();
|
|
595
|
+
// Mark agent as active
|
|
596
|
+
this.setAgentStatus(agentId, "active");
|
|
597
|
+
// Emit agent started event
|
|
598
|
+
this.emit("agent_started", {
|
|
599
|
+
agentId,
|
|
600
|
+
oduName: this.getODUName(agentId),
|
|
601
|
+
timestamp: Date.now(),
|
|
602
|
+
queueSize: batch.length,
|
|
603
|
+
});
|
|
604
|
+
this.runningAgents.set(agentId, {
|
|
605
|
+
agentId,
|
|
606
|
+
instance: agent,
|
|
607
|
+
promise,
|
|
608
|
+
startedAt: Date.now(),
|
|
609
|
+
status: "active",
|
|
610
|
+
});
|
|
611
|
+
this.logger.info(`Agent ${agentId} started (status: active)`, {
|
|
612
|
+
agentId,
|
|
613
|
+
historyLength: history.length,
|
|
614
|
+
queuedMessages: batch.length,
|
|
615
|
+
sender: batch[0].from,
|
|
616
|
+
});
|
|
617
|
+
// 7. Monitor completion
|
|
618
|
+
promise
|
|
619
|
+
.then(() => this.onAgentComplete(agentId))
|
|
620
|
+
.catch((error) => this.onAgentError(agentId, error));
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
this.logger.error(`Failed to start agent ${agentId}`, {
|
|
624
|
+
agentId,
|
|
625
|
+
error: error instanceof Error ? error.message : String(error),
|
|
626
|
+
});
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Get display name from fully-qualified agent ID
|
|
632
|
+
* toolbox-ea-worktrees → worktrees
|
|
633
|
+
* meta-ia → meta-ia (keep IAs fully-qualified)
|
|
634
|
+
*/
|
|
635
|
+
getDisplayName(agentId) {
|
|
636
|
+
const parts = agentId.split("-");
|
|
637
|
+
if (parts.length >= 3 && parts[1] === "ea") {
|
|
638
|
+
// EA: return task name part
|
|
639
|
+
return parts.slice(2).join("-");
|
|
640
|
+
}
|
|
641
|
+
// IA or other: return full name
|
|
642
|
+
return agentId;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* When agent completes, check for more work
|
|
646
|
+
*/
|
|
647
|
+
onAgentComplete(agentId) {
|
|
648
|
+
this.logger.info(`Agent ${agentId} completed`, { agentId });
|
|
649
|
+
// Mark agent as idle
|
|
650
|
+
this.setAgentStatus(agentId, "idle");
|
|
651
|
+
// Remove from running agents
|
|
652
|
+
this.runningAgents.delete(agentId);
|
|
653
|
+
// Emit agent completed event
|
|
654
|
+
this.emit("agent_completed", {
|
|
655
|
+
agentId,
|
|
656
|
+
oduName: this.getODUName(agentId),
|
|
657
|
+
timestamp: Date.now(),
|
|
658
|
+
success: true,
|
|
659
|
+
});
|
|
660
|
+
// Notify completion listeners
|
|
661
|
+
this.notifyCompletion(agentId, { success: true });
|
|
662
|
+
// Are there more messages queued?
|
|
663
|
+
if (this.hasPending(agentId) && !this.startingAgents.has(agentId)) {
|
|
664
|
+
const queueSize = this.getQueueSize(agentId);
|
|
665
|
+
this.logger.info(`Processing next batch for ${agentId} (${queueSize} messages queued)`, {
|
|
666
|
+
agentId,
|
|
667
|
+
queueSize,
|
|
668
|
+
});
|
|
669
|
+
// Mark as starting and process next batch
|
|
670
|
+
this.startingAgents.add(agentId);
|
|
671
|
+
this.processNextBatch(agentId)
|
|
672
|
+
.then(() => this.startingAgents.delete(agentId))
|
|
673
|
+
.catch((error) => {
|
|
674
|
+
this.logger.error(`Error processing next batch for ${agentId}`, {
|
|
675
|
+
agentId,
|
|
676
|
+
error: error instanceof Error ? error.message : String(error),
|
|
677
|
+
});
|
|
678
|
+
this.startingAgents.delete(agentId);
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
this.logger.debug(`Agent ${agentId} idle (no more messages)`, {
|
|
683
|
+
agentId,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Handle agent errors
|
|
689
|
+
*/
|
|
690
|
+
onAgentError(agentId, error) {
|
|
691
|
+
this.logger.error(`Agent ${agentId} failed`, {
|
|
692
|
+
agentId,
|
|
693
|
+
error: error.message,
|
|
694
|
+
});
|
|
695
|
+
// Mark agent as idle (failed, but can receive new messages)
|
|
696
|
+
this.setAgentStatus(agentId, "idle");
|
|
697
|
+
// Remove from running
|
|
698
|
+
this.runningAgents.delete(agentId);
|
|
699
|
+
// Notify completion listeners with error
|
|
700
|
+
this.notifyCompletion(agentId, { success: false, error: error.message });
|
|
701
|
+
// Don't restart on error - let user handle it
|
|
702
|
+
// Messages stay in queue for manual intervention
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Notify all completion listeners for an agent
|
|
706
|
+
* Called when agent completes (success or error)
|
|
707
|
+
*/
|
|
708
|
+
notifyCompletion(agentId, result) {
|
|
709
|
+
const callbacks = this.completionCallbacks.get(agentId) || [];
|
|
710
|
+
callbacks.forEach((cb) => {
|
|
711
|
+
cb(result);
|
|
712
|
+
});
|
|
713
|
+
this.completionCallbacks.delete(agentId); // One-time callbacks
|
|
714
|
+
if (callbacks.length > 0) {
|
|
715
|
+
this.logger.debug(`Notified ${callbacks.length} listener(s) for ${agentId}`, {
|
|
716
|
+
agentId,
|
|
717
|
+
listenersCount: callbacks.length,
|
|
718
|
+
success: result.success,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Interrupt a running agent and restart with new messages
|
|
724
|
+
*/
|
|
725
|
+
async interruptAndRestart(agentId) {
|
|
726
|
+
const running = this.runningAgents.get(agentId);
|
|
727
|
+
if (!running)
|
|
728
|
+
return;
|
|
729
|
+
this.logger.info(`Sending interrupt to ${agentId}`, { agentId });
|
|
730
|
+
// Send interrupt signal
|
|
731
|
+
const instance = running.instance;
|
|
732
|
+
if (instance.interrupt) {
|
|
733
|
+
instance.interrupt();
|
|
734
|
+
}
|
|
735
|
+
// Wait for graceful stop (agent saves state)
|
|
736
|
+
try {
|
|
737
|
+
await running.promise;
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
// Interrupt causes early exit, that's expected
|
|
741
|
+
this.logger.debug(`Agent ${agentId} interrupted successfully`, {
|
|
742
|
+
agentId,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
// Remove from running
|
|
746
|
+
this.runningAgents.delete(agentId);
|
|
747
|
+
// Mark as starting and process next batch with new messages
|
|
748
|
+
if (!this.startingAgents.has(agentId)) {
|
|
749
|
+
this.startingAgents.add(agentId);
|
|
750
|
+
try {
|
|
751
|
+
await this.processNextBatch(agentId);
|
|
752
|
+
}
|
|
753
|
+
finally {
|
|
754
|
+
this.startingAgents.delete(agentId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Add message to queue with priority + FIFO sorting
|
|
760
|
+
*/
|
|
761
|
+
enqueue(message) {
|
|
762
|
+
if (!this.queues.has(message.to)) {
|
|
763
|
+
this.queues.set(message.to, []);
|
|
764
|
+
}
|
|
765
|
+
this.queues.get(message.to)?.push(message);
|
|
766
|
+
this.sortQueue(message.to);
|
|
767
|
+
// Emit message queued event
|
|
768
|
+
this.emit("message_queued", {
|
|
769
|
+
messageId: message.id,
|
|
770
|
+
from: message.from,
|
|
771
|
+
to: message.to,
|
|
772
|
+
priority: message.priority,
|
|
773
|
+
timestamp: message.timestamp,
|
|
774
|
+
queueSize: this.queues.get(message.to)?.length,
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Sort queue: priority first, then FIFO within priority
|
|
779
|
+
*/
|
|
780
|
+
sortQueue(agentId) {
|
|
781
|
+
const queue = this.queues.get(agentId);
|
|
782
|
+
if (!queue)
|
|
783
|
+
return;
|
|
784
|
+
queue.sort((a, b) => {
|
|
785
|
+
const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
|
|
786
|
+
const aPriority = priorityOrder[a.priority];
|
|
787
|
+
const bPriority = priorityOrder[b.priority];
|
|
788
|
+
if (aPriority !== bPriority) {
|
|
789
|
+
return aPriority - bPriority;
|
|
790
|
+
}
|
|
791
|
+
return a.timestamp - b.timestamp; // FIFO within same priority
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Parse agent ID to determine ODU name
|
|
796
|
+
* Examples:
|
|
797
|
+
* - 'toolbox-ia' → 'toolbox'
|
|
798
|
+
* - 'toolbox-ea-abc123' → 'toolbox'
|
|
799
|
+
* - 'meta-ia' → 'meta'
|
|
800
|
+
*/
|
|
801
|
+
getODUName(agentId) {
|
|
802
|
+
const parts = agentId.split("-");
|
|
803
|
+
if (parts.length < 2) {
|
|
804
|
+
throw new Error(`Invalid agent ID format: ${agentId}`);
|
|
805
|
+
}
|
|
806
|
+
// Regular instance: first part is ODU name
|
|
807
|
+
return parts[0];
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Load session from Nexus session store (new format)
|
|
811
|
+
*/
|
|
812
|
+
async loadSessionFromStore(_storePath, agentId) {
|
|
813
|
+
try {
|
|
814
|
+
const sessionKey = `agent:${agentId}`;
|
|
815
|
+
const agentIdNormalized = normalizeAgentId(DEFAULT_AGENT_ID);
|
|
816
|
+
const session = await loadSession(agentIdNormalized, sessionKey);
|
|
817
|
+
if (!session) {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
// Load history from new format
|
|
821
|
+
const history = session.history.map((turn) => ({
|
|
822
|
+
role: turn.role,
|
|
823
|
+
content: turn.content,
|
|
824
|
+
timestamp: new Date(turn.timestamp).getTime(),
|
|
825
|
+
}));
|
|
826
|
+
return {
|
|
827
|
+
history,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
catch (error) {
|
|
831
|
+
this.logger.error(`Failed to load session for ${agentId}`, {
|
|
832
|
+
agentId,
|
|
833
|
+
error: error instanceof Error ? error.message : String(error),
|
|
834
|
+
});
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Register or update EA in session store (new format)
|
|
840
|
+
* EAs persist forever once created
|
|
841
|
+
*/
|
|
842
|
+
async registerEA(_storePath, agentId, taskName) {
|
|
843
|
+
try {
|
|
844
|
+
const sessionKey = `agent:${agentId}`;
|
|
845
|
+
const agentIdNormalized = normalizeAgentId(DEFAULT_AGENT_ID);
|
|
846
|
+
const now = Date.now();
|
|
847
|
+
// Check if EA already exists
|
|
848
|
+
const existing = await loadSession(agentIdNormalized, sessionKey);
|
|
849
|
+
if (existing) {
|
|
850
|
+
// Update last updated timestamp
|
|
851
|
+
await writeSessionMetadata(agentIdNormalized, sessionKey, {
|
|
852
|
+
...existing.metadata,
|
|
853
|
+
updatedAt: now,
|
|
854
|
+
});
|
|
855
|
+
this.logger.debug(`Updated session for ${agentId}`, { agentId });
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
// Create new EA registration
|
|
859
|
+
const displayName = this.getDisplayName(agentId);
|
|
860
|
+
const oduName = this.getODUName(agentId);
|
|
861
|
+
const newEntry = {
|
|
862
|
+
sessionId: crypto.randomUUID(),
|
|
863
|
+
updatedAt: now,
|
|
864
|
+
displayName: taskName || displayName,
|
|
865
|
+
chatType: "direct",
|
|
866
|
+
};
|
|
867
|
+
await writeSessionMetadata(agentIdNormalized, sessionKey, {
|
|
868
|
+
...newEntry,
|
|
869
|
+
created: new Date().toISOString(),
|
|
870
|
+
});
|
|
871
|
+
this.logger.info(`Registered new EA: ${agentId}`, {
|
|
872
|
+
agentId,
|
|
873
|
+
oduName,
|
|
874
|
+
displayName,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
this.logger.error(`Failed to register EA ${agentId}`, {
|
|
880
|
+
agentId,
|
|
881
|
+
error: error instanceof Error ? error.message : String(error),
|
|
882
|
+
});
|
|
883
|
+
// Don't throw - registration failure shouldn't block agent execution
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
// ============================================================
|
|
887
|
+
// PUBLIC OBSERVABILITY METHODS (for GUI and monitoring)
|
|
888
|
+
// ============================================================
|
|
889
|
+
/**
|
|
890
|
+
* Get all registered IAs
|
|
891
|
+
* Returns array of IA metadata for GUI display
|
|
892
|
+
*/
|
|
893
|
+
getRegisteredIAs() {
|
|
894
|
+
const ias = [];
|
|
895
|
+
for (const [id] of this.registeredIAs.entries()) {
|
|
896
|
+
ias.push({
|
|
897
|
+
id,
|
|
898
|
+
oduName: this.getODUName(id),
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return ias;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Get all running EAs with their status
|
|
905
|
+
* Returns array of EA metadata for GUI display
|
|
906
|
+
*/
|
|
907
|
+
getRunningAgents() {
|
|
908
|
+
const agents = [];
|
|
909
|
+
for (const [agentId, runningAgent] of this.runningAgents.entries()) {
|
|
910
|
+
agents.push({
|
|
911
|
+
agentId,
|
|
912
|
+
status: runningAgent.status,
|
|
913
|
+
startedAt: runningAgent.startedAt,
|
|
914
|
+
oduName: this.getODUName(agentId),
|
|
915
|
+
queueSize: this.getQueueSize(agentId),
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
return agents;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get all queues with their sizes
|
|
922
|
+
* Returns map of agentId -> queue size
|
|
923
|
+
*/
|
|
924
|
+
getAllQueues() {
|
|
925
|
+
const queueSizes = new Map();
|
|
926
|
+
for (const [agentId, queue] of this.queues.entries()) {
|
|
927
|
+
queueSizes.set(agentId, queue.length);
|
|
928
|
+
}
|
|
929
|
+
return queueSizes;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Get all agent IDs (both IAs and EAs) that the broker knows about
|
|
933
|
+
* Includes running, queued, and registered agents
|
|
934
|
+
*/
|
|
935
|
+
getAllKnownAgents() {
|
|
936
|
+
const agents = [];
|
|
937
|
+
// Add all registered IAs
|
|
938
|
+
for (const [id] of this.registeredIAs.entries()) {
|
|
939
|
+
agents.push({
|
|
940
|
+
agentId: id,
|
|
941
|
+
type: "ia",
|
|
942
|
+
status: this.getAgentStatus(id),
|
|
943
|
+
oduName: this.getODUName(id),
|
|
944
|
+
queueSize: this.getQueueSize(id),
|
|
945
|
+
isRunning: false, // IAs are always available, not "running" in the same sense
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
// Add all running EAs
|
|
949
|
+
for (const [id, runningAgent] of this.runningAgents.entries()) {
|
|
950
|
+
agents.push({
|
|
951
|
+
agentId: id,
|
|
952
|
+
type: "ea",
|
|
953
|
+
status: runningAgent.status,
|
|
954
|
+
oduName: this.getODUName(id),
|
|
955
|
+
queueSize: this.getQueueSize(id),
|
|
956
|
+
isRunning: true,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
// Add queued agents that aren't running
|
|
960
|
+
for (const [id, queue] of this.queues.entries()) {
|
|
961
|
+
if (!this.runningAgents.has(id) &&
|
|
962
|
+
!this.registeredIAs.has(id) &&
|
|
963
|
+
queue.length > 0) {
|
|
964
|
+
agents.push({
|
|
965
|
+
agentId: id,
|
|
966
|
+
type: "ea",
|
|
967
|
+
status: "idle",
|
|
968
|
+
oduName: this.getODUName(id),
|
|
969
|
+
queueSize: queue.length,
|
|
970
|
+
isRunning: false,
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return agents;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Event emitter support for real-time updates
|
|
978
|
+
* Listeners can subscribe to broker events
|
|
979
|
+
*/
|
|
980
|
+
eventListeners = new Map();
|
|
981
|
+
/**
|
|
982
|
+
* Subscribe to broker events
|
|
983
|
+
* Events: 'agent_started', 'agent_completed', 'agent_status_changed', 'message_queued'
|
|
984
|
+
*/
|
|
985
|
+
on(event, callback) {
|
|
986
|
+
if (!this.eventListeners.has(event)) {
|
|
987
|
+
this.eventListeners.set(event, []);
|
|
988
|
+
}
|
|
989
|
+
this.eventListeners.get(event)?.push(callback);
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Unsubscribe from broker events
|
|
993
|
+
*/
|
|
994
|
+
off(event, callback) {
|
|
995
|
+
const listeners = this.eventListeners.get(event);
|
|
996
|
+
if (!listeners)
|
|
997
|
+
return;
|
|
998
|
+
const index = listeners.indexOf(callback);
|
|
999
|
+
if (index > -1) {
|
|
1000
|
+
listeners.splice(index, 1);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Emit event to all subscribers
|
|
1005
|
+
*/
|
|
1006
|
+
emit(event, data) {
|
|
1007
|
+
const listeners = this.eventListeners.get(event);
|
|
1008
|
+
if (!listeners)
|
|
1009
|
+
return;
|
|
1010
|
+
for (const callback of listeners) {
|
|
1011
|
+
try {
|
|
1012
|
+
callback(data);
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
this.logger.error(`Error in event listener for ${event}`, {
|
|
1016
|
+
event,
|
|
1017
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|