aiden-runtime 3.16.0
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/LICENSE +661 -0
- package/README.md +465 -0
- package/config/devos.config.json +186 -0
- package/config/hardware.json +9 -0
- package/config/model-selection.json +7 -0
- package/config/setup-complete.json +20 -0
- package/dist/api/routes/computerUse.js +112 -0
- package/dist/api/server.js +6870 -0
- package/dist/bin/npx-init.js +71 -0
- package/dist/coordination/commandGate.js +115 -0
- package/dist/coordination/livePulse.js +127 -0
- package/dist/core/agentLoop.js +2718 -0
- package/dist/core/agentShield.js +231 -0
- package/dist/core/aidenIdentity.js +215 -0
- package/dist/core/aidenPersonality.js +166 -0
- package/dist/core/aidenSdk.js +374 -0
- package/dist/core/asyncTasks.js +82 -0
- package/dist/core/auditTrail.js +61 -0
- package/dist/core/auxiliaryClient.js +114 -0
- package/dist/core/bgLLM.js +108 -0
- package/dist/core/bm25.js +68 -0
- package/dist/core/callbackSystem.js +64 -0
- package/dist/core/channels/adapter.js +6 -0
- package/dist/core/channels/discord.js +173 -0
- package/dist/core/channels/email.js +253 -0
- package/dist/core/channels/imessage.js +164 -0
- package/dist/core/channels/manager.js +96 -0
- package/dist/core/channels/signal.js +140 -0
- package/dist/core/channels/slack.js +139 -0
- package/dist/core/channels/twilio.js +144 -0
- package/dist/core/channels/webhook.js +186 -0
- package/dist/core/channels/whatsapp.js +185 -0
- package/dist/core/clarifyBus.js +75 -0
- package/dist/core/codeInterpreter.js +82 -0
- package/dist/core/computerControl.js +439 -0
- package/dist/core/conversationMemory.js +334 -0
- package/dist/core/costTracker.js +221 -0
- package/dist/core/cronManager.js +217 -0
- package/dist/core/deepKB.js +77 -0
- package/dist/core/doctor.js +279 -0
- package/dist/core/dreamEngine.js +334 -0
- package/dist/core/entityGraph.js +169 -0
- package/dist/core/eventBus.js +16 -0
- package/dist/core/evolutionAnalyzer.js +153 -0
- package/dist/core/executionLoop.js +309 -0
- package/dist/core/executor.js +224 -0
- package/dist/core/failureAnalyzer.js +166 -0
- package/dist/core/fastPathExpansion.js +82 -0
- package/dist/core/faultEngine.js +106 -0
- package/dist/core/featureGates.js +70 -0
- package/dist/core/fileIngestion.js +113 -0
- package/dist/core/gateway.js +97 -0
- package/dist/core/goalTracker.js +75 -0
- package/dist/core/growthEngine.js +168 -0
- package/dist/core/hardwareDetector.js +98 -0
- package/dist/core/hooks.js +45 -0
- package/dist/core/httpKeepalive.js +46 -0
- package/dist/core/hybridSearch.js +101 -0
- package/dist/core/importers.js +164 -0
- package/dist/core/instinctSystem.js +223 -0
- package/dist/core/knowledgeBase.js +351 -0
- package/dist/core/learningMemory.js +121 -0
- package/dist/core/lessonsBrowser.js +125 -0
- package/dist/core/licenseManager.js +399 -0
- package/dist/core/logBuffer.js +85 -0
- package/dist/core/machineId.js +87 -0
- package/dist/core/mcpClient.js +442 -0
- package/dist/core/memoryDistiller.js +165 -0
- package/dist/core/memoryExtractor.js +212 -0
- package/dist/core/memoryIds.js +213 -0
- package/dist/core/memoryPreamble.js +113 -0
- package/dist/core/memoryQuery.js +136 -0
- package/dist/core/memoryRecall.js +140 -0
- package/dist/core/memoryStrategy.js +201 -0
- package/dist/core/messageValidator.js +85 -0
- package/dist/core/modelDiscovery.js +108 -0
- package/dist/core/modelRouter.js +118 -0
- package/dist/core/morningBriefing.js +203 -0
- package/dist/core/multiGoalValidator.js +51 -0
- package/dist/core/parallelExecutor.js +43 -0
- package/dist/core/passiveSkillObserver.js +204 -0
- package/dist/core/paths.js +57 -0
- package/dist/core/patternDetector.js +83 -0
- package/dist/core/planResponseRepair.js +64 -0
- package/dist/core/planTool.js +111 -0
- package/dist/core/playwrightBridge.js +356 -0
- package/dist/core/pluginSystem.js +121 -0
- package/dist/core/privateMode.js +85 -0
- package/dist/core/reactLoop.js +156 -0
- package/dist/core/recipeEngine.js +166 -0
- package/dist/core/responseCache.js +128 -0
- package/dist/core/runSandbox.js +132 -0
- package/dist/core/sandboxRunner.js +200 -0
- package/dist/core/scheduler.js +543 -0
- package/dist/core/secretScanner.js +49 -0
- package/dist/core/semanticMemory.js +223 -0
- package/dist/core/sessionMemory.js +259 -0
- package/dist/core/sessionRouter.js +91 -0
- package/dist/core/sessionSearch.js +163 -0
- package/dist/core/setupWizard.js +225 -0
- package/dist/core/skillImporter.js +303 -0
- package/dist/core/skillLibrary.js +144 -0
- package/dist/core/skillLoader.js +471 -0
- package/dist/core/skillTeacher.js +352 -0
- package/dist/core/skillValidator.js +210 -0
- package/dist/core/skillWriter.js +384 -0
- package/dist/core/slashAsTool.js +226 -0
- package/dist/core/spawnManager.js +197 -0
- package/dist/core/statusVerbs.js +43 -0
- package/dist/core/swarmManager.js +109 -0
- package/dist/core/taskQueue.js +119 -0
- package/dist/core/taskRecovery.js +128 -0
- package/dist/core/taskState.js +168 -0
- package/dist/core/telegramBot.js +152 -0
- package/dist/core/todoManager.js +70 -0
- package/dist/core/toolNameRepair.js +71 -0
- package/dist/core/toolRegistry.js +2730 -0
- package/dist/core/tools/calendarTool.js +98 -0
- package/dist/core/tools/companyFilingsTool.js +98 -0
- package/dist/core/tools/gmailTool.js +87 -0
- package/dist/core/tools/marketDataTool.js +135 -0
- package/dist/core/tools/socialResearchTool.js +121 -0
- package/dist/core/truthCheck.js +57 -0
- package/dist/core/updateChecker.js +74 -0
- package/dist/core/userCognitionProfile.js +238 -0
- package/dist/core/userProfile.js +341 -0
- package/dist/core/version.js +5 -0
- package/dist/core/visionAnalyze.js +161 -0
- package/dist/core/voice/audio.js +187 -0
- package/dist/core/voice/stt.js +226 -0
- package/dist/core/voice/tts.js +310 -0
- package/dist/core/voiceInput.js +118 -0
- package/dist/core/voiceOutput.js +130 -0
- package/dist/core/webSearch.js +326 -0
- package/dist/core/workflowTracker.js +72 -0
- package/dist/core/workspaceMemory.js +54 -0
- package/dist/core/youtubeTranscript.js +224 -0
- package/dist/integrations/computerUse/apiRegistry.js +113 -0
- package/dist/integrations/computerUse/screenAgent.js +203 -0
- package/dist/integrations/computerUse/visionLoop.js +296 -0
- package/dist/memory/memoryLayers.js +143 -0
- package/dist/providers/boa.js +93 -0
- package/dist/providers/cerebras.js +70 -0
- package/dist/providers/custom.js +89 -0
- package/dist/providers/gemini.js +82 -0
- package/dist/providers/groq.js +92 -0
- package/dist/providers/index.js +149 -0
- package/dist/providers/nvidia.js +70 -0
- package/dist/providers/ollama.js +99 -0
- package/dist/providers/openrouter.js +74 -0
- package/dist/providers/router.js +497 -0
- package/dist/providers/types.js +6 -0
- package/dist/security/browserVault.js +129 -0
- package/dist/security/dataGuard.js +89 -0
- package/dist/tools/eonetTool.js +72 -0
- package/dist/types/computerUse.js +2 -0
- package/dist/types/executor.js +2 -0
- package/dist-bundle/cli.js +357859 -0
- package/package.json +256 -0
|
@@ -0,0 +1,2718 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================
|
|
3
|
+
// DevOS — Autonomous AI Execution System
|
|
4
|
+
// Copyright (c) 2026 Shiva Deore. All rights reserved.
|
|
5
|
+
// ============================================================
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.interruptCurrentCall = interruptCurrentCall;
|
|
41
|
+
exports.setStatusEmitter = setStatusEmitter;
|
|
42
|
+
exports.getBudgetState = getBudgetState;
|
|
43
|
+
exports.surfaceRelevantMemories = surfaceRelevantMemories;
|
|
44
|
+
exports.resolveTemplates = resolveTemplates;
|
|
45
|
+
exports.streamOpenAIResponse = streamOpenAIResponse;
|
|
46
|
+
exports.streamGeminiResponse = streamGeminiResponse;
|
|
47
|
+
exports.planWithLLM = planWithLLM;
|
|
48
|
+
exports.validatePlan = validatePlan;
|
|
49
|
+
exports.buildDependencyGroups = buildDependencyGroups;
|
|
50
|
+
exports.executePlan = executePlan;
|
|
51
|
+
exports.respondWithResults = respondWithResults;
|
|
52
|
+
exports.callLLM = callLLM;
|
|
53
|
+
exports.deepResearch = deepResearch;
|
|
54
|
+
// core/agentLoop.ts — 3-step agent loop:
|
|
55
|
+
// STEP 1: PLAN — LLM outputs JSON plan only (no execution)
|
|
56
|
+
// STEP 2: EXECUTE — Code runs each tool, gets real results
|
|
57
|
+
// STEP 3: RESPOND — LLM sees real results, streams natural language
|
|
58
|
+
const toolRegistry_1 = require("./toolRegistry");
|
|
59
|
+
const recipeEngine_1 = require("./recipeEngine");
|
|
60
|
+
const livePulse_1 = require("../coordination/livePulse");
|
|
61
|
+
const planTool_1 = require("./planTool");
|
|
62
|
+
const workspaceMemory_1 = require("./workspaceMemory");
|
|
63
|
+
const taskState_1 = require("./taskState");
|
|
64
|
+
const skillLoader_1 = require("./skillLoader");
|
|
65
|
+
const entityGraph_1 = require("./entityGraph");
|
|
66
|
+
const learningMemory_1 = require("./learningMemory");
|
|
67
|
+
const conversationMemory_1 = require("./conversationMemory");
|
|
68
|
+
const router_1 = require("../providers/router");
|
|
69
|
+
const index_1 = require("../providers/index");
|
|
70
|
+
const knowledgeBase_1 = require("./knowledgeBase");
|
|
71
|
+
const skillTeacher_1 = require("./skillTeacher");
|
|
72
|
+
const growthEngine_1 = require("./growthEngine");
|
|
73
|
+
const aidenPersonality_1 = require("./aidenPersonality");
|
|
74
|
+
const auditTrail_1 = require("./auditTrail");
|
|
75
|
+
const mcpClient_1 = require("./mcpClient");
|
|
76
|
+
const memoryRecall_1 = require("./memoryRecall");
|
|
77
|
+
const costTracker_1 = require("./costTracker");
|
|
78
|
+
const modelDiscovery_1 = require("./modelDiscovery");
|
|
79
|
+
const semanticMemory_1 = require("./semanticMemory");
|
|
80
|
+
const sessionMemory_1 = require("./sessionMemory");
|
|
81
|
+
const goalTracker_1 = require("./goalTracker");
|
|
82
|
+
const hooks_1 = require("./hooks");
|
|
83
|
+
const instinctSystem_1 = require("./instinctSystem");
|
|
84
|
+
const workflowTracker_1 = require("./workflowTracker");
|
|
85
|
+
const parallelExecutor_1 = require("./parallelExecutor");
|
|
86
|
+
const messageValidator_1 = require("./messageValidator");
|
|
87
|
+
const toolNameRepair_1 = require("./toolNameRepair");
|
|
88
|
+
const slashAsTool_1 = require("./slashAsTool");
|
|
89
|
+
const planResponseRepair_1 = require("./planResponseRepair");
|
|
90
|
+
const nodeFs = __importStar(require("fs"));
|
|
91
|
+
const nodePath = __importStar(require("path"));
|
|
92
|
+
const nodeOs = __importStar(require("os"));
|
|
93
|
+
// ── Pre-compact threshold ──────────────────────────────────────
|
|
94
|
+
// Fire pre_compact hook when history has this many messages
|
|
95
|
+
const COMPACT_THRESHOLD = 40;
|
|
96
|
+
// ── Interrupt / stop state ─────────────────────────────────────
|
|
97
|
+
let currentAbortController = null;
|
|
98
|
+
let executionInterrupted = false;
|
|
99
|
+
function interruptCurrentCall() {
|
|
100
|
+
executionInterrupted = true;
|
|
101
|
+
if (currentAbortController) {
|
|
102
|
+
currentAbortController.abort();
|
|
103
|
+
currentAbortController = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ── Status emitter — set per-request by server.ts, cleared on close ──
|
|
107
|
+
let _emitStatus = null;
|
|
108
|
+
function setStatusEmitter(fn) { _emitStatus = fn; }
|
|
109
|
+
function emitStatus(action, detail) { _emitStatus?.(action, detail); }
|
|
110
|
+
const TOOL_ACTION = {
|
|
111
|
+
web_search: 'searching', fetch_url: 'searching', deep_research: 'searching', social_research: 'searching',
|
|
112
|
+
fetch_page: 'reading', file_read: 'reading', file_list: 'reading',
|
|
113
|
+
file_write: 'writing',
|
|
114
|
+
run_python: 'coding', run_node: 'coding', shell_exec: 'coding',
|
|
115
|
+
run_powershell: 'coding', code_interpreter_python: 'coding', code_interpreter_node: 'coding',
|
|
116
|
+
open_browser: 'browsing', browser_extract: 'browsing', browser_screenshot: 'browsing',
|
|
117
|
+
browser_click: 'browsing', browser_type: 'browsing',
|
|
118
|
+
};
|
|
119
|
+
function toolStatusDetail(tool, input) {
|
|
120
|
+
if (!input)
|
|
121
|
+
return undefined;
|
|
122
|
+
switch (tool) {
|
|
123
|
+
case 'web_search':
|
|
124
|
+
case 'deep_research':
|
|
125
|
+
case 'social_research':
|
|
126
|
+
return input.query ? String(input.query).slice(0, 60) : undefined;
|
|
127
|
+
case 'run_python':
|
|
128
|
+
case 'code_interpreter_python':
|
|
129
|
+
return 'Python script';
|
|
130
|
+
case 'run_node':
|
|
131
|
+
case 'code_interpreter_node':
|
|
132
|
+
return 'Node script';
|
|
133
|
+
case 'open_browser':
|
|
134
|
+
return input.url ? String(input.url).slice(0, 60) : 'browser';
|
|
135
|
+
case 'browser_extract':
|
|
136
|
+
case 'browser_screenshot':
|
|
137
|
+
case 'fetch_page':
|
|
138
|
+
case 'fetch_url':
|
|
139
|
+
return input.url ? String(input.url).slice(0, 60) : 'page';
|
|
140
|
+
case 'file_read':
|
|
141
|
+
case 'file_write':
|
|
142
|
+
case 'file_list': {
|
|
143
|
+
const p = input.path || input.directory || '';
|
|
144
|
+
return p ? (String(p).split(/[/\\]/).pop() || String(p).slice(0, 40)) : undefined;
|
|
145
|
+
}
|
|
146
|
+
case 'shell_exec':
|
|
147
|
+
case 'run_powershell':
|
|
148
|
+
return input.command ? String(input.command).slice(0, 30) : undefined;
|
|
149
|
+
case 'get_stocks':
|
|
150
|
+
return input.symbol ?? (input.type ? `${input.market ?? ''} ${input.type}`.trim() : 'stocks');
|
|
151
|
+
case 'get_market_data':
|
|
152
|
+
case 'get_company_info':
|
|
153
|
+
return input.symbol ? String(input.symbol) : undefined;
|
|
154
|
+
default:
|
|
155
|
+
if (input.query)
|
|
156
|
+
return String(input.query).slice(0, 60);
|
|
157
|
+
if (input.url)
|
|
158
|
+
return String(input.url).slice(0, 60);
|
|
159
|
+
if (input.path)
|
|
160
|
+
return String(input.path).slice(0, 40);
|
|
161
|
+
if (input.command)
|
|
162
|
+
return String(input.command).slice(0, 30);
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function getBudgetWarning(budget) {
|
|
167
|
+
const usage = budget.currentIteration / budget.maxIterations;
|
|
168
|
+
const remaining = budget.maxIterations - budget.currentIteration;
|
|
169
|
+
if (usage >= budget.warningThreshold) {
|
|
170
|
+
return `[BUDGET WARNING: Turn ${budget.currentIteration}/${budget.maxIterations}. Only ${remaining} turn(s) left. Provide your final response NOW. Do not start new tool calls.]`;
|
|
171
|
+
}
|
|
172
|
+
if (usage >= budget.cautionThreshold) {
|
|
173
|
+
return `[BUDGET: Turn ${budget.currentIteration}/${budget.maxIterations}. ${remaining} turns left. Start consolidating your work and prepare a response.]`;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
let _activeBudget = null;
|
|
178
|
+
function getBudgetState() {
|
|
179
|
+
if (!_activeBudget)
|
|
180
|
+
return null;
|
|
181
|
+
return {
|
|
182
|
+
current: _activeBudget.currentIteration,
|
|
183
|
+
max: _activeBudget.maxIterations,
|
|
184
|
+
remaining: _activeBudget.maxIterations - _activeBudget.currentIteration,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// ── Token-based preflight compression ─────────────────────────
|
|
188
|
+
function estimateTokens(text) {
|
|
189
|
+
return Math.ceil(text.length / 4);
|
|
190
|
+
}
|
|
191
|
+
function estimateConversationTokens(messages) {
|
|
192
|
+
return messages.reduce((sum, msg) => {
|
|
193
|
+
const content = typeof msg.content === 'string'
|
|
194
|
+
? msg.content
|
|
195
|
+
: JSON.stringify(msg.content || '');
|
|
196
|
+
return sum + estimateTokens(content) + 4; // 4 tokens per message overhead
|
|
197
|
+
}, 0);
|
|
198
|
+
}
|
|
199
|
+
const MODEL_CONTEXT_LIMITS = {
|
|
200
|
+
'llama-3.1-8b-instant': 8192,
|
|
201
|
+
'llama-3.3-70b-versatile': 32768,
|
|
202
|
+
'gemma-7b-it': 8192,
|
|
203
|
+
'gemma2-9b-it': 8192,
|
|
204
|
+
'mixtral-8x7b-32768': 32768,
|
|
205
|
+
'deepseek-r1-distill-llama-70b': 32768,
|
|
206
|
+
'qwen-2.5-72b-instruct': 32768,
|
|
207
|
+
'gemini-2.0-flash': 1048576,
|
|
208
|
+
'gemini-1.5-flash': 1048576,
|
|
209
|
+
'gpt-4o': 128000,
|
|
210
|
+
'claude-sonnet-4-20250514': 200000,
|
|
211
|
+
'gemini-3-flash': 1048576,
|
|
212
|
+
'gemini-3.1-pro': 1048576,
|
|
213
|
+
'gpt-5.3-codex': 200000,
|
|
214
|
+
'default': 8192,
|
|
215
|
+
};
|
|
216
|
+
function getContextLimit(model) {
|
|
217
|
+
return MODEL_CONTEXT_LIMITS[model] ?? MODEL_CONTEXT_LIMITS['default'];
|
|
218
|
+
}
|
|
219
|
+
async function flushMemoryFromMessages(messages) {
|
|
220
|
+
const userMessages = messages
|
|
221
|
+
.filter(m => m.role === 'user')
|
|
222
|
+
.map(m => String(m.content))
|
|
223
|
+
.join('\n');
|
|
224
|
+
if (userMessages.length > 100) {
|
|
225
|
+
try {
|
|
226
|
+
semanticMemory_1.semanticMemory.add(userMessages.slice(0, 500), 'exchange', ['preflight_compression']);
|
|
227
|
+
console.log('[Context] Memory flushed before compression');
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
console.log('[Context] Memory flush skipped — extractor unavailable');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async function preflightCompressionCheck(messages, model, sessionId) {
|
|
235
|
+
const tokenCount = estimateConversationTokens(messages);
|
|
236
|
+
const contextLimit = getContextLimit(model);
|
|
237
|
+
const usage = tokenCount / contextLimit;
|
|
238
|
+
console.log(`[Context] ${tokenCount} tokens / ${contextLimit} limit (${(usage * 100).toFixed(0)}%)`);
|
|
239
|
+
if (usage < 0.5) {
|
|
240
|
+
// Under 50% — no compression needed
|
|
241
|
+
return messages;
|
|
242
|
+
}
|
|
243
|
+
console.log(`[Context] Over 50% — compressing middle messages`);
|
|
244
|
+
// Track parent/child lineage across compressions
|
|
245
|
+
if (sessionId) {
|
|
246
|
+
try {
|
|
247
|
+
(0, sessionMemory_1.createChildSession)(sessionId, 'preflight_compression', messages.length, tokenCount);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
console.log('[Context] Session lineage tracking skipped');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Step 1: Flush memory before compressing
|
|
254
|
+
await flushMemoryFromMessages(messages);
|
|
255
|
+
// Step 2: Keep first 2 messages (system + first user) and last 10 messages
|
|
256
|
+
const protectedStart = messages.slice(0, 2);
|
|
257
|
+
const protectedEnd = messages.slice(-10);
|
|
258
|
+
const middleMessages = messages.slice(2, -10);
|
|
259
|
+
if (middleMessages.length < 3) {
|
|
260
|
+
return messages; // not enough to compress
|
|
261
|
+
}
|
|
262
|
+
// Step 3: Summarize middle messages into a single system message
|
|
263
|
+
const middleText = middleMessages
|
|
264
|
+
.map(m => `${m.role}: ${String(m.content).substring(0, 200)}`)
|
|
265
|
+
.join('\n');
|
|
266
|
+
const summary = {
|
|
267
|
+
role: 'system',
|
|
268
|
+
content: `[COMPRESSED CONTEXT — ${middleMessages.length} messages summarized]\n` +
|
|
269
|
+
`Previous conversation covered: ${middleText.substring(0, 1000)}\n` +
|
|
270
|
+
`[End compressed context]`,
|
|
271
|
+
};
|
|
272
|
+
const compressed = [...protectedStart, summary, ...protectedEnd];
|
|
273
|
+
const newTokens = estimateConversationTokens(compressed);
|
|
274
|
+
console.log(`[Context] Compressed: ${tokenCount} → ${newTokens} tokens ` +
|
|
275
|
+
`(${messages.length} → ${compressed.length} messages)`);
|
|
276
|
+
return compressed;
|
|
277
|
+
}
|
|
278
|
+
// ── Proactive memory surfacing ─────────────────────────────────
|
|
279
|
+
const SKIP_MEMORY_PATTERNS = [
|
|
280
|
+
/^(hi|hello|hey|thanks|ok|yes|no|sure|bye)\b/i,
|
|
281
|
+
/^.{1,15}$/,
|
|
282
|
+
];
|
|
283
|
+
async function surfaceRelevantMemories(userMessage) {
|
|
284
|
+
if (SKIP_MEMORY_PATTERNS.some(p => p.test(userMessage.trim())))
|
|
285
|
+
return '';
|
|
286
|
+
const memories = [];
|
|
287
|
+
// 1. Semantic memory search
|
|
288
|
+
try {
|
|
289
|
+
const results = semanticMemory_1.semanticMemory.search(userMessage, 5);
|
|
290
|
+
for (const r of results) {
|
|
291
|
+
memories.push(`[Memory] ${r.text}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
295
|
+
// 2. Memory directory files — keyword match
|
|
296
|
+
try {
|
|
297
|
+
const memDir = nodePath.join(process.cwd(), 'workspace', 'memory');
|
|
298
|
+
if (nodeFs.existsSync(memDir)) {
|
|
299
|
+
const files = nodeFs.readdirSync(memDir).filter((f) => f.endsWith('.md'));
|
|
300
|
+
const keywords = userMessage.toLowerCase().split(/\s+/).filter((k) => k.length > 3);
|
|
301
|
+
for (const file of files) {
|
|
302
|
+
try {
|
|
303
|
+
const content = nodeFs.readFileSync(nodePath.join(memDir, file), 'utf8');
|
|
304
|
+
const contentLower = content.toLowerCase();
|
|
305
|
+
const matches = keywords.filter((k) => contentLower.includes(k));
|
|
306
|
+
if (matches.length >= 2) {
|
|
307
|
+
const body = content.split('---').slice(2).join('---').trim();
|
|
308
|
+
if (body.length > 0 && body.length < 500) {
|
|
309
|
+
memories.push(`[Memory] ${body}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch { }
|
|
318
|
+
if (memories.length === 0)
|
|
319
|
+
return '';
|
|
320
|
+
const unique = [...new Set(memories)].slice(0, 8);
|
|
321
|
+
console.log(`[Memory] Surfaced ${unique.length} memories for: "${userMessage.substring(0, 40)}"`);
|
|
322
|
+
return '\n## Relevant Context from Memory\n' + unique.join('\n') + '\n';
|
|
323
|
+
}
|
|
324
|
+
// ── Template resolver ──────────────────────────────────────────
|
|
325
|
+
// Replaces {{step_N_output}} tokens with actual step outputs
|
|
326
|
+
function resolveTemplates(input, stepOutputs) {
|
|
327
|
+
return input.replace(/\{\{step_(\d+)_output\}\}/g, (_match, n) => {
|
|
328
|
+
const idx = parseInt(n, 10);
|
|
329
|
+
return stepOutputs[idx] ?? `(step ${idx} output unavailable)`;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// ── SSE stream helpers ─────────────────────────────────────────
|
|
333
|
+
async function streamOpenAIResponse(res, onToken) {
|
|
334
|
+
if (!res.body)
|
|
335
|
+
return;
|
|
336
|
+
const reader = res.body.getReader();
|
|
337
|
+
const decoder = new TextDecoder();
|
|
338
|
+
let buf = '';
|
|
339
|
+
while (true) {
|
|
340
|
+
const { done, value } = await reader.read();
|
|
341
|
+
if (done)
|
|
342
|
+
break;
|
|
343
|
+
buf += decoder.decode(value, { stream: true });
|
|
344
|
+
const lines = buf.split('\n');
|
|
345
|
+
buf = lines.pop() ?? '';
|
|
346
|
+
for (const line of lines) {
|
|
347
|
+
if (!line.startsWith('data: '))
|
|
348
|
+
continue;
|
|
349
|
+
const raw = line.replace('data: ', '').trim();
|
|
350
|
+
if (raw === '[DONE]')
|
|
351
|
+
return;
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(raw);
|
|
354
|
+
const token = parsed?.choices?.[0]?.delta?.content;
|
|
355
|
+
if (token)
|
|
356
|
+
onToken(token);
|
|
357
|
+
}
|
|
358
|
+
catch { }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function streamGeminiResponse(res, onToken) {
|
|
363
|
+
// Gemini streaming with ?alt=sse returns SSE events with data: prefix
|
|
364
|
+
if (!res.body)
|
|
365
|
+
return;
|
|
366
|
+
const reader = res.body.getReader();
|
|
367
|
+
const decoder = new TextDecoder();
|
|
368
|
+
let buf = '';
|
|
369
|
+
while (true) {
|
|
370
|
+
const { done, value } = await reader.read();
|
|
371
|
+
if (done)
|
|
372
|
+
break;
|
|
373
|
+
buf += decoder.decode(value, { stream: true });
|
|
374
|
+
const lines = buf.split('\n');
|
|
375
|
+
buf = lines.pop() ?? '';
|
|
376
|
+
for (const line of lines) {
|
|
377
|
+
if (!line.startsWith('data: '))
|
|
378
|
+
continue;
|
|
379
|
+
const raw = line.replace('data: ', '').trim();
|
|
380
|
+
try {
|
|
381
|
+
const parsed = JSON.parse(raw);
|
|
382
|
+
const text = parsed?.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
383
|
+
if (text)
|
|
384
|
+
onToken(text);
|
|
385
|
+
}
|
|
386
|
+
catch { }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// ── Provider endpoint map ──────────────────────────────────────
|
|
391
|
+
const OPENAI_COMPAT_ENDPOINTS = {
|
|
392
|
+
groq: 'https://api.groq.com/openai/v1/chat/completions',
|
|
393
|
+
openrouter: 'https://openrouter.ai/api/v1/chat/completions',
|
|
394
|
+
cerebras: 'https://api.cerebras.ai/v1/chat/completions',
|
|
395
|
+
nvidia: 'https://integrate.api.nvidia.com/v1/chat/completions',
|
|
396
|
+
github: 'https://models.inference.ai.azure.com/v1/chat/completions',
|
|
397
|
+
boa: 'https://api.bayofassets.com/v1/chat/completions',
|
|
398
|
+
};
|
|
399
|
+
function buildHeaders(providerName, apiKey) {
|
|
400
|
+
const headers = {
|
|
401
|
+
'Content-Type': 'application/json',
|
|
402
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
403
|
+
};
|
|
404
|
+
if (providerName === 'openrouter') {
|
|
405
|
+
headers['HTTP-Referer'] = 'http://localhost:3000';
|
|
406
|
+
headers['X-Title'] = 'DevOS';
|
|
407
|
+
}
|
|
408
|
+
return headers;
|
|
409
|
+
}
|
|
410
|
+
// ── Phase inference from tool steps ───────────────────────────
|
|
411
|
+
// Groups consecutive steps of the same capability type into phases.
|
|
412
|
+
function inferPhasesFromSteps(steps) {
|
|
413
|
+
const capabilityMap = {
|
|
414
|
+
web_search: 'research', fetch_page: 'research',
|
|
415
|
+
deep_research: 'research', fetch_url: 'research',
|
|
416
|
+
get_stocks: 'research',
|
|
417
|
+
open_browser: 'browsing', browser_click: 'browsing',
|
|
418
|
+
browser_extract: 'browsing', browser_type: 'browsing',
|
|
419
|
+
mouse_move: 'browsing', mouse_click: 'browsing',
|
|
420
|
+
keyboard_type: 'browsing', keyboard_press: 'browsing',
|
|
421
|
+
screenshot: 'browsing', screen_read: 'browsing',
|
|
422
|
+
vision_loop: 'browsing',
|
|
423
|
+
file_write: 'writing', file_read: 'reading',
|
|
424
|
+
file_list: 'reading', shell_exec: 'execution',
|
|
425
|
+
run_python: 'execution', run_node: 'execution',
|
|
426
|
+
system_info: 'execution', notify: 'execution',
|
|
427
|
+
clipboard_read: 'execution', clipboard_write: 'execution',
|
|
428
|
+
window_list: 'execution', window_focus: 'execution',
|
|
429
|
+
app_launch: 'execution', app_close: 'execution',
|
|
430
|
+
watch_folder: 'execution', watch_folder_list: 'execution',
|
|
431
|
+
};
|
|
432
|
+
const phaseNames = {
|
|
433
|
+
research: 'Research & Gather',
|
|
434
|
+
browsing: 'Browse & Extract',
|
|
435
|
+
writing: 'Write & Save',
|
|
436
|
+
reading: 'Read & Analyze',
|
|
437
|
+
execution: 'Execute Tasks',
|
|
438
|
+
delivery: 'Deliver Results',
|
|
439
|
+
};
|
|
440
|
+
const phases = [];
|
|
441
|
+
let currentCap = '';
|
|
442
|
+
let currentTools = [];
|
|
443
|
+
for (const step of steps) {
|
|
444
|
+
const cap = capabilityMap[step.tool] || 'execution';
|
|
445
|
+
if (cap !== currentCap && currentTools.length > 0) {
|
|
446
|
+
phases.push({
|
|
447
|
+
id: `phase_${phases.length + 1}`,
|
|
448
|
+
title: phaseNames[currentCap] || currentCap,
|
|
449
|
+
capabilities: [currentCap],
|
|
450
|
+
tools: [...currentTools],
|
|
451
|
+
});
|
|
452
|
+
currentTools = [];
|
|
453
|
+
}
|
|
454
|
+
currentCap = cap;
|
|
455
|
+
currentTools.push(step.tool);
|
|
456
|
+
}
|
|
457
|
+
if (currentTools.length > 0) {
|
|
458
|
+
phases.push({
|
|
459
|
+
id: `phase_${phases.length + 1}`,
|
|
460
|
+
title: phaseNames[currentCap] || currentCap,
|
|
461
|
+
capabilities: [currentCap],
|
|
462
|
+
tools: currentTools,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// Always end with a Deliver Results phase
|
|
466
|
+
phases.push({
|
|
467
|
+
id: `phase_${phases.length + 1}`,
|
|
468
|
+
title: 'Deliver Results',
|
|
469
|
+
capabilities: ['delivery'],
|
|
470
|
+
tools: ['respond'],
|
|
471
|
+
});
|
|
472
|
+
return phases;
|
|
473
|
+
}
|
|
474
|
+
// ── Keyword-based plan inference — fallback when LLM unavailable ──────
|
|
475
|
+
// Detects simple single-tool intents from the message text.
|
|
476
|
+
function inferPlanFromKeywords(message) {
|
|
477
|
+
const m = message.toLowerCase();
|
|
478
|
+
// notify
|
|
479
|
+
if (/send\s+(a\s+)?(desktop\s+)?notif|notify\s+me|desktop\s+alert/.test(m)) {
|
|
480
|
+
const msgMatch = message.match(/saying\s+(.+?)(?:\s*$)/i);
|
|
481
|
+
const notifMsg = msgMatch ? msgMatch[1].trim() : message;
|
|
482
|
+
return {
|
|
483
|
+
goal: message, requires_execution: true,
|
|
484
|
+
plan: [{ step: 1, tool: 'notify', input: { message: notifMsg }, description: 'Send notification' }],
|
|
485
|
+
phases: [],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
// file_read — matches "read the file /path/to/file", "read file C:\...", "tell me what it says"
|
|
489
|
+
const fileReadMatch = message.match(/read\s+(?:the\s+)?file\s+([^\s"']+)/i) ||
|
|
490
|
+
message.match(/read\s+([A-Z]:[/\\][^\s"']+)/i) ||
|
|
491
|
+
message.match(/read\s+(\/[^\s"']+\.\w{1,6})/i);
|
|
492
|
+
if (fileReadMatch) {
|
|
493
|
+
const filePath = fileReadMatch[1].trim();
|
|
494
|
+
return {
|
|
495
|
+
goal: message, requires_execution: true,
|
|
496
|
+
plan: [{ step: 1, tool: 'file_read', input: { path: filePath }, description: `Read ${filePath}` }],
|
|
497
|
+
phases: [],
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
// file_write — matches "write ... to /path/file"
|
|
501
|
+
const fileWriteMatch = message.match(/write\s+(.+?)\s+to\s+([^\s"']+\.\w{1,6})/i);
|
|
502
|
+
if (fileWriteMatch) {
|
|
503
|
+
const content = fileWriteMatch[1].trim();
|
|
504
|
+
const filePath = fileWriteMatch[2].trim();
|
|
505
|
+
return {
|
|
506
|
+
goal: message, requires_execution: true,
|
|
507
|
+
plan: [{ step: 1, tool: 'file_write', input: { path: filePath, content }, description: `Write to ${filePath}` }],
|
|
508
|
+
phases: [],
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// fetch_url — matches "Fetch https://...", "fetch http://...", "get https://..."
|
|
512
|
+
const fetchUrlMatch = message.match(/(?:fetch|get|open|load)\s+(https?:\/\/[^\s"']+)/i) ||
|
|
513
|
+
message.match(/(https?:\/\/[^\s"']+)/i);
|
|
514
|
+
if (fetchUrlMatch) {
|
|
515
|
+
const url = fetchUrlMatch[1].trim();
|
|
516
|
+
return {
|
|
517
|
+
goal: message, requires_execution: true,
|
|
518
|
+
plan: [{ step: 1, tool: 'fetch_url', input: { url }, description: `Fetch ${url}` }],
|
|
519
|
+
phases: [],
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
// web_search / search the web
|
|
523
|
+
if (/search\s+(the\s+)?web|web\s+search|look\s+up|find\s+info/.test(m)) {
|
|
524
|
+
const query = message.replace(/search\s+(the\s+)?web\s+(for\s+)?/i, '').replace(/look\s+up\s+/i, '').trim();
|
|
525
|
+
return {
|
|
526
|
+
goal: message, requires_execution: true,
|
|
527
|
+
plan: [{ step: 1, tool: 'web_search', input: { query: query || message }, description: 'Search' }],
|
|
528
|
+
phases: [],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
// get_stocks / stock gainers
|
|
532
|
+
if (/top\s+(gainers|losers|active)|nse\s+top|bse\s+top|stock\s+(market|data|gainers)|get\s+stocks/.test(m)) {
|
|
533
|
+
const isLosers = /loser/.test(m);
|
|
534
|
+
const market = /bse/.test(m) ? 'BSE' : 'NSE';
|
|
535
|
+
const type = isLosers ? 'losers' : /active/.test(m) ? 'active' : 'gainers';
|
|
536
|
+
return {
|
|
537
|
+
goal: message, requires_execution: true,
|
|
538
|
+
plan: [{ step: 1, tool: 'get_stocks', input: { market, type }, description: `Get ${market} top ${type}` }],
|
|
539
|
+
phases: [],
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// system_info
|
|
543
|
+
if (/system\s+info|hardware\s+info|what.{0,10}(cpu|ram|memory|os|specs)|show\s+system|computer\s+specs/.test(m)) {
|
|
544
|
+
return {
|
|
545
|
+
goal: message, requires_execution: true,
|
|
546
|
+
plan: [{ step: 1, tool: 'system_info', input: {}, description: 'Get system info' }],
|
|
547
|
+
phases: [],
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
// run_python / run_node fast-path intentionally removed.
|
|
551
|
+
// These tools require actual executable source code in their input, which we cannot
|
|
552
|
+
// fabricate from a natural-language description. If all LLMs are down we cannot
|
|
553
|
+
// generate code, so we fall through to null and let the caller handle gracefully.
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
// ── Sprint 5: Planner racing helper ──────────────────────────
|
|
557
|
+
// Fires top-2 available APIs simultaneously; returns first valid JSON string.
|
|
558
|
+
async function racePlannerAPIs(promptText, topN = 2) {
|
|
559
|
+
const cfg = (0, index_1.loadConfig)();
|
|
560
|
+
const candidates = [];
|
|
561
|
+
for (const cp of (cfg.customProviders ?? [])) {
|
|
562
|
+
if (!cp.enabled || !cp.baseUrl)
|
|
563
|
+
continue;
|
|
564
|
+
candidates.push({ provider: 'custom', model: cp.model, key: cp.apiKey, url: cp.baseUrl, tier: cp.tier ?? 99 });
|
|
565
|
+
}
|
|
566
|
+
for (const a of cfg.providers.apis) {
|
|
567
|
+
if (!a.enabled || a.rateLimited)
|
|
568
|
+
continue;
|
|
569
|
+
const k = a.key.startsWith('env:') ? (process.env[a.key.replace('env:', '')] || '') : a.key;
|
|
570
|
+
if (!k || !OPENAI_COMPAT_ENDPOINTS[a.provider])
|
|
571
|
+
continue;
|
|
572
|
+
candidates.push({ provider: a.provider, model: a.model, key: k, url: OPENAI_COMPAT_ENDPOINTS[a.provider], tier: a.tier ?? 50 });
|
|
573
|
+
}
|
|
574
|
+
const pool = candidates.sort((a, b) => a.tier - b.tier).slice(0, topN);
|
|
575
|
+
if (pool.length < 1)
|
|
576
|
+
return null;
|
|
577
|
+
const controllers = pool.map(() => new AbortController());
|
|
578
|
+
const callOne = async (entry, ctrl) => {
|
|
579
|
+
const messages = [{ role: 'user', content: promptText }];
|
|
580
|
+
const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${entry.key}` };
|
|
581
|
+
if (entry.provider !== 'custom') {
|
|
582
|
+
Object.assign(headers, buildHeaders(entry.provider, entry.key));
|
|
583
|
+
}
|
|
584
|
+
const r = await fetch(entry.url, {
|
|
585
|
+
method: 'POST',
|
|
586
|
+
headers,
|
|
587
|
+
body: JSON.stringify({ model: entry.model, messages, stream: false, max_tokens: 2000 }),
|
|
588
|
+
signal: AbortSignal.any([AbortSignal.timeout(45000), ctrl.signal]),
|
|
589
|
+
});
|
|
590
|
+
if (!r.ok)
|
|
591
|
+
throw new Error(`${entry.provider} ${r.status}`);
|
|
592
|
+
const d = await r.json();
|
|
593
|
+
const text = d?.choices?.[0]?.message?.content || '';
|
|
594
|
+
if (!text.trim() || !text.includes('{'))
|
|
595
|
+
throw new Error('no JSON');
|
|
596
|
+
return text;
|
|
597
|
+
};
|
|
598
|
+
const promises = pool.map((entry, i) => callOne(entry, controllers[i]).then(text => {
|
|
599
|
+
controllers.forEach((c, j) => { if (j !== i) {
|
|
600
|
+
try {
|
|
601
|
+
c.abort();
|
|
602
|
+
}
|
|
603
|
+
catch { }
|
|
604
|
+
} });
|
|
605
|
+
return text;
|
|
606
|
+
}));
|
|
607
|
+
try {
|
|
608
|
+
return await Promise.race(promises);
|
|
609
|
+
}
|
|
610
|
+
catch { }
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
// ── Compaction protection — critical files survive context reset ──
|
|
614
|
+
// When the sliding context window summarizes older messages, we re-inject
|
|
615
|
+
// these files word-for-word as a system message so identity and rules survive.
|
|
616
|
+
const COMPACTION_PROTECTED = [
|
|
617
|
+
'SOUL.md', // personality + boundaries
|
|
618
|
+
'STANDING_ORDERS.md', // persistent instructions
|
|
619
|
+
'LESSONS.md', // failure rules
|
|
620
|
+
'GOALS.md', // active goals
|
|
621
|
+
'USER.md', // user profile
|
|
622
|
+
];
|
|
623
|
+
async function rebuildContextAfterCompaction(contextHistory) {
|
|
624
|
+
const workspaceDir = nodePath.join(process.cwd(), 'workspace');
|
|
625
|
+
const protectedContent = [];
|
|
626
|
+
// Read all protected files
|
|
627
|
+
for (const filename of COMPACTION_PROTECTED) {
|
|
628
|
+
try {
|
|
629
|
+
const filepath = nodePath.join(workspaceDir, filename);
|
|
630
|
+
if (nodeFs.existsSync(filepath)) {
|
|
631
|
+
const content = nodeFs.readFileSync(filepath, 'utf-8');
|
|
632
|
+
if (content.trim()) {
|
|
633
|
+
protectedContent.push(`## ${filename}\n${content.trim()}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
catch { }
|
|
638
|
+
}
|
|
639
|
+
// Top 5 instincts by confidence (read directly from workspace/instincts.json)
|
|
640
|
+
let instinctCount = 0;
|
|
641
|
+
try {
|
|
642
|
+
const instinctsPath = nodePath.join(workspaceDir, 'instincts.json');
|
|
643
|
+
if (nodeFs.existsSync(instinctsPath)) {
|
|
644
|
+
const raw = JSON.parse(nodeFs.readFileSync(instinctsPath, 'utf-8'));
|
|
645
|
+
const topInsts = raw
|
|
646
|
+
.filter(i => i.status === 'active' && i.confidence >= 0.7)
|
|
647
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
648
|
+
.slice(0, 5);
|
|
649
|
+
if (topInsts.length > 0) {
|
|
650
|
+
const instinctText = topInsts
|
|
651
|
+
.map(i => `- ${i.action} (confidence: ${(i.confidence * 100).toFixed(0)}%)`)
|
|
652
|
+
.join('\n');
|
|
653
|
+
protectedContent.push(`## Active Instincts\n${instinctText}`);
|
|
654
|
+
instinctCount = topInsts.length;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
catch { }
|
|
659
|
+
if (protectedContent.length === 0)
|
|
660
|
+
return contextHistory;
|
|
661
|
+
console.log(`[Compaction] Protected ${COMPACTION_PROTECTED.length} files ` +
|
|
662
|
+
`+ ${instinctCount} instincts — re-injected into context`);
|
|
663
|
+
const protectedMessage = {
|
|
664
|
+
role: 'system',
|
|
665
|
+
content: `[PROTECTED CONTEXT — survives compaction]\n\n${protectedContent.join('\n\n---\n\n')}`,
|
|
666
|
+
};
|
|
667
|
+
return [protectedMessage, ...contextHistory];
|
|
668
|
+
}
|
|
669
|
+
// ── STEP 1: planWithLLM ────────────────────────────────────────
|
|
670
|
+
async function planWithLLM(message, history, apiKey, model, provider, memoryContext) {
|
|
671
|
+
// ── Pre-compact hook — fire at multiples of COMPACT_THRESHOLD ─
|
|
672
|
+
// Fires at 40, 80, 120 … to avoid triggering on every message after crossing 40.
|
|
673
|
+
if (history.length >= COMPACT_THRESHOLD && history.length % COMPACT_THRESHOLD === 0) {
|
|
674
|
+
(0, hooks_1.fireHook)('pre_compact', { historyLength: history.length, message }).catch(() => { });
|
|
675
|
+
}
|
|
676
|
+
// ── Vague goal detection — ask for clarification before planning ──
|
|
677
|
+
const VAGUE_PATTERNS = [/\bthe thing\b/i, /\bthe stuff\b/i, /\bthe place\b/i, /\bdo it\b$/i, /\bfix it\b$/i];
|
|
678
|
+
if (VAGUE_PATTERNS.some(p => p.test(message))) {
|
|
679
|
+
return {
|
|
680
|
+
goal: message,
|
|
681
|
+
requires_execution: false,
|
|
682
|
+
plan: [],
|
|
683
|
+
phases: [],
|
|
684
|
+
direct_response: 'I need more detail. What specifically should I do, with what, and where?',
|
|
685
|
+
reason: 'goal_too_vague',
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
// ── Recipe engine — YAML workflow definitions ─────────────────
|
|
689
|
+
// Check before LLM planning: if a recipe trigger matches, execute
|
|
690
|
+
// the structured workflow instead of the probabilistic planner.
|
|
691
|
+
const recipes = (0, recipeEngine_1.loadAllRecipes)();
|
|
692
|
+
const recipeMatch = (0, recipeEngine_1.matchRecipe)(message, recipes);
|
|
693
|
+
if (recipeMatch) {
|
|
694
|
+
try {
|
|
695
|
+
const recipeResult = await (0, recipeEngine_1.executeRecipe)(recipeMatch.recipe, recipeMatch.params);
|
|
696
|
+
return {
|
|
697
|
+
goal: message,
|
|
698
|
+
requires_execution: false,
|
|
699
|
+
plan: [],
|
|
700
|
+
phases: [],
|
|
701
|
+
direct_response: recipeResult.output || `Completed recipe: ${recipeMatch.recipe.name}`,
|
|
702
|
+
reason: `recipe:${recipeMatch.recipe.name}`,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
console.warn(`[Recipe] Execution failed for ${recipeMatch.recipe.name}: ${err} — falling through to LLM planner`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const ALLOWED_TOOLS = [
|
|
710
|
+
'web_search', 'fetch_page', 'open_browser', 'browser_extract',
|
|
711
|
+
'browser_click', 'browser_type', 'browser_screenshot', 'browser_scroll', 'browser_get_url',
|
|
712
|
+
'file_write', 'file_read',
|
|
713
|
+
'file_list', 'shell_exec', 'run_python', 'run_node',
|
|
714
|
+
'system_info', 'notify', 'deep_research', 'get_stocks',
|
|
715
|
+
'get_market_data', 'get_company_info', 'social_research',
|
|
716
|
+
'mouse_move', 'mouse_click', 'keyboard_type', 'keyboard_press',
|
|
717
|
+
'screenshot', 'screen_read', 'vision_loop', 'wait',
|
|
718
|
+
'code_interpreter_python', 'code_interpreter_node',
|
|
719
|
+
'clipboard_read', 'clipboard_write', 'window_list', 'window_focus',
|
|
720
|
+
'app_launch', 'app_close',
|
|
721
|
+
'watch_folder', 'watch_folder_list',
|
|
722
|
+
'send_file_local', 'receive_file_local',
|
|
723
|
+
'get_briefing',
|
|
724
|
+
'respond',
|
|
725
|
+
'clarify', 'todo', 'cronjob', 'vision_analyze',
|
|
726
|
+
'voice_speak', 'voice_transcribe', 'voice_clone', 'voice_design',
|
|
727
|
+
'lookup_skill', 'lookup_tool_schema',
|
|
728
|
+
'spawn', 'spawn_subagent', 'swarm',
|
|
729
|
+
...slashAsTool_1.SLASH_MIRROR_TOOL_NAMES,
|
|
730
|
+
];
|
|
731
|
+
// Sprint 13: append discovered MCP tools
|
|
732
|
+
const mcpToolNames = mcpClient_1.mcpClient.getAllCachedTools().map(t => t.name);
|
|
733
|
+
const allTools = mcpToolNames.length > 0
|
|
734
|
+
? [...ALLOWED_TOOLS, ...mcpToolNames]
|
|
735
|
+
: ALLOWED_TOOLS;
|
|
736
|
+
// Dynamic tool loading — filter to relevant tools per task category
|
|
737
|
+
// Reduces planner prompt from ~15K to ~3-5K tokens without losing capability.
|
|
738
|
+
// Validation (line ~898) still uses full allTools — filtering is prompt-only.
|
|
739
|
+
const categories = (0, toolRegistry_1.detectToolCategories)(message);
|
|
740
|
+
const categoryTools = (0, toolRegistry_1.getToolsForCategories)(categories);
|
|
741
|
+
// MCP tools always included; ALLOWED_TOOLS filtered by detected category
|
|
742
|
+
const plannerTools = allTools.filter(t => t.startsWith('mcp_') || categoryTools.includes(t));
|
|
743
|
+
console.log(`[Tools] ${plannerTools.length}/${allTools.length} tools loaded for categories: ${categories.join(', ')}`);
|
|
744
|
+
// Load any relevant skills to guide planning
|
|
745
|
+
const relevantSkills = skillLoader_1.skillLoader.findRelevant(message);
|
|
746
|
+
const skillContext = skillLoader_1.skillLoader.formatForPrompt(relevantSkills);
|
|
747
|
+
// Append instinct context to memory (micro-patterns learned from past tool calls)
|
|
748
|
+
const instinctCtx = instinctSystem_1.instinctSystem?.getRelevantInstincts(message) || '';
|
|
749
|
+
const fullMemCtx = (memoryContext || '') + (instinctCtx ? '\n\n' + instinctCtx : '');
|
|
750
|
+
// Build memory section — inject when available
|
|
751
|
+
const memorySection = fullMemCtx.trim()
|
|
752
|
+
? `\n\nCONVERSATION MEMORY (use to resolve references like "that file", "the report", "it"):\n${fullMemCtx}\n\nWhen the user says "that file", "the report", "the script" etc., use the paths/queries above to resolve them into concrete values in your plan inputs.\n`
|
|
753
|
+
: '';
|
|
754
|
+
// Build learning context — past experiences with similar tasks
|
|
755
|
+
const learningCtx = learningMemory_1.learningMemory.buildLearningContext(message);
|
|
756
|
+
const learningSection = learningCtx ? `\n${learningCtx}\n` : '';
|
|
757
|
+
// Build knowledge context — relevant chunks from user's knowledge base files
|
|
758
|
+
const knowledgeCtxPlanner = knowledgeBase_1.knowledgeBase.buildContext(message);
|
|
759
|
+
const knowledgeSection = knowledgeCtxPlanner
|
|
760
|
+
? `\n\n${knowledgeCtxPlanner}\n`
|
|
761
|
+
: '';
|
|
762
|
+
// LESSONS.md — permanent failure rules, injected every session
|
|
763
|
+
const lessonsContent = loadLessons();
|
|
764
|
+
const lessonsSection = lessonsContent
|
|
765
|
+
? `\n\nPERMANENT FAILURE RULES (learned from past task failures — follow strictly):\n${lessonsContent.split('\n').filter(l => /^\d+\./.test(l.trim())).map(l => ` ${l.trim()}`).join('\n')}\n`
|
|
766
|
+
: '';
|
|
767
|
+
// Sprint 21: unified memory recall — only when message references past context
|
|
768
|
+
// Gate prevents unnecessary hybrid-search I/O on routine messages
|
|
769
|
+
let memoryRecallSection = '';
|
|
770
|
+
if ((0, skillLoader_1.needsMemory)(message)) {
|
|
771
|
+
try {
|
|
772
|
+
const recalled = await (0, memoryRecall_1.unifiedMemoryRecall)(message, 5);
|
|
773
|
+
const memoryInjected = (0, memoryRecall_1.buildMemoryInjection)(recalled);
|
|
774
|
+
if (memoryInjected) {
|
|
775
|
+
memoryRecallSection = memoryInjected;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch { }
|
|
779
|
+
}
|
|
780
|
+
// N+27: inject distilled facts from past sessions into planner context
|
|
781
|
+
// N+33: also inject smart-sliced Honcho user profile (replaces dumb full-dump)
|
|
782
|
+
let distilledFactsSection = '';
|
|
783
|
+
try {
|
|
784
|
+
const factHits = semanticMemory_1.semanticMemory.search(message, 5, 0.3)
|
|
785
|
+
.filter((r) => r.metadata?.type === 'fact')
|
|
786
|
+
.slice(0, 5);
|
|
787
|
+
if (factHits.length > 0) {
|
|
788
|
+
const factLines = factHits
|
|
789
|
+
.map((r) => `- ${r.text ?? ''}`)
|
|
790
|
+
.filter((l) => l.length > 3)
|
|
791
|
+
.join('\n');
|
|
792
|
+
if (factLines.trim()) {
|
|
793
|
+
distilledFactsSection = `\n\nREMEMBERED CONTEXT (facts distilled from past sessions — use to resolve references and avoid repeating work):\n${factLines}\n`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch { }
|
|
798
|
+
// N+33: smart Honcho profile slice injection (zero LLM cost — regex classifier)
|
|
799
|
+
let honchoProfileSection = '';
|
|
800
|
+
try {
|
|
801
|
+
const { formatForPrompt } = await Promise.resolve().then(() => __importStar(require('./userProfile')));
|
|
802
|
+
honchoProfileSection = await formatForPrompt(message);
|
|
803
|
+
}
|
|
804
|
+
catch { }
|
|
805
|
+
// Resolve the actual Windows username and home directory at runtime
|
|
806
|
+
const _sysUsername = process.env.USERNAME || process.env.USER || nodeOs.userInfo().username || 'User';
|
|
807
|
+
const _sysHomedir = nodeOs.homedir();
|
|
808
|
+
const plannerPrompt = `You are DevOS Planner. Analyze the user request and output a JSON plan.
|
|
809
|
+
|
|
810
|
+
GOAL DECOMPOSITION: Before writing your plan, count the distinct intents in the user message.
|
|
811
|
+
If the message contains 2 or more distinct goals (e.g., "search X AND write a file", "do A then B", "1. … 2. …"), add a "goals" array to your JSON listing each goal as a short phrase (max 8 words each). Your plan MUST cover ALL listed goals — do not silently drop any.
|
|
812
|
+
Single-goal messages: omit "goals" or leave it as an empty array.
|
|
813
|
+
|
|
814
|
+
SYSTEM CONTEXT — use these exact values for all file paths:
|
|
815
|
+
- Windows username: ${_sysUsername}
|
|
816
|
+
- Home directory: ${_sysHomedir}
|
|
817
|
+
- Desktop: ${nodePath.join(_sysHomedir, 'Desktop')}
|
|
818
|
+
- Documents: ${nodePath.join(_sysHomedir, 'Documents')}
|
|
819
|
+
- Downloads: ${nodePath.join(_sysHomedir, 'Downloads')}
|
|
820
|
+
IMPORTANT: NEVER use "C:\\Users\\Aiden" — "Aiden" is the AI assistant's name, NOT the Windows username. Always use "${_sysUsername}" as the username in any path.
|
|
821
|
+
|
|
822
|
+
CRITICAL RULES:
|
|
823
|
+
1. If the answer is in your training data (capitals, definitions, facts, opinions, advice) → requires_execution: false
|
|
824
|
+
2. ONLY use tools when you need: live data, file operations, running code, or computer control
|
|
825
|
+
3. AVAILABLE TOOLS (use ONLY these — name: one-liner):
|
|
826
|
+
${plannerTools.map(t => ` ${t}: ${toolRegistry_1.TOOL_NAMES_ONLY[t] ?? ''}`).join('\n')}
|
|
827
|
+
For full parameter schema: call lookup_tool_schema({ toolName: "name" })
|
|
828
|
+
Tier-0 (no lookup needed): web_search, notify, lookup_skill, lookup_tool_schema, schedule_reminder, file_read, file_write, respond
|
|
829
|
+
4. DO NOT invent tools like "identify_top_3", "generate_report", "analyze" — these don't exist
|
|
830
|
+
5. Processing/analysis happens in your response — NOT as a tool step
|
|
831
|
+
6. NEVER use placeholders like "{{result}}" or "{output}" — steps must have real concrete inputs
|
|
832
|
+
7. For multi-step tasks: if step N+1 needs step N's output, use the literal string "PREVIOUS_OUTPUT"
|
|
833
|
+
CRITICAL: Step 1 CANNOT use "PREVIOUS_OUTPUT" — there is no previous step. Step 1 must always have a literal concrete input value (e.g. a real URL, search query, or file path).
|
|
834
|
+
8. Output ONLY valid JSON — no text before or after
|
|
835
|
+
|
|
836
|
+
SCHEDULER (CRITICAL): You have a real persistent scheduler. When the user asks for a reminder, alarm, or time-delayed action ("remind me in N seconds/minutes/hours", "in N minutes do X", "every day at..."):
|
|
837
|
+
- You MUST call schedule_reminder — this is the ONLY correct path.
|
|
838
|
+
- Params: message (what to say), delaySeconds calculated from the user's request (e.g. "10 minutes" → delaySeconds: 600)
|
|
839
|
+
- For recurring reminders add: recurring: "hourly" | "daily" | "weekly"
|
|
840
|
+
- After scheduling, confirm with the exact fire time (e.g. "Done — I'll remind you at 3:45 PM.")
|
|
841
|
+
- To see pending reminders: schedule_reminder with op: "list"
|
|
842
|
+
- To cancel: schedule_reminder with op: "cancel" and the reminder id
|
|
843
|
+
- STRICTLY FORBIDDEN — these are ALL wrong and must NEVER appear in a reminder plan:
|
|
844
|
+
• Using wait in a loop (e.g. wait(5000) × N) — this blocks the whole system
|
|
845
|
+
• Using run_node or run_python with setTimeout/sleep to simulate a delay
|
|
846
|
+
• Saying "Waiting N seconds..." in a respond step and then firing notify
|
|
847
|
+
• Responding inline with the reminder message instead of scheduling it
|
|
848
|
+
schedule_reminder fires a real desktop notification asynchronously — set it and respond immediately.
|
|
849
|
+
|
|
850
|
+
RUN_AGENT HONESTY: run_agent executes inline — the result comes directly in your next response. NEVER tell the user "your research is being processed", "the agent is working in background", or "results will be ready soon". If you use run_agent, the answer is available immediately in the same response turn.
|
|
851
|
+
|
|
852
|
+
SUBAGENTS (CRITICAL):
|
|
853
|
+
Use spawn_subagent when the user's task has independent parallel sub-questions (e.g., "research X AND summarize Y AND find Z"):
|
|
854
|
+
- Each spawn_subagent call runs an isolated agent with its own context and half your remaining iteration budget
|
|
855
|
+
- Spawn returns the subagent's synthesized answer — it is available immediately, not in the background
|
|
856
|
+
- After spawning, synthesize all results into a unified final response, clearly attributing: "From a parallel research subagent: <result>"
|
|
857
|
+
|
|
858
|
+
When NOT to use spawn_subagent:
|
|
859
|
+
- Simple linear tasks (plan the steps yourself)
|
|
860
|
+
- Single-tool questions (just call the tool)
|
|
861
|
+
- Quick lookups (respond directly)
|
|
862
|
+
|
|
863
|
+
NEVER say "the subagent is working in background" — spawn_subagent is synchronous and returns before your response.
|
|
864
|
+
|
|
865
|
+
WHEN TO USE TOOLS vs NOT:
|
|
866
|
+
✅ Use tools for:
|
|
867
|
+
- Weather, news, current prices → web_search
|
|
868
|
+
- Opening websites → open_browser
|
|
869
|
+
- Writing/reading files → file_write, file_read
|
|
870
|
+
- Running code → run_python, run_node
|
|
871
|
+
- System info → system_info
|
|
872
|
+
- Research with real sources → deep_research
|
|
873
|
+
- Git repo state (status, branch, commits, changes) → git_status — ALWAYS run the tool, never answer from training data
|
|
874
|
+
- Compound tasks needing multiple steps (fetch + process + save) → run
|
|
875
|
+
|
|
876
|
+
## When to use the run tool
|
|
877
|
+
|
|
878
|
+
For compound tasks that need multiple steps, prefer run over separate tool calls.
|
|
879
|
+
Write JavaScript that composes the aiden SDK:
|
|
880
|
+
|
|
881
|
+
aiden.web.search(query), aiden.file.write(path, content), aiden.shell.exec(cmd), etc.
|
|
882
|
+
|
|
883
|
+
This collapses what would be 5 LLM turns into 1. Much faster.
|
|
884
|
+
|
|
885
|
+
Example — instead of:
|
|
886
|
+
turn 1: web_search("hn top")
|
|
887
|
+
turn 2: fetch_url(article[0].url)
|
|
888
|
+
turn 3: web_search(related)
|
|
889
|
+
turn 4: file_write(summary)
|
|
890
|
+
|
|
891
|
+
Use run:
|
|
892
|
+
const top = await aiden.web.search("hn top")
|
|
893
|
+
const article = await aiden.web.fetch(top[0].url)
|
|
894
|
+
const related = await aiden.web.search(article.title)
|
|
895
|
+
await aiden.file.write("/tmp/brief.md", ...)
|
|
896
|
+
|
|
897
|
+
❌ Do NOT use tools for:
|
|
898
|
+
- "What is the capital of X" → just answer
|
|
899
|
+
- "Who is [famous person]" → just answer
|
|
900
|
+
- "Explain X concept" → just answer
|
|
901
|
+
- "What do you think about X" → just answer
|
|
902
|
+
- Any question answerable from training knowledge
|
|
903
|
+
|
|
904
|
+
TOOL INPUT RULES (Tier-0 examples — for all others call lookup_tool_schema first):
|
|
905
|
+
- web_search: { "query": "specific search term" }
|
|
906
|
+
- notify: { "message": "text to show", "title": "optional title" }
|
|
907
|
+
- respond: { "message": "your reply text" }
|
|
908
|
+
- lookup_skill: { "query": "task description" }
|
|
909
|
+
- lookup_tool_schema: { "toolName": "tool_name" } — returns full description for any tool
|
|
910
|
+
- wait: { "ms": 2000 } — ONLY after browser/UI actions. Max 5000ms. NOT for reminders.
|
|
911
|
+
|
|
912
|
+
TOOL DISCOVERY: If you are unsure of a tool's parameters, call lookup_tool_schema FIRST (as step 1 of your plan) with the toolName, then use the returned description to build the real tool step.
|
|
913
|
+
|
|
914
|
+
COMPUTER CONTROL RULES — follow strictly when controlling mouse/keyboard/browser:
|
|
915
|
+
- ALWAYS use open_browser BEFORE keyboard_type or mouse_click on browser
|
|
916
|
+
- ALWAYS add a wait step of 2000ms after open_browser before any interaction
|
|
917
|
+
- For web searches: step 1 = open_browser(url), step 2 = wait(2000), step 3 = keyboard_press(ctrl+l), step 4 = keyboard_type(query), step 5 = keyboard_press(enter)
|
|
918
|
+
- For clicking browser address bar: use keyboard_press(ctrl+l) to focus it first
|
|
919
|
+
- After typing a URL: use keyboard_press(enter) to navigate
|
|
920
|
+
- For vision_loop tasks: set max_steps to at least 5
|
|
921
|
+
- Never assume the browser is already open — always open it first
|
|
922
|
+
- After any mouse_click: add wait(800) to let UI respond
|
|
923
|
+
|
|
924
|
+
URL RULES:
|
|
925
|
+
- Always use COMPLETE URLs — never truncate a URL in a tool input
|
|
926
|
+
- For market-wide queries (gainers, losers, most active) → use get_stocks, NOT web_search
|
|
927
|
+
- For individual stock price / market data → use get_market_data({ "symbol": "RELIANCE" })
|
|
928
|
+
- For company profile, financials, P/E ratio, EPS → use get_company_info({ "symbol": "RELIANCE" })
|
|
929
|
+
- Example: get_stocks({ "market": "NSE", "type": "gainers" })
|
|
930
|
+
|
|
931
|
+
OUTPUT FORMAT (strict JSON only):
|
|
932
|
+
{
|
|
933
|
+
"goal": "exact user request",
|
|
934
|
+
"goals": ["goal 1 short phrase", "goal 2 short phrase"],
|
|
935
|
+
"requires_execution": true,
|
|
936
|
+
"reasoning": "one sentence why",
|
|
937
|
+
"plan": [
|
|
938
|
+
{ "step": 1, "tool": "web_search", "input": { "query": "weather London today" }, "description": "Get London weather" }
|
|
939
|
+
]
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
CODE TOOL RULES — the "code" / "script" field MUST contain executable source, NEVER a description:
|
|
943
|
+
- run_python: { "tool": "run_python", "input": { "code": "def reverse(s):\n return s[::-1]\n\nprint(reverse('hello'))" } }
|
|
944
|
+
NOT: { "input": { "code": "a python script that reverses a string" } }
|
|
945
|
+
- run_node: { "tool": "run_node", "input": { "code": "console.log([1,2,3].map(x => x*2))" } }
|
|
946
|
+
NOT: { "input": { "code": "node script to double array elements" } }
|
|
947
|
+
- shell_exec: { "tool": "shell_exec", "input": { "command": "echo hello" } }
|
|
948
|
+
NOT: { "input": { "command": "a command that prints hello" } }
|
|
949
|
+
If the user says "write a python script that prints fibonacci numbers up to 10", the plan step must contain the actual working Python source code, not the user's English description.
|
|
950
|
+
|
|
951
|
+
If requires_execution is false:
|
|
952
|
+
{ "goal": "...", "requires_execution": false, "reasoning": "...", "plan": [], "direct_response": "your answer here" }
|
|
953
|
+
|
|
954
|
+
NOTE: "goals" is only required when 2+ distinct intents are present. Single-goal messages may omit it.
|
|
955
|
+
|
|
956
|
+
THE 'respond' TOOL — use this for ALL conversational messages:
|
|
957
|
+
- 'respond' is ALWAYS a valid plan. When no external tool is needed, plan a single respond step.
|
|
958
|
+
- respond: { "message": "your answer text here" }
|
|
959
|
+
- Use respond for: greetings, capability questions, simple facts from training data, clarifying questions, short answers.
|
|
960
|
+
- Example: user says "hi" → { "goal": "hi", "requires_execution": true, "plan": [{ "step": 1, "tool": "respond", "input": { "message": "Hi! What can I help you with today?" } }] }
|
|
961
|
+
|
|
962
|
+
ACTION GATE RULES — apply BEFORE creating any plan:
|
|
963
|
+
1. CAPABILITY GATE: If message is "Can you do X?" / "Can you X?" / "Are you able to X?" → plan respond with answer
|
|
964
|
+
2. EXPLICIT-ASK GATE: ONLY use file_write if user said "write", "save", "create file". ONLY use deep_research if user said "research", "find out", "look up"
|
|
965
|
+
3. VAGUENESS GATE: If request is AMBIGUOUS, plan a respond step that asks ONE clarifying question:
|
|
966
|
+
- "do marketing" → respond: "What specifically? Copywriting, competitor research, Product Hunt listing, or content calendar?"
|
|
967
|
+
- "check my system" → respond: "What aspect? Hardware specs, running processes, disk space, or network?"
|
|
968
|
+
- "build something" → respond: "What would you like me to build?"
|
|
969
|
+
- Clear requests execute directly: "check NIFTY price" → get_market_data, "write a Python script to X" → run_python
|
|
970
|
+
4. NEVER create comparison tables, reports, or verdicts unless user explicitly asked for them
|
|
971
|
+
5. NEVER mention Pega, BlueWinston, Gaude Digital, or any third-party product by name
|
|
972
|
+
|
|
973
|
+
## SKILL DISCOVERY
|
|
974
|
+
|
|
975
|
+
Before planning any multi-step task (>=2 tools), call lookup_skill with the user's message as the query.
|
|
976
|
+
- If it returns a skill, read the Steps section and follow that tool sequence as your plan.
|
|
977
|
+
- If it returns no match (score < threshold), proceed with normal planning.
|
|
978
|
+
- lookup_skill is Tier 1 — it is fast and free. Never skip it for multi-step tasks.
|
|
979
|
+
|
|
980
|
+
## Tool Priority Rules (STRICT)
|
|
981
|
+
|
|
982
|
+
TIER 1 (USE FIRST): lookup_skill, respond, web_search, fetch_page, fetch_url, deep_research, get_market_data, get_stocks, get_company_info, social_research, system_info, notify, get_briefing, run_agent
|
|
983
|
+
→ ALWAYS try these before anything else
|
|
984
|
+
→ If a task CAN be done via API/data tool, use that
|
|
985
|
+
|
|
986
|
+
TIER 2 (USE SECOND): file_write, file_read, file_list, shell_exec, run_powershell, run_python, run_node, code_interpreter_python, code_interpreter_node, git_status, git_commit, git_push, clipboard_read, clipboard_write, spawn_subagent, swarm
|
|
987
|
+
→ Use when you need to read/write files, run scripts, or run git commands
|
|
988
|
+
|
|
989
|
+
TIER 3 (USE THIRD): open_browser, browser_click, browser_type, browser_extract, browser_screenshot, window_list, window_focus, app_launch, app_close
|
|
990
|
+
→ ONLY when task requires interacting with a website UI
|
|
991
|
+
→ NEVER use browser when an API tool can do the same job
|
|
992
|
+
→ For other selectors always pass selector: "<css selector>", never guess at element text.
|
|
993
|
+
|
|
994
|
+
BROWSER CHAIN (CRITICAL): When the user wants to CONSUME content — not just see search results — you MUST emit a TWO-STEP plan:
|
|
995
|
+
Step 1: open_browser with the search/query URL
|
|
996
|
+
Step 2: browser_click with target: "first_result"
|
|
997
|
+
browser_click handles site-specific waiting and navigation automatically for: youtube.com, google.com, duckduckgo.com, bing.com.
|
|
998
|
+
|
|
999
|
+
Phrases that REQUIRE the chain (open_browser → browser_click first_result):
|
|
1000
|
+
• "play [song/video/anything]" — open YouTube search → click first result
|
|
1001
|
+
• "watch [anything]"
|
|
1002
|
+
• "open the article about X" / "open the top result"
|
|
1003
|
+
• "read about X" when it implies opening a page, not just searching
|
|
1004
|
+
• "find and play" / "find and read" / "find and open"
|
|
1005
|
+
• Any request where the user clearly wants to land on the content page
|
|
1006
|
+
|
|
1007
|
+
Phrases that do NOT require the chain (open_browser alone is fine):
|
|
1008
|
+
• "search for X" / "search YouTube for X"
|
|
1009
|
+
• "show me search results for X"
|
|
1010
|
+
• "look up X" / "find news about X"
|
|
1011
|
+
• "open youtube" / "go to google.com" (no specific content target)
|
|
1012
|
+
|
|
1013
|
+
When in doubt, chain the click — users want the content, not the search page.
|
|
1014
|
+
|
|
1015
|
+
TIER 4 (LAST RESORT): mouse_move, mouse_click, keyboard_type, keyboard_press, screenshot, screen_read, vision_loop
|
|
1016
|
+
→ ONLY when browser fails or for desktop apps with no API
|
|
1017
|
+
→ ALWAYS explain WHY lower tiers won't work
|
|
1018
|
+
|
|
1019
|
+
VIOLATIONS (these are WRONG — do not do these):
|
|
1020
|
+
- Using open_browser to check stock price when get_market_data exists
|
|
1021
|
+
- Using screenshot to search when web_search exists
|
|
1022
|
+
- Using browser to get weather when web_search exists
|
|
1023
|
+
- Using vision_loop for any task where a simpler tool works
|
|
1024
|
+
|
|
1025
|
+
FAILURE REPLANNING RULES (when message contains "previous approach failed at"):
|
|
1026
|
+
- Keep new plan to max 2 steps
|
|
1027
|
+
- Use ONLY the specific alternative approach mentioned in the message
|
|
1028
|
+
- DO NOT add web_search, deep_research, file_write, or notify unless directly needed
|
|
1029
|
+
- DO NOT add unrelated analysis or comparison steps
|
|
1030
|
+
${skillContext}${memorySection}${learningSection}${knowledgeSection}${memoryRecallSection}${distilledFactsSection}${honchoProfileSection}${lessonsSection}${(() => { const s = (0, goalTracker_1.getActiveGoalsSummary)(); return s ? `\n\n## Your Active Goals\n${s}` : ''; })()}
|
|
1031
|
+
Output ONLY valid JSON, nothing else:`;
|
|
1032
|
+
const cleanHistory = history
|
|
1033
|
+
.filter((h) => h.content && String(h.content).trim().length > 0);
|
|
1034
|
+
console.log(`[Planner] History: ${cleanHistory.length} messages (${history.length} raw)`);
|
|
1035
|
+
// ── Sliding context window — keep last 10, summarize older messages ──
|
|
1036
|
+
const RECENT_WINDOW = 10;
|
|
1037
|
+
let contextHistory = cleanHistory;
|
|
1038
|
+
if (cleanHistory.length > RECENT_WINDOW) {
|
|
1039
|
+
const recent = cleanHistory.slice(-RECENT_WINDOW);
|
|
1040
|
+
const older = cleanHistory.slice(Math.max(0, cleanHistory.length - RECENT_WINDOW * 2), cleanHistory.length - RECENT_WINDOW);
|
|
1041
|
+
if (older.length > 0) {
|
|
1042
|
+
try {
|
|
1043
|
+
const summaryInput = older.map((m) => `${m.role}: ${String(m.content).slice(0, 200)}`).join('\n');
|
|
1044
|
+
const summary = await callLLM(`Summarize these messages in 2-3 sentences, keeping key facts and decisions:\n\n${summaryInput}`, '', (0, router_1.getOllamaModelForTask)('executor'), 'ollama').catch(() => null);
|
|
1045
|
+
if (summary) {
|
|
1046
|
+
const compacted = [{ role: 'system', content: `Earlier conversation summary: ${summary}` }, ...recent];
|
|
1047
|
+
contextHistory = await rebuildContextAfterCompaction(compacted);
|
|
1048
|
+
console.log(`[Planner] Context window: summarized ${older.length} older messages`);
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
contextHistory = await rebuildContextAfterCompaction(recent);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
contextHistory = recent;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const messages = [
|
|
1060
|
+
{ role: 'system', content: plannerPrompt },
|
|
1061
|
+
...contextHistory.slice(-3).map((h) => ({
|
|
1062
|
+
role: h.role === 'assistant' ? 'assistant' : 'user',
|
|
1063
|
+
content: String(h.content).slice(0, 300),
|
|
1064
|
+
})),
|
|
1065
|
+
{ role: 'user', content: message },
|
|
1066
|
+
];
|
|
1067
|
+
// ── Sprint 6: Task-tiered provider selection ─────────────────
|
|
1068
|
+
// Always use the best reasoning model for planning, regardless of what
|
|
1069
|
+
// the caller passed in. Falls back to caller's values if tiering has nothing.
|
|
1070
|
+
{
|
|
1071
|
+
const tiered = (0, router_1.getModelForTask)('planner');
|
|
1072
|
+
if (tiered.apiKey || tiered.providerName === 'ollama') {
|
|
1073
|
+
apiKey = tiered.apiKey;
|
|
1074
|
+
model = tiered.model;
|
|
1075
|
+
provider = tiered.providerName;
|
|
1076
|
+
console.log(`[Planner] Sprint 6 tiering: using ${tiered.apiName} (${provider}/${model})`);
|
|
1077
|
+
}
|
|
1078
|
+
else if (!apiKey) {
|
|
1079
|
+
// Caller had nothing either — last resort Ollama
|
|
1080
|
+
const cfg = (0, index_1.loadConfig)();
|
|
1081
|
+
apiKey = '';
|
|
1082
|
+
model = cfg.model?.activeModel || 'mistral:7b';
|
|
1083
|
+
provider = 'ollama';
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
let curApiKey = apiKey;
|
|
1087
|
+
let curModel = model;
|
|
1088
|
+
let curProvider = provider;
|
|
1089
|
+
let curApiName = provider; // tracks the api entry name (e.g. 'groq-1') for markRateLimited
|
|
1090
|
+
{
|
|
1091
|
+
const tiered = (0, router_1.getModelForTask)('planner');
|
|
1092
|
+
if (tiered.apiKey || tiered.providerName === 'ollama') {
|
|
1093
|
+
curApiName = tiered.apiName;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
let raw = '';
|
|
1097
|
+
let parsed = null;
|
|
1098
|
+
// Cap at 3 cloud attempts — getModelForTask() handles provider rotation automatically
|
|
1099
|
+
// (marks failures → skips rate-limited → picks next tier). Walking all 12+ providers
|
|
1100
|
+
// serially at 5s each caused 60-120s cascade when most were rate-limited.
|
|
1101
|
+
// If all 3 fail, the Ollama fallback below catches it.
|
|
1102
|
+
const _cfg = (0, index_1.loadConfig)();
|
|
1103
|
+
const _customAsApi = (_cfg.customProviders ?? [])
|
|
1104
|
+
.filter((cp) => cp.enabled)
|
|
1105
|
+
.map((cp) => ({ ...cp, provider: 'custom', key: cp.apiKey, rateLimited: false, tier: cp.tier ?? 99 }));
|
|
1106
|
+
const _plannerChain = [
|
|
1107
|
+
..._cfg.providers.apis.filter((a) => a.enabled && a.provider !== 'ollama'),
|
|
1108
|
+
..._customAsApi,
|
|
1109
|
+
].sort((a, b) => (a.tier ?? 99) - (b.tier ?? 99));
|
|
1110
|
+
const _availableCount = _plannerChain.filter((a) => !a.rateLimited).length;
|
|
1111
|
+
const maxPlannerAttempts = _availableCount === 0 ? 0 : Math.min(3, _availableCount);
|
|
1112
|
+
for (let attempt = 0; attempt < maxPlannerAttempts; attempt++) {
|
|
1113
|
+
raw = ''; // reset each attempt so stale values don't bleed through
|
|
1114
|
+
try {
|
|
1115
|
+
// Sprint 5: on first attempt, race top-2 providers simultaneously
|
|
1116
|
+
if (attempt === 0) {
|
|
1117
|
+
const promptText = messages.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
1118
|
+
const raceRaw = await racePlannerAPIs(promptText).catch(() => null);
|
|
1119
|
+
if (raceRaw && raceRaw.trim().length > 0) {
|
|
1120
|
+
raw = raceRaw;
|
|
1121
|
+
console.log('[Planner] Race winner resolved');
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (!raw) {
|
|
1125
|
+
raw = await callLLM(messages.map(m => `${m.role}: ${m.content}`).join('\n'), curApiKey, curModel, curProvider);
|
|
1126
|
+
}
|
|
1127
|
+
if (!raw || raw.trim().length === 0) {
|
|
1128
|
+
console.warn(`[Planner] Empty response attempt ${attempt + 1} (${curApiName}) — marking and rotating`);
|
|
1129
|
+
try {
|
|
1130
|
+
(0, router_1.markRateLimited)(curApiName);
|
|
1131
|
+
}
|
|
1132
|
+
catch { }
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
const jsonMatch = raw.replace(/```json\s*/g, '').replace(/```\s*/g, '').match(/\{[\s\S]*\}/);
|
|
1136
|
+
if (!jsonMatch) {
|
|
1137
|
+
// Phase 1 — repair: try to salvage plain-text / fenced responses before retrying
|
|
1138
|
+
const repair = (0, planResponseRepair_1.repairPlanResponse)(raw);
|
|
1139
|
+
if (repair.plan) {
|
|
1140
|
+
console.log(`[Planner] Repaired non-JSON response — treating as ${repair.directAnswer ? 'direct answer' : 'recovered plan'}`);
|
|
1141
|
+
parsed = repair.plan;
|
|
1142
|
+
try {
|
|
1143
|
+
(0, router_1.incrementUsage)(curApiName);
|
|
1144
|
+
}
|
|
1145
|
+
catch { }
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
console.warn(`[Planner] No JSON attempt ${attempt + 1}: ${raw.slice(0, 100)}`);
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1152
|
+
try {
|
|
1153
|
+
(0, router_1.incrementUsage)(curApiName);
|
|
1154
|
+
}
|
|
1155
|
+
catch { }
|
|
1156
|
+
try {
|
|
1157
|
+
if (curApiName !== 'ollama')
|
|
1158
|
+
(0, router_1.markHealthy)(curApiName);
|
|
1159
|
+
}
|
|
1160
|
+
catch { }
|
|
1161
|
+
break; // success — exit retry loop
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
catch (e) {
|
|
1166
|
+
console.warn(`[Planner] Attempt ${attempt + 1} error (${curApiName}): ${e.message}`);
|
|
1167
|
+
if (e.message?.includes('timeout') ||
|
|
1168
|
+
e.message?.includes('429') ||
|
|
1169
|
+
e.message?.includes('rate') ||
|
|
1170
|
+
e.message?.includes('aborted')) {
|
|
1171
|
+
try {
|
|
1172
|
+
(0, router_1.markRateLimited)(curApiName);
|
|
1173
|
+
console.log(`[Planner] Marked ${curApiName} as rate limited — will rotate away`);
|
|
1174
|
+
}
|
|
1175
|
+
catch { }
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Wait before next attempt — helps with rate-limit recovery
|
|
1179
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1180
|
+
// Rotate to next best planner provider for this attempt
|
|
1181
|
+
try {
|
|
1182
|
+
const tiered = (0, router_1.getModelForTask)('planner');
|
|
1183
|
+
if (tiered.apiKey || tiered.providerName === 'ollama') {
|
|
1184
|
+
curApiKey = tiered.apiKey;
|
|
1185
|
+
curModel = tiered.model;
|
|
1186
|
+
curProvider = tiered.providerName;
|
|
1187
|
+
curApiName = tiered.apiName;
|
|
1188
|
+
console.log(`[Planner] Rotating (tiered) to ${tiered.apiName} (${curProvider}/${curModel})`);
|
|
1189
|
+
}
|
|
1190
|
+
else {
|
|
1191
|
+
const cfg = (0, index_1.loadConfig)();
|
|
1192
|
+
curApiKey = '';
|
|
1193
|
+
curModel = cfg.model?.activeModel || 'mistral:7b';
|
|
1194
|
+
curProvider = 'ollama';
|
|
1195
|
+
curApiName = 'ollama';
|
|
1196
|
+
console.log(`[Planner] No cloud APIs — falling back to Ollama (${curModel})`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
catch { }
|
|
1200
|
+
}
|
|
1201
|
+
if (!parsed) {
|
|
1202
|
+
// Final guaranteed attempt with Ollama before giving up
|
|
1203
|
+
// Discover which model is actually installed via api/tags
|
|
1204
|
+
try {
|
|
1205
|
+
const cfg = (0, index_1.loadConfig)();
|
|
1206
|
+
let ollamaModel = process.env.OLLAMA_MODEL || cfg.ollama?.model || 'gemma4:e4b';
|
|
1207
|
+
try {
|
|
1208
|
+
const _ollamaBase = (process.env.OLLAMA_HOST ?? 'http://127.0.0.1:11434').replace(/\/$/, '');
|
|
1209
|
+
const tagsRes = await fetch(`${_ollamaBase}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
1210
|
+
if (tagsRes.ok) {
|
|
1211
|
+
const tagsData = await tagsRes.json();
|
|
1212
|
+
const firstModel = tagsData?.models?.[0]?.name;
|
|
1213
|
+
if (firstModel) {
|
|
1214
|
+
ollamaModel = firstModel;
|
|
1215
|
+
console.log(`[Planner] Ollama model discovered via api/tags: ${ollamaModel}`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
catch { /* Ollama not running — use config model */ }
|
|
1220
|
+
console.log(`[Planner] All cloud attempts failed — final Ollama attempt (${ollamaModel})`);
|
|
1221
|
+
const raw = await callLLM(messages.map(m => `${m.role}: ${m.content}`).join('\n'), '', ollamaModel, 'ollama');
|
|
1222
|
+
if (raw && raw.trim().length > 0) {
|
|
1223
|
+
const jsonMatch = raw.replace(/```json\s*/g, '').replace(/```\s*/g, '').match(/\{[\s\S]*\}/);
|
|
1224
|
+
if (jsonMatch) {
|
|
1225
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1226
|
+
console.log('[Planner] Ollama fallback succeeded');
|
|
1227
|
+
}
|
|
1228
|
+
else {
|
|
1229
|
+
// Repair fallback — Ollama often returns plain text for trivial questions
|
|
1230
|
+
const repair = (0, planResponseRepair_1.repairPlanResponse)(raw);
|
|
1231
|
+
if (repair.plan) {
|
|
1232
|
+
parsed = repair.plan;
|
|
1233
|
+
console.log(`[Planner] Ollama fallback repaired — ${repair.directAnswer ? 'direct answer' : 'recovered plan'}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
catch (e) {
|
|
1239
|
+
console.warn(`[Planner] Ollama fallback failed: ${e.message}`);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (!parsed) {
|
|
1243
|
+
// Keyword-based plan generation — when all LLMs fail, infer tool from message
|
|
1244
|
+
const heuristicPlan = inferPlanFromKeywords(message);
|
|
1245
|
+
if (heuristicPlan) {
|
|
1246
|
+
console.log(`[Planner] Keyword-based plan: ${JSON.stringify(heuristicPlan.plan.map(s => s.tool))}`);
|
|
1247
|
+
parsed = heuristicPlan;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
if (!parsed) {
|
|
1251
|
+
console.warn('[Planner] All LLM attempts failed — respond fallback');
|
|
1252
|
+
return {
|
|
1253
|
+
goal: message,
|
|
1254
|
+
requires_execution: true,
|
|
1255
|
+
plan: [{ step: 1, tool: 'respond', input: { message: "I'm not sure how to help with that right now. Could you rephrase your request?" }, description: 'Fallback response' }],
|
|
1256
|
+
phases: [],
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
// Guard against null/empty plan object
|
|
1260
|
+
if (!parsed.plan && !parsed.steps) {
|
|
1261
|
+
return {
|
|
1262
|
+
goal: message,
|
|
1263
|
+
requires_execution: false,
|
|
1264
|
+
plan: [],
|
|
1265
|
+
phases: [],
|
|
1266
|
+
direct_response: parsed.direct_response || "I'll answer directly.",
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
// Validate tool names — reject hallucinated tools
|
|
1270
|
+
const rawPlan = (parsed.plan || parsed.steps || []);
|
|
1271
|
+
const validatedPlan = rawPlan.filter((s) => {
|
|
1272
|
+
if (!allTools.includes(s.tool)) {
|
|
1273
|
+
console.warn(`[Planner] Rejected invalid tool: ${s.tool}`);
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
// Reject old-style placeholder inputs
|
|
1277
|
+
const inputStr = JSON.stringify(s.input || s.args || {});
|
|
1278
|
+
if (inputStr.includes('{{') || inputStr.includes('{result') || inputStr.includes('{output')) {
|
|
1279
|
+
console.warn(`[Planner] Rejected placeholder input in: ${s.tool}`);
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
return true;
|
|
1283
|
+
});
|
|
1284
|
+
const normalizedPlan = validatedPlan.map((s, idx) => ({
|
|
1285
|
+
step: s.step ?? (idx + 1),
|
|
1286
|
+
tool: s.tool || '',
|
|
1287
|
+
input: s.input || s.args || {},
|
|
1288
|
+
description: s.description || '',
|
|
1289
|
+
}));
|
|
1290
|
+
// Fix step ordering — research before write
|
|
1291
|
+
const orderedPlan = fixStepOrdering(normalizedPlan);
|
|
1292
|
+
// Create phased task plan and workspace
|
|
1293
|
+
const phases = inferPhasesFromSteps(orderedPlan);
|
|
1294
|
+
const taskPlan = planTool_1.planTool.create(message, phases);
|
|
1295
|
+
const workspace = new workspaceMemory_1.WorkspaceMemory(taskPlan.id);
|
|
1296
|
+
workspace.write('goal.txt', message);
|
|
1297
|
+
const candidatePlan = {
|
|
1298
|
+
goal: parsed.goal || message,
|
|
1299
|
+
requires_execution: parsed.requires_execution === true && orderedPlan.length > 0,
|
|
1300
|
+
plan: orderedPlan,
|
|
1301
|
+
direct_response: parsed.direct_response,
|
|
1302
|
+
planId: taskPlan.id,
|
|
1303
|
+
workspaceDir: taskPlan.workspaceDir,
|
|
1304
|
+
phases: taskPlan.phases,
|
|
1305
|
+
};
|
|
1306
|
+
// Validate before returning — log warnings, strip hard-invalid steps
|
|
1307
|
+
const validation = validatePlan(candidatePlan);
|
|
1308
|
+
if (validation.warnings.length > 0) {
|
|
1309
|
+
console.warn(`[Planner] Validation warnings:\n ${validation.warnings.join('\n ')}`);
|
|
1310
|
+
// Carry repair log onto the plan so SSE clients can show ↺ repair events
|
|
1311
|
+
const repairWarnings = validation.warnings.filter(w => w.includes('auto-repaired'));
|
|
1312
|
+
if (repairWarnings.length > 0)
|
|
1313
|
+
candidatePlan.repairLog = repairWarnings;
|
|
1314
|
+
}
|
|
1315
|
+
if (!validation.valid) {
|
|
1316
|
+
console.warn(`[Planner] Plan has validation errors:\n ${validation.errors.join('\n ')}`);
|
|
1317
|
+
// One retry — ask the LLM to fix the plan
|
|
1318
|
+
console.log('[Planner] Retrying with validation errors injected into prompt...');
|
|
1319
|
+
const retryMessages = [
|
|
1320
|
+
...messages,
|
|
1321
|
+
{
|
|
1322
|
+
role: 'assistant',
|
|
1323
|
+
content: raw.slice(0, 500),
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
role: 'user',
|
|
1327
|
+
content: `The plan you produced has errors:\n${validation.errors.join('\n')}\n\nFix these issues and output a corrected JSON plan.`,
|
|
1328
|
+
},
|
|
1329
|
+
];
|
|
1330
|
+
try {
|
|
1331
|
+
const retryRaw = await callLLM(retryMessages.map(m => `${m.role}: ${m.content}`).join('\n'), curApiKey, curModel, curProvider);
|
|
1332
|
+
const retryMatch = retryRaw.replace(/```json\s*/g, '').replace(/```\s*/g, '').match(/\{[\s\S]*\}/);
|
|
1333
|
+
if (retryMatch) {
|
|
1334
|
+
const retryParsed = JSON.parse(retryMatch[0]);
|
|
1335
|
+
const retryRaw2 = (retryParsed.plan || retryParsed.steps || []);
|
|
1336
|
+
const retryValid = retryRaw2.filter((s) => allTools.includes(s.tool));
|
|
1337
|
+
const retryNorm = retryValid.map((s, idx) => ({
|
|
1338
|
+
step: s.step ?? (idx + 1),
|
|
1339
|
+
tool: s.tool || '',
|
|
1340
|
+
input: s.input || s.args || {},
|
|
1341
|
+
description: s.description || '',
|
|
1342
|
+
}));
|
|
1343
|
+
const retryOrdered = fixStepOrdering(retryNorm);
|
|
1344
|
+
if (retryOrdered.length > 0) {
|
|
1345
|
+
candidatePlan.plan = retryOrdered;
|
|
1346
|
+
console.log(`[Planner] Retry succeeded: ${retryOrdered.length} valid steps`);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
catch (e) {
|
|
1351
|
+
console.warn(`[Planner] Retry failed: ${e.message}`);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return candidatePlan;
|
|
1355
|
+
}
|
|
1356
|
+
// ── Plan validation ────────────────────────────────────────────
|
|
1357
|
+
// Called after planWithLLM — rejects structurally bad plans before execution.
|
|
1358
|
+
const VALID_TOOLS = [
|
|
1359
|
+
'web_search', 'fetch_page', 'fetch_url', 'open_browser', 'browser_extract',
|
|
1360
|
+
'browser_click', 'browser_type', 'browser_screenshot', 'browser_scroll', 'browser_get_url',
|
|
1361
|
+
'file_write', 'file_read',
|
|
1362
|
+
'file_list', 'shell_exec', 'run_python', 'run_node', 'run_powershell',
|
|
1363
|
+
'system_info', 'notify', 'deep_research', 'get_stocks', 'run_agent', 'git_commit',
|
|
1364
|
+
'git_push', 'get_market_data', 'get_company_info',
|
|
1365
|
+
'mouse_move', 'mouse_click', 'keyboard_type', 'keyboard_press',
|
|
1366
|
+
'screenshot', 'screen_read', 'vision_loop', 'wait',
|
|
1367
|
+
'code_interpreter_python', 'code_interpreter_node',
|
|
1368
|
+
'clipboard_read', 'clipboard_write', 'window_list', 'window_focus',
|
|
1369
|
+
'app_launch', 'app_close',
|
|
1370
|
+
'watch_folder', 'watch_folder_list',
|
|
1371
|
+
'send_file_local', 'receive_file_local',
|
|
1372
|
+
'clarify', 'todo', 'cronjob', 'vision_analyze',
|
|
1373
|
+
'voice_speak', 'voice_transcribe', 'voice_clone', 'voice_design',
|
|
1374
|
+
'lookup_skill', 'lookup_tool_schema',
|
|
1375
|
+
'spawn', 'spawn_subagent', 'swarm',
|
|
1376
|
+
...slashAsTool_1.SLASH_MIRROR_TOOL_NAMES,
|
|
1377
|
+
];
|
|
1378
|
+
function validatePlan(plan) {
|
|
1379
|
+
const errors = [];
|
|
1380
|
+
const warnings = [];
|
|
1381
|
+
if (!plan.requires_execution || plan.plan.length === 0) {
|
|
1382
|
+
return { valid: true, errors, warnings };
|
|
1383
|
+
}
|
|
1384
|
+
for (const step of plan.plan) {
|
|
1385
|
+
// Check tool name — attempt fuzzy repair before flagging as error
|
|
1386
|
+
if (!VALID_TOOLS.includes(step.tool)) {
|
|
1387
|
+
const repair = (0, toolNameRepair_1.repairToolName)(step.tool, VALID_TOOLS);
|
|
1388
|
+
if (repair) {
|
|
1389
|
+
warnings.push(`Step ${step.step}: auto-repaired tool "${repair.original}" → "${repair.repaired}" (edit distance ${repair.distance})`);
|
|
1390
|
+
console.log(`[ToolRepair] ↺ "${repair.original}" → "${repair.repaired}" (distance ${repair.distance})`);
|
|
1391
|
+
step.tool = repair.repaired; // mutate in-place — plan will execute with correct name
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
errors.push(`Step ${step.step}: unknown tool "${step.tool}"`);
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const input = step.input || {};
|
|
1399
|
+
// Tool-specific required field checks
|
|
1400
|
+
switch (step.tool) {
|
|
1401
|
+
case 'web_search':
|
|
1402
|
+
if (!input.query && !input.topic && !input.command) {
|
|
1403
|
+
errors.push(`Step ${step.step}: web_search requires a "query" field`);
|
|
1404
|
+
}
|
|
1405
|
+
break;
|
|
1406
|
+
case 'deep_research':
|
|
1407
|
+
if (!input.topic && !input.query && !input.command) {
|
|
1408
|
+
errors.push(`Step ${step.step}: deep_research requires a "topic" field`);
|
|
1409
|
+
}
|
|
1410
|
+
break;
|
|
1411
|
+
case 'file_write':
|
|
1412
|
+
if (!input.path && !input.file) {
|
|
1413
|
+
errors.push(`Step ${step.step}: file_write requires a "path" field`);
|
|
1414
|
+
}
|
|
1415
|
+
if (input.content === undefined && input.content !== '') {
|
|
1416
|
+
warnings.push(`Step ${step.step}: file_write has no "content" — will write empty file`);
|
|
1417
|
+
}
|
|
1418
|
+
break;
|
|
1419
|
+
case 'file_read':
|
|
1420
|
+
if (!input.path && !input.file) {
|
|
1421
|
+
errors.push(`Step ${step.step}: file_read requires a "path" field`);
|
|
1422
|
+
}
|
|
1423
|
+
break;
|
|
1424
|
+
case 'open_browser':
|
|
1425
|
+
if (!input.url && !input.command) {
|
|
1426
|
+
errors.push(`Step ${step.step}: open_browser requires a "url" field`);
|
|
1427
|
+
}
|
|
1428
|
+
break;
|
|
1429
|
+
case 'shell_exec':
|
|
1430
|
+
if (!input.command && !input.cmd) {
|
|
1431
|
+
errors.push(`Step ${step.step}: shell_exec requires a "command" field`);
|
|
1432
|
+
}
|
|
1433
|
+
break;
|
|
1434
|
+
case 'run_python':
|
|
1435
|
+
case 'run_node':
|
|
1436
|
+
if (!input.script && !input.code && !input.command) {
|
|
1437
|
+
errors.push(`Step ${step.step}: ${step.tool} requires a "script" field`);
|
|
1438
|
+
}
|
|
1439
|
+
break;
|
|
1440
|
+
case 'fetch_page':
|
|
1441
|
+
case 'fetch_url':
|
|
1442
|
+
if (!input.url && !input.command) {
|
|
1443
|
+
errors.push(`Step ${step.step}: ${step.tool} requires a "url" field`);
|
|
1444
|
+
}
|
|
1445
|
+
break;
|
|
1446
|
+
case 'vision_loop':
|
|
1447
|
+
if (!input.goal) {
|
|
1448
|
+
errors.push(`Step ${step.step}: vision_loop requires a "goal" field`);
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
case 'wait':
|
|
1452
|
+
if (!input.ms && input.ms !== 0) {
|
|
1453
|
+
warnings.push(`Step ${step.step}: wait has no "ms" — will default to 1000ms`);
|
|
1454
|
+
}
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
// Reject residual placeholder patterns that were not caught by planner
|
|
1458
|
+
const inputStr = JSON.stringify(input);
|
|
1459
|
+
if (/\{\{|\{result|\{output|\bPREVIOUS_OUTPUT\b/.test(inputStr) && step.tool !== 'file_write') {
|
|
1460
|
+
if (step.step === 1) {
|
|
1461
|
+
warnings.push(`Step 1: PREVIOUS_OUTPUT is invalid for the first step (no prior output). Provide a literal input.`);
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
warnings.push(`Step ${step.step}: input contains placeholder — may fail at runtime`);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
return {
|
|
1469
|
+
valid: errors.length === 0,
|
|
1470
|
+
errors,
|
|
1471
|
+
warnings,
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
// ── Smart replan on failure ────────────────────────────────────
|
|
1475
|
+
const MAX_REPLANS = 2;
|
|
1476
|
+
async function handleToolFailure(replanState, failedTool, error, userMessage, completedResults, apiKey, model, provider) {
|
|
1477
|
+
const existing = replanState.failedSteps.get(failedTool);
|
|
1478
|
+
if (existing) {
|
|
1479
|
+
existing.attempts++;
|
|
1480
|
+
existing.error = error;
|
|
1481
|
+
}
|
|
1482
|
+
else {
|
|
1483
|
+
replanState.failedSteps.set(failedTool, { error, attempts: 1 });
|
|
1484
|
+
}
|
|
1485
|
+
if (replanState.replanCount >= MAX_REPLANS) {
|
|
1486
|
+
console.log('[Replan] Max replans reached — reporting failure');
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
replanState.replanCount++;
|
|
1490
|
+
const succeeded = completedResults.filter(r => r.success);
|
|
1491
|
+
const failed = Array.from(replanState.failedSteps.entries());
|
|
1492
|
+
console.log(`[Replan] Replanning (${replanState.replanCount}/${MAX_REPLANS}) after ${failedTool} failed: ${error.slice(0, 80)}`);
|
|
1493
|
+
const replanContext = `Previous approach failed. Use a DIFFERENT strategy.\n\n` +
|
|
1494
|
+
`Original request: ${userMessage}\n\n` +
|
|
1495
|
+
`Already completed:\n` +
|
|
1496
|
+
(succeeded.map(s => `✅ ${s.tool}: ${s.output.substring(0, 100)}`).join('\n') || 'Nothing yet') +
|
|
1497
|
+
`\n\nWhat failed:\n` +
|
|
1498
|
+
failed.map(([tool, f]) => `❌ ${tool}: ${f.error} (tried ${f.attempts}x)`).join('\n') +
|
|
1499
|
+
`\n\nRULES:\n` +
|
|
1500
|
+
`- Do NOT retry ${failedTool} with same approach\n` +
|
|
1501
|
+
`- Use a completely different tool or strategy\n` +
|
|
1502
|
+
`- Build on completed steps — don't redo them\n` +
|
|
1503
|
+
`- If API failed, try different data source\n` +
|
|
1504
|
+
`- If browser failed on a site, try fetch_url instead`;
|
|
1505
|
+
try {
|
|
1506
|
+
const newPlan = await planWithLLM(replanContext, [], apiKey, model, provider);
|
|
1507
|
+
if (newPlan?.plan?.length > 0) {
|
|
1508
|
+
console.log(`[Replan] New plan: ${newPlan.plan.map(s => s.tool).join(' → ')}`);
|
|
1509
|
+
return newPlan.plan;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
catch (e) {
|
|
1513
|
+
console.warn(`[Replan] planWithLLM failed: ${e.message}`);
|
|
1514
|
+
}
|
|
1515
|
+
return null;
|
|
1516
|
+
}
|
|
1517
|
+
// ── Sprint 28: shouldReplan ────────────────────────────────────
|
|
1518
|
+
// After each failed step, ask the LLM: should we replan?
|
|
1519
|
+
async function shouldReplan(originalGoal, completedSteps, failedStep, failureReason, apiKey, model, provider) {
|
|
1520
|
+
const prompt = `You are replanning a failed task.
|
|
1521
|
+
|
|
1522
|
+
Original goal: "${originalGoal}"
|
|
1523
|
+
|
|
1524
|
+
Steps completed so far:
|
|
1525
|
+
${completedSteps.map((s, i) => `${i + 1}. ${s.tool}: ${s.success ? 'succeeded' : 'failed'}`).join('\n') || 'None'}
|
|
1526
|
+
|
|
1527
|
+
Failed step: ${failedStep.tool}
|
|
1528
|
+
Failure reason: ${failureReason}
|
|
1529
|
+
|
|
1530
|
+
Should I replan with a different approach, or retry the same step?
|
|
1531
|
+
|
|
1532
|
+
Respond in JSON only:
|
|
1533
|
+
{
|
|
1534
|
+
"replan": true/false,
|
|
1535
|
+
"reason": "why",
|
|
1536
|
+
"newApproach": "describe the new approach if replanning, or null"
|
|
1537
|
+
}`;
|
|
1538
|
+
try {
|
|
1539
|
+
const raw = await callLLM(prompt, apiKey, model, provider);
|
|
1540
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
1541
|
+
const parsed = JSON.parse(match?.[0] || '{}');
|
|
1542
|
+
return { replan: parsed.replan === true, newApproach: parsed.newApproach || undefined };
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
return { replan: false };
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
// ── STEP 2: executePlan ────────────────────────────────────────
|
|
1549
|
+
// ── validateResultQuality — lightweight output sanity check ──
|
|
1550
|
+
function validateResultQuality(tool, input, output) {
|
|
1551
|
+
if (tool === 'web_search') {
|
|
1552
|
+
if (!output || output === '[]' || output === 'No results') {
|
|
1553
|
+
return { valid: false, reason: 'Empty search results' };
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
const results = typeof output === 'string' ? JSON.parse(output) : output;
|
|
1557
|
+
if (Array.isArray(results) && results.length === 0) {
|
|
1558
|
+
return { valid: false, reason: 'Zero search results' };
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
catch { }
|
|
1562
|
+
}
|
|
1563
|
+
if (tool === 'fetch_url' || tool === 'fetch_page') {
|
|
1564
|
+
const text = String(output).toLowerCase();
|
|
1565
|
+
if (text.includes('404') && text.includes('not found')) {
|
|
1566
|
+
return { valid: false, reason: '404 page returned' };
|
|
1567
|
+
}
|
|
1568
|
+
if (text.includes('403') && text.includes('forbidden')) {
|
|
1569
|
+
return { valid: false, reason: '403 forbidden' };
|
|
1570
|
+
}
|
|
1571
|
+
if (text.length < 50) {
|
|
1572
|
+
return { valid: false, reason: 'Suspiciously short page content' };
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
if (tool === 'get_market_data') {
|
|
1576
|
+
const text = String(output);
|
|
1577
|
+
if (text.includes('error') || text.includes('failed') || text.includes('null')) {
|
|
1578
|
+
return { valid: false, reason: 'Market data returned error' };
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
if (tool === 'file_read') {
|
|
1582
|
+
if (!output || String(output).trim().length === 0) {
|
|
1583
|
+
return { valid: false, reason: 'Empty file content' };
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (tool === 'run_python' || tool === 'run_node' || tool === 'shell_exec') {
|
|
1587
|
+
const text = String(output).toLowerCase();
|
|
1588
|
+
if (text.includes('traceback') || text.includes('error:') ||
|
|
1589
|
+
text.includes('exception') || text.includes('syntaxerror')) {
|
|
1590
|
+
return { valid: false, reason: 'Code execution error in output' };
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
if (tool === 'open_browser') {
|
|
1594
|
+
const text = String(output).toLowerCase();
|
|
1595
|
+
if (text.includes('err_') || text.includes('timed out') ||
|
|
1596
|
+
text.includes('cannot navigate')) {
|
|
1597
|
+
return { valid: false, reason: 'Browser navigation failed' };
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return { valid: true };
|
|
1601
|
+
}
|
|
1602
|
+
// ── LESSONS.md — permanent failure rules ──────────────────────
|
|
1603
|
+
// Auto-appended on task failure. Injected into every planning session.
|
|
1604
|
+
const LESSONS_PATH = nodePath.join(process.cwd(), 'workspace', 'LESSONS.md');
|
|
1605
|
+
const LESSONS_CAP = 50;
|
|
1606
|
+
const LESSONS_SUMMARIZE_AT = 25; // when cap exceeded, summarize oldest N lessons
|
|
1607
|
+
function loadLessons() {
|
|
1608
|
+
try {
|
|
1609
|
+
if (!nodeFs.existsSync(LESSONS_PATH))
|
|
1610
|
+
return '';
|
|
1611
|
+
return nodeFs.readFileSync(LESSONS_PATH, 'utf-8');
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
return '';
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function appendLesson(lesson) {
|
|
1618
|
+
try {
|
|
1619
|
+
nodeFs.mkdirSync(nodePath.dirname(LESSONS_PATH), { recursive: true });
|
|
1620
|
+
const today = new Date().toISOString().split('T')[0];
|
|
1621
|
+
const newLine = `\n${lesson.startsWith('[') ? lesson : `[${today}] ${lesson}`}`;
|
|
1622
|
+
let content = nodeFs.existsSync(LESSONS_PATH)
|
|
1623
|
+
? nodeFs.readFileSync(LESSONS_PATH, 'utf-8')
|
|
1624
|
+
: '# LESSONS.md — Permanent Failure Rules\n\n## Rules\n';
|
|
1625
|
+
// Count existing lesson lines (numbered lines in ## Rules section)
|
|
1626
|
+
const lessonLines = content
|
|
1627
|
+
.split('\n')
|
|
1628
|
+
.filter(l => /^\d+\./.test(l.trim()));
|
|
1629
|
+
if (lessonLines.length >= LESSONS_CAP) {
|
|
1630
|
+
// Summarize oldest LESSONS_SUMMARIZE_AT lessons into 5 consolidated rules
|
|
1631
|
+
console.log(`[Lessons] Cap reached (${lessonLines.length}). Summarizing oldest ${LESSONS_SUMMARIZE_AT} lessons.`);
|
|
1632
|
+
const oldest = lessonLines.slice(0, LESSONS_SUMMARIZE_AT);
|
|
1633
|
+
const remaining = lessonLines.slice(LESSONS_SUMMARIZE_AT);
|
|
1634
|
+
const summarized = [
|
|
1635
|
+
`[consolidated] Avoid retrying tools that fail with permission or auth errors — report immediately.`,
|
|
1636
|
+
`[consolidated] When web_search returns empty, rephrase with different keywords before retrying.`,
|
|
1637
|
+
`[consolidated] Do not use error-string outputs as valid data — fall back to alternative tools.`,
|
|
1638
|
+
`[consolidated] When replan is triggered repeatedly for the same goal, stop and report.`,
|
|
1639
|
+
`[consolidated] Browser navigation failures (ERR_/timeout) require a fresh approach, not a retry.`,
|
|
1640
|
+
];
|
|
1641
|
+
const headerLines = content.split('\n').filter(l => !(/^\d+\./.test(l.trim())));
|
|
1642
|
+
const newRules = [...summarized, ...remaining, lesson.startsWith('[') ? lesson : `[${today}] ${lesson}`];
|
|
1643
|
+
const numbered = newRules.map((r, i) => `${i + 1}. ${r.replace(/^\d+\.\s*/, '')}`);
|
|
1644
|
+
const headerText = headerLines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
1645
|
+
content = `${headerText}\n\n${numbered.join('\n')}\n`;
|
|
1646
|
+
console.log(`[Lessons] Summarized ${oldest.length} old lessons → 5 rules. Total: ${numbered.length}`);
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
const nextNum = lessonLines.length + 1;
|
|
1650
|
+
content = content.trimEnd() + `\n${nextNum}.${newLine}\n`;
|
|
1651
|
+
}
|
|
1652
|
+
nodeFs.writeFileSync(LESSONS_PATH, content, 'utf-8');
|
|
1653
|
+
console.log(`[Lessons] Appended: ${lesson.slice(0, 80)}`);
|
|
1654
|
+
}
|
|
1655
|
+
catch (e) {
|
|
1656
|
+
console.error('[Lessons] Failed to append lesson:', e.message);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
// ── executeToolWithRetry — step-level retry with exponential backoff ──
|
|
1660
|
+
// Tools that mutate state are excluded from retry to prevent double-execution.
|
|
1661
|
+
const NO_RETRY_TOOLS = new Set([
|
|
1662
|
+
'shell_exec', 'run_python', 'run_node', 'notify',
|
|
1663
|
+
'mouse_click', 'keyboard_type', 'keyboard_press',
|
|
1664
|
+
'app_launch', 'app_close',
|
|
1665
|
+
'open_browser', 'browser_extract', 'browser_screenshot', 'browser_click', 'browser_type', 'browser_scroll', 'browser_get_url',
|
|
1666
|
+
]);
|
|
1667
|
+
async function executeToolWithRetry(tool, input, maxRetries = 2) {
|
|
1668
|
+
const retryable = !NO_RETRY_TOOLS.has(tool);
|
|
1669
|
+
const effectiveMax = retryable ? maxRetries : 0;
|
|
1670
|
+
for (let attempt = 0; attempt <= effectiveMax; attempt++) {
|
|
1671
|
+
try {
|
|
1672
|
+
const result = await (0, toolRegistry_1.executeTool)(tool, input);
|
|
1673
|
+
if (result.success) {
|
|
1674
|
+
const quality = validateResultQuality(tool, input, result.output || result);
|
|
1675
|
+
if (!quality.valid) {
|
|
1676
|
+
console.log(`[Quality] ${tool} returned but quality check failed: ${quality.reason}`);
|
|
1677
|
+
if (attempt < effectiveMax) {
|
|
1678
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
|
|
1679
|
+
console.log(`[Quality] Retrying ${tool} in ${delay}ms`);
|
|
1680
|
+
await new Promise(r => setTimeout(r, delay));
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
console.log(`[Quality] ${tool} — accepting low-quality result after ${effectiveMax} retries`);
|
|
1684
|
+
appendLesson(`${tool} produced low-quality output (${quality.reason}) after ${effectiveMax} retries — consider alternative approach for this tool.`);
|
|
1685
|
+
}
|
|
1686
|
+
return result;
|
|
1687
|
+
}
|
|
1688
|
+
if (attempt < effectiveMax) {
|
|
1689
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
|
|
1690
|
+
console.log(`[Exec] ${tool} failed, retrying in ${delay}ms (attempt ${attempt + 1}/${effectiveMax})`);
|
|
1691
|
+
await new Promise(r => setTimeout(r, delay));
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
return result;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
catch (error) {
|
|
1698
|
+
if (attempt >= effectiveMax)
|
|
1699
|
+
throw error;
|
|
1700
|
+
const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
|
|
1701
|
+
console.log(`[Exec] ${tool} threw error, retrying in ${delay}ms`);
|
|
1702
|
+
await new Promise(r => setTimeout(r, delay));
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
appendLesson(`${tool} failed after ${effectiveMax} retries — avoid this tool or approach for similar tasks.`);
|
|
1706
|
+
return { success: false, output: '', error: 'Max retries exceeded', duration: 0, retries: effectiveMax };
|
|
1707
|
+
}
|
|
1708
|
+
// —— Sprint 8: dependency-group builder ——————————————
|
|
1709
|
+
// Groups consecutive tool steps into batches: parallel-safe tools are
|
|
1710
|
+
// batched together; sequential tools break the batch.
|
|
1711
|
+
const PARALLEL_SAFE = new Set([
|
|
1712
|
+
'web_search', 'system_info', 'get_stocks', 'get_market_data',
|
|
1713
|
+
'social_research', 'fetch_url', 'fetch_page', 'get_company_info',
|
|
1714
|
+
'deep_research', 'code_interpreter_python', 'code_interpreter_node',
|
|
1715
|
+
'clipboard_read', 'window_list', 'watch_folder_list',
|
|
1716
|
+
'get_calendar', 'read_email', 'get_natural_events', 'ingest_youtube',
|
|
1717
|
+
]);
|
|
1718
|
+
const SEQUENTIAL_ONLY = new Set([
|
|
1719
|
+
'file_write', 'run_python', 'run_node', 'shell_exec',
|
|
1720
|
+
'open_browser', 'browser_click', 'browser_type', 'browser_extract',
|
|
1721
|
+
'mouse_move', 'mouse_click', 'keyboard_type', 'keyboard_press',
|
|
1722
|
+
'screenshot', 'screen_read', 'vision_loop', 'notify', 'wait',
|
|
1723
|
+
'clipboard_write', 'window_focus', 'app_launch', 'app_close',
|
|
1724
|
+
'watch_folder',
|
|
1725
|
+
]);
|
|
1726
|
+
function buildDependencyGroups(steps) {
|
|
1727
|
+
const groups = [];
|
|
1728
|
+
let currentGroup = [];
|
|
1729
|
+
for (const step of steps) {
|
|
1730
|
+
const inputStr = JSON.stringify(step.input || {});
|
|
1731
|
+
const dependsOnPrevious = inputStr.includes('PREVIOUS_OUTPUT') || SEQUENTIAL_ONLY.has(step.tool);
|
|
1732
|
+
if (PARALLEL_SAFE.has(step.tool) && !dependsOnPrevious) {
|
|
1733
|
+
currentGroup.push(step);
|
|
1734
|
+
}
|
|
1735
|
+
else {
|
|
1736
|
+
if (currentGroup.length > 0) {
|
|
1737
|
+
groups.push([...currentGroup]);
|
|
1738
|
+
currentGroup = [];
|
|
1739
|
+
}
|
|
1740
|
+
groups.push([step]);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
if (currentGroup.length > 0)
|
|
1744
|
+
groups.push(currentGroup);
|
|
1745
|
+
return groups;
|
|
1746
|
+
}
|
|
1747
|
+
async function executePlan(plan, onStep, onPhaseChange, existingState, replanApiKey, replanModel, replanProvider) {
|
|
1748
|
+
executionInterrupted = false; // reset on each new plan execution
|
|
1749
|
+
// ── Iteration budget ─────────────────────────────────────────
|
|
1750
|
+
const budget = {
|
|
1751
|
+
maxIterations: Math.max(plan.plan.length + 5, 15),
|
|
1752
|
+
currentIteration: 0,
|
|
1753
|
+
cautionThreshold: 0.7,
|
|
1754
|
+
warningThreshold: 0.9,
|
|
1755
|
+
};
|
|
1756
|
+
_activeBudget = budget;
|
|
1757
|
+
const results = [];
|
|
1758
|
+
const stepOutputs = {};
|
|
1759
|
+
const planStart = Date.now();
|
|
1760
|
+
const replanState = { failedSteps: new Map(), replanCount: 0 };
|
|
1761
|
+
console.log(`[ExecutePlan] Starting: ${plan.plan.length} steps, goal: "${plan.goal.slice(0, 60)}"`);
|
|
1762
|
+
// Workflow tracking — feed the Watch Mode node graph
|
|
1763
|
+
(0, workflowTracker_1.startWorkflow)(plan.goal);
|
|
1764
|
+
(0, workflowTracker_1.addNode)({ id: 'main', agent: 'aiden', label: plan.goal.slice(0, 50), status: 'active', toolCalls: 0, startedAt: Date.now() });
|
|
1765
|
+
// Workspace memory for persisting intermediate artifacts
|
|
1766
|
+
const workspace = plan.planId ? new workspaceMemory_1.WorkspaceMemory(plan.planId) : null;
|
|
1767
|
+
// Initialize or reuse persistent task state (enables crash recovery)
|
|
1768
|
+
const taskId = plan.planId || `task_${Date.now()}`;
|
|
1769
|
+
const state = existingState || taskState_1.taskStateManager.create(taskId, plan.goal, plan.plan.length, plan.planId);
|
|
1770
|
+
// Restore step outputs from already-completed steps so PREVIOUS_OUTPUT works on resume
|
|
1771
|
+
for (const savedStep of state.steps) {
|
|
1772
|
+
if (savedStep.status === 'completed' && savedStep.output) {
|
|
1773
|
+
stepOutputs[savedStep.index] = savedStep.output;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
// Maps each tool to its capability bucket (for phase transition detection)
|
|
1777
|
+
const capabilityMap = {
|
|
1778
|
+
web_search: 'research', fetch_page: 'research',
|
|
1779
|
+
deep_research: 'research', fetch_url: 'research',
|
|
1780
|
+
get_stocks: 'research',
|
|
1781
|
+
open_browser: 'browsing', browser_click: 'browsing',
|
|
1782
|
+
browser_extract: 'browsing', browser_type: 'browsing',
|
|
1783
|
+
mouse_move: 'browsing', mouse_click: 'browsing',
|
|
1784
|
+
keyboard_type: 'browsing', keyboard_press: 'browsing',
|
|
1785
|
+
screenshot: 'browsing', screen_read: 'browsing',
|
|
1786
|
+
vision_loop: 'browsing',
|
|
1787
|
+
file_write: 'writing', file_read: 'reading',
|
|
1788
|
+
file_list: 'reading', shell_exec: 'execution',
|
|
1789
|
+
run_python: 'execution', run_node: 'execution',
|
|
1790
|
+
system_info: 'execution', notify: 'execution',
|
|
1791
|
+
clipboard_read: 'execution', clipboard_write: 'execution',
|
|
1792
|
+
window_list: 'execution', window_focus: 'execution',
|
|
1793
|
+
app_launch: 'execution', app_close: 'execution',
|
|
1794
|
+
watch_folder: 'execution', watch_folder_list: 'execution',
|
|
1795
|
+
};
|
|
1796
|
+
let lastCapability = '';
|
|
1797
|
+
let currentPhaseIdx = 0;
|
|
1798
|
+
const totalPhases = plan.phases?.length || 1;
|
|
1799
|
+
// —— Sprint 8: single-step executor ————————————————————
|
|
1800
|
+
// Called by executePlan for both sequential (group.length===1) and parallel paths.
|
|
1801
|
+
async function executeSingleStep(step, stepOutputs, state, plan, workspace, onStep) {
|
|
1802
|
+
// BUDGET CHECK
|
|
1803
|
+
if (taskState_1.taskStateManager.isOverBudget(state)) {
|
|
1804
|
+
const budgetMsg = `Token budget exceeded (${state.tokenUsage}/${state.tokenLimit}) — task stopped`;
|
|
1805
|
+
console.warn(`[AgentLoop] ${budgetMsg}`);
|
|
1806
|
+
taskState_1.taskStateManager.fail(state, budgetMsg);
|
|
1807
|
+
return { step: step.step, tool: step.tool, input: step.input, success: false, output: '', error: budgetMsg, duration: 0 };
|
|
1808
|
+
}
|
|
1809
|
+
const totalSteps = plan.plan.length;
|
|
1810
|
+
const stepStart = Date.now();
|
|
1811
|
+
console.log(`[Exec] Step ${step.step}/${totalSteps}: ${step.tool} — RUNNING`);
|
|
1812
|
+
console.log(`[ExecutePlan] Step ${step.step}: ${step.tool} — input: ${JSON.stringify(step.input).slice(0, 100)}`);
|
|
1813
|
+
livePulse_1.livePulse.tool('Aiden', step.tool, JSON.stringify(step.input).slice(0, 80));
|
|
1814
|
+
// Validate tool exists
|
|
1815
|
+
if (!toolRegistry_1.TOOLS[step.tool]) {
|
|
1816
|
+
const stepResult = {
|
|
1817
|
+
step: step.step, tool: step.tool, input: step.input,
|
|
1818
|
+
success: false, output: '',
|
|
1819
|
+
error: `Tool "${step.tool}" does not exist. Available: ${Object.keys(toolRegistry_1.TOOLS).slice(0, 8).join(', ')}`,
|
|
1820
|
+
duration: 0,
|
|
1821
|
+
};
|
|
1822
|
+
onStep(step, stepResult);
|
|
1823
|
+
livePulse_1.livePulse.error('Aiden', `Invalid tool: ${step.tool}`);
|
|
1824
|
+
return stepResult;
|
|
1825
|
+
}
|
|
1826
|
+
// Tools that legitimately take zero input
|
|
1827
|
+
const NO_INPUT_TOOLS = ['system_info', 'screenshot', 'get_hardware', 'screen_read', 'vision_loop', 'health_check', 'respond'];
|
|
1828
|
+
if (!NO_INPUT_TOOLS.includes(step.tool)) {
|
|
1829
|
+
if (!step.input || Object.keys(step.input).length === 0) {
|
|
1830
|
+
console.log(`[ExecutePlan] Skipping step ${step.step} (${step.tool}) — empty input`);
|
|
1831
|
+
return { step: step.step, tool: step.tool, input: step.input, success: false, output: '', error: 'empty input', duration: 0 };
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
// Resolve PREVIOUS_OUTPUT and {{step_N_output}} tokens
|
|
1835
|
+
let resolvedInput = resolvePreviousOutput(step.input, stepOutputs, step.step);
|
|
1836
|
+
// Mark step started in persistent state
|
|
1837
|
+
taskState_1.taskStateManager.startStep(state, step.step, step.tool, resolvedInput);
|
|
1838
|
+
// Emit status before tool execution
|
|
1839
|
+
emitStatus(TOOL_ACTION[step.tool] ?? 'tooling', toolStatusDetail(step.tool, resolvedInput));
|
|
1840
|
+
// Execute the tool (step-level retry + per-tool timeout)
|
|
1841
|
+
let toolResult = await executeToolWithRetry(step.tool, resolvedInput);
|
|
1842
|
+
// file_write fallback — retry at Desktop if original path failed
|
|
1843
|
+
if (!toolResult.success && step.tool === 'file_write' && resolvedInput.path) {
|
|
1844
|
+
const desktopPath = nodePath.join(nodeOs.homedir(), 'Desktop', nodePath.basename(resolvedInput.path));
|
|
1845
|
+
if (desktopPath !== resolvedInput.path) {
|
|
1846
|
+
livePulse_1.livePulse.error('Aiden', `file_write failed — retrying at Desktop: ${desktopPath}`);
|
|
1847
|
+
const fallback = await (0, toolRegistry_1.executeTool)('file_write', { ...resolvedInput, path: desktopPath });
|
|
1848
|
+
if (fallback.success) {
|
|
1849
|
+
toolResult = { ...fallback, output: fallback.output + ' (saved to Desktop)' };
|
|
1850
|
+
resolvedInput = { ...resolvedInput, path: desktopPath };
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (toolResult.retries > 0) {
|
|
1855
|
+
livePulse_1.livePulse.act('Aiden', `${step.tool} succeeded after ${toolResult.retries} retry(s)`);
|
|
1856
|
+
}
|
|
1857
|
+
let stepResult = {
|
|
1858
|
+
step: step.step, tool: step.tool, input: resolvedInput,
|
|
1859
|
+
success: toolResult.success,
|
|
1860
|
+
output: toolResult.output || '',
|
|
1861
|
+
error: toolResult.error,
|
|
1862
|
+
duration: toolResult.duration,
|
|
1863
|
+
};
|
|
1864
|
+
// Persist significant outputs to workspace
|
|
1865
|
+
if (toolResult.success && workspace && toolResult.output.length > 300) {
|
|
1866
|
+
workspace.write(`step_${step.step}_${step.tool}.txt`, toolResult.output);
|
|
1867
|
+
}
|
|
1868
|
+
// Verify file_write actually landed on disk
|
|
1869
|
+
if (toolResult.success && step.tool === 'file_write') {
|
|
1870
|
+
const targetPath = resolvedInput.path || '';
|
|
1871
|
+
if (targetPath && !nodeFs.existsSync(targetPath)) {
|
|
1872
|
+
stepResult.success = false;
|
|
1873
|
+
stepResult.error = `Verification failed: file not found at ${targetPath}`;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const execStatus = stepResult.success ? 'SUCCESS' : 'FAILED';
|
|
1877
|
+
const execDuration = Date.now() - stepStart;
|
|
1878
|
+
console.log(`[Exec] Step ${step.step}/${totalSteps}: ${step.tool} — ${execStatus} (${execDuration}ms)`);
|
|
1879
|
+
if (!stepResult.success) {
|
|
1880
|
+
console.log(`[Exec] Step ${step.step}: ${step.tool} — FAILED after ${toolResult.retries ?? 0} retries: ${stepResult.error || 'unknown error'}`);
|
|
1881
|
+
}
|
|
1882
|
+
console.log(`[ExecutePlan] Step ${step.step} result: ${stepResult.success ? '✓' : '✗'} ${stepResult.error || stepResult.output?.slice(0, 80) || ''}`);
|
|
1883
|
+
console.log(`[Tool] ${step.tool} (Tier ${(0, toolRegistry_1.getToolTier)(step.tool)}) — ${stepResult.duration}ms`);
|
|
1884
|
+
stepOutputs[step.step] = stepResult.output;
|
|
1885
|
+
(0, workflowTracker_1.updateNode)('main', { currentTool: step.tool, toolCalls: Object.keys(stepOutputs).length, tier: (0, toolRegistry_1.getToolTier)(step.tool), status: 'active' });
|
|
1886
|
+
// Persist step to executions log for crash recovery / audit
|
|
1887
|
+
try {
|
|
1888
|
+
const execDir = nodePath.join(process.cwd(), 'workspace', 'executions');
|
|
1889
|
+
nodeFs.mkdirSync(execDir, { recursive: true });
|
|
1890
|
+
const execFile = nodePath.join(execDir, `exec_${state.id}.json`);
|
|
1891
|
+
const existing = nodeFs.existsSync(execFile)
|
|
1892
|
+
? JSON.parse(nodeFs.readFileSync(execFile, 'utf8'))
|
|
1893
|
+
: { id: `exec_${state.id}`, goal: plan.goal, steps: [], status: 'in_progress', startedAt: Date.now() };
|
|
1894
|
+
existing.steps = existing.steps.filter((s) => s.step !== step.step);
|
|
1895
|
+
existing.steps.push({
|
|
1896
|
+
step: step.step,
|
|
1897
|
+
tool: step.tool,
|
|
1898
|
+
status: stepResult.success ? 'success' : 'failed',
|
|
1899
|
+
duration: execDuration,
|
|
1900
|
+
timestamp: new Date().toISOString(),
|
|
1901
|
+
error: stepResult.error,
|
|
1902
|
+
});
|
|
1903
|
+
existing.totalDuration = Date.now() - (existing.startedAt || Date.now());
|
|
1904
|
+
nodeFs.writeFileSync(execFile, JSON.stringify(existing, null, 2));
|
|
1905
|
+
}
|
|
1906
|
+
catch { /* non-blocking — never crash the agent loop */ }
|
|
1907
|
+
onStep(step, stepResult);
|
|
1908
|
+
// Audit trail
|
|
1909
|
+
auditTrail_1.auditTrail.record({
|
|
1910
|
+
action: 'tool',
|
|
1911
|
+
tool: step.tool,
|
|
1912
|
+
input: JSON.stringify(step.input).slice(0, 200),
|
|
1913
|
+
output: stepResult.output?.slice(0, 200),
|
|
1914
|
+
durationMs: stepResult.duration,
|
|
1915
|
+
success: stepResult.success,
|
|
1916
|
+
error: stepResult.error,
|
|
1917
|
+
goal: plan.goal,
|
|
1918
|
+
traceId: plan.planId,
|
|
1919
|
+
});
|
|
1920
|
+
// Fire after_tool_call hook (non-blocking) — feeds instinct system
|
|
1921
|
+
(0, hooks_1.fireHook)('after_tool_call', {
|
|
1922
|
+
toolName: step.tool,
|
|
1923
|
+
input: resolvedInput,
|
|
1924
|
+
success: stepResult.success,
|
|
1925
|
+
}).catch(() => { });
|
|
1926
|
+
// Persist step result to task state
|
|
1927
|
+
if (stepResult.success) {
|
|
1928
|
+
taskState_1.taskStateManager.completeStep(state, step.step, stepResult.output, stepResult.duration);
|
|
1929
|
+
livePulse_1.livePulse.done('Aiden', `${step.tool} ✓ ${stepResult.output.slice(0, 60)}`);
|
|
1930
|
+
}
|
|
1931
|
+
else {
|
|
1932
|
+
taskState_1.taskStateManager.failStep(state, step.step, stepResult.error || 'unknown error');
|
|
1933
|
+
livePulse_1.livePulse.error('Aiden', `${step.tool} failed: ${stepResult.error}`);
|
|
1934
|
+
}
|
|
1935
|
+
return stepResult;
|
|
1936
|
+
}
|
|
1937
|
+
// —— Sprint 8: group-based dispatch (parallel where safe) ———————————
|
|
1938
|
+
const groups = buildDependencyGroups(plan.plan);
|
|
1939
|
+
console.log(`[ExecutePlan] Dependency groups: ${groups.map(g => g.length === 1 ? g[0].tool : `[${g.map(s => s.tool).join('+')}]`).join(' → ')}`);
|
|
1940
|
+
if ((0, parallelExecutor_1.hasParallelism)(groups))
|
|
1941
|
+
console.log(`[ExecutePlan] Parallel execution enabled — ${groups.filter(g => g.length > 1).length} concurrent batch(es) detected`);
|
|
1942
|
+
let _gi = 0;
|
|
1943
|
+
while (_gi < groups.length) {
|
|
1944
|
+
const group = groups[_gi++];
|
|
1945
|
+
// Phase-transition detection — use first step of each group
|
|
1946
|
+
const thisCap = capabilityMap[group[0].tool] || 'execution';
|
|
1947
|
+
if (thisCap !== lastCapability && lastCapability !== '') {
|
|
1948
|
+
if (plan.planId) {
|
|
1949
|
+
planTool_1.planTool.advancePhase(plan.planId, `Completed ${lastCapability}`);
|
|
1950
|
+
currentPhaseIdx++;
|
|
1951
|
+
const nextPhase = planTool_1.planTool.getCurrentPhase(plan.planId);
|
|
1952
|
+
if (nextPhase && onPhaseChange) {
|
|
1953
|
+
onPhaseChange(nextPhase, currentPhaseIdx, totalPhases);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
lastCapability = thisCap;
|
|
1958
|
+
// Skip already-completed steps (crash recovery idempotency)
|
|
1959
|
+
const unskipped = group.filter(s => !taskState_1.taskStateManager.isStepCompleted(state, s.step));
|
|
1960
|
+
for (const s of group) {
|
|
1961
|
+
if (taskState_1.taskStateManager.isStepCompleted(state, s.step)) {
|
|
1962
|
+
console.log(`[AgentLoop] Step ${s.step} (${s.tool}) already completed — skipping`);
|
|
1963
|
+
const savedStep = state.steps.find(ss => ss.index === s.step);
|
|
1964
|
+
if (savedStep?.output)
|
|
1965
|
+
stepOutputs[s.step] = savedStep.output;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (unskipped.length === 0)
|
|
1969
|
+
continue;
|
|
1970
|
+
if (unskipped.length === 1) {
|
|
1971
|
+
// —— Sequential single step ————————————————
|
|
1972
|
+
const step = unskipped[0];
|
|
1973
|
+
// ── Budget: increment before execution ────────────────────────
|
|
1974
|
+
budget.currentIteration++;
|
|
1975
|
+
if (budget.currentIteration >= budget.maxIterations) {
|
|
1976
|
+
console.log('[Budget] Exhausted — forcing final response');
|
|
1977
|
+
const summary = results.filter(s => s.success)
|
|
1978
|
+
.map(s => `✓ ${s.tool}: ${String(s.output).substring(0, 100)}`).join('\n');
|
|
1979
|
+
results.push({
|
|
1980
|
+
step: step.step, tool: 'budget_exhausted', input: {},
|
|
1981
|
+
success: false, output: `I've reached my iteration limit. Here's what I completed:\n\n${summary}\n\nLet me know if you need me to continue.`,
|
|
1982
|
+
error: 'iteration budget exhausted', duration: 0,
|
|
1983
|
+
});
|
|
1984
|
+
break;
|
|
1985
|
+
}
|
|
1986
|
+
const stepResult = await executeSingleStep(step, stepOutputs, state, plan, workspace, onStep);
|
|
1987
|
+
stepOutputs[step.step] = stepResult.output;
|
|
1988
|
+
// ── Budget: append pressure warning to result output ──────────
|
|
1989
|
+
const budgetWarning = getBudgetWarning(budget);
|
|
1990
|
+
if (budgetWarning) {
|
|
1991
|
+
stepResult.output = stepResult.output + '\n\n' + budgetWarning;
|
|
1992
|
+
}
|
|
1993
|
+
results.push(stepResult);
|
|
1994
|
+
// ── Interrupt check ────────────────────────────────────────────
|
|
1995
|
+
if (executionInterrupted) {
|
|
1996
|
+
console.log('[AgentLoop] Execution interrupted by user — stopping early');
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
// ── Smart replan on failure ────────────────────────────────────
|
|
2000
|
+
if (!stepResult.success) {
|
|
2001
|
+
// Resolve credentials: prefer explicit params, then route through getNextAvailableAPI
|
|
2002
|
+
let _rpKey = replanApiKey || '';
|
|
2003
|
+
let _rpModel = replanModel || '';
|
|
2004
|
+
let _rpProvider = replanProvider || '';
|
|
2005
|
+
if (!_rpKey && !_rpModel) {
|
|
2006
|
+
try {
|
|
2007
|
+
const _next = (0, router_1.getNextAvailableAPI)();
|
|
2008
|
+
if (_next) {
|
|
2009
|
+
_rpKey = _next.entry.key.startsWith('env:')
|
|
2010
|
+
? (process.env[_next.entry.key.replace('env:', '')] || '')
|
|
2011
|
+
: _next.entry.key;
|
|
2012
|
+
_rpModel = _next.entry.model;
|
|
2013
|
+
_rpProvider = _next.entry.provider;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
catch { }
|
|
2017
|
+
}
|
|
2018
|
+
if (_rpKey || _rpProvider === 'ollama') {
|
|
2019
|
+
const newSteps = await handleToolFailure(replanState, step.tool, stepResult.error || 'unknown error', plan.goal, results, _rpKey, _rpModel, _rpProvider);
|
|
2020
|
+
if (newSteps && newSteps.length > 0) {
|
|
2021
|
+
livePulse_1.livePulse.act('Aiden', `Replanning with different strategy (${replanState.replanCount}/${MAX_REPLANS})`);
|
|
2022
|
+
auditTrail_1.auditTrail.record({
|
|
2023
|
+
action: 'system',
|
|
2024
|
+
tool: 'replan',
|
|
2025
|
+
input: `Failed: ${step.tool}`,
|
|
2026
|
+
output: `New plan: ${newSteps.map(s => s.tool).join(' → ')}`,
|
|
2027
|
+
durationMs: 0,
|
|
2028
|
+
success: true,
|
|
2029
|
+
goal: plan.goal,
|
|
2030
|
+
traceId: plan.planId,
|
|
2031
|
+
});
|
|
2032
|
+
const newGroups = buildDependencyGroups(newSteps);
|
|
2033
|
+
groups.splice(_gi, groups.length - _gi, ...newGroups);
|
|
2034
|
+
console.log(`[Replan] Spliced ${newGroups.length} new group(s) into execution from position ${_gi}`);
|
|
2035
|
+
}
|
|
2036
|
+
else if (replanState.replanCount >= MAX_REPLANS) {
|
|
2037
|
+
const failedList = Array.from(replanState.failedSteps.entries())
|
|
2038
|
+
.map(([tool, f]) => `- ${tool}: ${f.error}`)
|
|
2039
|
+
.join('\n');
|
|
2040
|
+
console.log(`[Replan] All ${MAX_REPLANS} replans exhausted for goal: "${plan.goal.slice(0, 60)}"`);
|
|
2041
|
+
appendLesson(`Replan exhausted (${MAX_REPLANS} attempts) for goal: "${plan.goal.slice(0, 80)}". Failed tools: ${Array.from(replanState.failedSteps.keys()).join(', ')}.`);
|
|
2042
|
+
results.push({
|
|
2043
|
+
step: step.step + 1, tool: 'replan_exhausted', input: {},
|
|
2044
|
+
success: false, output: '',
|
|
2045
|
+
error: `Tried ${MAX_REPLANS + 1} different approaches:\n${failedList}\n\nWould you like me to try a different approach?`,
|
|
2046
|
+
duration: 0,
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
else {
|
|
2053
|
+
// —— Parallel group ———————————————————————
|
|
2054
|
+
// Chunk oversized groups so we never exceed MAX_PARALLEL concurrent calls
|
|
2055
|
+
const chunks = unskipped.length > parallelExecutor_1.MAX_PARALLEL ? (0, parallelExecutor_1.chunkSteps)(unskipped, parallelExecutor_1.MAX_PARALLEL) : [unskipped];
|
|
2056
|
+
for (const chunk of chunks) {
|
|
2057
|
+
// ── Budget: one increment per parallel chunk ───────────────────
|
|
2058
|
+
budget.currentIteration++;
|
|
2059
|
+
livePulse_1.livePulse.act('Aiden', `Running ${chunk.length} steps in parallel: ${chunk.map(s => s.tool).join(', ')}`);
|
|
2060
|
+
// Emit parallel metadata onto workflow nodes before dispatch
|
|
2061
|
+
for (const s of chunk) {
|
|
2062
|
+
(0, workflowTracker_1.updateNode)(`step_${s.step}`, { parallel: true, groupSize: chunk.length });
|
|
2063
|
+
}
|
|
2064
|
+
const settled = await Promise.allSettled(chunk.map(step => executeSingleStep(step, stepOutputs, state, plan, workspace, onStep)));
|
|
2065
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
2066
|
+
const s = chunk[i];
|
|
2067
|
+
const result = settled[i];
|
|
2068
|
+
if (result.status === 'fulfilled') {
|
|
2069
|
+
stepOutputs[s.step] = result.value.output;
|
|
2070
|
+
results.push(result.value);
|
|
2071
|
+
}
|
|
2072
|
+
else {
|
|
2073
|
+
const errResult = {
|
|
2074
|
+
step: s.step, tool: s.tool, input: s.input,
|
|
2075
|
+
success: false, output: '', error: String(result.reason), duration: 0,
|
|
2076
|
+
};
|
|
2077
|
+
results.push(errResult);
|
|
2078
|
+
taskState_1.taskStateManager.failStep(state, s.step, errResult.error || 'parallel rejected');
|
|
2079
|
+
livePulse_1.livePulse.error('Aiden', `${s.tool} parallel rejected: ${result.reason}`);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
// Complete final phase
|
|
2086
|
+
if (plan.planId) {
|
|
2087
|
+
planTool_1.planTool.advancePhase(plan.planId, 'All steps completed');
|
|
2088
|
+
}
|
|
2089
|
+
// Finalize task state
|
|
2090
|
+
const allSucceeded = results.every(r => r.success);
|
|
2091
|
+
if (allSucceeded) {
|
|
2092
|
+
taskState_1.taskStateManager.complete(state);
|
|
2093
|
+
}
|
|
2094
|
+
else {
|
|
2095
|
+
const failed = results.filter(r => !r.success).map(r => r.tool).join(', ');
|
|
2096
|
+
taskState_1.taskStateManager.fail(state, failed ? `Steps failed: ${failed}` : 'Incomplete execution');
|
|
2097
|
+
}
|
|
2098
|
+
// Workflow tracking — close the node graph
|
|
2099
|
+
(0, workflowTracker_1.updateNode)('main', { status: allSucceeded ? 'completed' : 'failed', completedAt: Date.now() });
|
|
2100
|
+
(0, workflowTracker_1.completeWorkflow)(allSucceeded ? 'completed' : 'failed');
|
|
2101
|
+
// Record experience for self-learning
|
|
2102
|
+
const filesCreatedInPlan = results
|
|
2103
|
+
.filter(r => r.tool === 'file_write' && r.success && r.input?.path)
|
|
2104
|
+
.map(r => r.input.path)
|
|
2105
|
+
.filter(Boolean);
|
|
2106
|
+
learningMemory_1.learningMemory.record({
|
|
2107
|
+
task: plan.goal,
|
|
2108
|
+
success: allSucceeded,
|
|
2109
|
+
steps: results.map(r => r.tool),
|
|
2110
|
+
duration: Date.now() - planStart,
|
|
2111
|
+
tokenUsage: state.tokenUsage,
|
|
2112
|
+
filesCreated: filesCreatedInPlan,
|
|
2113
|
+
errorMessage: !allSucceeded
|
|
2114
|
+
? results.find(r => !r.success)?.error
|
|
2115
|
+
: undefined,
|
|
2116
|
+
});
|
|
2117
|
+
// Self-teaching — generate/update SKILL.md for this tool sequence
|
|
2118
|
+
const executedTools = results.map(r => r.tool);
|
|
2119
|
+
const totalDuration = results.reduce((s, r) => s + (r.duration || 0), 0);
|
|
2120
|
+
const anyFailed = results.some(r => !r.success);
|
|
2121
|
+
if (allSucceeded && executedTools.length > 0) {
|
|
2122
|
+
// GrowthEngine — record success for gap-resolution tracking
|
|
2123
|
+
growthEngine_1.growthEngine.logSuccess(plan.goal, executedTools);
|
|
2124
|
+
try {
|
|
2125
|
+
const next = (0, router_1.getNextAvailableAPI)();
|
|
2126
|
+
if (next) {
|
|
2127
|
+
const key = next.entry.key.startsWith('env:')
|
|
2128
|
+
? (process.env[next.entry.key.replace('env:', '')] || '')
|
|
2129
|
+
: next.entry.key;
|
|
2130
|
+
skillTeacher_1.skillTeacher.recordSuccess(plan.goal, executedTools, totalDuration, callLLM, key, next.entry.model, next.entry.provider).catch(() => { });
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
catch { }
|
|
2134
|
+
}
|
|
2135
|
+
else if (anyFailed) {
|
|
2136
|
+
// GrowthEngine — record failure with full error context
|
|
2137
|
+
const firstError = results.find(r => !r.success)?.error ?? 'Unknown error';
|
|
2138
|
+
growthEngine_1.growthEngine.logFailure(plan.goal, firstError, executedTools);
|
|
2139
|
+
skillTeacher_1.skillTeacher.recordFailure(plan.goal, executedTools);
|
|
2140
|
+
}
|
|
2141
|
+
// Execution summary
|
|
2142
|
+
const successCount = results.filter(r => r.success).length;
|
|
2143
|
+
const execTotalMs = Date.now() - planStart;
|
|
2144
|
+
console.log(`[Exec] Complete: ${successCount}/${results.length} steps succeeded in ${execTotalMs}ms`);
|
|
2145
|
+
// Finalize executions log
|
|
2146
|
+
try {
|
|
2147
|
+
const execFile = nodePath.join(process.cwd(), 'workspace', 'executions', `exec_${state.id}.json`);
|
|
2148
|
+
if (nodeFs.existsSync(execFile)) {
|
|
2149
|
+
const log = JSON.parse(nodeFs.readFileSync(execFile, 'utf8'));
|
|
2150
|
+
log.status = allSucceeded ? 'completed' : 'failed';
|
|
2151
|
+
log.totalDuration = execTotalMs;
|
|
2152
|
+
nodeFs.writeFileSync(execFile, JSON.stringify(log, null, 2));
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
catch { /* non-blocking */ }
|
|
2156
|
+
return results;
|
|
2157
|
+
}
|
|
2158
|
+
// ── Step ordering fixer ────────────────────────────────────────
|
|
2159
|
+
// Ensures research/fetch steps always run before file_write steps.
|
|
2160
|
+
// Prevents file_write from executing before deep_research has data.
|
|
2161
|
+
function fixStepOrdering(steps) {
|
|
2162
|
+
const researchTools = ['web_search', 'deep_research', 'fetch_url', 'fetch_page'];
|
|
2163
|
+
const writeTools = ['file_write'];
|
|
2164
|
+
const research = steps.filter(s => researchTools.includes(s.tool));
|
|
2165
|
+
const writes = steps.filter(s => writeTools.includes(s.tool));
|
|
2166
|
+
const others = steps.filter(s => !researchTools.includes(s.tool) && !writeTools.includes(s.tool));
|
|
2167
|
+
// Order: research → other → write — re-number steps
|
|
2168
|
+
return [...research, ...others, ...writes]
|
|
2169
|
+
.map((s, i) => ({ ...s, step: i + 1 }));
|
|
2170
|
+
}
|
|
2171
|
+
// Resolve PREVIOUS_OUTPUT and {{step_N_output}} in step inputs
|
|
2172
|
+
function resolvePreviousOutput(input, stepOutputs, currentStep) {
|
|
2173
|
+
const resolved = {};
|
|
2174
|
+
const lastOutput = stepOutputs[currentStep - 1] || '';
|
|
2175
|
+
// Step 1 with PREVIOUS_OUTPUT = planner bug. Log a warning and substitute with
|
|
2176
|
+
// empty string so the tool fails with a clear "no input" error rather than
|
|
2177
|
+
// passing the literal placeholder text to the API.
|
|
2178
|
+
if (currentStep === 1) {
|
|
2179
|
+
const inputStr = JSON.stringify(input);
|
|
2180
|
+
if (inputStr.includes('PREVIOUS_OUTPUT')) {
|
|
2181
|
+
console.warn('[Planner] Step 1 used PREVIOUS_OUTPUT — no previous output exists. Substituting empty string.');
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
for (const [key, value] of Object.entries(input)) {
|
|
2185
|
+
if (typeof value === 'string') {
|
|
2186
|
+
resolved[key] = value
|
|
2187
|
+
.replace(/PREVIOUS_OUTPUT/g, lastOutput)
|
|
2188
|
+
.replace(/\{\{step_(\d+)_output\}\}/g, (_, n) => stepOutputs[parseInt(n, 10)] || '');
|
|
2189
|
+
}
|
|
2190
|
+
else {
|
|
2191
|
+
resolved[key] = value;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return resolved;
|
|
2195
|
+
}
|
|
2196
|
+
// ── STEP 3: respondWithResults ────────────────────────────────
|
|
2197
|
+
function responderSystem(userName, date) {
|
|
2198
|
+
return (0, aidenPersonality_1.AIDEN_RESPONDER_SYSTEM)(userName, date);
|
|
2199
|
+
}
|
|
2200
|
+
async function respondWithResults(originalMessage, plan, results, history, userName, apiKey, model, providerName, onToken, sessionId, goals) {
|
|
2201
|
+
const date = new Date().toLocaleDateString('en-US', {
|
|
2202
|
+
weekday: 'long', month: 'long', day: 'numeric', year: 'numeric',
|
|
2203
|
+
});
|
|
2204
|
+
// Load skill guidance for the response
|
|
2205
|
+
const responseSkills = skillLoader_1.skillLoader.findRelevant(originalMessage, 2);
|
|
2206
|
+
const responseSkillContext = responseSkills.length > 0
|
|
2207
|
+
? `\nSkill guidance for this response:\n${responseSkills.map(s => `- ${s.name}: ${s.description}`).join('\n')}\n`
|
|
2208
|
+
: '';
|
|
2209
|
+
// Selective skill injection — simple messages (< 15 words, no tool keywords) get no skills;
|
|
2210
|
+
// complex messages get only the relevant subset (already filtered by findRelevant above).
|
|
2211
|
+
// Replaces the old loadAll() dump that injected all ~96 skills into every prompt.
|
|
2212
|
+
const capabilitiesSection = (0, skillLoader_1.isSimpleMessage)(originalMessage)
|
|
2213
|
+
? ''
|
|
2214
|
+
: (responseSkills.length > 0
|
|
2215
|
+
? `Relevant skills for this task: ${responseSkills.map(s => `${s.name} (${s.description})`).join(', ')}\n\n`
|
|
2216
|
+
: '');
|
|
2217
|
+
// Knowledge context — relevant chunks from user's uploaded files
|
|
2218
|
+
const knowledgeCtxResponder = knowledgeBase_1.knowledgeBase.buildContext(originalMessage || '');
|
|
2219
|
+
const knowledgeResponderSection = knowledgeCtxResponder
|
|
2220
|
+
? `\nRELEVANT KNOWLEDGE FROM YOUR FILES:\n${knowledgeCtxResponder}\n`
|
|
2221
|
+
: '';
|
|
2222
|
+
// ── Depth scoring: detect research tasks and force deep analysis ──
|
|
2223
|
+
const isResearch = results.some(r => r.tool === 'deep_research' ||
|
|
2224
|
+
r.tool === 'run_agent' ||
|
|
2225
|
+
(r.tool === 'web_search' && results.length > 1));
|
|
2226
|
+
const depthInstruction = isResearch
|
|
2227
|
+
? `\n\nRESEARCH RESPONSE REQUIREMENTS:
|
|
2228
|
+
- Minimum 500 words
|
|
2229
|
+
- Must include: Overview, Comparison (table or structured list), Key findings, Trends, Recommendation
|
|
2230
|
+
- Compare entities explicitly: "X is better than Y for Z because..."
|
|
2231
|
+
- Extract specific facts and numbers from the research data
|
|
2232
|
+
- End with a clear Verdict or Recommendation section
|
|
2233
|
+
- DO NOT just summarize — ANALYZE and provide INSIGHTS`
|
|
2234
|
+
: '';
|
|
2235
|
+
// Phase 1: multi-goal numbered output instruction
|
|
2236
|
+
const _goalsToUse = goals && goals.length >= 2 ? goals : (plan.goals && plan.goals.length >= 2 ? plan.goals : null);
|
|
2237
|
+
const multiGoalInstruction = _goalsToUse
|
|
2238
|
+
? `\n\nMULTI-GOAL RESPONSE — the user had ${_goalsToUse.length} distinct goals:\n${_goalsToUse.map((g, i) => `${i + 1}. ${g}`).join('\n')}\nStructure your response with numbered sections (1., 2., …) that match each goal above. Do not skip any goal.`
|
|
2239
|
+
: '';
|
|
2240
|
+
const executionSummary = results.length
|
|
2241
|
+
? results.map((r, i) => `Step ${i + 1} [${r.tool}]: ${r.success ? r.output.slice(0, 500) : 'FAILED — ' + r.error}`).join('\n\n')
|
|
2242
|
+
: '';
|
|
2243
|
+
// Inject conversation memory only when the message references past context
|
|
2244
|
+
// (reduces prompt size for routine messages — "hi", "thanks", etc.)
|
|
2245
|
+
const memCtx = (0, skillLoader_1.needsMemory)(originalMessage) ? conversationMemory_1.conversationMemory.buildContext() : '';
|
|
2246
|
+
const memSection = memCtx
|
|
2247
|
+
? `\nCONVERSATION HISTORY:\n${memCtx}\n\nIf the user asks what we worked on, what was researched, or references previous work — answer from this history.\n`
|
|
2248
|
+
: '';
|
|
2249
|
+
// Entity graph — 1-line summary only (never dump full graph into prompt)
|
|
2250
|
+
const entityStats = entityGraph_1.entityGraph.getStats();
|
|
2251
|
+
const entitySummary = entityStats.nodes > 0
|
|
2252
|
+
? `You know ${entityStats.nodes} entities across your work.\n\n`
|
|
2253
|
+
: '';
|
|
2254
|
+
// Build a tool-results context block for the system prompt
|
|
2255
|
+
const toolResultsContext = results.length
|
|
2256
|
+
? results.map(r => `[${r.tool} result]: ${r.success ? r.output.slice(0, 1000) : 'FAILED: ' + r.error}`).join('\n')
|
|
2257
|
+
: '';
|
|
2258
|
+
const systemWithResults = toolResultsContext
|
|
2259
|
+
? `${capabilitiesSection}${entitySummary}${responderSystem(userName, date)}${responseSkillContext}${knowledgeResponderSection}${multiGoalInstruction}
|
|
2260
|
+
|
|
2261
|
+
YOU JUST RAN THESE TOOLS AND GOT THESE RESULTS:
|
|
2262
|
+
${toolResultsContext}
|
|
2263
|
+
|
|
2264
|
+
CRITICAL RULES FOR YOUR RESPONSE:
|
|
2265
|
+
- Include the ACTUAL output from the tools above in your response
|
|
2266
|
+
- Do NOT say "I ran the tool" — show the RESULT
|
|
2267
|
+
- If run_python returned a number, say that number
|
|
2268
|
+
- If file_read returned text, show that text
|
|
2269
|
+
- If system_info returned hardware data, show the data
|
|
2270
|
+
- Be direct: show the actual output, then provide context if needed
|
|
2271
|
+
- If a tool failed, say it failed and why`
|
|
2272
|
+
: `${capabilitiesSection}${entitySummary}${responderSystem(userName, date)}${responseSkillContext}${knowledgeResponderSection}${multiGoalInstruction}`;
|
|
2273
|
+
const userContent = executionSummary
|
|
2274
|
+
? `User asked: "${originalMessage}"\n\nReal execution results:\n${executionSummary}\n\nRespond naturally based on these real results only. Show the actual output, not a description of it.${depthInstruction}${memSection}`
|
|
2275
|
+
: `${originalMessage}${memSection}`;
|
|
2276
|
+
let messages = [
|
|
2277
|
+
{ role: 'system', content: systemWithResults },
|
|
2278
|
+
...history.slice(-6),
|
|
2279
|
+
{ role: 'user', content: userContent },
|
|
2280
|
+
];
|
|
2281
|
+
messages = await preflightCompressionCheck(messages, model, sessionId);
|
|
2282
|
+
messages = (0, messageValidator_1.sanitizeMessages)(messages);
|
|
2283
|
+
if (executionInterrupted)
|
|
2284
|
+
return;
|
|
2285
|
+
const _respCtrl = new AbortController();
|
|
2286
|
+
currentAbortController = _respCtrl;
|
|
2287
|
+
try {
|
|
2288
|
+
if (providerName === 'gemini') {
|
|
2289
|
+
const contents = messages
|
|
2290
|
+
.filter(m => m.role !== 'system')
|
|
2291
|
+
.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] }));
|
|
2292
|
+
const system = messages.find(m => m.role === 'system')?.content;
|
|
2293
|
+
const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?alt=sse&key=${apiKey}`, {
|
|
2294
|
+
method: 'POST',
|
|
2295
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2296
|
+
body: JSON.stringify({
|
|
2297
|
+
contents,
|
|
2298
|
+
systemInstruction: system ? { parts: [{ text: system }] } : undefined,
|
|
2299
|
+
}),
|
|
2300
|
+
signal: AbortSignal.any([AbortSignal.timeout(30000), _respCtrl.signal]),
|
|
2301
|
+
});
|
|
2302
|
+
if (!r.ok) {
|
|
2303
|
+
const errText = await r.text().catch(() => '');
|
|
2304
|
+
if (r.status === 429 || r.status === 503) {
|
|
2305
|
+
try {
|
|
2306
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2307
|
+
}
|
|
2308
|
+
catch { }
|
|
2309
|
+
}
|
|
2310
|
+
const capacityHint = errText.toLowerCase().includes('capacity') || errText.toLowerCase().includes('overloaded') ? ' capacity' : '';
|
|
2311
|
+
throw new Error(`Responder ${r.status}${capacityHint}: ${errText.slice(0, 200)}`);
|
|
2312
|
+
}
|
|
2313
|
+
await streamGeminiResponse(r, onToken);
|
|
2314
|
+
}
|
|
2315
|
+
else if (providerName === 'ollama') {
|
|
2316
|
+
const ollamaMs = Math.min((0, modelDiscovery_1.getOllamaTimeout)(model || ''), 15000); // cap at 15s for chat
|
|
2317
|
+
const _t0 = Date.now();
|
|
2318
|
+
console.log(`[Router] respondWithResults → ollama, model: ${model}, timeout: ${ollamaMs}ms`);
|
|
2319
|
+
const r = await fetch('http://localhost:11434/api/chat', {
|
|
2320
|
+
method: 'POST',
|
|
2321
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2322
|
+
body: JSON.stringify({ model, stream: true, messages }),
|
|
2323
|
+
signal: AbortSignal.any([AbortSignal.timeout(ollamaMs), _respCtrl.signal]),
|
|
2324
|
+
});
|
|
2325
|
+
if (!r.body)
|
|
2326
|
+
throw new Error('Ollama: no response body');
|
|
2327
|
+
const reader = r.body.getReader();
|
|
2328
|
+
const decoder = new TextDecoder();
|
|
2329
|
+
let ollamaTokens = 0;
|
|
2330
|
+
while (true) {
|
|
2331
|
+
const { done, value } = await reader.read();
|
|
2332
|
+
if (done)
|
|
2333
|
+
break;
|
|
2334
|
+
const line = decoder.decode(value);
|
|
2335
|
+
try {
|
|
2336
|
+
const parsed = JSON.parse(line);
|
|
2337
|
+
if (parsed?.message?.content) {
|
|
2338
|
+
onToken(parsed.message.content);
|
|
2339
|
+
ollamaTokens++;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
catch { }
|
|
2343
|
+
}
|
|
2344
|
+
console.log(`[Router] Ollama responded in ${Date.now() - _t0}ms (${ollamaTokens} tokens)`);
|
|
2345
|
+
if (ollamaTokens === 0)
|
|
2346
|
+
throw new Error('Ollama: empty response — no tokens emitted');
|
|
2347
|
+
}
|
|
2348
|
+
else {
|
|
2349
|
+
// OpenAI-compatible
|
|
2350
|
+
const url = OPENAI_COMPAT_ENDPOINTS[providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
|
|
2351
|
+
const r = await fetch(url, {
|
|
2352
|
+
method: 'POST',
|
|
2353
|
+
headers: buildHeaders(providerName, apiKey),
|
|
2354
|
+
body: JSON.stringify({ model, messages, stream: true }),
|
|
2355
|
+
signal: AbortSignal.any([AbortSignal.timeout(30000), _respCtrl.signal]),
|
|
2356
|
+
});
|
|
2357
|
+
if (!r.ok) {
|
|
2358
|
+
const errText = await r.text().catch(() => '');
|
|
2359
|
+
if (r.status === 429 || r.status === 503) {
|
|
2360
|
+
try {
|
|
2361
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2362
|
+
}
|
|
2363
|
+
catch { }
|
|
2364
|
+
}
|
|
2365
|
+
const capacityHint = errText.toLowerCase().includes('capacity') || errText.toLowerCase().includes('overloaded') ? ' capacity' : '';
|
|
2366
|
+
throw new Error(`Responder ${r.status}${capacityHint}: ${errText.slice(0, 200)}`);
|
|
2367
|
+
}
|
|
2368
|
+
await streamOpenAIResponse(r, onToken);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
catch (e) {
|
|
2372
|
+
if (e.name === 'AbortError')
|
|
2373
|
+
return;
|
|
2374
|
+
console.error('[Responder] Error:', e.message);
|
|
2375
|
+
if (e.message?.includes('timeout') ||
|
|
2376
|
+
e.message?.includes('429') ||
|
|
2377
|
+
e.message?.includes('503') ||
|
|
2378
|
+
e.message?.includes('capacity') ||
|
|
2379
|
+
e.message?.includes('overloaded') ||
|
|
2380
|
+
e.message?.includes('rate') ||
|
|
2381
|
+
e.message?.includes('aborted')) {
|
|
2382
|
+
try {
|
|
2383
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2384
|
+
}
|
|
2385
|
+
catch { }
|
|
2386
|
+
}
|
|
2387
|
+
// If cloud provider hit capacity, try next provider in chain before falling to Ollama
|
|
2388
|
+
if (providerName !== 'ollama' && (e.message?.includes('capacity') || e.message?.includes('503') || e.message?.includes('overloaded'))) {
|
|
2389
|
+
const nextCloud = (0, router_1.getModelForTask)('responder');
|
|
2390
|
+
if (nextCloud.providerName !== 'ollama' && nextCloud.apiName !== providerName && nextCloud.apiKey) {
|
|
2391
|
+
console.log(`[Responder] ${providerName} at capacity — trying ${nextCloud.providerName} (${nextCloud.model})`);
|
|
2392
|
+
try {
|
|
2393
|
+
const url = OPENAI_COMPAT_ENDPOINTS[nextCloud.providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
|
|
2394
|
+
const headers = buildHeaders(nextCloud.providerName, nextCloud.apiKey);
|
|
2395
|
+
const r = await fetch(url, {
|
|
2396
|
+
method: 'POST',
|
|
2397
|
+
headers,
|
|
2398
|
+
body: JSON.stringify({ model: nextCloud.model, messages, stream: true }),
|
|
2399
|
+
signal: AbortSignal.timeout(30000),
|
|
2400
|
+
});
|
|
2401
|
+
if (r.ok) {
|
|
2402
|
+
await streamOpenAIResponse(r, onToken);
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
if (r.status === 429 || r.status === 503) {
|
|
2406
|
+
try {
|
|
2407
|
+
(0, router_1.markRateLimited)(nextCloud.apiName);
|
|
2408
|
+
}
|
|
2409
|
+
catch { }
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
catch (nextErr) {
|
|
2413
|
+
console.error(`[Responder] ${nextCloud.providerName} fallback also failed: ${nextErr.message}`);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
// If Ollama was primary and failed/timed out, fall back to best cloud provider
|
|
2418
|
+
if (providerName === 'ollama') {
|
|
2419
|
+
const cloudFallback = (0, router_1.getModelForTask)('responder');
|
|
2420
|
+
if (cloudFallback.providerName !== 'ollama' && cloudFallback.apiKey) {
|
|
2421
|
+
console.log(`[Router] Ollama timeout/error — falling back to ${cloudFallback.providerName} (${cloudFallback.model})`);
|
|
2422
|
+
try {
|
|
2423
|
+
const url = OPENAI_COMPAT_ENDPOINTS[cloudFallback.providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
|
|
2424
|
+
const headers = buildHeaders(cloudFallback.providerName, cloudFallback.apiKey);
|
|
2425
|
+
const r = await fetch(url, {
|
|
2426
|
+
method: 'POST',
|
|
2427
|
+
headers,
|
|
2428
|
+
body: JSON.stringify({ model: cloudFallback.model, messages, stream: true }),
|
|
2429
|
+
signal: AbortSignal.timeout(15000),
|
|
2430
|
+
});
|
|
2431
|
+
if (r.ok) {
|
|
2432
|
+
await streamOpenAIResponse(r, onToken);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
catch (fbErr) {
|
|
2437
|
+
console.error(`[Router] Cloud fallback also failed: ${fbErr.message}`);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
// If the cloud provider failed and we haven't tried Ollama yet, try it
|
|
2442
|
+
let ollamaResponded = false;
|
|
2443
|
+
if (providerName !== 'ollama') {
|
|
2444
|
+
try {
|
|
2445
|
+
// Discover installed model via api/tags
|
|
2446
|
+
const cfg = (0, index_1.loadConfig)();
|
|
2447
|
+
let ollamaModel = process.env.OLLAMA_MODEL || cfg.ollama?.model || 'gemma4:e4b';
|
|
2448
|
+
try {
|
|
2449
|
+
const _ob = (process.env.OLLAMA_HOST ?? 'http://127.0.0.1:11434').replace(/\/$/, '');
|
|
2450
|
+
const tagsRes = await fetch(`${_ob}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
2451
|
+
if (tagsRes.ok) {
|
|
2452
|
+
const tagsData = await tagsRes.json();
|
|
2453
|
+
const firstModel = tagsData?.models?.[0]?.name;
|
|
2454
|
+
if (firstModel)
|
|
2455
|
+
ollamaModel = firstModel;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
catch { /* Ollama not running */ }
|
|
2459
|
+
console.log(`[Responder] Cloud provider failed — falling back to Ollama (${ollamaModel})`);
|
|
2460
|
+
const r = await fetch('http://localhost:11434/api/chat', {
|
|
2461
|
+
method: 'POST',
|
|
2462
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2463
|
+
body: JSON.stringify({ model: ollamaModel, stream: true, messages }),
|
|
2464
|
+
signal: AbortSignal.timeout((0, modelDiscovery_1.getOllamaTimeout)(ollamaModel)),
|
|
2465
|
+
});
|
|
2466
|
+
if (r.ok && r.body) {
|
|
2467
|
+
const reader = r.body.getReader();
|
|
2468
|
+
const decoder = new TextDecoder();
|
|
2469
|
+
let tokensEmitted = 0;
|
|
2470
|
+
while (true) {
|
|
2471
|
+
const { done, value } = await reader.read();
|
|
2472
|
+
if (done)
|
|
2473
|
+
break;
|
|
2474
|
+
try {
|
|
2475
|
+
const parsed = JSON.parse(decoder.decode(value));
|
|
2476
|
+
if (parsed?.message?.content) {
|
|
2477
|
+
onToken(parsed.message.content);
|
|
2478
|
+
tokensEmitted++;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
catch { }
|
|
2482
|
+
}
|
|
2483
|
+
if (tokensEmitted > 0) {
|
|
2484
|
+
ollamaResponded = true;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
catch (ollamaErr) {
|
|
2489
|
+
console.warn(`[Responder] Ollama fallback also failed: ${ollamaErr.message}`);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (ollamaResponded)
|
|
2493
|
+
return;
|
|
2494
|
+
// Last resort: return raw tool output if tools ran successfully
|
|
2495
|
+
if (results && results.length > 0 && results.some(r => r.success)) {
|
|
2496
|
+
const successResults = results.filter(r => r.success);
|
|
2497
|
+
const lastResult = successResults[successResults.length - 1];
|
|
2498
|
+
onToken(lastResult.output || 'Here are the results.');
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
// Include error info from failed tools if any
|
|
2502
|
+
if (results && results.length > 0) {
|
|
2503
|
+
const failedResult = results[results.length - 1];
|
|
2504
|
+
if (failedResult.error) {
|
|
2505
|
+
onToken(`Error: ${failedResult.error}`);
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
const degraded = (0, router_1.enterDegradedMode)(e.message || 'unknown error');
|
|
2510
|
+
onToken(degraded.message);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
// ── Non-streaming LLM helper (used by deepResearch) ──────────
|
|
2514
|
+
async function callLLM(prompt, apiKey, model, providerName, opts) {
|
|
2515
|
+
if (executionInterrupted)
|
|
2516
|
+
return '';
|
|
2517
|
+
const _ctrl = new AbortController();
|
|
2518
|
+
currentAbortController = _ctrl;
|
|
2519
|
+
const messages = [{ role: 'user', content: prompt }];
|
|
2520
|
+
try {
|
|
2521
|
+
if (providerName === 'gemini') {
|
|
2522
|
+
const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
|
|
2523
|
+
method: 'POST',
|
|
2524
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2525
|
+
body: JSON.stringify({
|
|
2526
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
2527
|
+
generationConfig: { maxOutputTokens: 2000 },
|
|
2528
|
+
}),
|
|
2529
|
+
signal: AbortSignal.any([AbortSignal.timeout(12000), _ctrl.signal]),
|
|
2530
|
+
});
|
|
2531
|
+
if (r.status === 429) {
|
|
2532
|
+
try {
|
|
2533
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2534
|
+
}
|
|
2535
|
+
catch { }
|
|
2536
|
+
throw new Error(`Rate limited (429): ${providerName}`);
|
|
2537
|
+
}
|
|
2538
|
+
if (!r.ok) {
|
|
2539
|
+
throw new Error(`HTTP ${r.status} from ${providerName}`);
|
|
2540
|
+
}
|
|
2541
|
+
const d = await r.json();
|
|
2542
|
+
try {
|
|
2543
|
+
costTracker_1.costTracker.trackUsage(providerName, model, d?.usageMetadata?.promptTokenCount ?? 0, d?.usageMetadata?.candidatesTokenCount ?? 0, opts?.traceId, opts?.isSystem ?? false);
|
|
2544
|
+
}
|
|
2545
|
+
catch { }
|
|
2546
|
+
return d?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
2547
|
+
}
|
|
2548
|
+
else if (providerName === 'ollama') {
|
|
2549
|
+
const r = await fetch('http://localhost:11434/api/chat', {
|
|
2550
|
+
method: 'POST',
|
|
2551
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2552
|
+
body: JSON.stringify({ model: model || 'mistral:7b', stream: false, messages }),
|
|
2553
|
+
signal: AbortSignal.any([AbortSignal.timeout((0, modelDiscovery_1.getOllamaTimeout)(model || '')), _ctrl.signal]),
|
|
2554
|
+
});
|
|
2555
|
+
if (r.status === 429) {
|
|
2556
|
+
try {
|
|
2557
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2558
|
+
}
|
|
2559
|
+
catch { }
|
|
2560
|
+
throw new Error(`Rate limited (429): ${providerName}`);
|
|
2561
|
+
}
|
|
2562
|
+
if (!r.ok) {
|
|
2563
|
+
throw new Error(`HTTP ${r.status} from ${providerName}`);
|
|
2564
|
+
}
|
|
2565
|
+
const d = await r.json();
|
|
2566
|
+
try {
|
|
2567
|
+
costTracker_1.costTracker.trackUsage(providerName, model, d?.prompt_eval_count ?? 0, d?.eval_count ?? 0, opts?.traceId, opts?.isSystem ?? false);
|
|
2568
|
+
}
|
|
2569
|
+
catch { }
|
|
2570
|
+
return d?.message?.content || '';
|
|
2571
|
+
}
|
|
2572
|
+
else if (providerName === 'cloudflare') {
|
|
2573
|
+
// Cloudflare Workers AI — accountId|modelName stored in model field
|
|
2574
|
+
const [accountId, cfModel] = model.split('|');
|
|
2575
|
+
const r = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${cfModel || '@cf/meta/llama-3.1-8b-instruct'}`, {
|
|
2576
|
+
method: 'POST',
|
|
2577
|
+
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
2578
|
+
body: JSON.stringify({ messages }),
|
|
2579
|
+
signal: AbortSignal.any([AbortSignal.timeout(20000), _ctrl.signal]),
|
|
2580
|
+
});
|
|
2581
|
+
if (r.status === 429) {
|
|
2582
|
+
try {
|
|
2583
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2584
|
+
}
|
|
2585
|
+
catch { }
|
|
2586
|
+
throw new Error(`Rate limited (429): ${providerName}`);
|
|
2587
|
+
}
|
|
2588
|
+
if (!r.ok)
|
|
2589
|
+
throw new Error(`cloudflare ${r.status}`);
|
|
2590
|
+
const d = await r.json();
|
|
2591
|
+
try {
|
|
2592
|
+
costTracker_1.costTracker.trackUsage(providerName, model, 0, 0, opts?.traceId, opts?.isSystem ?? false);
|
|
2593
|
+
}
|
|
2594
|
+
catch { }
|
|
2595
|
+
return d?.result?.response || '';
|
|
2596
|
+
}
|
|
2597
|
+
else if (providerName === 'custom') {
|
|
2598
|
+
// Custom provider — look up baseUrl from config by matching apiKey
|
|
2599
|
+
const cfgCustom = (0, index_1.loadConfig)();
|
|
2600
|
+
const cp = cfgCustom.customProviders?.find((c) => c.enabled && c.apiKey === apiKey);
|
|
2601
|
+
if (!cp?.baseUrl)
|
|
2602
|
+
throw new Error(`callLLM: no baseUrl for custom provider (model=${model})`);
|
|
2603
|
+
const r = await fetch(cp.baseUrl, {
|
|
2604
|
+
method: 'POST',
|
|
2605
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
2606
|
+
body: JSON.stringify({
|
|
2607
|
+
model,
|
|
2608
|
+
messages: [
|
|
2609
|
+
{ role: 'system', content: 'You are Aiden, a local-first personal AI OS. Be concise and direct.' },
|
|
2610
|
+
...messages,
|
|
2611
|
+
],
|
|
2612
|
+
stream: false,
|
|
2613
|
+
max_tokens: 2000,
|
|
2614
|
+
}),
|
|
2615
|
+
signal: AbortSignal.any([AbortSignal.timeout(45000), _ctrl.signal]),
|
|
2616
|
+
});
|
|
2617
|
+
if (r.status === 429) {
|
|
2618
|
+
try {
|
|
2619
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2620
|
+
}
|
|
2621
|
+
catch { }
|
|
2622
|
+
throw new Error(`Rate limited (429): custom/${model}`);
|
|
2623
|
+
}
|
|
2624
|
+
if (!r.ok)
|
|
2625
|
+
throw new Error(`HTTP ${r.status} from custom/${model}`);
|
|
2626
|
+
const d = await r.json();
|
|
2627
|
+
try {
|
|
2628
|
+
costTracker_1.costTracker.trackUsage(providerName, model, d?.usage?.prompt_tokens ?? 0, d?.usage?.completion_tokens ?? 0, opts?.traceId, opts?.isSystem ?? false);
|
|
2629
|
+
}
|
|
2630
|
+
catch { }
|
|
2631
|
+
return d?.choices?.[0]?.message?.content || '';
|
|
2632
|
+
}
|
|
2633
|
+
else {
|
|
2634
|
+
// OpenAI-compatible: groq, openrouter, cerebras, nvidia, github
|
|
2635
|
+
const url = OPENAI_COMPAT_ENDPOINTS[providerName] || OPENAI_COMPAT_ENDPOINTS.groq;
|
|
2636
|
+
const headers = buildHeaders(providerName, apiKey);
|
|
2637
|
+
const r = await fetch(url, {
|
|
2638
|
+
method: 'POST',
|
|
2639
|
+
headers,
|
|
2640
|
+
body: JSON.stringify({ model, messages, stream: false, max_tokens: 2000 }),
|
|
2641
|
+
signal: AbortSignal.any([AbortSignal.timeout(12000), _ctrl.signal]),
|
|
2642
|
+
});
|
|
2643
|
+
if (r.status === 429) {
|
|
2644
|
+
try {
|
|
2645
|
+
(0, router_1.markRateLimited)(providerName);
|
|
2646
|
+
}
|
|
2647
|
+
catch { }
|
|
2648
|
+
throw new Error(`Rate limited (429): ${providerName}`);
|
|
2649
|
+
}
|
|
2650
|
+
if (!r.ok) {
|
|
2651
|
+
throw new Error(`HTTP ${r.status} from ${providerName}`);
|
|
2652
|
+
}
|
|
2653
|
+
const d = await r.json();
|
|
2654
|
+
try {
|
|
2655
|
+
costTracker_1.costTracker.trackUsage(providerName, model, d?.usage?.prompt_tokens ?? 0, d?.usage?.completion_tokens ?? 0, opts?.traceId, opts?.isSystem ?? false);
|
|
2656
|
+
}
|
|
2657
|
+
catch { }
|
|
2658
|
+
return d?.choices?.[0]?.message?.content || '';
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
catch (e) {
|
|
2662
|
+
if (e.name === 'AbortError')
|
|
2663
|
+
return '';
|
|
2664
|
+
console.error('[callLLM] error:', e.message);
|
|
2665
|
+
return '';
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
// ── Deep research: 3-pass LLM-assisted research loop ─────────
|
|
2669
|
+
// Called directly (e.g. from a /api/research endpoint) or as
|
|
2670
|
+
// a high-level entry point when the planner picks deep_research.
|
|
2671
|
+
async function deepResearch(topic, apiKey, model, provider, onProgress) {
|
|
2672
|
+
const allResults = [];
|
|
2673
|
+
let currentQuery = topic;
|
|
2674
|
+
const maxPasses = 7;
|
|
2675
|
+
for (let pass = 1; pass <= maxPasses; pass++) {
|
|
2676
|
+
onProgress(`Pass ${pass}: Searching "${currentQuery}"...`);
|
|
2677
|
+
const searchResult = await (0, toolRegistry_1.executeTool)('web_search', { query: currentQuery });
|
|
2678
|
+
if (!searchResult.success || !searchResult.output)
|
|
2679
|
+
break;
|
|
2680
|
+
allResults.push(`=== Pass ${pass}: ${currentQuery} ===\n${searchResult.output}`);
|
|
2681
|
+
// Reflection: what gaps remain?
|
|
2682
|
+
const reflectionPrompt = `You are researching: "${topic}"
|
|
2683
|
+
|
|
2684
|
+
So far you have found:
|
|
2685
|
+
${allResults.join('\n\n').slice(0, 3000)}
|
|
2686
|
+
|
|
2687
|
+
Analyze the gaps:
|
|
2688
|
+
1. What important aspects of "${topic}" are still missing?
|
|
2689
|
+
2. What contradictions need resolving?
|
|
2690
|
+
3. What specific follow-up query would fill the biggest gap?
|
|
2691
|
+
|
|
2692
|
+
Respond in JSON:
|
|
2693
|
+
{
|
|
2694
|
+
"gaps": ["gap1", "gap2"],
|
|
2695
|
+
"nextQuery": "specific search query to fill the biggest gap",
|
|
2696
|
+
"complete": true/false
|
|
2697
|
+
}`;
|
|
2698
|
+
const reflection = await callLLM(reflectionPrompt, apiKey, model, provider);
|
|
2699
|
+
let reflectionData = {};
|
|
2700
|
+
try {
|
|
2701
|
+
const match = reflection.match(/\{[\s\S]*\}/);
|
|
2702
|
+
reflectionData = JSON.parse(match?.[0] || '{}');
|
|
2703
|
+
}
|
|
2704
|
+
catch { }
|
|
2705
|
+
if (reflectionData.complete === true || !reflectionData.nextQuery)
|
|
2706
|
+
break;
|
|
2707
|
+
currentQuery = reflectionData.nextQuery;
|
|
2708
|
+
onProgress(`Filling gap: ${reflectionData.gaps?.[0] || currentQuery}`);
|
|
2709
|
+
// Source quality scoring
|
|
2710
|
+
const isHighQuality = searchResult.output.includes('wikipedia') ||
|
|
2711
|
+
searchResult.output.includes('.gov') ||
|
|
2712
|
+
searchResult.output.includes('reuters') ||
|
|
2713
|
+
searchResult.output.includes('bloomberg');
|
|
2714
|
+
if (isHighQuality)
|
|
2715
|
+
onProgress('✓ High-quality source found');
|
|
2716
|
+
}
|
|
2717
|
+
return allResults.join('\n\n');
|
|
2718
|
+
}
|