@blockrun/franklin 3.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.
Files changed (138) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +256 -0
  3. package/dist/agent/commands.d.ts +27 -0
  4. package/dist/agent/commands.js +659 -0
  5. package/dist/agent/compact.d.ts +31 -0
  6. package/dist/agent/compact.js +366 -0
  7. package/dist/agent/context.d.ts +11 -0
  8. package/dist/agent/context.js +184 -0
  9. package/dist/agent/error-classifier.d.ts +10 -0
  10. package/dist/agent/error-classifier.js +61 -0
  11. package/dist/agent/llm.d.ts +63 -0
  12. package/dist/agent/llm.js +448 -0
  13. package/dist/agent/loop.d.ts +12 -0
  14. package/dist/agent/loop.js +346 -0
  15. package/dist/agent/optimize.d.ts +53 -0
  16. package/dist/agent/optimize.js +262 -0
  17. package/dist/agent/permissions.d.ts +39 -0
  18. package/dist/agent/permissions.js +226 -0
  19. package/dist/agent/reduce.d.ts +49 -0
  20. package/dist/agent/reduce.js +317 -0
  21. package/dist/agent/streaming-executor.d.ts +36 -0
  22. package/dist/agent/streaming-executor.js +149 -0
  23. package/dist/agent/tokens.d.ts +53 -0
  24. package/dist/agent/tokens.js +185 -0
  25. package/dist/agent/types.d.ts +125 -0
  26. package/dist/agent/types.js +5 -0
  27. package/dist/banner.d.ts +1 -0
  28. package/dist/banner.js +27 -0
  29. package/dist/commands/balance.d.ts +1 -0
  30. package/dist/commands/balance.js +40 -0
  31. package/dist/commands/config.d.ts +14 -0
  32. package/dist/commands/config.js +107 -0
  33. package/dist/commands/daemon.d.ts +3 -0
  34. package/dist/commands/daemon.js +117 -0
  35. package/dist/commands/history.d.ts +5 -0
  36. package/dist/commands/history.js +31 -0
  37. package/dist/commands/init.d.ts +3 -0
  38. package/dist/commands/init.js +92 -0
  39. package/dist/commands/logs.d.ts +5 -0
  40. package/dist/commands/logs.js +89 -0
  41. package/dist/commands/models.d.ts +1 -0
  42. package/dist/commands/models.js +56 -0
  43. package/dist/commands/plugin.d.ts +14 -0
  44. package/dist/commands/plugin.js +176 -0
  45. package/dist/commands/proxy.d.ts +13 -0
  46. package/dist/commands/proxy.js +106 -0
  47. package/dist/commands/setup.d.ts +1 -0
  48. package/dist/commands/setup.js +49 -0
  49. package/dist/commands/start.d.ts +8 -0
  50. package/dist/commands/start.js +292 -0
  51. package/dist/commands/stats.d.ts +10 -0
  52. package/dist/commands/stats.js +94 -0
  53. package/dist/commands/uninit.d.ts +1 -0
  54. package/dist/commands/uninit.js +63 -0
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +41 -0
  57. package/dist/index.d.ts +2 -0
  58. package/dist/index.js +179 -0
  59. package/dist/mcp/client.d.ts +44 -0
  60. package/dist/mcp/client.js +147 -0
  61. package/dist/mcp/config.d.ts +20 -0
  62. package/dist/mcp/config.js +138 -0
  63. package/dist/plugin-sdk/channel.d.ts +100 -0
  64. package/dist/plugin-sdk/channel.js +10 -0
  65. package/dist/plugin-sdk/index.d.ts +14 -0
  66. package/dist/plugin-sdk/index.js +9 -0
  67. package/dist/plugin-sdk/plugin.d.ts +87 -0
  68. package/dist/plugin-sdk/plugin.js +7 -0
  69. package/dist/plugin-sdk/search.d.ts +13 -0
  70. package/dist/plugin-sdk/search.js +4 -0
  71. package/dist/plugin-sdk/tracker.d.ts +27 -0
  72. package/dist/plugin-sdk/tracker.js +5 -0
  73. package/dist/plugin-sdk/workflow.d.ts +126 -0
  74. package/dist/plugin-sdk/workflow.js +11 -0
  75. package/dist/plugins/registry.d.ts +33 -0
  76. package/dist/plugins/registry.js +155 -0
  77. package/dist/plugins/runner.d.ts +21 -0
  78. package/dist/plugins/runner.js +453 -0
  79. package/dist/plugins-bundled/social/index.d.ts +10 -0
  80. package/dist/plugins-bundled/social/index.js +363 -0
  81. package/dist/plugins-bundled/social/plugin.json +14 -0
  82. package/dist/plugins-bundled/social/prompts.d.ts +19 -0
  83. package/dist/plugins-bundled/social/prompts.js +67 -0
  84. package/dist/plugins-bundled/social/types.d.ts +58 -0
  85. package/dist/plugins-bundled/social/types.js +16 -0
  86. package/dist/pricing.d.ts +21 -0
  87. package/dist/pricing.js +91 -0
  88. package/dist/proxy/fallback.d.ts +38 -0
  89. package/dist/proxy/fallback.js +144 -0
  90. package/dist/proxy/server.d.ts +18 -0
  91. package/dist/proxy/server.js +576 -0
  92. package/dist/proxy/sse-translator.d.ts +29 -0
  93. package/dist/proxy/sse-translator.js +270 -0
  94. package/dist/router/index.d.ts +22 -0
  95. package/dist/router/index.js +269 -0
  96. package/dist/session/search.d.ts +33 -0
  97. package/dist/session/search.js +229 -0
  98. package/dist/session/storage.d.ts +48 -0
  99. package/dist/session/storage.js +173 -0
  100. package/dist/stats/insights.d.ts +55 -0
  101. package/dist/stats/insights.js +195 -0
  102. package/dist/stats/tracker.d.ts +54 -0
  103. package/dist/stats/tracker.js +165 -0
  104. package/dist/tools/askuser.d.ts +6 -0
  105. package/dist/tools/askuser.js +76 -0
  106. package/dist/tools/bash.d.ts +5 -0
  107. package/dist/tools/bash.js +336 -0
  108. package/dist/tools/edit.d.ts +5 -0
  109. package/dist/tools/edit.js +148 -0
  110. package/dist/tools/glob.d.ts +5 -0
  111. package/dist/tools/glob.js +158 -0
  112. package/dist/tools/grep.d.ts +5 -0
  113. package/dist/tools/grep.js +194 -0
  114. package/dist/tools/imagegen.d.ts +6 -0
  115. package/dist/tools/imagegen.js +172 -0
  116. package/dist/tools/index.d.ts +17 -0
  117. package/dist/tools/index.js +30 -0
  118. package/dist/tools/read.d.ts +11 -0
  119. package/dist/tools/read.js +90 -0
  120. package/dist/tools/subagent.d.ts +5 -0
  121. package/dist/tools/subagent.js +116 -0
  122. package/dist/tools/task.d.ts +5 -0
  123. package/dist/tools/task.js +91 -0
  124. package/dist/tools/webfetch.d.ts +5 -0
  125. package/dist/tools/webfetch.js +166 -0
  126. package/dist/tools/websearch.d.ts +5 -0
  127. package/dist/tools/websearch.js +103 -0
  128. package/dist/tools/write.d.ts +5 -0
  129. package/dist/tools/write.js +114 -0
  130. package/dist/ui/app.d.ts +26 -0
  131. package/dist/ui/app.js +545 -0
  132. package/dist/ui/model-picker.d.ts +14 -0
  133. package/dist/ui/model-picker.js +161 -0
  134. package/dist/ui/terminal.d.ts +35 -0
  135. package/dist/ui/terminal.js +337 -0
  136. package/dist/wallet/manager.d.ts +10 -0
  137. package/dist/wallet/manager.js +23 -0
  138. package/package.json +79 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Context compaction for runcode.
3
+ * When conversation history approaches the context window limit,
4
+ * summarize older messages and replace them with the summary.
5
+ */
6
+ import { ModelClient } from './llm.js';
7
+ import type { Dialogue } from './types.js';
8
+ export declare const COMPACT_HEADER = "[CONTEXT COMPACTION] Earlier turns in this conversation were compacted to save context space. The summary below describes work that was already completed, and the current session state may still reflect that work (for example, files may already be changed). Use the summary and the current state to continue from where things left off, and avoid repeating work:";
9
+ /**
10
+ * Check if compaction is needed and perform it if so.
11
+ * Returns the (possibly compacted) history.
12
+ */
13
+ export declare function autoCompactIfNeeded(history: Dialogue[], model: string, client: ModelClient, debug?: boolean): Promise<{
14
+ history: Dialogue[];
15
+ compacted: boolean;
16
+ }>;
17
+ /**
18
+ * Force compaction regardless of threshold (for /compact command).
19
+ */
20
+ export declare function forceCompact(history: Dialogue[], model: string, client: ModelClient, debug?: boolean): Promise<{
21
+ history: Dialogue[];
22
+ compacted: boolean;
23
+ }>;
24
+ /**
25
+ * Clear old tool results AND truncate old tool_use inputs to save tokens.
26
+ * This is the primary defense against context snowball:
27
+ * - tool_result content (Read output, Bash output, Grep matches) grows fast
28
+ * - tool_use input (Edit replacements, Bash commands) also accumulates
29
+ * Both are cleared for all but the last N tool exchanges.
30
+ */
31
+ export declare function microCompact(history: Dialogue[], keepLastN?: number): Dialogue[];
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Context compaction for runcode.
3
+ * When conversation history approaches the context window limit,
4
+ * summarize older messages and replace them with the summary.
5
+ */
6
+ import { estimateHistoryTokens, getCompactionThreshold, COMPACTION_SUMMARY_RESERVE, } from './tokens.js';
7
+ // Structured compaction prompt (pattern from nousresearch/hermes-agent
8
+ // `agent/context_compressor.py`). The structured sections preserve more
9
+ // signal than free-form summaries and make it easier for the model to
10
+ // continue work from where it left off.
11
+ export const COMPACT_HEADER = `[CONTEXT COMPACTION] Earlier turns in this conversation were compacted to save context space. The summary below describes work that was already completed, and the current session state may still reflect that work (for example, files may already be changed). Use the summary and the current state to continue from where things left off, and avoid repeating work:`;
12
+ const COMPACT_SYSTEM_PROMPT = `You are a conversation summarizer. Produce a STRUCTURED summary of the conversation so far that preserves all decision-relevant context for continuing the task.
13
+
14
+ Critical rules:
15
+ - Preserve EXACT file paths, function names, line numbers, variable names
16
+ - Preserve EXACT error messages (verbatim)
17
+ - Preserve user preferences and corrections (especially "don't do X" instructions)
18
+ - Preserve decisions with their rationale (not just the decision)
19
+ - DO NOT include reasoning that led to decisions — only the decisions themselves
20
+ - DO NOT include pleasantries, meta-commentary, or apologies
21
+ - Use bullet points inside each section
22
+ - Be specific: "edited src/foo.ts:42 to add error handling" not "made some changes"
23
+
24
+ REQUIRED output format (use these exact section headers):
25
+
26
+ ## Goal
27
+ [One clear sentence: what the user is trying to accomplish]
28
+
29
+ ## Progress
30
+ [Chronological bullet list of what has been done so far]
31
+
32
+ ## Decisions
33
+ [Key decisions made, each with its rationale]
34
+
35
+ ## Files Modified
36
+ [Each file touched, with a one-line description of what changed]
37
+
38
+ ## Tool Results Still Relevant
39
+ [Any tool output (file reads, grep matches, bash output) that later steps still depend on — include the actual content, not a reference]
40
+
41
+ ## User Preferences & Corrections
42
+ [Anything the user explicitly asked for or corrected — these are load-bearing]
43
+
44
+ ## Next Steps
45
+ [What comes next, in priority order]
46
+
47
+ If there's an existing [CONTEXT COMPACTION] summary in the messages being compacted, MERGE its content into your output rather than nesting. Do not produce a summary of a summary.`;
48
+ /**
49
+ * Check if compaction is needed and perform it if so.
50
+ * Returns the (possibly compacted) history.
51
+ */
52
+ export async function autoCompactIfNeeded(history, model, client, debug) {
53
+ const currentTokens = estimateHistoryTokens(history);
54
+ const threshold = getCompactionThreshold(model);
55
+ if (currentTokens < threshold) {
56
+ return { history, compacted: false };
57
+ }
58
+ if (debug) {
59
+ console.error(`[runcode] Auto-compacting: ~${currentTokens} tokens, threshold=${threshold}`);
60
+ }
61
+ const beforeTokens = estimateHistoryTokens(history);
62
+ try {
63
+ const compacted = await compactHistory(history, model, client, debug);
64
+ const afterTokens = estimateHistoryTokens(compacted);
65
+ if (afterTokens >= beforeTokens) {
66
+ if (debug) {
67
+ console.error(`[runcode] Auto-compaction grew history (${beforeTokens} → ${afterTokens}) — skipping`);
68
+ }
69
+ return { history, compacted: false };
70
+ }
71
+ return { history: compacted, compacted: true };
72
+ }
73
+ catch (err) {
74
+ if (debug) {
75
+ console.error(`[runcode] Compaction failed: ${err.message}`);
76
+ }
77
+ // Fallback: truncate oldest messages instead of crashing
78
+ const truncated = emergencyTruncate(history, threshold);
79
+ return { history: truncated, compacted: true };
80
+ }
81
+ }
82
+ /**
83
+ * Force compaction regardless of threshold (for /compact command).
84
+ */
85
+ export async function forceCompact(history, model, client, debug) {
86
+ if (history.length <= 4) {
87
+ return { history, compacted: false };
88
+ }
89
+ const beforeTokens = estimateHistoryTokens(history);
90
+ try {
91
+ const compacted = await compactHistory(history, model, client, debug);
92
+ const afterTokens = estimateHistoryTokens(compacted);
93
+ // Only accept compaction if it actually reduces tokens
94
+ if (afterTokens >= beforeTokens) {
95
+ if (debug) {
96
+ console.error(`[runcode] Compaction produced larger history (${beforeTokens} → ${afterTokens}) — reverting`);
97
+ }
98
+ return { history, compacted: false };
99
+ }
100
+ return { history: compacted, compacted: true };
101
+ }
102
+ catch (err) {
103
+ if (debug) {
104
+ console.error(`[runcode] Force compaction failed: ${err.message}`);
105
+ }
106
+ const threshold = getCompactionThreshold(model);
107
+ const truncated = emergencyTruncate(history, threshold);
108
+ return { history: truncated, compacted: true };
109
+ }
110
+ }
111
+ /**
112
+ * Compact conversation history by summarizing older messages.
113
+ */
114
+ async function compactHistory(history, model, client, debug) {
115
+ if (history.length <= 4) {
116
+ // Too few messages to compact meaningfully
117
+ return history;
118
+ }
119
+ // Split: keep the most recent messages, summarize the rest
120
+ const keepCount = findKeepBoundary(history);
121
+ const toSummarize = history.slice(0, history.length - keepCount);
122
+ const toKeep = history.slice(history.length - keepCount);
123
+ if (toSummarize.length === 0) {
124
+ return history;
125
+ }
126
+ if (debug) {
127
+ console.error(`[runcode] Summarizing ${toSummarize.length} messages, keeping ${toKeep.length}`);
128
+ }
129
+ // Build summary request
130
+ const summaryMessages = [
131
+ {
132
+ role: 'user',
133
+ content: formatForSummarization(toSummarize),
134
+ },
135
+ ];
136
+ const { content: summaryParts } = await client.complete({
137
+ model: pickCompactionModel(model),
138
+ messages: summaryMessages,
139
+ system: COMPACT_SYSTEM_PROMPT,
140
+ max_tokens: COMPACTION_SUMMARY_RESERVE,
141
+ stream: true,
142
+ });
143
+ // Extract summary text
144
+ let summaryText = '';
145
+ for (const part of summaryParts) {
146
+ if (part.type === 'text') {
147
+ summaryText += part.text;
148
+ }
149
+ }
150
+ if (!summaryText) {
151
+ throw new Error('Empty summary returned from model');
152
+ }
153
+ // Build compacted history: summary as first message, then kept messages.
154
+ // The COMPACT_HEADER prefix lets future compactions detect and merge rather
155
+ // than nest summaries.
156
+ const compacted = [
157
+ {
158
+ role: 'user',
159
+ content: `${COMPACT_HEADER}\n\n${summaryText}`,
160
+ },
161
+ {
162
+ role: 'assistant',
163
+ content: 'Got it. I have the structured context from earlier work and will continue from where things left off.',
164
+ },
165
+ ...toKeep,
166
+ ];
167
+ if (debug) {
168
+ const newTokens = estimateHistoryTokens(compacted);
169
+ console.error(`[runcode] Compacted: ${estimateHistoryTokens(history)} → ${newTokens} tokens`);
170
+ }
171
+ return compacted;
172
+ }
173
+ /**
174
+ * Find how many recent messages to keep (don't summarize).
175
+ * Keeps the most recent tool exchange + the last few user/assistant turns.
176
+ */
177
+ function findKeepBoundary(history) {
178
+ // Keep the last 8-20 messages (absolute range, not percentage)
179
+ // Prevents "never compacts" bug when history grows large
180
+ const minKeep = Math.min(8, history.length);
181
+ const maxKeep = Math.min(20, history.length - 1);
182
+ let keep = Math.max(minKeep, Math.min(maxKeep, Math.ceil(history.length * 0.3)));
183
+ // Make sure we don't split in the middle of a tool exchange
184
+ // (assistant with tool_use must be followed by user with tool_result)
185
+ while (keep < history.length) {
186
+ const boundary = history.length - keep;
187
+ const msgAtBoundary = history[boundary];
188
+ // If boundary is a user message with tool_results, include the prior assistant message
189
+ if (msgAtBoundary.role === 'user' &&
190
+ Array.isArray(msgAtBoundary.content) &&
191
+ msgAtBoundary.content.length > 0 &&
192
+ typeof msgAtBoundary.content[0] !== 'string' &&
193
+ 'type' in msgAtBoundary.content[0] &&
194
+ msgAtBoundary.content[0].type === 'tool_result') {
195
+ keep++;
196
+ continue;
197
+ }
198
+ break;
199
+ }
200
+ return Math.min(keep, history.length - 1); // Always summarize at least 1 message
201
+ }
202
+ /**
203
+ * Format messages for the summarization model.
204
+ */
205
+ function formatForSummarization(messages) {
206
+ const parts = ['Here is the conversation to summarize:\n'];
207
+ for (const msg of messages) {
208
+ const role = msg.role.toUpperCase();
209
+ if (typeof msg.content === 'string') {
210
+ parts.push(`[${role}]: ${msg.content}`);
211
+ }
212
+ else {
213
+ const textParts = [];
214
+ for (const part of msg.content) {
215
+ if ('type' in part) {
216
+ switch (part.type) {
217
+ case 'text':
218
+ textParts.push(part.text);
219
+ break;
220
+ case 'tool_use':
221
+ textParts.push(`[Called tool: ${part.name}(${JSON.stringify(part.input).slice(0, 200)})]`);
222
+ break;
223
+ case 'tool_result': {
224
+ const content = typeof part.content === 'string' ? part.content : JSON.stringify(part.content);
225
+ const truncated = content.length > 500 ? content.slice(0, 500) + '...' : content;
226
+ textParts.push(`[Tool result${part.is_error ? ' (ERROR)' : ''}: ${truncated}]`);
227
+ break;
228
+ }
229
+ case 'thinking':
230
+ // Skip thinking blocks in summary
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ if (textParts.length > 0) {
236
+ parts.push(`[${role}]: ${textParts.join('\n')}`);
237
+ }
238
+ }
239
+ }
240
+ return parts.join('\n\n');
241
+ }
242
+ /**
243
+ * Pick a cheaper/faster model for compaction to save cost.
244
+ */
245
+ function pickCompactionModel(primaryModel) {
246
+ // Use cheapest capable model for summarization to save cost
247
+ // Tier down: opus/pro → sonnet, sonnet → haiku, everything else → flash (cheapest capable)
248
+ if (primaryModel.includes('opus') || primaryModel.includes('pro')) {
249
+ return 'anthropic/claude-sonnet-4.6';
250
+ }
251
+ if (primaryModel.includes('sonnet') || primaryModel.includes('gpt-5.4') || primaryModel.includes('gemini-2.5-pro')) {
252
+ return 'anthropic/claude-haiku-4.5-20251001';
253
+ }
254
+ if (primaryModel.includes('haiku') || primaryModel.includes('mini') || primaryModel.includes('nano')) {
255
+ return 'google/gemini-2.5-flash'; // Cheapest capable model
256
+ }
257
+ // Free/unknown models — use flash
258
+ return 'google/gemini-2.5-flash';
259
+ }
260
+ /**
261
+ * Emergency fallback: drop oldest messages until under threshold.
262
+ * Used when the summarization model call itself fails.
263
+ */
264
+ function emergencyTruncate(history, targetTokens) {
265
+ const result = [...history];
266
+ while (result.length > 2 && estimateHistoryTokens(result) > targetTokens) {
267
+ result.shift();
268
+ }
269
+ // Ensure first message is from user (API requirement)
270
+ if (result.length > 0 && result[0].role === 'assistant') {
271
+ result.unshift({
272
+ role: 'user',
273
+ content: '[Earlier conversation truncated due to context limit]',
274
+ });
275
+ }
276
+ return result;
277
+ }
278
+ /**
279
+ * Clear old tool results AND truncate old tool_use inputs to save tokens.
280
+ * This is the primary defense against context snowball:
281
+ * - tool_result content (Read output, Bash output, Grep matches) grows fast
282
+ * - tool_use input (Edit replacements, Bash commands) also accumulates
283
+ * Both are cleared for all but the last N tool exchanges.
284
+ */
285
+ export function microCompact(history, keepLastN = 3) {
286
+ // Find all tool_use IDs in assistant messages, in order
287
+ const allToolUseIds = [];
288
+ for (const msg of history) {
289
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
290
+ for (const part of msg.content) {
291
+ if (part.type === 'tool_use') {
292
+ allToolUseIds.push(part.id);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ if (allToolUseIds.length <= keepLastN) {
298
+ return history;
299
+ }
300
+ // IDs to clear (all except the most recent N)
301
+ const clearIds = new Set(allToolUseIds.slice(0, -keepLastN));
302
+ if (clearIds.size === 0)
303
+ return history;
304
+ const result = [];
305
+ let changed = false;
306
+ for (const msg of history) {
307
+ if (msg.role === 'user' && Array.isArray(msg.content)) {
308
+ // Clear old tool_result content
309
+ let modified = false;
310
+ const cleared = msg.content.map((part) => {
311
+ if (part.type === 'tool_result' && clearIds.has(part.tool_use_id)) {
312
+ // Already cleared — skip
313
+ if (part.content === '[Tool result cleared to save context]')
314
+ return part;
315
+ modified = true;
316
+ return {
317
+ type: 'tool_result',
318
+ tool_use_id: part.tool_use_id,
319
+ content: '[Tool result cleared to save context]',
320
+ is_error: part.is_error,
321
+ };
322
+ }
323
+ return part;
324
+ });
325
+ if (modified) {
326
+ changed = true;
327
+ result.push({ role: 'user', content: cleared });
328
+ }
329
+ else {
330
+ result.push(msg);
331
+ }
332
+ }
333
+ else if (msg.role === 'assistant' && Array.isArray(msg.content)) {
334
+ // Truncate old tool_use inputs (keep name + id, shrink input)
335
+ let modified = false;
336
+ const truncated = msg.content.map((part) => {
337
+ if (part.type === 'tool_use' && clearIds.has(part.id)) {
338
+ const inputStr = JSON.stringify(part.input);
339
+ if (inputStr.length > 200) {
340
+ modified = true;
341
+ // Keep just enough to know what was called
342
+ const summary = {};
343
+ const input = part.input;
344
+ for (const [k, v] of Object.entries(input)) {
345
+ const val = typeof v === 'string' ? v.slice(0, 100) : v;
346
+ summary[k] = val;
347
+ }
348
+ return { ...part, input: summary };
349
+ }
350
+ }
351
+ return part;
352
+ });
353
+ if (modified) {
354
+ changed = true;
355
+ result.push({ role: 'assistant', content: truncated });
356
+ }
357
+ else {
358
+ result.push(msg);
359
+ }
360
+ }
361
+ else {
362
+ result.push(msg);
363
+ }
364
+ }
365
+ return changed ? result : history;
366
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Context Manager for runcode
3
+ * Assembles system instructions, reads project config, injects environment info.
4
+ */
5
+ /**
6
+ * Build the full system instructions array for a session.
7
+ * Result is memoized per workingDir for the process lifetime.
8
+ */
9
+ export declare function assembleInstructions(workingDir: string): string[];
10
+ /** Invalidate cache for a workingDir (call after /clear or session reset). */
11
+ export declare function invalidateInstructionCache(workingDir: string): void;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Context Manager for runcode
3
+ * Assembles system instructions, reads project config, injects environment info.
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ // ─── System Instructions Assembly ──────────────────────────────────────────
9
+ const BASE_INSTRUCTIONS = `You are runcode, an AI coding agent that helps users with software engineering tasks.
10
+ You have access to tools for reading, writing, editing files, running shell commands, searching codebases, web browsing, and more.
11
+
12
+ # Core Principles
13
+ - Read before writing: always understand existing code before making changes.
14
+ - Be precise: make minimal, targeted changes. Don't refactor code you weren't asked to touch.
15
+ - Be safe: never introduce security vulnerabilities. Validate at system boundaries.
16
+ - Be honest: if you're unsure, say so. Don't guess at implementation details.
17
+
18
+ # Tool Usage
19
+ - **Read**: Read files with line numbers. Use offset/limit for large files.
20
+ - **Edit**: Targeted string replacement (preferred for existing files). old_string must be unique.
21
+ - **Write**: Create new files or full rewrites.
22
+ - **Bash**: Run shell commands. Default timeout 2min. Batch sequential commands with && to reduce round-trips.
23
+ - **Glob**: Find files by pattern. Skips node_modules/.git.
24
+ - **Grep**: Regex search. Default: file paths. output_mode "content" for matching lines.
25
+ - **WebFetch** / **WebSearch**: Fetch pages or search the web.
26
+ - **Task**: Track multi-step work.
27
+ - **Agent**: Spawn parallel sub-agents.
28
+
29
+ # Best Practices
30
+ - Glob/Grep before Read; Read before Edit.
31
+ - **Parallel**: call independent tools together in one response.
32
+ - **Batch bash**: combine sequential shell commands into one Bash call with && or a script. Only split when you need to inspect intermediate output.
33
+ - **AskUser**: Only use AskUser when you are about to perform a destructive action (deleting files, dropping databases) and need explicit confirmation. NEVER use AskUser to ask what the user wants — just answer their message directly. If their request is vague, make a reasonable assumption and proceed.
34
+ - Never write to /etc, /usr, ~/.ssh, ~/.aws. Don't commit secrets.
35
+ - Type /help to see all slash commands.`;
36
+ // Cache assembled instructions per workingDir — avoids re-running git commands
37
+ // when sub-agents are spawned (common in parallel tool use patterns).
38
+ const _instructionCache = new Map();
39
+ /**
40
+ * Build the full system instructions array for a session.
41
+ * Result is memoized per workingDir for the process lifetime.
42
+ */
43
+ export function assembleInstructions(workingDir) {
44
+ const cached = _instructionCache.get(workingDir);
45
+ if (cached)
46
+ return cached;
47
+ const parts = [BASE_INSTRUCTIONS];
48
+ // Read RUNCODE.md or CLAUDE.md from the project
49
+ const projectConfig = readProjectConfig(workingDir);
50
+ if (projectConfig) {
51
+ parts.push(`# Project Instructions\n\n${projectConfig}`);
52
+ }
53
+ // Inject environment info
54
+ parts.push(buildEnvironmentSection(workingDir));
55
+ // Inject git context
56
+ const gitInfo = getGitContext(workingDir);
57
+ if (gitInfo) {
58
+ parts.push(`# Git Context\n\n${gitInfo}`);
59
+ }
60
+ _instructionCache.set(workingDir, parts);
61
+ return parts;
62
+ }
63
+ /** Invalidate cache for a workingDir (call after /clear or session reset). */
64
+ export function invalidateInstructionCache(workingDir) {
65
+ _instructionCache.delete(workingDir);
66
+ }
67
+ // ─── Project Config ────────────────────────────────────────────────────────
68
+ /**
69
+ * Look for RUNCODE.md, then CLAUDE.md in the working directory and parents.
70
+ */
71
+ function readProjectConfig(dir) {
72
+ const configNames = ['RUNCODE.md', 'CLAUDE.md'];
73
+ let current = path.resolve(dir);
74
+ const root = path.parse(current).root;
75
+ while (current !== root) {
76
+ for (const name of configNames) {
77
+ const filePath = path.join(current, name);
78
+ try {
79
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
80
+ if (content)
81
+ return content;
82
+ }
83
+ catch {
84
+ // File doesn't exist, keep looking
85
+ }
86
+ }
87
+ const parent = path.dirname(current);
88
+ if (parent === current)
89
+ break;
90
+ current = parent;
91
+ }
92
+ return null;
93
+ }
94
+ // ─── Environment ───────────────────────────────────────────────────────────
95
+ function buildEnvironmentSection(workingDir) {
96
+ const lines = ['# Environment'];
97
+ lines.push(`- Working directory: ${workingDir}`);
98
+ lines.push(`- Platform: ${process.platform}`);
99
+ lines.push(`- Node.js: ${process.version}`);
100
+ // Detect shell
101
+ const shell = process.env.SHELL || process.env.COMSPEC || 'unknown';
102
+ lines.push(`- Shell: ${path.basename(shell)}`);
103
+ // Date
104
+ lines.push(`- Date: ${new Date().toISOString().split('T')[0]}`);
105
+ return lines.join('\n');
106
+ }
107
+ // ─── Git Context ───────────────────────────────────────────────────────────
108
+ const GIT_TIMEOUT_MS = 5_000;
109
+ // Max chars for git log output — long commit messages can bloat the system prompt
110
+ const MAX_GIT_LOG_CHARS = 2_000;
111
+ function getGitContext(workingDir) {
112
+ try {
113
+ const isGit = execSync('git rev-parse --is-inside-work-tree', {
114
+ cwd: workingDir,
115
+ encoding: 'utf-8',
116
+ stdio: ['pipe', 'pipe', 'pipe'],
117
+ timeout: GIT_TIMEOUT_MS,
118
+ }).trim();
119
+ if (isGit !== 'true')
120
+ return null;
121
+ const lines = [];
122
+ // Current branch
123
+ try {
124
+ const branch = execSync('git branch --show-current', {
125
+ cwd: workingDir,
126
+ encoding: 'utf-8',
127
+ stdio: ['pipe', 'pipe', 'pipe'],
128
+ timeout: GIT_TIMEOUT_MS,
129
+ }).trim();
130
+ if (branch)
131
+ lines.push(`Branch: ${branch}`);
132
+ }
133
+ catch { /* detached HEAD or error */ }
134
+ // Git status (brief)
135
+ try {
136
+ const status = execSync('git status --short', {
137
+ cwd: workingDir,
138
+ encoding: 'utf-8',
139
+ stdio: ['pipe', 'pipe', 'pipe'],
140
+ timeout: GIT_TIMEOUT_MS,
141
+ }).trim();
142
+ if (status) {
143
+ const fileCount = status.split('\n').length;
144
+ lines.push(`Changed files: ${fileCount}`);
145
+ }
146
+ else {
147
+ lines.push('Status: clean');
148
+ }
149
+ }
150
+ catch { /* ignore */ }
151
+ // Recent commits (last 5) — capped to prevent huge messages bloating context
152
+ try {
153
+ let log = execSync('git log --oneline -5', {
154
+ cwd: workingDir,
155
+ encoding: 'utf-8',
156
+ stdio: ['pipe', 'pipe', 'pipe'],
157
+ timeout: GIT_TIMEOUT_MS,
158
+ }).trim();
159
+ if (log) {
160
+ if (log.length > MAX_GIT_LOG_CHARS) {
161
+ log = log.slice(0, MAX_GIT_LOG_CHARS) + '\n... (truncated)';
162
+ }
163
+ lines.push(`\nRecent commits:\n${log}`);
164
+ }
165
+ }
166
+ catch { /* ignore */ }
167
+ // Git user
168
+ try {
169
+ const user = execSync('git config user.name', {
170
+ cwd: workingDir,
171
+ encoding: 'utf-8',
172
+ stdio: ['pipe', 'pipe', 'pipe'],
173
+ timeout: GIT_TIMEOUT_MS,
174
+ }).trim();
175
+ if (user)
176
+ lines.push(`User: ${user}`);
177
+ }
178
+ catch { /* ignore */ }
179
+ return lines.length > 0 ? lines.join('\n') : null;
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Classify model/runtime errors so recovery and UX can be more consistent.
3
+ */
4
+ export type AgentErrorCategory = 'rate_limit' | 'payment' | 'network' | 'timeout' | 'context_limit' | 'server' | 'unknown';
5
+ export interface AgentErrorInfo {
6
+ category: AgentErrorCategory;
7
+ label: 'RateLimit' | 'Payment' | 'Network' | 'Timeout' | 'Context' | 'Server' | 'Unknown';
8
+ isTransient: boolean;
9
+ }
10
+ export declare function classifyAgentError(message: string): AgentErrorInfo;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Classify model/runtime errors so recovery and UX can be more consistent.
3
+ */
4
+ function includesAny(text, patterns) {
5
+ return patterns.some((p) => text.includes(p));
6
+ }
7
+ export function classifyAgentError(message) {
8
+ const err = message.toLowerCase();
9
+ if (includesAny(err, [
10
+ 'insufficient',
11
+ 'payment',
12
+ 'verification failed',
13
+ 'balance',
14
+ '402',
15
+ 'free tier',
16
+ ])) {
17
+ return { category: 'payment', label: 'Payment', isTransient: false };
18
+ }
19
+ if (includesAny(err, [
20
+ '429',
21
+ 'rate limit',
22
+ 'too many requests',
23
+ ])) {
24
+ return { category: 'rate_limit', label: 'RateLimit', isTransient: true };
25
+ }
26
+ if (includesAny(err, [
27
+ 'prompt is too long',
28
+ 'context length',
29
+ 'maximum context',
30
+ ])) {
31
+ return { category: 'context_limit', label: 'Context', isTransient: false };
32
+ }
33
+ if (includesAny(err, [
34
+ 'timeout',
35
+ 'timed out',
36
+ ])) {
37
+ return { category: 'timeout', label: 'Timeout', isTransient: true };
38
+ }
39
+ if (includesAny(err, [
40
+ 'fetch failed',
41
+ 'econnrefused',
42
+ 'econnreset',
43
+ 'enotfound',
44
+ 'network',
45
+ 'socket hang up',
46
+ ])) {
47
+ return { category: 'network', label: 'Network', isTransient: true };
48
+ }
49
+ if (includesAny(err, [
50
+ '500',
51
+ '502',
52
+ '503',
53
+ '504',
54
+ 'internal server error',
55
+ 'bad gateway',
56
+ 'service unavailable',
57
+ ])) {
58
+ return { category: 'server', label: 'Server', isTransient: true };
59
+ }
60
+ return { category: 'unknown', label: 'Unknown', isTransient: false };
61
+ }