neoagent 2.5.2-beta.9 → 3.0.1-beta.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/README.md +5 -1
- package/docs/integrations.md +11 -7
- package/flutter_app/lib/main_controller.dart +34 -0
- package/flutter_app/lib/main_security.dart +19 -0
- package/lib/schema_migrations.js +224 -0
- package/package.json +2 -2
- package/server/admin/admin.css +29 -0
- package/server/admin/admin.js +81 -1
- package/server/admin/index.html +97 -9
- package/server/db/database.js +57 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32788 -32752
- package/server/routes/security.js +19 -0
- package/server/routes/settings.js +1 -0
- package/server/routes/skills.js +9 -5
- package/server/services/ai/deliverables/artifact_helpers.js +92 -9
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/hooks.js +3 -2
- package/server/services/ai/learning.js +8 -3
- package/server/services/ai/loop/agent_engine_core.js +67 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/conversation_loop.js +391 -252
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/messaging_delivery.js +52 -6
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +6 -5
- package/server/services/ai/loop/tool_dispatch.js +1 -0
- package/server/services/ai/loopPolicy.js +15 -6
- package/server/services/ai/messagingFallback.js +23 -1
- package/server/services/ai/preModelCompaction.js +31 -0
- package/server/services/ai/repetitionGuard.js +47 -2
- package/server/services/ai/settings.js +8 -0
- package/server/services/ai/systemPrompt.js +24 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +39 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolRunner.js +262 -55
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +201 -31
- package/server/services/cli/shell_worker_pool.js +87 -5
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/manager.js +16 -0
- package/server/services/memory/manager.js +39 -24
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/runtime/manager.js +6 -2
- package/server/services/security/approval_gate_service.js +108 -5
- package/server/services/security/tool_categories.js +113 -4
- package/server/services/security/tool_security_hook.js +7 -2
- package/server/services/skills/runtime.js +2 -0
- package/server/services/workspace/manager.js +56 -0
|
@@ -85,6 +85,25 @@ router.post('/approvals/:approvalId', (req, res) => {
|
|
|
85
85
|
normalizedScope,
|
|
86
86
|
);
|
|
87
87
|
if (!resolved) {
|
|
88
|
+
const storedApproval = approvalGateService.getStoredApproval?.(
|
|
89
|
+
approvalId,
|
|
90
|
+
req.session.userId,
|
|
91
|
+
);
|
|
92
|
+
if (storedApproval?.status === 'expired' || storedApproval?.status === 'timeout') {
|
|
93
|
+
return res.status(410).json({
|
|
94
|
+
error: 'Approval expired because the run was interrupted or the server restarted.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (runId) {
|
|
98
|
+
const run = db.prepare(
|
|
99
|
+
'SELECT status FROM agent_runs WHERE id = ? AND user_id = ?'
|
|
100
|
+
).get(runId, req.session.userId);
|
|
101
|
+
if (run?.status === 'interrupted') {
|
|
102
|
+
return res.status(410).json({
|
|
103
|
+
error: 'Approval expired because the run was interrupted or the server restarted.',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
88
107
|
return res.status(404).json({ error: 'Approval not found or already resolved' });
|
|
89
108
|
}
|
|
90
109
|
res.json({ ok: true, approvalId, decision, scope: normalizedScope });
|
package/server/routes/skills.js
CHANGED
|
@@ -56,7 +56,7 @@ function parseSkillDocument(content) {
|
|
|
56
56
|
router.get('/', async (req, res) => {
|
|
57
57
|
try {
|
|
58
58
|
const runner = await getSkillRunner(req.app);
|
|
59
|
-
const skills = runner.getAll().map(serializeInstalledSkill);
|
|
59
|
+
const skills = runner.getAll(req.session.userId).map(serializeInstalledSkill);
|
|
60
60
|
res.json(skills.sort(sortInstalledSkills));
|
|
61
61
|
} catch (err) {
|
|
62
62
|
res.status(500).json({ error: sanitizeError(err) });
|
|
@@ -107,7 +107,7 @@ router.get('/audit/summary', (req, res) => {
|
|
|
107
107
|
router.get('/:name', async (req, res) => {
|
|
108
108
|
try {
|
|
109
109
|
const runner = await getSkillRunner(req.app);
|
|
110
|
-
const skill = runner.getSkill(req.params.name);
|
|
110
|
+
const skill = runner.getSkill(req.params.name, req.session.userId);
|
|
111
111
|
if (!skill) return res.status(404).json({ error: 'Skill not found' });
|
|
112
112
|
const fs = require('fs');
|
|
113
113
|
const content = fs.readFileSync(skill.filePath, 'utf-8');
|
|
@@ -132,6 +132,7 @@ router.post('/', async (req, res) => {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const result = runner.createSkill(
|
|
135
|
+
req.session.userId,
|
|
135
136
|
req.body.filename || parsed.name,
|
|
136
137
|
parsed.description,
|
|
137
138
|
parsed.instructions,
|
|
@@ -149,7 +150,8 @@ router.put('/:name', async (req, res) => {
|
|
|
149
150
|
try {
|
|
150
151
|
const runner = await getSkillRunner(req.app);
|
|
151
152
|
if (typeof req.body.enabled === 'boolean' && !req.body.content) {
|
|
152
|
-
const result = runner.setSkillEnabled(req.params.name, req.body.enabled);
|
|
153
|
+
const result = runner.setSkillEnabled(req.session.userId, req.params.name, req.body.enabled);
|
|
154
|
+
if (result.code === 'forbidden') return res.status(403).json(result);
|
|
153
155
|
if (result.error) return res.status(404).json(result);
|
|
154
156
|
return res.json(result);
|
|
155
157
|
}
|
|
@@ -159,11 +161,12 @@ router.put('/:name', async (req, res) => {
|
|
|
159
161
|
if (parsed.metadata?.command && !isValidCommandTemplate(parsed.metadata.command)) {
|
|
160
162
|
return res.status(400).json({ error: 'Skill command template contains invalid characters' });
|
|
161
163
|
}
|
|
162
|
-
const result = runner.updateSkill(req.params.name, {
|
|
164
|
+
const result = runner.updateSkill(req.session.userId, req.params.name, {
|
|
163
165
|
description: parsed.description,
|
|
164
166
|
instructions: parsed.instructions,
|
|
165
167
|
metadata: parsed.metadata
|
|
166
168
|
});
|
|
169
|
+
if (result.code === 'forbidden') return res.status(403).json(result);
|
|
167
170
|
if (result.error) return res.status(404).json(result);
|
|
168
171
|
res.json(result);
|
|
169
172
|
} catch (err) {
|
|
@@ -174,7 +177,8 @@ router.put('/:name', async (req, res) => {
|
|
|
174
177
|
router.delete('/:name', async (req, res) => {
|
|
175
178
|
try {
|
|
176
179
|
const runner = await getSkillRunner(req.app);
|
|
177
|
-
const result = runner.deleteSkill(req.params.name);
|
|
180
|
+
const result = runner.deleteSkill(req.session.userId, req.params.name);
|
|
181
|
+
if (result.code === 'forbidden') return res.status(403).json(result);
|
|
178
182
|
if (result.error) return res.status(404).json(result);
|
|
179
183
|
res.json(result);
|
|
180
184
|
} catch (err) {
|
|
@@ -72,10 +72,78 @@ const CANDIDATE_KEYS = [
|
|
|
72
72
|
'mediaPaths',
|
|
73
73
|
'screenshotPath',
|
|
74
74
|
'uiDumpPath',
|
|
75
|
-
'
|
|
76
|
-
'
|
|
75
|
+
'artifact',
|
|
76
|
+
'artifacts',
|
|
77
|
+
'artifactPath',
|
|
78
|
+
'artifactPaths',
|
|
79
|
+
'artifactUrl',
|
|
80
|
+
'artifactUrls',
|
|
81
|
+
'artifactUri',
|
|
82
|
+
'artifactUris',
|
|
83
|
+
'downloadUrl',
|
|
84
|
+
'downloadUrls',
|
|
85
|
+
'downloadUri',
|
|
86
|
+
'downloadUris',
|
|
77
87
|
];
|
|
78
88
|
|
|
89
|
+
const GENERIC_CANDIDATE_KEYS = new Set([
|
|
90
|
+
'path',
|
|
91
|
+
'paths',
|
|
92
|
+
'file',
|
|
93
|
+
'files',
|
|
94
|
+
'filePath',
|
|
95
|
+
'filePaths',
|
|
96
|
+
'fullPath',
|
|
97
|
+
'fullPaths',
|
|
98
|
+
'downloadUrl',
|
|
99
|
+
'downloadUrls',
|
|
100
|
+
'downloadUri',
|
|
101
|
+
'downloadUris',
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const EXPLICIT_CANDIDATE_KEYS = new Set(
|
|
105
|
+
CANDIDATE_KEYS.filter((key) => !GENERIC_CANDIDATE_KEYS.has(key))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const ARTIFACT_CONTAINER_KEYS = new Set([
|
|
109
|
+
'artifact',
|
|
110
|
+
'artifacts',
|
|
111
|
+
'attachment',
|
|
112
|
+
'attachments',
|
|
113
|
+
'deliverable',
|
|
114
|
+
'deliverables',
|
|
115
|
+
'download',
|
|
116
|
+
'downloads',
|
|
117
|
+
'file',
|
|
118
|
+
'files',
|
|
119
|
+
'media',
|
|
120
|
+
'preview',
|
|
121
|
+
'screenshot',
|
|
122
|
+
'screenshots',
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
const CONTAINER_URL_KEYS = new Set(['url', 'urls', 'uri', 'uris', 'href', 'hrefs']);
|
|
126
|
+
|
|
127
|
+
const EVIDENCE_RESULT_TOOLS = /^(execute_command|github_|list_|search_|read_|get_|find_|http_request|web_search|browser_get|browser_read|code_navigate|query_structured_data|memory_|session_search|recordings_|read_health_data)/;
|
|
128
|
+
|
|
129
|
+
function allowsGenericCandidateKeys(toolName = '') {
|
|
130
|
+
return !EVIDENCE_RESULT_TOOLS.test(String(toolName || ''));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isExplicitCandidateKey(keyHint = '', parentKeyHint = '', options = {}) {
|
|
134
|
+
if (EXPLICIT_CANDIDATE_KEYS.has(keyHint)) return true;
|
|
135
|
+
if (
|
|
136
|
+
ARTIFACT_CONTAINER_KEYS.has(parentKeyHint)
|
|
137
|
+
&& CANDIDATE_KEYS.includes(keyHint)
|
|
138
|
+
&& (!GENERIC_CANDIDATE_KEYS.has(parentKeyHint) || options.allowGenericKeys === true)
|
|
139
|
+
) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (GENERIC_CANDIDATE_KEYS.has(keyHint)) return options.allowGenericKeys === true;
|
|
143
|
+
if (!CONTAINER_URL_KEYS.has(keyHint)) return false;
|
|
144
|
+
return ARTIFACT_CONTAINER_KEYS.has(parentKeyHint);
|
|
145
|
+
}
|
|
146
|
+
|
|
79
147
|
function inferExtension(candidate = '') {
|
|
80
148
|
return path.extname(String(candidate || '').split('?')[0]).toLowerCase();
|
|
81
149
|
}
|
|
@@ -102,8 +170,17 @@ function normalizePathOrUri(value) {
|
|
|
102
170
|
if (!text) return null;
|
|
103
171
|
if (text.startsWith('/api/artifacts/')) return { uri: text, path: null };
|
|
104
172
|
if (/^https?:\/\//i.test(text)) return { uri: text, path: null };
|
|
105
|
-
if (
|
|
106
|
-
if (
|
|
173
|
+
if (text.startsWith('//')) return null;
|
|
174
|
+
if (/^[A-Za-z]:\\/.test(text)) {
|
|
175
|
+
const ext = path.extname(text.split('?')[0]).toLowerCase();
|
|
176
|
+
if (!FILE_EXTENSION_TO_KIND[ext]) return null;
|
|
177
|
+
return { path: text, uri: null };
|
|
178
|
+
}
|
|
179
|
+
if (path.isAbsolute(text)) {
|
|
180
|
+
const ext = path.extname(text.split('?')[0]).toLowerCase();
|
|
181
|
+
if (!FILE_EXTENSION_TO_KIND[ext]) return null;
|
|
182
|
+
return { path: text, uri: null };
|
|
183
|
+
}
|
|
107
184
|
return null;
|
|
108
185
|
}
|
|
109
186
|
|
|
@@ -154,6 +231,7 @@ async function extractArtifactsFromResult(toolName, result) {
|
|
|
154
231
|
const seen = new Set();
|
|
155
232
|
const seenCandidates = new Set();
|
|
156
233
|
const fallbackKind = inferArtifactKind(toolName, 'artifact');
|
|
234
|
+
const allowGenericKeys = allowsGenericCandidateKeys(toolName);
|
|
157
235
|
|
|
158
236
|
async function pushCandidate(candidate) {
|
|
159
237
|
const candidateKey = String(candidate || '').trim();
|
|
@@ -167,23 +245,26 @@ async function extractArtifactsFromResult(toolName, result) {
|
|
|
167
245
|
artifacts.push(artifact);
|
|
168
246
|
}
|
|
169
247
|
|
|
170
|
-
async function visit(value, keyHint = '') {
|
|
248
|
+
async function visit(value, keyHint = '', parentKeyHint = '') {
|
|
171
249
|
if (value == null) return;
|
|
172
250
|
if (typeof value === 'string') {
|
|
173
|
-
const explicit =
|
|
174
|
-
if (explicit)
|
|
251
|
+
const explicit = isExplicitCandidateKey(keyHint, parentKeyHint, { allowGenericKeys });
|
|
252
|
+
if (explicit) {
|
|
253
|
+
if (normalizePathOrUri(value)) await pushCandidate(value);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
175
256
|
for (const candidate of scanStringForCandidates(value, { explicit })) {
|
|
176
257
|
await pushCandidate(candidate);
|
|
177
258
|
}
|
|
178
259
|
return;
|
|
179
260
|
}
|
|
180
261
|
if (Array.isArray(value)) {
|
|
181
|
-
for (const item of value) await visit(item, keyHint);
|
|
262
|
+
for (const item of value) await visit(item, keyHint, parentKeyHint);
|
|
182
263
|
return;
|
|
183
264
|
}
|
|
184
265
|
if (typeof value === 'object') {
|
|
185
266
|
for (const [key, nested] of Object.entries(value)) {
|
|
186
|
-
await visit(nested, key);
|
|
267
|
+
await visit(nested, key, keyHint);
|
|
187
268
|
}
|
|
188
269
|
}
|
|
189
270
|
}
|
|
@@ -193,8 +274,10 @@ async function extractArtifactsFromResult(toolName, result) {
|
|
|
193
274
|
}
|
|
194
275
|
|
|
195
276
|
module.exports = {
|
|
277
|
+
allowsGenericCandidateKeys,
|
|
196
278
|
extractArtifactsFromResult,
|
|
197
279
|
inferArtifactKind,
|
|
198
280
|
inferMimeType,
|
|
281
|
+
isExplicitCandidateKey,
|
|
199
282
|
normalizePathOrUri,
|
|
200
283
|
};
|
|
@@ -47,6 +47,23 @@ async function selectDeliverableWorkflow({
|
|
|
47
47
|
tools = [],
|
|
48
48
|
options = {},
|
|
49
49
|
}) {
|
|
50
|
+
if (!engine || typeof engine.requestStructuredJson !== 'function') {
|
|
51
|
+
return {
|
|
52
|
+
selection: {
|
|
53
|
+
status: 'standard',
|
|
54
|
+
type: null,
|
|
55
|
+
confidence: 0,
|
|
56
|
+
goal: '',
|
|
57
|
+
requestedOutputs: [],
|
|
58
|
+
supportingCapabilities: [],
|
|
59
|
+
},
|
|
60
|
+
usage: 0,
|
|
61
|
+
raw: '',
|
|
62
|
+
skipped: true,
|
|
63
|
+
reason: 'structured selector unavailable',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
50
67
|
const response = await engine.requestStructuredJson({
|
|
51
68
|
provider,
|
|
52
69
|
providerName,
|
|
@@ -81,7 +81,8 @@ class AgentHooks {
|
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Run all handlers for an event, merging their return values.
|
|
84
|
-
* If any handler returns { block: true }, short-circuits and
|
|
84
|
+
* If any handler returns { block: true }, short-circuits and preserves the
|
|
85
|
+
* handler's metadata so the engine can explain why the tool was blocked.
|
|
85
86
|
*
|
|
86
87
|
* @param {string} event
|
|
87
88
|
* @param {object} ctx - Context passed to every handler
|
|
@@ -98,7 +99,7 @@ class AgentHooks {
|
|
|
98
99
|
console.warn(`[Hooks] Handler "${id}" for "${event}" threw:`, err.message);
|
|
99
100
|
continue; // don't let a bad hook crash the loop
|
|
100
101
|
}
|
|
101
|
-
if (result?.block === true) return {
|
|
102
|
+
if (result?.block === true) return { ...merged, ...result };
|
|
102
103
|
if (result && typeof result === 'object') {
|
|
103
104
|
merged = { ...merged, ...result };
|
|
104
105
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const db = require('../../db/database');
|
|
3
|
+
const { getAiSettings } = require('./settings');
|
|
3
4
|
|
|
4
5
|
function sanitizeSkillName(input) {
|
|
5
6
|
const base = String(input || '')
|
|
@@ -111,6 +112,8 @@ class LearningManager {
|
|
|
111
112
|
if (!userId || !agentId || !runId || !task || !finalContent) return null;
|
|
112
113
|
if (triggerType && triggerType !== 'user') return null;
|
|
113
114
|
if (triggerSource && triggerSource !== 'web') return null;
|
|
115
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
116
|
+
if (!aiSettings.auto_skill_learning) return null;
|
|
114
117
|
|
|
115
118
|
const successfulSteps = Array.isArray(steps)
|
|
116
119
|
? steps.filter((step) => step.status === 'completed' && step.tool_name)
|
|
@@ -145,12 +148,13 @@ class LearningManager {
|
|
|
145
148
|
).get(userId, agentId, draft.metadata.workflow_signature);
|
|
146
149
|
if (Number(observation?.observation_count || 0) < 3) return null;
|
|
147
150
|
|
|
148
|
-
const existing =
|
|
149
|
-
|
|
151
|
+
const existing = this.skillRunner.findSkillByWorkflowSignature(
|
|
152
|
+
userId,
|
|
153
|
+
draft.metadata.workflow_signature,
|
|
150
154
|
);
|
|
151
155
|
if (existing) {
|
|
152
156
|
if (existing.metadata?.enabled !== false || existing.metadata?.draft !== true) return null;
|
|
153
|
-
return this.skillRunner.updateSkill(existing.name, {
|
|
157
|
+
return this.skillRunner.updateSkill(userId, existing.name, {
|
|
154
158
|
description: draft.description,
|
|
155
159
|
instructions: draft.instructions,
|
|
156
160
|
metadata: {
|
|
@@ -163,6 +167,7 @@ class LearningManager {
|
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
const result = this.skillRunner.createSkill(
|
|
170
|
+
userId,
|
|
166
171
|
draft.name,
|
|
167
172
|
draft.description,
|
|
168
173
|
draft.instructions,
|
|
@@ -247,6 +247,7 @@ class AgentEngine {
|
|
|
247
247
|
const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
|
|
248
248
|
const skillRunner = context.skillRunner || this.skillRunner || null;
|
|
249
249
|
const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
|
|
250
|
+
userId,
|
|
250
251
|
maxTotalChars: 9000,
|
|
251
252
|
maxDescriptionChars: 180,
|
|
252
253
|
maxTriggerChars: 100,
|
|
@@ -1180,6 +1181,24 @@ class AgentEngine {
|
|
|
1180
1181
|
}
|
|
1181
1182
|
|
|
1182
1183
|
async spawnSubagent(userId, parentRunId, task, options = {}) {
|
|
1184
|
+
const parentRunMeta = this.getRunMeta(parentRunId);
|
|
1185
|
+
const parentDepth = Math.max(0, Number(parentRunMeta?.subagentDepth) || 0);
|
|
1186
|
+
if (parentDepth >= 1) {
|
|
1187
|
+
return {
|
|
1188
|
+
error: 'Sub-agents cannot spawn additional sub-agents. Continue the current child run or return results to the parent run.',
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const aiSettings = getAiSettings(userId, options.agentId || null);
|
|
1193
|
+
const maxSubagentsPerRun = Math.max(1, Number(aiSettings.subagent_max_children_per_run) || 10);
|
|
1194
|
+
const existingSubagents = Array.from(this.subagents.values())
|
|
1195
|
+
.filter((record) => record.parentRunId === parentRunId);
|
|
1196
|
+
if (existingSubagents.length >= maxSubagentsPerRun) {
|
|
1197
|
+
return {
|
|
1198
|
+
error: `This run has already spawned ${existingSubagents.length} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1183
1202
|
const handle = uuidv4();
|
|
1184
1203
|
const childRunId = uuidv4();
|
|
1185
1204
|
let relevantMemories = [];
|
|
@@ -1256,6 +1275,8 @@ class AgentEngine {
|
|
|
1256
1275
|
triggerSource: 'agent',
|
|
1257
1276
|
runId: childRunId,
|
|
1258
1277
|
agentId: options.agentId || null,
|
|
1278
|
+
subagentDepth: parentDepth + 1,
|
|
1279
|
+
disallowedToolNames: ['spawn_subagent'],
|
|
1259
1280
|
},
|
|
1260
1281
|
options.model || null
|
|
1261
1282
|
);
|
|
@@ -1521,6 +1542,52 @@ class AgentEngine {
|
|
|
1521
1542
|
return { handle, status: 'cancelled' };
|
|
1522
1543
|
}
|
|
1523
1544
|
|
|
1545
|
+
interruptRun(runId, reason = 'Server shutting down while run was in progress.') {
|
|
1546
|
+
const runMeta = this.activeRuns.get(runId);
|
|
1547
|
+
const delegatedChildren = db.prepare(
|
|
1548
|
+
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
1549
|
+
).all(runId);
|
|
1550
|
+
if (runMeta) {
|
|
1551
|
+
runMeta.status = 'interrupted';
|
|
1552
|
+
runMeta.stopReason = reason;
|
|
1553
|
+
runMeta.aborted = true;
|
|
1554
|
+
this.emit(runMeta.userId, 'run:stopping', { runId });
|
|
1555
|
+
for (const pid of runMeta.toolPids) {
|
|
1556
|
+
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
1557
|
+
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
runMeta.toolPids.clear();
|
|
1561
|
+
}
|
|
1562
|
+
for (const child of delegatedChildren) {
|
|
1563
|
+
if (child.child_run_id && child.child_run_id !== runId) {
|
|
1564
|
+
this.interruptRun(child.child_run_id, reason);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
db.prepare(
|
|
1568
|
+
`UPDATE agent_delegations
|
|
1569
|
+
SET status = 'interrupted',
|
|
1570
|
+
error = COALESCE(NULLIF(error, ''), ?),
|
|
1571
|
+
updated_at = datetime('now'),
|
|
1572
|
+
completed_at = datetime('now')
|
|
1573
|
+
WHERE parent_run_id = ? AND status = 'running'`
|
|
1574
|
+
).run(reason, runId);
|
|
1575
|
+
db.prepare(
|
|
1576
|
+
`UPDATE agent_runs
|
|
1577
|
+
SET status = 'interrupted',
|
|
1578
|
+
error = COALESCE(NULLIF(error, ''), ?),
|
|
1579
|
+
updated_at = datetime('now'),
|
|
1580
|
+
completed_at = datetime('now')
|
|
1581
|
+
WHERE id = ?`
|
|
1582
|
+
).run(reason, runId);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
interruptAllActiveRuns(reason = 'Server shutting down while run was in progress.') {
|
|
1586
|
+
for (const runId of Array.from(this.activeRuns.keys())) {
|
|
1587
|
+
this.interruptRun(runId, reason);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1524
1591
|
stopRun(runId) {
|
|
1525
1592
|
const runMeta = this.activeRuns.get(runId);
|
|
1526
1593
|
const delegatedChildren = db.prepare(
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { summarizeForLog } = require('../logFormat');
|
|
4
|
+
|
|
5
|
+
function latestFailedToolExecution(toolExecutions = []) {
|
|
6
|
+
return [...toolExecutions].reverse().find((item) => item && item.ok === false) || null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function shouldContinueAfterBlankToolFailure({
|
|
10
|
+
lastContent = '',
|
|
11
|
+
failedStepCount = 0,
|
|
12
|
+
remainingIterations = 0,
|
|
13
|
+
toolExecutions = [],
|
|
14
|
+
} = {}) {
|
|
15
|
+
return !String(lastContent || '').trim()
|
|
16
|
+
&& Number(failedStepCount || 0) > 0
|
|
17
|
+
&& Number(remainingIterations || 0) > 0
|
|
18
|
+
&& Boolean(latestFailedToolExecution(toolExecutions));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
|
|
22
|
+
const failedExecution = latestFailedToolExecution(toolExecutions);
|
|
23
|
+
if (!failedExecution) return '';
|
|
24
|
+
const toolName = failedExecution.toolName || failedExecution.tool || 'the previous tool';
|
|
25
|
+
const failure = failedExecution.error || failedExecution.summary || failedExecution.status || 'unknown failure';
|
|
26
|
+
return [
|
|
27
|
+
`The previous tool "${toolName}" failed with: ${summarizeForLog(failure, 240)}.`,
|
|
28
|
+
'The latest assistant turn returned no user-facing answer and no tool call, so the task is not terminal.',
|
|
29
|
+
'Continue with the next safe recovery action: retry with corrected arguments, use another available tool, verify from existing evidence, or report a real blocker only if no autonomous path remains.',
|
|
30
|
+
].join(' ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
buildBlankAfterToolFailureGuidance,
|
|
35
|
+
latestFailedToolExecution,
|
|
36
|
+
shouldContinueAfterBlankToolFailure,
|
|
37
|
+
};
|