@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.
- package/dist/capabilities/detector.js +214 -0
- package/dist/capabilities/registry.js +98 -0
- package/dist/channels/location.js +44 -0
- package/dist/channels/web/index.js +2 -0
- package/dist/control-plane/broker/broker.js +969 -0
- package/dist/control-plane/compaction.js +284 -0
- package/dist/control-plane/factory.js +31 -0
- package/dist/control-plane/index.js +10 -0
- package/dist/control-plane/odu/agents.js +187 -0
- package/dist/control-plane/odu/interaction-tools.js +196 -0
- package/dist/control-plane/odu/prompt-loader.js +95 -0
- package/dist/control-plane/odu/runtime.js +467 -0
- package/dist/control-plane/odu/types.js +6 -0
- package/dist/control-plane/odu-control-plane.js +314 -0
- package/dist/control-plane/single-agent.js +249 -0
- package/dist/control-plane/types.js +11 -0
- package/dist/credentials/store.js +323 -0
- package/dist/logging/redact.js +109 -0
- package/dist/markdown/fences.js +58 -0
- package/dist/memory/embeddings.js +146 -0
- package/dist/memory/index.js +382 -0
- package/dist/memory/internal.js +163 -0
- package/dist/pairing/pairing-store.js +194 -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/routing/resolve-route.js +144 -0
- package/dist/routing/session-key.js +63 -0
- package/dist/utils/provider-utils.js +28 -0
- 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,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
|
+
}
|