neoagent 2.5.2-beta.1 → 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.
- package/flutter_app/lib/main_chat.dart +0 -3
- package/flutter_app/lib/main_shared.dart +40 -29
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +19016 -19016
- package/server/services/ai/deliverables/artifact_helpers.js +19 -9
- package/server/services/ai/engine.js +2 -4312
- package/server/services/ai/loop/agent_engine_core.js +1590 -0
- package/server/services/ai/loop/callbacks.js +151 -0
- package/server/services/ai/loop/completion_judge.js +252 -0
- package/server/services/ai/loop/conversation_loop.js +2343 -0
- package/server/services/ai/loop/delivery_state.js +27 -0
- package/server/services/ai/loop/error_recovery.js +49 -0
- package/server/services/ai/loop/iteration_budget.js +24 -0
- package/server/services/ai/loop/messaging_delivery.js +250 -0
- package/server/services/ai/loop/model_io.js +258 -0
- package/server/services/ai/loop/progress_monitor.js +80 -0
- package/server/services/ai/loop/run_state.js +356 -0
- package/server/services/ai/loop/tool_dispatch.js +230 -0
- package/server/services/ai/tools.js +42 -2
- package/server/services/messaging/manager.js +7 -0
- package/server/services/runtime/backends/local-vm.js +7 -7
|
@@ -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
|
+
};
|
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
normalizeOutgoingMessageForPlatform,
|
|
10
10
|
} = require('../messaging/formatting_guides');
|
|
11
11
|
const { INTERIM_KINDS, normalizeInterimKind } = require('./interim');
|
|
12
|
+
const { normalizeWhatsAppId } = require('../../utils/whatsapp');
|
|
12
13
|
const {
|
|
13
14
|
executeIntegratedTool,
|
|
14
15
|
getIntegratedToolDefinitions,
|
|
@@ -320,6 +321,31 @@ function normalizeMessagingTarget(target = {}) {
|
|
|
320
321
|
return { platform, to };
|
|
321
322
|
}
|
|
322
323
|
|
|
324
|
+
function canonicalMessagingAddress(platform, value) {
|
|
325
|
+
const normalizedPlatform = String(platform || '').trim().toLowerCase();
|
|
326
|
+
const raw = String(value || '').trim();
|
|
327
|
+
if (!normalizedPlatform || !raw) return '';
|
|
328
|
+
if (normalizedPlatform !== 'whatsapp') return raw;
|
|
329
|
+
|
|
330
|
+
const lower = raw.toLowerCase();
|
|
331
|
+
const normalizedId = normalizeWhatsAppId(lower);
|
|
332
|
+
if (!normalizedId) return '';
|
|
333
|
+
if (lower.includes('@g.us')) return `group:${normalizedId}`;
|
|
334
|
+
if (lower.includes('@lid')) return `lid:${normalizedId}`;
|
|
335
|
+
return `direct:${normalizedId}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isOriginMessagingDelivery({ triggerSource, source, chatId, platform, to }) {
|
|
339
|
+
if (triggerSource !== 'messaging') return true;
|
|
340
|
+
const originPlatform = String(source || '').trim().toLowerCase();
|
|
341
|
+
const targetPlatform = String(platform || '').trim().toLowerCase();
|
|
342
|
+
if (!originPlatform || !targetPlatform || originPlatform !== targetPlatform) return false;
|
|
343
|
+
|
|
344
|
+
const originAddress = canonicalMessagingAddress(originPlatform, chatId);
|
|
345
|
+
const targetAddress = canonicalMessagingAddress(targetPlatform, to);
|
|
346
|
+
return Boolean(originAddress && targetAddress && originAddress === targetAddress);
|
|
347
|
+
}
|
|
348
|
+
|
|
323
349
|
function buildAndroidUiMatchProperties(extra = {}) {
|
|
324
350
|
return {
|
|
325
351
|
x: { type: 'number', description: 'Absolute X coordinate' },
|
|
@@ -1623,6 +1649,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1623
1649
|
case 'browser_extract': {
|
|
1624
1650
|
const { provider, backend } = await bc();
|
|
1625
1651
|
if (!provider) return { error: 'Browser controller not available' };
|
|
1652
|
+
if (!args.selector) return { error: 'browser_extract requires a "selector" argument' };
|
|
1626
1653
|
return { ...await provider.extract(args.selector, args.attribute, args.all), backend };
|
|
1627
1654
|
}
|
|
1628
1655
|
|
|
@@ -1635,7 +1662,9 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1635
1662
|
case 'browser_evaluate': {
|
|
1636
1663
|
const { provider, backend } = await bc();
|
|
1637
1664
|
if (!provider) return { error: 'Browser controller not available' };
|
|
1638
|
-
|
|
1665
|
+
const script = args.script ?? args.javascript;
|
|
1666
|
+
if (!script) return { error: 'browser_evaluate requires a "script" argument' };
|
|
1667
|
+
return { ...await provider.evaluate(script), backend };
|
|
1639
1668
|
}
|
|
1640
1669
|
|
|
1641
1670
|
case 'android_start_emulator': {
|
|
@@ -2244,7 +2273,18 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2244
2273
|
persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks'
|
|
2245
2274
|
});
|
|
2246
2275
|
// Track that the agent explicitly sent a message during this run
|
|
2247
|
-
if (
|
|
2276
|
+
if (
|
|
2277
|
+
!suppressReply
|
|
2278
|
+
&& sendResult?.success === true
|
|
2279
|
+
&& sendResult?.suppressed !== true
|
|
2280
|
+
&& isOriginMessagingDelivery({
|
|
2281
|
+
triggerSource,
|
|
2282
|
+
source: context.source,
|
|
2283
|
+
chatId: context.chatId,
|
|
2284
|
+
platform: args.platform,
|
|
2285
|
+
to: args.to,
|
|
2286
|
+
})
|
|
2287
|
+
) {
|
|
2248
2288
|
markProactiveMessageSent({ runState, deliveryState, content: normalizedMessage });
|
|
2249
2289
|
if (runState && triggerSource === 'messaging') {
|
|
2250
2290
|
runState.explicitMessageSent = true;
|
|
@@ -515,6 +515,13 @@ class MessagingManager extends EventEmitter {
|
|
|
515
515
|
}
|
|
516
516
|
|
|
517
517
|
const result = await platform.sendMessage(to, normalizedContent, sendOptions);
|
|
518
|
+
if (result?.success === false) {
|
|
519
|
+
const reason = result.error || result.reason || 'platform rejected the message';
|
|
520
|
+
const error = new Error(`Platform ${platformName} delivery failed: ${reason}`);
|
|
521
|
+
error.code = 'MESSAGING_DELIVERY_FAILED';
|
|
522
|
+
error.deliveryResult = result;
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
518
525
|
|
|
519
526
|
db.prepare('INSERT INTO messages (user_id, agent_id, run_id, role, content, platform, platform_chat_id, media_path, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
520
527
|
.run(userId, agentId, runId, 'assistant', normalizedContent, platformName, to, mediaPath, metadata ? JSON.stringify(metadata) : null);
|