osai-agent 4.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.
- package/LICENSE +7 -0
- package/package.json +72 -0
- package/src/agent/context.js +141 -0
- package/src/agent/loop/context-summary.js +196 -0
- package/src/agent/loop/directory-utils.js +102 -0
- package/src/agent/loop/local.js +196 -0
- package/src/agent/loop/loop-detection.js +288 -0
- package/src/agent/loop/stream-parser.js +515 -0
- package/src/agent/loop/tool-executor.js +470 -0
- package/src/agent/loop/verification.js +263 -0
- package/src/agent/loop/websocket.js +80 -0
- package/src/agent/prompt.js +259 -0
- package/src/agent/react-loop.js +697 -0
- package/src/agent/subagent.js +263 -0
- package/src/commands/config.js +53 -0
- package/src/commands/connect.js +190 -0
- package/src/commands/devices.js +121 -0
- package/src/commands/login.js +77 -0
- package/src/commands/logout.js +31 -0
- package/src/commands/mcp.js +258 -0
- package/src/commands/provider.js +633 -0
- package/src/commands/register.js +74 -0
- package/src/commands/run.js +150 -0
- package/src/commands/search.js +64 -0
- package/src/commands/session.js +57 -0
- package/src/commands/skills.js +54 -0
- package/src/commands/stop-subagent.js +58 -0
- package/src/index.js +208 -0
- package/src/llm/direct.js +317 -0
- package/src/memory/store.js +215 -0
- package/src/mock-readline.js +27 -0
- package/src/parser/dependencies.js +71 -0
- package/src/parser/markdown.js +505 -0
- package/src/parser/stream.js +96 -0
- package/src/prompts/modes/CODING.js +160 -0
- package/src/prompts/modes/GENERAL.js +105 -0
- package/src/prompts/modes/NETWORK.js +69 -0
- package/src/prompts/modes/SSH.js +53 -0
- package/src/prompts/systemPrompt.js +85 -0
- package/src/safety/check.js +210 -0
- package/src/services/crypto.js +78 -0
- package/src/services/executor.js +68 -0
- package/src/services/history.js +58 -0
- package/src/services/server-url.js +11 -0
- package/src/services/session.js +194 -0
- package/src/services/ssh.js +176 -0
- package/src/services/websocket.js +112 -0
- package/src/skills/loader.js +231 -0
- package/src/tools/browser.js +434 -0
- package/src/tools/local.js +1254 -0
- package/src/tools/mcp-client.js +209 -0
- package/src/tools/registry.js +132 -0
- package/src/tools/search-providers.js +237 -0
- package/src/tools/ssh.js +74 -0
- package/src/ui/App.js +2031 -0
- package/src/ui/animation.js +47 -0
- package/src/ui/components/AskUserDialog.js +33 -0
- package/src/ui/components/ConfirmationDialog.js +45 -0
- package/src/ui/components/DiffView.js +201 -0
- package/src/ui/components/Header.js +157 -0
- package/src/ui/components/HistoryPicker.js +130 -0
- package/src/ui/components/InputShell.js +22 -0
- package/src/ui/components/MessageHistory.js +1200 -0
- package/src/ui/components/ModalPanel.js +40 -0
- package/src/ui/components/ModePicker.js +161 -0
- package/src/ui/components/PlanDialog.js +48 -0
- package/src/ui/components/ProviderMenu.js +1095 -0
- package/src/ui/components/SavePicker.js +106 -0
- package/src/ui/components/SelectMenu.js +194 -0
- package/src/ui/components/SlashMenu.js +168 -0
- package/src/ui/components/SubagentPanel.js +138 -0
- package/src/ui/components/TextInputSafe.js +117 -0
- package/src/ui/components/TodoPanel.js +54 -0
- package/src/ui/components/ToolExecution.js +261 -0
- package/src/ui/components/TranscriptViewport.js +99 -0
- package/src/ui/diff.js +249 -0
- package/src/ui/h.js +7 -0
- package/src/ui/mouse-scroll.js +63 -0
- package/src/ui/slash-picker.js +58 -0
- package/src/ui/terminal.js +41 -0
- package/src/ui/theme.js +5 -0
- package/src/ui/welcome.js +12 -0
- package/src/utils/constants.js +231 -0
- package/src/utils/helpers.js +154 -0
- package/src/utils/logger.js +81 -0
- package/src/utils/sound.js +33 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { buildSystemPrompt } from '../../prompts/systemPrompt.js';
|
|
2
|
+
import { streamLocalCompletion, getLocalProviderConfig } from '../../llm/direct.js';
|
|
3
|
+
import { discoverSkills } from '../../skills/loader.js';
|
|
4
|
+
import { mcpClientManager } from '../../tools/mcp-client.js';
|
|
5
|
+
import { memory } from '../../memory/store.js';
|
|
6
|
+
import { DEFAULTS, COMPLETION_SIGNALS } from '../../utils/constants.js';
|
|
7
|
+
import { logger } from '../../utils/logger.js';
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
async _callWorkerLocal() {
|
|
11
|
+
const cleanHistory = [...this.conversationHistory];
|
|
12
|
+
if (cleanHistory.length === 0) throw new Error('No valid conversation history');
|
|
13
|
+
|
|
14
|
+
const lastMessage = cleanHistory[cleanHistory.length - 1];
|
|
15
|
+
const historyWithoutLast = cleanHistory.slice(0, -1);
|
|
16
|
+
let memoryContext = null;
|
|
17
|
+
try { memoryContext = await memory.buildContextSummary(3); } catch {}
|
|
18
|
+
|
|
19
|
+
const estTokens = this._estimateTokens();
|
|
20
|
+
this.totalTokensUsed = estTokens;
|
|
21
|
+
this.onStats(estTokens);
|
|
22
|
+
|
|
23
|
+
const maxAttempts = DEFAULTS.API_RETRY_ATTEMPTS;
|
|
24
|
+
let retrying = false;
|
|
25
|
+
|
|
26
|
+
const provider = getLocalProviderConfig();
|
|
27
|
+
if (!provider) {
|
|
28
|
+
this.onError('No local LLM provider configured. Run: osai-agent provider set <type> --key <key>');
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let skillsCatalog = [];
|
|
33
|
+
try { skillsCatalog = await discoverSkills(); } catch {}
|
|
34
|
+
|
|
35
|
+
this.currentProvider = provider.type;
|
|
36
|
+
this.currentModel = provider.model;
|
|
37
|
+
|
|
38
|
+
const mcpConfig = this._config.get('mcpServers', {});
|
|
39
|
+
const mcpTools = Object.keys(mcpConfig).length > 0
|
|
40
|
+
? mcpClientManager.getToolDescriptions()
|
|
41
|
+
: [];
|
|
42
|
+
|
|
43
|
+
const deviceInfo = this.device?.name
|
|
44
|
+
? `${this.device.name} (${this.device.ip}:${this.device.port || 22})`
|
|
45
|
+
: '';
|
|
46
|
+
|
|
47
|
+
const systemPrompt = buildSystemPrompt(this._userOS, this.mode, {
|
|
48
|
+
executionMode: this.executionMode,
|
|
49
|
+
mcpTools,
|
|
50
|
+
skills: skillsCatalog.map(s => ({ name: s.name, description: s.description, source: s.source })),
|
|
51
|
+
isSubagent: this.isSubagent,
|
|
52
|
+
deviceInfo,
|
|
53
|
+
...(memoryContext ? { memoryContext } : {}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
57
|
+
if (this._shouldCancel()) {
|
|
58
|
+
logger.info('Local worker call cancelled before attempt');
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const stream = streamLocalCompletion({
|
|
64
|
+
provider,
|
|
65
|
+
systemPrompt,
|
|
66
|
+
history: historyWithoutLast,
|
|
67
|
+
message: lastMessage.content,
|
|
68
|
+
signal: this.abortController?.signal,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const reader = stream.getReader();
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let fullResponse = '';
|
|
74
|
+
let responseText = '';
|
|
75
|
+
let inThinkBlock = false;
|
|
76
|
+
let thinkContentAccumulator = '';
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
if (this._shouldCancel()) break;
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done) break;
|
|
82
|
+
|
|
83
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
84
|
+
const lines = chunk.split('\n');
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (!line.startsWith('data: ')) continue;
|
|
88
|
+
const payload = line.slice(6).trim();
|
|
89
|
+
if (!payload || payload === '[DONE]') continue;
|
|
90
|
+
try {
|
|
91
|
+
const event = JSON.parse(payload);
|
|
92
|
+
if (event.type === 'chunk' && event.content) {
|
|
93
|
+
fullResponse += event.content;
|
|
94
|
+
responseText += event.content;
|
|
95
|
+
let content = event.content;
|
|
96
|
+
// Detect <think> blocks and route to thought handler
|
|
97
|
+
const thinkOpenIdx = content.indexOf('<think>');
|
|
98
|
+
const thinkCloseIdx = content.indexOf('</think>');
|
|
99
|
+
if (inThinkBlock || thinkOpenIdx !== -1) {
|
|
100
|
+
if (inThinkBlock) {
|
|
101
|
+
if (thinkCloseIdx !== -1) {
|
|
102
|
+
const beforeClose = content.slice(0, thinkCloseIdx);
|
|
103
|
+
if (beforeClose.trim()) {
|
|
104
|
+
thinkContentAccumulator += beforeClose;
|
|
105
|
+
this.onThought(beforeClose);
|
|
106
|
+
}
|
|
107
|
+
inThinkBlock = false;
|
|
108
|
+
thinkContentAccumulator = '';
|
|
109
|
+
this.onThinkingEnd();
|
|
110
|
+
content = content.slice(thinkCloseIdx + 8);
|
|
111
|
+
} else {
|
|
112
|
+
thinkContentAccumulator += content;
|
|
113
|
+
this.onThought(content);
|
|
114
|
+
content = '';
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const beforeThink = content.slice(0, thinkOpenIdx);
|
|
118
|
+
const afterThink = content.slice(thinkOpenIdx + 7);
|
|
119
|
+
inThinkBlock = true;
|
|
120
|
+
thinkContentAccumulator = '';
|
|
121
|
+
this.onThinkingStart();
|
|
122
|
+
const closeInRemainder = afterThink.indexOf('</think>');
|
|
123
|
+
if (closeInRemainder !== -1) {
|
|
124
|
+
const thinkContent = afterThink.slice(0, closeInRemainder);
|
|
125
|
+
if (thinkContent.trim()) {
|
|
126
|
+
thinkContentAccumulator = thinkContent;
|
|
127
|
+
this.onThought(thinkContent);
|
|
128
|
+
}
|
|
129
|
+
inThinkBlock = false;
|
|
130
|
+
thinkContentAccumulator = '';
|
|
131
|
+
this.onThinkingEnd();
|
|
132
|
+
content = (beforeThink || '') + afterThink.slice(closeInRemainder + 8);
|
|
133
|
+
} else {
|
|
134
|
+
if (afterThink.trim()) {
|
|
135
|
+
thinkContentAccumulator = afterThink;
|
|
136
|
+
this.onThought(afterThink);
|
|
137
|
+
}
|
|
138
|
+
content = beforeThink || '';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (content) {
|
|
143
|
+
this.onMarkdown(content);
|
|
144
|
+
}
|
|
145
|
+
} else if (event.type === 'error') {
|
|
146
|
+
throw new Error(event.message);
|
|
147
|
+
}
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (e.message && e.message !== 'Unexpected end of JSON input') {
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clean up unclosed think block at end of stream
|
|
157
|
+
if (inThinkBlock) {
|
|
158
|
+
inThinkBlock = false;
|
|
159
|
+
thinkContentAccumulator = '';
|
|
160
|
+
this.onThinkingEnd();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (this._shouldCancel()) return null;
|
|
164
|
+
|
|
165
|
+
if (retrying) { this.onUpdateLastText(''); retrying = false; }
|
|
166
|
+
|
|
167
|
+
if (!fullResponse) return null;
|
|
168
|
+
|
|
169
|
+
const cleanFullResponse = fullResponse
|
|
170
|
+
.replace(/^\s*\{(?:\\")?tool(?:\\")?\s*:\s*.*$/gim, '')
|
|
171
|
+
.replace(/<tool\b[^>]*>[\s\S]*?<\/tool\s*>|<tool\b[^>]*\/>/gim, '')
|
|
172
|
+
.trim();
|
|
173
|
+
this._appendConversationMessage('assistant', cleanFullResponse || fullResponse);
|
|
174
|
+
const extractedToolCalls = this._extractToolCalls(fullResponse);
|
|
175
|
+
this._detectCompletionSignal(fullResponse);
|
|
176
|
+
return { toolCalls: extractedToolCalls, fullResponse };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error.name === 'AbortError' || this._shouldCancel()) {
|
|
179
|
+
logger.info('Local worker call aborted');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (attempt >= maxAttempts - 1) {
|
|
183
|
+
this.onError(error.message);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
retrying = true;
|
|
187
|
+
const backoffMs = DEFAULTS.API_RETRY_DELAY * (attempt + 1);
|
|
188
|
+
this.onMarkdown(`⏳ Retrying in ${Math.ceil(backoffMs / 1000)}s...`);
|
|
189
|
+
const slept = await this._cancellableSleep(backoffMs);
|
|
190
|
+
if (!slept || this._shouldCancel()) return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
},
|
|
196
|
+
};
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { TOOLS, TOOL_SEQUENCE_WINDOW_SIZE, MAX_ITERATIONS, MODES } from '../../utils/constants.js';
|
|
2
|
+
import { resolvePath } from '../../tools/local.js';
|
|
3
|
+
import { simpleHash } from '../../utils/helpers.js';
|
|
4
|
+
import { logger } from '../../utils/logger.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
_buildLoopBlockedMessage(toolCall) {
|
|
10
|
+
const tool = toolCall?.tool || TOOLS.LOCAL_CMD;
|
|
11
|
+
const path = toolCall?.path || '';
|
|
12
|
+
if (tool === TOOLS.READ_FILE && path) {
|
|
13
|
+
const normalizedPath = this._normalizeTrackedPath(path) || path;
|
|
14
|
+
const count = this._readFileCallCount.get(normalizedPath) || 0;
|
|
15
|
+
return `[BLOCKED] The file "${path}" has been read ${count} times without any modification in between. Reading the same file repeatedly is pointless — the content does not change. Use the content already in context to make your edits with EDIT_FILE or WRITE_FILE.`;
|
|
16
|
+
}
|
|
17
|
+
if (this._searchStreak >= 4) {
|
|
18
|
+
return `[BLOCKED] You have performed ${this._searchStreak} consecutive search operations (grep, glob, search) without any edit. Searching repeatedly without making changes is a loop. Stop, examine what you already know, and either make an edit or change strategy.`;
|
|
19
|
+
}
|
|
20
|
+
if (this._detectCrossToolLoop()) {
|
|
21
|
+
return '[BLOCKED] Cross-tool loop detected: you are cycling through READ → EDIT → DIAG (or similar patterns) without making real progress. Stop, re-assess the problem, and change your approach completely.';
|
|
22
|
+
}
|
|
23
|
+
return '[BLOCKED] Repeated identical command/output detected. Stopping to avoid an infinite loop.';
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
_detectCrossToolLoop() {
|
|
27
|
+
if (this._toolSequenceWindow.length < TOOL_SEQUENCE_WINDOW_SIZE) return false;
|
|
28
|
+
|
|
29
|
+
const pairs = [];
|
|
30
|
+
for (let i = this._toolSequenceWindow.length - 4; i < this._toolSequenceWindow.length; i++) {
|
|
31
|
+
if (i >= 0) pairs.push(this._toolSequenceWindow[i].tool);
|
|
32
|
+
}
|
|
33
|
+
if (pairs.length < 4) return false;
|
|
34
|
+
|
|
35
|
+
const alternating = pairs.every((t, i) => {
|
|
36
|
+
if (i < 2) return true;
|
|
37
|
+
return t === pairs[i - 2];
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (alternating && pairs[0] !== pairs[1]) {
|
|
41
|
+
const targets = this._toolSequenceWindow.slice(-4).map(e => e.target);
|
|
42
|
+
const uniqueTargets = new Set(targets.filter(Boolean));
|
|
43
|
+
if (uniqueTargets.size <= 2) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
_batchFileReads(toolCalls) {
|
|
52
|
+
if (!toolCalls || toolCalls.length <= 1) return toolCalls || [];
|
|
53
|
+
const FILE_READ_TOOLS = new Set([
|
|
54
|
+
TOOLS.READ_FILE, TOOLS.GLOB, TOOLS.GREP,
|
|
55
|
+
TOOLS.SEARCH_FILE, TOOLS.LIST_DIR, TOOLS.TREE_VIEW, TOOLS.FILE_INFO,
|
|
56
|
+
]);
|
|
57
|
+
const allReads = toolCalls.every(tc => FILE_READ_TOOLS.has(tc.tool));
|
|
58
|
+
if (allReads) return toolCalls;
|
|
59
|
+
return [toolCalls[0]];
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
_getFileDependencies(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
if (!filePath) return [];
|
|
65
|
+
const resolved = resolvePath(filePath);
|
|
66
|
+
if (!resolved) return [];
|
|
67
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
68
|
+
const baseDir = path.dirname(resolved);
|
|
69
|
+
const result = [];
|
|
70
|
+
|
|
71
|
+
if (ext === '.html' || ext === '.htm') {
|
|
72
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
73
|
+
const linkRegex = /<link[^>]+href\s*=\s*["']([^"']+)["']/gi;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = linkRegex.exec(content)) !== null) {
|
|
76
|
+
const href = m[1].split('?')[0].split('#')[0];
|
|
77
|
+
if (href && !href.startsWith('http')) {
|
|
78
|
+
const resolvedPath = path.resolve(baseDir, href);
|
|
79
|
+
if (fs.existsSync(resolvedPath)) result.push(resolvedPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const scriptRegex = /<script[^>]+src\s*=\s*["']([^"']+)["']/gi;
|
|
83
|
+
while ((m = scriptRegex.exec(content)) !== null) {
|
|
84
|
+
const src = m[1].split('?')[0].split('#')[0];
|
|
85
|
+
if (src && !src.startsWith('http')) {
|
|
86
|
+
const resolvedPath = path.resolve(baseDir, src);
|
|
87
|
+
if (fs.existsSync(resolvedPath)) result.push(resolvedPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else if (ext === '.js' || ext === '.mjs' || ext === '.jsx' || ext === '.ts' || ext === '.tsx') {
|
|
91
|
+
const content = fs.readFileSync(resolved, 'utf-8');
|
|
92
|
+
const importRegex = /(?:import\s+(?:[\s\S]*?\s+from\s+)?["']([^"']+)["']|require\s*\(\s*["']([^"']+)["']\s*\))/g;
|
|
93
|
+
while ((m = importRegex.exec(content)) !== null) {
|
|
94
|
+
const spec = m[1] || m[2];
|
|
95
|
+
if (spec && (spec.startsWith('.') || spec.startsWith('/'))) {
|
|
96
|
+
const resolvedPath = path.resolve(baseDir, spec);
|
|
97
|
+
const exts = ['.js', '.mjs', '.jsx', '.ts', '.tsx', '.json', ''];
|
|
98
|
+
for (const ext of exts) {
|
|
99
|
+
const attempt = resolvedPath + ext;
|
|
100
|
+
if (fs.existsSync(attempt)) {
|
|
101
|
+
result.push(attempt);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return [...new Set(result)];
|
|
110
|
+
} catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
_detectRepeatedToolLoop(toolCall, result, outputForModel) {
|
|
116
|
+
const signature = JSON.stringify(toolCall || {});
|
|
117
|
+
const fingerprint = `${result?.success ? '1' : '0'}:${String(outputForModel || '').trim().slice(0, 500)}`;
|
|
118
|
+
|
|
119
|
+
if (signature === this._lastToolSignature) this._sameToolCallStreak += 1;
|
|
120
|
+
else this._sameToolCallStreak = 1;
|
|
121
|
+
|
|
122
|
+
if (signature === this._lastToolSignature && fingerprint === this._lastToolResultFingerprint) {
|
|
123
|
+
this._sameToolResultStreak += 1;
|
|
124
|
+
} else {
|
|
125
|
+
this._sameToolResultStreak = 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this._lastToolSignature = signature;
|
|
129
|
+
this._lastToolResultFingerprint = fingerprint;
|
|
130
|
+
|
|
131
|
+
if (!result?.success) {
|
|
132
|
+
this._consecutiveFailedToolStreak += 1;
|
|
133
|
+
} else {
|
|
134
|
+
this._consecutiveFailedToolStreak = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tool = toolCall.tool || TOOLS.LOCAL_CMD;
|
|
138
|
+
|
|
139
|
+
const SEARCH_TOOLS = new Set([TOOLS.GREP, TOOLS.SEARCH_FILE, TOOLS.GLOB, TOOLS.LIST_DIR, TOOLS.TREE_VIEW]);
|
|
140
|
+
const WRITE_TOOLS = new Set([TOOLS.WRITE_FILE, TOOLS.EDIT_FILE, TOOLS.APPEND_FILE, TOOLS.DELETE_FILE, TOOLS.MOVE_FILE, TOOLS.COPY_FILE, TOOLS.CREATE_DIR, TOOLS.RUN_SCRIPT]);
|
|
141
|
+
if (SEARCH_TOOLS.has(tool)) {
|
|
142
|
+
this._searchStreak++;
|
|
143
|
+
if (this._searchStreak >= 4) {
|
|
144
|
+
logger.debug('Search streak limit reached', { streak: this._searchStreak, mode: this.mode });
|
|
145
|
+
}
|
|
146
|
+
} else if (WRITE_TOOLS.has(tool)) {
|
|
147
|
+
this._searchStreak = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (tool === TOOLS.LOCAL_CMD && result?.success && toolCall.cmd) {
|
|
151
|
+
const cmd = toolCall.cmd;
|
|
152
|
+
const pathMatch = cmd.match(/(?:explorer|start)\s+(.+)/i);
|
|
153
|
+
if (pathMatch) {
|
|
154
|
+
const targetPath = pathMatch[1].replace(/["']/g, '').trim();
|
|
155
|
+
if (targetPath) {
|
|
156
|
+
const count = (this._localCmdPathCount.get(targetPath) || 0) + 1;
|
|
157
|
+
this._localCmdPathCount.set(targetPath, count);
|
|
158
|
+
if (count >= 3) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (tool === TOOLS.READ_FILE && toolCall.path && result?.success) {
|
|
165
|
+
const normalizedPath = this._normalizeTrackedPath(toolCall.path) || toolCall.path;
|
|
166
|
+
const count = (this._readFileCallCount.get(normalizedPath) || 0) + 1;
|
|
167
|
+
this._readFileCallCount.set(normalizedPath, count);
|
|
168
|
+
if (count >= 3 && !this.isSubagent) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (!result.output || String(result.output).trim() === '') {
|
|
172
|
+
this._consecutiveFailedToolStreak += 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if ([TOOLS.WRITE_FILE, TOOLS.EDIT_FILE, TOOLS.APPEND_FILE].includes(tool) && toolCall.path) {
|
|
176
|
+
const normalizedPath = this._normalizeTrackedPath(toolCall.path) || toolCall.path;
|
|
177
|
+
this._readFileCallCount.set(normalizedPath, 0);
|
|
178
|
+
|
|
179
|
+
if (result.success) {
|
|
180
|
+
let newContent = '';
|
|
181
|
+
try { newContent = fs.readFileSync(normalizedPath, 'utf8'); } catch {}
|
|
182
|
+
this._readFiles.add(normalizedPath);
|
|
183
|
+
this._readFileState.set(normalizedPath, {
|
|
184
|
+
lastReadInteraction: this._toolInteractionCounter,
|
|
185
|
+
lastReadIteration: this.iteration,
|
|
186
|
+
lastReadAt: new Date().toISOString(),
|
|
187
|
+
contentHash: simpleHash(newContent),
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
this._readFileState.delete(normalizedPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (this._searchStreak >= 4 && !this.isSubagent) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (this._sameToolCallStreak >= 3 && this._sameToolResultStreak >= 2)
|
|
199
|
+
|| this._consecutiveFailedToolStreak >= 5;
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
_buildToolResultMessage(toolCall, result, fullOutput) {
|
|
203
|
+
const toolName = toolCall.tool || TOOLS.LOCAL_CMD;
|
|
204
|
+
const identifier = toolCall.cmd || toolCall.path || toolCall.url || toolCall.query || '';
|
|
205
|
+
const failureHint = !result.success
|
|
206
|
+
? this.isSubagent
|
|
207
|
+
? `\nWARNING: This tool FAILED. Do NOT retry the same tool or a similar tool expecting a different result. If it failed because confirmation, sensitive-file access, permission, or ENOENT blocked this path, skip that target, continue with another allowed read-only tool, and mention the skipped target in your final summary.`
|
|
208
|
+
: `\nWARNING: This tool FAILED. Do NOT retry the same tool or a similar tool expecting a different result.\nIf the error is unrecoverable (e.g. missing binary, permission denied, ENOENT), output [BLOCKED] immediately.`
|
|
209
|
+
: '';
|
|
210
|
+
|
|
211
|
+
let readRepeatHint = '';
|
|
212
|
+
if (toolCall.tool === TOOLS.READ_FILE && toolCall.path && result.success) {
|
|
213
|
+
const normalizedPath = this._normalizeTrackedPath(toolCall.path) || toolCall.path;
|
|
214
|
+
const count = this._readFileCallCount.get(normalizedPath) || 0;
|
|
215
|
+
if (count >= 2) {
|
|
216
|
+
readRepeatHint = `\nWARNING: You have now read "${toolCall.path}" ${count} time(s). The file content does not change between reads. Do NOT read it again. Use the content in context to proceed with EDIT_FILE or WRITE_FILE immediately.`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let emptyFileHint = '';
|
|
221
|
+
if (toolCall.tool === TOOLS.READ_FILE && result.success) {
|
|
222
|
+
const output = String(result.output || '').trim();
|
|
223
|
+
if (!output) {
|
|
224
|
+
emptyFileHint = `\nWARNING: The file appears empty or does not exist at that path. Verify the path is correct. Do NOT retry reading the same path expecting different content.`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let listDirHint = '';
|
|
229
|
+
if (toolCall.tool === TOOLS.LIST_DIR && result.success) {
|
|
230
|
+
listDirHint = `\nTIP: If you need to read a file's content, use READ_FILE with the exact file path. LIST_DIR only shows filenames — it does NOT show file contents.`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let subagentHint = '';
|
|
234
|
+
if (toolCall.tool === TOOLS.TASK) {
|
|
235
|
+
if (result.success && result.subagentLaunched) {
|
|
236
|
+
subagentHint = `\n[SUBAGENT_RESULT] You launched subagent "${result.subagentId || 'unknown'}". It has finished. The findings are in the Output below — use them to continue your work as the parent agent. Do NOT re-launch the same subagent unless you need a different scope.`;
|
|
237
|
+
} else if (!result.success && result.subagentLaunched) {
|
|
238
|
+
subagentHint = `\n[SUBAGENT_RESULT] Your subagent was launched but failed. Read the error and either narrow the task or investigate directly.`;
|
|
239
|
+
} else if (!result.success) {
|
|
240
|
+
subagentHint = `\n[SUBAGENT_RESULT] Subagent was NOT launched. ${result.error || 'Fix the issue and retry if needed.'}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.mode === MODES.CODING) {
|
|
245
|
+
const nextSteps = this.isSubagent
|
|
246
|
+
? `1. Interpret this result in ONE short sentence.
|
|
247
|
+
2. If work remains, continue with another allowed read-only tool. Skip blocked/sensitive targets instead of stopping.
|
|
248
|
+
3. Do NOT repeat the same search/grep/glob pattern. If you already explored enough, output [DONE] with a concise findings summary.
|
|
249
|
+
4. Output [DONE] only after the requested exploration is actually complete.
|
|
250
|
+
5. If impossible because every useful path is blocked, output [BLOCKED] with the blocker in one sentence.
|
|
251
|
+
6. Before your next tool call, think/reason about the goal and the best approach. Only emit the tool call once your reasoning is complete.`
|
|
252
|
+
: `1. Interpret this result in ONE short sentence.
|
|
253
|
+
2. If work remains, evaluate the result and decide the BEST next action — do NOT repeat the same search/grep/glob pattern. If you already explored enough, make an edit or output [BLOCKED].
|
|
254
|
+
3. Output [DONE] only after the correction is actually applied and verified.
|
|
255
|
+
4. If impossible, output [BLOCKED] with the blocker in one sentence.
|
|
256
|
+
5. Before your next tool call, think/reason about the goal and the best approach. Only emit the tool call once your reasoning is complete.`;
|
|
257
|
+
|
|
258
|
+
return `[TOOL_RESULT]
|
|
259
|
+
Tool: ${toolName}
|
|
260
|
+
Target: ${identifier}
|
|
261
|
+
Status: ${result.success ? 'SUCCESS' : 'ERROR'}
|
|
262
|
+
Iteration: ${this.iteration}/${MAX_ITERATIONS}
|
|
263
|
+
${failureHint}${readRepeatHint}${emptyFileHint}${listDirHint}${subagentHint}
|
|
264
|
+
Output:
|
|
265
|
+
${fullOutput}
|
|
266
|
+
|
|
267
|
+
Now you MUST:
|
|
268
|
+
${nextSteps}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return `[TOOL_RESULT]
|
|
272
|
+
Tool: ${toolName}
|
|
273
|
+
Target: ${identifier}
|
|
274
|
+
Status: ${result.success ? 'SUCCESS' : 'ERROR'}
|
|
275
|
+
Iteration: ${this.iteration}/${MAX_ITERATIONS}
|
|
276
|
+
${failureHint}${readRepeatHint}${emptyFileHint}${listDirHint}${subagentHint}
|
|
277
|
+
Output:
|
|
278
|
+
${fullOutput}
|
|
279
|
+
|
|
280
|
+
Now you MUST:
|
|
281
|
+
1. Interpret this result in ONE short sentence.
|
|
282
|
+
2. If you are a subagent and work remains, continue with another allowed read-only tool. Skip blocked/sensitive targets instead of stopping.
|
|
283
|
+
3. If work remains, evaluate the result and decide the BEST next action — do NOT repeat the same search/grep/glob pattern. If you already explored enough, output [DONE] with a concise findings summary.
|
|
284
|
+
4. Output [DONE] only after the requested exploration is actually complete.
|
|
285
|
+
5. If impossible because every useful path is blocked, output [BLOCKED] with the blocker in one sentence.
|
|
286
|
+
6. Before your next tool call, think/reason about the goal and the best approach. Only emit the tool call once your reasoning is complete.`;
|
|
287
|
+
},
|
|
288
|
+
};
|