@intent-systems/nexus 2026.1.5-3 → 2026.1.5-4

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.
Files changed (39) hide show
  1. package/dist/capabilities/detector.js +214 -0
  2. package/dist/capabilities/registry.js +98 -0
  3. package/dist/channels/location.js +44 -0
  4. package/dist/channels/web/index.js +2 -0
  5. package/dist/control-plane/broker/broker.js +969 -0
  6. package/dist/control-plane/compaction.js +284 -0
  7. package/dist/control-plane/factory.js +31 -0
  8. package/dist/control-plane/index.js +10 -0
  9. package/dist/control-plane/odu/agents.js +187 -0
  10. package/dist/control-plane/odu/interaction-tools.js +196 -0
  11. package/dist/control-plane/odu/prompt-loader.js +95 -0
  12. package/dist/control-plane/odu/runtime.js +467 -0
  13. package/dist/control-plane/odu/types.js +6 -0
  14. package/dist/control-plane/odu-control-plane.js +314 -0
  15. package/dist/control-plane/single-agent.js +249 -0
  16. package/dist/control-plane/types.js +11 -0
  17. package/dist/credentials/store.js +323 -0
  18. package/dist/logging/redact.js +109 -0
  19. package/dist/markdown/fences.js +58 -0
  20. package/dist/memory/embeddings.js +146 -0
  21. package/dist/memory/index.js +382 -0
  22. package/dist/memory/internal.js +163 -0
  23. package/dist/pairing/pairing-store.js +194 -0
  24. package/dist/plugins/cli.js +42 -0
  25. package/dist/plugins/discovery.js +253 -0
  26. package/dist/plugins/install.js +181 -0
  27. package/dist/plugins/loader.js +290 -0
  28. package/dist/plugins/registry.js +105 -0
  29. package/dist/plugins/status.js +29 -0
  30. package/dist/plugins/tools.js +39 -0
  31. package/dist/plugins/types.js +1 -0
  32. package/dist/routing/resolve-route.js +144 -0
  33. package/dist/routing/session-key.js +63 -0
  34. package/dist/utils/provider-utils.js +28 -0
  35. package/package.json +4 -29
  36. package/patches/@mariozechner__pi-ai.patch +215 -0
  37. package/patches/playwright-core@1.57.0.patch +13 -0
  38. package/patches/qrcode-terminal.patch +12 -0
  39. package/scripts/postinstall.js +202 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Unified ODU Runtime
3
+ *
4
+ * Convention-driven agent runtime that works for any ODU.
5
+ * Adapted from magic-toolbox for Nexus.
6
+ */
7
+ import path from 'node:path';
8
+ import { InteractionAgent, ExecutionAgent } from './agents.js';
9
+ import { loadHistory, appendToHistory, } from '../../config/sessions.js';
10
+ import { resolveUserPath } from '../../utils.js';
11
+ import { DEFAULT_AGENT_ID } from '../../routing/session-key.js';
12
+ import { loadPrompt } from './prompt-loader.js';
13
+ import { DEFAULT_MODEL } from '../../agents/defaults.js';
14
+ /**
15
+ * Unified Interaction Agent Runtime
16
+ *
17
+ * Convention-driven IA that works for any ODU.
18
+ * Wraps Nexus embedded agent system.
19
+ */
20
+ export class ODUInteractionAgent extends InteractionAgent {
21
+ // Static registry of all ODU IA singletons (fractal architecture support)
22
+ static instances = new Map();
23
+ conversationHistory = [];
24
+ broker;
25
+ constructor(config) {
26
+ // Singleton key: oduPath + userId (supports nested ODUs)
27
+ const key = `${config.oduPath}:${config.userId}`;
28
+ // Return existing instance if already created
29
+ const existing = ODUInteractionAgent.instances.get(key);
30
+ if (existing) {
31
+ return existing;
32
+ }
33
+ // Create new instance
34
+ super(config);
35
+ this.oduConfig = config.oduConfig;
36
+ this.broker = config.broker;
37
+ // Register this singleton
38
+ ODUInteractionAgent.instances.set(key, this);
39
+ // Load conversation history asynchronously
40
+ // (constructors can't be async, so we do this in the background)
41
+ if (config.sessionId) {
42
+ this.loadConversationHistory(config.sessionId).catch((error) => {
43
+ this.log.warn('Failed to load conversation history', { error });
44
+ });
45
+ }
46
+ // Register ODU with broker
47
+ const agentFactory = (agentId, taskDescription, history) => {
48
+ return new ODUExecutionAgent({
49
+ agentId,
50
+ userId: this.userId,
51
+ task: {
52
+ userId: this.userId,
53
+ type: this.oduConfig.name,
54
+ description: taskDescription,
55
+ taskName: agentId,
56
+ },
57
+ oduPath: this.oduPath,
58
+ config: this.config,
59
+ history,
60
+ broker: this.broker,
61
+ });
62
+ };
63
+ // Register ODU (EA factory) with broker
64
+ this.broker.registerODU(this.oduConfig.name, path.join(resolveUserPath(this.oduPath), '../state'), agentFactory);
65
+ // Register THIS IA with broker so it can receive messages
66
+ const iaId = `${this.oduConfig.name}-ia`;
67
+ this.broker.registerIA(iaId, this);
68
+ this.log.info('IA registered with broker', { iaId });
69
+ }
70
+ /**
71
+ * Resolve the model to use for IA from config
72
+ * Priority: controlPlane.odu.iaModel.primary > agent.model.primary > DEFAULT_MODEL
73
+ */
74
+ resolveIAModel() {
75
+ const oduConfig = this.config?.controlPlane?.odu;
76
+ const agentConfig = this.config?.agent;
77
+ // Check ODU-specific IA model first
78
+ if (oduConfig?.iaModel?.primary) {
79
+ // Extract just the model name from provider/model format
80
+ const parts = oduConfig.iaModel.primary.split('/');
81
+ return parts.length > 1 ? parts[1] : parts[0];
82
+ }
83
+ // Fall back to agent.model
84
+ if (agentConfig?.model?.primary) {
85
+ const parts = agentConfig.model.primary.split('/');
86
+ return parts.length > 1 ? parts[1] : parts[0];
87
+ }
88
+ // Default to opus
89
+ return DEFAULT_MODEL;
90
+ }
91
+ /**
92
+ * Load conversation history from new session format
93
+ */
94
+ async loadConversationHistory(sessionId) {
95
+ try {
96
+ // Use DEFAULT_AGENT_ID and sessionId as the sessionKey
97
+ const agentId = DEFAULT_AGENT_ID;
98
+ const sessionKey = sessionId;
99
+ // Load history from new format
100
+ const history = await loadHistory(agentId, sessionKey);
101
+ if (history && history.length > 0) {
102
+ // Convert HistoryTurn[] to conversation history format
103
+ this.conversationHistory = history.map((turn) => ({
104
+ role: turn.role,
105
+ content: turn.content,
106
+ tool_calls: turn.tool_calls,
107
+ tool_call_id: turn.tool_call_id,
108
+ timestamp: turn.timestamp,
109
+ }));
110
+ this.log.info('Loaded conversation history', {
111
+ turns: this.conversationHistory.length,
112
+ });
113
+ }
114
+ }
115
+ catch (error) {
116
+ this.log.error('Failed to load conversation history', { error });
117
+ }
118
+ }
119
+ /**
120
+ * Handle a user message and return complete response
121
+ */
122
+ async chatSync(userMessage) {
123
+ // ALWAYS interrupt if currently streaming
124
+ if (this.isStreaming) {
125
+ this.log.info('New message received while streaming, interrupting');
126
+ this.interrupt();
127
+ // Wait a tiny bit for stream to stop
128
+ await new Promise((resolve) => setTimeout(resolve, 50));
129
+ }
130
+ // Add message to queue
131
+ this.queueMessage(userMessage);
132
+ // If already processing (but not streaming), return queued message
133
+ if (this.isProcessing && !this.isStreaming) {
134
+ return 'Message queued. Processing...';
135
+ }
136
+ this.isProcessing = true;
137
+ try {
138
+ let finalResponse = '';
139
+ // Process all queued messages
140
+ while (this.messageQueue.length > 0) {
141
+ // Dequeue all current messages
142
+ const messages = this.messageQueue.splice(0);
143
+ // Sort by priority (high priority first)
144
+ messages.sort((a, b) => {
145
+ const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
146
+ const aPriority = priorityOrder[a.priority] || 2;
147
+ const bPriority = priorityOrder[b.priority] || 2;
148
+ if (aPriority !== bPriority)
149
+ return aPriority - bPriority;
150
+ return a.timestamp - b.timestamp;
151
+ });
152
+ // Extract sender from first message
153
+ const from = messages[0].from;
154
+ // Combine messages into single prompt
155
+ const combinedMessage = messages.length === 1
156
+ ? messages[0].content
157
+ : messages.map((m, i) => `Message ${i + 1}:\n${m.content}`).join('\n\n---\n\n');
158
+ // Reset interrupt flag for new processing
159
+ this.shouldInterrupt = false;
160
+ // Process the combined message with sender context (with streaming + interrupt support)
161
+ finalResponse = await this.processSingleMessage(combinedMessage, from);
162
+ // If interrupted, accumulated response is preserved
163
+ // New messages will be in the queue for next iteration
164
+ }
165
+ return finalResponse;
166
+ }
167
+ finally {
168
+ this.isProcessing = false;
169
+ this.isStreaming = false;
170
+ }
171
+ }
172
+ /**
173
+ * Process a single message (extracted for queue handling)
174
+ */
175
+ async processSingleMessage(userMessage, from) {
176
+ // If message from another agent, prepend context so IA knows who to respond to
177
+ if (from && from !== 'user' && from !== 'system') {
178
+ userMessage = `[Message from agent: ${from}]\n\n${userMessage}`;
179
+ }
180
+ this.log.info('Processing message', {
181
+ messageLength: userMessage.length,
182
+ from,
183
+ });
184
+ // Import dependencies dynamically
185
+ const { default: Anthropic } = await import('@anthropic-ai/sdk');
186
+ const { createInteractionTools, handleToolInvocation } = await import('./interaction-tools.js');
187
+ // Get API key from environment
188
+ const apiKey = process.env.ANTHROPIC_API_KEY;
189
+ if (!apiKey) {
190
+ throw new Error('ANTHROPIC_API_KEY environment variable is required for ODU interaction agent');
191
+ }
192
+ // Create Anthropic client
193
+ const client = new Anthropic({ apiKey });
194
+ // Build system prompt
195
+ const systemPrompt = await this.buildSystemPrompt();
196
+ // Create interaction tools context
197
+ const toolsContext = {
198
+ broker: this.broker,
199
+ userId: this.userId,
200
+ oduName: this.oduConfig.name,
201
+ oduPurpose: this.oduConfig.purpose,
202
+ agentId: `${this.oduConfig.name}-ia`,
203
+ };
204
+ // Get tool definitions
205
+ const tools = createInteractionTools(toolsContext);
206
+ // Build conversation history in Anthropic format
207
+ const messages = [];
208
+ // Add conversation history
209
+ for (const turn of this.conversationHistory) {
210
+ if (turn.role === 'user') {
211
+ messages.push({ role: 'user', content: turn.content });
212
+ }
213
+ else if (turn.role === 'assistant') {
214
+ // Build assistant message with tool calls if present
215
+ const content = [];
216
+ if (turn.content) {
217
+ content.push({ type: 'text', text: turn.content });
218
+ }
219
+ if (turn.tool_calls && turn.tool_calls.length > 0) {
220
+ for (const toolCall of turn.tool_calls) {
221
+ content.push({
222
+ type: 'tool_use',
223
+ id: toolCall.id,
224
+ name: toolCall.function.name,
225
+ input: JSON.parse(toolCall.function.arguments),
226
+ });
227
+ }
228
+ }
229
+ messages.push({ role: 'assistant', content });
230
+ }
231
+ else if (turn.role === 'tool') {
232
+ // Tool results must be in the previous assistant message's content
233
+ // For now, we'll skip them as they should already be recorded
234
+ continue;
235
+ }
236
+ }
237
+ // Add current user message
238
+ messages.push({ role: 'user', content: userMessage });
239
+ // Resolve model from config (IA uses controlPlane.odu.iaModel or agent.model)
240
+ const iaModel = this.resolveIAModel();
241
+ // Call Claude API with tool support
242
+ let response = await client.messages.create({
243
+ model: iaModel,
244
+ max_tokens: 4096,
245
+ system: systemPrompt,
246
+ messages,
247
+ tools,
248
+ });
249
+ // Handle tool calls in a loop (agent may make multiple tool calls)
250
+ let iterationCount = 0;
251
+ const maxIterations = 10; // Prevent infinite loops
252
+ while (response.stop_reason === 'tool_use' && iterationCount < maxIterations) {
253
+ iterationCount++;
254
+ // Extract tool calls from response
255
+ const toolUseBlocks = response.content.filter((block) => block.type === 'tool_use');
256
+ // Execute each tool and collect results
257
+ const toolResults = [];
258
+ for (const toolBlock of toolUseBlocks) {
259
+ try {
260
+ const result = await handleToolInvocation(toolBlock.name, toolBlock.input, toolsContext);
261
+ toolResults.push({
262
+ type: 'tool_result',
263
+ tool_use_id: toolBlock.id,
264
+ content: result,
265
+ });
266
+ }
267
+ catch (error) {
268
+ const errorMessage = error instanceof Error ? error.message : String(error);
269
+ toolResults.push({
270
+ type: 'tool_result',
271
+ tool_use_id: toolBlock.id,
272
+ content: `Error: ${errorMessage}`,
273
+ is_error: true,
274
+ });
275
+ }
276
+ }
277
+ // Add assistant response to messages
278
+ messages.push({ role: 'assistant', content: response.content });
279
+ // Add tool results as user message
280
+ messages.push({ role: 'user', content: toolResults });
281
+ // Continue conversation
282
+ response = await client.messages.create({
283
+ model: 'claude-sonnet-4-20250514',
284
+ max_tokens: 4096,
285
+ system: systemPrompt,
286
+ messages,
287
+ tools,
288
+ });
289
+ }
290
+ // Extract final text response
291
+ const textBlocks = response.content.filter((block) => block.type === 'text');
292
+ const finalResponse = textBlocks.map((block) => block.text).join('\n');
293
+ // Save conversation history (both in-memory and to disk)
294
+ const userTurn = {
295
+ turn_id: `turn-${Date.now()}-user`,
296
+ role: 'user',
297
+ content: userMessage,
298
+ timestamp: new Date().toISOString(),
299
+ };
300
+ const assistantTurn = {
301
+ turn_id: `turn-${Date.now()}-assistant`,
302
+ role: 'assistant',
303
+ content: finalResponse,
304
+ timestamp: new Date().toISOString(),
305
+ };
306
+ // Update in-memory history
307
+ this.conversationHistory.push(userTurn);
308
+ this.conversationHistory.push(assistantTurn);
309
+ // Persist to disk using the IA's session
310
+ const agentId = DEFAULT_AGENT_ID;
311
+ const sessionKey = this.sessionId;
312
+ try {
313
+ await appendToHistory(agentId, sessionKey, userTurn);
314
+ await appendToHistory(agentId, sessionKey, assistantTurn);
315
+ this.log.debug('Persisted conversation to history.jsonl', { sessionKey });
316
+ }
317
+ catch (error) {
318
+ this.log.warn('Failed to persist conversation history', { error, sessionKey });
319
+ }
320
+ return finalResponse;
321
+ }
322
+ /**
323
+ * Build system prompt for the IA
324
+ */
325
+ async buildSystemPrompt() {
326
+ // Load InteractionAgent.md prompt with template variables
327
+ const prompt = await loadPrompt('InteractionAgent.md', {
328
+ oduName: this.oduConfig.name,
329
+ oduPurpose: this.oduConfig.purpose,
330
+ oduPath: this.oduPath,
331
+ });
332
+ return prompt;
333
+ }
334
+ }
335
+ /**
336
+ * Unified Execution Agent Runtime
337
+ *
338
+ * Convention-driven EA that works for any ODU.
339
+ * Wraps Nexus embedded agent system with isolated workspace.
340
+ */
341
+ export class ODUExecutionAgent extends ExecutionAgent {
342
+ broker;
343
+ constructor(config) {
344
+ super(config);
345
+ this.broker = config.broker;
346
+ if (config.oduConfig) {
347
+ this.oduConfig = config.oduConfig;
348
+ }
349
+ else {
350
+ // Derive ODU name from path
351
+ const oduName = path.basename(resolveUserPath(config.oduPath || '~/nexus/home'));
352
+ this.oduConfig = {
353
+ name: oduName,
354
+ purpose: `Execute tasks for ${oduName} ODU`,
355
+ };
356
+ }
357
+ }
358
+ /**
359
+ * Execute the task using Nexus embedded agent
360
+ */
361
+ async execute() {
362
+ this.log.info('Starting EA execution', {
363
+ taskType: this.task.type,
364
+ taskDescription: (this.task.description || '').substring(0, 200),
365
+ historyLength: this.history.length,
366
+ });
367
+ try {
368
+ // Import runEmbeddedPiAgent dynamically
369
+ const { runEmbeddedPiAgent } = await import('../../agents/pi-embedded-runner.js');
370
+ const { resolveNexusAgentDir } = await import('../../agents/agent-paths.js');
371
+ const { randomUUID } = await import('node:crypto');
372
+ const path = await import('node:path');
373
+ // Build initial task prompt
374
+ const taskPrompt = this.buildInitialPrompt();
375
+ // Resolve workspace directory for this EA
376
+ // Use shared workspace (~/nexus/home) - all EAs share the same workspace
377
+ // This aligns with Cursor and single-agent mode behavior
378
+ const workspaceDir = resolveUserPath(this.oduPath);
379
+ // Resolve session file path
380
+ const agentDir = resolveNexusAgentDir();
381
+ const sessionFile = path.join(workspaceDir, '.agent-session.json');
382
+ // Build system prompt with EA template
383
+ const systemPrompt = await this.buildSystemPrompt();
384
+ // Run embedded pi-agent with full coding tools
385
+ const result = await runEmbeddedPiAgent({
386
+ sessionId: this.agentId,
387
+ sessionKey: this.agentId,
388
+ sessionFile,
389
+ workspaceDir,
390
+ agentDir,
391
+ config: this.config,
392
+ prompt: taskPrompt,
393
+ // Use default provider/model if not configured
394
+ provider: undefined, // Will use defaults from config
395
+ model: undefined, // Will use defaults from config
396
+ thinkLevel: this.config?.agent?.thinkingDefault,
397
+ verboseLevel: this.config?.agent?.verboseDefault,
398
+ timeoutMs: (this.config?.agent?.timeoutSeconds || 300) * 1000,
399
+ runId: randomUUID(),
400
+ extraSystemPrompt: systemPrompt,
401
+ });
402
+ // Extract final response from payloads
403
+ const finalResponse = result.payloads?.map((p) => p.text).filter(Boolean).join('\n\n') || 'Task completed';
404
+ this.log.info('EA execution complete', {
405
+ responseLength: finalResponse.length,
406
+ usage: result.meta.agentMeta?.usage,
407
+ });
408
+ // Send result back to IA via broker
409
+ const oduName = this.oduConfig.name;
410
+ const iaId = `${oduName}-ia`;
411
+ await this.broker.send({
412
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
413
+ from: this.agentId,
414
+ to: iaId,
415
+ content: finalResponse,
416
+ priority: 'normal',
417
+ timestamp: Date.now(),
418
+ metadata: {
419
+ source: 'ea',
420
+ completionResult: true,
421
+ usage: result.meta.agentMeta?.usage,
422
+ },
423
+ });
424
+ return finalResponse;
425
+ }
426
+ catch (error) {
427
+ this.log.error('EA execution failed', { error });
428
+ // Send error back to IA
429
+ const oduName = this.oduConfig.name;
430
+ const iaId = `${oduName}-ia`;
431
+ const errorMessage = error instanceof Error ? error.message : String(error);
432
+ await this.broker.send({
433
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
434
+ from: this.agentId,
435
+ to: iaId,
436
+ content: `Task failed: ${errorMessage}`,
437
+ priority: 'normal',
438
+ timestamp: Date.now(),
439
+ metadata: {
440
+ source: 'ea',
441
+ completionResult: true,
442
+ error: true,
443
+ },
444
+ });
445
+ throw error;
446
+ }
447
+ }
448
+ /**
449
+ * Build system prompt for the EA
450
+ */
451
+ async buildSystemPrompt() {
452
+ // Load ExecutionAgent.md prompt with template variables
453
+ const prompt = await loadPrompt('ExecutionAgent.md', {
454
+ oduName: this.oduConfig.name,
455
+ oduPurpose: this.oduConfig.purpose,
456
+ oduPath: this.oduPath,
457
+ task: this.task.description || 'Execute the assigned task',
458
+ taskName: this.task.taskName || 'task',
459
+ agentId: this.agentId,
460
+ // For now, leave skills and capabilities empty
461
+ // These will be populated when we implement skill/capability loading
462
+ availableSkills: '(Skills will be listed here)',
463
+ availableCapabilities: '(Capabilities will be listed here)',
464
+ });
465
+ return prompt;
466
+ }
467
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * ODU (Orchestration Domain Unit) Types
3
+ *
4
+ * Adapted from magic-toolbox for Nexus
5
+ */
6
+ export {};