neoagent 3.0.1-beta.8 → 3.1.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/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Complete, schema-driven erasure and export of a single user's data.
|
|
4
|
+
//
|
|
5
|
+
// Both the admin "GDPR: delete all user data" action and the self-service
|
|
6
|
+
// account-deletion endpoint go through eraseUserData() so the two can never
|
|
7
|
+
// drift apart. Rather than hand-maintain a list of tables (which previously
|
|
8
|
+
// missed billing, 2FA, embeddings and other user-scoped tables), we discover
|
|
9
|
+
// every table that has a `user_id` column at runtime via PRAGMA table_info and
|
|
10
|
+
// clear all of them. Child tables that are keyed by a parent id instead of
|
|
11
|
+
// user_id are cleared explicitly below.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const db = require('../../db/database');
|
|
16
|
+
const sessionsDb = require('../../db/sessions_db');
|
|
17
|
+
const { DATA_DIR, AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
18
|
+
const { sanitizeWorkspaceKey } = require('../workspace/manager');
|
|
19
|
+
|
|
20
|
+
// Tables that carry a `user_id` column but are global/shared configuration and
|
|
21
|
+
// must never be deleted as part of erasing one user.
|
|
22
|
+
const GLOBAL_TABLES = new Set(['billing_plans']);
|
|
23
|
+
|
|
24
|
+
// Column names whose values are credentials/secrets and must be redacted from a
|
|
25
|
+
// portability export (the user already controls these; echoing them back adds
|
|
26
|
+
// risk without adding portability value).
|
|
27
|
+
const SENSITIVE_COLUMN = /pass|secret|token|hash|cipher|encrypt|_iv$|nonce|totp|recovery|priv(ate)?_?key|api_?key|credential/i;
|
|
28
|
+
|
|
29
|
+
function userScopedTables() {
|
|
30
|
+
const rows = db
|
|
31
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
32
|
+
.all();
|
|
33
|
+
const tables = [];
|
|
34
|
+
for (const { name } of rows) {
|
|
35
|
+
if (name === 'users' || GLOBAL_TABLES.has(name) || name.startsWith('admin_')) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (name.startsWith('sqlite_')) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
let columns;
|
|
42
|
+
try {
|
|
43
|
+
columns = db.prepare(`PRAGMA table_info(${name})`).all();
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (columns.some((column) => column.name === 'user_id')) {
|
|
48
|
+
tables.push(name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return tables;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Child tables keyed by a parent id rather than user_id. Foreign keys are
|
|
55
|
+
// declared ON DELETE CASCADE, so with PRAGMA foreign_keys = ON these are removed
|
|
56
|
+
// automatically when the parent row goes — but we delete them explicitly first
|
|
57
|
+
// so erasure is correct even if foreign keys are ever disabled.
|
|
58
|
+
function deleteChildRows(uid) {
|
|
59
|
+
const childDeletes = [
|
|
60
|
+
'DELETE FROM conversation_messages WHERE conversation_id IN (SELECT id FROM conversations WHERE user_id = ?)',
|
|
61
|
+
'DELETE FROM agent_steps WHERE run_id IN (SELECT id FROM agent_runs WHERE user_id = ?)',
|
|
62
|
+
'DELETE FROM ai_widget_snapshots WHERE widget_id IN (SELECT id FROM ai_widgets WHERE user_id = ?)',
|
|
63
|
+
'DELETE FROM recording_transcript_segments WHERE session_id IN (SELECT id FROM recording_sessions WHERE user_id = ?)',
|
|
64
|
+
'DELETE FROM recording_chunks WHERE source_id IN (SELECT s.id FROM recording_sources s JOIN recording_sessions rs ON s.session_id = rs.id WHERE rs.user_id = ?)',
|
|
65
|
+
'DELETE FROM recording_sources WHERE session_id IN (SELECT id FROM recording_sessions WHERE user_id = ?)',
|
|
66
|
+
'DELETE FROM memory_source_links WHERE memory_id IN (SELECT id FROM memories WHERE user_id = ?)',
|
|
67
|
+
];
|
|
68
|
+
for (const sql of childDeletes) {
|
|
69
|
+
try {
|
|
70
|
+
db.prepare(sql).run(uid);
|
|
71
|
+
} catch {
|
|
72
|
+
// Table may not exist on older schema versions — ignore.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function collectArtifactPaths(uid) {
|
|
78
|
+
try {
|
|
79
|
+
return db
|
|
80
|
+
.prepare('SELECT storage_path FROM artifacts WHERE user_id = ?')
|
|
81
|
+
.all(uid)
|
|
82
|
+
.map((row) => row.storage_path)
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function removeArtifactFiles(uid, storagePaths) {
|
|
90
|
+
const artifactsRoot = path.resolve(path.join(DATA_DIR, 'artifacts'));
|
|
91
|
+
for (const storagePath of storagePaths) {
|
|
92
|
+
try {
|
|
93
|
+
const abs = path.resolve(
|
|
94
|
+
path.isAbsolute(storagePath) ? storagePath : path.join(DATA_DIR, storagePath),
|
|
95
|
+
);
|
|
96
|
+
if (abs.startsWith(artifactsRoot + path.sep)) {
|
|
97
|
+
fs.rmSync(abs, { force: true });
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
/* best effort */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const userArtifactDir = path.resolve(path.join(DATA_DIR, 'artifacts', String(uid)));
|
|
104
|
+
if (userArtifactDir.startsWith(artifactsRoot + path.sep)) {
|
|
105
|
+
try {
|
|
106
|
+
fs.rmSync(userArtifactDir, { recursive: true, force: true });
|
|
107
|
+
} catch {
|
|
108
|
+
/* best effort */
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removeWorkspaceDir(uid) {
|
|
114
|
+
const workspacesRoot = path.resolve(path.join(AGENT_DATA_DIR, 'workspaces'));
|
|
115
|
+
const workspaceDir = path.resolve(
|
|
116
|
+
path.join(workspacesRoot, sanitizeWorkspaceKey(uid)),
|
|
117
|
+
);
|
|
118
|
+
if (workspaceDir.startsWith(workspacesRoot + path.sep)) {
|
|
119
|
+
try {
|
|
120
|
+
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
121
|
+
} catch {
|
|
122
|
+
/* best effort */
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function purgeSessionStore(uid) {
|
|
128
|
+
// better-sqlite3-session-store keeps a `sessions` table whose `sess` column is
|
|
129
|
+
// the JSON-serialised session. Remove any row belonging to this user so their
|
|
130
|
+
// login is invalidated everywhere immediately.
|
|
131
|
+
for (const needle of [`%"userId":${uid}%`, `%"userId":"${uid}"%`]) {
|
|
132
|
+
try {
|
|
133
|
+
sessionsDb.prepare('DELETE FROM sessions WHERE sess LIKE ?').run(needle);
|
|
134
|
+
} catch {
|
|
135
|
+
/* table/store may differ — best effort */
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function killUserRuntime(uid, runtimeManager) {
|
|
141
|
+
try {
|
|
142
|
+
const vmManager = runtimeManager?.browserBackend?.vmManager;
|
|
143
|
+
if (vmManager && typeof vmManager.killVm === 'function') {
|
|
144
|
+
// Fire-and-forget: container teardown should not block the HTTP response.
|
|
145
|
+
Promise.resolve(vmManager.killVm(String(uid))).catch(() => {});
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
/* best effort */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Permanently erase all data belonging to a user (GDPR Art. 17).
|
|
154
|
+
*
|
|
155
|
+
* @param {number|string} userId
|
|
156
|
+
* @param {object} [opts]
|
|
157
|
+
* @param {object} [opts.runtimeManager] live RuntimeManager so the user's
|
|
158
|
+
* sandbox container can be torn down as part of erasure.
|
|
159
|
+
* @returns {{ ok: true, tablesCleared: number }}
|
|
160
|
+
*/
|
|
161
|
+
function eraseUserData(userId, opts = {}) {
|
|
162
|
+
const uid = Number(userId);
|
|
163
|
+
if (!Number.isInteger(uid) || uid <= 0) {
|
|
164
|
+
const error = new Error('Invalid user id');
|
|
165
|
+
error.code = 'INVALID_ID';
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
const exists = db.prepare('SELECT id FROM users WHERE id = ?').get(uid);
|
|
169
|
+
if (!exists) {
|
|
170
|
+
const error = new Error('User not found');
|
|
171
|
+
error.code = 'NOT_FOUND';
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const artifactPaths = collectArtifactPaths(uid);
|
|
176
|
+
const tables = userScopedTables();
|
|
177
|
+
|
|
178
|
+
const erase = db.transaction(() => {
|
|
179
|
+
deleteChildRows(uid);
|
|
180
|
+
for (const table of tables) {
|
|
181
|
+
// `table` comes from sqlite_master introspection, never from user input.
|
|
182
|
+
db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).run(uid);
|
|
183
|
+
}
|
|
184
|
+
db.prepare('DELETE FROM users WHERE id = ?').run(uid);
|
|
185
|
+
});
|
|
186
|
+
erase();
|
|
187
|
+
|
|
188
|
+
// Off-database state: files on disk, the running sandbox, and active sessions.
|
|
189
|
+
removeArtifactFiles(uid, artifactPaths);
|
|
190
|
+
removeWorkspaceDir(uid);
|
|
191
|
+
purgeSessionStore(uid);
|
|
192
|
+
killUserRuntime(uid, opts.runtimeManager);
|
|
193
|
+
|
|
194
|
+
return { ok: true, tablesCleared: tables.length };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function redactRow(row) {
|
|
198
|
+
const out = {};
|
|
199
|
+
for (const [key, value] of Object.entries(row)) {
|
|
200
|
+
out[key] = SENSITIVE_COLUMN.test(key) && value != null ? '[redacted]' : value;
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Build a structured, machine-readable export of a user's personal data
|
|
207
|
+
* (GDPR Art. 20 data portability). Credential/secret columns are redacted.
|
|
208
|
+
*
|
|
209
|
+
* @param {number|string} userId
|
|
210
|
+
* @returns {object}
|
|
211
|
+
*/
|
|
212
|
+
function exportUserData(userId) {
|
|
213
|
+
const uid = Number(userId);
|
|
214
|
+
if (!Number.isInteger(uid) || uid <= 0) {
|
|
215
|
+
const error = new Error('Invalid user id');
|
|
216
|
+
error.code = 'INVALID_ID';
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(uid);
|
|
220
|
+
if (!user) {
|
|
221
|
+
const error = new Error('User not found');
|
|
222
|
+
error.code = 'NOT_FOUND';
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const data = { account: redactRow(user) };
|
|
227
|
+
for (const table of userScopedTables()) {
|
|
228
|
+
try {
|
|
229
|
+
const rows = db.prepare(`SELECT * FROM ${table} WHERE user_id = ?`).all(uid);
|
|
230
|
+
if (rows.length > 0) {
|
|
231
|
+
data[table] = rows.map(redactRow);
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
/* skip unreadable table */
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Conversation messages are keyed by conversation id, not user_id.
|
|
238
|
+
try {
|
|
239
|
+
const messages = db
|
|
240
|
+
.prepare(
|
|
241
|
+
'SELECT m.* FROM conversation_messages m JOIN conversations c ON m.conversation_id = c.id WHERE c.user_id = ?',
|
|
242
|
+
)
|
|
243
|
+
.all(uid);
|
|
244
|
+
if (messages.length > 0) {
|
|
245
|
+
data.conversation_messages = messages.map(redactRow);
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
/* best effort */
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
exportedAt: new Date().toISOString(),
|
|
253
|
+
schema: 'neoagent.user-export.v1',
|
|
254
|
+
userId: uid,
|
|
255
|
+
data,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
eraseUserData,
|
|
261
|
+
exportUserData,
|
|
262
|
+
userScopedTables,
|
|
263
|
+
};
|
|
@@ -17,11 +17,14 @@
|
|
|
17
17
|
* Use for self-improvement, memory consolidation, analytics.
|
|
18
18
|
* Errors are swallowed — this hook must not affect run outcome.
|
|
19
19
|
*
|
|
20
|
+
* on_loop_iteration(ctx: { userId, runId, agentId, iteration, triggerType, triggerSource, totalTokens })
|
|
21
|
+
* → Steering. Return { systemSteering } to inject a system message for the
|
|
22
|
+
* current iteration, or { stop: true, reason } to stop before more work.
|
|
23
|
+
*
|
|
20
24
|
* ── NOT YET WIRED (planned) ────────────────────────────────────────────────
|
|
21
25
|
*
|
|
22
26
|
* before_prompt_build — inject extra system messages before model call
|
|
23
27
|
* after_tool_call — observe/transform tool result after execution
|
|
24
|
-
* on_loop_iteration — called at the top of each iteration; can inject steering
|
|
25
28
|
*
|
|
26
29
|
* To wire one, call globalHooks.run(event, ctx) at the relevant point in
|
|
27
30
|
* engine.js and handle the returned object. Follow the before_tool_call
|
|
@@ -247,6 +247,13 @@ class AgentEngine {
|
|
|
247
247
|
const { MemoryManager } = require('../../memory/manager');
|
|
248
248
|
const memoryManager = this.memoryManager || new MemoryManager();
|
|
249
249
|
const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
|
|
250
|
+
const timelineService = context.timelineService || this.app?.locals?.timelineService || null;
|
|
251
|
+
const timelinePrompt = timelineService?.buildPromptContext?.(userId, {
|
|
252
|
+
agentId: context.agentId || null,
|
|
253
|
+
query: context.rawUserMessage || context.userMessage || '',
|
|
254
|
+
limit: 6,
|
|
255
|
+
sources: ['screen', 'tasks', 'runs'],
|
|
256
|
+
}) || '';
|
|
250
257
|
const skillRunner = context.skillRunner || this.skillRunner || null;
|
|
251
258
|
const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
|
|
252
259
|
userId,
|
|
@@ -256,7 +263,7 @@ class AgentEngine {
|
|
|
256
263
|
}) || '';
|
|
257
264
|
return {
|
|
258
265
|
stable: [promptSections.stable, skillsPrompt].filter(Boolean).join('\n\n'),
|
|
259
|
-
dynamic: promptSections.dynamic,
|
|
266
|
+
dynamic: [promptSections.dynamic, timelinePrompt].filter(Boolean).join('\n\n'),
|
|
260
267
|
};
|
|
261
268
|
}
|
|
262
269
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { summarizeForLog } = require('../logFormat');
|
|
4
|
+
const { isInternalToolingFailure, normalizeOutgoingMessage } = require('../messagingFallback');
|
|
4
5
|
|
|
5
6
|
function latestFailedToolExecution(toolExecutions = []) {
|
|
6
7
|
return [...toolExecutions].reverse().find((item) => item && item.ok === false) || null;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
function isRecoverableInternalToolFailure(toolExecution = null) {
|
|
11
|
+
if (!toolExecution || toolExecution.ok !== false) return false;
|
|
12
|
+
const failure = String(toolExecution.error || toolExecution.summary || toolExecution.status || '').trim();
|
|
13
|
+
return isInternalToolingFailure(failure);
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
function shouldContinueAfterBlankToolFailure({
|
|
10
17
|
lastContent = '',
|
|
11
18
|
failedStepCount = 0,
|
|
@@ -18,6 +25,19 @@ function shouldContinueAfterBlankToolFailure({
|
|
|
18
25
|
&& Boolean(latestFailedToolExecution(toolExecutions));
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
function shouldContinueAfterRecoverableToolFailure({
|
|
29
|
+
lastContent = '',
|
|
30
|
+
remainingIterations = 0,
|
|
31
|
+
toolExecutions = [],
|
|
32
|
+
} = {}) {
|
|
33
|
+
if (Number(remainingIterations || 0) <= 0) return false;
|
|
34
|
+
const failedExecution = latestFailedToolExecution(toolExecutions);
|
|
35
|
+
if (!isRecoverableInternalToolFailure(failedExecution)) return false;
|
|
36
|
+
const visible = normalizeOutgoingMessage(lastContent || '');
|
|
37
|
+
if (!visible) return false;
|
|
38
|
+
return /\b(blocked|could not|cannot|can't|do not have a confirmed finished result|not found|missing|internal tool issue)\b/i.test(visible);
|
|
39
|
+
}
|
|
40
|
+
|
|
21
41
|
function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
|
|
22
42
|
const failedExecution = latestFailedToolExecution(toolExecutions);
|
|
23
43
|
if (!failedExecution) return '';
|
|
@@ -30,8 +50,24 @@ function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
|
|
|
30
50
|
].join(' ');
|
|
31
51
|
}
|
|
32
52
|
|
|
53
|
+
function buildRecoverableToolFailureGuidance(toolExecutions = []) {
|
|
54
|
+
const failedExecution = latestFailedToolExecution(toolExecutions);
|
|
55
|
+
if (!failedExecution) return '';
|
|
56
|
+
const toolName = failedExecution.toolName || failedExecution.tool || 'the previous tool';
|
|
57
|
+
const failure = failedExecution.error || failedExecution.summary || failedExecution.status || 'unknown failure';
|
|
58
|
+
return [
|
|
59
|
+
`The latest blocker came from an internal tool/path issue in "${toolName}": ${summarizeForLog(failure, 240)}.`,
|
|
60
|
+
'This is a recoverable execution problem, not a final user-facing blocker yet.',
|
|
61
|
+
'Do another autonomous recovery step now: locate the correct file or directory, switch to the right workspace path, use list_directory/search_files to discover the target, or rely on user-provided logs if the file only exists on another server.',
|
|
62
|
+
'Do not send a blocker reply until those recovery steps are exhausted.',
|
|
63
|
+
].join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
33
66
|
module.exports = {
|
|
34
67
|
buildBlankAfterToolFailureGuidance,
|
|
68
|
+
buildRecoverableToolFailureGuidance,
|
|
69
|
+
isRecoverableInternalToolFailure,
|
|
35
70
|
latestFailedToolExecution,
|
|
36
71
|
shouldContinueAfterBlankToolFailure,
|
|
72
|
+
shouldContinueAfterRecoverableToolFailure,
|
|
37
73
|
};
|