neoagent 2.5.2-beta.0 → 2.5.2-beta.10

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.
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ const { v4: uuidv4 } = require('uuid');
4
+ const db = require('../../../db/database');
5
+ const { activateTools } = require('../toolSelector');
6
+ const { recordRunEvent } = require('../runEvents');
7
+ const { parseMaybeJson } = require('../logFormat');
8
+ const { mergeGoalContracts } = require('./completion_judge');
9
+ const { buildInitialProgressLedger } = require('./progress_monitor');
10
+ const {
11
+ createDeliveryState,
12
+ markInterimDelivered,
13
+ markFinalDelivered,
14
+ } = require('./delivery_state');
15
+
16
+ function isoNow() {
17
+ return new Date().toISOString();
18
+ }
19
+
20
+ function persistRunMetadata(_engine, runId, patch = {}) {
21
+ if (!runId || !patch || typeof patch !== 'object') return;
22
+ const existing = db.prepare('SELECT metadata_json FROM agent_runs WHERE id = ?').get(runId);
23
+ const current = parseMaybeJson(existing?.metadata_json, {}) || {};
24
+ const next = { ...current, ...patch };
25
+ db.prepare('UPDATE agent_runs SET metadata_json = ? WHERE id = ?')
26
+ .run(JSON.stringify(next), runId);
27
+ }
28
+
29
+ function updateRunGoalContract(engine, runId, patch = {}, options = {}) {
30
+ const runMeta = engine.getRunMeta(runId);
31
+ if (!runMeta) return null;
32
+ runMeta.goalContract = mergeGoalContracts(runMeta.goalContract, patch);
33
+ if (options.persist !== false) {
34
+ persistRunMetadata(engine, runId, {
35
+ goalContract: runMeta.goalContract,
36
+ });
37
+ }
38
+ return runMeta.goalContract;
39
+ }
40
+
41
+ function buildProgressLedgerSnapshot(_engine, runMeta) {
42
+ if (!runMeta?.progressLedger) return null;
43
+ return {
44
+ currentStep: runMeta.progressLedger.currentStep || null,
45
+ currentTool: runMeta.progressLedger.currentTool || null,
46
+ currentStepStartedAt: runMeta.progressLedger.currentStepStartedAt || null,
47
+ lastVerifiedProgressAt: runMeta.progressLedger.lastVerifiedProgressAt || null,
48
+ lastUserVisibleUpdateAt: runMeta.progressLedger.lastUserVisibleUpdateAt || null,
49
+ lastFinalDeliveryAt: runMeta.progressLedger.lastFinalDeliveryAt || null,
50
+ heartbeatCount: Number(runMeta.progressLedger.heartbeatCount || 0),
51
+ stallNotifiedAt: runMeta.progressLedger.stallNotifiedAt || null,
52
+ progressState: runMeta.progressLedger.progressState || 'active',
53
+ currentPhase: runMeta.progressLedger.currentPhase || 'idle',
54
+ };
55
+ }
56
+
57
+ function persistProgressLedger(engine, runId) {
58
+ const runMeta = engine.getRunMeta(runId);
59
+ if (!runMeta?.progressLedger) return;
60
+ persistRunMetadata(engine, runId, {
61
+ progressLedger: buildProgressLedgerSnapshot(engine, runMeta),
62
+ });
63
+ }
64
+
65
+ function updateRunProgress(engine, runId, patch = {}, options = {}) {
66
+ const runMeta = engine.getRunMeta(runId);
67
+ if (!runMeta) return null;
68
+ if (!runMeta.progressLedger) {
69
+ runMeta.progressLedger = buildInitialProgressLedger({
70
+ startedAt: runMeta.startedAtIso || isoNow(),
71
+ });
72
+ }
73
+
74
+ const previousState = runMeta.progressLedger.progressState || 'active';
75
+ runMeta.progressLedger = {
76
+ ...runMeta.progressLedger,
77
+ ...patch,
78
+ };
79
+
80
+ if (options.verified === true) {
81
+ runMeta.progressLedger.lastVerifiedProgressAt = options.timestamp || isoNow();
82
+ runMeta.progressLedger.progressState = 'active';
83
+ runMeta.progressLedger.stallNotifiedAt = null;
84
+ recordRunEventSafe(engine, runMeta.userId, runId, 'progress_verified', {
85
+ phase: runMeta.progressLedger.currentPhase || 'idle',
86
+ currentStep: runMeta.progressLedger.currentStep || null,
87
+ currentTool: runMeta.progressLedger.currentTool || null,
88
+ }, { agentId: runMeta.agentId, stepId: options.stepId || null });
89
+ if (previousState === 'stalled') {
90
+ recordRunEventSafe(engine, runMeta.userId, runId, 'progress_resumed', {
91
+ phase: runMeta.progressLedger.currentPhase || 'idle',
92
+ currentStep: runMeta.progressLedger.currentStep || null,
93
+ currentTool: runMeta.progressLedger.currentTool || null,
94
+ }, { agentId: runMeta.agentId, stepId: options.stepId || null });
95
+ }
96
+ }
97
+
98
+ if (options.persist !== false) {
99
+ persistProgressLedger(engine, runId);
100
+ }
101
+ return runMeta.progressLedger;
102
+ }
103
+
104
+ function markRunVisibleProgress(engine, runId, timestamp = isoNow()) {
105
+ const runMeta = engine.getRunMeta(runId);
106
+ if (!runMeta) return null;
107
+ if (!runMeta.deliveryState) runMeta.deliveryState = createDeliveryState();
108
+ markInterimDelivered(runMeta.deliveryState);
109
+ const ledger = updateRunProgress(engine, runId, {
110
+ lastUserVisibleUpdateAt: timestamp,
111
+ }, {
112
+ persist: false,
113
+ });
114
+ persistProgressLedger(engine, runId);
115
+ return ledger;
116
+ }
117
+
118
+ function markRunFinalDelivery(engine, runId, content = '', timestamp = isoNow()) {
119
+ const runMeta = engine.getRunMeta(runId);
120
+ if (!runMeta) return null;
121
+ if (!runMeta.deliveryState) runMeta.deliveryState = createDeliveryState();
122
+ markFinalDelivered(runMeta.deliveryState);
123
+ runMeta.messagingSent = true;
124
+ runMeta.finalDeliverySent = true;
125
+ runMeta.lastSentMessage = String(content || '').trim() || runMeta.lastSentMessage || '';
126
+ const ledger = updateRunProgress(engine, runId, {
127
+ lastUserVisibleUpdateAt: timestamp,
128
+ lastFinalDeliveryAt: timestamp,
129
+ progressState: 'complete',
130
+ }, {
131
+ persist: false,
132
+ });
133
+ persistProgressLedger(engine, runId);
134
+ return ledger;
135
+ }
136
+
137
+ function recordRunEventSafe(_engine, userId, runId, eventType, payload = {}, options = {}) {
138
+ try {
139
+ return recordRunEvent({
140
+ runId,
141
+ userId,
142
+ agentId: options.agentId || null,
143
+ eventType,
144
+ requestId: options.requestId || null,
145
+ stepId: options.stepId || null,
146
+ payload,
147
+ });
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function initializeToolRuntime(engine, runId, allTools, initialTools, options = {}) {
154
+ const runMeta = engine.getRunMeta(runId);
155
+ if (!runMeta) return;
156
+ runMeta.toolCatalog = Array.isArray(allTools) ? allTools : [];
157
+ runMeta.activeTools = Array.isArray(initialTools) ? initialTools : [];
158
+ runMeta.toolSelectionOptions = {
159
+ widgetId: options.widgetId || null,
160
+ };
161
+ }
162
+
163
+ function getActiveTools(engine, runId) {
164
+ return engine.getRunMeta(runId)?.activeTools || [];
165
+ }
166
+
167
+ function activateToolsForRun(engine, runId, names = []) {
168
+ const runMeta = engine.getRunMeta(runId);
169
+ if (!runMeta) throw new Error('Run is not active.');
170
+ const result = activateTools(
171
+ runMeta.activeTools,
172
+ runMeta.toolCatalog,
173
+ names,
174
+ runMeta.toolSelectionOptions,
175
+ );
176
+ runMeta.activeTools = result.tools;
177
+ recordRunEventSafe(engine, runMeta.userId, runId, 'tools_activated', {
178
+ activated: result.activated,
179
+ evicted: result.evicted,
180
+ unknown: result.unknown,
181
+ notActivated: result.notActivated,
182
+ activeToolNames: result.tools.map((tool) => tool.name),
183
+ }, { agentId: runMeta.agentId });
184
+ return {
185
+ success: result.unknown.length === 0 && result.notActivated.length === 0,
186
+ activated: result.activated,
187
+ evicted: result.evicted,
188
+ unknown: result.unknown,
189
+ not_activated: result.notActivated,
190
+ active_tools: result.tools.map((tool) => tool.name),
191
+ };
192
+ }
193
+
194
+ function findActiveRunForUser(engine, userId, predicate = null) {
195
+ let candidate = null;
196
+ for (const [runId, runMeta] of engine.activeRuns.entries()) {
197
+ if (runMeta.userId !== userId || runMeta.aborted) continue;
198
+ if (typeof predicate === 'function' && !predicate(runMeta, runId)) continue;
199
+ if (!candidate || (runMeta.startedAt || 0) >= (candidate.startedAt || 0)) {
200
+ candidate = { runId, ...runMeta };
201
+ }
202
+ }
203
+ return candidate;
204
+ }
205
+
206
+ function findSteerableRunForUser(engine, userId, triggerSource = 'web') {
207
+ return findActiveRunForUser(
208
+ engine,
209
+ userId,
210
+ (runMeta) => runMeta.triggerSource === triggerSource && runMeta.triggerType === 'user'
211
+ );
212
+ }
213
+
214
+ function enqueueSteering(engine, runId, content, metadata = {}) {
215
+ const runMeta = engine.getRunMeta(runId);
216
+ const trimmed = typeof content === 'string' ? content.trim() : '';
217
+ if (!runMeta || runMeta.aborted || !trimmed) return null;
218
+
219
+ const item = {
220
+ id: uuidv4(),
221
+ content: trimmed,
222
+ metadata,
223
+ createdAt: isoNow(),
224
+ };
225
+
226
+ runMeta.steeringQueue.push(item);
227
+ engine.emit(runMeta.userId, 'run:steer_queued', {
228
+ runId,
229
+ content: item.content,
230
+ pendingCount: runMeta.steeringQueue.length,
231
+ });
232
+
233
+ return {
234
+ runId,
235
+ pendingCount: runMeta.steeringQueue.length,
236
+ item,
237
+ };
238
+ }
239
+
240
+ function enqueueSystemSteering(engine, runId, content, metadata = {}) {
241
+ const runMeta = engine.getRunMeta(runId);
242
+ const trimmed = typeof content === 'string' ? content.trim() : '';
243
+ if (!runMeta || runMeta.aborted || !trimmed) return null;
244
+ if (!Array.isArray(runMeta.systemSteeringQueue)) {
245
+ runMeta.systemSteeringQueue = [];
246
+ }
247
+ const signature = JSON.stringify({
248
+ content: trimmed,
249
+ reason: metadata.reason || '',
250
+ });
251
+ if (runMeta.systemSteeringQueue.some((item) => item.signature === signature)) {
252
+ return null;
253
+ }
254
+ const item = {
255
+ id: uuidv4(),
256
+ content: trimmed,
257
+ metadata,
258
+ signature,
259
+ createdAt: isoNow(),
260
+ };
261
+ runMeta.systemSteeringQueue.push(item);
262
+ return item;
263
+ }
264
+
265
+ function applyQueuedSystemSteering(engine, runId, messages) {
266
+ const runMeta = engine.getRunMeta(runId);
267
+ if (!runMeta?.systemSteeringQueue?.length) {
268
+ return { messages, appliedCount: 0 };
269
+ }
270
+
271
+ const queued = runMeta.systemSteeringQueue.splice(0, runMeta.systemSteeringQueue.length);
272
+ for (const entry of queued) {
273
+ messages.push({ role: 'system', content: entry.content });
274
+ }
275
+
276
+ return { messages, appliedCount: queued.length };
277
+ }
278
+
279
+ function applyQueuedSteering(engine, runId, messages, { userId, conversationId }) {
280
+ const runMeta = engine.getRunMeta(runId);
281
+ if (!runMeta?.steeringQueue?.length) {
282
+ return { messages, appliedCount: 0 };
283
+ }
284
+
285
+ const queued = runMeta.steeringQueue.splice(0, runMeta.steeringQueue.length);
286
+ messages.push({
287
+ role: 'system',
288
+ content: [
289
+ 'The user sent follow-up messages while you were already working.',
290
+ 'Treat them as steering or next-up context for the same conversation.',
291
+ 'If a message materially changes the active task, incorporate it now.',
292
+ 'If it is unrelated or better handled after the current task, finish the current work first and then address it.',
293
+ ].join(' '),
294
+ });
295
+
296
+ for (const entry of queued) {
297
+ messages.push({ role: 'user', content: entry.content });
298
+ if (conversationId) {
299
+ db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
300
+ .run(conversationId, 'user', entry.content);
301
+ }
302
+ }
303
+
304
+ engine.emit(userId, 'run:steer_applied', {
305
+ runId,
306
+ count: queued.length,
307
+ pendingCount: runMeta.steeringQueue.length,
308
+ latestContent: queued[queued.length - 1]?.content || '',
309
+ });
310
+
311
+ return { messages, appliedCount: queued.length };
312
+ }
313
+
314
+ function isRunStopped(engine, runId) {
315
+ return engine.getRunMeta(runId)?.aborted === true;
316
+ }
317
+
318
+ function attachProcessToRun(engine, runId, pid) {
319
+ const runMeta = engine.getRunMeta(runId);
320
+ if (!runMeta || !pid) return;
321
+ runMeta.toolPids.add(pid);
322
+ if (runMeta.aborted) {
323
+ if (engine.runtimeManager && typeof engine.runtimeManager.killCommand === 'function') {
324
+ void engine.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
325
+ }
326
+ }
327
+ }
328
+
329
+ function detachProcessFromRun(engine, runId, pid) {
330
+ const runMeta = engine.getRunMeta(runId);
331
+ if (!runMeta || !pid) return;
332
+ runMeta.toolPids.delete(pid);
333
+ }
334
+
335
+ module.exports = {
336
+ activateToolsForRun,
337
+ applyQueuedSteering,
338
+ applyQueuedSystemSteering,
339
+ attachProcessToRun,
340
+ buildProgressLedgerSnapshot,
341
+ detachProcessFromRun,
342
+ enqueueSteering,
343
+ enqueueSystemSteering,
344
+ findActiveRunForUser,
345
+ findSteerableRunForUser,
346
+ getActiveTools,
347
+ initializeToolRuntime,
348
+ isRunStopped,
349
+ markRunFinalDelivery,
350
+ markRunVisibleProgress,
351
+ persistProgressLedger,
352
+ persistRunMetadata,
353
+ recordRunEventSafe,
354
+ updateRunGoalContract,
355
+ updateRunProgress,
356
+ };
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ const { v4: uuidv4 } = require('uuid');
4
+ const db = require('../../../db/database');
5
+ const { globalHooks } = require('../hooks');
6
+ const { summarizeForLog } = require('../logFormat');
7
+ const { inferToolFailureMessage } = require('../toolEvidence');
8
+
9
+ function getAvailableTools(_engine, app, options = {}) {
10
+ const { getAvailableTools: loadAvailableTools } = require('../tools');
11
+ return loadAvailableTools(app, options);
12
+ }
13
+
14
+ async function executeTool(engine, toolName, args, context) {
15
+ const { executeTool: runTool } = require('../tools');
16
+ return runTool(toolName, args, context, engine);
17
+ }
18
+
19
+ function isReadOnlyToolCall(toolCall) {
20
+ const name = String(toolCall?.function?.name || '');
21
+ const readOnly = new Set([
22
+ 'read_file',
23
+ 'list_directory',
24
+ 'search_files',
25
+ 'code_navigate',
26
+ 'query_structured_data',
27
+ 'memory_recall',
28
+ 'memory_read',
29
+ 'session_search',
30
+ 'web_search',
31
+ 'list_tasks',
32
+ 'list_skills',
33
+ 'list_subagents',
34
+ 'recordings_list',
35
+ 'recordings_get',
36
+ 'recordings_search',
37
+ 'read_health_data',
38
+ ]);
39
+ if (name === 'http_request') {
40
+ try {
41
+ const args = JSON.parse(toolCall.function.arguments || '{}');
42
+ return String(args.method || 'GET').toUpperCase() === 'GET';
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ return readOnly.has(name);
48
+ }
49
+
50
+ async function executeReadOnlyBatch(engine, toolCalls, context = {}) {
51
+ const {
52
+ userId,
53
+ runId,
54
+ agentId,
55
+ app,
56
+ triggerType,
57
+ triggerSource,
58
+ conversationId,
59
+ startingStepIndex,
60
+ options = {},
61
+ } = context;
62
+ const prepared = [];
63
+ let nextStepIndex = startingStepIndex;
64
+ for (const toolCall of toolCalls) {
65
+ nextStepIndex += 1;
66
+ let toolArgs = {};
67
+ try { toolArgs = JSON.parse(toolCall.function.arguments || '{}'); } catch {}
68
+ const toolName = toolCall.function.name;
69
+ const repetitionGuard = engine.getRunMeta(runId)?.repetitionGuard;
70
+ if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
71
+ const result = {
72
+ status: 'blocked',
73
+ reason: 'The same read-only call already returned an unchanged result twice.',
74
+ };
75
+ prepared.push({
76
+ toolCall,
77
+ toolName,
78
+ toolArgs,
79
+ stepIndex: nextStepIndex,
80
+ blocked: true,
81
+ result,
82
+ error: result.reason,
83
+ });
84
+ engine.recordRunEvent(userId, runId, 'repetition_blocked', {
85
+ toolName,
86
+ toolArgs,
87
+ parallel: true,
88
+ }, { agentId });
89
+ continue;
90
+ }
91
+ if (globalHooks.has('before_tool_call')) {
92
+ const hookResult = await globalHooks.run('before_tool_call', {
93
+ toolName,
94
+ toolArgs,
95
+ runId,
96
+ userId,
97
+ agentId,
98
+ iteration: context.iteration,
99
+ });
100
+ if (hookResult.block) {
101
+ prepared.push({
102
+ toolCall,
103
+ toolName,
104
+ toolArgs,
105
+ stepIndex: nextStepIndex,
106
+ blocked: true,
107
+ result: { status: 'blocked', reason: hookResult.reason || 'Blocked by policy.', blocked_by: hookResult.blocked_by || 'policy' },
108
+ });
109
+ continue;
110
+ }
111
+ if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
112
+ }
113
+ const stepId = uuidv4();
114
+ db.prepare(
115
+ `INSERT INTO agent_steps (
116
+ id, run_id, step_index, type, description, status, tool_name, tool_input, started_at
117
+ ) VALUES (?, ?, ?, ?, ?, 'running', ?, ?, datetime('now'))`
118
+ ).run(
119
+ stepId,
120
+ runId,
121
+ nextStepIndex,
122
+ engine.getStepType(toolName),
123
+ `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)}`,
124
+ toolName,
125
+ JSON.stringify(toolArgs),
126
+ );
127
+ engine.emit(userId, 'run:tool_start', {
128
+ runId,
129
+ stepId,
130
+ stepIndex: nextStepIndex,
131
+ toolName,
132
+ toolArgs,
133
+ type: engine.getStepType(toolName),
134
+ });
135
+ engine.recordRunEvent(userId, runId, 'tool_started', {
136
+ stepIndex: nextStepIndex,
137
+ toolName,
138
+ toolArgs,
139
+ type: engine.getStepType(toolName),
140
+ parallel: true,
141
+ }, { agentId, stepId });
142
+ prepared.push({ toolCall, toolName, toolArgs, stepId, stepIndex: nextStepIndex });
143
+ }
144
+ engine.recordRunEvent(userId, runId, 'parallel_batch_started', {
145
+ toolNames: prepared.map((item) => item.toolName),
146
+ count: prepared.length,
147
+ }, { agentId });
148
+ const results = await Promise.all(prepared.map(async (item) => {
149
+ if (item.blocked) return item;
150
+ const startedAt = Date.now();
151
+ try {
152
+ const result = await executeTool(engine, item.toolName, item.toolArgs, {
153
+ userId,
154
+ runId,
155
+ agentId,
156
+ app,
157
+ triggerType,
158
+ triggerSource,
159
+ conversationId,
160
+ source: options.source || null,
161
+ chatId: options.chatId || null,
162
+ taskId: options.taskId || null,
163
+ widgetId: options.widgetId || null,
164
+ deliveryState: options.deliveryState || null,
165
+ allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
166
+ allowExternalSideEffects: false,
167
+ });
168
+ const error = inferToolFailureMessage(item.toolName, result);
169
+ const status = error ? 'failed' : 'completed';
170
+ db.prepare(
171
+ `UPDATE agent_steps
172
+ SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime('now')
173
+ WHERE id = ?`
174
+ ).run(
175
+ status,
176
+ JSON.stringify(result).slice(0, 20000),
177
+ error || null,
178
+ result?.screenshotPath || null,
179
+ item.stepId,
180
+ );
181
+ engine.emit(userId, 'run:tool_end', {
182
+ runId,
183
+ stepId: item.stepId,
184
+ toolName: item.toolName,
185
+ result,
186
+ error: error || undefined,
187
+ status,
188
+ });
189
+ engine.recordRunEvent(userId, runId, error ? 'tool_failed' : 'tool_completed', {
190
+ toolName: item.toolName,
191
+ status,
192
+ durationMs: Date.now() - startedAt,
193
+ resultPreview: summarizeForLog(result),
194
+ parallel: true,
195
+ }, { agentId, stepId: item.stepId });
196
+ return { ...item, result, error };
197
+ } catch (err) {
198
+ db.prepare(
199
+ `UPDATE agent_steps SET status = 'failed', error = ?, completed_at = datetime('now') WHERE id = ?`
200
+ ).run(err.message, item.stepId);
201
+ engine.emit(userId, 'run:tool_end', {
202
+ runId,
203
+ stepId: item.stepId,
204
+ toolName: item.toolName,
205
+ error: err.message,
206
+ status: 'failed',
207
+ });
208
+ engine.recordRunEvent(userId, runId, 'tool_failed', {
209
+ toolName: item.toolName,
210
+ status: 'failed',
211
+ error: err.message,
212
+ durationMs: Date.now() - startedAt,
213
+ parallel: true,
214
+ }, { agentId, stepId: item.stepId });
215
+ return { ...item, result: { error: err.message }, error: err.message };
216
+ }
217
+ }));
218
+ engine.recordRunEvent(userId, runId, 'parallel_batch_completed', {
219
+ toolNames: results.map((item) => item.toolName),
220
+ failedCount: results.filter((item) => item.error).length,
221
+ }, { agentId });
222
+ return { results, endingStepIndex: nextStepIndex };
223
+ }
224
+
225
+ module.exports = {
226
+ executeReadOnlyBatch,
227
+ executeTool,
228
+ getAvailableTools,
229
+ isReadOnlyToolCall,
230
+ };