neoagent 2.4.1-beta.35 → 2.4.1-beta.37
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/flutter_app/lib/main_operations.dart +1246 -691
- package/package.json +1 -1
- package/server/db/ftsQuery.js +27 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +68156 -67730
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/triggers.js +4 -4
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +119 -501
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +37 -24
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/grok.js +5 -67
- package/server/services/ai/providers/nvidia.js +6 -29
- package/server/services/ai/providers/ollama.js +48 -28
- package/server/services/ai/providers/openai.js +2 -27
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/taskAnalysis.js +4 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/memory/manager.js +1 -7
- package/server/services/websocket.js +8 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
|
-
const {
|
|
2
|
+
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
3
|
|
|
4
4
|
const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
|
|
5
5
|
|
|
@@ -21,7 +21,7 @@ const REASONING_MODELS = new Set([
|
|
|
21
21
|
'qwen/qwq-32b',
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
-
class NvidiaProvider extends
|
|
24
|
+
class NvidiaProvider extends OpenAICompatibleProvider {
|
|
25
25
|
constructor(config = {}) {
|
|
26
26
|
super(config);
|
|
27
27
|
this.name = 'nvidia';
|
|
@@ -68,7 +68,7 @@ class NvidiaProvider extends BaseProvider {
|
|
|
68
68
|
} catch (err) {
|
|
69
69
|
throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
|
|
70
70
|
}
|
|
71
|
-
return this.
|
|
71
|
+
return this.normalizeResponse(response);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
async *stream(messages, tools = [], options = {}) {
|
|
@@ -92,7 +92,7 @@ class NvidiaProvider extends BaseProvider {
|
|
|
92
92
|
|
|
93
93
|
for await (const chunk of stream) {
|
|
94
94
|
if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
|
|
95
|
-
finalUsage = this.
|
|
95
|
+
finalUsage = this.normalizeUsage(chunk.usage);
|
|
96
96
|
continue;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -121,11 +121,11 @@ class NvidiaProvider extends BaseProvider {
|
|
|
121
121
|
|
|
122
122
|
const finishReason = chunk.choices[0]?.finish_reason;
|
|
123
123
|
if (finishReason === 'tool_calls' || (finishReason === 'stop' && toolCalls.length > 0)) {
|
|
124
|
-
yield { type: 'tool_calls', toolCalls, content, usage: this.
|
|
124
|
+
yield { type: 'tool_calls', toolCalls, content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
if (finishReason === 'stop') {
|
|
128
|
-
yield { type: 'done', content, usage: this.
|
|
128
|
+
yield { type: 'done', content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -137,29 +137,6 @@ class NvidiaProvider extends BaseProvider {
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
_normalizeResponse(response) {
|
|
141
|
-
const choice = response.choices[0];
|
|
142
|
-
const msg = choice.message;
|
|
143
|
-
return {
|
|
144
|
-
content: msg.content || '',
|
|
145
|
-
toolCalls: msg.tool_calls?.map((tc) => ({
|
|
146
|
-
id: tc.id,
|
|
147
|
-
type: 'function',
|
|
148
|
-
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
149
|
-
})) || [],
|
|
150
|
-
finishReason: choice.finish_reason,
|
|
151
|
-
usage: this._normalizeUsage(response.usage),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
_normalizeUsage(usage) {
|
|
156
|
-
if (!usage) return null;
|
|
157
|
-
return {
|
|
158
|
-
promptTokens: usage.prompt_tokens ?? 0,
|
|
159
|
-
completionTokens: usage.completion_tokens ?? 0,
|
|
160
|
-
totalTokens: usage.total_tokens ?? 0,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
140
|
}
|
|
164
141
|
|
|
165
142
|
module.exports = { NvidiaProvider };
|
|
@@ -81,33 +81,65 @@ class OllamaProvider extends BaseProvider {
|
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
const model = options.model || this.config.model || 'llama3.1';
|
|
86
|
-
await this.ensureModel(model);
|
|
84
|
+
buildChatBody(messages, tools, options, stream) {
|
|
87
85
|
const body = {
|
|
88
|
-
model,
|
|
86
|
+
model: options.model || this.config.model || 'llama3.1',
|
|
89
87
|
messages: messages.map(m => ({
|
|
90
88
|
role: m.role,
|
|
91
89
|
content: m.content || '',
|
|
92
90
|
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
|
|
93
91
|
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
|
|
94
92
|
})),
|
|
95
|
-
stream
|
|
93
|
+
stream,
|
|
96
94
|
options: {
|
|
97
95
|
temperature: options.temperature ?? 0.7,
|
|
98
96
|
num_predict: options.maxTokens || 16384
|
|
99
97
|
}
|
|
100
98
|
};
|
|
101
|
-
|
|
102
99
|
if (tools.length > 0) {
|
|
103
100
|
body.tools = this.formatToolsForOllama(tools);
|
|
104
101
|
}
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
105
104
|
|
|
105
|
+
// Ollama returns HTTP 200 with an error body for some failures and a non-2xx
|
|
106
|
+
// status for others; surface both as real errors instead of letting callers
|
|
107
|
+
// see a silently empty response. Tags models that reject tools so the caller
|
|
108
|
+
// can transparently retry without them.
|
|
109
|
+
async postChat(body) {
|
|
106
110
|
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
107
111
|
method: 'POST',
|
|
108
112
|
headers: { 'Content-Type': 'application/json' },
|
|
109
113
|
body: JSON.stringify(body)
|
|
110
114
|
});
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
const detail = await res.text().catch(() => '');
|
|
117
|
+
let message = detail;
|
|
118
|
+
try { message = JSON.parse(detail)?.error || detail; } catch {}
|
|
119
|
+
const err = new Error(`Ollama /api/chat failed (HTTP ${res.status}): ${message || res.statusText}`);
|
|
120
|
+
if (/does not support tools|tools.*not supported/i.test(message)) {
|
|
121
|
+
err.code = 'OLLAMA_TOOLS_UNSUPPORTED';
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
return res;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async chat(messages, tools = [], options = {}) {
|
|
129
|
+
const model = options.model || this.config.model || 'llama3.1';
|
|
130
|
+
await this.ensureModel(model);
|
|
131
|
+
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, false));
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
|
|
137
|
+
console.warn(`[Ollama] Model '${model}' does not support tools; retrying without them.`);
|
|
138
|
+
res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, false));
|
|
139
|
+
} else {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
111
143
|
|
|
112
144
|
const data = await res.json();
|
|
113
145
|
const msg = data.message || {};
|
|
@@ -135,31 +167,19 @@ class OllamaProvider extends BaseProvider {
|
|
|
135
167
|
async *stream(messages, tools = [], options = {}) {
|
|
136
168
|
const model = options.model || this.config.model || 'llama3.1';
|
|
137
169
|
await this.ensureModel(model);
|
|
138
|
-
const body = {
|
|
139
|
-
model,
|
|
140
|
-
messages: messages.map(m => ({
|
|
141
|
-
role: m.role,
|
|
142
|
-
content: m.content || '',
|
|
143
|
-
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
|
|
144
|
-
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
|
|
145
|
-
})),
|
|
146
|
-
stream: true,
|
|
147
|
-
options: {
|
|
148
|
-
temperature: options.temperature ?? 0.7,
|
|
149
|
-
num_predict: options.maxTokens || 16384
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
170
|
|
|
153
|
-
|
|
154
|
-
|
|
171
|
+
let res;
|
|
172
|
+
try {
|
|
173
|
+
res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, true));
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
|
|
176
|
+
console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
|
|
177
|
+
res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, true));
|
|
178
|
+
} else {
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
155
181
|
}
|
|
156
182
|
|
|
157
|
-
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
158
|
-
method: 'POST',
|
|
159
|
-
headers: { 'Content-Type': 'application/json' },
|
|
160
|
-
body: JSON.stringify(body)
|
|
161
|
-
});
|
|
162
|
-
|
|
163
183
|
const reader = res.body.getReader();
|
|
164
184
|
const decoder = new TextDecoder();
|
|
165
185
|
let content = '';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
|
-
const {
|
|
2
|
+
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
3
|
|
|
4
|
-
class OpenAIProvider extends
|
|
4
|
+
class OpenAIProvider extends OpenAICompatibleProvider {
|
|
5
5
|
constructor(config = {}) {
|
|
6
6
|
super(config);
|
|
7
7
|
this.name = 'openai';
|
|
@@ -173,31 +173,6 @@ class OpenAIProvider extends BaseProvider {
|
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
async analyzeImage(options = {}) {
|
|
177
|
-
const model = options.model || this.getDefaultVisionModel();
|
|
178
|
-
const b64 = BaseProvider.readImageAsBase64(options.imagePath);
|
|
179
|
-
const response = await this.client.chat.completions.create({
|
|
180
|
-
model,
|
|
181
|
-
max_tokens: options.maxTokens || 4096,
|
|
182
|
-
messages: [{
|
|
183
|
-
role: 'user',
|
|
184
|
-
content: [
|
|
185
|
-
{ type: 'text', text: options.question || 'Describe this image in detail.' },
|
|
186
|
-
{
|
|
187
|
-
type: 'image_url',
|
|
188
|
-
image_url: {
|
|
189
|
-
url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
}]
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
content: response.choices[0]?.message?.content || '',
|
|
198
|
-
model: response.model || model,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
176
|
}
|
|
202
177
|
|
|
203
178
|
module.exports = { OpenAIProvider };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { BaseProvider } = require('./base');
|
|
2
|
+
|
|
3
|
+
// Shared base for providers that speak the OpenAI Chat Completions wire format
|
|
4
|
+
// (OpenAI, Grok, NVIDIA NIM, GitHub Copilot, ...). It owns the response/usage
|
|
5
|
+
// normalization and vision request that were previously copy-pasted into each
|
|
6
|
+
// provider. Per-provider concerns — client construction, model lists, context
|
|
7
|
+
// windows, reasoning detection, and the streaming loop — stay in the subclasses,
|
|
8
|
+
// since those genuinely differ between vendors.
|
|
9
|
+
class OpenAICompatibleProvider extends BaseProvider {
|
|
10
|
+
normalizeUsage(usage) {
|
|
11
|
+
if (!usage) return null;
|
|
12
|
+
return {
|
|
13
|
+
promptTokens: usage.prompt_tokens ?? usage.promptTokens ?? 0,
|
|
14
|
+
completionTokens: usage.completion_tokens ?? usage.completionTokens ?? 0,
|
|
15
|
+
totalTokens: usage.total_tokens ?? usage.totalTokens ?? 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
normalizeResponse(response) {
|
|
20
|
+
const choice = response?.choices?.[0];
|
|
21
|
+
if (!choice) {
|
|
22
|
+
throw new Error(`Provider '${this.name}' returned no choices in the response`);
|
|
23
|
+
}
|
|
24
|
+
const msg = choice.message || {};
|
|
25
|
+
return {
|
|
26
|
+
content: msg.content || '',
|
|
27
|
+
toolCalls: (msg.tool_calls || [])
|
|
28
|
+
.filter((tc) => tc?.function)
|
|
29
|
+
.map((tc) => ({
|
|
30
|
+
id: tc.id,
|
|
31
|
+
type: 'function',
|
|
32
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
33
|
+
})),
|
|
34
|
+
finishReason: choice.finish_reason,
|
|
35
|
+
usage: this.normalizeUsage(response.usage),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async analyzeImage(options = {}) {
|
|
40
|
+
if (!this.supportsVision()) {
|
|
41
|
+
throw new Error(`Provider '${this.name}' does not support image analysis`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const model = options.model || this.getDefaultVisionModel();
|
|
45
|
+
const b64 = BaseProvider.readImageAsBase64(options.imagePath);
|
|
46
|
+
const response = await this.client.chat.completions.create({
|
|
47
|
+
model,
|
|
48
|
+
max_tokens: options.maxTokens || 4096,
|
|
49
|
+
messages: [{
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: [
|
|
52
|
+
{ type: 'text', text: options.question || 'Describe this image in detail.' },
|
|
53
|
+
{
|
|
54
|
+
type: 'image_url',
|
|
55
|
+
image_url: {
|
|
56
|
+
url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
content: response.choices[0]?.message?.content || '',
|
|
65
|
+
model: response.model || model,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { OpenAICompatibleProvider };
|
|
@@ -561,10 +561,13 @@ function buildExecutionGuidance({ analysis, plan = null, capabilityHealth }) {
|
|
|
561
561
|
return lines.filter(Boolean).join('\n\n');
|
|
562
562
|
}
|
|
563
563
|
|
|
564
|
-
function buildVerifierPrompt({ analysis, toolExecutionSummary, evidenceSources, finalReply }) {
|
|
564
|
+
function buildVerifierPrompt({ analysis, tools = [], toolExecutionSummary, evidenceSources, finalReply }) {
|
|
565
|
+
const toolNames = summarizeTools(tools).join(', ');
|
|
565
566
|
return composeJsonPrompt([
|
|
566
567
|
JSON_ONLY_RESPONSE_RULE,
|
|
567
568
|
...VERIFIER_PROMPT_INSTRUCTIONS,
|
|
569
|
+
formatAvailableToolsLine(toolNames),
|
|
570
|
+
'Do not claim a tool or capability was unavailable if it is listed as available in this run. A missing tool execution is missing evidence, not proof of missing tool exposure.',
|
|
568
571
|
`Freshness risk: ${analysis.freshness_risk}`,
|
|
569
572
|
`Verification need: ${analysis.verification_need}`,
|
|
570
573
|
`Completion confidence required: ${analysis.completion_confidence_required || 'medium'}`,
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Classifies tool executions into run evidence (what changed, what failed, what
|
|
4
|
+
// is relevant to the user's answer) and builds the deterministic recovery
|
|
5
|
+
// context the engine feeds back to the model after a failure. Kept free of
|
|
6
|
+
// engine state so the classification rules are pure and unit testable.
|
|
7
|
+
|
|
8
|
+
const { compactToolResult } = require('./toolResult');
|
|
9
|
+
const { summarizeForLog } = require('./logFormat');
|
|
10
|
+
const { normalizeOutgoingMessage, clampRunContext } = require('./messagingFallback');
|
|
11
|
+
|
|
12
|
+
// Ordered classification rules mapping a tool name to its evidence "source"
|
|
13
|
+
// bucket. First matching rule wins, so order is significant. Declared as data
|
|
14
|
+
// rather than a nested ternary so new tool families can be slotted in by adding
|
|
15
|
+
// a row instead of editing control flow.
|
|
16
|
+
const EVIDENCE_SOURCE_RULES = [
|
|
17
|
+
{ source: 'browser', match: (name) => name.startsWith('browser_') },
|
|
18
|
+
{ source: 'android', match: (name) => name.startsWith('android_') },
|
|
19
|
+
{ source: 'mcp', match: (name) => name.startsWith('mcp_') },
|
|
20
|
+
{ source: 'memory', match: (name) => name.startsWith('memory_') || name === 'session_search' },
|
|
21
|
+
{ source: 'search', match: (name) => name === 'web_search' },
|
|
22
|
+
{ source: 'http', match: (name) => name === 'http_request' },
|
|
23
|
+
{ source: 'files', match: (name) => ['read_file', 'search_files', 'list_directory', 'write_file', 'edit_file'].includes(name) },
|
|
24
|
+
{ source: 'command', match: (name) => name === 'execute_command' },
|
|
25
|
+
{ source: 'skills', match: (name) => name.includes('skill') },
|
|
26
|
+
{ source: 'tasks', match: (name) => name === 'create_task' || name === 'update_task' || name === 'delete_task' || name === 'list_tasks' || name.includes('widget') },
|
|
27
|
+
{ source: 'messaging', match: (name) => name === 'send_message' || name === 'make_call' },
|
|
28
|
+
{ source: 'data', match: (name) => name.startsWith('recordings_') || name === 'read_health_data' },
|
|
29
|
+
{ source: 'vision', match: (name) => name === 'analyze_image' },
|
|
30
|
+
{ source: 'subagent', match: (name) => name.includes('subagent') },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function deriveEvidenceSource(name) {
|
|
34
|
+
const rule = EVIDENCE_SOURCE_RULES.find((entry) => entry.match(name));
|
|
35
|
+
return rule ? rule.source : 'tool';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function classifyToolExecution(toolName, toolArgs = {}, result, errorMessage = '') {
|
|
39
|
+
const name = String(toolName || '');
|
|
40
|
+
const evidenceRelevantPrefixes = ['browser_', 'android_'];
|
|
41
|
+
const evidenceRelevantExact = new Set([
|
|
42
|
+
'web_search',
|
|
43
|
+
'http_request',
|
|
44
|
+
'read_file',
|
|
45
|
+
'search_files',
|
|
46
|
+
'list_directory',
|
|
47
|
+
'session_search',
|
|
48
|
+
'memory_recall',
|
|
49
|
+
'analyze_image',
|
|
50
|
+
'read_health_data',
|
|
51
|
+
'recordings_list',
|
|
52
|
+
'recordings_get',
|
|
53
|
+
'recordings_search',
|
|
54
|
+
'list_tasks',
|
|
55
|
+
'wait_subagent',
|
|
56
|
+
]);
|
|
57
|
+
const stateChangingExact = new Set([
|
|
58
|
+
'execute_command',
|
|
59
|
+
'write_file',
|
|
60
|
+
'edit_file',
|
|
61
|
+
'send_interim_update',
|
|
62
|
+
'send_message',
|
|
63
|
+
'make_call',
|
|
64
|
+
'create_skill',
|
|
65
|
+
'update_skill',
|
|
66
|
+
'delete_skill',
|
|
67
|
+
'create_task',
|
|
68
|
+
'update_task',
|
|
69
|
+
'delete_task',
|
|
70
|
+
'create_ai_widget',
|
|
71
|
+
'update_ai_widget',
|
|
72
|
+
'delete_ai_widget',
|
|
73
|
+
'save_widget_snapshot',
|
|
74
|
+
'mcp_add_server',
|
|
75
|
+
'mcp_remove_server',
|
|
76
|
+
'spawn_subagent',
|
|
77
|
+
'cancel_subagent',
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const evidenceSource = deriveEvidenceSource(name);
|
|
81
|
+
|
|
82
|
+
const evidenceRelevant = evidenceRelevantExact.has(name)
|
|
83
|
+
|| evidenceRelevantPrefixes.some((prefix) => name.startsWith(prefix));
|
|
84
|
+
const stateChanged = stateChangingExact.has(name)
|
|
85
|
+
|| name.startsWith('android_')
|
|
86
|
+
|| ['browser_click', 'browser_type', 'browser_evaluate'].includes(name);
|
|
87
|
+
|
|
88
|
+
let normalizedError = String(errorMessage || result?.error || '').trim();
|
|
89
|
+
if (!normalizedError && name === 'execute_command' && result && typeof result === 'object') {
|
|
90
|
+
if (result.timedOut) {
|
|
91
|
+
normalizedError = `Command timed out after ${result.durationMs || 'unknown'} ms`;
|
|
92
|
+
} else if (result.killed || result.signal) {
|
|
93
|
+
normalizedError = 'Command was killed before it finished';
|
|
94
|
+
} else if (typeof result.exitCode === 'number' && result.exitCode !== 0) {
|
|
95
|
+
normalizedError = summarizeForLog(result.stderr || result.stdout || `Command exited with code ${result.exitCode}`, 220);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!normalizedError && result && typeof result === 'object') {
|
|
100
|
+
const nestedResult = result.result && typeof result.result === 'object' && !Array.isArray(result.result)
|
|
101
|
+
? result.result
|
|
102
|
+
: null;
|
|
103
|
+
const detail = normalizeOutgoingMessage(
|
|
104
|
+
result.reason
|
|
105
|
+
|| result.message
|
|
106
|
+
|| nestedResult?.reason
|
|
107
|
+
|| nestedResult?.message
|
|
108
|
+
|| ''
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (result.skipped === true || nestedResult?.skipped === true) {
|
|
112
|
+
normalizedError = detail || 'Tool reported skipped outcome.';
|
|
113
|
+
} else if (result.success === false || nestedResult?.success === false) {
|
|
114
|
+
normalizedError = detail || 'Tool reported success=false.';
|
|
115
|
+
} else if (result.sent === false || nestedResult?.sent === false) {
|
|
116
|
+
normalizedError = detail || 'Tool reported sent=false.';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
toolName: name,
|
|
122
|
+
ok: !normalizedError,
|
|
123
|
+
error: normalizedError,
|
|
124
|
+
evidenceSource,
|
|
125
|
+
evidenceRelevant,
|
|
126
|
+
stateChanged,
|
|
127
|
+
dependsOnOutput: true,
|
|
128
|
+
summary: compactToolResult(name, toolArgs, result || { error: errorMessage || 'Tool failed' }, {
|
|
129
|
+
softLimit: 500,
|
|
130
|
+
hardLimit: 900,
|
|
131
|
+
}),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function summarizeToolExecutions(toolExecutions = [], maxItems = 10) {
|
|
136
|
+
return toolExecutions.slice(-maxItems).map((item, index) => {
|
|
137
|
+
const status = item.ok ? 'ok' : `error=${item.error}`;
|
|
138
|
+
return `${index + 1}. ${item.toolName} [${item.evidenceSource}] ${status} :: ${clampRunContext(item.summary || '', 220)}`;
|
|
139
|
+
}).join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function summarizeAvailableTools(tools = [], { exclude = [] } = {}) {
|
|
143
|
+
const excluded = new Set((Array.isArray(exclude) ? exclude : [exclude]).filter(Boolean));
|
|
144
|
+
return tools
|
|
145
|
+
.map((tool) => String(tool?.name || '').trim())
|
|
146
|
+
.filter((name) => name && !excluded.has(name))
|
|
147
|
+
.slice(0, 24)
|
|
148
|
+
.join(', ');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function inferToolFailureMessage(toolName, result) {
|
|
152
|
+
const explicitError = normalizeOutgoingMessage(result?.error || '');
|
|
153
|
+
if (explicitError) return explicitError;
|
|
154
|
+
|
|
155
|
+
if (!result || typeof result !== 'object') return '';
|
|
156
|
+
|
|
157
|
+
if (toolName === 'execute_command') {
|
|
158
|
+
if (result.timedOut) {
|
|
159
|
+
return `Command timed out after ${result.durationMs || 'unknown'} ms`;
|
|
160
|
+
}
|
|
161
|
+
if (result.killed || result.signal) {
|
|
162
|
+
return 'Command was killed before it finished';
|
|
163
|
+
}
|
|
164
|
+
if (typeof result.exitCode === 'number' && result.exitCode !== 0) {
|
|
165
|
+
return summarizeForLog(result.stderr || result.stdout || `Command exited with code ${result.exitCode}`, 220);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (toolName === 'http_request' && typeof result.status === 'number' && result.status >= 400) {
|
|
170
|
+
const bodySnippet = normalizeOutgoingMessage(result.body || '');
|
|
171
|
+
return summarizeForLog(
|
|
172
|
+
bodySnippet
|
|
173
|
+
? `HTTP request returned status ${result.status}: ${bodySnippet}`
|
|
174
|
+
: `HTTP request returned status ${result.status}`,
|
|
175
|
+
240
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [], userMessage, visibleMessageSent = false }) {
|
|
183
|
+
const lastFailure = [...toolExecutions].reverse().find((item) => !item.ok);
|
|
184
|
+
const alternativeTools = summarizeAvailableTools(tools, { exclude: lastFailure?.toolName || null });
|
|
185
|
+
const parts = [
|
|
186
|
+
'This is an internal recovery retry for the same user task. Continue the task instead of stopping.',
|
|
187
|
+
userMessage ? `Original task: ${clampRunContext(userMessage, 260)}` : '',
|
|
188
|
+
lastFailure?.toolName ? `Previous attempt failed on tool: ${lastFailure.toolName}.` : '',
|
|
189
|
+
lastFailure?.error ? `Concrete failure: ${summarizeForLog(lastFailure.error, 260)}.` : '',
|
|
190
|
+
err?.message ? `Run-level error after that failure: ${summarizeForLog(err.message, 220)}.` : '',
|
|
191
|
+
'Do not send a blocker message just because one tool path failed.',
|
|
192
|
+
'Use a different safe approach if available: alternate tool, different query, browser path, HTTP fetch, file/code inspection, or command verification.',
|
|
193
|
+
visibleMessageSent ? 'A user-facing message was already sent in a previous internal attempt. Continue silently unless you have a materially new finished result or a real external blocker.' : '',
|
|
194
|
+
alternativeTools ? `Other available tools in this run: ${alternativeTools}.` : '',
|
|
195
|
+
'Only stop if the remaining problem truly requires an external dependency or user action outside this run.'
|
|
196
|
+
];
|
|
197
|
+
return parts.filter(Boolean).join(' ');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
classifyToolExecution,
|
|
202
|
+
deriveEvidenceSource,
|
|
203
|
+
summarizeToolExecutions,
|
|
204
|
+
summarizeAvailableTools,
|
|
205
|
+
inferToolFailureMessage,
|
|
206
|
+
buildAutonomousRecoveryContext,
|
|
207
|
+
};
|
|
@@ -14,9 +14,16 @@
|
|
|
14
14
|
|
|
15
15
|
const MCP_ALWAYS_INCLUDE_THRESHOLD = 20;
|
|
16
16
|
const MAX_TOOLS = 128; // Strict provider limit (e.g. Github Copilot / OpenAI)
|
|
17
|
+
const ALWAYS_INCLUDE_BUILT_INS = [
|
|
18
|
+
'send_message',
|
|
19
|
+
'create_task',
|
|
20
|
+
'list_tasks',
|
|
21
|
+
'delete_task',
|
|
22
|
+
'update_task',
|
|
23
|
+
];
|
|
17
24
|
|
|
18
25
|
function ensureRequiredTools(selectedTools = [], builtInTools = [], options = {}) {
|
|
19
|
-
const requiredNames = [];
|
|
26
|
+
const requiredNames = [...ALWAYS_INCLUDE_BUILT_INS];
|
|
20
27
|
if (options.widgetId) requiredNames.push('save_widget_snapshot');
|
|
21
28
|
if (!requiredNames.length) return selectedTools;
|
|
22
29
|
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
} = require('./intelligence');
|
|
22
22
|
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
23
23
|
const { isMainAgent, resolveAgentId } = require('../agents/manager');
|
|
24
|
+
const { buildFtsQuery } = require('../../db/ftsQuery');
|
|
24
25
|
const {
|
|
25
26
|
decryptLocalValue,
|
|
26
27
|
encryptLocalValue,
|
|
@@ -187,13 +188,6 @@ function parseStringSetting(value) {
|
|
|
187
188
|
}
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
function buildFtsQuery(query) {
|
|
191
|
-
const tokens = String(query || '')
|
|
192
|
-
.match(/[\p{L}\p{N}_-]{2,}/gu) || [];
|
|
193
|
-
if (!tokens.length) return null;
|
|
194
|
-
return tokens.map((token) => `${token.replace(/"/g, '')}*`).join(' AND ');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
191
|
function stripHighlight(text) {
|
|
198
192
|
return String(text || '').replace(/<\/?mark>/g, '');
|
|
199
193
|
}
|
|
@@ -317,7 +317,7 @@ function setupWebSocket(io, services) {
|
|
|
317
317
|
const data = asObject(raw);
|
|
318
318
|
const runId = toOptionalString(data?.runId, 128);
|
|
319
319
|
console.warn(`[WS] agent:abort received from user ${userId} for run ${runId || 'unknown'}`);
|
|
320
|
-
agentEngine.abort(runId || null);
|
|
320
|
+
agentEngine.abort(runId || null, { userId });
|
|
321
321
|
socket.emit('agent:aborted', { runId: runId || null });
|
|
322
322
|
} catch (err) {
|
|
323
323
|
console.error(`[WS] agent:abort failed for user ${userId}:`, err);
|
|
@@ -366,8 +366,14 @@ function setupWebSocket(io, services) {
|
|
|
366
366
|
}
|
|
367
367
|
console.log(`[WS] agent:run_detail requested by user ${userId} run=${runId}`);
|
|
368
368
|
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(runId, userId);
|
|
369
|
+
if (!run) {
|
|
370
|
+
// Don't leak steps/history/events for a run the user doesn't own:
|
|
371
|
+
// agent_steps and run events are keyed only by run_id, so the run
|
|
372
|
+
// ownership check is the sole authorization gate.
|
|
373
|
+
return socket.emit('agent:run_detail', { run: null, steps: [], history: [], events: [] });
|
|
374
|
+
}
|
|
369
375
|
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(runId);
|
|
370
|
-
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(runId);
|
|
376
|
+
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? AND user_id = ? ORDER BY created_at ASC').all(runId, userId);
|
|
371
377
|
const events = listRunEvents(runId);
|
|
372
378
|
socket.emit('agent:run_detail', { run, steps, history, events });
|
|
373
379
|
} catch (err) {
|