@sschepis/robodev 1.0.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.
@@ -0,0 +1,330 @@
1
+ // History management and token estimation
2
+ // Handles context limits and conversation history optimization
3
+
4
+ import { consoleStyler } from '../ui/console-styler.mjs';
5
+ import { config } from '../config.mjs';
6
+
7
+ // Average characters per token (approximate for English text)
8
+ const CHARS_PER_TOKEN = 4;
9
+
10
+ /**
11
+ * Manages conversation history, token estimation, and context limits.
12
+ */
13
+ export class HistoryManager {
14
+ /**
15
+ * @param {number|null} maxTokens - Maximum allow tokens
16
+ * @param {number|null} contextWindowSize - Total context window size
17
+ */
18
+ constructor(maxTokens = null, contextWindowSize = null) {
19
+ this.maxTokens = maxTokens || config.ai.maxTokens || 4096;
20
+ this.contextWindowSize = contextWindowSize || config.ai.contextWindowSize || 128000;
21
+
22
+ /** @type {Array<Object>} */
23
+ this.history = [];
24
+ this.systemMessage = null;
25
+ }
26
+
27
+ /**
28
+ * Initialize history with a system message
29
+ * @param {string} systemPrompt
30
+ */
31
+ initialize(systemPrompt) {
32
+ this.systemMessage = {
33
+ role: 'system',
34
+ content: systemPrompt
35
+ };
36
+ this.history = [this.systemMessage];
37
+ }
38
+
39
+ /**
40
+ * Add a message to history
41
+ * @param {string} role - 'user', 'assistant', 'system', or 'tool'
42
+ * @param {string} content - Message content
43
+ * @param {Array<Object>|null} toolCalls - Optional tool calls
44
+ * @param {string|null} toolCallId - Optional tool call ID
45
+ * @param {string|null} name - Optional name for tool messages
46
+ */
47
+ addMessage(role, content, toolCalls = null, toolCallId = null, name = null) {
48
+ const message = { role, content };
49
+
50
+ if (toolCalls) {
51
+ message.tool_calls = toolCalls;
52
+ }
53
+
54
+ if (toolCallId) {
55
+ message.tool_call_id = toolCallId;
56
+ }
57
+
58
+ if (name) {
59
+ message.name = name;
60
+ }
61
+
62
+ this.history.push(message);
63
+
64
+ // Check context limits and optimize if needed
65
+ this.enforceContextLimits();
66
+ }
67
+
68
+ /**
69
+ * Add a complete message object directly
70
+ * @param {Object} message
71
+ */
72
+ pushMessage(message) {
73
+ this.history.push(message);
74
+ this.enforceContextLimits();
75
+ }
76
+
77
+ /**
78
+ * Get the full history
79
+ * @returns {Array<Object>}
80
+ */
81
+ getHistory() {
82
+ return this.history;
83
+ }
84
+
85
+ /**
86
+ * Update the system prompt
87
+ * @param {string} newContent
88
+ */
89
+ updateSystemPrompt(newContent) {
90
+ if (this.history.length > 0 && this.history[0].role === 'system') {
91
+ this.history[0].content = newContent;
92
+ this.systemMessage.content = newContent;
93
+ } else {
94
+ this.systemMessage = { role: 'system', content: newContent };
95
+ this.history.unshift(this.systemMessage);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Delete specified number of exchanges from history (backwards)
101
+ * @param {number} count
102
+ * @returns {number} Number of exchanges deleted
103
+ */
104
+ deleteHistoryExchanges(count) {
105
+ if (count <= 0) return 0;
106
+
107
+ let deletedExchanges = 0;
108
+
109
+ // Work backwards through history, preserving system message at index 0
110
+ for (let i = 0; i < count; i++) {
111
+ // Find the most recent complete exchange (user + assistant + any tools)
112
+ let foundUserMessage = false;
113
+ let messagesToDelete = [];
114
+
115
+ // Scan backwards from end of history
116
+ for (let j = this.history.length - 1; j > 0; j--) { // Start from index 1 to preserve system message
117
+ const message = this.history[j];
118
+
119
+ if (message.role === 'user' && !foundUserMessage) {
120
+ // Found the start of the exchange to delete
121
+ foundUserMessage = true;
122
+ messagesToDelete.unshift(j); // Add to beginning since we're going backwards
123
+ } else if (foundUserMessage) {
124
+ // Part of the exchange (assistant, tool responses)
125
+ messagesToDelete.unshift(j);
126
+
127
+ // Check if this completes an exchange (hit previous user message or system)
128
+ if (message.role === 'user' || message.role === 'system') {
129
+ break;
130
+ }
131
+ } else if (message.role === 'assistant' || message.role === 'tool') {
132
+ // Dangling assistant/tool message, include it in deletion
133
+ messagesToDelete.unshift(j);
134
+ }
135
+ }
136
+
137
+ // Delete the identified messages
138
+ if (messagesToDelete.length > 0) {
139
+ // Sort in descending order to delete from end first (preserves indices)
140
+ messagesToDelete.sort((a, b) => b - a);
141
+ for (const index of messagesToDelete) {
142
+ this.history.splice(index, 1);
143
+ }
144
+ deletedExchanges++;
145
+ } else {
146
+ // No more exchanges to delete
147
+ break;
148
+ }
149
+ }
150
+
151
+ return deletedExchanges;
152
+ }
153
+
154
+ /**
155
+ * Reset history to just the system prompt
156
+ */
157
+ reset() {
158
+ if (this.systemMessage) {
159
+ this.history = [this.systemMessage];
160
+ } else {
161
+ this.history = [];
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Set history to a specific set of messages (e.g. for retries)
167
+ * @param {Array<Object>} messages
168
+ */
169
+ setHistory(messages) {
170
+ this.history = [...messages];
171
+ // Ensure system message is tracked
172
+ if (this.history.length > 0 && this.history[0].role === 'system') {
173
+ this.systemMessage = this.history[0];
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Estimate token count for a string
179
+ * @param {string} text
180
+ * @returns {number}
181
+ */
182
+ estimateTokens(text) {
183
+ if (!text) return 0;
184
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
185
+ }
186
+
187
+ /**
188
+ * Estimate total tokens in history
189
+ * @returns {number}
190
+ */
191
+ getTotalTokens() {
192
+ let total = 0;
193
+ for (const msg of this.history) {
194
+ // Content tokens
195
+ total += this.estimateTokens(msg.content || '');
196
+
197
+ // Role overhead (approximate)
198
+ total += 4;
199
+
200
+ // Tool calls tokens
201
+ if (msg.tool_calls) {
202
+ for (const call of msg.tool_calls) {
203
+ total += this.estimateTokens(call.function.name);
204
+ total += this.estimateTokens(call.function.arguments);
205
+ }
206
+ }
207
+ }
208
+ return total;
209
+ }
210
+
211
+ /**
212
+ * Enforce context window limits by summarizing or truncating
213
+ */
214
+ enforceContextLimits() {
215
+ const currentTokens = this.getTotalTokens();
216
+
217
+ // If we're approaching the limit (90% capacity)
218
+ if (currentTokens > this.contextWindowSize * 0.9) {
219
+ consoleStyler.log('system', `⚠️ Context limit approaching (${currentTokens}/${this.contextWindowSize} tokens). Optimizing history...`);
220
+
221
+ // Calculate how many tokens we need to free up (aim for 70% capacity)
222
+ const targetTokens = Math.floor(this.contextWindowSize * 0.7);
223
+
224
+ // Simple strategy: Remove oldest exchanges (after system prompt)
225
+ // A smarter strategy would be to summarize, but that requires an LLM call
226
+
227
+ let attempts = 0;
228
+ const maxAttempts = 20; // Prevent infinite loops
229
+
230
+ while (this.getTotalTokens() > targetTokens && this.history.length > 2 && attempts < maxAttempts) {
231
+ // Remove the oldest exchange (index 1 is usually the first user message)
232
+ // We use deleteHistoryExchanges logic but target specific indices
233
+
234
+ // Find first user message after system prompt
235
+ let firstUserIndex = -1;
236
+ for (let i = 1; i < this.history.length; i++) {
237
+ if (this.history[i].role === 'user') {
238
+ firstUserIndex = i;
239
+ break;
240
+ }
241
+ }
242
+
243
+ if (firstUserIndex === -1) break; // No user messages found
244
+
245
+ // Find the next user message to define the exchange boundary
246
+ let nextUserIndex = -1;
247
+ for (let i = firstUserIndex + 1; i < this.history.length; i++) {
248
+ if (this.history[i].role === 'user') {
249
+ nextUserIndex = i;
250
+ break;
251
+ }
252
+ }
253
+
254
+ // If no next user message, we're at the last exchange - don't delete recent context unless critical
255
+ if (nextUserIndex === -1 && currentTokens < this.contextWindowSize) {
256
+ break;
257
+ }
258
+
259
+ // Delete everything from firstUserIndex up to (but not including) nextUserIndex
260
+ // If nextUserIndex is -1, delete until end (only if we are critically over limit)
261
+ const deleteCount = (nextUserIndex !== -1 ? nextUserIndex : this.history.length) - firstUserIndex;
262
+
263
+ this.history.splice(firstUserIndex, deleteCount);
264
+ attempts++;
265
+ }
266
+
267
+ consoleStyler.log('system', `✓ History optimized. New token count: ~${this.getTotalTokens()}`);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Get context statistics
273
+ * @returns {Object}
274
+ */
275
+ getStats() {
276
+ return {
277
+ messageCount: this.history.length,
278
+ estimatedTokens: this.getTotalTokens(),
279
+ contextWindowSize: this.contextWindowSize,
280
+ utilizationPercent: Math.round((this.getTotalTokens() / this.contextWindowSize) * 100)
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Save history to a file
286
+ * @param {string} filePath
287
+ * @returns {Promise<boolean>}
288
+ */
289
+ async save(filePath) {
290
+ try {
291
+ const fs = await import('fs');
292
+ const data = {
293
+ timestamp: new Date().toISOString(),
294
+ history: this.history,
295
+ systemMessage: this.systemMessage
296
+ };
297
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
298
+ return true;
299
+ } catch (error) {
300
+ consoleStyler.log('error', `Failed to save history: ${error.message}`);
301
+ return false;
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Load history from a file
307
+ * @param {string} filePath
308
+ * @returns {Promise<boolean>}
309
+ */
310
+ async load(filePath) {
311
+ try {
312
+ const fs = await import('fs');
313
+ if (!fs.existsSync(filePath)) {
314
+ return false;
315
+ }
316
+
317
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
318
+
319
+ if (data.history && Array.isArray(data.history)) {
320
+ this.history = data.history;
321
+ this.systemMessage = data.systemMessage || this.history.find(msg => msg.role === 'system');
322
+ return true;
323
+ }
324
+ return false;
325
+ } catch (error) {
326
+ consoleStyler.log('error', `Failed to load history: ${error.message}`);
327
+ return false;
328
+ }
329
+ }
330
+ }
@@ -0,0 +1,182 @@
1
+ // System prompt generation
2
+ // Creates the system prompt with workspace context and guidelines
3
+
4
+ export function createSystemPrompt(workingDir, workspace = null, manifestContent = null) {
5
+ let prompt = `You are a JavaScript/Node.js command executor. Your output consists of direct commands and concise results, not explanations.
6
+
7
+ **Working Directory:** ${workingDir}
8
+ The user is executing commands from this directory. When working with files or paths, consider this as the current working directory unless otherwise specified.`;
9
+
10
+ // Add Living Manifest if available
11
+ if (manifestContent) {
12
+ prompt += `
13
+
14
+ **LIVING MANIFEST (SYSTEM_MAP.md):**
15
+ The following is the authoritative state of the system. You MUST adhere to Global Invariants and respect Feature Locks.
16
+
17
+ ${manifestContent}
18
+
19
+ **STRUCTURED DEVELOPMENT RULES:**
20
+ 1. Check "Global Invariants" before writing any code.
21
+ 2. Check "Feature Registry" to see if a feature is Locked.
22
+ - If "Interface" lock is active, you CANNOT change API signatures without a refactor request.
23
+ - If "None" or "Discovery", you are free to design.
24
+ 3. Update the manifest using the provided tools as you progress through phases (Discovery -> Interface -> Implementation).
25
+ `;
26
+ }
27
+
28
+ // Add workspace context if active
29
+ if (workspace) {
30
+ prompt += `
31
+
32
+ **ACTIVE WORKSPACE:**
33
+ • Task Goal: ${workspace.task_goal}
34
+ • Current Step: ${workspace.current_step}
35
+ • Status: ${workspace.status}
36
+ • Progress Data: ${JSON.stringify(workspace.progress_data)}
37
+ • Next Steps: ${workspace.next_steps.join(', ')}
38
+
39
+ IMPORTANT: You are continuing work on the above task. Use the workspace context to maintain continuity. Update the workspace as you make progress using the manage_workspace tool.`;
40
+ }
41
+
42
+ prompt += `
43
+
44
+ **Core Principles:**
45
+ * **Truthfulness:** Be strictly truthful. Never fabricate outcomes, always report failures accurately, and admit when you cannot complete a task.
46
+ * **Language:** Default to modern ES6+ JavaScript and \`async/await\`. Interpret requests to "create" or "build" as "write JavaScript code."
47
+ * **Workspace Management:** For complex multi-step tasks, use the \`manage_workspace\` tool to maintain context across retries and quality evaluations.
48
+ * **Work Reporting:** ALWAYS include a \`workPerformed\` field in your responses when you perform any action or use tools. This should be a brief, clear statement like "I executed JavaScript code to fetch data from the API" or "I created a file with the requested content". This helps users understand what work was completed.
49
+
50
+ Before answering, work through the request step-by-step:
51
+
52
+ 1. UNDERSTAND: What is the core question being asked?
53
+ 2. ANALYZE: What are the key factors/components involved?
54
+ 3. REASON: What logical connections can I make?
55
+ 4. SYNTHESIZE: How do these elements combine?
56
+ 5. CONCLUDE: What is the most accurate/helpful response?
57
+
58
+ Then provide your answer.
59
+
60
+ **Execution Protocol:**
61
+ 1. **Plan:** Analyze the request and formulate a step-by-step technical plan. For complex tasks, create a workspace to track progress.
62
+ 2. **Execute:** Carry out the plan using your available tools. Update workspace as you progress.
63
+ 3. **Recover:** On error, use your \`analyze_and_recover\` tool to find an alternative solution before giving up.
64
+ 4. **Report:** State the final, factual result. Update workspace status when task is complete.
65
+
66
+ **Technical Constraints:**
67
+ * For Node.js v18 compatibility, prefer built-in modules (\`fetch\`) over packages with known issues (\`axios\`, \`undici\`).
68
+ * If a primary tool like \`cheerio\` fails, use a fallback like regex or built-in DOM parsing.
69
+
70
+ **Node.js v18 Compatibility Guidelines:**
71
+ * ALWAYS use built-in fetch instead of axios for HTTP requests
72
+ * For web scraping: Use regex patterns or built-in string methods instead of cheerio
73
+ * Avoid these packages: axios, undici, node-fetch, cheerio (they have File API issues in Node v18)
74
+ * When scraping HTML, use patterns like: /<h[1-6][^>]*>(.*?)<\/h[1-6]>/gi for headlines
75
+ * For complex HTML parsing, use built-in DOMParser alternatives or regex
76
+
77
+ Execute commands. Report results. Recover from errors. Move to next step.`;
78
+
79
+ return prompt;
80
+ }
81
+
82
+ // Create enhanced system prompt with work reporting instruction
83
+ export function createEnhancedSystemPrompt(workingDir, workspace = null) {
84
+ const basePrompt = createSystemPrompt(workingDir, workspace);
85
+
86
+ return basePrompt + `
87
+
88
+ **IMPORTANT RESPONSE FORMAT:**
89
+ Always structure your responses to include actionable information and clear work reporting. When you use tools or execute code, explicitly state what was accomplished in a \`workPerformed\` field or section.`;
90
+ }
91
+
92
+ // Create system prompt for quality evaluation
93
+ export function createQualityEvaluationPrompt() {
94
+ return `You are an AI response quality evaluator. Your job is to objectively assess whether AI responses appropriately address user queries.
95
+
96
+ **Evaluation Criteria:**
97
+ - **Completeness:** Does the response fully address all parts of the user's request?
98
+ - **Accuracy:** Is the information provided correct and factual?
99
+ - **Usefulness:** Does the response provide practical value to the user?
100
+ - **Tool Usage:** If tools were used, were they appropriate and effective?
101
+ - **Clarity:** Is the response clear and well-structured?
102
+
103
+ **Scoring Scale:**
104
+ - 9-10: Excellent response that exceeds expectations
105
+ - 7-8: Good response that meets expectations well
106
+ - 5-6: Adequate response with minor issues
107
+ - 3-4: Poor response with significant problems
108
+ - 1-2: Completely inadequate response
109
+
110
+ **Important:** Consider BOTH the text response AND any tools that were executed. A brief text response paired with successful tool execution that accomplishes the user's goal should be rated highly.`;
111
+ }
112
+
113
+ // Create system prompt for tool generation
114
+ export function createToolGenerationPrompt() {
115
+ return `You are a JavaScript function generator. Your job is to convert code snippets into reusable, parameterized functions.
116
+
117
+ **Requirements:**
118
+ 1. Extract hardcoded values as function parameters
119
+ 2. Add comprehensive error handling
120
+ 3. Include detailed JSDoc comments
121
+ 4. Return meaningful data structures
122
+ 5. Handle edge cases and validation
123
+ 6. Use modern ES6+ syntax with async/await
124
+ 7. Make functions self-contained
125
+
126
+ **Output:** Return ONLY the function code, no explanations or markdown formatting.`;
127
+ }
128
+
129
+ // Create system prompt for schema generation
130
+ export function createSchemaGenerationPrompt() {
131
+ return `You are a JSON schema generator for OpenAI function calling format.
132
+
133
+ **Requirements:**
134
+ 1. Analyze function parameters and their types
135
+ 2. Provide clear descriptions for each parameter
136
+ 3. Identify required vs optional parameters
137
+ 4. Use proper JSON schema types and formats
138
+ 5. Follow OpenAI function calling specification
139
+
140
+ **Output:** Return ONLY the JSON schema object, no explanations or markdown formatting.`;
141
+ }
142
+
143
+ // Get appropriate system prompt based on context
144
+ export function getSystemPrompt(context = {}) {
145
+ const {
146
+ type = 'default',
147
+ workingDir = process.cwd(),
148
+ workspace = null,
149
+ manifestContent = null,
150
+ enhanced = false
151
+ } = context;
152
+
153
+ switch (type) {
154
+ case 'quality':
155
+ return createQualityEvaluationPrompt();
156
+
157
+ case 'tool-generation':
158
+ return createToolGenerationPrompt();
159
+
160
+ case 'schema-generation':
161
+ return createSchemaGenerationPrompt();
162
+
163
+ case 'enhanced':
164
+ return createEnhancedSystemPrompt(workingDir, workspace);
165
+
166
+ default:
167
+ return enhanced
168
+ ? createEnhancedSystemPrompt(workingDir, workspace)
169
+ : createSystemPrompt(workingDir, workspace, manifestContent);
170
+ }
171
+ }
172
+
173
+ // Add work performed instruction to existing messages
174
+ export function enhanceMessagesWithWorkReporting(messages) {
175
+ if (messages.length > 1) {
176
+ const lastUserMessage = messages[messages.length - 1];
177
+ if (lastUserMessage.role === 'user') {
178
+ lastUserMessage.content += `\n\nIMPORTANT: Please include a 'workPerformed' field in your response with a brief summary of any work completed (e.g., "I executed JavaScript code to analyze the data" or "I created a file with the requested content").`;
179
+ }
180
+ }
181
+ return messages;
182
+ }