@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,284 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { compactEmbeddedPiSession, } from "../agents/pi-embedded-runner.js";
6
+ import { archiveHistory, loadHistory, resolveSessionDir, writeSummary, } from "../config/sessions.js";
7
+ import { createSubsystemLogger } from "../logging.js";
8
+ const log = createSubsystemLogger("compaction");
9
+ /**
10
+ * Compact a session by archiving old history and generating a summary.
11
+ *
12
+ * This function:
13
+ * 1. Loads current history from history.jsonl
14
+ * 2. Optionally calls pi-agent's session.compact() if usePiAgent is true
15
+ * 3. Partitions history into (old messages to archive, recent messages to keep)
16
+ * 4. Archives old messages to archives/{n}.jsonl (incrementing n)
17
+ * 5. Generates an LLM-based summary (or uses pi-agent's summary if available)
18
+ * 6. Writes the summary to summary.md
19
+ * 7. Keeps recent messages in history.jsonl for continuity
20
+ *
21
+ * After compaction, the agent can reference summary.md for archived context,
22
+ * use recent messages in history.jsonl for immediate continuity,
23
+ * and search archives/ if needed (Cursor-style dynamic context discovery).
24
+ */
25
+ export async function compactSession(agentId, sessionId, options = {}) {
26
+ try {
27
+ log.info(`Compacting session ${agentId}/${sessionId}`);
28
+ // If usePiAgent is enabled, call pi-agent's session.compact() first
29
+ let piAgentResult;
30
+ let piAgentSummary;
31
+ let piAgentFirstKeptEntryId;
32
+ if (options.usePiAgent && options.sessionFile && options.workspaceDir) {
33
+ log.info(`Calling pi-agent compaction for session ${agentId}/${sessionId}`);
34
+ piAgentResult = await compactEmbeddedPiSession({
35
+ sessionId,
36
+ sessionFile: options.sessionFile,
37
+ workspaceDir: options.workspaceDir,
38
+ agentDir: options.agentDir,
39
+ config: options.config,
40
+ customInstructions: options.customInstructions,
41
+ });
42
+ if (piAgentResult.ok && piAgentResult.compacted && piAgentResult.result) {
43
+ piAgentSummary = piAgentResult.result.summary;
44
+ piAgentFirstKeptEntryId = piAgentResult.result.firstKeptEntryId;
45
+ log.info(`Pi-agent compaction succeeded: ${piAgentResult.result.tokensBefore} tokens before, firstKeptEntryId: ${piAgentFirstKeptEntryId}`);
46
+ }
47
+ else {
48
+ log.warn(`Pi-agent compaction failed or did not compact: ${piAgentResult.reason ?? "unknown reason"}`);
49
+ }
50
+ }
51
+ // Load current history from our new session format
52
+ const history = await loadHistory(agentId, sessionId);
53
+ if (history.length === 0) {
54
+ log.info(`No history to compact for session ${agentId}/${sessionId}`);
55
+ return {
56
+ success: false,
57
+ error: "No history to compact",
58
+ piAgentResult,
59
+ };
60
+ }
61
+ log.debug(`Loaded ${history.length} turns for session ${agentId}/${sessionId}`);
62
+ // Partition history into (old messages to archive, recent messages to keep)
63
+ const keepRecentMessages = options.keepRecentMessages ?? 10;
64
+ const keepRecentTokens = options.keepRecentTokens;
65
+ const { toArchive, toKeep, firstKeptTurnId } = partitionHistory(history, keepRecentMessages, keepRecentTokens);
66
+ if (toArchive.length === 0) {
67
+ log.info(`No messages to archive for session ${agentId}/${sessionId} (${toKeep.length} messages below threshold)`);
68
+ return {
69
+ success: false,
70
+ error: "No messages to archive (all messages are recent)",
71
+ piAgentResult,
72
+ };
73
+ }
74
+ log.debug(`Partitioned ${history.length} turns: ${toArchive.length} to archive, ${toKeep.length} to keep`);
75
+ // Archive old messages and keep recent ones FIRST so we know the archive path
76
+ const archiveNumber = await archiveHistory(agentId, sessionId, toKeep);
77
+ const archivePath = path.join(resolveSessionDir(agentId, sessionId), "archives", `${String(archiveNumber).padStart(3, "0")}.jsonl`);
78
+ // Use pi-agent's summary if available, otherwise generate our own
79
+ let summary;
80
+ if (piAgentSummary) {
81
+ log.info("Using summary from pi-agent compaction");
82
+ // Add metadata header to pi-agent summary with archive path
83
+ summary = `# Conversation Summary
84
+
85
+ Generated: ${new Date().toISOString()}
86
+ Turns summarized: ${toArchive.length}
87
+ Archive: ${archivePath}
88
+ Source: pi-agent compaction
89
+
90
+ ---
91
+
92
+ ${piAgentSummary}
93
+ `;
94
+ }
95
+ else {
96
+ log.info("Generating summary using Anthropic API");
97
+ summary = await generateSummary(toArchive, options, archivePath);
98
+ }
99
+ log.info(`Archived ${toArchive.length} turns to archives/${String(archiveNumber).padStart(3, "0")}.jsonl, kept ${toKeep.length} recent turns`);
100
+ // Write summary to summary.md
101
+ await writeSummary(agentId, sessionId, summary);
102
+ log.info(`Wrote summary to summary.md (${summary.length} characters)`);
103
+ return {
104
+ success: true,
105
+ archiveNumber,
106
+ summary,
107
+ turnsArchived: toArchive.length,
108
+ turnsKept: toKeep.length,
109
+ firstKeptTurnId: piAgentFirstKeptEntryId ?? firstKeptTurnId,
110
+ piAgentResult,
111
+ };
112
+ }
113
+ catch (error) {
114
+ const errorMessage = error instanceof Error ? error.message : String(error);
115
+ log.error(`Failed to compact session ${agentId}/${sessionId}: ${errorMessage}`);
116
+ return {
117
+ success: false,
118
+ error: errorMessage,
119
+ };
120
+ }
121
+ }
122
+ /**
123
+ * Partition history into (old messages to archive, recent messages to keep).
124
+ * Uses either token-based or message-based threshold.
125
+ */
126
+ function partitionHistory(history, keepRecentMessages, keepRecentTokens) {
127
+ if (keepRecentTokens !== undefined) {
128
+ // Token-based partitioning (approximate token count using character length)
129
+ // Rough approximation: 1 token ≈ 4 characters
130
+ let tokenCount = 0;
131
+ let keepIndex = history.length;
132
+ // Work backwards from the end
133
+ for (let i = history.length - 1; i >= 0; i--) {
134
+ const turn = history[i];
135
+ const estimatedTokens = Math.ceil((turn.content?.length ?? 0) / 4 +
136
+ (turn.tool_calls
137
+ ? JSON.stringify(turn.tool_calls).length / 4
138
+ : 0));
139
+ tokenCount += estimatedTokens;
140
+ if (tokenCount > keepRecentTokens) {
141
+ // Found the split point
142
+ keepIndex = i + 1;
143
+ break;
144
+ }
145
+ }
146
+ const toArchive = history.slice(0, keepIndex);
147
+ const toKeep = history.slice(keepIndex);
148
+ const firstKeptTurnId = toKeep.length > 0 ? toKeep[0].turn_id : undefined;
149
+ return { toArchive, toKeep, firstKeptTurnId };
150
+ }
151
+ else {
152
+ // Message-based partitioning
153
+ const keepCount = Math.min(keepRecentMessages, history.length);
154
+ const archiveCount = history.length - keepCount;
155
+ const toArchive = history.slice(0, archiveCount);
156
+ const toKeep = history.slice(archiveCount);
157
+ const firstKeptTurnId = toKeep.length > 0 ? toKeep[0].turn_id : undefined;
158
+ return { toArchive, toKeep, firstKeptTurnId };
159
+ }
160
+ }
161
+ /**
162
+ * Write history to a temporary file for LLM reference (Cursor-style pattern).
163
+ * Returns the file path and formatted history content.
164
+ */
165
+ async function writeHistoryToTempFile(history) {
166
+ const formattedHistory = formatHistoryForSummary(history);
167
+ // Create temp file in OS temp directory
168
+ const tmpDir = os.tmpdir();
169
+ const tmpFilePath = path.join(tmpDir, `nexus-history-${Date.now()}.txt`);
170
+ await fs.promises.writeFile(tmpFilePath, formattedHistory, "utf-8");
171
+ return { tmpFilePath, formattedHistory };
172
+ }
173
+ /**
174
+ * Generate a summary of conversation history using Claude.
175
+ * Writes history to a temp file first so the LLM can reference specific details (Cursor-style).
176
+ */
177
+ async function generateSummary(history, options, archivePath) {
178
+ const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
179
+ if (!apiKey) {
180
+ throw new Error("ANTHROPIC_API_KEY environment variable is required for compaction");
181
+ }
182
+ const anthropic = new Anthropic({ apiKey });
183
+ const model = options.model ?? "claude-3-5-haiku-20241022";
184
+ const maxTokens = options.maxTokens ?? 2000;
185
+ // Write history to temp file for LLM reference (Cursor-style pattern)
186
+ const { tmpFilePath, formattedHistory } = await writeHistoryToTempFile(history);
187
+ try {
188
+ const systemPrompt = `You are an expert at summarizing conversation history for context preservation.
189
+
190
+ Your task is to create a concise, information-dense summary of the conversation history provided.
191
+
192
+ Guidelines:
193
+ - Focus on key decisions, outcomes, and important context
194
+ - Preserve technical details that might be referenced later
195
+ - Include any unresolved issues or pending tasks
196
+ - Use clear, structured markdown format
197
+ - Be comprehensive but concise (aim for 500-1500 words)
198
+ - Start with a brief overview, then provide detailed sections as needed
199
+ - If you need to reference specific details from the history, you can refer to line numbers (the history is formatted with timestamps)
200
+
201
+ Structure your summary with:
202
+ 1. Overview (2-3 sentences)
203
+ 2. Key Topics & Decisions
204
+ 3. Technical Details (if applicable)
205
+ 4. Pending Items / Next Steps (if any)`;
206
+ log.debug(`Generating summary for ${history.length} turns using ${model}`);
207
+ log.debug(`History written to temp file: ${tmpFilePath}`);
208
+ const response = await anthropic.messages.create({
209
+ model,
210
+ max_tokens: maxTokens,
211
+ system: systemPrompt,
212
+ messages: [
213
+ {
214
+ role: "user",
215
+ content: `Please summarize this conversation history.
216
+
217
+ The full conversation history has been written to: ${tmpFilePath}
218
+
219
+ History content:
220
+
221
+ ${formattedHistory}`,
222
+ },
223
+ ],
224
+ });
225
+ const summaryText = response.content[0]?.type === "text" ? response.content[0].text : "";
226
+ if (!summaryText) {
227
+ throw new Error("Failed to generate summary: empty response from Claude");
228
+ }
229
+ // Add metadata header with PERMANENT archive path (not temp file)
230
+ // This allows agents to search the archive later if the summary lacks detail
231
+ const archiveNote = archivePath
232
+ ? `Archive: ${archivePath}`
233
+ : `History file: ${tmpFilePath}`;
234
+ const metadata = `# Conversation Summary
235
+
236
+ Generated: ${new Date().toISOString()}
237
+ Turns summarized: ${history.length}
238
+ Model: ${model}
239
+ ${archiveNote}
240
+
241
+ ---
242
+
243
+ ${summaryText}
244
+ `;
245
+ return metadata;
246
+ }
247
+ finally {
248
+ // Clean up temp file
249
+ try {
250
+ await fs.promises.unlink(tmpFilePath);
251
+ log.debug(`Cleaned up temp history file: ${tmpFilePath}`);
252
+ }
253
+ catch (error) {
254
+ log.warn(`Failed to clean up temp history file ${tmpFilePath}: ${error}`);
255
+ }
256
+ }
257
+ }
258
+ /**
259
+ * Format history turns into a readable format for summarization.
260
+ */
261
+ function formatHistoryForSummary(history) {
262
+ const lines = [];
263
+ for (const turn of history) {
264
+ const timestamp = turn.timestamp
265
+ ? new Date(turn.timestamp).toISOString()
266
+ : "unknown";
267
+ const role = turn.role.toUpperCase();
268
+ lines.push(`[${timestamp}] ${role}:`);
269
+ if (turn.content) {
270
+ lines.push(turn.content);
271
+ }
272
+ if (turn.tool_calls && turn.tool_calls.length > 0) {
273
+ lines.push("\nTool Calls:");
274
+ for (const toolCall of turn.tool_calls) {
275
+ lines.push(` - ${toolCall.function.name}(${toolCall.function.arguments})`);
276
+ }
277
+ }
278
+ if (turn.tool_call_id) {
279
+ lines.push(`Tool Response (call_id: ${turn.tool_call_id})`);
280
+ }
281
+ lines.push(""); // Blank line between turns
282
+ }
283
+ return lines.join("\n");
284
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Factory for creating the appropriate AgentControlPlane implementation
3
+ * based on configuration.
4
+ */
5
+ import { SingleAgentControlPlane } from "./single-agent.js";
6
+ import { ODUControlPlane } from "./odu-control-plane.js";
7
+ /**
8
+ * Creates an AgentControlPlane instance based on the config.
9
+ *
10
+ * @param options - Factory options (config, workspace, agent dir)
11
+ * @returns AgentControlPlane instance
12
+ * @throws Error if mode is 'odu' (not yet implemented)
13
+ */
14
+ export function createControlPlane(options) {
15
+ const mode = options?.config?.controlPlane?.mode ?? "single";
16
+ switch (mode) {
17
+ case "single":
18
+ return new SingleAgentControlPlane({
19
+ config: options?.config,
20
+ workspaceDir: options?.workspaceDir,
21
+ agentDir: options?.agentDir,
22
+ });
23
+ case "odu":
24
+ return new ODUControlPlane({
25
+ config: options?.config,
26
+ workspaceDir: options?.workspaceDir,
27
+ });
28
+ default:
29
+ throw new Error(`Unknown control plane mode: ${mode}. Valid modes: 'single', 'odu'`);
30
+ }
31
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Agent Control Plane - Abstraction layer for agent orchestration.
3
+ *
4
+ * This module provides an abstract interface for managing agent sessions
5
+ * and routing messages. It allows switching between different orchestration
6
+ * strategies without changing the Gateway (Access Plane) code.
7
+ */
8
+ export { SingleAgentControlPlane } from "./single-agent.js";
9
+ export { ODUControlPlane } from "./odu-control-plane.js";
10
+ export { createControlPlane } from "./factory.js";
@@ -0,0 +1,187 @@
1
+ /**
2
+ * ODU Agent Base Classes
3
+ *
4
+ * Adapted from magic-toolbox for Nexus
5
+ * Wraps Nexus pi-coding-agent session API
6
+ */
7
+ import { createSubsystemLogger } from '../../logging.js';
8
+ /**
9
+ * Base Interaction Agent
10
+ *
11
+ * User-facing orchestrator that delegates to Execution Agents
12
+ * Uses message queue for handling rapid bursts of messages
13
+ */
14
+ export class InteractionAgent {
15
+ agentId;
16
+ sessionId;
17
+ userId;
18
+ oduPath;
19
+ oduConfig;
20
+ config;
21
+ model;
22
+ maxTurns;
23
+ log;
24
+ // Message queue and processing state
25
+ messageQueue = [];
26
+ isProcessing = false;
27
+ isStreaming = false;
28
+ shouldInterrupt = false;
29
+ accumulatedResponse = '';
30
+ constructor(config) {
31
+ this.userId = config.userId;
32
+ this.agentId = config.agentId || `${config.oduPath || 'nexus'}-ia`;
33
+ this.sessionId = config.sessionId || this.agentId;
34
+ this.oduPath = config.oduPath || '~/nexus/home';
35
+ this.config = config.config;
36
+ this.model = config.model || 'claude-sonnet-4-5-20250929';
37
+ this.maxTurns = config.maxTurns || 20;
38
+ // Initialize ODU config (will be set by subclass)
39
+ this.oduConfig = {
40
+ name: 'nexus',
41
+ purpose: 'Nexus Interaction Agent',
42
+ };
43
+ this.log = createSubsystemLogger(`odu/${this.agentId}`);
44
+ }
45
+ /**
46
+ * Queue a message for processing
47
+ */
48
+ queueMessage(content, priority = 'normal', from) {
49
+ this.messageQueue.push({
50
+ id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
51
+ content,
52
+ priority,
53
+ from,
54
+ timestamp: Date.now(),
55
+ });
56
+ }
57
+ /**
58
+ * Process queued messages without adding a new message
59
+ * Called by broker when messages are queued externally
60
+ *
61
+ * Returns IMMEDIATE acknowledgment, then processes async in background
62
+ */
63
+ async processQueue() {
64
+ this.log.info('processQueue() called', {
65
+ queueLength: this.messageQueue.length,
66
+ isProcessing: this.isProcessing,
67
+ });
68
+ // If already processing, return early
69
+ if (this.isProcessing) {
70
+ return 'IA is currently processing another request';
71
+ }
72
+ // If no messages queued, return early
73
+ if (this.messageQueue.length === 0) {
74
+ return 'No messages to process';
75
+ }
76
+ // Peek at first message to generate acknowledgment
77
+ const firstMessage = this.messageQueue[0];
78
+ const ackMessage = firstMessage.from
79
+ ? `Acknowledged request from ${firstMessage.from}. Processing now...`
80
+ : 'Acknowledged. Processing...';
81
+ // Start processing in background (don't await!)
82
+ this.processQueueAsync().catch((error) => {
83
+ this.log.error('Background processing error', { error });
84
+ });
85
+ // Return acknowledgment immediately
86
+ return ackMessage;
87
+ }
88
+ /**
89
+ * Internal async processing (runs in background after ack sent)
90
+ */
91
+ async processQueueAsync() {
92
+ this.isProcessing = true;
93
+ try {
94
+ let finalResponse = '';
95
+ // Process all queued messages
96
+ while (this.messageQueue.length > 0) {
97
+ // Dequeue all current messages
98
+ const messages = this.messageQueue.splice(0);
99
+ // Sort by priority
100
+ messages.sort((a, b) => {
101
+ const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 };
102
+ const aPriority = priorityOrder[a.priority] || 2;
103
+ const bPriority = priorityOrder[b.priority] || 2;
104
+ if (aPriority !== bPriority)
105
+ return aPriority - bPriority;
106
+ return a.timestamp - b.timestamp;
107
+ });
108
+ // Extract sender from first message
109
+ const from = messages[0].from;
110
+ // Combine messages
111
+ const combinedMessage = messages.length === 1
112
+ ? messages[0].content
113
+ : messages.map((m, i) => `Message ${i + 1}:\n${m.content}`).join('\n\n---\n\n');
114
+ // Reset interrupt flag
115
+ this.shouldInterrupt = false;
116
+ // Process the combined message with sender context
117
+ finalResponse = await this.processSingleMessage(combinedMessage, from);
118
+ }
119
+ this.log.info('Async processing complete', {
120
+ responseLength: finalResponse.length,
121
+ });
122
+ }
123
+ finally {
124
+ this.isProcessing = false;
125
+ this.isStreaming = false;
126
+ }
127
+ }
128
+ /**
129
+ * Interrupt current streaming operation
130
+ */
131
+ interrupt() {
132
+ if (this.isStreaming) {
133
+ this.log.info('Interrupting mid-stream', {
134
+ accumulatedLength: this.accumulatedResponse.length,
135
+ });
136
+ this.shouldInterrupt = true;
137
+ this.isStreaming = false;
138
+ }
139
+ }
140
+ /**
141
+ * Check if currently being interrupted
142
+ */
143
+ checkInterrupt() {
144
+ return this.shouldInterrupt;
145
+ }
146
+ }
147
+ /**
148
+ * Base Execution Agent
149
+ *
150
+ * Autonomous task executor with isolated workspace
151
+ * Executes a single task and stops (broker manages lifecycle)
152
+ */
153
+ export class ExecutionAgent {
154
+ agentId;
155
+ task;
156
+ userId;
157
+ oduPath;
158
+ oduConfig;
159
+ config;
160
+ model;
161
+ maxTurns;
162
+ history;
163
+ log;
164
+ constructor(config) {
165
+ this.userId = config.userId;
166
+ this.task = config.task;
167
+ this.agentId = config.agentId || `${config.oduPath || 'nexus'}-ea-${this.task.taskName || 'task'}`;
168
+ this.oduPath = config.oduPath || '~/nexus/home';
169
+ this.config = config.config;
170
+ // Support model override: task.model > config.model > default Haiku 4.5
171
+ this.model = config.task.model || config.model || 'claude-haiku-4-5-20251001';
172
+ this.maxTurns = config.maxTurns || 50;
173
+ this.history = config.history || [];
174
+ // Initialize ODU config (will be set by subclass)
175
+ this.oduConfig = {
176
+ name: 'nexus',
177
+ purpose: 'Nexus Execution Agent',
178
+ };
179
+ this.log = createSubsystemLogger(`odu/${this.agentId}`);
180
+ }
181
+ /**
182
+ * Build initial prompt for the task
183
+ */
184
+ buildInitialPrompt() {
185
+ return this.task.description || 'Complete the task';
186
+ }
187
+ }