@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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ODUControlPlane - AgentControlPlane implementation using broker + ODU runtime.
|
|
3
|
+
*
|
|
4
|
+
* This implementation uses the ActiveMessageBroker to route messages between
|
|
5
|
+
* an Interaction Agent (IA) singleton and dynamically created Execution Agents (EAs).
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - User messages → IA (singleton, always-on)
|
|
9
|
+
* - IA delegates tasks → EAs (created on-demand)
|
|
10
|
+
* - EAs complete and respond → IA
|
|
11
|
+
* - IA responds → User
|
|
12
|
+
*/
|
|
13
|
+
import crypto from "node:crypto";
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import { loadConfig } from "../config/config.js";
|
|
16
|
+
import { listAgentSessions, loadSession, resolveSessionDir, writeSessionMetadata, } from "../config/sessions.js";
|
|
17
|
+
import { createSubsystemLogger } from "../logging.js";
|
|
18
|
+
import { DEFAULT_AGENT_ID, normalizeAgentId, parseAgentSessionKey, } from "../routing/session-key.js";
|
|
19
|
+
import { ActiveMessageBroker } from "./broker/broker.js";
|
|
20
|
+
import { ODUInteractionAgent } from "./odu/runtime.js";
|
|
21
|
+
/**
|
|
22
|
+
* ODUControlPlane implements AgentControlPlane using the broker pattern.
|
|
23
|
+
*
|
|
24
|
+
* Key differences from SingleAgentControlPlane:
|
|
25
|
+
* - Creates a singleton IA per user/ODU
|
|
26
|
+
* - IA receives all messages and delegates to EAs via broker
|
|
27
|
+
* - Session continuity enables learning across tasks
|
|
28
|
+
*/
|
|
29
|
+
export class ODUControlPlane {
|
|
30
|
+
config;
|
|
31
|
+
workspaceDir;
|
|
32
|
+
broker;
|
|
33
|
+
logger;
|
|
34
|
+
oduConfig;
|
|
35
|
+
// Track created IAs (userId -> IA instance)
|
|
36
|
+
interactionAgents = new Map();
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.config = options?.config ?? loadConfig();
|
|
39
|
+
this.workspaceDir = options?.workspaceDir ?? process.cwd();
|
|
40
|
+
this.logger = createSubsystemLogger("odu-control-plane");
|
|
41
|
+
// Create broker instance
|
|
42
|
+
this.broker = new ActiveMessageBroker(this.config);
|
|
43
|
+
// Default ODU config (can be overridden via options)
|
|
44
|
+
this.oduConfig = options?.oduConfig ?? {
|
|
45
|
+
name: "nexus",
|
|
46
|
+
purpose: "AI operating system - universal agent orchestration",
|
|
47
|
+
};
|
|
48
|
+
this.logger.info("ODUControlPlane initialized", {
|
|
49
|
+
oduName: this.oduConfig.name,
|
|
50
|
+
workspaceDir: this.workspaceDir,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get or create the IA for a user/session
|
|
55
|
+
*/
|
|
56
|
+
getOrCreateIA(userId, sessionId) {
|
|
57
|
+
let ia = this.interactionAgents.get(userId);
|
|
58
|
+
if (!ia) {
|
|
59
|
+
this.logger.info("Creating new IA for user", { userId, sessionId });
|
|
60
|
+
ia = ODUInteractionAgent.getOrCreate({
|
|
61
|
+
userId,
|
|
62
|
+
sessionId,
|
|
63
|
+
oduPath: this.workspaceDir,
|
|
64
|
+
config: this.config,
|
|
65
|
+
oduConfig: this.oduConfig,
|
|
66
|
+
broker: this.broker,
|
|
67
|
+
});
|
|
68
|
+
this.interactionAgents.set(userId, ia);
|
|
69
|
+
}
|
|
70
|
+
return ia;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Convert session key to userId
|
|
74
|
+
* For ODU mode, we use the session key as the userId (e.g., "whatsapp:+15551234567")
|
|
75
|
+
*/
|
|
76
|
+
sessionKeyToUserId(sessionKey) {
|
|
77
|
+
// In ODU mode, each session key represents a unique user/conversation
|
|
78
|
+
return sessionKey;
|
|
79
|
+
}
|
|
80
|
+
async sendMessage(options) {
|
|
81
|
+
const { sessionKey, message, agentId: optAgentId, thinkingLevel, verboseLevel, isHeartbeat, streaming, onChunk: _onChunk, onComplete, onError, } = options;
|
|
82
|
+
try {
|
|
83
|
+
// Parse session key to get agentId
|
|
84
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
85
|
+
const agentId = normalizeAgentId(optAgentId ?? parsed?.agentId ?? DEFAULT_AGENT_ID);
|
|
86
|
+
// Convert session key to userId for ODU
|
|
87
|
+
const userId = this.sessionKeyToUserId(sessionKey);
|
|
88
|
+
// Load or create session
|
|
89
|
+
const session = await loadSession(agentId, sessionKey);
|
|
90
|
+
let sessionId;
|
|
91
|
+
if (!session) {
|
|
92
|
+
// Create new session
|
|
93
|
+
sessionId = crypto.randomUUID();
|
|
94
|
+
const sessionMetadata = {
|
|
95
|
+
sessionId,
|
|
96
|
+
agentId,
|
|
97
|
+
created: new Date().toISOString(),
|
|
98
|
+
chatType: "direct",
|
|
99
|
+
thinkingLevel: thinkingLevel ?? this.config.agent?.thinkingDefault,
|
|
100
|
+
verboseLevel: verboseLevel ?? this.config.agent?.verboseDefault,
|
|
101
|
+
compactionCount: 0,
|
|
102
|
+
};
|
|
103
|
+
await writeSessionMetadata(agentId, sessionKey, sessionMetadata);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
sessionId = session.metadata.sessionId;
|
|
107
|
+
// Update session metadata (writeSessionMetadata handles updated timestamp)
|
|
108
|
+
await writeSessionMetadata(agentId, sessionKey, session.metadata);
|
|
109
|
+
}
|
|
110
|
+
// Get or create IA for this user
|
|
111
|
+
const _ia = this.getOrCreateIA(userId, sessionId);
|
|
112
|
+
// Build broker message
|
|
113
|
+
const brokerMessage = {
|
|
114
|
+
id: crypto.randomUUID(),
|
|
115
|
+
from: "user",
|
|
116
|
+
to: `${this.oduConfig.name}-ia`,
|
|
117
|
+
content: message,
|
|
118
|
+
priority: isHeartbeat ? "low" : "normal",
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
conversationId: sessionId,
|
|
121
|
+
metadata: {
|
|
122
|
+
source: "user",
|
|
123
|
+
sessionKey,
|
|
124
|
+
thinkingLevel,
|
|
125
|
+
verboseLevel,
|
|
126
|
+
streaming,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
// Send message via broker
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
// For now, we send and wait for acknowledgment
|
|
132
|
+
// Future: implement streaming support
|
|
133
|
+
this.logger.debug("Sending message to IA via broker", {
|
|
134
|
+
sessionKey,
|
|
135
|
+
userId,
|
|
136
|
+
messagePreview: message.substring(0, 100),
|
|
137
|
+
});
|
|
138
|
+
let responseText = "";
|
|
139
|
+
try {
|
|
140
|
+
// Send message via broker and wait for ack
|
|
141
|
+
responseText = await this.broker.sendAndWaitForAck(brokerMessage);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
this.logger.error("Broker send failed", { error, sessionKey });
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
const durationMs = Date.now() - startTime;
|
|
148
|
+
// Update session metadata with latest timestamp
|
|
149
|
+
const updatedSession = await loadSession(agentId, sessionKey);
|
|
150
|
+
if (updatedSession) {
|
|
151
|
+
await writeSessionMetadata(agentId, sessionKey, updatedSession.metadata);
|
|
152
|
+
}
|
|
153
|
+
// Call completion callback if provided
|
|
154
|
+
if (onComplete) {
|
|
155
|
+
onComplete();
|
|
156
|
+
}
|
|
157
|
+
// Return result
|
|
158
|
+
// Note: config.agent.model can be a string or AgentModelListConfig object
|
|
159
|
+
// For simplicity, we just use "odu" provider and don't try to extract the model string
|
|
160
|
+
return {
|
|
161
|
+
success: true,
|
|
162
|
+
sessionId,
|
|
163
|
+
payloads: [
|
|
164
|
+
{
|
|
165
|
+
text: responseText,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
meta: {
|
|
169
|
+
durationMs,
|
|
170
|
+
provider: "odu",
|
|
171
|
+
model: updatedSession?.metadata.model,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
this.logger.error("sendMessage failed", { error, sessionKey });
|
|
177
|
+
if (onError) {
|
|
178
|
+
onError(error);
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: error.message,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async getSession(sessionKey) {
|
|
187
|
+
try {
|
|
188
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
189
|
+
const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
|
190
|
+
const session = await loadSession(agentId, sessionKey);
|
|
191
|
+
if (!session) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
// Session metadata has 'updated' (ISO string) from new format
|
|
195
|
+
const updated = session.metadata.updated ||
|
|
196
|
+
new Date().toISOString();
|
|
197
|
+
return {
|
|
198
|
+
key: sessionKey,
|
|
199
|
+
sessionId: session.metadata.sessionId,
|
|
200
|
+
displayName: session.metadata.displayName,
|
|
201
|
+
updatedAt: new Date(updated).getTime(),
|
|
202
|
+
model: session.metadata.model,
|
|
203
|
+
usage: {
|
|
204
|
+
inputTokens: 0, // TODO: calculate from history
|
|
205
|
+
outputTokens: 0,
|
|
206
|
+
totalTokens: 0,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
this.logger.error("getSession failed", { error, sessionKey });
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async listSessions() {
|
|
216
|
+
try {
|
|
217
|
+
// List sessions for default agent
|
|
218
|
+
const agentId = DEFAULT_AGENT_ID;
|
|
219
|
+
const sessionKeys = await listAgentSessions(agentId);
|
|
220
|
+
const sessions = [];
|
|
221
|
+
// Load each session
|
|
222
|
+
for (const sessionKey of sessionKeys) {
|
|
223
|
+
const session = await loadSession(agentId, sessionKey);
|
|
224
|
+
if (session) {
|
|
225
|
+
// Session metadata has 'updated' (ISO string) from new format
|
|
226
|
+
const updated = session.metadata.updated ||
|
|
227
|
+
new Date().toISOString();
|
|
228
|
+
sessions.push({
|
|
229
|
+
key: sessionKey,
|
|
230
|
+
sessionId: session.metadata.sessionId,
|
|
231
|
+
displayName: session.metadata.displayName,
|
|
232
|
+
updatedAt: new Date(updated).getTime(),
|
|
233
|
+
model: session.metadata.model,
|
|
234
|
+
usage: {
|
|
235
|
+
inputTokens: 0, // TODO: calculate from history
|
|
236
|
+
outputTokens: 0,
|
|
237
|
+
totalTokens: 0,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Sort by updatedAt descending (most recent first)
|
|
243
|
+
sessions.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
244
|
+
return sessions;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
this.logger.error("listSessions failed", { error });
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async getStatus() {
|
|
252
|
+
try {
|
|
253
|
+
const sessions = await this.listSessions();
|
|
254
|
+
const runningAgents = this.broker.getRunningAgents();
|
|
255
|
+
const queues = this.broker.getAllQueues();
|
|
256
|
+
let queuedMessages = 0;
|
|
257
|
+
for (const count of queues.values()) {
|
|
258
|
+
queuedMessages += count;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
mode: "odu",
|
|
262
|
+
activeSessions: sessions.length,
|
|
263
|
+
queuedMessages,
|
|
264
|
+
healthy: true,
|
|
265
|
+
metadata: {
|
|
266
|
+
runningAgents: runningAgents.length,
|
|
267
|
+
interactionAgents: this.interactionAgents.size,
|
|
268
|
+
oduName: this.oduConfig.name,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
this.logger.error("getStatus failed", { error });
|
|
274
|
+
return {
|
|
275
|
+
mode: "odu",
|
|
276
|
+
activeSessions: 0,
|
|
277
|
+
queuedMessages: 0,
|
|
278
|
+
healthy: false,
|
|
279
|
+
metadata: {
|
|
280
|
+
error: error.message,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async resetSession(sessionKey) {
|
|
286
|
+
try {
|
|
287
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
288
|
+
const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
|
289
|
+
// Delete session directory
|
|
290
|
+
const sessionDir = resolveSessionDir(agentId, sessionKey);
|
|
291
|
+
await fs.rm(sessionDir, { recursive: true, force: true });
|
|
292
|
+
// Also remove IA if it exists
|
|
293
|
+
const userId = this.sessionKeyToUserId(sessionKey);
|
|
294
|
+
this.interactionAgents.delete(userId);
|
|
295
|
+
this.logger.info("Session reset", { sessionKey });
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
this.logger.error("resetSession failed", { error, sessionKey });
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async shutdown() {
|
|
303
|
+
try {
|
|
304
|
+
this.logger.info("Shutting down ODUControlPlane");
|
|
305
|
+
// Clear all IAs
|
|
306
|
+
this.interactionAgents.clear();
|
|
307
|
+
// Broker doesn't have a shutdown method yet, but we could add one later
|
|
308
|
+
// For now, just clear our state
|
|
309
|
+
this.logger.info("ODUControlPlane shutdown complete");
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
this.logger.error("shutdown failed", { error });
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SingleAgentControlPlane - Wraps the current NexusBot embedded agent behavior.
|
|
3
|
+
*
|
|
4
|
+
* This implementation maintains the existing single-agent-per-session pattern.
|
|
5
|
+
* No behavior changes - just adapting the existing code to the AgentControlPlane interface.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { resolveNexusAgentDir } from "../agents/agent-paths.js";
|
|
11
|
+
import { queueEmbeddedPiMessage } from "../agents/pi-embedded.js";
|
|
12
|
+
import { runEmbeddedPiAgent, } from "../agents/pi-embedded-runner.js";
|
|
13
|
+
import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
|
|
14
|
+
import { loadConfig } from "../config/config.js";
|
|
15
|
+
import { listAgentSessions, loadSession, resolveSessionDir, writeSessionMetadata, } from "../config/sessions.js";
|
|
16
|
+
import { DEFAULT_AGENT_ID, normalizeAgentId, parseAgentSessionKey, } from "../routing/session-key.js";
|
|
17
|
+
/**
|
|
18
|
+
* SingleAgentControlPlane wraps the existing runEmbeddedPiAgent function
|
|
19
|
+
* to implement the AgentControlPlane interface.
|
|
20
|
+
*
|
|
21
|
+
* This maintains the current behavior where each session has its own
|
|
22
|
+
* embedded agent, and messages are processed through the existing
|
|
23
|
+
* runEmbeddedPiAgent flow.
|
|
24
|
+
*/
|
|
25
|
+
export class SingleAgentControlPlane {
|
|
26
|
+
config;
|
|
27
|
+
workspaceDir;
|
|
28
|
+
agentDir;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.config = options?.config ?? loadConfig();
|
|
31
|
+
this.workspaceDir = options?.workspaceDir ?? process.cwd();
|
|
32
|
+
this.agentDir = options?.agentDir ?? resolveNexusAgentDir();
|
|
33
|
+
}
|
|
34
|
+
async sendMessage(options) {
|
|
35
|
+
const { sessionKey, message, agentId: optAgentId, thinkingLevel, verboseLevel, isHeartbeat: _isHeartbeat, streaming, onChunk, onComplete, onError, } = options;
|
|
36
|
+
try {
|
|
37
|
+
// Parse session key to get agentId and sessionId
|
|
38
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
39
|
+
const agentId = normalizeAgentId(optAgentId ?? parsed?.agentId ?? DEFAULT_AGENT_ID);
|
|
40
|
+
// In the new format, we use sessionKey as the directory name for O(1) lookup
|
|
41
|
+
// The sessionId in metadata is still a UUID for uniqueness
|
|
42
|
+
const existingSession = await loadSession(agentId, sessionKey);
|
|
43
|
+
let sessionId;
|
|
44
|
+
let sessionEntry;
|
|
45
|
+
if (!existingSession) {
|
|
46
|
+
// Create new session
|
|
47
|
+
sessionId = randomUUID(); // Internal UUID
|
|
48
|
+
sessionEntry = {
|
|
49
|
+
sessionId,
|
|
50
|
+
updatedAt: Date.now(),
|
|
51
|
+
thinkingLevel: thinkingLevel ?? this.config.agent?.thinkingDefault,
|
|
52
|
+
verboseLevel: verboseLevel ?? this.config.agent?.verboseDefault,
|
|
53
|
+
};
|
|
54
|
+
// Write initial metadata
|
|
55
|
+
// Directory name = sessionKey, sessionId in metadata = UUID
|
|
56
|
+
await writeSessionMetadata(agentId, sessionKey, {
|
|
57
|
+
...sessionEntry,
|
|
58
|
+
created: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
sessionId = existingSession.metadata.sessionId;
|
|
63
|
+
sessionEntry = existingSession.metadata;
|
|
64
|
+
}
|
|
65
|
+
const sessionDir = resolveSessionDir(agentId, sessionKey);
|
|
66
|
+
const sessionFile = path.join(sessionDir, "history.jsonl");
|
|
67
|
+
// If streaming and already has an active agent, try to queue the message
|
|
68
|
+
if (streaming && queueEmbeddedPiMessage(sessionId, message)) {
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
sessionId,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Load skills snapshot
|
|
75
|
+
const skillsSnapshot = buildWorkspaceSkillSnapshot(this.workspaceDir, {
|
|
76
|
+
config: this.config,
|
|
77
|
+
});
|
|
78
|
+
// Build callbacks for streaming/chunks
|
|
79
|
+
const partialPayloads = [];
|
|
80
|
+
const onPartialReply = async (payload) => {
|
|
81
|
+
if (onChunk) {
|
|
82
|
+
onChunk({
|
|
83
|
+
type: "text",
|
|
84
|
+
content: payload.text,
|
|
85
|
+
metadata: { mediaUrls: payload.mediaUrls },
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
partialPayloads.push(payload);
|
|
89
|
+
};
|
|
90
|
+
const onToolResult = async (payload) => {
|
|
91
|
+
if (onChunk) {
|
|
92
|
+
onChunk({
|
|
93
|
+
type: "tool",
|
|
94
|
+
content: payload.text,
|
|
95
|
+
metadata: { mediaUrls: payload.mediaUrls },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
// Run the embedded agent
|
|
100
|
+
const runId = randomUUID();
|
|
101
|
+
const result = await runEmbeddedPiAgent({
|
|
102
|
+
sessionId,
|
|
103
|
+
sessionKey,
|
|
104
|
+
sessionFile,
|
|
105
|
+
workspaceDir: this.workspaceDir,
|
|
106
|
+
agentDir: this.agentDir,
|
|
107
|
+
config: this.config,
|
|
108
|
+
skillsSnapshot,
|
|
109
|
+
prompt: message,
|
|
110
|
+
provider: sessionEntry.providerOverride,
|
|
111
|
+
model: sessionEntry.modelOverride,
|
|
112
|
+
authProfileId: sessionEntry.authProfileOverride,
|
|
113
|
+
thinkLevel: thinkingLevel,
|
|
114
|
+
verboseLevel: verboseLevel,
|
|
115
|
+
timeoutMs: (this.config.agent?.timeoutSeconds ?? 600) * 1000,
|
|
116
|
+
runId,
|
|
117
|
+
onPartialReply: streaming ? onPartialReply : undefined,
|
|
118
|
+
onToolResult: streaming ? onToolResult : undefined,
|
|
119
|
+
});
|
|
120
|
+
// Call onComplete if provided
|
|
121
|
+
if (onComplete) {
|
|
122
|
+
onComplete();
|
|
123
|
+
}
|
|
124
|
+
// Convert result to SendMessageResult
|
|
125
|
+
// Note: EmbeddedPiRunResult doesn't have an error field; errors are thrown as exceptions
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
sessionId,
|
|
129
|
+
payloads: result.payloads,
|
|
130
|
+
meta: {
|
|
131
|
+
durationMs: result.meta.durationMs,
|
|
132
|
+
provider: result.meta.agentMeta?.provider,
|
|
133
|
+
model: result.meta.agentMeta?.model,
|
|
134
|
+
usage: result.meta.agentMeta?.usage,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (onError) {
|
|
140
|
+
onError(error);
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: error.message,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async getSession(sessionKey) {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
151
|
+
const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
|
152
|
+
// Load session from new format
|
|
153
|
+
const session = await loadSession(agentId, sessionKey);
|
|
154
|
+
if (!session) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const entry = session.metadata;
|
|
158
|
+
return {
|
|
159
|
+
key: sessionKey,
|
|
160
|
+
sessionId: entry.sessionId,
|
|
161
|
+
displayName: entry.displayName,
|
|
162
|
+
updatedAt: entry.updatedAt,
|
|
163
|
+
model: entry.model,
|
|
164
|
+
usage: {
|
|
165
|
+
inputTokens: entry.inputTokens,
|
|
166
|
+
outputTokens: entry.outputTokens,
|
|
167
|
+
totalTokens: entry.totalTokens,
|
|
168
|
+
},
|
|
169
|
+
entry,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async listSessions() {
|
|
177
|
+
try {
|
|
178
|
+
const agentId = DEFAULT_AGENT_ID;
|
|
179
|
+
// List all session keys (directory names) for the agent using new format
|
|
180
|
+
const sessionKeys = await listAgentSessions(agentId);
|
|
181
|
+
const sessions = [];
|
|
182
|
+
for (const sessionKey of sessionKeys) {
|
|
183
|
+
// Load each session (sessionKey is the directory name)
|
|
184
|
+
const session = await loadSession(agentId, sessionKey);
|
|
185
|
+
if (session) {
|
|
186
|
+
const entry = session.metadata;
|
|
187
|
+
sessions.push({
|
|
188
|
+
key: sessionKey, // sessionKey is the directory name
|
|
189
|
+
sessionId: entry.sessionId, // UUID from metadata
|
|
190
|
+
displayName: entry.displayName,
|
|
191
|
+
updatedAt: entry.updatedAt,
|
|
192
|
+
model: entry.model,
|
|
193
|
+
usage: {
|
|
194
|
+
inputTokens: entry.inputTokens,
|
|
195
|
+
outputTokens: entry.outputTokens,
|
|
196
|
+
totalTokens: entry.totalTokens,
|
|
197
|
+
},
|
|
198
|
+
entry,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Sort by updatedAt descending (most recent first)
|
|
203
|
+
sessions.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
|
204
|
+
return sessions;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async getStatus() {
|
|
211
|
+
try {
|
|
212
|
+
const sessions = await this.listSessions();
|
|
213
|
+
return {
|
|
214
|
+
mode: "single",
|
|
215
|
+
activeSessions: sessions.length,
|
|
216
|
+
healthy: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return {
|
|
221
|
+
mode: "single",
|
|
222
|
+
activeSessions: 0,
|
|
223
|
+
healthy: false,
|
|
224
|
+
metadata: {
|
|
225
|
+
error: error.message,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async resetSession(sessionKey) {
|
|
231
|
+
const parsed = parseAgentSessionKey(sessionKey);
|
|
232
|
+
const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
|
|
233
|
+
// Remove the session directory (new format)
|
|
234
|
+
const sessionDir = resolveSessionDir(agentId, sessionKey);
|
|
235
|
+
try {
|
|
236
|
+
await fs.promises.rm(sessionDir, { recursive: true, force: true });
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// If directory doesn't exist, that's fine
|
|
240
|
+
if (error.code !== "ENOENT") {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async shutdown() {
|
|
246
|
+
// Single agent mode doesn't need cleanup
|
|
247
|
+
// Future: could flush any pending operations
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentControlPlane - Abstract interface for agent orchestration.
|
|
3
|
+
*
|
|
4
|
+
* This interface allows swapping between different agent control strategies:
|
|
5
|
+
* - SingleAgentControlPlane: Current NexusBot behavior (one agent per session)
|
|
6
|
+
* - ODUControlPlane: Orchestration Domain Unit pattern (Interaction Agent + Execution Agents)
|
|
7
|
+
*
|
|
8
|
+
* The Gateway (Access Plane) interacts with agents through this interface,
|
|
9
|
+
* not directly with agent implementations.
|
|
10
|
+
*/
|
|
11
|
+
export {};
|