neoagent 2.5.2-beta.8 → 3.0.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/package.json +2 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +4 -4
- package/server/services/ai/deliverables/artifact_helpers.js +108 -16
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/engine.js +2 -4724
- package/server/services/ai/loop/agent_engine_core.js +1590 -0
- package/server/services/ai/loop/blank_recovery.js +37 -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 +2447 -0
- package/server/services/ai/loop/delivery_state.js +27 -0
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/iteration_budget.js +24 -0
- package/server/services/ai/loop/messaging_delivery.js +296 -0
- package/server/services/ai/loop/model_io.js +258 -0
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +81 -0
- package/server/services/ai/loop/run_state.js +356 -0
- package/server/services/ai/loop/tool_dispatch.js +231 -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/systemPrompt.js +6 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +15 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +196 -26
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/workspace/manager.js +56 -0
|
@@ -1,4725 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const db = require('../../db/database');
|
|
4
|
-
const { compact } = require('./compaction');
|
|
5
|
-
const { compactPayloadForModel } = require('./preModelCompaction');
|
|
6
|
-
const {
|
|
7
|
-
getConversationContext,
|
|
8
|
-
buildSummaryCarrier,
|
|
9
|
-
refreshConversationSummary,
|
|
10
|
-
sanitizeConversationMessages
|
|
11
|
-
} = require('./history');
|
|
12
|
-
const { ensureDefaultAiSettings, getAiSettings } = require('./settings');
|
|
13
|
-
const {
|
|
14
|
-
activateTools,
|
|
15
|
-
buildToolCatalog,
|
|
16
|
-
selectInitialTools,
|
|
17
|
-
selectToolsForTask,
|
|
18
|
-
} = require('./toolSelector');
|
|
19
|
-
const { compactToolResult } = require('./toolResult');
|
|
20
|
-
const { salvageTextToolCalls } = require('./toolCallSalvage');
|
|
21
|
-
const { sanitizeModelOutput } = require('./outputSanitizer');
|
|
22
|
-
const {
|
|
23
|
-
buildAnalysisPrompt,
|
|
24
|
-
buildExecutionGuidance,
|
|
25
|
-
buildPlanPrompt,
|
|
26
|
-
buildVerifierPrompt,
|
|
27
|
-
isDirectAnswerEligibleAnalysis,
|
|
28
|
-
normalizeExecutionPlan,
|
|
29
|
-
normalizeTaskAnalysis,
|
|
30
|
-
normalizeVerificationResult,
|
|
31
|
-
parseJsonObject,
|
|
32
|
-
promoteAnalysisMode,
|
|
33
|
-
shouldRunVerifier,
|
|
34
|
-
} = require('./taskAnalysis');
|
|
35
|
-
const { getCapabilityHealth, summarizeCapabilityHealth } = require('./capabilityHealth');
|
|
36
|
-
const {
|
|
37
|
-
buildPlatformFormattingGuide,
|
|
38
|
-
splitOutgoingMessageForPlatform,
|
|
39
|
-
} = require('../messaging/formatting_guides');
|
|
40
|
-
const {
|
|
41
|
-
buildInterimMetadata,
|
|
42
|
-
buildInterimSignature,
|
|
43
|
-
normalizeInterimKind,
|
|
44
|
-
} = require('./interim');
|
|
45
|
-
const { recordRunEvent } = require('./runEvents');
|
|
46
|
-
const {
|
|
47
|
-
buildDeliverableWorkflowGuidance,
|
|
48
|
-
DeliverableValidationError,
|
|
49
|
-
extractArtifactsFromResult,
|
|
50
|
-
getDeliverableWorkflow,
|
|
51
|
-
selectDeliverableWorkflow,
|
|
52
|
-
validateDeliverableExecution,
|
|
53
|
-
} = require('./deliverables');
|
|
54
|
-
const { buildLoopPolicy, resolveToolResultLimits } = require('./loopPolicy');
|
|
55
|
-
const { globalHooks } = require('./hooks');
|
|
56
|
-
const { withProviderRetry, isTransientError } = require('./providerRetry');
|
|
57
|
-
const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('./completion');
|
|
58
|
-
const { normalizeUsage, recordModelUsage } = require('./usage');
|
|
59
|
-
const { enforceRateLimits } = require('./rate_limits');
|
|
60
|
-
const { ToolRepetitionGuard } = require('./repetitionGuard');
|
|
61
|
-
const { shortenRunId, summarizeForLog, parseMaybeJson } = require('./logFormat');
|
|
62
|
-
const {
|
|
63
|
-
normalizeOutgoingMessage,
|
|
64
|
-
clampRunContext,
|
|
65
|
-
joinSentMessages,
|
|
66
|
-
normalizeInterimText,
|
|
67
|
-
buildBlankMessagingReplyPrompt,
|
|
68
|
-
buildDeterministicMessagingFallback,
|
|
69
|
-
buildMessagingFailureScenario,
|
|
70
|
-
buildDeterministicMessagingErrorReply,
|
|
71
|
-
buildModelFailureLoopPrompt,
|
|
72
|
-
} = require('./messagingFallback');
|
|
73
|
-
const {
|
|
74
|
-
classifyToolExecution,
|
|
75
|
-
summarizeToolExecutions,
|
|
76
|
-
summarizeAvailableTools,
|
|
77
|
-
inferToolFailureMessage,
|
|
78
|
-
buildAutonomousRecoveryContext,
|
|
79
|
-
} = require('./toolEvidence');
|
|
80
|
-
const {
|
|
81
|
-
buildMemoryConsolidationInstructions,
|
|
82
|
-
normalizeMemoryCandidates,
|
|
83
|
-
} = require('../memory/consolidation');
|
|
84
|
-
const {
|
|
85
|
-
buildPlannerPrompt,
|
|
86
|
-
buildRerankerPrompt,
|
|
87
|
-
mergeRetrievalResults,
|
|
88
|
-
normalizeRerankResult,
|
|
89
|
-
normalizeRetrievalPlan,
|
|
90
|
-
shouldEnhanceRetrieval,
|
|
91
|
-
} = require('../memory/retrieval_reasoning');
|
|
1
|
+
'use strict';
|
|
92
2
|
|
|
93
|
-
|
|
94
|
-
if (!task || typeof task !== 'string') return 'Untitled';
|
|
95
|
-
const msgMatch = task.match(/received a (?:message|media|image|video|file|audio)[^:]*:\s*(.+)/is);
|
|
96
|
-
if (msgMatch) {
|
|
97
|
-
const body = msgMatch[1].replace(/\n[\s\S]*/s, '').trim();
|
|
98
|
-
return body.slice(0, 90) || 'Incoming message';
|
|
99
|
-
}
|
|
100
|
-
const cleaned = task.replace(/^\[.*?\]\s*/i, '').replace(/^(system|task|prompt)[:\s]+/i, '').trim();
|
|
101
|
-
return cleaned.slice(0, 90);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function buildInitialRunMetadata(options = {}) {
|
|
105
|
-
const metadata = {};
|
|
106
|
-
if (options.taskId != null && String(options.taskId).trim()) {
|
|
107
|
-
metadata.taskId = options.taskId;
|
|
108
|
-
}
|
|
109
|
-
if (options.widgetId != null && String(options.widgetId).trim()) {
|
|
110
|
-
metadata.widgetId = options.widgetId;
|
|
111
|
-
}
|
|
112
|
-
return metadata;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const MESSAGING_PROGRESS_FIRST_UPDATE_MS = 60 * 1000;
|
|
116
|
-
const MESSAGING_PROGRESS_REPEAT_MS = 90 * 1000;
|
|
117
|
-
const MESSAGING_PROGRESS_STALL_MS = 240 * 1000;
|
|
118
|
-
const MESSAGING_PROGRESS_TICK_MS = 15 * 1000;
|
|
119
|
-
const GOAL_CONTRACT_SUCCESS_CRITERIA_LIMIT = 12;
|
|
120
|
-
const MODEL_CALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
121
|
-
|
|
122
|
-
function isoNow() {
|
|
123
|
-
return new Date().toISOString();
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function timestampMs(value, fallback = 0) {
|
|
127
|
-
const resolved = value ? Date.parse(value) : NaN;
|
|
128
|
-
return Number.isFinite(resolved) ? resolved : fallback;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function formatElapsedDuration(durationMs) {
|
|
132
|
-
const totalSeconds = Math.max(1, Math.floor(Number(durationMs || 0) / 1000));
|
|
133
|
-
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
134
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
135
|
-
const seconds = totalSeconds % 60;
|
|
136
|
-
if (seconds === 0) return `${minutes}m`;
|
|
137
|
-
return `${minutes}m ${seconds}s`;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function normalizeErrorKey(errorMsg) {
|
|
141
|
-
const msg = String(errorMsg || '').toLowerCase();
|
|
142
|
-
if (/outside.*(workspace|per-user)/i.test(msg)) return 'outside_workspace';
|
|
143
|
-
if (/eisdir|illegal operation on a directory/i.test(msg)) return 'eisdir';
|
|
144
|
-
if (/enoent|no such file/i.test(msg)) return 'enoent';
|
|
145
|
-
if (/can.?t cd to|no such directory/i.test(msg)) return 'bad_cwd';
|
|
146
|
-
if (/not found/i.test(msg)) return 'not_found';
|
|
147
|
-
if (/owner_repo.*format|must be.*owner.*repo|owner.*repo.*string|owner.*repo.*combined/i.test(msg)) return 'owner_repo_format';
|
|
148
|
-
return msg.slice(0, 60);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function trackErrorPattern(errorMsg, runMeta) {
|
|
152
|
-
if (!errorMsg) return;
|
|
153
|
-
const key = normalizeErrorKey(errorMsg);
|
|
154
|
-
if (!runMeta.errorPatterns) runMeta.errorPatterns = new Map();
|
|
155
|
-
runMeta.errorPatterns.set(key, (runMeta.errorPatterns.get(key) || 0) + 1);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function buildErrorPatternGuidance(key, count) {
|
|
159
|
-
// Immediate guidance on first occurrence for high-signal patterns that waste
|
|
160
|
-
// multiple iterations before self-correcting.
|
|
161
|
-
const immediateGuides = {
|
|
162
|
-
eisdir: 'That path is a directory (or a VM-only path like /tmp that read_file cannot reach). Use execute_command with `cat <path>` to read files inside VMs, or list_directory to inspect a directory.',
|
|
163
|
-
owner_repo_format: 'The parameter "owner_repo" expects a single combined string like "NeoLabs-Systems/NeoAgent" — not separate owner/repo fields. Pass the full "owner/repo" as one value.',
|
|
164
|
-
};
|
|
165
|
-
if (immediateGuides[key]) {
|
|
166
|
-
const prefix = count > 1 ? `REPEATED ERROR (${count}×): ` : 'ERROR GUIDANCE: ';
|
|
167
|
-
return `${prefix}${immediateGuides[key]}`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (count < 3) return null;
|
|
171
|
-
const guides = {
|
|
172
|
-
outside_workspace: 'read_file cannot access /tmp paths. Use execute_command with `cat <path>` instead.',
|
|
173
|
-
enoent: 'That path does not exist. Use execute_command with `find . -name "..."` to locate the correct path first.',
|
|
174
|
-
bad_cwd: 'The VM home directory is not ~/. Use absolute paths starting from /tmp or discover the workspace root first.',
|
|
175
|
-
not_found: 'This path or resource was not found. Try listing the parent directory or checking with a broader search first.',
|
|
176
|
-
};
|
|
177
|
-
const guide = guides[key];
|
|
178
|
-
if (!guide) return null;
|
|
179
|
-
return `REPEATED ERROR (${count}×): ${guide}`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const OUTPUT_FINGERPRINT_TOOLS = /^(list_|search_|read_|get_|find_|github_list|github_get|github_search)/;
|
|
183
|
-
|
|
184
|
-
function fingerprintOutput(toolName, result) {
|
|
185
|
-
if (!toolName || !OUTPUT_FINGERPRINT_TOOLS.test(toolName)) return null;
|
|
186
|
-
const raw = typeof result === 'string' ? result : JSON.stringify(result ?? '');
|
|
187
|
-
if (raw.length < 200) return null;
|
|
188
|
-
// djb2 hash over first 3000 chars — fast, collision-unlikely for our sizes
|
|
189
|
-
let h = 5381;
|
|
190
|
-
const limit = Math.min(raw.length, 3000);
|
|
191
|
-
for (let i = 0; i < limit; i++) h = ((h << 5) + h) ^ raw.charCodeAt(i);
|
|
192
|
-
return h >>> 0;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Tools that represent concrete forward progress (write, create, send, update, run).
|
|
196
|
-
// Anything NOT in this set is considered read-only for the analysis-paralysis gate.
|
|
197
|
-
// execute_command counts as progress — it can do anything, including modify state.
|
|
198
|
-
function isProgressTool(toolName) {
|
|
199
|
-
if (!toolName) return false;
|
|
200
|
-
// Neutral / bookkeeping — don't count either way
|
|
201
|
-
if (toolName === 'activate_tools' || toolName === 'save_widget_snapshot') return false;
|
|
202
|
-
// Explicitly read-only patterns
|
|
203
|
-
if (/^(list_|search_|read_file|get_file|find_files?|github_list|github_get|github_search|browser_get|browser_read)/.test(toolName)) return false;
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function resolveModelCallTimeoutMs(options = {}) {
|
|
208
|
-
const requested = Number(options?.modelCallTimeoutMs);
|
|
209
|
-
if (Number.isFinite(requested) && requested > 0) {
|
|
210
|
-
return Math.max(10, requested);
|
|
211
|
-
}
|
|
212
|
-
return MODEL_CALL_TIMEOUT_MS;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async function withModelCallTimeout(promise, options = {}, label = 'Model call') {
|
|
216
|
-
const timeoutMs = resolveModelCallTimeoutMs(options);
|
|
217
|
-
let timer = null;
|
|
218
|
-
const timeout = new Promise((_, reject) => {
|
|
219
|
-
timer = setTimeout(() => {
|
|
220
|
-
const error = new Error(`${label} timed out after ${formatElapsedDuration(timeoutMs)}.`);
|
|
221
|
-
error.code = 'MODEL_CALL_TIMEOUT';
|
|
222
|
-
reject(error);
|
|
223
|
-
}, timeoutMs);
|
|
224
|
-
});
|
|
225
|
-
try {
|
|
226
|
-
return await Promise.race([Promise.resolve(promise), timeout]);
|
|
227
|
-
} finally {
|
|
228
|
-
if (timer) clearTimeout(timer);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function cloneInterimHistory(history = []) {
|
|
233
|
-
if (!Array.isArray(history)) return [];
|
|
234
|
-
return history.map((item) => ({
|
|
235
|
-
content: String(item?.content || '').trim(),
|
|
236
|
-
kind: normalizeInterimKind(item?.kind),
|
|
237
|
-
expectsReply: item?.expectsReply === true,
|
|
238
|
-
deferFollowUp: item?.deferFollowUp === true,
|
|
239
|
-
createdAt: item?.createdAt || isoNow(),
|
|
240
|
-
})).filter((item) => item.content);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function createInterimSignatureSet(history = [], platform = null) {
|
|
244
|
-
const signatures = new Set();
|
|
245
|
-
for (const item of cloneInterimHistory(history)) {
|
|
246
|
-
signatures.add(buildInterimSignature({
|
|
247
|
-
content: item.content,
|
|
248
|
-
kind: item.kind,
|
|
249
|
-
expectsReply: item.expectsReply === true,
|
|
250
|
-
platform,
|
|
251
|
-
}));
|
|
252
|
-
}
|
|
253
|
-
return signatures;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function buildInitialProgressLedger({ startedAt, retryState = {} } = {}) {
|
|
257
|
-
const startedAtIso = startedAt || isoNow();
|
|
258
|
-
const interimHistory = cloneInterimHistory(retryState.interimHistory);
|
|
259
|
-
const lastInterimMessage = interimHistory[interimHistory.length - 1]?.content || '';
|
|
260
|
-
const lastVisibleAt = retryState.lastUserVisibleUpdateAt || (lastInterimMessage ? startedAtIso : null);
|
|
261
|
-
return {
|
|
262
|
-
currentStep: retryState.currentStep || null,
|
|
263
|
-
currentTool: retryState.currentTool || null,
|
|
264
|
-
currentStepStartedAt: retryState.currentStepStartedAt || null,
|
|
265
|
-
lastVerifiedProgressAt: retryState.lastVerifiedProgressAt || startedAtIso,
|
|
266
|
-
lastUserVisibleUpdateAt: lastVisibleAt,
|
|
267
|
-
lastFinalDeliveryAt: retryState.lastFinalDeliveryAt || null,
|
|
268
|
-
heartbeatCount: Number(retryState.heartbeatCount || 0),
|
|
269
|
-
stallNotifiedAt: retryState.stallNotifiedAt || null,
|
|
270
|
-
progressState: retryState.progressState || 'active',
|
|
271
|
-
currentPhase: retryState.currentPhase || 'idle',
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function hasVisibleInterimActivity(runMeta) {
|
|
276
|
-
return Boolean(
|
|
277
|
-
runMeta?.lastInterimMessage
|
|
278
|
-
|| (Array.isArray(runMeta?.interimMessages) && runMeta.interimMessages.length > 0)
|
|
279
|
-
|| Number(runMeta?.progressLedger?.heartbeatCount || 0) > 0
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function requireSuccessfulMessagingDelivery(result, label = 'Messaging delivery') {
|
|
284
|
-
if (result?.success === true && result?.suppressed !== true) {
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
const reason = String(
|
|
288
|
-
result?.error
|
|
289
|
-
|| result?.reason
|
|
290
|
-
|| result?.result?.error
|
|
291
|
-
|| result?.result?.reason
|
|
292
|
-
|| 'the platform did not confirm delivery',
|
|
293
|
-
).trim();
|
|
294
|
-
const error = new Error(`${label} failed: ${reason}`);
|
|
295
|
-
error.code = 'MESSAGING_DELIVERY_FAILED';
|
|
296
|
-
error.deliveryResult = result || null;
|
|
297
|
-
throw error;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function normalizeGoalCriteria(value) {
|
|
301
|
-
if (!Array.isArray(value)) return [];
|
|
302
|
-
const seen = new Set();
|
|
303
|
-
const items = [];
|
|
304
|
-
for (const entry of value) {
|
|
305
|
-
const text = String(entry || '').trim();
|
|
306
|
-
if (!text) continue;
|
|
307
|
-
const signature = text.toLowerCase();
|
|
308
|
-
if (seen.has(signature)) continue;
|
|
309
|
-
seen.add(signature);
|
|
310
|
-
items.push(text);
|
|
311
|
-
if (items.length >= GOAL_CONTRACT_SUCCESS_CRITERIA_LIMIT) break;
|
|
312
|
-
}
|
|
313
|
-
return items;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function normalizeGoalContract(raw = null) {
|
|
317
|
-
if (!raw || typeof raw !== 'object') return null;
|
|
318
|
-
const goal = String(raw.goal || '').trim();
|
|
319
|
-
const successCriteria = normalizeGoalCriteria(
|
|
320
|
-
raw.successCriteria || raw.success_criteria || [],
|
|
321
|
-
);
|
|
322
|
-
const rawCompletionConfidence = String(
|
|
323
|
-
raw.completionConfidenceRequired || raw.completion_confidence_required || '',
|
|
324
|
-
).trim();
|
|
325
|
-
const completionConfidenceRequired = rawCompletionConfidence
|
|
326
|
-
? normalizeCompletionConfidence(rawCompletionConfidence)
|
|
327
|
-
: '';
|
|
328
|
-
const progressUpdatePolicy = ['none', 'optional', 'required'].includes(String(
|
|
329
|
-
raw.progressUpdatePolicy || raw.progress_update_policy || '',
|
|
330
|
-
).trim().toLowerCase())
|
|
331
|
-
? String(raw.progressUpdatePolicy || raw.progress_update_policy || '').trim().toLowerCase()
|
|
332
|
-
: '';
|
|
333
|
-
const autonomyLevel = ['minimal', 'normal', 'high'].includes(String(
|
|
334
|
-
raw.autonomyLevel || raw.autonomy_level || '',
|
|
335
|
-
).trim().toLowerCase())
|
|
336
|
-
? String(raw.autonomyLevel || raw.autonomy_level || '').trim().toLowerCase()
|
|
337
|
-
: '';
|
|
338
|
-
const complexity = ['simple', 'standard', 'complex'].includes(String(
|
|
339
|
-
raw.complexity || '',
|
|
340
|
-
).trim().toLowerCase())
|
|
341
|
-
? String(raw.complexity || '').trim().toLowerCase()
|
|
342
|
-
: '';
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
!goal
|
|
346
|
-
&& successCriteria.length === 0
|
|
347
|
-
&& !completionConfidenceRequired
|
|
348
|
-
&& !progressUpdatePolicy
|
|
349
|
-
&& !autonomyLevel
|
|
350
|
-
&& !complexity
|
|
351
|
-
) {
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
goal,
|
|
357
|
-
successCriteria,
|
|
358
|
-
completionConfidenceRequired,
|
|
359
|
-
progressUpdatePolicy: progressUpdatePolicy || '',
|
|
360
|
-
autonomyLevel: autonomyLevel || '',
|
|
361
|
-
complexity: complexity || '',
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function mergeGoalContracts(existing = null, patch = null) {
|
|
366
|
-
const current = normalizeGoalContract(existing) || null;
|
|
367
|
-
const nextPatch = normalizeGoalContract(patch) || null;
|
|
368
|
-
if (!current && !nextPatch) return null;
|
|
369
|
-
|
|
370
|
-
const goal = String(current?.goal || nextPatch?.goal || '').trim();
|
|
371
|
-
const successCriteria = normalizeGoalCriteria([
|
|
372
|
-
...(current?.successCriteria || []),
|
|
373
|
-
...(nextPatch?.successCriteria || []),
|
|
374
|
-
]);
|
|
375
|
-
const completionConfidenceRequired = nextPatch?.completionConfidenceRequired
|
|
376
|
-
|| current?.completionConfidenceRequired
|
|
377
|
-
|| 'medium';
|
|
378
|
-
const progressUpdatePolicy = nextPatch?.progressUpdatePolicy
|
|
379
|
-
|| current?.progressUpdatePolicy
|
|
380
|
-
|| '';
|
|
381
|
-
const autonomyLevel = nextPatch?.autonomyLevel
|
|
382
|
-
|| current?.autonomyLevel
|
|
383
|
-
|| '';
|
|
384
|
-
const complexity = nextPatch?.complexity
|
|
385
|
-
|| current?.complexity
|
|
386
|
-
|| '';
|
|
387
|
-
|
|
388
|
-
return normalizeGoalContract({
|
|
389
|
-
goal,
|
|
390
|
-
successCriteria,
|
|
391
|
-
completionConfidenceRequired,
|
|
392
|
-
progressUpdatePolicy,
|
|
393
|
-
autonomyLevel,
|
|
394
|
-
complexity,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function goalContractFromAnalysis(analysis = null) {
|
|
399
|
-
if (!analysis || typeof analysis !== 'object') return null;
|
|
400
|
-
return normalizeGoalContract({
|
|
401
|
-
goal: analysis.goal,
|
|
402
|
-
successCriteria: analysis.success_criteria,
|
|
403
|
-
completionConfidenceRequired: analysis.completion_confidence_required,
|
|
404
|
-
progressUpdatePolicy: analysis.progress_update_policy,
|
|
405
|
-
autonomyLevel: analysis.autonomy_level,
|
|
406
|
-
complexity: analysis.complexity,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function goalContractFromPlan(plan = null) {
|
|
411
|
-
if (!plan || typeof plan !== 'object') return null;
|
|
412
|
-
return normalizeGoalContract({
|
|
413
|
-
successCriteria: plan.success_criteria,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function buildResolvedGoalContract(runMeta, analysis = null, plan = null) {
|
|
418
|
-
let contract = mergeGoalContracts(runMeta?.goalContract || null, goalContractFromAnalysis(analysis));
|
|
419
|
-
contract = mergeGoalContracts(contract, goalContractFromPlan(plan));
|
|
420
|
-
return contract;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function buildGoalContractPrompt(contract, label = 'Persistent run goal') {
|
|
424
|
-
const normalized = normalizeGoalContract(contract);
|
|
425
|
-
if (!normalized) return '';
|
|
426
|
-
const lines = [];
|
|
427
|
-
if (normalized.goal) {
|
|
428
|
-
lines.push(`${label}: ${normalized.goal}`);
|
|
429
|
-
}
|
|
430
|
-
if (normalized.successCriteria.length > 0) {
|
|
431
|
-
lines.push(`Persistent success criteria:\n- ${normalized.successCriteria.join('\n- ')}`);
|
|
432
|
-
}
|
|
433
|
-
const contractLine = [
|
|
434
|
-
normalized.complexity ? `complexity=${normalized.complexity}` : '',
|
|
435
|
-
normalized.autonomyLevel ? `autonomy_level=${normalized.autonomyLevel}` : '',
|
|
436
|
-
normalized.progressUpdatePolicy ? `progress_update_policy=${normalized.progressUpdatePolicy}` : '',
|
|
437
|
-
normalized.completionConfidenceRequired ? `completion_confidence_required=${normalized.completionConfidenceRequired}` : '',
|
|
438
|
-
].filter(Boolean).join('; ');
|
|
439
|
-
if (contractLine) {
|
|
440
|
-
lines.push(`Persistent autonomy contract: ${contractLine}`);
|
|
441
|
-
}
|
|
442
|
-
return lines.join('\n');
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function resolveRunGoalContext(runMeta, analysis = null, plan = null) {
|
|
446
|
-
const goalContract = buildResolvedGoalContract(runMeta, analysis, plan);
|
|
447
|
-
const successCriteria = goalContract?.successCriteria?.length
|
|
448
|
-
? goalContract.successCriteria.slice(0, 6)
|
|
449
|
-
: (Array.isArray(plan?.success_criteria)
|
|
450
|
-
? plan.success_criteria
|
|
451
|
-
.map((item) => String(item || '').trim())
|
|
452
|
-
.filter(Boolean)
|
|
453
|
-
.slice(0, 6)
|
|
454
|
-
: []);
|
|
455
|
-
const effectiveGoal = goalContract?.goal || analysis?.goal || '';
|
|
456
|
-
const effectiveComplexity = goalContract?.complexity || analysis?.complexity || 'standard';
|
|
457
|
-
const effectiveAutonomyLevel = goalContract?.autonomyLevel || analysis?.autonomy_level || 'normal';
|
|
458
|
-
const effectiveProgressPolicy = goalContract?.progressUpdatePolicy || analysis?.progress_update_policy || 'optional';
|
|
459
|
-
const effectiveCompletionConfidence = goalContract?.completionConfidenceRequired
|
|
460
|
-
|| analysis?.completion_confidence_required
|
|
461
|
-
|| 'medium';
|
|
462
|
-
const persistedGoalPrompt = buildGoalContractPrompt(goalContract);
|
|
463
|
-
return {
|
|
464
|
-
goalContract,
|
|
465
|
-
successCriteria,
|
|
466
|
-
effectiveGoal,
|
|
467
|
-
effectiveComplexity,
|
|
468
|
-
effectiveAutonomyLevel,
|
|
469
|
-
effectiveProgressPolicy,
|
|
470
|
-
effectiveCompletionConfidence,
|
|
471
|
-
persistedGoalPrompt,
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function buildCompletionDecisionPrompt({
|
|
476
|
-
triggerSource,
|
|
477
|
-
messagingSent = false,
|
|
478
|
-
goalContext,
|
|
479
|
-
parallelWork = false,
|
|
480
|
-
tools,
|
|
481
|
-
toolExecutions,
|
|
482
|
-
lastReply,
|
|
483
|
-
iteration,
|
|
484
|
-
maxIterations,
|
|
485
|
-
}) {
|
|
486
|
-
const draftReply = normalizeOutgoingMessage(lastReply) || '';
|
|
487
|
-
const lines = [
|
|
488
|
-
'Return JSON only.',
|
|
489
|
-
'Decide whether this run should continue autonomously or stop now.',
|
|
490
|
-
'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason"}',
|
|
491
|
-
'Rules:',
|
|
492
|
-
'- Use "continue" whenever any safe next step remains in this same run.',
|
|
493
|
-
'- Use "complete" only when the requested outcome is actually achieved and the latest draft is the finished user-facing answer.',
|
|
494
|
-
'- Use "blocked" only when a specific external dependency, missing user input, or permission outside this run is required and the latest draft is the blocker reply.',
|
|
495
|
-
'- If the latest draft asks the user for a missing required value, confirmation, or choice needed to proceed, use "blocked" so the run waits instead of repeating the same ask.',
|
|
496
|
-
'- A progress note, next-step note, apology, plan, or promise to investigate is "continue", not "complete".',
|
|
497
|
-
'- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
|
|
498
|
-
'- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
|
|
499
|
-
`- If completion_confidence_required is ${goalContext.effectiveCompletionConfidence} and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.`,
|
|
500
|
-
triggerSource === 'messaging' && messagingSent
|
|
501
|
-
? '- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.'
|
|
502
|
-
: triggerSource === 'messaging'
|
|
503
|
-
? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.'
|
|
504
|
-
: '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
|
|
505
|
-
];
|
|
506
|
-
|
|
507
|
-
lines.push(
|
|
508
|
-
goalContext.effectiveGoal ? `Goal: ${goalContext.effectiveGoal}` : '',
|
|
509
|
-
goalContext.persistedGoalPrompt,
|
|
510
|
-
`Autonomy contract: complexity=${goalContext.effectiveComplexity}; autonomy_level=${goalContext.effectiveAutonomyLevel}; progress_update_policy=${goalContext.effectiveProgressPolicy}; parallel_work=${parallelWork === true}; completion_confidence_required=${goalContext.effectiveCompletionConfidence}.`,
|
|
511
|
-
goalContext.successCriteria.length > 0
|
|
512
|
-
? `Success criteria:\n${goalContext.successCriteria.map((item, index) => `${index + 1}. ${item}`).join('\n')}`
|
|
513
|
-
: '',
|
|
514
|
-
`Current iteration: ${iteration} of ${maxIterations}.`,
|
|
515
|
-
`Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
|
|
516
|
-
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 8) || 'none'}`,
|
|
517
|
-
`Latest draft reply:\n${draftReply || '(empty)'}`,
|
|
518
|
-
);
|
|
519
|
-
return lines.filter(Boolean).join('\n');
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
|
|
523
|
-
const allowed = new Set(['continue', 'complete', 'blocked']);
|
|
524
|
-
const requestedStatus = String(raw.status || '').trim().toLowerCase();
|
|
525
|
-
return {
|
|
526
|
-
status: allowed.has(requestedStatus) ? requestedStatus : fallbackStatus,
|
|
527
|
-
reason: String(raw.reason || '').trim().slice(0, 400),
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function planningDepthForForceMode(forceMode) {
|
|
532
|
-
return forceMode === 'plan_execute' ? 'deep' : 'light';
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function buildSkipTaskAnalysisResult(forceMode) {
|
|
536
|
-
return {
|
|
537
|
-
mode: forceMode === 'plan_execute' ? 'plan_execute' : 'execute',
|
|
538
|
-
reply_mode: 'task',
|
|
539
|
-
freshness_risk: 'none',
|
|
540
|
-
verification_need: 'none',
|
|
541
|
-
planning_depth: planningDepthForForceMode(forceMode),
|
|
542
|
-
confidence: 0.5,
|
|
543
|
-
suggested_tools: [],
|
|
544
|
-
needs_subagents: false,
|
|
545
|
-
draft_reply: '',
|
|
546
|
-
goal: 'Complete the user request accurately.',
|
|
547
|
-
success_criteria: [],
|
|
548
|
-
complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
|
|
549
|
-
autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
|
|
550
|
-
progress_update_policy: 'optional',
|
|
551
|
-
parallel_work: false,
|
|
552
|
-
completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
|
|
557
|
-
return {
|
|
558
|
-
mode: forceMode || 'execute',
|
|
559
|
-
verification_need: 'light',
|
|
560
|
-
planning_depth: planningDepthForForceMode(forceMode),
|
|
561
|
-
goal: userMessage ? String(userMessage).trim().slice(0, 300) : '',
|
|
562
|
-
complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
|
|
563
|
-
autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
|
|
564
|
-
progress_update_policy: 'optional',
|
|
565
|
-
parallel_work: false,
|
|
566
|
-
completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function applyForcedAnalysisMode(analysis, forceMode) {
|
|
571
|
-
if (!analysis || typeof analysis !== 'object') return analysis;
|
|
572
|
-
if (forceMode !== 'plan_execute') return analysis;
|
|
573
|
-
return {
|
|
574
|
-
...analysis,
|
|
575
|
-
mode: 'plan_execute',
|
|
576
|
-
planning_depth: 'deep',
|
|
577
|
-
complexity: 'complex',
|
|
578
|
-
autonomy_level: 'high',
|
|
579
|
-
completion_confidence_required: analysis.completion_confidence_required || 'high',
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function buildAutonomyPolicyFromAnalysis(analysis = {}) {
|
|
584
|
-
return {
|
|
585
|
-
complexity: analysis.complexity || 'standard',
|
|
586
|
-
autonomy_level: analysis.autonomy_level || 'normal',
|
|
587
|
-
progress_update_policy: analysis.progress_update_policy || 'optional',
|
|
588
|
-
parallel_work: analysis.parallel_work === true,
|
|
589
|
-
completion_confidence_required: analysis.completion_confidence_required || 'medium',
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
|
|
594
|
-
const { getSupportedModels, createProviderInstance } = require('./models');
|
|
595
|
-
const agentId = providerConfig.agentId || null;
|
|
596
|
-
const aiSettings = getAiSettings(userId, agentId);
|
|
597
|
-
const models = await getSupportedModels(userId, agentId);
|
|
598
|
-
|
|
599
|
-
let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
|
|
600
|
-
const defaultChatModel = aiSettings.default_chat_model || 'auto';
|
|
601
|
-
const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
|
|
602
|
-
const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
|
|
603
|
-
|
|
604
|
-
const knownModelIds = new Set(models.map((m) => m.id));
|
|
605
|
-
const selectableModels = models.filter((m) => m.available !== false);
|
|
606
|
-
|
|
607
|
-
enabledIds = Array.isArray(enabledIds)
|
|
608
|
-
? enabledIds
|
|
609
|
-
.map((id) => String(id))
|
|
610
|
-
.filter((id) => knownModelIds.has(id))
|
|
611
|
-
: [];
|
|
612
|
-
|
|
613
|
-
let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
|
|
614
|
-
if (availableModels.length === 0) {
|
|
615
|
-
enabledIds = selectableModels.map((m) => m.id);
|
|
616
|
-
availableModels = [...selectableModels];
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
|
|
620
|
-
|
|
621
|
-
if (!fallbackModel) {
|
|
622
|
-
throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
let selectedModelDef = fallbackModel;
|
|
626
|
-
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
627
|
-
|
|
628
|
-
if (modelOverride && typeof modelOverride === 'string') {
|
|
629
|
-
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
630
|
-
if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
|
|
631
|
-
selectedModelDef = requested;
|
|
632
|
-
return {
|
|
633
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
634
|
-
model: selectedModelDef.id,
|
|
635
|
-
providerName: selectedModelDef.provider
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
641
|
-
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
642
|
-
} else {
|
|
643
|
-
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
644
|
-
? providerConfig.selectionHint
|
|
645
|
-
: {};
|
|
646
|
-
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
647
|
-
const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
|
|
648
|
-
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
649
|
-
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
|
|
650
|
-
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
651
|
-
? preferredPurpose
|
|
652
|
-
: '';
|
|
653
|
-
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
654
|
-
const chooseForPurpose = (purpose) => {
|
|
655
|
-
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
656
|
-
if (candidates.length === 0) return null;
|
|
657
|
-
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
658
|
-
return [...candidates].sort((left, right) => (
|
|
659
|
-
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
660
|
-
))[0];
|
|
661
|
-
}
|
|
662
|
-
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
663
|
-
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
|
|
664
|
-
}
|
|
665
|
-
return candidates[0];
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
if (smarterSelection && requestedPurpose) {
|
|
669
|
-
selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
670
|
-
} else if (smarterSelection && highAutonomy) {
|
|
671
|
-
selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
672
|
-
} else if (isSubagent) {
|
|
673
|
-
selectedModelDef = chooseForPurpose('fast') || fallbackModel;
|
|
674
|
-
} else {
|
|
675
|
-
selectedModelDef = chooseForPurpose('general') || fallbackModel;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return {
|
|
680
|
-
provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
|
|
681
|
-
model: selectedModelDef.id,
|
|
682
|
-
providerName: selectedModelDef.provider
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null, failureError = null) {
|
|
687
|
-
const { getSupportedModels } = require('./models');
|
|
688
|
-
const aiSettings = getAiSettings(userId, agentId);
|
|
689
|
-
const models = await getSupportedModels(userId, agentId);
|
|
690
|
-
const availableModels = models.filter((model) => model.available !== false);
|
|
691
|
-
const knownIds = new Set(availableModels.map((model) => model.id));
|
|
692
|
-
const enabledIds = Array.isArray(aiSettings.enabled_models)
|
|
693
|
-
? aiSettings.enabled_models.map((id) => String(id)).filter((id) => knownIds.has(id))
|
|
694
|
-
: [];
|
|
695
|
-
const pool = enabledIds.length > 0
|
|
696
|
-
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
697
|
-
: availableModels;
|
|
698
|
-
const currentModel = pool.find((model) => model.id === currentModelId)
|
|
699
|
-
|| availableModels.find((model) => model.id === currentModelId)
|
|
700
|
-
|| null;
|
|
701
|
-
|
|
702
|
-
// When the failure is a provider-level rate limit, the preferred fallback is
|
|
703
|
-
// likely on the same provider and will hit the same limit. Skip it and prefer
|
|
704
|
-
// a fallback from a different provider instead.
|
|
705
|
-
const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
|
|
706
|
-
|
|
707
|
-
if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
|
|
708
|
-
const preferred = pool.find((model) => model.id === preferredFallbackId)
|
|
709
|
-
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
710
|
-
if (preferred) return preferred.id;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
if (currentModel?.provider) {
|
|
714
|
-
const differentProvider = pool.find((model) => model.id !== currentModelId && model.provider !== currentModel.provider)
|
|
715
|
-
|| availableModels.find((model) => model.id !== currentModelId && model.provider !== currentModel.provider);
|
|
716
|
-
if (differentProvider) return differentProvider.id;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// If no different-provider model exists, still try the preferred fallback
|
|
720
|
-
// even on rate limits (it's better than nothing).
|
|
721
|
-
if (preferredFallbackId && preferredFallbackId !== currentModelId) {
|
|
722
|
-
const preferred = pool.find((model) => model.id === preferredFallbackId)
|
|
723
|
-
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
724
|
-
if (preferred) return preferred.id;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
const differentModel = pool.find((model) => model.id !== currentModelId)
|
|
728
|
-
|| availableModels.find((model) => model.id !== currentModelId);
|
|
729
|
-
return differentModel?.id || null;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function estimateTokenValue(value) {
|
|
733
|
-
if (!value) return 0;
|
|
734
|
-
if (typeof value === 'string') return Math.ceil(value.length / 4);
|
|
735
|
-
return Math.ceil(JSON.stringify(value).length / 4);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
class AgentEngine {
|
|
739
|
-
constructor(io, services = {}) {
|
|
740
|
-
this.io = io;
|
|
741
|
-
this.activeRuns = new Map();
|
|
742
|
-
this.subagents = new Map();
|
|
743
|
-
this.app = services.app || null;
|
|
744
|
-
this.browserController = services.browserController || null;
|
|
745
|
-
this.androidController = services.androidController || null;
|
|
746
|
-
this.runtimeManager = services.runtimeManager || null;
|
|
747
|
-
this.workspaceManager = services.workspaceManager || null;
|
|
748
|
-
this.messagingManager = services.messagingManager || null;
|
|
749
|
-
this.mcpManager = services.mcpManager || services.mcpClient || null;
|
|
750
|
-
this.skillRunner = services.skillRunner || null;
|
|
751
|
-
this.taskRuntime = services.taskRuntime || null;
|
|
752
|
-
this.memoryManager = services.memoryManager || null;
|
|
753
|
-
this.voiceRuntimeManager = services.voiceRuntimeManager || null;
|
|
754
|
-
this.messagingDeliveryRetry = services.messagingDeliveryRetry || {};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
async buildSystemPrompt(userId, context = {}) {
|
|
758
|
-
const { buildSystemPromptSections } = require('./systemPrompt');
|
|
759
|
-
const { MemoryManager } = require('../memory/manager');
|
|
760
|
-
const memoryManager = this.memoryManager || new MemoryManager();
|
|
761
|
-
const promptSections = await buildSystemPromptSections(userId, context, memoryManager);
|
|
762
|
-
const skillRunner = context.skillRunner || this.skillRunner || null;
|
|
763
|
-
const skillsPrompt = skillRunner?.getSkillsForPrompt?.({
|
|
764
|
-
maxTotalChars: 9000,
|
|
765
|
-
maxDescriptionChars: 180,
|
|
766
|
-
maxTriggerChars: 100,
|
|
767
|
-
}) || '';
|
|
768
|
-
return {
|
|
769
|
-
stable: [promptSections.stable, skillsPrompt].filter(Boolean).join('\n\n'),
|
|
770
|
-
dynamic: promptSections.dynamic,
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
async buildMemoryRecall({
|
|
775
|
-
memoryManager,
|
|
776
|
-
userId,
|
|
777
|
-
agentId,
|
|
778
|
-
query,
|
|
779
|
-
provider,
|
|
780
|
-
providerName,
|
|
781
|
-
model,
|
|
782
|
-
runId,
|
|
783
|
-
stepId = null,
|
|
784
|
-
options = {},
|
|
785
|
-
returnDetails = false,
|
|
786
|
-
}) {
|
|
787
|
-
const initial = await memoryManager.recallMemory(userId, query, 12, { agentId });
|
|
788
|
-
|
|
789
|
-
const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
|
|
790
|
-
if (pendingChunks.length) {
|
|
791
|
-
this.extractPendingChunks(pendingChunks, {
|
|
792
|
-
userId,
|
|
793
|
-
agentId,
|
|
794
|
-
provider,
|
|
795
|
-
providerName,
|
|
796
|
-
model,
|
|
797
|
-
memoryManager,
|
|
798
|
-
}).catch((err) => console.warn('[Memory] Background chunk extraction failed:', err.message));
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const decision = shouldEnhanceRetrieval(initial);
|
|
802
|
-
if (!decision.enhance) {
|
|
803
|
-
const message = await memoryManager.buildRecallMessage(userId, query, {
|
|
804
|
-
agentId,
|
|
805
|
-
recalled: initial.slice(0, 5),
|
|
806
|
-
});
|
|
807
|
-
return returnDetails
|
|
808
|
-
? { message, results: initial.slice(0, 12), enhanced: false, reason: decision.reason }
|
|
809
|
-
: message;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const stats = memoryManager.getMemoryStats?.(userId, { agentId })
|
|
813
|
-
|| { total: initial.length };
|
|
814
|
-
if (!Number(stats.total || 0)) {
|
|
815
|
-
return returnDetails
|
|
816
|
-
? { message: null, results: [], enhanced: false, reason: 'empty_memory' }
|
|
817
|
-
: null;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const startedAt = Date.now();
|
|
821
|
-
let plan = null;
|
|
822
|
-
let merged = initial;
|
|
823
|
-
let reranked = initial;
|
|
824
|
-
try {
|
|
825
|
-
const planned = await this.requestStructuredJson({
|
|
826
|
-
provider,
|
|
827
|
-
providerName,
|
|
828
|
-
model,
|
|
829
|
-
messages: [],
|
|
830
|
-
prompt: buildPlannerPrompt(query, initial, new Date().toISOString()),
|
|
831
|
-
maxTokens: 650,
|
|
832
|
-
normalize: (raw) => normalizeRetrievalPlan(raw, query),
|
|
833
|
-
fallback: normalizeRetrievalPlan({}, query),
|
|
834
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
835
|
-
telemetry: { runId, stepId, userId, agentId },
|
|
836
|
-
phase: 'memory_retrieval_plan',
|
|
837
|
-
});
|
|
838
|
-
plan = planned.value;
|
|
839
|
-
const resultSets = [initial];
|
|
840
|
-
for (const variant of plan.queryVariants) {
|
|
841
|
-
if (variant === query && initial.length) continue;
|
|
842
|
-
resultSets.push(await memoryManager.recallMemory(userId, variant, 20, {
|
|
843
|
-
agentId,
|
|
844
|
-
validAt: plan.validAt,
|
|
845
|
-
includeHistory: plan.temporalMode === 'historical',
|
|
846
|
-
}));
|
|
847
|
-
}
|
|
848
|
-
merged = mergeRetrievalResults(resultSets, 30);
|
|
849
|
-
if (merged.length > 1) {
|
|
850
|
-
const rerankResponse = await this.requestStructuredJson({
|
|
851
|
-
provider,
|
|
852
|
-
providerName,
|
|
853
|
-
model,
|
|
854
|
-
messages: [],
|
|
855
|
-
prompt: buildRerankerPrompt(query, plan, merged.slice(0, 24)),
|
|
856
|
-
maxTokens: 1200,
|
|
857
|
-
normalize: (raw) => normalizeRerankResult(raw, merged),
|
|
858
|
-
fallback: merged,
|
|
859
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
860
|
-
telemetry: { runId, stepId, userId, agentId },
|
|
861
|
-
phase: 'memory_retrieval_rerank',
|
|
862
|
-
});
|
|
863
|
-
reranked = rerankResponse.value;
|
|
864
|
-
} else {
|
|
865
|
-
reranked = merged;
|
|
866
|
-
}
|
|
867
|
-
} catch (error) {
|
|
868
|
-
console.warn('[Memory] Retrieval enhancement failed:', error.message);
|
|
869
|
-
plan = null;
|
|
870
|
-
merged = initial;
|
|
871
|
-
reranked = initial;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
memoryManager.recordRetrievalEnhancement?.(userId, {
|
|
875
|
-
query,
|
|
876
|
-
reason: decision.reason,
|
|
877
|
-
plan,
|
|
878
|
-
initialCount: initial.length,
|
|
879
|
-
mergedCount: merged.length,
|
|
880
|
-
resultIds: reranked.slice(0, 5).map((result) => result.id),
|
|
881
|
-
latencyMs: Date.now() - startedAt,
|
|
882
|
-
}, { agentId, runId });
|
|
883
|
-
|
|
884
|
-
const message = await memoryManager.buildRecallMessage(userId, query, {
|
|
885
|
-
agentId,
|
|
886
|
-
recalled: reranked.slice(0, 5),
|
|
887
|
-
});
|
|
888
|
-
return returnDetails
|
|
889
|
-
? {
|
|
890
|
-
message,
|
|
891
|
-
results: reranked.slice(0, 12),
|
|
892
|
-
enhanced: plan !== null,
|
|
893
|
-
reason: decision.reason,
|
|
894
|
-
plan,
|
|
895
|
-
}
|
|
896
|
-
: message;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
async extractPendingChunks(chunks, {
|
|
900
|
-
userId,
|
|
901
|
-
agentId,
|
|
902
|
-
provider,
|
|
903
|
-
providerName,
|
|
904
|
-
model,
|
|
905
|
-
memoryManager,
|
|
906
|
-
}) {
|
|
907
|
-
const ids = chunks.map((c) => c.id);
|
|
908
|
-
memoryManager.markChunksExtracted?.(ids, { success: true });
|
|
909
|
-
|
|
910
|
-
const consolidationSchema = JSON.stringify({
|
|
911
|
-
memory_candidates: [{
|
|
912
|
-
memory: 'Concise standalone fact.',
|
|
913
|
-
subject: 'Canonical entity or person.',
|
|
914
|
-
predicate: 'Normalized relationship or attribute.',
|
|
915
|
-
object: 'Current atomic value.',
|
|
916
|
-
relation: 'new | updates | extends | derives',
|
|
917
|
-
category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
|
|
918
|
-
confidence: 0.8,
|
|
919
|
-
importance: 5,
|
|
920
|
-
is_static: false,
|
|
921
|
-
valid_from: null,
|
|
922
|
-
valid_to: null,
|
|
923
|
-
forget_after: null,
|
|
924
|
-
evidence: 'Short source-grounded quote.',
|
|
925
|
-
}],
|
|
926
|
-
}, null, 2);
|
|
927
|
-
|
|
928
|
-
for (const chunk of chunks) {
|
|
929
|
-
try {
|
|
930
|
-
const result = await this.requestStructuredJson({
|
|
931
|
-
provider,
|
|
932
|
-
providerName,
|
|
933
|
-
model,
|
|
934
|
-
messages: [],
|
|
935
|
-
prompt: [
|
|
936
|
-
'Return JSON only. Extract durable memory facts from the document chunk below.',
|
|
937
|
-
buildMemoryConsolidationInstructions(new Date().toISOString()),
|
|
938
|
-
`Source type: ${chunk.sourceType || 'document'}`,
|
|
939
|
-
chunk.title ? `Document title: ${chunk.title}` : '',
|
|
940
|
-
`Content:\n${String(chunk.content || '').slice(0, 2400)}`,
|
|
941
|
-
`Schema:\n${consolidationSchema}`,
|
|
942
|
-
].filter(Boolean).join('\n\n'),
|
|
943
|
-
maxTokens: 800,
|
|
944
|
-
normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
|
|
945
|
-
fallback: [],
|
|
946
|
-
phase: 'document_extraction',
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
const candidates = Array.isArray(result.value) ? result.value : [];
|
|
950
|
-
if (candidates.length) {
|
|
951
|
-
await memoryManager.consolidateMemoryCandidates(userId, candidates, {
|
|
952
|
-
agentId,
|
|
953
|
-
metadata: {
|
|
954
|
-
trustLevel: 'external_source',
|
|
955
|
-
sourceChunkMemoryId: chunk.id,
|
|
956
|
-
},
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
} catch (err) {
|
|
960
|
-
memoryManager.markChunksExtracted?.([chunk.id], { success: false });
|
|
961
|
-
console.warn('[Memory] Document chunk extraction failed:', err.message);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
persistRunMetadata(runId, patch = {}) {
|
|
967
|
-
if (!runId || !patch || typeof patch !== 'object') return;
|
|
968
|
-
const existing = db.prepare('SELECT metadata_json FROM agent_runs WHERE id = ?').get(runId);
|
|
969
|
-
const current = parseMaybeJson(existing?.metadata_json, {}) || {};
|
|
970
|
-
const next = { ...current, ...patch };
|
|
971
|
-
db.prepare('UPDATE agent_runs SET metadata_json = ? WHERE id = ?')
|
|
972
|
-
.run(JSON.stringify(next), runId);
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
updateRunGoalContract(runId, patch = {}, options = {}) {
|
|
976
|
-
const runMeta = this.getRunMeta(runId);
|
|
977
|
-
if (!runMeta) return null;
|
|
978
|
-
runMeta.goalContract = mergeGoalContracts(runMeta.goalContract, patch);
|
|
979
|
-
if (options.persist !== false) {
|
|
980
|
-
this.persistRunMetadata(runId, {
|
|
981
|
-
goalContract: runMeta.goalContract,
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
return runMeta.goalContract;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
buildProgressLedgerSnapshot(runMeta) {
|
|
988
|
-
if (!runMeta?.progressLedger) return null;
|
|
989
|
-
return {
|
|
990
|
-
currentStep: runMeta.progressLedger.currentStep || null,
|
|
991
|
-
currentTool: runMeta.progressLedger.currentTool || null,
|
|
992
|
-
currentStepStartedAt: runMeta.progressLedger.currentStepStartedAt || null,
|
|
993
|
-
lastVerifiedProgressAt: runMeta.progressLedger.lastVerifiedProgressAt || null,
|
|
994
|
-
lastUserVisibleUpdateAt: runMeta.progressLedger.lastUserVisibleUpdateAt || null,
|
|
995
|
-
lastFinalDeliveryAt: runMeta.progressLedger.lastFinalDeliveryAt || null,
|
|
996
|
-
heartbeatCount: Number(runMeta.progressLedger.heartbeatCount || 0),
|
|
997
|
-
stallNotifiedAt: runMeta.progressLedger.stallNotifiedAt || null,
|
|
998
|
-
progressState: runMeta.progressLedger.progressState || 'active',
|
|
999
|
-
currentPhase: runMeta.progressLedger.currentPhase || 'idle',
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
persistProgressLedger(runId) {
|
|
1004
|
-
const runMeta = this.getRunMeta(runId);
|
|
1005
|
-
if (!runMeta?.progressLedger) return;
|
|
1006
|
-
this.persistRunMetadata(runId, {
|
|
1007
|
-
progressLedger: this.buildProgressLedgerSnapshot(runMeta),
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
updateRunProgress(runId, patch = {}, options = {}) {
|
|
1012
|
-
const runMeta = this.getRunMeta(runId);
|
|
1013
|
-
if (!runMeta) return null;
|
|
1014
|
-
if (!runMeta.progressLedger) {
|
|
1015
|
-
runMeta.progressLedger = buildInitialProgressLedger({
|
|
1016
|
-
startedAt: runMeta.startedAtIso || isoNow(),
|
|
1017
|
-
});
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const previousState = runMeta.progressLedger.progressState || 'active';
|
|
1021
|
-
runMeta.progressLedger = {
|
|
1022
|
-
...runMeta.progressLedger,
|
|
1023
|
-
...patch,
|
|
1024
|
-
};
|
|
1025
|
-
|
|
1026
|
-
if (options.verified === true) {
|
|
1027
|
-
runMeta.progressLedger.lastVerifiedProgressAt = options.timestamp || isoNow();
|
|
1028
|
-
runMeta.progressLedger.progressState = 'active';
|
|
1029
|
-
runMeta.progressLedger.stallNotifiedAt = null;
|
|
1030
|
-
this.recordRunEvent(runMeta.userId, runId, 'progress_verified', {
|
|
1031
|
-
phase: runMeta.progressLedger.currentPhase || 'idle',
|
|
1032
|
-
currentStep: runMeta.progressLedger.currentStep || null,
|
|
1033
|
-
currentTool: runMeta.progressLedger.currentTool || null,
|
|
1034
|
-
}, { agentId: runMeta.agentId, stepId: options.stepId || null });
|
|
1035
|
-
if (previousState === 'stalled') {
|
|
1036
|
-
this.recordRunEvent(runMeta.userId, runId, 'progress_resumed', {
|
|
1037
|
-
phase: runMeta.progressLedger.currentPhase || 'idle',
|
|
1038
|
-
currentStep: runMeta.progressLedger.currentStep || null,
|
|
1039
|
-
currentTool: runMeta.progressLedger.currentTool || null,
|
|
1040
|
-
}, { agentId: runMeta.agentId, stepId: options.stepId || null });
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (options.persist !== false) {
|
|
1045
|
-
this.persistProgressLedger(runId);
|
|
1046
|
-
}
|
|
1047
|
-
return runMeta.progressLedger;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
markRunVisibleProgress(runId, timestamp = isoNow()) {
|
|
1051
|
-
const runMeta = this.getRunMeta(runId);
|
|
1052
|
-
if (!runMeta) return null;
|
|
1053
|
-
const ledger = this.updateRunProgress(runId, {
|
|
1054
|
-
lastUserVisibleUpdateAt: timestamp,
|
|
1055
|
-
}, {
|
|
1056
|
-
persist: false,
|
|
1057
|
-
});
|
|
1058
|
-
this.persistProgressLedger(runId);
|
|
1059
|
-
return ledger;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
markRunFinalDelivery(runId, content = '', timestamp = isoNow()) {
|
|
1063
|
-
const runMeta = this.getRunMeta(runId);
|
|
1064
|
-
if (!runMeta) return null;
|
|
1065
|
-
runMeta.messagingSent = true;
|
|
1066
|
-
runMeta.finalDeliverySent = true;
|
|
1067
|
-
runMeta.lastSentMessage = String(content || '').trim() || runMeta.lastSentMessage || '';
|
|
1068
|
-
const ledger = this.updateRunProgress(runId, {
|
|
1069
|
-
lastUserVisibleUpdateAt: timestamp,
|
|
1070
|
-
lastFinalDeliveryAt: timestamp,
|
|
1071
|
-
progressState: 'complete',
|
|
1072
|
-
}, {
|
|
1073
|
-
persist: false,
|
|
1074
|
-
});
|
|
1075
|
-
this.persistProgressLedger(runId);
|
|
1076
|
-
return ledger;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
recordRunEvent(userId, runId, eventType, payload = {}, options = {}) {
|
|
1080
|
-
try {
|
|
1081
|
-
return recordRunEvent({
|
|
1082
|
-
runId,
|
|
1083
|
-
userId,
|
|
1084
|
-
agentId: options.agentId || null,
|
|
1085
|
-
eventType,
|
|
1086
|
-
requestId: options.requestId || null,
|
|
1087
|
-
stepId: options.stepId || null,
|
|
1088
|
-
payload,
|
|
1089
|
-
});
|
|
1090
|
-
} catch {
|
|
1091
|
-
return null;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
async persistDeliverableMemory(userId, runId, agentId, deliverableResult) {
|
|
1096
|
-
if (!this.memoryManager?.saveMemory || !deliverableResult?.summary) return;
|
|
1097
|
-
try {
|
|
1098
|
-
await this.memoryManager.saveMemory(
|
|
1099
|
-
userId,
|
|
1100
|
-
deliverableResult.summary,
|
|
1101
|
-
'tasks',
|
|
1102
|
-
deliverableResult.validation?.status === 'passed' ? 7 : 5,
|
|
1103
|
-
{
|
|
1104
|
-
agentId,
|
|
1105
|
-
sourceRef: {
|
|
1106
|
-
sourceType: 'deliverable_run',
|
|
1107
|
-
sourceId: runId,
|
|
1108
|
-
sourceLabel: deliverableResult.type || 'deliverable',
|
|
1109
|
-
},
|
|
1110
|
-
metadata: {
|
|
1111
|
-
deliverableType: deliverableResult.type,
|
|
1112
|
-
status: deliverableResult.status,
|
|
1113
|
-
artifactCount: Array.isArray(deliverableResult.artifacts)
|
|
1114
|
-
? deliverableResult.artifacts.length
|
|
1115
|
-
: 0,
|
|
1116
|
-
artifacts: Array.isArray(deliverableResult.artifacts)
|
|
1117
|
-
? deliverableResult.artifacts.slice(0, 6)
|
|
1118
|
-
: [],
|
|
1119
|
-
},
|
|
1120
|
-
},
|
|
1121
|
-
);
|
|
1122
|
-
} catch (error) {
|
|
1123
|
-
console.error('[Engine] Failed to persist deliverable memory:', error?.message || error);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
async publishInterimUpdate({
|
|
1128
|
-
userId,
|
|
1129
|
-
runId,
|
|
1130
|
-
agentId = null,
|
|
1131
|
-
triggerSource = 'web',
|
|
1132
|
-
conversationId = null,
|
|
1133
|
-
platform = null,
|
|
1134
|
-
chatId = null,
|
|
1135
|
-
content,
|
|
1136
|
-
kind,
|
|
1137
|
-
expectsReply = false,
|
|
1138
|
-
deferFollowUp = false,
|
|
1139
|
-
} = {}) {
|
|
1140
|
-
const runMeta = this.getRunMeta(runId);
|
|
1141
|
-
if (!runMeta || runMeta.aborted) {
|
|
1142
|
-
return { sent: false, skipped: true, reason: 'Run is no longer active.' };
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const normalizedKind = normalizeInterimKind(kind);
|
|
1146
|
-
const normalizedContent = normalizeInterimText(
|
|
1147
|
-
content,
|
|
1148
|
-
triggerSource === 'messaging' ? platform : null
|
|
1149
|
-
);
|
|
1150
|
-
if (!normalizedContent || normalizedContent.toUpperCase() === '[NO RESPONSE]') {
|
|
1151
|
-
return { sent: false, skipped: true, reason: 'Interim content must be non-empty.' };
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const signature = buildInterimSignature({
|
|
1155
|
-
content: normalizedContent,
|
|
1156
|
-
kind: normalizedKind,
|
|
1157
|
-
expectsReply,
|
|
1158
|
-
platform: triggerSource === 'messaging' ? platform : 'web',
|
|
1159
|
-
});
|
|
1160
|
-
if (runMeta.interimSignatures?.has(signature)) {
|
|
1161
|
-
return { sent: false, skipped: true, duplicate: true };
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const metadata = buildInterimMetadata({
|
|
1165
|
-
kind: normalizedKind,
|
|
1166
|
-
expectsReply,
|
|
1167
|
-
});
|
|
1168
|
-
if (deferFollowUp === true) {
|
|
1169
|
-
metadata.defer_follow_up = true;
|
|
1170
|
-
}
|
|
1171
|
-
const createdAt = new Date().toISOString();
|
|
1172
|
-
|
|
1173
|
-
if (triggerSource === 'messaging') {
|
|
1174
|
-
if (!platform || !chatId || !this.messagingManager) {
|
|
1175
|
-
return { sent: false, skipped: true, reason: 'Messaging context is not available.' };
|
|
1176
|
-
}
|
|
1177
|
-
const deliveryResult = await this.messagingManager.sendMessage(userId, platform, chatId, normalizedContent, {
|
|
1178
|
-
agentId,
|
|
1179
|
-
runId,
|
|
1180
|
-
persistConversation: true,
|
|
1181
|
-
metadata,
|
|
1182
|
-
deliveryKind: 'interim',
|
|
1183
|
-
});
|
|
1184
|
-
requireSuccessfulMessagingDelivery(deliveryResult, 'Interim messaging delivery');
|
|
1185
|
-
} else if (triggerSource === 'voice_live') {
|
|
1186
|
-
const voiceSessionId = runMeta.voiceSessionId || null;
|
|
1187
|
-
const manager = this.voiceRuntimeManager || this.app?.locals?.voiceRuntimeManager || null;
|
|
1188
|
-
if (!voiceSessionId || !manager || typeof manager.publishInterimUpdate !== 'function') {
|
|
1189
|
-
return { sent: false, skipped: true, reason: 'Voice session context is not available.' };
|
|
1190
|
-
}
|
|
1191
|
-
await manager.publishInterimUpdate({
|
|
1192
|
-
sessionId: voiceSessionId,
|
|
1193
|
-
content: normalizedContent,
|
|
1194
|
-
kind: normalizedKind,
|
|
1195
|
-
expectsReply,
|
|
1196
|
-
deferFollowUp,
|
|
1197
|
-
});
|
|
1198
|
-
} else {
|
|
1199
|
-
db.prepare(
|
|
1200
|
-
'INSERT INTO conversation_history (user_id, agent_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?, ?)'
|
|
1201
|
-
).run(userId, agentId, runId, 'assistant', normalizedContent, JSON.stringify(metadata));
|
|
1202
|
-
|
|
1203
|
-
if (conversationId) {
|
|
1204
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
1205
|
-
.run(conversationId, 'assistant', normalizedContent);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (!runMeta.interimSignatures) runMeta.interimSignatures = new Set();
|
|
1210
|
-
if (!Array.isArray(runMeta.interimMessages)) runMeta.interimMessages = [];
|
|
1211
|
-
runMeta.interimSignatures.add(signature);
|
|
1212
|
-
runMeta.interimMessages.push({
|
|
1213
|
-
content: normalizedContent,
|
|
1214
|
-
kind: normalizedKind,
|
|
1215
|
-
expectsReply: expectsReply === true,
|
|
1216
|
-
deferFollowUp: deferFollowUp === true,
|
|
1217
|
-
createdAt,
|
|
1218
|
-
});
|
|
1219
|
-
runMeta.lastInterimMessage = normalizedContent;
|
|
1220
|
-
this.markRunVisibleProgress(runId, createdAt);
|
|
1221
|
-
|
|
1222
|
-
this.emit(userId, 'run:assistant_interim', {
|
|
1223
|
-
runId,
|
|
1224
|
-
content: normalizedContent,
|
|
1225
|
-
kind: normalizedKind,
|
|
1226
|
-
expectsReply: expectsReply === true,
|
|
1227
|
-
deferFollowUp: deferFollowUp === true,
|
|
1228
|
-
triggerSource,
|
|
1229
|
-
platform: triggerSource === 'messaging' ? platform : 'web',
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
const terminalInterim = expectsReply === true;
|
|
1233
|
-
if (terminalInterim) {
|
|
1234
|
-
runMeta.terminalInterim = {
|
|
1235
|
-
kind: normalizedKind,
|
|
1236
|
-
content: normalizedContent,
|
|
1237
|
-
createdAt,
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
this.persistRunMetadata(runId, {
|
|
1241
|
-
latestInterim: {
|
|
1242
|
-
kind: normalizedKind,
|
|
1243
|
-
expectsReply: expectsReply === true,
|
|
1244
|
-
deferFollowUp: deferFollowUp === true,
|
|
1245
|
-
content: normalizedContent,
|
|
1246
|
-
createdAt,
|
|
1247
|
-
},
|
|
1248
|
-
progressLedger: this.buildProgressLedgerSnapshot(runMeta),
|
|
1249
|
-
terminalInterim: terminalInterim
|
|
1250
|
-
? { kind: normalizedKind, content: normalizedContent, createdAt }
|
|
1251
|
-
: null,
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
return {
|
|
1255
|
-
sent: true,
|
|
1256
|
-
kind: normalizedKind,
|
|
1257
|
-
expectsReply: expectsReply === true,
|
|
1258
|
-
deferFollowUp: deferFollowUp === true,
|
|
1259
|
-
content: normalizedContent,
|
|
1260
|
-
terminal: terminalInterim,
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
async requestStructuredJson({
|
|
1265
|
-
provider,
|
|
1266
|
-
providerName,
|
|
1267
|
-
model,
|
|
1268
|
-
messages,
|
|
1269
|
-
prompt,
|
|
1270
|
-
maxTokens = 1400,
|
|
1271
|
-
normalize,
|
|
1272
|
-
fallback = {},
|
|
1273
|
-
reasoningEffort,
|
|
1274
|
-
telemetry = null,
|
|
1275
|
-
phase = 'structured',
|
|
1276
|
-
}) {
|
|
1277
|
-
const startedAt = Date.now();
|
|
1278
|
-
const structuredStep = `model:${phase}`;
|
|
1279
|
-
if (telemetry?.runId) {
|
|
1280
|
-
this.updateRunProgress(telemetry.runId, {
|
|
1281
|
-
currentPhase: 'model',
|
|
1282
|
-
currentStep: structuredStep,
|
|
1283
|
-
currentTool: null,
|
|
1284
|
-
currentStepStartedAt: isoNow(),
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
let completed = false;
|
|
1289
|
-
try {
|
|
1290
|
-
const response = await withProviderRetry(
|
|
1291
|
-
() => withModelCallTimeout(
|
|
1292
|
-
provider.chat(
|
|
1293
|
-
sanitizeConversationMessages([
|
|
1294
|
-
...messages,
|
|
1295
|
-
{ role: 'system', content: prompt },
|
|
1296
|
-
]),
|
|
1297
|
-
[],
|
|
1298
|
-
{
|
|
1299
|
-
model,
|
|
1300
|
-
maxTokens,
|
|
1301
|
-
reasoningEffort: reasoningEffort || this.getReasoningEffort(providerName, {}),
|
|
1302
|
-
}
|
|
1303
|
-
),
|
|
1304
|
-
telemetry || {},
|
|
1305
|
-
`${phase} model call`,
|
|
1306
|
-
),
|
|
1307
|
-
{ label: `Engine ${model} (structured)` }
|
|
1308
|
-
);
|
|
1309
|
-
completed = true;
|
|
1310
|
-
if (telemetry?.runId && telemetry?.userId) {
|
|
1311
|
-
recordModelUsage({
|
|
1312
|
-
runId: telemetry.runId,
|
|
1313
|
-
stepId: telemetry.stepId || null,
|
|
1314
|
-
userId: telemetry.userId,
|
|
1315
|
-
agentId: telemetry.agentId || null,
|
|
1316
|
-
provider: providerName,
|
|
1317
|
-
model,
|
|
1318
|
-
phase,
|
|
1319
|
-
usage: response.usage,
|
|
1320
|
-
latencyMs: Date.now() - startedAt,
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
const parsed = parseJsonObject(response.content || '');
|
|
1325
|
-
const normalizedUsage = normalizeUsage(response.usage);
|
|
1326
|
-
return {
|
|
1327
|
-
value: normalize(parsed || {}, fallback),
|
|
1328
|
-
raw: response.content || '',
|
|
1329
|
-
usage: normalizedUsage?.totalTokens || 0,
|
|
1330
|
-
};
|
|
1331
|
-
} finally {
|
|
1332
|
-
const runMeta = telemetry?.runId ? this.getRunMeta(telemetry.runId) : null;
|
|
1333
|
-
if (runMeta?.progressLedger?.currentStep === structuredStep) {
|
|
1334
|
-
this.updateRunProgress(telemetry.runId, {
|
|
1335
|
-
currentPhase: 'idle',
|
|
1336
|
-
currentStep: null,
|
|
1337
|
-
currentTool: null,
|
|
1338
|
-
currentStepStartedAt: null,
|
|
1339
|
-
}, {
|
|
1340
|
-
verified: completed,
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
async requestModelResponse({
|
|
1347
|
-
provider,
|
|
1348
|
-
providerName,
|
|
1349
|
-
model,
|
|
1350
|
-
messages,
|
|
1351
|
-
tools,
|
|
1352
|
-
options,
|
|
1353
|
-
runId,
|
|
1354
|
-
iteration,
|
|
1355
|
-
}) {
|
|
1356
|
-
const startedAt = Date.now();
|
|
1357
|
-
const requestMessages = sanitizeConversationMessages(messages);
|
|
1358
|
-
const callOptions = {
|
|
1359
|
-
model,
|
|
1360
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1361
|
-
};
|
|
1362
|
-
|
|
1363
|
-
const attemptModelCall = async () => {
|
|
1364
|
-
let response = null;
|
|
1365
|
-
let streamContent = '';
|
|
1366
|
-
|
|
1367
|
-
if (options.stream !== false) {
|
|
1368
|
-
let emittedContent = false;
|
|
1369
|
-
const stream = provider.stream(requestMessages, tools, callOptions);
|
|
1370
|
-
const iterator = stream[Symbol.asyncIterator]();
|
|
1371
|
-
try {
|
|
1372
|
-
while (true) {
|
|
1373
|
-
const next = await withModelCallTimeout(
|
|
1374
|
-
iterator.next(),
|
|
1375
|
-
options,
|
|
1376
|
-
`Model stream iteration ${iteration}`,
|
|
1377
|
-
);
|
|
1378
|
-
if (next.done) break;
|
|
1379
|
-
const chunk = next.value;
|
|
1380
|
-
if (chunk.type === 'content') {
|
|
1381
|
-
emittedContent = true;
|
|
1382
|
-
streamContent += chunk.content;
|
|
1383
|
-
this.emit(options.userId, 'run:stream', {
|
|
1384
|
-
runId,
|
|
1385
|
-
content: sanitizeModelOutput(streamContent, { model }),
|
|
1386
|
-
iteration,
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
if (chunk.type === 'done') {
|
|
1390
|
-
response = chunk;
|
|
1391
|
-
}
|
|
1392
|
-
if (chunk.type === 'tool_calls') {
|
|
1393
|
-
response = {
|
|
1394
|
-
content: chunk.content || streamContent,
|
|
1395
|
-
toolCalls: chunk.toolCalls,
|
|
1396
|
-
providerContentBlocks: chunk.providerContentBlocks || null,
|
|
1397
|
-
finishReason: 'tool_calls',
|
|
1398
|
-
usage: chunk.usage || null,
|
|
1399
|
-
};
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
} catch (err) {
|
|
1403
|
-
Promise.resolve(iterator.return?.()).catch(() => {});
|
|
1404
|
-
// Once tokens have streamed to the client a retry would duplicate
|
|
1405
|
-
// output, so only the pre-stream window is safe to replay.
|
|
1406
|
-
if (emittedContent) err.__providerRetryUnsafe = true;
|
|
1407
|
-
throw err;
|
|
1408
|
-
}
|
|
1409
|
-
} else {
|
|
1410
|
-
response = await withModelCallTimeout(
|
|
1411
|
-
provider.chat(requestMessages, tools, callOptions),
|
|
1412
|
-
options,
|
|
1413
|
-
`Model iteration ${iteration}`,
|
|
1414
|
-
);
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
return { response, streamContent };
|
|
1418
|
-
};
|
|
1419
|
-
|
|
1420
|
-
const { response, streamContent } = await withProviderRetry(attemptModelCall, {
|
|
1421
|
-
...(options.retry || {}),
|
|
1422
|
-
label: `Engine ${model}`,
|
|
1423
|
-
isRetryable: (err) => !err?.__providerRetryUnsafe && isTransientError(err),
|
|
1424
|
-
onRetry: ({ attempt, delayMs }) => {
|
|
1425
|
-
this.emit(options.userId, 'run:interim', {
|
|
1426
|
-
runId,
|
|
1427
|
-
message: `Model service busy; retrying (attempt ${attempt}) in ${Math.max(1, Math.round(delayMs / 1000))}s.`,
|
|
1428
|
-
phase: 'recovering',
|
|
1429
|
-
});
|
|
1430
|
-
},
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
const resolvedResponse = response || {
|
|
1434
|
-
content: streamContent,
|
|
1435
|
-
toolCalls: [],
|
|
1436
|
-
finishReason: 'stop',
|
|
1437
|
-
usage: null,
|
|
1438
|
-
};
|
|
1439
|
-
const hasContent = Boolean(String(resolvedResponse.content || streamContent || '').trim());
|
|
1440
|
-
const hasToolCalls = Array.isArray(resolvedResponse.toolCalls) && resolvedResponse.toolCalls.length > 0;
|
|
1441
|
-
if (!hasContent && !hasToolCalls) {
|
|
1442
|
-
const error = new Error(`Model ${model} returned an empty response.`);
|
|
1443
|
-
error.code = 'MODEL_EMPTY_RESPONSE';
|
|
1444
|
-
throw error;
|
|
1445
|
-
}
|
|
1446
|
-
if (options.runId && options.userId) {
|
|
1447
|
-
recordModelUsage({
|
|
1448
|
-
runId: options.runId,
|
|
1449
|
-
stepId: options.stepId || null,
|
|
1450
|
-
userId: options.userId,
|
|
1451
|
-
agentId: options.agentId || null,
|
|
1452
|
-
provider: providerName,
|
|
1453
|
-
model,
|
|
1454
|
-
phase: options.phase || 'model_turn',
|
|
1455
|
-
usage: resolvedResponse.usage,
|
|
1456
|
-
latencyMs: Date.now() - startedAt,
|
|
1457
|
-
metadata: { iteration },
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
return {
|
|
1462
|
-
response: resolvedResponse,
|
|
1463
|
-
responseModel: model,
|
|
1464
|
-
streamContent,
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
async analyzeTask({
|
|
1469
|
-
provider,
|
|
1470
|
-
providerName,
|
|
1471
|
-
model,
|
|
1472
|
-
messages,
|
|
1473
|
-
tools,
|
|
1474
|
-
capabilityHealth,
|
|
1475
|
-
forceMode,
|
|
1476
|
-
userMessage,
|
|
1477
|
-
options,
|
|
1478
|
-
}) {
|
|
1479
|
-
const summary = summarizeCapabilityHealth(capabilityHealth);
|
|
1480
|
-
const response = await this.requestStructuredJson({
|
|
1481
|
-
provider,
|
|
1482
|
-
providerName,
|
|
1483
|
-
model,
|
|
1484
|
-
messages,
|
|
1485
|
-
prompt: buildAnalysisPrompt({
|
|
1486
|
-
capabilityHealth: summary,
|
|
1487
|
-
tools,
|
|
1488
|
-
forceMode,
|
|
1489
|
-
}),
|
|
1490
|
-
maxTokens: 1100,
|
|
1491
|
-
normalize: normalizeTaskAnalysis,
|
|
1492
|
-
fallback: buildAnalyzeTaskFallback(forceMode, userMessage),
|
|
1493
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1494
|
-
telemetry: options,
|
|
1495
|
-
phase: 'task_analysis',
|
|
1496
|
-
});
|
|
1497
|
-
|
|
1498
|
-
return {
|
|
1499
|
-
analysis: response.value,
|
|
1500
|
-
raw: response.raw,
|
|
1501
|
-
usage: response.usage,
|
|
1502
|
-
capabilitySummary: summary,
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
async createExecutionPlan({
|
|
1507
|
-
provider,
|
|
1508
|
-
providerName,
|
|
1509
|
-
model,
|
|
1510
|
-
messages,
|
|
1511
|
-
analysis,
|
|
1512
|
-
capabilitySummary,
|
|
1513
|
-
options,
|
|
1514
|
-
}) {
|
|
1515
|
-
const response = await this.requestStructuredJson({
|
|
1516
|
-
provider,
|
|
1517
|
-
providerName,
|
|
1518
|
-
model,
|
|
1519
|
-
messages,
|
|
1520
|
-
prompt: buildPlanPrompt(analysis, capabilitySummary),
|
|
1521
|
-
maxTokens: 1400,
|
|
1522
|
-
normalize: normalizeExecutionPlan,
|
|
1523
|
-
fallback: {
|
|
1524
|
-
success_criteria: analysis.success_criteria,
|
|
1525
|
-
},
|
|
1526
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1527
|
-
telemetry: options,
|
|
1528
|
-
phase: 'execution_plan',
|
|
1529
|
-
});
|
|
1530
|
-
|
|
1531
|
-
return {
|
|
1532
|
-
plan: response.value,
|
|
1533
|
-
raw: response.raw,
|
|
1534
|
-
usage: response.usage,
|
|
1535
|
-
};
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
async verifyFinalResponse({
|
|
1539
|
-
provider,
|
|
1540
|
-
providerName,
|
|
1541
|
-
model,
|
|
1542
|
-
messages,
|
|
1543
|
-
analysis,
|
|
1544
|
-
tools,
|
|
1545
|
-
toolExecutions,
|
|
1546
|
-
finalReply,
|
|
1547
|
-
options,
|
|
1548
|
-
}) {
|
|
1549
|
-
const evidenceSources = [...new Set(
|
|
1550
|
-
toolExecutions
|
|
1551
|
-
.map((item) => item.evidenceSource)
|
|
1552
|
-
.filter(Boolean)
|
|
1553
|
-
)];
|
|
1554
|
-
const response = await this.requestStructuredJson({
|
|
1555
|
-
provider,
|
|
1556
|
-
providerName,
|
|
1557
|
-
model,
|
|
1558
|
-
messages,
|
|
1559
|
-
prompt: buildVerifierPrompt({
|
|
1560
|
-
analysis,
|
|
1561
|
-
tools,
|
|
1562
|
-
toolExecutionSummary: summarizeToolExecutions(toolExecutions),
|
|
1563
|
-
evidenceSources,
|
|
1564
|
-
finalReply,
|
|
1565
|
-
}),
|
|
1566
|
-
maxTokens: 1200,
|
|
1567
|
-
normalize: (raw) => normalizeVerificationResult(raw, finalReply),
|
|
1568
|
-
fallback: {
|
|
1569
|
-
status: analysis.freshness_risk === 'none' ? 'verified' : 'insufficient_evidence',
|
|
1570
|
-
final_reply: finalReply,
|
|
1571
|
-
},
|
|
1572
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1573
|
-
telemetry: options,
|
|
1574
|
-
phase: 'verification',
|
|
1575
|
-
});
|
|
1576
|
-
|
|
1577
|
-
return {
|
|
1578
|
-
verification: response.value,
|
|
1579
|
-
raw: response.raw,
|
|
1580
|
-
usage: response.usage,
|
|
1581
|
-
evidenceSources,
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
async refreshConversationState({
|
|
1586
|
-
conversationId,
|
|
1587
|
-
runId,
|
|
1588
|
-
provider,
|
|
1589
|
-
providerName,
|
|
1590
|
-
model,
|
|
1591
|
-
finalReply,
|
|
1592
|
-
analysis,
|
|
1593
|
-
verification,
|
|
1594
|
-
historyWindow,
|
|
1595
|
-
options,
|
|
1596
|
-
}) {
|
|
1597
|
-
if (!conversationId) return null;
|
|
1598
|
-
const { MemoryManager } = require('../memory/manager');
|
|
1599
|
-
const memoryManager = this.memoryManager || new MemoryManager();
|
|
1600
|
-
const context = getConversationContext(conversationId, Math.max(historyWindow, 8));
|
|
1601
|
-
const existingState = memoryManager.getConversationState(conversationId);
|
|
1602
|
-
const promptMessages = [
|
|
1603
|
-
{
|
|
1604
|
-
role: 'system',
|
|
1605
|
-
content: [
|
|
1606
|
-
'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
|
|
1607
|
-
'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
|
|
1608
|
-
buildMemoryConsolidationInstructions(new Date().toISOString()),
|
|
1609
|
-
'Schema:',
|
|
1610
|
-
JSON.stringify({
|
|
1611
|
-
summary: '',
|
|
1612
|
-
open_commitments: [],
|
|
1613
|
-
unresolved_questions: [],
|
|
1614
|
-
referenced_entities: [],
|
|
1615
|
-
last_verified_facts: [],
|
|
1616
|
-
memory_candidates: [{
|
|
1617
|
-
memory: 'Concise standalone fact for future context.',
|
|
1618
|
-
subject: 'Canonical entity or person.',
|
|
1619
|
-
predicate: 'Normalized relationship or attribute.',
|
|
1620
|
-
object: 'Current atomic value.',
|
|
1621
|
-
relation: 'new | updates | extends | derives',
|
|
1622
|
-
category: 'identity | preferences | projects | contacts | events | tasks | episodic | assistant_self',
|
|
1623
|
-
confidence: 0.9,
|
|
1624
|
-
importance: 7,
|
|
1625
|
-
is_static: false,
|
|
1626
|
-
valid_from: null,
|
|
1627
|
-
valid_to: null,
|
|
1628
|
-
forget_after: null,
|
|
1629
|
-
evidence: 'Short source-grounded evidence.',
|
|
1630
|
-
}],
|
|
1631
|
-
}, null, 2),
|
|
1632
|
-
].join('\n\n')
|
|
1633
|
-
},
|
|
1634
|
-
{
|
|
1635
|
-
role: 'user',
|
|
1636
|
-
content: [
|
|
1637
|
-
existingState?.summary ? `Existing state:\n${JSON.stringify(existingState, null, 2)}` : 'Existing state: none',
|
|
1638
|
-
context.summary ? `Conversation summary:\n${context.summary}` : 'Conversation summary: none',
|
|
1639
|
-
`Recent thread messages:\n${JSON.stringify(context.recentMessages.slice(-8), null, 2)}`,
|
|
1640
|
-
`Latest final reply:\n${finalReply || '(empty)'}`,
|
|
1641
|
-
verification?.status ? `Verification status: ${verification.status}` : '',
|
|
1642
|
-
verification?.final_reply && verification.final_reply !== finalReply ? `Verified reply:\n${verification.final_reply}` : '',
|
|
1643
|
-
analysis?.goal ? `Thread goal: ${analysis.goal}` : '',
|
|
1644
|
-
].filter(Boolean).join('\n\n')
|
|
1645
|
-
}
|
|
1646
|
-
];
|
|
1647
|
-
|
|
1648
|
-
const response = await withModelCallTimeout(
|
|
1649
|
-
provider.chat(promptMessages, [], {
|
|
1650
|
-
model,
|
|
1651
|
-
maxTokens: 800,
|
|
1652
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
1653
|
-
}),
|
|
1654
|
-
options,
|
|
1655
|
-
'Conversation state refresh',
|
|
1656
|
-
);
|
|
1657
|
-
const parsed = parseJsonObject(response.content || '') || {};
|
|
1658
|
-
const nextState = {
|
|
1659
|
-
summary: String(parsed.summary || existingState?.summary || '').trim(),
|
|
1660
|
-
open_commitments: Array.isArray(parsed.open_commitments) ? parsed.open_commitments.slice(0, 8).map((item) => String(item || '').trim()).filter(Boolean) : [],
|
|
1661
|
-
unresolved_questions: Array.isArray(parsed.unresolved_questions) ? parsed.unresolved_questions.slice(0, 8).map((item) => String(item || '').trim()).filter(Boolean) : [],
|
|
1662
|
-
referenced_entities: Array.isArray(parsed.referenced_entities) ? parsed.referenced_entities.slice(0, 12).map((item) => String(item || '').trim()).filter(Boolean) : [],
|
|
1663
|
-
last_verified_facts: Array.isArray(parsed.last_verified_facts) ? parsed.last_verified_facts.slice(0, 10).map((item) => String(item || '').trim()).filter(Boolean) : [],
|
|
1664
|
-
};
|
|
1665
|
-
|
|
1666
|
-
if (verification?.status === 'verified' && String(finalReply || '').trim()) {
|
|
1667
|
-
nextState.last_verified_facts = [...new Set([
|
|
1668
|
-
...nextState.last_verified_facts,
|
|
1669
|
-
clampRunContext(verification.final_reply || finalReply, 280),
|
|
1670
|
-
])].slice(-10);
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
memoryManager.updateConversationState(conversationId, nextState);
|
|
1674
|
-
const memoryCandidates = normalizeMemoryCandidates(parsed.memory_candidates);
|
|
1675
|
-
if (memoryCandidates.length) {
|
|
1676
|
-
await memoryManager.consolidateMemoryCandidates(
|
|
1677
|
-
options.userId,
|
|
1678
|
-
memoryCandidates,
|
|
1679
|
-
{
|
|
1680
|
-
agentId: options.agentId || null,
|
|
1681
|
-
conversationId,
|
|
1682
|
-
runId,
|
|
1683
|
-
},
|
|
1684
|
-
);
|
|
1685
|
-
const { invalidateSystemPromptCache } = require('./systemPrompt');
|
|
1686
|
-
invalidateSystemPromptCache(options.userId, options.agentId || null);
|
|
1687
|
-
}
|
|
1688
|
-
return nextState;
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
getAvailableTools(app, options = {}) {
|
|
1692
|
-
const { getAvailableTools } = require('./tools');
|
|
1693
|
-
return getAvailableTools(app, options);
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
async executeTool(toolName, args, context) {
|
|
1697
|
-
const { executeTool } = require('./tools');
|
|
1698
|
-
return executeTool(toolName, args, context, this);
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
isReadOnlyToolCall(toolCall) {
|
|
1702
|
-
const name = String(toolCall?.function?.name || '');
|
|
1703
|
-
const readOnly = new Set([
|
|
1704
|
-
'read_file',
|
|
1705
|
-
'list_directory',
|
|
1706
|
-
'search_files',
|
|
1707
|
-
'code_navigate',
|
|
1708
|
-
'query_structured_data',
|
|
1709
|
-
'memory_recall',
|
|
1710
|
-
'memory_read',
|
|
1711
|
-
'session_search',
|
|
1712
|
-
'web_search',
|
|
1713
|
-
'list_tasks',
|
|
1714
|
-
'list_skills',
|
|
1715
|
-
'list_subagents',
|
|
1716
|
-
'recordings_list',
|
|
1717
|
-
'recordings_get',
|
|
1718
|
-
'recordings_search',
|
|
1719
|
-
'read_health_data',
|
|
1720
|
-
]);
|
|
1721
|
-
if (name === 'http_request') {
|
|
1722
|
-
try {
|
|
1723
|
-
const args = JSON.parse(toolCall.function.arguments || '{}');
|
|
1724
|
-
return String(args.method || 'GET').toUpperCase() === 'GET';
|
|
1725
|
-
} catch {
|
|
1726
|
-
return false;
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
return readOnly.has(name);
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
async executeReadOnlyBatch(toolCalls, context = {}) {
|
|
1733
|
-
const {
|
|
1734
|
-
userId,
|
|
1735
|
-
runId,
|
|
1736
|
-
agentId,
|
|
1737
|
-
app,
|
|
1738
|
-
triggerType,
|
|
1739
|
-
triggerSource,
|
|
1740
|
-
conversationId,
|
|
1741
|
-
startingStepIndex,
|
|
1742
|
-
options = {},
|
|
1743
|
-
} = context;
|
|
1744
|
-
const prepared = [];
|
|
1745
|
-
let nextStepIndex = startingStepIndex;
|
|
1746
|
-
for (const toolCall of toolCalls) {
|
|
1747
|
-
nextStepIndex += 1;
|
|
1748
|
-
let toolArgs = {};
|
|
1749
|
-
try { toolArgs = JSON.parse(toolCall.function.arguments || '{}'); } catch {}
|
|
1750
|
-
const toolName = toolCall.function.name;
|
|
1751
|
-
const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
|
|
1752
|
-
if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
|
|
1753
|
-
const result = {
|
|
1754
|
-
status: 'blocked',
|
|
1755
|
-
reason: 'The same read-only call already returned an unchanged result twice.',
|
|
1756
|
-
};
|
|
1757
|
-
prepared.push({
|
|
1758
|
-
toolCall,
|
|
1759
|
-
toolName,
|
|
1760
|
-
toolArgs,
|
|
1761
|
-
stepIndex: nextStepIndex,
|
|
1762
|
-
blocked: true,
|
|
1763
|
-
result,
|
|
1764
|
-
error: result.reason,
|
|
1765
|
-
});
|
|
1766
|
-
this.recordRunEvent(userId, runId, 'repetition_blocked', {
|
|
1767
|
-
toolName,
|
|
1768
|
-
toolArgs,
|
|
1769
|
-
parallel: true,
|
|
1770
|
-
}, { agentId });
|
|
1771
|
-
continue;
|
|
1772
|
-
}
|
|
1773
|
-
if (globalHooks.has('before_tool_call')) {
|
|
1774
|
-
const hookResult = await globalHooks.run('before_tool_call', {
|
|
1775
|
-
toolName,
|
|
1776
|
-
toolArgs,
|
|
1777
|
-
runId,
|
|
1778
|
-
userId,
|
|
1779
|
-
agentId,
|
|
1780
|
-
iteration: context.iteration,
|
|
1781
|
-
});
|
|
1782
|
-
if (hookResult.block) {
|
|
1783
|
-
prepared.push({
|
|
1784
|
-
toolCall,
|
|
1785
|
-
toolName,
|
|
1786
|
-
toolArgs,
|
|
1787
|
-
stepIndex: nextStepIndex,
|
|
1788
|
-
blocked: true,
|
|
1789
|
-
result: { status: 'blocked', reason: hookResult.reason || 'Blocked by policy.', blocked_by: hookResult.blocked_by || 'policy' },
|
|
1790
|
-
});
|
|
1791
|
-
continue;
|
|
1792
|
-
}
|
|
1793
|
-
if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
|
|
1794
|
-
}
|
|
1795
|
-
const stepId = uuidv4();
|
|
1796
|
-
db.prepare(
|
|
1797
|
-
`INSERT INTO agent_steps (
|
|
1798
|
-
id, run_id, step_index, type, description, status, tool_name, tool_input, started_at
|
|
1799
|
-
) VALUES (?, ?, ?, ?, ?, 'running', ?, ?, datetime('now'))`
|
|
1800
|
-
).run(
|
|
1801
|
-
stepId,
|
|
1802
|
-
runId,
|
|
1803
|
-
nextStepIndex,
|
|
1804
|
-
this.getStepType(toolName),
|
|
1805
|
-
`${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)}`,
|
|
1806
|
-
toolName,
|
|
1807
|
-
JSON.stringify(toolArgs),
|
|
1808
|
-
);
|
|
1809
|
-
this.emit(userId, 'run:tool_start', {
|
|
1810
|
-
runId,
|
|
1811
|
-
stepId,
|
|
1812
|
-
stepIndex: nextStepIndex,
|
|
1813
|
-
toolName,
|
|
1814
|
-
toolArgs,
|
|
1815
|
-
type: this.getStepType(toolName),
|
|
1816
|
-
});
|
|
1817
|
-
this.recordRunEvent(userId, runId, 'tool_started', {
|
|
1818
|
-
stepIndex: nextStepIndex,
|
|
1819
|
-
toolName,
|
|
1820
|
-
toolArgs,
|
|
1821
|
-
type: this.getStepType(toolName),
|
|
1822
|
-
parallel: true,
|
|
1823
|
-
}, { agentId, stepId });
|
|
1824
|
-
prepared.push({ toolCall, toolName, toolArgs, stepId, stepIndex: nextStepIndex });
|
|
1825
|
-
}
|
|
1826
|
-
this.recordRunEvent(userId, runId, 'parallel_batch_started', {
|
|
1827
|
-
toolNames: prepared.map((item) => item.toolName),
|
|
1828
|
-
count: prepared.length,
|
|
1829
|
-
}, { agentId });
|
|
1830
|
-
const results = await Promise.all(prepared.map(async (item) => {
|
|
1831
|
-
if (item.blocked) return item;
|
|
1832
|
-
const startedAt = Date.now();
|
|
1833
|
-
try {
|
|
1834
|
-
const result = await this.executeTool(item.toolName, item.toolArgs, {
|
|
1835
|
-
userId,
|
|
1836
|
-
runId,
|
|
1837
|
-
agentId,
|
|
1838
|
-
app,
|
|
1839
|
-
triggerType,
|
|
1840
|
-
triggerSource,
|
|
1841
|
-
conversationId,
|
|
1842
|
-
source: options.source || null,
|
|
1843
|
-
chatId: options.chatId || null,
|
|
1844
|
-
taskId: options.taskId || null,
|
|
1845
|
-
widgetId: options.widgetId || null,
|
|
1846
|
-
deliveryState: options.deliveryState || null,
|
|
1847
|
-
allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
|
|
1848
|
-
allowExternalSideEffects: false,
|
|
1849
|
-
});
|
|
1850
|
-
const error = inferToolFailureMessage(item.toolName, result);
|
|
1851
|
-
const status = error ? 'failed' : 'completed';
|
|
1852
|
-
db.prepare(
|
|
1853
|
-
`UPDATE agent_steps
|
|
1854
|
-
SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime('now')
|
|
1855
|
-
WHERE id = ?`
|
|
1856
|
-
).run(
|
|
1857
|
-
status,
|
|
1858
|
-
JSON.stringify(result).slice(0, 20000),
|
|
1859
|
-
error || null,
|
|
1860
|
-
result?.screenshotPath || null,
|
|
1861
|
-
item.stepId,
|
|
1862
|
-
);
|
|
1863
|
-
this.emit(userId, 'run:tool_end', {
|
|
1864
|
-
runId,
|
|
1865
|
-
stepId: item.stepId,
|
|
1866
|
-
toolName: item.toolName,
|
|
1867
|
-
result,
|
|
1868
|
-
error: error || undefined,
|
|
1869
|
-
status,
|
|
1870
|
-
});
|
|
1871
|
-
this.recordRunEvent(userId, runId, error ? 'tool_failed' : 'tool_completed', {
|
|
1872
|
-
toolName: item.toolName,
|
|
1873
|
-
status,
|
|
1874
|
-
durationMs: Date.now() - startedAt,
|
|
1875
|
-
resultPreview: summarizeForLog(result),
|
|
1876
|
-
parallel: true,
|
|
1877
|
-
}, { agentId, stepId: item.stepId });
|
|
1878
|
-
return { ...item, result, error };
|
|
1879
|
-
} catch (err) {
|
|
1880
|
-
db.prepare(
|
|
1881
|
-
`UPDATE agent_steps SET status = 'failed', error = ?, completed_at = datetime('now') WHERE id = ?`
|
|
1882
|
-
).run(err.message, item.stepId);
|
|
1883
|
-
this.emit(userId, 'run:tool_end', {
|
|
1884
|
-
runId,
|
|
1885
|
-
stepId: item.stepId,
|
|
1886
|
-
toolName: item.toolName,
|
|
1887
|
-
error: err.message,
|
|
1888
|
-
status: 'failed',
|
|
1889
|
-
});
|
|
1890
|
-
this.recordRunEvent(userId, runId, 'tool_failed', {
|
|
1891
|
-
toolName: item.toolName,
|
|
1892
|
-
status: 'failed',
|
|
1893
|
-
error: err.message,
|
|
1894
|
-
durationMs: Date.now() - startedAt,
|
|
1895
|
-
parallel: true,
|
|
1896
|
-
}, { agentId, stepId: item.stepId });
|
|
1897
|
-
return { ...item, result: { error: err.message }, error: err.message };
|
|
1898
|
-
}
|
|
1899
|
-
}));
|
|
1900
|
-
this.recordRunEvent(userId, runId, 'parallel_batch_completed', {
|
|
1901
|
-
toolNames: results.map((item) => item.toolName),
|
|
1902
|
-
failedCount: results.filter((item) => item.error).length,
|
|
1903
|
-
}, { agentId });
|
|
1904
|
-
return { results, endingStepIndex: nextStepIndex };
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
async persistRunContext(userId, {
|
|
1908
|
-
triggerSource,
|
|
1909
|
-
runTitle,
|
|
1910
|
-
userMessage,
|
|
1911
|
-
lastContent,
|
|
1912
|
-
stepIndex,
|
|
1913
|
-
skipPersistence = false
|
|
1914
|
-
}) {
|
|
1915
|
-
if (skipPersistence) {
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
|
-
void userId;
|
|
1919
|
-
void triggerSource;
|
|
1920
|
-
void runTitle;
|
|
1921
|
-
void userMessage;
|
|
1922
|
-
void lastContent;
|
|
1923
|
-
void stepIndex;
|
|
1924
|
-
// Run receipts belong in agent_runs/session history, not long-term memory.
|
|
1925
|
-
// Long-term memory should only contain durable facts or explicitly saved context.
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
getRunMeta(runId) {
|
|
1930
|
-
return this.activeRuns.get(runId) || null;
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
initializeToolRuntime(runId, allTools, initialTools, options = {}) {
|
|
1934
|
-
const runMeta = this.getRunMeta(runId);
|
|
1935
|
-
if (!runMeta) return;
|
|
1936
|
-
runMeta.toolCatalog = Array.isArray(allTools) ? allTools : [];
|
|
1937
|
-
runMeta.activeTools = Array.isArray(initialTools) ? initialTools : [];
|
|
1938
|
-
runMeta.toolSelectionOptions = {
|
|
1939
|
-
widgetId: options.widgetId || null,
|
|
1940
|
-
};
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
getActiveTools(runId) {
|
|
1944
|
-
return this.getRunMeta(runId)?.activeTools || [];
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
activateToolsForRun(runId, names = []) {
|
|
1948
|
-
const runMeta = this.getRunMeta(runId);
|
|
1949
|
-
if (!runMeta) throw new Error('Run is not active.');
|
|
1950
|
-
const result = activateTools(
|
|
1951
|
-
runMeta.activeTools,
|
|
1952
|
-
runMeta.toolCatalog,
|
|
1953
|
-
names,
|
|
1954
|
-
runMeta.toolSelectionOptions,
|
|
1955
|
-
);
|
|
1956
|
-
runMeta.activeTools = result.tools;
|
|
1957
|
-
this.recordRunEvent(runMeta.userId, runId, 'tools_activated', {
|
|
1958
|
-
activated: result.activated,
|
|
1959
|
-
evicted: result.evicted,
|
|
1960
|
-
unknown: result.unknown,
|
|
1961
|
-
notActivated: result.notActivated,
|
|
1962
|
-
activeToolNames: result.tools.map((tool) => tool.name),
|
|
1963
|
-
}, { agentId: runMeta.agentId });
|
|
1964
|
-
return {
|
|
1965
|
-
success: result.unknown.length === 0 && result.notActivated.length === 0,
|
|
1966
|
-
activated: result.activated,
|
|
1967
|
-
evicted: result.evicted,
|
|
1968
|
-
unknown: result.unknown,
|
|
1969
|
-
not_activated: result.notActivated,
|
|
1970
|
-
active_tools: result.tools.map((tool) => tool.name),
|
|
1971
|
-
};
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
findActiveRunForUser(userId, predicate = null) {
|
|
1975
|
-
let candidate = null;
|
|
1976
|
-
for (const [runId, runMeta] of this.activeRuns.entries()) {
|
|
1977
|
-
if (runMeta.userId !== userId || runMeta.aborted) continue;
|
|
1978
|
-
if (typeof predicate === 'function' && !predicate(runMeta, runId)) continue;
|
|
1979
|
-
if (!candidate || (runMeta.startedAt || 0) >= (candidate.startedAt || 0)) {
|
|
1980
|
-
candidate = { runId, ...runMeta };
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
return candidate;
|
|
1984
|
-
}
|
|
1985
|
-
|
|
1986
|
-
findSteerableRunForUser(userId, triggerSource = 'web') {
|
|
1987
|
-
return this.findActiveRunForUser(
|
|
1988
|
-
userId,
|
|
1989
|
-
(runMeta) => runMeta.triggerSource === triggerSource && runMeta.triggerType === 'user'
|
|
1990
|
-
);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
enqueueSteering(runId, content, metadata = {}) {
|
|
1994
|
-
const runMeta = this.getRunMeta(runId);
|
|
1995
|
-
const trimmed = typeof content === 'string' ? content.trim() : '';
|
|
1996
|
-
if (!runMeta || runMeta.aborted || !trimmed) return null;
|
|
1997
|
-
|
|
1998
|
-
const item = {
|
|
1999
|
-
id: uuidv4(),
|
|
2000
|
-
content: trimmed,
|
|
2001
|
-
metadata,
|
|
2002
|
-
createdAt: new Date().toISOString()
|
|
2003
|
-
};
|
|
2004
|
-
|
|
2005
|
-
runMeta.steeringQueue.push(item);
|
|
2006
|
-
this.emit(runMeta.userId, 'run:steer_queued', {
|
|
2007
|
-
runId,
|
|
2008
|
-
content: item.content,
|
|
2009
|
-
pendingCount: runMeta.steeringQueue.length
|
|
2010
|
-
});
|
|
2011
|
-
|
|
2012
|
-
return {
|
|
2013
|
-
runId,
|
|
2014
|
-
pendingCount: runMeta.steeringQueue.length,
|
|
2015
|
-
item
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
enqueueSystemSteering(runId, content, metadata = {}) {
|
|
2020
|
-
const runMeta = this.getRunMeta(runId);
|
|
2021
|
-
const trimmed = typeof content === 'string' ? content.trim() : '';
|
|
2022
|
-
if (!runMeta || runMeta.aborted || !trimmed) return null;
|
|
2023
|
-
if (!Array.isArray(runMeta.systemSteeringQueue)) {
|
|
2024
|
-
runMeta.systemSteeringQueue = [];
|
|
2025
|
-
}
|
|
2026
|
-
const signature = JSON.stringify({
|
|
2027
|
-
content: trimmed,
|
|
2028
|
-
reason: metadata.reason || '',
|
|
2029
|
-
});
|
|
2030
|
-
if (runMeta.systemSteeringQueue.some((item) => item.signature === signature)) {
|
|
2031
|
-
return null;
|
|
2032
|
-
}
|
|
2033
|
-
const item = {
|
|
2034
|
-
id: uuidv4(),
|
|
2035
|
-
content: trimmed,
|
|
2036
|
-
metadata,
|
|
2037
|
-
signature,
|
|
2038
|
-
createdAt: isoNow(),
|
|
2039
|
-
};
|
|
2040
|
-
runMeta.systemSteeringQueue.push(item);
|
|
2041
|
-
return item;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
applyQueuedSystemSteering(runId, messages) {
|
|
2045
|
-
const runMeta = this.getRunMeta(runId);
|
|
2046
|
-
if (!runMeta?.systemSteeringQueue?.length) {
|
|
2047
|
-
return { messages, appliedCount: 0 };
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
const queued = runMeta.systemSteeringQueue.splice(0, runMeta.systemSteeringQueue.length);
|
|
2051
|
-
for (const entry of queued) {
|
|
2052
|
-
messages.push({ role: 'system', content: entry.content });
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
return { messages, appliedCount: queued.length };
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
applyQueuedSteering(runId, messages, { userId, conversationId }) {
|
|
2059
|
-
const runMeta = this.getRunMeta(runId);
|
|
2060
|
-
if (!runMeta?.steeringQueue?.length) {
|
|
2061
|
-
return { messages, appliedCount: 0 };
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
const queued = runMeta.steeringQueue.splice(0, runMeta.steeringQueue.length);
|
|
2065
|
-
messages.push({
|
|
2066
|
-
role: 'system',
|
|
2067
|
-
content: [
|
|
2068
|
-
'The user sent follow-up messages while you were already working.',
|
|
2069
|
-
'Treat them as steering or next-up context for the same conversation.',
|
|
2070
|
-
'If a message materially changes the active task, incorporate it now.',
|
|
2071
|
-
'If it is unrelated or better handled after the current task, finish the current work first and then address it.'
|
|
2072
|
-
].join(' ')
|
|
2073
|
-
});
|
|
2074
|
-
|
|
2075
|
-
for (const entry of queued) {
|
|
2076
|
-
messages.push({ role: 'user', content: entry.content });
|
|
2077
|
-
if (conversationId) {
|
|
2078
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
2079
|
-
.run(conversationId, 'user', entry.content);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
this.emit(userId, 'run:steer_applied', {
|
|
2084
|
-
runId,
|
|
2085
|
-
count: queued.length,
|
|
2086
|
-
pendingCount: runMeta.steeringQueue.length,
|
|
2087
|
-
latestContent: queued[queued.length - 1]?.content || ''
|
|
2088
|
-
});
|
|
2089
|
-
|
|
2090
|
-
return { messages, appliedCount: queued.length };
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
buildMessagingHeartbeatText(runMeta, options = {}) {
|
|
2094
|
-
const stalled = options.stalled === true;
|
|
2095
|
-
const now = Date.now();
|
|
2096
|
-
const runStartedAtMs = Number.isFinite(runMeta?.startedAt) ? runMeta.startedAt : now;
|
|
2097
|
-
const stepStartedAtMs = timestampMs(
|
|
2098
|
-
runMeta?.progressLedger?.currentStepStartedAt,
|
|
2099
|
-
0,
|
|
2100
|
-
);
|
|
2101
|
-
const runElapsed = formatElapsedDuration(now - runStartedAtMs);
|
|
2102
|
-
const stepElapsed = formatElapsedDuration(now - (stepStartedAtMs || runStartedAtMs));
|
|
2103
|
-
const unverifiedElapsed = formatElapsedDuration(now - timestampMs(
|
|
2104
|
-
runMeta?.progressLedger?.lastVerifiedProgressAt,
|
|
2105
|
-
runStartedAtMs,
|
|
2106
|
-
));
|
|
2107
|
-
const currentTool = String(runMeta?.progressLedger?.currentTool || '').trim();
|
|
2108
|
-
const runTitle = String(runMeta?.title || '').trim().slice(0, 60);
|
|
2109
|
-
const titlePrefix = runTitle ? `[${runTitle}] ` : '';
|
|
2110
|
-
if (currentTool) {
|
|
2111
|
-
return stalled
|
|
2112
|
-
? `${titlePrefix}Still working on ${currentTool}. Run active ${runElapsed}; no verified progress for ${unverifiedElapsed}.`
|
|
2113
|
-
: `${titlePrefix}Still working on ${currentTool}. Run active ${runElapsed}; current step ${stepElapsed} so far.`;
|
|
2114
|
-
}
|
|
2115
|
-
return stalled
|
|
2116
|
-
? `${titlePrefix}Still working on this. Run active ${runElapsed}; no verified progress for ${unverifiedElapsed}.`
|
|
2117
|
-
: `${titlePrefix}Still working on this. Run active ${runElapsed}.`;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
async sendRuntimeMessagingHeartbeat(runId, options = {}) {
|
|
2121
|
-
const runMeta = this.getRunMeta(runId);
|
|
2122
|
-
if (!runMeta || runMeta.aborted) return { sent: false, skipped: true };
|
|
2123
|
-
if (runMeta.triggerSource !== 'messaging' || !runMeta.messagingContext?.platform || !runMeta.messagingContext?.chatId) {
|
|
2124
|
-
return { sent: false, skipped: true };
|
|
2125
|
-
}
|
|
2126
|
-
if (!this.messagingManager) {
|
|
2127
|
-
return { sent: false, skipped: true };
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
const createdAt = isoNow();
|
|
2131
|
-
const content = this.buildMessagingHeartbeatText(runMeta, options);
|
|
2132
|
-
const deliveryResult = await this.messagingManager.sendMessage(
|
|
2133
|
-
runMeta.userId,
|
|
2134
|
-
runMeta.messagingContext.platform,
|
|
2135
|
-
runMeta.messagingContext.chatId,
|
|
2136
|
-
content,
|
|
2137
|
-
{
|
|
2138
|
-
agentId: runMeta.agentId,
|
|
2139
|
-
runId,
|
|
2140
|
-
persistConversation: true,
|
|
2141
|
-
metadata: {
|
|
2142
|
-
interim: true,
|
|
2143
|
-
interim_kind: options.stalled === true ? 'blocker' : 'progress',
|
|
2144
|
-
runtime_heartbeat: true,
|
|
2145
|
-
expects_reply: false,
|
|
2146
|
-
},
|
|
2147
|
-
deliveryKind: 'interim',
|
|
2148
|
-
},
|
|
2149
|
-
);
|
|
2150
|
-
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging heartbeat delivery');
|
|
2151
|
-
|
|
2152
|
-
runMeta.lastInterimMessage = content;
|
|
2153
|
-
if (!Array.isArray(runMeta.interimMessages)) {
|
|
2154
|
-
runMeta.interimMessages = [];
|
|
2155
|
-
}
|
|
2156
|
-
runMeta.interimMessages.push({
|
|
2157
|
-
content,
|
|
2158
|
-
kind: options.stalled === true ? 'blocker' : 'progress',
|
|
2159
|
-
expectsReply: false,
|
|
2160
|
-
deferFollowUp: false,
|
|
2161
|
-
createdAt,
|
|
2162
|
-
});
|
|
2163
|
-
const heartbeatCount = Number(runMeta.progressLedger?.heartbeatCount || 0) + 1;
|
|
2164
|
-
this.updateRunProgress(runId, {
|
|
2165
|
-
heartbeatCount,
|
|
2166
|
-
lastUserVisibleUpdateAt: createdAt,
|
|
2167
|
-
});
|
|
2168
|
-
this.recordRunEvent(runMeta.userId, runId, 'progress_heartbeat_sent', {
|
|
2169
|
-
stalled: options.stalled === true,
|
|
2170
|
-
currentTool: runMeta.progressLedger?.currentTool || null,
|
|
2171
|
-
currentStep: runMeta.progressLedger?.currentStep || null,
|
|
2172
|
-
}, { agentId: runMeta.agentId });
|
|
2173
|
-
this.enqueueSystemSteering(
|
|
2174
|
-
runId,
|
|
2175
|
-
'A runtime progress update was just sent on your behalf because you were blocked in a tool. On your NEXT free turn: use send_interim_update to write 1-2 sentences in your own words describing what you are doing and why. Keep it short and concrete. Then continue toward the final answer.',
|
|
2176
|
-
{ reason: 'heartbeat_ai_followup' },
|
|
2177
|
-
);
|
|
2178
|
-
return { sent: true, content };
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
shouldSendMessagingFinalFallback(runMeta, content, platform = null) {
|
|
2182
|
-
const cleanedContent = normalizeOutgoingMessage(content || '', platform, {
|
|
2183
|
-
collapseWhitespace: false,
|
|
2184
|
-
});
|
|
2185
|
-
const lastFinalDeliveryMessage = normalizeOutgoingMessage(
|
|
2186
|
-
runMeta?.lastSentMessage
|
|
2187
|
-
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
2188
|
-
|| '',
|
|
2189
|
-
platform,
|
|
2190
|
-
);
|
|
2191
|
-
return Boolean(
|
|
2192
|
-
cleanedContent
|
|
2193
|
-
&& !runMeta?.terminalInterim
|
|
2194
|
-
&& runMeta?.explicitMessageSent !== true
|
|
2195
|
-
&& runMeta?.finalDeliverySent !== true
|
|
2196
|
-
&& !lastFinalDeliveryMessage
|
|
2197
|
-
);
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
async deliverMessagingFinalFallback({
|
|
2201
|
-
runId,
|
|
2202
|
-
userId,
|
|
2203
|
-
agentId,
|
|
2204
|
-
platform,
|
|
2205
|
-
chatId,
|
|
2206
|
-
content,
|
|
2207
|
-
}) {
|
|
2208
|
-
const runMeta = this.getRunMeta(runId);
|
|
2209
|
-
if (!runMeta || !this.messagingManager) return { sent: false, skipped: true };
|
|
2210
|
-
const cleanedContent = normalizeOutgoingMessage(content || '', platform, {
|
|
2211
|
-
collapseWhitespace: false,
|
|
2212
|
-
});
|
|
2213
|
-
if (!this.shouldSendMessagingFinalFallback(runMeta, cleanedContent, platform)) {
|
|
2214
|
-
return { sent: false, skipped: true };
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
const chunks = splitOutgoingMessageForPlatform(platform, cleanedContent);
|
|
2218
|
-
console.info(
|
|
2219
|
-
`[Run ${shortenRunId(runId)}] messaging_fallback chunks=${chunks.length} to=${summarizeForLog(chatId, 80)}`
|
|
2220
|
-
);
|
|
2221
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
2222
|
-
if (i > 0) {
|
|
2223
|
-
const delay = Math.max(1000, Math.min(chunks[i].length * 30, 4000));
|
|
2224
|
-
await this.messagingManager.sendTyping(userId, platform, chatId, true, { agentId }).catch(() => {});
|
|
2225
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2226
|
-
}
|
|
2227
|
-
try {
|
|
2228
|
-
await withProviderRetry(async () => {
|
|
2229
|
-
const deliveryResult = await this.messagingManager.sendMessage(
|
|
2230
|
-
userId,
|
|
2231
|
-
platform,
|
|
2232
|
-
chatId,
|
|
2233
|
-
chunks[i],
|
|
2234
|
-
{ runId, agentId },
|
|
2235
|
-
);
|
|
2236
|
-
return requireSuccessfulMessagingDelivery(deliveryResult, 'Final messaging delivery');
|
|
2237
|
-
}, {
|
|
2238
|
-
...this.messagingDeliveryRetry,
|
|
2239
|
-
label: `MessagingDelivery ${platform}`,
|
|
2240
|
-
isRetryable: (error) => (
|
|
2241
|
-
error?.retryable !== false
|
|
2242
|
-
&& (
|
|
2243
|
-
error?.code === 'MESSAGING_DELIVERY_FAILED'
|
|
2244
|
-
|| isTransientError(error)
|
|
2245
|
-
)
|
|
2246
|
-
),
|
|
2247
|
-
});
|
|
2248
|
-
} catch (error) {
|
|
2249
|
-
error.disableAutonomousRetry = true;
|
|
2250
|
-
throw error;
|
|
2251
|
-
}
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
runMeta.lastSentMessage = chunks[chunks.length - 1] || cleanedContent;
|
|
2255
|
-
runMeta.sentMessages = Array.isArray(runMeta.sentMessages)
|
|
2256
|
-
? [...runMeta.sentMessages, ...chunks]
|
|
2257
|
-
: chunks.slice();
|
|
2258
|
-
this.markRunFinalDelivery(runId, runMeta.lastSentMessage);
|
|
2259
|
-
return { sent: true, chunks };
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
async tickMessagingProgressSupervisor(runId) {
|
|
2263
|
-
const runMeta = this.getRunMeta(runId);
|
|
2264
|
-
if (!runMeta || runMeta.aborted || runMeta.triggerSource !== 'messaging') {
|
|
2265
|
-
return { sent: false, skipped: true };
|
|
2266
|
-
}
|
|
2267
|
-
if (runMeta.terminalInterim) {
|
|
2268
|
-
return { sent: false, skipped: true };
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
const now = Date.now();
|
|
2272
|
-
const ledger = runMeta.progressLedger || buildInitialProgressLedger({
|
|
2273
|
-
startedAt: runMeta.startedAtIso || isoNow(),
|
|
2274
|
-
});
|
|
2275
|
-
runMeta.progressLedger = ledger;
|
|
2276
|
-
const startedAtMs = Number.isFinite(runMeta.startedAt) ? runMeta.startedAt : now;
|
|
2277
|
-
|
|
2278
|
-
const lastVerifiedAtMs = timestampMs(ledger.lastVerifiedProgressAt, startedAtMs);
|
|
2279
|
-
const lastVisibleAtMs = timestampMs(ledger.lastUserVisibleUpdateAt, 0);
|
|
2280
|
-
const heartbeatThresholdMs = lastVisibleAtMs > 0
|
|
2281
|
-
? MESSAGING_PROGRESS_REPEAT_MS
|
|
2282
|
-
: MESSAGING_PROGRESS_FIRST_UPDATE_MS;
|
|
2283
|
-
const comparisonVisibleAtMs = lastVisibleAtMs > 0 ? lastVisibleAtMs : startedAtMs;
|
|
2284
|
-
const stalled = (now - lastVerifiedAtMs) >= MESSAGING_PROGRESS_STALL_MS;
|
|
2285
|
-
|
|
2286
|
-
if (stalled && !ledger.stallNotifiedAt) {
|
|
2287
|
-
this.updateRunProgress(runId, {
|
|
2288
|
-
stallNotifiedAt: isoNow(),
|
|
2289
|
-
progressState: 'stalled',
|
|
2290
|
-
});
|
|
2291
|
-
this.recordRunEvent(runMeta.userId, runId, 'progress_stalled', {
|
|
2292
|
-
currentTool: ledger.currentTool || null,
|
|
2293
|
-
currentStep: ledger.currentStep || null,
|
|
2294
|
-
phase: ledger.currentPhase || 'idle',
|
|
2295
|
-
}, { agentId: runMeta.agentId });
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
if ((now - comparisonVisibleAtMs) < heartbeatThresholdMs) {
|
|
2299
|
-
return { sent: false, skipped: true };
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
if (
|
|
2303
|
-
(ledger.currentPhase === 'tool' || ledger.currentPhase === 'model')
|
|
2304
|
-
&& ledger.currentStepStartedAt
|
|
2305
|
-
) {
|
|
2306
|
-
return this.sendRuntimeMessagingHeartbeat(runId, { stalled });
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
if (ledger.currentPhase !== 'idle') {
|
|
2310
|
-
return { sent: false, skipped: true };
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
const lastSupervisorNudgeAtMs = timestampMs(runMeta.lastSupervisorNudgeAt, 0);
|
|
2314
|
-
if (lastSupervisorNudgeAtMs > 0 && (now - lastSupervisorNudgeAtMs) < heartbeatThresholdMs) {
|
|
2315
|
-
return { sent: false, skipped: true };
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
const elapsed = formatElapsedDuration(now - startedAtMs);
|
|
2319
|
-
const nudge = stalled
|
|
2320
|
-
? `You have been running for ${elapsed} and appear stalled. Use send_interim_update RIGHT NOW to write 1-2 sentences explaining the blocker in your own words, then either resolve it or call task_complete with what you have. Do not leave the user without an answer.`
|
|
2321
|
-
: `You have been running for ${elapsed} without sending an update to the user. Use send_interim_update RIGHT NOW to write 1-2 sentences explaining what you are currently doing. Keep it short and concrete. Then continue working toward the final answer.`;
|
|
2322
|
-
const queued = this.enqueueSystemSteering(runId, nudge, {
|
|
2323
|
-
reason: stalled ? 'stalled_progress_check' : 'progress_check',
|
|
2324
|
-
});
|
|
2325
|
-
if (!queued) {
|
|
2326
|
-
return { sent: false, skipped: true };
|
|
2327
|
-
}
|
|
2328
|
-
runMeta.lastSupervisorNudgeAt = isoNow();
|
|
2329
|
-
this.updateRunProgress(runId, {
|
|
2330
|
-
lastUserVisibleUpdateAt: ledger.lastUserVisibleUpdateAt || null,
|
|
2331
|
-
});
|
|
2332
|
-
return { sent: false, queued: true };
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
startMessagingProgressSupervisor(runId) {
|
|
2336
|
-
const runMeta = this.getRunMeta(runId);
|
|
2337
|
-
if (!runMeta || runMeta.triggerSource !== 'messaging' || !runMeta.messagingContext?.platform || !runMeta.messagingContext?.chatId) {
|
|
2338
|
-
return false;
|
|
2339
|
-
}
|
|
2340
|
-
if (runMeta.messagingProgressSupervisor?.timer) {
|
|
2341
|
-
return true;
|
|
2342
|
-
}
|
|
2343
|
-
const timer = setInterval(() => {
|
|
2344
|
-
this.tickMessagingProgressSupervisor(runId).catch((error) => {
|
|
2345
|
-
console.warn('[Engine] Messaging progress supervisor failed:', error?.message || error);
|
|
2346
|
-
});
|
|
2347
|
-
}, MESSAGING_PROGRESS_TICK_MS);
|
|
2348
|
-
timer.unref?.();
|
|
2349
|
-
runMeta.messagingProgressSupervisor = { timer };
|
|
2350
|
-
return true;
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
stopMessagingProgressSupervisor(runId) {
|
|
2354
|
-
const runMeta = this.getRunMeta(runId);
|
|
2355
|
-
const timer = runMeta?.messagingProgressSupervisor?.timer || null;
|
|
2356
|
-
if (timer) {
|
|
2357
|
-
clearInterval(timer);
|
|
2358
|
-
}
|
|
2359
|
-
if (runMeta?.messagingProgressSupervisor) {
|
|
2360
|
-
runMeta.messagingProgressSupervisor = null;
|
|
2361
|
-
}
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
isRunStopped(runId) {
|
|
2365
|
-
return this.getRunMeta(runId)?.aborted === true;
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
attachProcessToRun(runId, pid) {
|
|
2369
|
-
const runMeta = this.getRunMeta(runId);
|
|
2370
|
-
if (!runMeta || !pid) return;
|
|
2371
|
-
runMeta.toolPids.add(pid);
|
|
2372
|
-
if (runMeta.aborted) {
|
|
2373
|
-
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
2374
|
-
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
|
-
|
|
2379
|
-
detachProcessFromRun(runId, pid) {
|
|
2380
|
-
const runMeta = this.getRunMeta(runId);
|
|
2381
|
-
if (!runMeta || !pid) return;
|
|
2382
|
-
runMeta.toolPids.delete(pid);
|
|
2383
|
-
}
|
|
2384
|
-
|
|
2385
|
-
// getIterationLimit() removed — use buildLoopPolicy() directly.
|
|
2386
|
-
// maxIterations is derived in runWithModel from loopPolicy.maxIterations.
|
|
2387
|
-
|
|
2388
|
-
getReasoningEffort(providerName, options = {}) {
|
|
2389
|
-
if (providerName === 'google') return undefined;
|
|
2390
|
-
if (options.latencyProfile === 'voice') {
|
|
2391
|
-
return 'low';
|
|
2392
|
-
}
|
|
2393
|
-
return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
|
-
shouldFastCompleteVoiceReply({
|
|
2397
|
-
options = {},
|
|
2398
|
-
toolExecutions = [],
|
|
2399
|
-
failedStepCount = 0,
|
|
2400
|
-
messagingSent = false,
|
|
2401
|
-
lastReply = '',
|
|
2402
|
-
}) {
|
|
2403
|
-
return options.latencyProfile === 'voice'
|
|
2404
|
-
&& toolExecutions.length === 0
|
|
2405
|
-
&& failedStepCount === 0
|
|
2406
|
-
&& !messagingSent
|
|
2407
|
-
&& Boolean(String(lastReply || '').trim());
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
getMessagingRetryLimit(maxIterations) {
|
|
2411
|
-
// Cap at 3: more than 3 autonomous messaging retries indicates a structural
|
|
2412
|
-
// problem (model unavailable, bad config) that more retries won't solve.
|
|
2413
|
-
return Math.min(3, Math.max(1, maxIterations));
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
|
|
2417
|
-
const messages = [];
|
|
2418
|
-
if (systemPrompt && typeof systemPrompt === 'object') {
|
|
2419
|
-
if (systemPrompt.stable) {
|
|
2420
|
-
messages.push({
|
|
2421
|
-
role: 'system',
|
|
2422
|
-
content: systemPrompt.stable,
|
|
2423
|
-
});
|
|
2424
|
-
}
|
|
2425
|
-
if (systemPrompt.dynamic) {
|
|
2426
|
-
messages.push({ role: 'system', content: systemPrompt.dynamic });
|
|
2427
|
-
}
|
|
2428
|
-
} else {
|
|
2429
|
-
messages.push({ role: 'system', content: systemPrompt });
|
|
2430
|
-
}
|
|
2431
|
-
if (summaryMessage) messages.push(summaryMessage);
|
|
2432
|
-
if (Array.isArray(historyMessages)) messages.push(...historyMessages);
|
|
2433
|
-
if (recallMsg) messages.push({ role: 'system', content: recallMsg });
|
|
2434
|
-
return messages;
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
buildUserMessage(userMessage, options = {}) {
|
|
2438
|
-
if (!options.mediaAttachments || options.mediaAttachments.length === 0) {
|
|
2439
|
-
return { role: 'user', content: userMessage };
|
|
2440
|
-
}
|
|
2441
|
-
|
|
2442
|
-
const contentArr = [{ type: 'text', text: userMessage }];
|
|
2443
|
-
for (const att of options.mediaAttachments) {
|
|
2444
|
-
if ((att.type === 'image' || att.type === 'video') && att.path) {
|
|
2445
|
-
try {
|
|
2446
|
-
if (fs.existsSync(att.path)) {
|
|
2447
|
-
const b64 = fs.readFileSync(att.path).toString('base64');
|
|
2448
|
-
const mime = att.path.endsWith('.png') ? 'image/png' : att.path.endsWith('.gif') ? 'image/gif' : 'image/jpeg';
|
|
2449
|
-
contentArr.push({ type: 'image_url', image_url: { url: `data:${mime};base64,${b64}` } });
|
|
2450
|
-
}
|
|
2451
|
-
} catch (err) {
|
|
2452
|
-
console.warn(`[AgentEngine] Failed to read attachment at ${att.path}:`, err?.message);
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
return { role: 'user', content: contentArr.length > 1 ? contentArr : userMessage };
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
estimatePromptMetrics(messages, tools) {
|
|
2461
|
-
const metrics = {
|
|
2462
|
-
systemPromptTokens: 0,
|
|
2463
|
-
toolSchemaTokens: estimateTokenValue(tools),
|
|
2464
|
-
historyTokens: 0,
|
|
2465
|
-
recalledMemoryTokens: 0,
|
|
2466
|
-
toolReplayTokens: 0,
|
|
2467
|
-
totalEstimatedTokens: 0
|
|
2468
|
-
};
|
|
2469
|
-
|
|
2470
|
-
messages.forEach((msg, index) => {
|
|
2471
|
-
const contentTokens = estimateTokenValue(msg.content);
|
|
2472
|
-
const callTokens = estimateTokenValue(msg.tool_calls);
|
|
2473
|
-
const total = contentTokens + callTokens;
|
|
2474
|
-
|
|
2475
|
-
if (msg.role === 'tool') {
|
|
2476
|
-
metrics.toolReplayTokens += total;
|
|
2477
|
-
} else if (msg.role === 'system' && index === 0) {
|
|
2478
|
-
metrics.systemPromptTokens += total;
|
|
2479
|
-
} else if (msg.role === 'system' && /^\[Recalled memory/.test(msg.content || '')) {
|
|
2480
|
-
metrics.recalledMemoryTokens += total;
|
|
2481
|
-
} else {
|
|
2482
|
-
metrics.historyTokens += total;
|
|
2483
|
-
}
|
|
2484
|
-
});
|
|
2485
|
-
|
|
2486
|
-
metrics.totalEstimatedTokens = metrics.systemPromptTokens
|
|
2487
|
-
+ metrics.toolSchemaTokens
|
|
2488
|
-
+ metrics.historyTokens
|
|
2489
|
-
+ metrics.recalledMemoryTokens
|
|
2490
|
-
+ metrics.toolReplayTokens;
|
|
2491
|
-
|
|
2492
|
-
return metrics;
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
mergePromptMetrics(summary, metrics, iteration, toolCount) {
|
|
2496
|
-
return {
|
|
2497
|
-
iterationsObserved: Math.max(summary.iterationsObserved || 0, iteration),
|
|
2498
|
-
toolCount,
|
|
2499
|
-
maxEstimatedTokens: Math.max(summary.maxEstimatedTokens || 0, metrics.totalEstimatedTokens),
|
|
2500
|
-
maxSystemPromptTokens: Math.max(summary.maxSystemPromptTokens || 0, metrics.systemPromptTokens),
|
|
2501
|
-
maxToolSchemaTokens: Math.max(summary.maxToolSchemaTokens || 0, metrics.toolSchemaTokens),
|
|
2502
|
-
maxHistoryTokens: Math.max(summary.maxHistoryTokens || 0, metrics.historyTokens),
|
|
2503
|
-
maxRecalledMemoryTokens: Math.max(summary.maxRecalledMemoryTokens || 0, metrics.recalledMemoryTokens),
|
|
2504
|
-
maxToolReplayTokens: Math.max(summary.maxToolReplayTokens || 0, metrics.toolReplayTokens),
|
|
2505
|
-
lastEstimate: metrics
|
|
2506
|
-
};
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
async persistPromptMetrics(runId, metrics) {
|
|
2510
|
-
db.prepare('UPDATE agent_runs SET prompt_metrics = ? WHERE id = ?')
|
|
2511
|
-
.run(JSON.stringify(metrics), runId);
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
async run(userId, userMessage, options = {}) {
|
|
2515
|
-
return this.runWithModel(
|
|
2516
|
-
userId,
|
|
2517
|
-
userMessage,
|
|
2518
|
-
options,
|
|
2519
|
-
typeof options.model === 'string' && options.model.trim()
|
|
2520
|
-
? options.model.trim()
|
|
2521
|
-
: null,
|
|
2522
|
-
);
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
|
|
2526
|
-
const triggerType = options.triggerType || 'user';
|
|
2527
|
-
const { resolveAgentId } = require('../agents/manager');
|
|
2528
|
-
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
2529
|
-
ensureDefaultAiSettings(userId, agentId);
|
|
2530
|
-
const aiSettings = getAiSettings(userId, agentId);
|
|
2531
|
-
|
|
2532
|
-
enforceRateLimits(userId);
|
|
2533
|
-
|
|
2534
|
-
const runId = options.runId || uuidv4();
|
|
2535
|
-
const conversationId = options.conversationId;
|
|
2536
|
-
const app = options.app || this.app;
|
|
2537
|
-
const triggerSource = options.triggerSource || 'web';
|
|
2538
|
-
const historyWindow = Math.max(
|
|
2539
|
-
1,
|
|
2540
|
-
Number(options.historyWindow || aiSettings.chat_history_window) || aiSettings.chat_history_window,
|
|
2541
|
-
);
|
|
2542
|
-
// loopPolicy is built after task analysis so analysisMode can be passed in;
|
|
2543
|
-
// we build a provisional policy now (with default mode) and rebuild after
|
|
2544
|
-
// analysis when the mode is known. See the post-analysis policy rebuild below.
|
|
2545
|
-
let loopPolicy = buildLoopPolicy(aiSettings, triggerType, 'execute', options);
|
|
2546
|
-
let maxIterations = loopPolicy.maxIterations;
|
|
2547
|
-
const providerStatusConfig = {
|
|
2548
|
-
agentId,
|
|
2549
|
-
onStatus: (status) => {
|
|
2550
|
-
if (!status?.message) return;
|
|
2551
|
-
this.emit(userId, 'run:interim', {
|
|
2552
|
-
runId,
|
|
2553
|
-
message: status.message,
|
|
2554
|
-
phase: status.phase
|
|
2555
|
-
});
|
|
2556
|
-
}
|
|
2557
|
-
};
|
|
2558
|
-
const selectedProvider = await getProviderForUser(
|
|
2559
|
-
userId,
|
|
2560
|
-
userMessage,
|
|
2561
|
-
triggerType === 'subagent',
|
|
2562
|
-
_modelOverride,
|
|
2563
|
-
providerStatusConfig
|
|
2564
|
-
);
|
|
2565
|
-
let provider = selectedProvider.provider;
|
|
2566
|
-
let model = selectedProvider.model;
|
|
2567
|
-
let providerName = selectedProvider.providerName;
|
|
2568
|
-
const switchToFallbackModel = async (failedModel, error, phase) => {
|
|
2569
|
-
const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
|
|
2570
|
-
if (!fallbackModelId || fallbackModelId === failedModel) return false;
|
|
2571
|
-
console.log(`[Engine] ${phase} failed on ${failedModel}; attempting fallback to: ${fallbackModelId}`);
|
|
2572
|
-
this.emit(userId, 'run:interim', {
|
|
2573
|
-
runId,
|
|
2574
|
-
message: `Model service failed on ${failedModel}; retrying with ${fallbackModelId}.`,
|
|
2575
|
-
phase: 'model_fallback'
|
|
2576
|
-
});
|
|
2577
|
-
const fallback = await getProviderForUser(
|
|
2578
|
-
userId,
|
|
2579
|
-
userMessage,
|
|
2580
|
-
triggerType === 'subagent',
|
|
2581
|
-
fallbackModelId,
|
|
2582
|
-
providerStatusConfig
|
|
2583
|
-
);
|
|
2584
|
-
provider = fallback.provider;
|
|
2585
|
-
model = fallback.model;
|
|
2586
|
-
providerName = fallback.providerName;
|
|
2587
|
-
return true;
|
|
2588
|
-
};
|
|
2589
|
-
const runWithModelFallback = async (phase, fn) => {
|
|
2590
|
-
try {
|
|
2591
|
-
return await fn();
|
|
2592
|
-
} catch (err) {
|
|
2593
|
-
const failedModel = model;
|
|
2594
|
-
const switched = await switchToFallbackModel(failedModel, err, phase);
|
|
2595
|
-
if (!switched) throw err;
|
|
2596
|
-
return await fn();
|
|
2597
|
-
}
|
|
2598
|
-
};
|
|
2599
|
-
|
|
2600
|
-
const runTitle = generateTitle(userMessage);
|
|
2601
|
-
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
2602
|
-
db.prepare(`INSERT OR REPLACE INTO agent_runs(
|
|
2603
|
-
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
2604
|
-
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)`).run(
|
|
2605
|
-
runId,
|
|
2606
|
-
userId,
|
|
2607
|
-
agentId,
|
|
2608
|
-
runTitle,
|
|
2609
|
-
triggerType,
|
|
2610
|
-
triggerSource,
|
|
2611
|
-
model,
|
|
2612
|
-
Object.keys(initialRunMetadata).length ? JSON.stringify(initialRunMetadata) : null,
|
|
2613
|
-
);
|
|
2614
|
-
|
|
2615
|
-
const retryMessagingState = options.messagingRetryState || {};
|
|
2616
|
-
const carriedFinalMessage = String(retryMessagingState.lastFinalMessage || '').trim();
|
|
2617
|
-
const carriedExplicitMessageSent = retryMessagingState.explicitMessageSent === true;
|
|
2618
|
-
const carriedInterimHistory = cloneInterimHistory(retryMessagingState.interimHistory);
|
|
2619
|
-
const carriedLastInterimMessage = carriedInterimHistory[carriedInterimHistory.length - 1]?.content || '';
|
|
2620
|
-
const carriedGoalContract = mergeGoalContracts(
|
|
2621
|
-
normalizeGoalContract({
|
|
2622
|
-
goal: clampRunContext(userMessage, 1200),
|
|
2623
|
-
}),
|
|
2624
|
-
retryMessagingState.goalContract,
|
|
2625
|
-
);
|
|
2626
|
-
const startedAtIso = isoNow();
|
|
2627
|
-
const progressLedger = buildInitialProgressLedger({
|
|
2628
|
-
startedAt: startedAtIso,
|
|
2629
|
-
retryState: retryMessagingState,
|
|
2630
|
-
});
|
|
2631
|
-
|
|
2632
|
-
this.activeRuns.set(runId, {
|
|
2633
|
-
userId,
|
|
2634
|
-
agentId,
|
|
2635
|
-
title: runTitle,
|
|
2636
|
-
status: 'running',
|
|
2637
|
-
aborted: false,
|
|
2638
|
-
messagingSent: false,
|
|
2639
|
-
noResponse: false,
|
|
2640
|
-
explicitMessageSent: carriedExplicitMessageSent,
|
|
2641
|
-
finalDeliverySent: carriedExplicitMessageSent,
|
|
2642
|
-
lastSentMessage: carriedExplicitMessageSent ? carriedFinalMessage : '',
|
|
2643
|
-
sentMessages: [],
|
|
2644
|
-
widgetSnapshotSaved: false,
|
|
2645
|
-
triggerType,
|
|
2646
|
-
triggerSource,
|
|
2647
|
-
startedAt: Date.now(),
|
|
2648
|
-
startedAtIso,
|
|
2649
|
-
lastToolName: null,
|
|
2650
|
-
lastToolTarget: null,
|
|
2651
|
-
lastInterimMessage: carriedExplicitMessageSent ? '' : carriedLastInterimMessage,
|
|
2652
|
-
interimMessages: carriedExplicitMessageSent ? [] : carriedInterimHistory,
|
|
2653
|
-
interimSignatures: carriedExplicitMessageSent
|
|
2654
|
-
? new Set()
|
|
2655
|
-
: createInterimSignatureSet(carriedInterimHistory, options.source || null),
|
|
2656
|
-
terminalInterim: null,
|
|
2657
|
-
voiceSessionId: options.voiceSessionId || null,
|
|
2658
|
-
steeringQueue: [],
|
|
2659
|
-
systemSteeringQueue: [],
|
|
2660
|
-
toolPids: new Set(),
|
|
2661
|
-
repetitionGuard: new ToolRepetitionGuard(),
|
|
2662
|
-
seenOutputHashes: new Map(),
|
|
2663
|
-
consecutiveReadOnlyIterations: 0,
|
|
2664
|
-
messagingContext: triggerSource === 'messaging'
|
|
2665
|
-
? {
|
|
2666
|
-
platform: options.source || null,
|
|
2667
|
-
chatId: options.chatId || null,
|
|
2668
|
-
}
|
|
2669
|
-
: null,
|
|
2670
|
-
goalContract: carriedGoalContract,
|
|
2671
|
-
progressLedger,
|
|
2672
|
-
});
|
|
2673
|
-
this.persistRunMetadata(runId, {
|
|
2674
|
-
progressLedger,
|
|
2675
|
-
goalContract: carriedGoalContract,
|
|
2676
|
-
});
|
|
2677
|
-
this.startMessagingProgressSupervisor(runId);
|
|
2678
|
-
this.emit(userId, 'run:start', { runId, agentId, title: runTitle, model, triggerType, triggerSource });
|
|
2679
|
-
this.recordRunEvent(userId, runId, 'run_started', {
|
|
2680
|
-
title: runTitle,
|
|
2681
|
-
model,
|
|
2682
|
-
triggerType,
|
|
2683
|
-
triggerSource,
|
|
2684
|
-
}, { agentId });
|
|
2685
|
-
console.info(
|
|
2686
|
-
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${model} title=${summarizeForLog(runTitle, 120)}`
|
|
2687
|
-
);
|
|
2688
|
-
|
|
2689
|
-
const systemPrompt = await this.buildSystemPrompt(userId, {
|
|
2690
|
-
...(options.context || {}),
|
|
2691
|
-
userMessage,
|
|
2692
|
-
agentId,
|
|
2693
|
-
triggerSource,
|
|
2694
|
-
});
|
|
2695
|
-
// Pass short descriptions so the model always knows every available tool.
|
|
2696
|
-
// compactToolDefinition caps tool desc at 120 chars, param desc at 70 chars.
|
|
2697
|
-
const builtInTools = this.getAvailableTools(app, {
|
|
2698
|
-
includeDescriptions: true,
|
|
2699
|
-
userId,
|
|
2700
|
-
agentId,
|
|
2701
|
-
triggerType,
|
|
2702
|
-
triggerSource,
|
|
2703
|
-
widgetId: options.widgetId || null,
|
|
2704
|
-
});
|
|
2705
|
-
const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || this.mcpManager;
|
|
2706
|
-
const integrationManager = app?.locals?.integrationManager || null;
|
|
2707
|
-
const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
|
|
2708
|
-
const allTools = selectToolsForTask(userMessage, builtInTools, mcpTools, options);
|
|
2709
|
-
let tools = allTools;
|
|
2710
|
-
const toolNames = allTools.map((tool) => tool.name).filter(Boolean);
|
|
2711
|
-
const coreToolStatus = {
|
|
2712
|
-
send_message: toolNames.includes('send_message'),
|
|
2713
|
-
create_task: toolNames.includes('create_task'),
|
|
2714
|
-
list_tasks: toolNames.includes('list_tasks'),
|
|
2715
|
-
update_task: toolNames.includes('update_task'),
|
|
2716
|
-
delete_task: toolNames.includes('delete_task'),
|
|
2717
|
-
};
|
|
2718
|
-
this.recordRunEvent(userId, runId, 'tool_inventory', {
|
|
2719
|
-
total: toolNames.length,
|
|
2720
|
-
builtInTotal: builtInTools.length,
|
|
2721
|
-
mcpTotal: mcpTools.length,
|
|
2722
|
-
core: coreToolStatus,
|
|
2723
|
-
}, { agentId });
|
|
2724
|
-
console.info(
|
|
2725
|
-
`[Run ${shortenRunId(runId)}] tools total=${toolNames.length} builtIns=${builtInTools.length} mcp=${mcpTools.length} core=${JSON.stringify(coreToolStatus)}`
|
|
2726
|
-
);
|
|
2727
|
-
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine: this });
|
|
2728
|
-
const capabilitySummary = summarizeCapabilityHealth(capabilityHealth);
|
|
2729
|
-
const integrationSummary = integrationManager?.summarizeConnectedProviders?.(userId, agentId) || '';
|
|
2730
|
-
|
|
2731
|
-
const { MemoryManager } = require('../memory/manager');
|
|
2732
|
-
const memoryManager = this.memoryManager || new MemoryManager();
|
|
2733
|
-
const recallQuery = options.context?.rawUserMessage || userMessage;
|
|
2734
|
-
const recallMsg = options.skipGlobalRecall === true
|
|
2735
|
-
? null
|
|
2736
|
-
: await this.buildMemoryRecall({
|
|
2737
|
-
memoryManager,
|
|
2738
|
-
userId,
|
|
2739
|
-
agentId,
|
|
2740
|
-
query: recallQuery,
|
|
2741
|
-
provider,
|
|
2742
|
-
providerName,
|
|
2743
|
-
model,
|
|
2744
|
-
runId,
|
|
2745
|
-
options,
|
|
2746
|
-
});
|
|
2747
|
-
|
|
2748
|
-
let summaryMessage = null;
|
|
2749
|
-
let historyMessages = [];
|
|
2750
|
-
|
|
2751
|
-
if (conversationId && options.skipConversationHistory !== true) {
|
|
2752
|
-
const conversationContext = getConversationContext(conversationId, historyWindow);
|
|
2753
|
-
summaryMessage = buildSummaryCarrier(conversationContext.summary || options.priorSummary || '');
|
|
2754
|
-
historyMessages = conversationContext.recentMessages.length > 0
|
|
2755
|
-
? conversationContext.recentMessages
|
|
2756
|
-
: (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
2757
|
-
} else {
|
|
2758
|
-
summaryMessage = buildSummaryCarrier(options.priorSummary || '');
|
|
2759
|
-
historyMessages = (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2762
|
-
let messages = this.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
|
|
2763
|
-
if (capabilitySummary) {
|
|
2764
|
-
messages.push({ role: 'system', content: `[Capability health]\n${capabilitySummary}` });
|
|
2765
|
-
}
|
|
2766
|
-
if (integrationSummary) {
|
|
2767
|
-
messages.push({ role: 'system', content: `[Official integrations]\n${integrationSummary}` });
|
|
2768
|
-
}
|
|
2769
|
-
const threadStateMessage = conversationId ? memoryManager.buildConversationStateMessage(conversationId) : null;
|
|
2770
|
-
if (threadStateMessage) {
|
|
2771
|
-
messages.push({ role: 'system', content: threadStateMessage });
|
|
2772
|
-
}
|
|
2773
|
-
if (carriedGoalContract) {
|
|
2774
|
-
messages.push({
|
|
2775
|
-
role: 'system',
|
|
2776
|
-
content: buildGoalContractPrompt(carriedGoalContract, 'Persisted run goal'),
|
|
2777
|
-
});
|
|
2778
|
-
}
|
|
2779
|
-
this.recordRunEvent(userId, runId, 'memory_injected', {
|
|
2780
|
-
hasRecallContext: Boolean(recallMsg),
|
|
2781
|
-
hasThreadState: Boolean(threadStateMessage),
|
|
2782
|
-
recallPreview: recallMsg ? String(recallMsg).slice(0, 240) : '',
|
|
2783
|
-
}, { agentId });
|
|
2784
|
-
messages.push(this.buildUserMessage(userMessage, options));
|
|
2785
|
-
messages = sanitizeConversationMessages(messages);
|
|
2786
|
-
|
|
2787
|
-
if (conversationId) {
|
|
2788
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content) VALUES (?, ?, ?)')
|
|
2789
|
-
.run(conversationId, 'user', userMessage);
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
let iteration = 0;
|
|
2793
|
-
let totalTokens = 0;
|
|
2794
|
-
let lastContent = '';
|
|
2795
|
-
let stepIndex = 0;
|
|
2796
|
-
let failedStepCount = 0;
|
|
2797
|
-
let modelFailureRecoveries = 0;
|
|
2798
|
-
let promptMetrics = {};
|
|
2799
|
-
let toolExecutions = [];
|
|
2800
|
-
let compactionMetrics = [];
|
|
2801
|
-
let analysis = null;
|
|
2802
|
-
let plan = null;
|
|
2803
|
-
let verification = null;
|
|
2804
|
-
let deliverableWorkflow = null;
|
|
2805
|
-
let deliverablePlan = null;
|
|
2806
|
-
let deliverableArtifacts = [];
|
|
2807
|
-
let deliverableValidation = null;
|
|
2808
|
-
let directAnswerEligible = false;
|
|
2809
|
-
let analysisUsage = 0;
|
|
2810
|
-
|
|
2811
|
-
try {
|
|
2812
|
-
if (options.skipTaskAnalysis === true) {
|
|
2813
|
-
analysis = buildSkipTaskAnalysisResult(options.forceMode);
|
|
2814
|
-
} else {
|
|
2815
|
-
const analysisResult = await runWithModelFallback('task analysis', () => this.analyzeTask({
|
|
2816
|
-
provider,
|
|
2817
|
-
providerName,
|
|
2818
|
-
model,
|
|
2819
|
-
messages,
|
|
2820
|
-
tools,
|
|
2821
|
-
capabilityHealth,
|
|
2822
|
-
forceMode: options.forceMode || null,
|
|
2823
|
-
userMessage,
|
|
2824
|
-
options: { ...options, triggerSource, runId, userId, agentId },
|
|
2825
|
-
}));
|
|
2826
|
-
analysisUsage = analysisResult.usage || 0;
|
|
2827
|
-
totalTokens += analysisUsage;
|
|
2828
|
-
analysis = applyForcedAnalysisMode({ ...analysisResult.analysis }, options.forceMode);
|
|
2829
|
-
if (!analysis.goal && userMessage) {
|
|
2830
|
-
analysis.goal = String(userMessage).trim().slice(0, 300);
|
|
2831
|
-
}
|
|
2832
|
-
analysis.mode = promoteAnalysisMode(analysis.mode, {
|
|
2833
|
-
verificationNeed: analysis.verification_need,
|
|
2834
|
-
freshnessRisk: analysis.freshness_risk,
|
|
2835
|
-
draftReply: analysis.draft_reply,
|
|
2836
|
-
planningDepth: analysis.planning_depth,
|
|
2837
|
-
});
|
|
2838
|
-
|
|
2839
|
-
stepIndex += 1;
|
|
2840
|
-
const analysisStepId = uuidv4();
|
|
2841
|
-
db.prepare(`INSERT INTO agent_steps
|
|
2842
|
-
(id, run_id, step_index, type, description, status, result, started_at, completed_at)
|
|
2843
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
2844
|
-
.run(
|
|
2845
|
-
analysisStepId,
|
|
2846
|
-
runId,
|
|
2847
|
-
stepIndex,
|
|
2848
|
-
'analysis',
|
|
2849
|
-
'Task analysis contract',
|
|
2850
|
-
'completed',
|
|
2851
|
-
JSON.stringify(analysis).slice(0, 20000)
|
|
2852
|
-
);
|
|
2853
|
-
this.persistRunMetadata(runId, {
|
|
2854
|
-
taskAnalysis: analysis,
|
|
2855
|
-
capabilityHealth,
|
|
2856
|
-
});
|
|
2857
|
-
this.updateRunGoalContract(runId, goalContractFromAnalysis(analysis));
|
|
2858
|
-
this.emit(userId, 'run:analysis', {
|
|
2859
|
-
runId,
|
|
2860
|
-
...analysis,
|
|
2861
|
-
capabilitySummary,
|
|
2862
|
-
});
|
|
2863
|
-
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
tools = selectInitialTools(allTools, analysis.suggested_tools, {
|
|
2867
|
-
widgetId: options.widgetId || null,
|
|
2868
|
-
});
|
|
2869
|
-
this.initializeToolRuntime(runId, allTools, tools, options);
|
|
2870
|
-
messages.push({
|
|
2871
|
-
role: 'system',
|
|
2872
|
-
content: [
|
|
2873
|
-
'[Available tool catalog]',
|
|
2874
|
-
buildToolCatalog(allTools),
|
|
2875
|
-
'',
|
|
2876
|
-
`Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
|
|
2877
|
-
'Use activate_tools with exact catalog names when another schema is required.',
|
|
2878
|
-
].join('\n'),
|
|
2879
|
-
});
|
|
2880
|
-
this.recordRunEvent(userId, runId, 'tool_selection_applied', {
|
|
2881
|
-
activeToolNames: tools.map((tool) => tool.name),
|
|
2882
|
-
catalogSize: allTools.length,
|
|
2883
|
-
}, { agentId });
|
|
2884
|
-
|
|
2885
|
-
const activeDefaultModelSetting = triggerType === 'subagent'
|
|
2886
|
-
? aiSettings.default_subagent_model
|
|
2887
|
-
: aiSettings.default_chat_model;
|
|
2888
|
-
if (!_modelOverride && activeDefaultModelSetting === 'auto' && aiSettings.smarter_model_selector !== false) {
|
|
2889
|
-
const requestedPurpose = analysis?.mode === 'plan_execute' || analysis?.complexity === 'complex' || analysis?.autonomy_level === 'high'
|
|
2890
|
-
? 'planning'
|
|
2891
|
-
: triggerType === 'subagent'
|
|
2892
|
-
? 'fast'
|
|
2893
|
-
: '';
|
|
2894
|
-
if (requestedPurpose) {
|
|
2895
|
-
const selectedAfterAnalysis = await getProviderForUser(
|
|
2896
|
-
userId,
|
|
2897
|
-
userMessage,
|
|
2898
|
-
triggerType === 'subagent',
|
|
2899
|
-
null,
|
|
2900
|
-
{
|
|
2901
|
-
...providerStatusConfig,
|
|
2902
|
-
selectionHint: {
|
|
2903
|
-
purpose: requestedPurpose,
|
|
2904
|
-
complexity: analysis?.complexity,
|
|
2905
|
-
autonomyLevel: analysis?.autonomy_level,
|
|
2906
|
-
requiredConfidence: analysis?.completion_confidence_required,
|
|
2907
|
-
costMode: aiSettings.cost_mode,
|
|
2908
|
-
},
|
|
2909
|
-
}
|
|
2910
|
-
);
|
|
2911
|
-
if (selectedAfterAnalysis.model !== model) {
|
|
2912
|
-
provider = selectedAfterAnalysis.provider;
|
|
2913
|
-
model = selectedAfterAnalysis.model;
|
|
2914
|
-
providerName = selectedAfterAnalysis.providerName;
|
|
2915
|
-
db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2916
|
-
.run(model, runId);
|
|
2917
|
-
this.emit(userId, 'run:interim', {
|
|
2918
|
-
runId,
|
|
2919
|
-
message: `Switched to ${model} for this run after task analysis.`,
|
|
2920
|
-
phase: 'model_selection'
|
|
2921
|
-
});
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
|
|
2926
|
-
// Rebuild loop policy with the resolved analysis mode. Runs in both the
|
|
2927
|
-
// normal path and the skipTaskAnalysis path so that forceMode='plan_execute'
|
|
2928
|
-
// (or any mode set by buildSkipTaskAnalysisResult) raises the iteration
|
|
2929
|
-
// ceiling correctly.
|
|
2930
|
-
loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute', {
|
|
2931
|
-
...options,
|
|
2932
|
-
autonomyPolicy: buildAutonomyPolicyFromAnalysis(analysis),
|
|
2933
|
-
});
|
|
2934
|
-
maxIterations = loopPolicy.maxIterations;
|
|
2935
|
-
|
|
2936
|
-
if (options.skipDeliverableWorkflow !== true) {
|
|
2937
|
-
const deliverableSelectionResult = await selectDeliverableWorkflow({
|
|
2938
|
-
engine: this,
|
|
2939
|
-
provider,
|
|
2940
|
-
providerName,
|
|
2941
|
-
model,
|
|
2942
|
-
messages,
|
|
2943
|
-
tools,
|
|
2944
|
-
options: { ...options, runId, userId, agentId },
|
|
2945
|
-
});
|
|
2946
|
-
totalTokens += deliverableSelectionResult.usage || 0;
|
|
2947
|
-
const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
|
|
2948
|
-
if (selectedWorkflow?.canHandle(deliverableSelectionResult.selection)) {
|
|
2949
|
-
deliverableWorkflow = {
|
|
2950
|
-
workflow: selectedWorkflow,
|
|
2951
|
-
selection: deliverableSelectionResult.selection,
|
|
2952
|
-
request: selectedWorkflow.normalizeRequest({
|
|
2953
|
-
...deliverableSelectionResult.selection,
|
|
2954
|
-
userMessage,
|
|
2955
|
-
}),
|
|
2956
|
-
};
|
|
2957
|
-
deliverablePlan = selectedWorkflow.buildExecutionPlan(deliverableWorkflow.request, {
|
|
2958
|
-
analysis,
|
|
2959
|
-
tools,
|
|
2960
|
-
options,
|
|
2961
|
-
});
|
|
2962
|
-
await selectedWorkflow.run(deliverablePlan, {
|
|
2963
|
-
engine: this,
|
|
2964
|
-
userId,
|
|
2965
|
-
agentId,
|
|
2966
|
-
runId,
|
|
2967
|
-
agentId,
|
|
2968
|
-
app,
|
|
2969
|
-
});
|
|
2970
|
-
this.persistRunMetadata(runId, {
|
|
2971
|
-
deliverableWorkflow: {
|
|
2972
|
-
...deliverableWorkflow.selection,
|
|
2973
|
-
plan: deliverablePlan,
|
|
2974
|
-
},
|
|
2975
|
-
});
|
|
2976
|
-
this.updateRunGoalContract(runId, {
|
|
2977
|
-
goal: deliverableWorkflow.selection.goal,
|
|
2978
|
-
});
|
|
2979
|
-
this.recordRunEvent(userId, runId, 'deliverable_workflow_selected', {
|
|
2980
|
-
type: deliverableWorkflow.selection.type,
|
|
2981
|
-
confidence: deliverableWorkflow.selection.confidence,
|
|
2982
|
-
goal: deliverableWorkflow.selection.goal,
|
|
2983
|
-
requestedOutputs: deliverableWorkflow.selection.requestedOutputs,
|
|
2984
|
-
}, { agentId });
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
if (analysis.mode === 'plan_execute') {
|
|
2989
|
-
const planResult = await runWithModelFallback('execution planning', () => this.createExecutionPlan({
|
|
2990
|
-
provider,
|
|
2991
|
-
providerName,
|
|
2992
|
-
model,
|
|
2993
|
-
messages,
|
|
2994
|
-
analysis,
|
|
2995
|
-
capabilitySummary,
|
|
2996
|
-
options: { ...options, runId, userId, agentId },
|
|
2997
|
-
}));
|
|
2998
|
-
totalTokens += planResult.usage || 0;
|
|
2999
|
-
plan = planResult.plan;
|
|
3000
|
-
stepIndex += 1;
|
|
3001
|
-
const planStepId = uuidv4();
|
|
3002
|
-
db.prepare(`INSERT INTO agent_steps
|
|
3003
|
-
(id, run_id, step_index, type, description, status, result, started_at, completed_at)
|
|
3004
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
3005
|
-
.run(
|
|
3006
|
-
planStepId,
|
|
3007
|
-
runId,
|
|
3008
|
-
stepIndex,
|
|
3009
|
-
'planning',
|
|
3010
|
-
'Execution plan',
|
|
3011
|
-
'completed',
|
|
3012
|
-
JSON.stringify(plan).slice(0, 20000)
|
|
3013
|
-
);
|
|
3014
|
-
this.persistRunMetadata(runId, { executionPlan: plan });
|
|
3015
|
-
this.updateRunGoalContract(runId, goalContractFromPlan(plan));
|
|
3016
|
-
this.emit(userId, 'run:plan', {
|
|
3017
|
-
runId,
|
|
3018
|
-
steps: plan.steps,
|
|
3019
|
-
successCriteria: plan.success_criteria,
|
|
3020
|
-
verificationFocus: plan.verification_focus,
|
|
3021
|
-
});
|
|
3022
|
-
}
|
|
3023
|
-
|
|
3024
|
-
const runGoalContract = this.getRunMeta(runId)?.goalContract || null;
|
|
3025
|
-
if (runGoalContract) {
|
|
3026
|
-
messages.push({
|
|
3027
|
-
role: 'system',
|
|
3028
|
-
content: buildGoalContractPrompt(runGoalContract, 'Run goal contract'),
|
|
3029
|
-
});
|
|
3030
|
-
}
|
|
3031
|
-
messages.push({
|
|
3032
|
-
role: 'system',
|
|
3033
|
-
content: buildExecutionGuidance({
|
|
3034
|
-
analysis,
|
|
3035
|
-
plan,
|
|
3036
|
-
capabilityHealth: capabilitySummary,
|
|
3037
|
-
}),
|
|
3038
|
-
});
|
|
3039
|
-
if (deliverablePlan) {
|
|
3040
|
-
messages.push({
|
|
3041
|
-
role: 'system',
|
|
3042
|
-
content: buildDeliverableWorkflowGuidance(deliverablePlan),
|
|
3043
|
-
});
|
|
3044
|
-
this.recordRunEvent(userId, runId, 'deliverable_execution_started', {
|
|
3045
|
-
type: deliverableWorkflow?.selection?.type,
|
|
3046
|
-
preferredTools: deliverablePlan.preferredTools || [],
|
|
3047
|
-
expectedOutputs: deliverablePlan.expectedOutputs || [],
|
|
3048
|
-
}, { agentId });
|
|
3049
|
-
}
|
|
3050
|
-
messages = sanitizeConversationMessages(messages);
|
|
3051
|
-
|
|
3052
|
-
directAnswerEligible = isDirectAnswerEligibleAnalysis(analysis)
|
|
3053
|
-
&& Boolean(normalizeOutgoingMessage(analysis.draft_reply));
|
|
3054
|
-
|
|
3055
|
-
if (directAnswerEligible) {
|
|
3056
|
-
iteration = 1;
|
|
3057
|
-
lastContent = analysis.draft_reply.trim();
|
|
3058
|
-
messages.push({ role: 'assistant', content: lastContent });
|
|
3059
|
-
if (conversationId) {
|
|
3060
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
3061
|
-
.run(conversationId, 'assistant', lastContent, analysisUsage);
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
// BUG FIX: consecutiveToolFailures was previously declared INSIDE the
|
|
3066
|
-
// while loop (resetting each iteration). It is now tracked across the
|
|
3067
|
-
// full run so the failure guard fires correctly after 5 consecutive failures
|
|
3068
|
-
// regardless of which iteration they fall in.
|
|
3069
|
-
let consecutiveToolFailures = 0;
|
|
3070
|
-
|
|
3071
|
-
while (!directAnswerEligible && iteration < maxIterations) {
|
|
3072
|
-
if (this.isRunStopped(runId)) break;
|
|
3073
|
-
iteration++;
|
|
3074
|
-
|
|
3075
|
-
const systemSteeringAtLoopStart = this.applyQueuedSystemSteering(runId, messages);
|
|
3076
|
-
messages = systemSteeringAtLoopStart.messages;
|
|
3077
|
-
const steeringAtLoopStart = this.applyQueuedSteering(runId, messages, {
|
|
3078
|
-
userId,
|
|
3079
|
-
conversationId
|
|
3080
|
-
});
|
|
3081
|
-
messages = steeringAtLoopStart.messages;
|
|
3082
|
-
messages = sanitizeConversationMessages(messages);
|
|
3083
|
-
|
|
3084
|
-
// Analysis-paralysis gate: fire at the start of every iteration where
|
|
3085
|
-
// the agent has spent N turns only reading/listing/searching without
|
|
3086
|
-
// taking any concrete action. Escalates in urgency each turn.
|
|
3087
|
-
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
3088
|
-
const readOnlyCount = this.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
3089
|
-
if (readOnlyCount >= 3) {
|
|
3090
|
-
const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
|
|
3091
|
-
messages.push({
|
|
3092
|
-
role: 'system',
|
|
3093
|
-
content: `${urgency} — ${readOnlyCount} consecutive read-only turns: You have been gathering information for ${readOnlyCount} turns without writing, creating, sending, or running anything. You must take ONE concrete action this turn (create a file, open a PR, run a command that modifies state, send a message) or call task_complete to report what you found and why you cannot proceed. Do not read or list anything further.`,
|
|
3094
|
-
});
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
|
|
3098
|
-
this.updateRunProgress(runId, {
|
|
3099
|
-
currentPhase: 'model',
|
|
3100
|
-
currentStep: `model:${iteration}`,
|
|
3101
|
-
currentTool: null,
|
|
3102
|
-
currentStepStartedAt: isoNow(),
|
|
3103
|
-
});
|
|
3104
|
-
|
|
3105
|
-
let metrics = this.estimatePromptMetrics(messages, tools);
|
|
3106
|
-
const contextWindow = provider.getContextWindow(model);
|
|
3107
|
-
if (metrics.totalEstimatedTokens > contextWindow * loopPolicy.compactionThreshold) {
|
|
3108
|
-
messages = await withModelCallTimeout(
|
|
3109
|
-
compact(messages, provider, model, contextWindow),
|
|
3110
|
-
options,
|
|
3111
|
-
`Context compaction before iteration ${iteration}`,
|
|
3112
|
-
);
|
|
3113
|
-
messages = sanitizeConversationMessages(messages);
|
|
3114
|
-
this.emit(userId, 'run:compaction', { runId, iteration });
|
|
3115
|
-
metrics = this.estimatePromptMetrics(messages, tools);
|
|
3116
|
-
}
|
|
3117
|
-
|
|
3118
|
-
promptMetrics = this.mergePromptMetrics(promptMetrics, metrics, iteration, tools.length);
|
|
3119
|
-
this.persistPromptMetrics(runId, promptMetrics).catch(() => { });
|
|
3120
|
-
this.emit(userId, 'run:thinking', { runId, iteration });
|
|
3121
|
-
this.recordRunEvent(userId, runId, 'model_turn_started', {
|
|
3122
|
-
iteration,
|
|
3123
|
-
toolCount: tools.length,
|
|
3124
|
-
}, { agentId });
|
|
3125
|
-
|
|
3126
|
-
let response;
|
|
3127
|
-
let responseModel = model;
|
|
3128
|
-
let streamContent = '';
|
|
3129
|
-
|
|
3130
|
-
const tryModelCall = async (retryForFallback = true) => {
|
|
3131
|
-
try {
|
|
3132
|
-
const modelCall = await this.requestModelResponse({
|
|
3133
|
-
provider,
|
|
3134
|
-
providerName,
|
|
3135
|
-
model,
|
|
3136
|
-
messages,
|
|
3137
|
-
tools,
|
|
3138
|
-
options: { ...options, userId, agentId, runId, phase: 'model_turn' },
|
|
3139
|
-
runId,
|
|
3140
|
-
iteration,
|
|
3141
|
-
});
|
|
3142
|
-
response = modelCall.response;
|
|
3143
|
-
responseModel = modelCall.responseModel;
|
|
3144
|
-
streamContent = modelCall.streamContent;
|
|
3145
|
-
} catch (err) {
|
|
3146
|
-
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
3147
|
-
const fallbackModelId = retryForFallback
|
|
3148
|
-
? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id, err)
|
|
3149
|
-
: null;
|
|
3150
|
-
if (fallbackModelId) {
|
|
3151
|
-
const failedModel = model;
|
|
3152
|
-
console.log(`[Engine] Attempting fallback to: ${fallbackModelId}`);
|
|
3153
|
-
const fallback = await getProviderForUser(
|
|
3154
|
-
userId,
|
|
3155
|
-
userMessage,
|
|
3156
|
-
triggerType === 'subagent',
|
|
3157
|
-
fallbackModelId,
|
|
3158
|
-
providerStatusConfig
|
|
3159
|
-
);
|
|
3160
|
-
provider = fallback.provider;
|
|
3161
|
-
model = fallback.model;
|
|
3162
|
-
providerName = fallback.providerName;
|
|
3163
|
-
|
|
3164
|
-
const retryMessages = sanitizeConversationMessages([
|
|
3165
|
-
...messages,
|
|
3166
|
-
{
|
|
3167
|
-
role: 'system',
|
|
3168
|
-
content: buildModelFailureLoopPrompt({
|
|
3169
|
-
failedModel,
|
|
3170
|
-
nextModel: model,
|
|
3171
|
-
errorMessage: err.message
|
|
3172
|
-
})
|
|
3173
|
-
}
|
|
3174
|
-
]);
|
|
3175
|
-
|
|
3176
|
-
const fallbackCall = await this.requestModelResponse({
|
|
3177
|
-
provider,
|
|
3178
|
-
providerName,
|
|
3179
|
-
model,
|
|
3180
|
-
messages: retryMessages,
|
|
3181
|
-
tools,
|
|
3182
|
-
options: { ...options, userId },
|
|
3183
|
-
runId,
|
|
3184
|
-
iteration,
|
|
3185
|
-
});
|
|
3186
|
-
response = fallbackCall.response;
|
|
3187
|
-
responseModel = fallbackCall.responseModel;
|
|
3188
|
-
streamContent = fallbackCall.streamContent;
|
|
3189
|
-
} else {
|
|
3190
|
-
throw err;
|
|
3191
|
-
}
|
|
3192
|
-
}
|
|
3193
|
-
};
|
|
3194
|
-
|
|
3195
|
-
try {
|
|
3196
|
-
await tryModelCall();
|
|
3197
|
-
} catch (err) {
|
|
3198
|
-
const modelError = String(err?.message || 'Model call failed');
|
|
3199
|
-
|
|
3200
|
-
if (modelFailureRecoveries < loopPolicy.maxModelFailureRecoveries) {
|
|
3201
|
-
const failedModel = model;
|
|
3202
|
-
const switched = await switchToFallbackModel(failedModel, err, 'model turn');
|
|
3203
|
-
if (!switched) throw err;
|
|
3204
|
-
modelFailureRecoveries += 1;
|
|
3205
|
-
failedStepCount += 1;
|
|
3206
|
-
messages.push({
|
|
3207
|
-
role: 'system',
|
|
3208
|
-
content: buildModelFailureLoopPrompt({
|
|
3209
|
-
failedModel,
|
|
3210
|
-
nextModel: model,
|
|
3211
|
-
errorMessage: modelError
|
|
3212
|
-
})
|
|
3213
|
-
});
|
|
3214
|
-
this.emit(userId, 'run:interim', {
|
|
3215
|
-
runId,
|
|
3216
|
-
message: 'Model call failed; adapting and retrying autonomously.',
|
|
3217
|
-
phase: 'recovering'
|
|
3218
|
-
});
|
|
3219
|
-
continue;
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
throw err;
|
|
3223
|
-
}
|
|
3224
|
-
|
|
3225
|
-
if (!response) {
|
|
3226
|
-
response = { content: streamContent, toolCalls: [], finishReason: 'stop', usage: null };
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
if (response.usage) {
|
|
3230
|
-
totalTokens += response.usage.totalTokens || 0;
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
lastContent = sanitizeModelOutput(response.content || streamContent || '', { model: responseModel });
|
|
3234
|
-
|
|
3235
|
-
if ((!response.toolCalls || response.toolCalls.length === 0) && lastContent) {
|
|
3236
|
-
const salvaged = salvageTextToolCalls(lastContent, tools);
|
|
3237
|
-
if (salvaged.toolCalls.length > 0) {
|
|
3238
|
-
response.toolCalls = salvaged.toolCalls;
|
|
3239
|
-
response.finishReason = 'tool_calls';
|
|
3240
|
-
response.content = salvaged.content;
|
|
3241
|
-
lastContent = salvaged.content;
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
|
|
3245
|
-
this.recordRunEvent(userId, runId, 'model_turn_completed', {
|
|
3246
|
-
iteration,
|
|
3247
|
-
toolCallCount: response.toolCalls?.length || 0,
|
|
3248
|
-
contentPreview: String(lastContent || streamContent || '').slice(0, 240),
|
|
3249
|
-
}, { agentId });
|
|
3250
|
-
this.updateRunProgress(runId, {}, {
|
|
3251
|
-
verified: true,
|
|
3252
|
-
});
|
|
3253
|
-
|
|
3254
|
-
const assistantMessage = { role: 'assistant', content: lastContent };
|
|
3255
|
-
if (response.toolCalls?.length) assistantMessage.tool_calls = response.toolCalls;
|
|
3256
|
-
if (response.providerContentBlocks?.length) assistantMessage.providerContentBlocks = response.providerContentBlocks;
|
|
3257
|
-
messages.push(assistantMessage);
|
|
3258
|
-
|
|
3259
|
-
if (conversationId) {
|
|
3260
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_calls, tokens) VALUES (?, ?, ?, ?, ?)')
|
|
3261
|
-
.run(
|
|
3262
|
-
conversationId,
|
|
3263
|
-
'assistant',
|
|
3264
|
-
lastContent,
|
|
3265
|
-
response.toolCalls?.length ? JSON.stringify(response.toolCalls) : null,
|
|
3266
|
-
response.usage?.totalTokens || 0
|
|
3267
|
-
);
|
|
3268
|
-
}
|
|
3269
|
-
|
|
3270
|
-
if (!response.toolCalls || response.toolCalls.length === 0) {
|
|
3271
|
-
this.updateRunProgress(runId, {
|
|
3272
|
-
currentPhase: 'idle',
|
|
3273
|
-
currentStep: null,
|
|
3274
|
-
currentTool: null,
|
|
3275
|
-
currentStepStartedAt: null,
|
|
3276
|
-
});
|
|
3277
|
-
// Check for queued steering first — if something was injected while the
|
|
3278
|
-
// model was responding (e.g. a heartbeat nudge), give the model a chance
|
|
3279
|
-
// to act on it before we treat this as a final answer.
|
|
3280
|
-
const systemSteeringAfterResponse = this.applyQueuedSystemSteering(runId, messages);
|
|
3281
|
-
messages = systemSteeringAfterResponse.messages;
|
|
3282
|
-
if (systemSteeringAfterResponse.appliedCount > 0) {
|
|
3283
|
-
iteration = Math.max(0, iteration - 1);
|
|
3284
|
-
lastContent = '';
|
|
3285
|
-
continue;
|
|
3286
|
-
}
|
|
3287
|
-
const steeringAfterResponse = this.applyQueuedSteering(runId, messages, {
|
|
3288
|
-
userId,
|
|
3289
|
-
conversationId
|
|
3290
|
-
});
|
|
3291
|
-
messages = steeringAfterResponse.messages;
|
|
3292
|
-
if (steeringAfterResponse.appliedCount > 0) {
|
|
3293
|
-
iteration = Math.max(0, iteration - 1);
|
|
3294
|
-
lastContent = '';
|
|
3295
|
-
continue;
|
|
3296
|
-
}
|
|
3297
|
-
if (this.shouldFastCompleteVoiceReply({
|
|
3298
|
-
options,
|
|
3299
|
-
toolExecutions,
|
|
3300
|
-
failedStepCount,
|
|
3301
|
-
messagingSent: this.activeRuns.get(runId)?.messagingSent || false,
|
|
3302
|
-
lastReply: lastContent,
|
|
3303
|
-
})) {
|
|
3304
|
-
break;
|
|
3305
|
-
}
|
|
3306
|
-
// AI returned text with no tool calls → trust it as the final answer.
|
|
3307
|
-
directAnswerEligible = true;
|
|
3308
|
-
break;
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
const canRunParallelBatch = (
|
|
3312
|
-
response.toolCalls.length > 1
|
|
3313
|
-
&& response.toolCalls.every((toolCall) => this.isReadOnlyToolCall(toolCall))
|
|
3314
|
-
);
|
|
3315
|
-
if (canRunParallelBatch) {
|
|
3316
|
-
const parallelToolNames = response.toolCalls
|
|
3317
|
-
.map((toolCall) => toolCall.function?.name)
|
|
3318
|
-
.filter(Boolean);
|
|
3319
|
-
this.updateRunProgress(runId, {
|
|
3320
|
-
currentPhase: 'tool',
|
|
3321
|
-
currentStep: `parallel:${iteration}`,
|
|
3322
|
-
currentTool: parallelToolNames.join(', ') || 'parallel tools',
|
|
3323
|
-
currentStepStartedAt: isoNow(),
|
|
3324
|
-
});
|
|
3325
|
-
const batch = await this.executeReadOnlyBatch(response.toolCalls, {
|
|
3326
|
-
userId,
|
|
3327
|
-
runId,
|
|
3328
|
-
agentId,
|
|
3329
|
-
app,
|
|
3330
|
-
triggerType,
|
|
3331
|
-
triggerSource,
|
|
3332
|
-
conversationId,
|
|
3333
|
-
startingStepIndex: stepIndex,
|
|
3334
|
-
iteration,
|
|
3335
|
-
options,
|
|
3336
|
-
});
|
|
3337
|
-
stepIndex = batch.endingStepIndex;
|
|
3338
|
-
for (const item of batch.results) {
|
|
3339
|
-
const execution = classifyToolExecution(
|
|
3340
|
-
item.toolName,
|
|
3341
|
-
item.toolArgs,
|
|
3342
|
-
item.result,
|
|
3343
|
-
item.error || '',
|
|
3344
|
-
);
|
|
3345
|
-
execution.input = item.toolArgs;
|
|
3346
|
-
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
3347
|
-
toolExecutions.push(execution);
|
|
3348
|
-
this.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
|
|
3349
|
-
if (item.error) failedStepCount += 1;
|
|
3350
|
-
const modelPayload = compactPayloadForModel(item.toolName, item.result);
|
|
3351
|
-
const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
|
|
3352
|
-
const toolMessage = {
|
|
3353
|
-
role: 'tool',
|
|
3354
|
-
name: item.toolName,
|
|
3355
|
-
tool_call_id: item.toolCall.id,
|
|
3356
|
-
content: compactToolResult(item.toolName, item.toolArgs, modelPayload.result, {
|
|
3357
|
-
softLimit: toolResultLimits.softLimit,
|
|
3358
|
-
hardLimit: toolResultLimits.hardLimit,
|
|
3359
|
-
}),
|
|
3360
|
-
};
|
|
3361
|
-
messages.push(toolMessage);
|
|
3362
|
-
if (conversationId) {
|
|
3363
|
-
db.prepare(
|
|
3364
|
-
`INSERT INTO conversation_messages (
|
|
3365
|
-
conversation_id, role, content, tool_call_id, name
|
|
3366
|
-
) VALUES (?, 'tool', ?, ?, ?)`
|
|
3367
|
-
).run(conversationId, toolMessage.content, item.toolCall.id, item.toolName);
|
|
3368
|
-
}
|
|
3369
|
-
}
|
|
3370
|
-
this.persistRunMetadata(runId, {
|
|
3371
|
-
evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
|
|
3372
|
-
subagentState: this.listSubagents(runId),
|
|
3373
|
-
deliverableArtifacts,
|
|
3374
|
-
compactionMetrics: compactionMetrics.slice(-20),
|
|
3375
|
-
});
|
|
3376
|
-
this.updateRunProgress(runId, {
|
|
3377
|
-
currentPhase: 'idle',
|
|
3378
|
-
currentStep: null,
|
|
3379
|
-
currentTool: null,
|
|
3380
|
-
currentStepStartedAt: null,
|
|
3381
|
-
}, {
|
|
3382
|
-
verified: true,
|
|
3383
|
-
});
|
|
3384
|
-
continue;
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
for (const toolCall of response.toolCalls) {
|
|
3388
|
-
if (this.isRunStopped(runId)) break;
|
|
3389
|
-
stepIndex++;
|
|
3390
|
-
const stepId = uuidv4();
|
|
3391
|
-
const toolName = toolCall.function.name;
|
|
3392
|
-
const stepStartedAt = Date.now();
|
|
3393
|
-
let toolArgs;
|
|
3394
|
-
try {
|
|
3395
|
-
toolArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
3396
|
-
} catch {
|
|
3397
|
-
toolArgs = {};
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
// ── task_complete: AI explicitly signals the task is fully done ──
|
|
3401
|
-
// Trust the model — no separate judge LLM call needed.
|
|
3402
|
-
if (toolName === 'task_complete') {
|
|
3403
|
-
const finalMessage = String(toolArgs.message || '').trim();
|
|
3404
|
-
this.recordRunEvent(userId, runId, 'task_complete_signaled', {
|
|
3405
|
-
accepted: true,
|
|
3406
|
-
iteration,
|
|
3407
|
-
messageLength: finalMessage.length,
|
|
3408
|
-
}, { agentId });
|
|
3409
|
-
console.info(
|
|
3410
|
-
`[Run ${shortenRunId(runId)}] task_complete accepted at iteration=${iteration}`
|
|
3411
|
-
);
|
|
3412
|
-
lastContent = finalMessage;
|
|
3413
|
-
directAnswerEligible = true;
|
|
3414
|
-
break;
|
|
3415
|
-
}
|
|
3416
|
-
|
|
3417
|
-
const repetitionGuard = this.getRunMeta(runId)?.repetitionGuard;
|
|
3418
|
-
if (repetitionGuard?.shouldBlock(toolName, toolArgs)) {
|
|
3419
|
-
const blockedResult = {
|
|
3420
|
-
tool: toolName,
|
|
3421
|
-
status: 'blocked',
|
|
3422
|
-
reason: 'The same tool call already returned an unchanged result twice. Change the approach or complete with the available evidence.',
|
|
3423
|
-
};
|
|
3424
|
-
messages.push({
|
|
3425
|
-
role: 'tool',
|
|
3426
|
-
name: toolName,
|
|
3427
|
-
tool_call_id: toolCall.id,
|
|
3428
|
-
content: JSON.stringify(blockedResult),
|
|
3429
|
-
});
|
|
3430
|
-
this.recordRunEvent(userId, runId, 'repetition_blocked', {
|
|
3431
|
-
toolName,
|
|
3432
|
-
toolArgs,
|
|
3433
|
-
}, { agentId });
|
|
3434
|
-
this.emit(userId, 'run:tool_end', {
|
|
3435
|
-
runId,
|
|
3436
|
-
toolName,
|
|
3437
|
-
status: 'blocked',
|
|
3438
|
-
result: blockedResult,
|
|
3439
|
-
});
|
|
3440
|
-
messages.push({
|
|
3441
|
-
role: 'system',
|
|
3442
|
-
content: 'The repeated call was blocked because it made no progress. Use a different tool, change the arguments, or finish with a truthful result.',
|
|
3443
|
-
});
|
|
3444
|
-
continue;
|
|
3445
|
-
}
|
|
3446
|
-
|
|
3447
|
-
// ── before_tool_call hook ──
|
|
3448
|
-
// Plugins can block a tool call (e.g. security policy) or mutate args.
|
|
3449
|
-
if (globalHooks.has('before_tool_call')) {
|
|
3450
|
-
const hookCtx = { toolName, toolArgs, runId, userId, agentId, iteration };
|
|
3451
|
-
const hookResult = await globalHooks.run('before_tool_call', hookCtx);
|
|
3452
|
-
if (hookResult.block) {
|
|
3453
|
-
const blockReason = hookResult.reason || 'Blocked by policy.';
|
|
3454
|
-
const blockedBy = hookResult.blocked_by || 'policy';
|
|
3455
|
-
console.warn(`[Run ${shortenRunId(runId)}] before_tool_call hook blocked tool=${toolName} reason="${blockReason}"`);
|
|
3456
|
-
messages.push({
|
|
3457
|
-
role: 'tool',
|
|
3458
|
-
name: toolName,
|
|
3459
|
-
tool_call_id: toolCall.id,
|
|
3460
|
-
content: JSON.stringify({ tool: toolName, status: 'blocked', reason: blockReason, blocked_by: blockedBy }),
|
|
3461
|
-
});
|
|
3462
|
-
continue;
|
|
3463
|
-
}
|
|
3464
|
-
if (hookResult.toolArgs) toolArgs = hookResult.toolArgs;
|
|
3465
|
-
}
|
|
3466
|
-
|
|
3467
|
-
db.prepare('INSERT INTO agent_steps (id, run_id, step_index, type, description, status, tool_name, tool_input, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime(\'now\'))')
|
|
3468
|
-
.run(stepId, runId, stepIndex, this.getStepType(toolName), `${toolName}: ${JSON.stringify(toolArgs).slice(0, 200)} `, 'running', toolName, JSON.stringify(toolArgs));
|
|
3469
|
-
this.updateRunProgress(runId, {
|
|
3470
|
-
currentPhase: 'tool',
|
|
3471
|
-
currentStep: stepId,
|
|
3472
|
-
currentTool: toolName,
|
|
3473
|
-
currentStepStartedAt: isoNow(),
|
|
3474
|
-
}, {
|
|
3475
|
-
stepId,
|
|
3476
|
-
});
|
|
3477
|
-
|
|
3478
|
-
this.emit(userId, 'run:tool_start', {
|
|
3479
|
-
runId, stepId, stepIndex, toolName, toolArgs,
|
|
3480
|
-
type: this.getStepType(toolName)
|
|
3481
|
-
});
|
|
3482
|
-
this.recordRunEvent(userId, runId, 'tool_started', {
|
|
3483
|
-
stepIndex,
|
|
3484
|
-
toolName,
|
|
3485
|
-
toolArgs,
|
|
3486
|
-
type: this.getStepType(toolName),
|
|
3487
|
-
}, { agentId, stepId });
|
|
3488
|
-
console.info(
|
|
3489
|
-
`[Run ${shortenRunId(runId)}] step=${stepIndex} start tool=${toolName} args=${summarizeForLog(toolArgs)}`
|
|
3490
|
-
);
|
|
3491
|
-
|
|
3492
|
-
let toolResult;
|
|
3493
|
-
let toolErrorMessage = '';
|
|
3494
|
-
try {
|
|
3495
|
-
toolResult = await this.executeTool(toolName, toolArgs, {
|
|
3496
|
-
userId,
|
|
3497
|
-
runId,
|
|
3498
|
-
agentId,
|
|
3499
|
-
app,
|
|
3500
|
-
triggerType,
|
|
3501
|
-
triggerSource,
|
|
3502
|
-
conversationId,
|
|
3503
|
-
source: options.source || null,
|
|
3504
|
-
chatId: options.chatId || null,
|
|
3505
|
-
taskId: options.taskId || null,
|
|
3506
|
-
widgetId: options.widgetId || null,
|
|
3507
|
-
deliveryState: options.deliveryState || null,
|
|
3508
|
-
allowMultipleProactiveMessages: options.allowMultipleProactiveMessages === true,
|
|
3509
|
-
allowExternalSideEffects: options.allowExternalSideEffects === true,
|
|
3510
|
-
});
|
|
3511
|
-
this.detachProcessFromRun(runId, toolResult?.pid);
|
|
3512
|
-
toolErrorMessage = inferToolFailureMessage(toolName, toolResult);
|
|
3513
|
-
if (toolErrorMessage) {
|
|
3514
|
-
failedStepCount++;
|
|
3515
|
-
}
|
|
3516
|
-
const screenshotPath = toolResult?.screenshotPath || null;
|
|
3517
|
-
const stepStatus = this.isRunStopped(runId) ? 'stopped' : (toolErrorMessage ? 'failed' : 'completed');
|
|
3518
|
-
db.prepare('UPDATE agent_steps SET status = ?, result = ?, error = ?, screenshot_path = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
3519
|
-
.run(stepStatus, JSON.stringify(toolResult).slice(0, 20000), toolErrorMessage || null, screenshotPath, stepId);
|
|
3520
|
-
if (toolErrorMessage) {
|
|
3521
|
-
this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: toolErrorMessage, result: toolResult, screenshotPath, status: stepStatus });
|
|
3522
|
-
this.recordRunEvent(userId, runId, 'tool_failed', {
|
|
3523
|
-
toolName,
|
|
3524
|
-
status: stepStatus,
|
|
3525
|
-
error: toolErrorMessage,
|
|
3526
|
-
durationMs: Date.now() - stepStartedAt,
|
|
3527
|
-
resultPreview: summarizeForLog(toolResult),
|
|
3528
|
-
}, { agentId, stepId });
|
|
3529
|
-
console.warn(
|
|
3530
|
-
`[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(toolErrorMessage, 160)}`
|
|
3531
|
-
);
|
|
3532
|
-
} else {
|
|
3533
|
-
this.emit(userId, 'run:tool_end', { runId, stepId, toolName, result: toolResult, screenshotPath, status: stepStatus });
|
|
3534
|
-
this.recordRunEvent(userId, runId, 'tool_completed', {
|
|
3535
|
-
toolName,
|
|
3536
|
-
status: stepStatus,
|
|
3537
|
-
durationMs: Date.now() - stepStartedAt,
|
|
3538
|
-
resultPreview: summarizeForLog(toolResult),
|
|
3539
|
-
}, { agentId, stepId });
|
|
3540
|
-
console.info(
|
|
3541
|
-
`[Run ${shortenRunId(runId)}] step=${stepIndex} done tool=${toolName} status=${stepStatus} durationMs=${Date.now() - stepStartedAt} result=${summarizeForLog(toolResult)}`
|
|
3542
|
-
);
|
|
3543
|
-
}
|
|
3544
|
-
} catch (err) {
|
|
3545
|
-
toolResult = { error: err.message };
|
|
3546
|
-
toolErrorMessage = String(err.message || 'Tool execution failed');
|
|
3547
|
-
failedStepCount++;
|
|
3548
|
-
this.detachProcessFromRun(runId, toolResult?.pid);
|
|
3549
|
-
db.prepare('UPDATE agent_steps SET status = ?, error = ?, completed_at = datetime(\'now\') WHERE id = ?')
|
|
3550
|
-
.run('failed', err.message, stepId);
|
|
3551
|
-
this.emit(userId, 'run:tool_end', { runId, stepId, toolName, error: err.message, status: 'failed' });
|
|
3552
|
-
this.recordRunEvent(userId, runId, 'tool_failed', {
|
|
3553
|
-
toolName,
|
|
3554
|
-
status: 'failed',
|
|
3555
|
-
error: err.message,
|
|
3556
|
-
durationMs: Date.now() - stepStartedAt,
|
|
3557
|
-
}, { agentId, stepId });
|
|
3558
|
-
console.warn(
|
|
3559
|
-
`[Run ${shortenRunId(runId)}] step=${stepIndex} failed tool=${toolName} durationMs=${Date.now() - stepStartedAt} error=${summarizeForLog(err.message, 160)}`
|
|
3560
|
-
);
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
|
|
3564
|
-
execution.input = toolArgs;
|
|
3565
|
-
repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
3566
|
-
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
3567
|
-
toolExecutions.push(execution);
|
|
3568
|
-
if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
|
|
3569
|
-
for (const artifact of execution.artifacts) {
|
|
3570
|
-
if (!deliverableArtifacts.some((existing) => (
|
|
3571
|
-
(existing.path && artifact.path && existing.path === artifact.path)
|
|
3572
|
-
|| (existing.uri && artifact.uri && existing.uri === artifact.uri)
|
|
3573
|
-
))) {
|
|
3574
|
-
deliverableArtifacts.push(artifact);
|
|
3575
|
-
this.recordRunEvent(userId, runId, 'deliverable_artifact_produced', {
|
|
3576
|
-
type: deliverableWorkflow.selection.type,
|
|
3577
|
-
toolName,
|
|
3578
|
-
artifact,
|
|
3579
|
-
}, { agentId, stepId });
|
|
3580
|
-
}
|
|
3581
|
-
}
|
|
3582
|
-
}
|
|
3583
|
-
this.persistRunMetadata(runId, {
|
|
3584
|
-
evidenceSources: [...new Set(toolExecutions.map((item) => item.evidenceSource).filter(Boolean))],
|
|
3585
|
-
subagentState: this.listSubagents(runId),
|
|
3586
|
-
deliverableArtifacts,
|
|
3587
|
-
compactionMetrics: compactionMetrics.slice(-20),
|
|
3588
|
-
});
|
|
3589
|
-
const modelPayload = compactPayloadForModel(toolName, toolResult);
|
|
3590
|
-
if (modelPayload.metrics?.applied) {
|
|
3591
|
-
const metric = {
|
|
3592
|
-
toolName,
|
|
3593
|
-
stepId,
|
|
3594
|
-
...modelPayload.metrics,
|
|
3595
|
-
createdAt: new Date().toISOString(),
|
|
3596
|
-
};
|
|
3597
|
-
compactionMetrics.push(metric);
|
|
3598
|
-
this.persistRunMetadata(runId, {
|
|
3599
|
-
compactionMetrics: compactionMetrics.slice(-20),
|
|
3600
|
-
});
|
|
3601
|
-
this.recordRunEvent(userId, runId, 'pre_model_compaction_applied', {
|
|
3602
|
-
toolName,
|
|
3603
|
-
metrics: modelPayload.metrics,
|
|
3604
|
-
}, { agentId, stepId });
|
|
3605
|
-
}
|
|
3606
|
-
|
|
3607
|
-
const toolResultLimits = resolveToolResultLimits(toolName, loopPolicy);
|
|
3608
|
-
const toolMessage = {
|
|
3609
|
-
role: 'tool',
|
|
3610
|
-
name: toolName,
|
|
3611
|
-
tool_call_id: toolCall.id,
|
|
3612
|
-
content: compactToolResult(toolName, toolArgs, modelPayload.result, {
|
|
3613
|
-
softLimit: toolResultLimits.softLimit,
|
|
3614
|
-
hardLimit: toolResultLimits.hardLimit,
|
|
3615
|
-
})
|
|
3616
|
-
};
|
|
3617
|
-
messages.push(toolMessage);
|
|
3618
|
-
if (toolName === 'activate_tools' && !toolErrorMessage) {
|
|
3619
|
-
tools = this.getActiveTools(runId);
|
|
3620
|
-
}
|
|
3621
|
-
|
|
3622
|
-
if (toolErrorMessage) {
|
|
3623
|
-
consecutiveToolFailures += 1;
|
|
3624
|
-
const currentRunMeta = this.getRunMeta(runId);
|
|
3625
|
-
trackErrorPattern(toolErrorMessage, currentRunMeta);
|
|
3626
|
-
const errorKey = normalizeErrorKey(toolErrorMessage);
|
|
3627
|
-
const errorCount = currentRunMeta?.errorPatterns?.get(errorKey) || 0;
|
|
3628
|
-
const patternGuide = buildErrorPatternGuidance(errorKey, errorCount);
|
|
3629
|
-
const alternativeTools = summarizeAvailableTools(tools, { exclude: toolName });
|
|
3630
|
-
messages.push({
|
|
3631
|
-
role: 'system',
|
|
3632
|
-
content: [
|
|
3633
|
-
`Tool "${toolName}" failed with error: ${summarizeForLog(toolErrorMessage, 240)}.`,
|
|
3634
|
-
'This tool failure is not, by itself, a user-facing blocker.',
|
|
3635
|
-
'Continue autonomously: retry with corrected arguments, try an alternative tool/path, or verify the outcome using other available tools.',
|
|
3636
|
-
alternativeTools ? `Other available tools in this run: ${alternativeTools}.` : '',
|
|
3637
|
-
patternGuide || '',
|
|
3638
|
-
'Only stop and tell the user you are blocked if the remaining issue truly requires an external dependency or user action outside this run.'
|
|
3639
|
-
].filter(Boolean).join(' ')
|
|
3640
|
-
});
|
|
3641
|
-
|
|
3642
|
-
if (consecutiveToolFailures >= loopPolicy.maxConsecutiveToolFailures) {
|
|
3643
|
-
messages.push({
|
|
3644
|
-
role: 'system',
|
|
3645
|
-
content: `There were ${consecutiveToolFailures} consecutive tool failures. Stop calling tools now and return a clear blocker response that summarizes attempted actions and concrete errors.`
|
|
3646
|
-
});
|
|
3647
|
-
break;
|
|
3648
|
-
}
|
|
3649
|
-
} else {
|
|
3650
|
-
consecutiveToolFailures = 0;
|
|
3651
|
-
// Output fingerprint guard: steer away from re-fetching data already seen.
|
|
3652
|
-
if (!toolErrorMessage) {
|
|
3653
|
-
const currentRunMeta = this.getRunMeta(runId);
|
|
3654
|
-
const fp = fingerprintOutput(toolName, toolResult);
|
|
3655
|
-
if (fp !== null && currentRunMeta?.seenOutputHashes) {
|
|
3656
|
-
const prior = currentRunMeta.seenOutputHashes.get(fp);
|
|
3657
|
-
if (prior) {
|
|
3658
|
-
messages.push({
|
|
3659
|
-
role: 'system',
|
|
3660
|
-
content: `DUPLICATE DATA: This response is identical to what "${prior.toolName}" returned in iteration ${prior.iteration}. You already have this information. Stop fetching and use what you have — proceed to the next concrete action.`,
|
|
3661
|
-
});
|
|
3662
|
-
} else {
|
|
3663
|
-
currentRunMeta.seenOutputHashes.set(fp, { toolName, iteration });
|
|
3664
|
-
// External state: persist large read results to disk so the
|
|
3665
|
-
// model can reference them after context compaction without
|
|
3666
|
-
// re-fetching. Only for significant payloads.
|
|
3667
|
-
const persistRaw = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult ?? '');
|
|
3668
|
-
if (persistRaw.length >= 1000 && runId) {
|
|
3669
|
-
const persistPath = `/tmp/run-${runId.slice(0, 8)}-${toolName}.json`;
|
|
3670
|
-
try {
|
|
3671
|
-
require('fs').writeFileSync(persistPath, persistRaw.slice(0, 40000));
|
|
3672
|
-
if (!currentRunMeta.persistedDataPaths) currentRunMeta.persistedDataPaths = [];
|
|
3673
|
-
if (!currentRunMeta.persistedDataPaths.includes(persistPath)) {
|
|
3674
|
-
currentRunMeta.persistedDataPaths.push(persistPath);
|
|
3675
|
-
messages.push({
|
|
3676
|
-
role: 'system',
|
|
3677
|
-
content: `Data from "${toolName}" (iteration ${iteration}) persisted to ${persistPath}. If context compacts and you need this data again, use execute_command with \`cat ${persistPath}\` instead of re-fetching.`,
|
|
3678
|
-
});
|
|
3679
|
-
}
|
|
3680
|
-
} catch { /* non-fatal — disk full or permissions */ }
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
|
|
3687
|
-
if (toolName === 'send_interim_update') {
|
|
3688
|
-
messages.push({
|
|
3689
|
-
role: 'system',
|
|
3690
|
-
content: 'An interim user-visible update was already sent. Do not later output meta commentary about having already replied. When you have the final answer, give the answer itself. If you need to deliver that final answer to the user in messaging, use send_message.'
|
|
3691
|
-
});
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
if (toolName === 'execute_command' && (toolResult?.timedOut || toolResult?.killed)) {
|
|
3695
|
-
messages.push({
|
|
3696
|
-
role: 'system',
|
|
3697
|
-
content: 'The previous shell command did not finish cleanly. Keep working until you rerun it with enough time or verify the requested outcome with follow-up commands.'
|
|
3698
|
-
});
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
if (
|
|
3702
|
-
toolName === 'execute_command'
|
|
3703
|
-
&& toolResult?.exitCode !== undefined
|
|
3704
|
-
&& toolResult.exitCode !== 0
|
|
3705
|
-
) {
|
|
3706
|
-
messages.push({
|
|
3707
|
-
role: 'system',
|
|
3708
|
-
content: 'The previous shell command exited non-zero. Treat its output as partial evidence only. If it chained multiple shell segments, later segments may not have run. Do not summarize missing sections as observed facts; rerun or verify them separately first.'
|
|
3709
|
-
});
|
|
3710
|
-
}
|
|
3711
|
-
|
|
3712
|
-
if (conversationId) {
|
|
3713
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tool_call_id, name) VALUES (?, ?, ?, ?, ?)')
|
|
3714
|
-
.run(conversationId, 'tool', toolMessage.content, toolCall.id, toolName);
|
|
3715
|
-
}
|
|
3716
|
-
|
|
3717
|
-
this.updateRunProgress(runId, {
|
|
3718
|
-
currentPhase: 'idle',
|
|
3719
|
-
currentStep: null,
|
|
3720
|
-
currentTool: null,
|
|
3721
|
-
currentStepStartedAt: null,
|
|
3722
|
-
}, {
|
|
3723
|
-
verified: true,
|
|
3724
|
-
stepId,
|
|
3725
|
-
});
|
|
3726
|
-
|
|
3727
|
-
const runMeta = this.activeRuns.get(runId);
|
|
3728
|
-
if (runMeta) {
|
|
3729
|
-
runMeta.lastToolName = toolName;
|
|
3730
|
-
runMeta.lastToolTarget = toolName === 'send_message' ? toolArgs.to : null;
|
|
3731
|
-
if (toolName === 'save_widget_snapshot' && !toolErrorMessage) {
|
|
3732
|
-
runMeta.widgetSnapshotSaved = true;
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
|
|
3736
|
-
if (toolName === 'save_widget_snapshot' && !toolErrorMessage) {
|
|
3737
|
-
lastContent = 'Widget snapshot updated.';
|
|
3738
|
-
break;
|
|
3739
|
-
}
|
|
3740
|
-
|
|
3741
|
-
if (runMeta?.terminalInterim) {
|
|
3742
|
-
break;
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
// Update analysis-paralysis counter after each iteration's tool calls.
|
|
3747
|
-
// Resets to 0 when any progress tool was called; otherwise increments.
|
|
3748
|
-
if (!directAnswerEligible && response?.toolCalls?.length > 0
|
|
3749
|
-
&& (analysis.mode === 'execute' || analysis.mode === 'plan_execute')) {
|
|
3750
|
-
const iterMeta = this.getRunMeta(runId);
|
|
3751
|
-
if (iterMeta) {
|
|
3752
|
-
const calledProgress = response.toolCalls.some((tc) => isProgressTool(tc.function?.name || ''));
|
|
3753
|
-
iterMeta.consecutiveReadOnlyIterations = calledProgress
|
|
3754
|
-
? 0
|
|
3755
|
-
: (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
3756
|
-
}
|
|
3757
|
-
}
|
|
3758
|
-
|
|
3759
|
-
if (this.isRunStopped(runId)) break;
|
|
3760
|
-
if (this.getRunMeta(runId)?.terminalInterim) break;
|
|
3761
|
-
if (this.getRunMeta(runId)?.widgetSnapshotSaved) break;
|
|
3762
|
-
if (!this.activeRuns.has(runId)) break;
|
|
3763
|
-
}
|
|
3764
|
-
|
|
3765
|
-
if (this.isRunStopped(runId)) {
|
|
3766
|
-
db.prepare('UPDATE agent_runs SET status = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
3767
|
-
.run('stopped', runId);
|
|
3768
|
-
console.warn(
|
|
3769
|
-
`[Run ${shortenRunId(runId)}] stopped trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
3770
|
-
);
|
|
3771
|
-
this.stopMessagingProgressSupervisor(runId);
|
|
3772
|
-
this.activeRuns.delete(runId);
|
|
3773
|
-
this.emit(userId, 'run:stopped', { runId, triggerSource });
|
|
3774
|
-
this.recordRunEvent(userId, runId, 'run_stopped', {
|
|
3775
|
-
triggerSource,
|
|
3776
|
-
totalTokens,
|
|
3777
|
-
iterations: iteration,
|
|
3778
|
-
}, { agentId });
|
|
3779
|
-
return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
|
|
3780
|
-
}
|
|
3781
|
-
|
|
3782
|
-
const runMeta = this.activeRuns.get(runId);
|
|
3783
|
-
if (runMeta?.terminalInterim) {
|
|
3784
|
-
lastContent = '';
|
|
3785
|
-
}
|
|
3786
|
-
if (runMeta?.widgetSnapshotSaved && !lastContent) {
|
|
3787
|
-
lastContent = 'Widget snapshot updated.';
|
|
3788
|
-
}
|
|
3789
|
-
const messagingSent = runMeta?.messagingSent || false;
|
|
3790
|
-
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
3791
|
-
|
|
3792
|
-
if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
|
|
3793
|
-
// Simplified blank reply recovery: one model call with direct instruction,
|
|
3794
|
-
// then fall back to a deterministic message. No multi-attempt LLM loop.
|
|
3795
|
-
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery model=${model}`);
|
|
3796
|
-
let recoveredTokens = 0;
|
|
3797
|
-
try {
|
|
3798
|
-
const recoveryResponse = await withModelCallTimeout(
|
|
3799
|
-
provider.chat(
|
|
3800
|
-
sanitizeConversationMessages([
|
|
3801
|
-
...messages,
|
|
3802
|
-
{
|
|
3803
|
-
role: 'system',
|
|
3804
|
-
content: buildBlankMessagingReplyPrompt(1, options?.source || null)
|
|
3805
|
-
}
|
|
3806
|
-
]),
|
|
3807
|
-
[],
|
|
3808
|
-
{
|
|
3809
|
-
model,
|
|
3810
|
-
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
3811
|
-
}
|
|
3812
|
-
),
|
|
3813
|
-
options,
|
|
3814
|
-
'Blank messaging reply recovery',
|
|
3815
|
-
);
|
|
3816
|
-
recoveredTokens = recoveryResponse.usage?.totalTokens || 0;
|
|
3817
|
-
lastContent = sanitizeModelOutput(recoveryResponse.content || '', { model });
|
|
3818
|
-
} catch (recoverErr) {
|
|
3819
|
-
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`);
|
|
3820
|
-
}
|
|
3821
|
-
totalTokens += recoveredTokens;
|
|
3822
|
-
if (!normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
3823
|
-
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
3824
|
-
}
|
|
3825
|
-
if (normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
3826
|
-
messages.push({ role: 'assistant', content: lastContent });
|
|
3827
|
-
if (conversationId) {
|
|
3828
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
3829
|
-
.run(conversationId, 'assistant', lastContent, recoveredTokens);
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
|
|
3834
|
-
if (
|
|
3835
|
-
!normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
3836
|
-
&& !messagingSent
|
|
3837
|
-
&& runMeta?.widgetSnapshotSaved !== true
|
|
3838
|
-
) {
|
|
3839
|
-
const explicitNoResponse = (
|
|
3840
|
-
runMeta?.noResponse === true
|
|
3841
|
-
|| options.deliveryState?.noResponse === true
|
|
3842
|
-
);
|
|
3843
|
-
if (
|
|
3844
|
-
(triggerSource === 'schedule' || triggerSource === 'tasks')
|
|
3845
|
-
&& !explicitNoResponse
|
|
3846
|
-
) {
|
|
3847
|
-
throw new Error(
|
|
3848
|
-
'Background run ended without producing a result or an explicit no-response decision.',
|
|
3849
|
-
);
|
|
3850
|
-
}
|
|
3851
|
-
if (iteration >= maxIterations) {
|
|
3852
|
-
// Grace call: budget exhausted but no content yet.
|
|
3853
|
-
// Strip tools and ask the model to summarise what it accomplished.
|
|
3854
|
-
// Mirrors the Hermes handle_max_iterations() pattern.
|
|
3855
|
-
console.warn(`[Run ${shortenRunId(runId)}] iteration_limit runId=${shortenRunId(runId)} — making grace call`);
|
|
3856
|
-
try {
|
|
3857
|
-
const graceMessages = sanitizeConversationMessages([
|
|
3858
|
-
...messages,
|
|
3859
|
-
{
|
|
3860
|
-
role: 'user',
|
|
3861
|
-
content: 'You have reached the maximum number of tool-calling iterations allowed. Please provide a final response summarising what you found and accomplished so far, without calling any more tools.',
|
|
3862
|
-
},
|
|
3863
|
-
]);
|
|
3864
|
-
const graceResponse = await withModelCallTimeout(
|
|
3865
|
-
provider.chat(graceMessages, [], {
|
|
3866
|
-
model,
|
|
3867
|
-
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
3868
|
-
}),
|
|
3869
|
-
options,
|
|
3870
|
-
`Grace call after ${maxIterations} iterations`,
|
|
3871
|
-
);
|
|
3872
|
-
totalTokens += graceResponse.usage?.totalTokens || 0;
|
|
3873
|
-
lastContent = sanitizeModelOutput(graceResponse.content || '', { model });
|
|
3874
|
-
if (lastContent) {
|
|
3875
|
-
messages.push({ role: 'assistant', content: lastContent });
|
|
3876
|
-
if (conversationId) {
|
|
3877
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
3878
|
-
.run(conversationId, 'assistant', lastContent, graceResponse.usage?.totalTokens || 0);
|
|
3879
|
-
}
|
|
3880
|
-
}
|
|
3881
|
-
} catch (graceErr) {
|
|
3882
|
-
console.warn(`[Run ${shortenRunId(runId)}] grace call failed: ${graceErr?.message}`);
|
|
3883
|
-
}
|
|
3884
|
-
if (!normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
3885
|
-
throw new Error(`Iteration limit reached before explicit completion after ${maxIterations} iterations.`);
|
|
3886
|
-
}
|
|
3887
|
-
}
|
|
3888
|
-
if (stepIndex > 0 && !lastToolWasMessaging && iteration < maxIterations) {
|
|
3889
|
-
throw new Error('Run ended without an explicit completion or blocker reply.');
|
|
3890
|
-
}
|
|
3891
|
-
}
|
|
3892
|
-
|
|
3893
|
-
const sentMessageText = joinSentMessages(runMeta?.sentMessages);
|
|
3894
|
-
const normalizedLastContent = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
3895
|
-
let finalResponseText = messagingSent
|
|
3896
|
-
? (sentMessageText || (normalizedLastContent ? lastContent.trim() : ''))
|
|
3897
|
-
: (normalizedLastContent ? lastContent.trim() : sentMessageText);
|
|
3898
|
-
const lastFinalDeliveryMessage = normalizeOutgoingMessage(
|
|
3899
|
-
runMeta?.lastSentMessage
|
|
3900
|
-
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
3901
|
-
|| '',
|
|
3902
|
-
options?.source || null
|
|
3903
|
-
);
|
|
3904
|
-
|
|
3905
|
-
if (
|
|
3906
|
-
options.skipVerifier !== true
|
|
3907
|
-
&& shouldRunVerifier({
|
|
3908
|
-
analysis,
|
|
3909
|
-
toolExecutions,
|
|
3910
|
-
finalReply: finalResponseText,
|
|
3911
|
-
})) {
|
|
3912
|
-
const verificationResult = await runWithModelFallback('final verification', () => this.verifyFinalResponse({
|
|
3913
|
-
provider,
|
|
3914
|
-
providerName,
|
|
3915
|
-
model,
|
|
3916
|
-
messages,
|
|
3917
|
-
analysis,
|
|
3918
|
-
tools,
|
|
3919
|
-
toolExecutions,
|
|
3920
|
-
finalReply: finalResponseText,
|
|
3921
|
-
options: { ...options, runId, userId, agentId },
|
|
3922
|
-
}));
|
|
3923
|
-
totalTokens += verificationResult.usage || 0;
|
|
3924
|
-
verification = verificationResult.verification;
|
|
3925
|
-
if (verification.final_reply) {
|
|
3926
|
-
finalResponseText = verification.final_reply;
|
|
3927
|
-
lastContent = verification.final_reply;
|
|
3928
|
-
}
|
|
3929
|
-
|
|
3930
|
-
stepIndex += 1;
|
|
3931
|
-
const verificationStepId = uuidv4();
|
|
3932
|
-
db.prepare(`INSERT INTO agent_steps
|
|
3933
|
-
(id, run_id, step_index, type, description, status, result, started_at, completed_at)
|
|
3934
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`)
|
|
3935
|
-
.run(
|
|
3936
|
-
verificationStepId,
|
|
3937
|
-
runId,
|
|
3938
|
-
stepIndex,
|
|
3939
|
-
'verification',
|
|
3940
|
-
'Evidence verification',
|
|
3941
|
-
verification.status === 'verified' ? 'completed' : 'failed',
|
|
3942
|
-
JSON.stringify(verification).slice(0, 20000)
|
|
3943
|
-
);
|
|
3944
|
-
this.persistRunMetadata(runId, {
|
|
3945
|
-
verification,
|
|
3946
|
-
evidenceSources: verificationResult.evidenceSources,
|
|
3947
|
-
});
|
|
3948
|
-
this.emit(userId, 'run:verification', {
|
|
3949
|
-
runId,
|
|
3950
|
-
...verification,
|
|
3951
|
-
evidenceSources: verificationResult.evidenceSources,
|
|
3952
|
-
});
|
|
3953
|
-
}
|
|
3954
|
-
|
|
3955
|
-
if (deliverableWorkflow && deliverablePlan) {
|
|
3956
|
-
this.recordRunEvent(userId, runId, 'deliverable_validation_started', {
|
|
3957
|
-
type: deliverableWorkflow.selection.type,
|
|
3958
|
-
artifactCount: deliverableArtifacts.length,
|
|
3959
|
-
}, { agentId });
|
|
3960
|
-
const validationResult = await validateDeliverableExecution({
|
|
3961
|
-
workflow: deliverableWorkflow.workflow,
|
|
3962
|
-
request: deliverableWorkflow.request,
|
|
3963
|
-
plan: deliverablePlan,
|
|
3964
|
-
finalReply: finalResponseText,
|
|
3965
|
-
artifacts: deliverableArtifacts,
|
|
3966
|
-
toolExecutions,
|
|
3967
|
-
runId,
|
|
3968
|
-
});
|
|
3969
|
-
deliverableValidation = validationResult.validation;
|
|
3970
|
-
this.persistRunMetadata(runId, {
|
|
3971
|
-
deliverable: validationResult.result,
|
|
3972
|
-
});
|
|
3973
|
-
if (deliverableValidation.status !== 'passed') {
|
|
3974
|
-
this.recordRunEvent(userId, runId, 'deliverable_validation_failed', {
|
|
3975
|
-
type: deliverableWorkflow.selection.type,
|
|
3976
|
-
errors: deliverableValidation.errors,
|
|
3977
|
-
warnings: deliverableValidation.warnings,
|
|
3978
|
-
}, { agentId });
|
|
3979
|
-
throw new DeliverableValidationError(
|
|
3980
|
-
deliverableValidation.summary || `Deliverable validation failed for ${deliverableWorkflow.selection.type}.`,
|
|
3981
|
-
{
|
|
3982
|
-
validation: deliverableValidation,
|
|
3983
|
-
result: validationResult.result,
|
|
3984
|
-
},
|
|
3985
|
-
);
|
|
3986
|
-
}
|
|
3987
|
-
await this.persistDeliverableMemory(userId, runId, agentId, validationResult.result);
|
|
3988
|
-
this.recordRunEvent(userId, runId, 'deliverable_completed', {
|
|
3989
|
-
type: deliverableWorkflow.selection.type,
|
|
3990
|
-
artifactCount: validationResult.result.artifacts.length,
|
|
3991
|
-
summary: validationResult.result.summary,
|
|
3992
|
-
}, { agentId });
|
|
3993
|
-
}
|
|
3994
|
-
|
|
3995
|
-
db.prepare('UPDATE agent_runs SET status = ?, total_tokens = ?, final_response = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
3996
|
-
.run('completed', totalTokens, finalResponseText || null, runId);
|
|
3997
|
-
|
|
3998
|
-
if (conversationId) {
|
|
3999
|
-
db.prepare('UPDATE conversations SET total_tokens = total_tokens + ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
4000
|
-
.run(totalTokens, conversationId);
|
|
4001
|
-
if (options.skipConversationMaintenance !== true) {
|
|
4002
|
-
refreshConversationSummary(conversationId, provider, model, historyWindow).catch((err) => {
|
|
4003
|
-
console.error('[AI] Conversation summary refresh failed:', err.message);
|
|
4004
|
-
});
|
|
4005
|
-
}
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
await this.persistPromptMetrics(runId, {
|
|
4009
|
-
...promptMetrics,
|
|
4010
|
-
finalTotalTokens: totalTokens
|
|
4011
|
-
});
|
|
4012
|
-
|
|
4013
|
-
await this.persistRunContext(userId, {
|
|
4014
|
-
triggerSource,
|
|
4015
|
-
runTitle,
|
|
4016
|
-
userMessage,
|
|
4017
|
-
lastContent: finalResponseText,
|
|
4018
|
-
stepIndex,
|
|
4019
|
-
skipPersistence: options.skipRunContextPersistence === true
|
|
4020
|
-
});
|
|
4021
|
-
|
|
4022
|
-
// Fallback: if this was a messaging-triggered run and no final delivery
|
|
4023
|
-
// was already sent in this run, auto-send the final assistant text.
|
|
4024
|
-
// Interim progress updates do not suppress this final delivery.
|
|
4025
|
-
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
4026
|
-
if (this.shouldSendMessagingFinalFallback(runMeta, lastContent || '', options.source) && !lastFinalDeliveryMessage) {
|
|
4027
|
-
await this.deliverMessagingFinalFallback({
|
|
4028
|
-
runId,
|
|
4029
|
-
userId,
|
|
4030
|
-
agentId,
|
|
4031
|
-
platform: options.source,
|
|
4032
|
-
chatId: options.chatId,
|
|
4033
|
-
content: lastContent || '',
|
|
4034
|
-
});
|
|
4035
|
-
}
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
if (conversationId && options.skipConversationMaintenance !== true) {
|
|
4039
|
-
await this.refreshConversationState({
|
|
4040
|
-
conversationId,
|
|
4041
|
-
runId,
|
|
4042
|
-
provider,
|
|
4043
|
-
providerName,
|
|
4044
|
-
model,
|
|
4045
|
-
finalReply: finalResponseText,
|
|
4046
|
-
analysis,
|
|
4047
|
-
verification,
|
|
4048
|
-
historyWindow,
|
|
4049
|
-
options: { ...options, userId, agentId },
|
|
4050
|
-
}).catch((err) => {
|
|
4051
|
-
console.error('[AI] Conversation working state refresh failed:', err.message);
|
|
4052
|
-
});
|
|
4053
|
-
}
|
|
4054
|
-
|
|
4055
|
-
console.info(
|
|
4056
|
-
`[Run ${shortenRunId(runId)}] completed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} durationMs=${runMeta?.startedAt ? Date.now() - runMeta.startedAt : 0} finalResponse=${finalResponseText ? 'yes' : 'no'} sentMessages=${runMeta?.sentMessages?.length || 0}`
|
|
4057
|
-
);
|
|
4058
|
-
this.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
4059
|
-
this.stopMessagingProgressSupervisor(runId);
|
|
4060
|
-
this.activeRuns.delete(runId);
|
|
4061
|
-
this.emit(userId, 'run:complete', {
|
|
4062
|
-
runId,
|
|
4063
|
-
content: lastContent,
|
|
4064
|
-
totalTokens,
|
|
4065
|
-
iterations: iteration,
|
|
4066
|
-
triggerSource,
|
|
4067
|
-
executionMode: analysis?.mode || 'execute',
|
|
4068
|
-
verificationStatus: verification?.status || 'skipped',
|
|
4069
|
-
});
|
|
4070
|
-
this.recordRunEvent(userId, runId, 'run_completed', {
|
|
4071
|
-
contentPreview: String(finalResponseText || lastContent || '').slice(0, 240),
|
|
4072
|
-
totalTokens,
|
|
4073
|
-
iterations: iteration,
|
|
4074
|
-
triggerSource,
|
|
4075
|
-
executionMode: analysis?.mode || 'execute',
|
|
4076
|
-
verificationStatus: verification?.status || 'skipped',
|
|
4077
|
-
}, { agentId });
|
|
4078
|
-
// ── on_loop_end hook ──
|
|
4079
|
-
// Fire-and-forget: plugins can use this for self-improvement, memory
|
|
4080
|
-
// consolidation, analytics, or other post-run housekeeping.
|
|
4081
|
-
if (globalHooks.has('on_loop_end')) {
|
|
4082
|
-
globalHooks.run('on_loop_end', {
|
|
4083
|
-
userId, runId, agentId, status: 'completed',
|
|
4084
|
-
iterations: iteration, totalTokens,
|
|
4085
|
-
taskAnalysis: analysis,
|
|
4086
|
-
finalContent: finalResponseText,
|
|
4087
|
-
}).catch(() => {});
|
|
4088
|
-
}
|
|
4089
|
-
if (this.learningManager) {
|
|
4090
|
-
try {
|
|
4091
|
-
const learningSteps = db.prepare(
|
|
4092
|
-
`SELECT tool_name, tool_input, result, status
|
|
4093
|
-
FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC`
|
|
4094
|
-
).all(runId);
|
|
4095
|
-
this.learningManager.maybeCaptureDraft({
|
|
4096
|
-
userId,
|
|
4097
|
-
agentId,
|
|
4098
|
-
runId,
|
|
4099
|
-
triggerSource,
|
|
4100
|
-
triggerType,
|
|
4101
|
-
task: userMessage,
|
|
4102
|
-
title: runTitle,
|
|
4103
|
-
finalContent: finalResponseText || lastContent,
|
|
4104
|
-
steps: learningSteps,
|
|
4105
|
-
});
|
|
4106
|
-
} catch (learningError) {
|
|
4107
|
-
console.warn('[Engine] Skill reflection failed:', learningError.message);
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
|
|
4111
|
-
return { runId, content: lastContent, totalTokens, iterations: iteration, status: 'completed' };
|
|
4112
|
-
} catch (err) {
|
|
4113
|
-
if (this.isRunStopped(runId)) {
|
|
4114
|
-
db.prepare('UPDATE agent_runs SET status = ?, updated_at = datetime(\'now\'), completed_at = datetime(\'now\') WHERE id = ?')
|
|
4115
|
-
.run('stopped', runId);
|
|
4116
|
-
console.warn(
|
|
4117
|
-
`[Run ${shortenRunId(runId)}] stopped trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
4118
|
-
);
|
|
4119
|
-
this.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
4120
|
-
this.stopMessagingProgressSupervisor(runId);
|
|
4121
|
-
this.activeRuns.delete(runId);
|
|
4122
|
-
this.emit(userId, 'run:stopped', { runId, triggerSource });
|
|
4123
|
-
this.recordRunEvent(userId, runId, 'run_stopped', {
|
|
4124
|
-
triggerSource,
|
|
4125
|
-
totalTokens,
|
|
4126
|
-
iterations: iteration,
|
|
4127
|
-
}, { agentId });
|
|
4128
|
-
return { runId, content: '', totalTokens, iterations: iteration, status: 'stopped' };
|
|
4129
|
-
}
|
|
4130
|
-
|
|
4131
|
-
const runMeta = this.activeRuns.get(runId);
|
|
4132
|
-
const retryCount = Number(options.messagingAutonomousRetryCount || 0);
|
|
4133
|
-
// Rate-limit errors (429) must not trigger messaging retries: the model
|
|
4134
|
-
// won't be available in the milliseconds between retries, so spawning new
|
|
4135
|
-
// runs just compounds the rate-limit pressure with no benefit.
|
|
4136
|
-
const isRateLimitError = /429|rate.?limit|free-models-per/i.test(String(err?.message || ''));
|
|
4137
|
-
const canRetryMessagingRun = (
|
|
4138
|
-
triggerSource === 'messaging'
|
|
4139
|
-
&& options.source
|
|
4140
|
-
&& options.chatId
|
|
4141
|
-
&& runMeta?.finalDeliverySent !== true
|
|
4142
|
-
&& runMeta?.messagingSent !== true
|
|
4143
|
-
&& err?.disableAutonomousRetry !== true
|
|
4144
|
-
&& !isRateLimitError
|
|
4145
|
-
&& retryCount < this.getMessagingRetryLimit(maxIterations)
|
|
4146
|
-
);
|
|
4147
|
-
|
|
4148
|
-
if (canRetryMessagingRun) {
|
|
4149
|
-
const recoveryContext = buildAutonomousRecoveryContext({
|
|
4150
|
-
err,
|
|
4151
|
-
toolExecutions,
|
|
4152
|
-
tools,
|
|
4153
|
-
userMessage,
|
|
4154
|
-
visibleMessageSent: Boolean(
|
|
4155
|
-
runMeta?.lastSentMessage
|
|
4156
|
-
|| runMeta?.lastInterimMessage
|
|
4157
|
-
|| runMeta?.messagingSent === true
|
|
4158
|
-
),
|
|
4159
|
-
});
|
|
4160
|
-
db.prepare('UPDATE agent_runs SET status = ?, error = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
4161
|
-
.run('retrying', err.message, runId);
|
|
4162
|
-
console.warn(
|
|
4163
|
-
`[Run ${shortenRunId(runId)}] retrying_messaging_attempt=${retryCount + 1} reason=${summarizeForLog(err.message, 140)}`
|
|
4164
|
-
);
|
|
4165
|
-
this.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
4166
|
-
this.stopMessagingProgressSupervisor(runId);
|
|
4167
|
-
this.activeRuns.delete(runId);
|
|
4168
|
-
this.emit(userId, 'run:interim', {
|
|
4169
|
-
runId,
|
|
4170
|
-
message: 'Retrying internally after a transient failure.',
|
|
4171
|
-
phase: 'retrying'
|
|
4172
|
-
});
|
|
4173
|
-
|
|
4174
|
-
const retryOptions = {
|
|
4175
|
-
...options,
|
|
4176
|
-
messagingAutonomousRetryCount: retryCount + 1,
|
|
4177
|
-
messagingRetryState: {
|
|
4178
|
-
lastFinalMessage: String(runMeta?.lastSentMessage || options?.messagingRetryState?.lastFinalMessage || '').trim(),
|
|
4179
|
-
explicitMessageSent: runMeta?.explicitMessageSent === true || options?.messagingRetryState?.explicitMessageSent === true,
|
|
4180
|
-
interimHistory: cloneInterimHistory([
|
|
4181
|
-
...(Array.isArray(options?.messagingRetryState?.interimHistory) ? options.messagingRetryState.interimHistory : []),
|
|
4182
|
-
...(Array.isArray(runMeta?.interimMessages) ? runMeta.interimMessages : []),
|
|
4183
|
-
]),
|
|
4184
|
-
goalContract: mergeGoalContracts(
|
|
4185
|
-
options?.messagingRetryState?.goalContract || null,
|
|
4186
|
-
runMeta?.goalContract || null,
|
|
4187
|
-
),
|
|
4188
|
-
lastUserVisibleUpdateAt: runMeta?.progressLedger?.lastUserVisibleUpdateAt || options?.messagingRetryState?.lastUserVisibleUpdateAt || null,
|
|
4189
|
-
lastFinalDeliveryAt: runMeta?.progressLedger?.lastFinalDeliveryAt || options?.messagingRetryState?.lastFinalDeliveryAt || null,
|
|
4190
|
-
heartbeatCount: Number(runMeta?.progressLedger?.heartbeatCount || options?.messagingRetryState?.heartbeatCount || 0),
|
|
4191
|
-
progressState: runMeta?.progressLedger?.progressState || options?.messagingRetryState?.progressState || 'active',
|
|
4192
|
-
lastVerifiedProgressAt: runMeta?.progressLedger?.lastVerifiedProgressAt || options?.messagingRetryState?.lastVerifiedProgressAt || null,
|
|
4193
|
-
},
|
|
4194
|
-
context: {
|
|
4195
|
-
...(options.context || {}),
|
|
4196
|
-
additionalContext: [
|
|
4197
|
-
options.context?.additionalContext || '',
|
|
4198
|
-
recoveryContext,
|
|
4199
|
-
].filter(Boolean).join('\n\n')
|
|
4200
|
-
}
|
|
4201
|
-
};
|
|
4202
|
-
delete retryOptions.runId;
|
|
4203
|
-
|
|
4204
|
-
return this.runWithModel(userId, userMessage, retryOptions, _modelOverride);
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
|
-
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
4208
|
-
|| err?.deliverableValidation?.summary
|
|
4209
|
-
|| '';
|
|
4210
|
-
let messagingFailureContent = '';
|
|
4211
|
-
let sendSucceeded = false;
|
|
4212
|
-
if (triggerSource === 'messaging' && options.source && options.chatId) {
|
|
4213
|
-
if (!runMeta?.finalDeliverySent && !runMeta?.messagingSent) {
|
|
4214
|
-
const manager = this.messagingManager;
|
|
4215
|
-
if (manager) {
|
|
4216
|
-
const failureScenario = buildMessagingFailureScenario({
|
|
4217
|
-
err,
|
|
4218
|
-
failedStepCount,
|
|
4219
|
-
stepIndex,
|
|
4220
|
-
toolExecutions,
|
|
4221
|
-
});
|
|
4222
|
-
try {
|
|
4223
|
-
const failedMessage = sanitizeConversationMessages([
|
|
4224
|
-
...messages,
|
|
4225
|
-
{
|
|
4226
|
-
role: 'system',
|
|
4227
|
-
content: `The run encountered a runtime error and cannot continue reliably. Use the actual run scenario below to explain the blocker naturally.\n\nScenario:\n${failureScenario || 'No additional scenario details were captured.'}\n\nDo not call tools. Write exactly one short user message. Do not ask the user to resend or restate the same task. Only ask the user for something if a specific external input, permission, or configuration change is actually required. Do not promise future work unless it will happen automatically before this reply is sent.\n\n${buildPlatformFormattingGuide(options?.source || null)}`
|
|
4228
|
-
}
|
|
4229
|
-
]);
|
|
4230
|
-
const modelReply = await withModelCallTimeout(
|
|
4231
|
-
provider.chat(failedMessage, [], {
|
|
4232
|
-
model,
|
|
4233
|
-
reasoningEffort: this.getReasoningEffort(providerName, options)
|
|
4234
|
-
}),
|
|
4235
|
-
options,
|
|
4236
|
-
'Messaging failure reply',
|
|
4237
|
-
);
|
|
4238
|
-
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
4239
|
-
if (normalizeOutgoingMessage(drafted, options?.source || null)) {
|
|
4240
|
-
messagingFailureContent = drafted.trim();
|
|
4241
|
-
}
|
|
4242
|
-
} catch {
|
|
4243
|
-
// Fall back to deterministic text below.
|
|
4244
|
-
}
|
|
4245
|
-
|
|
4246
|
-
if (!messagingFailureContent) {
|
|
4247
|
-
messagingFailureContent = buildDeterministicMessagingErrorReply({
|
|
4248
|
-
err,
|
|
4249
|
-
failedStepCount,
|
|
4250
|
-
stepIndex,
|
|
4251
|
-
toolExecutions,
|
|
4252
|
-
});
|
|
4253
|
-
}
|
|
4254
|
-
|
|
4255
|
-
try {
|
|
4256
|
-
const deliveryResult = await manager.sendMessage(
|
|
4257
|
-
userId,
|
|
4258
|
-
options.source,
|
|
4259
|
-
options.chatId,
|
|
4260
|
-
messagingFailureContent,
|
|
4261
|
-
{ runId, agentId },
|
|
4262
|
-
);
|
|
4263
|
-
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
4264
|
-
sendSucceeded = true;
|
|
4265
|
-
if (runMeta) {
|
|
4266
|
-
runMeta.lastSentMessage = messagingFailureContent;
|
|
4267
|
-
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
4268
|
-
runMeta.sentMessages.push(messagingFailureContent);
|
|
4269
|
-
}
|
|
4270
|
-
this.markRunFinalDelivery(runId, messagingFailureContent);
|
|
4271
|
-
} catch (sendErr) {
|
|
4272
|
-
console.error('[Engine] Messaging error fallback failed:', sendErr.message);
|
|
4273
|
-
messagingFailureContent = '';
|
|
4274
|
-
}
|
|
4275
|
-
}
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
|
|
4279
|
-
db.prepare('UPDATE agent_runs SET status = ?, error = ?, final_response = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
4280
|
-
.run(
|
|
4281
|
-
'failed',
|
|
4282
|
-
err.message,
|
|
4283
|
-
sendSucceeded
|
|
4284
|
-
? (messagingFailureContent || null)
|
|
4285
|
-
: (deliverableFailureResponse || null),
|
|
4286
|
-
runId,
|
|
4287
|
-
);
|
|
4288
|
-
console.error(
|
|
4289
|
-
`[Run ${shortenRunId(runId)}] failed trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens} error=${summarizeForLog(err.message, 180)}`
|
|
4290
|
-
);
|
|
4291
|
-
|
|
4292
|
-
this.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
4293
|
-
this.stopMessagingProgressSupervisor(runId);
|
|
4294
|
-
this.activeRuns.delete(runId);
|
|
4295
|
-
this.emit(userId, 'run:error', { runId, error: err.message });
|
|
4296
|
-
this.recordRunEvent(userId, runId, 'run_failed', {
|
|
4297
|
-
error: err.message,
|
|
4298
|
-
totalTokens,
|
|
4299
|
-
iterations: iteration,
|
|
4300
|
-
deliverableType: deliverableWorkflow?.selection?.type || null,
|
|
4301
|
-
}, { agentId });
|
|
4302
|
-
|
|
4303
|
-
if (messagingFailureContent) {
|
|
4304
|
-
return {
|
|
4305
|
-
runId,
|
|
4306
|
-
content: messagingFailureContent,
|
|
4307
|
-
totalTokens,
|
|
4308
|
-
iterations: iteration,
|
|
4309
|
-
status: 'failed'
|
|
4310
|
-
};
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
throw err;
|
|
4314
|
-
}
|
|
4315
|
-
}
|
|
4316
|
-
|
|
4317
|
-
async spawnSubagent(userId, parentRunId, task, options = {}) {
|
|
4318
|
-
const handle = uuidv4();
|
|
4319
|
-
const childRunId = uuidv4();
|
|
4320
|
-
let relevantMemories = [];
|
|
4321
|
-
try {
|
|
4322
|
-
relevantMemories = this.memoryManager
|
|
4323
|
-
? await this.memoryManager.recallMemory(userId, task, 4, {
|
|
4324
|
-
agentId: options.agentId || null,
|
|
4325
|
-
})
|
|
4326
|
-
: [];
|
|
4327
|
-
} catch {}
|
|
4328
|
-
const subEngine = new AgentEngine(this.io, {
|
|
4329
|
-
app: options.app || this.app,
|
|
4330
|
-
browserController: this.browserController,
|
|
4331
|
-
androidController: this.androidController,
|
|
4332
|
-
runtimeManager: this.runtimeManager,
|
|
4333
|
-
workspaceManager: this.workspaceManager,
|
|
4334
|
-
messagingManager: this.messagingManager,
|
|
4335
|
-
mcpManager: this.mcpManager,
|
|
4336
|
-
skillRunner: this.skillRunner,
|
|
4337
|
-
taskRuntime: this.taskRuntime,
|
|
4338
|
-
memoryManager: this.memoryManager,
|
|
4339
|
-
});
|
|
4340
|
-
|
|
4341
|
-
const subagentContract = [
|
|
4342
|
-
`Goal: ${String(task || '').trim()}`,
|
|
4343
|
-
options.context ? `Constraints and relevant context:\n${String(options.context).trim()}` : '',
|
|
4344
|
-
relevantMemories.length > 0
|
|
4345
|
-
? `Top relevant memories: ${JSON.stringify(relevantMemories.map((memory) => ({
|
|
4346
|
-
content: memory.content,
|
|
4347
|
-
confidence: memory.confidence,
|
|
4348
|
-
})))}`
|
|
4349
|
-
: '',
|
|
4350
|
-
Array.isArray(options.requiredArtifacts) && options.requiredArtifacts.length > 0
|
|
4351
|
-
? `Required artifacts: ${JSON.stringify(options.requiredArtifacts)}`
|
|
4352
|
-
: '',
|
|
4353
|
-
Array.isArray(options.selectedTools) && options.selectedTools.length > 0
|
|
4354
|
-
? `Selected tools: ${JSON.stringify(options.selectedTools.slice(0, 20))}`
|
|
4355
|
-
: '',
|
|
4356
|
-
'Return a single JSON object with exactly these top-level fields: status, findings, evidence, artifacts, confidence, remaining_blockers.',
|
|
4357
|
-
'status must be completed, partial, or blocked. findings, evidence, artifacts, and remaining_blockers must be arrays. confidence must be low, medium, or high.',
|
|
4358
|
-
].filter(Boolean).join('\n\n');
|
|
4359
|
-
const record = {
|
|
4360
|
-
handle,
|
|
4361
|
-
parentRunId,
|
|
4362
|
-
childRunId,
|
|
4363
|
-
userId,
|
|
4364
|
-
agentId: options.agentId || null,
|
|
4365
|
-
task: subagentContract,
|
|
4366
|
-
model: options.model || null,
|
|
4367
|
-
status: 'running',
|
|
4368
|
-
createdAt: new Date().toISOString(),
|
|
4369
|
-
result: null,
|
|
4370
|
-
error: null,
|
|
4371
|
-
engine: subEngine,
|
|
4372
|
-
promise: null,
|
|
4373
|
-
};
|
|
4374
|
-
this.subagents.set(handle, record);
|
|
4375
|
-
this.emit(userId, 'run:subagent', {
|
|
4376
|
-
runId: parentRunId,
|
|
4377
|
-
handle,
|
|
4378
|
-
childRunId,
|
|
4379
|
-
status: 'running',
|
|
4380
|
-
task: clampRunContext(task, 180),
|
|
4381
|
-
});
|
|
4382
|
-
|
|
4383
|
-
record.promise = (async () => {
|
|
4384
|
-
try {
|
|
4385
|
-
const result = await subEngine.runWithModel(
|
|
4386
|
-
userId,
|
|
4387
|
-
subagentContract,
|
|
4388
|
-
{
|
|
4389
|
-
app: options.app || this.app,
|
|
4390
|
-
triggerType: 'subagent',
|
|
4391
|
-
triggerSource: 'agent',
|
|
4392
|
-
runId: childRunId,
|
|
4393
|
-
agentId: options.agentId || null,
|
|
4394
|
-
},
|
|
4395
|
-
options.model || null
|
|
4396
|
-
);
|
|
4397
|
-
record.status = result.status || 'completed';
|
|
4398
|
-
let structured = null;
|
|
4399
|
-
try {
|
|
4400
|
-
const raw = String(result.content || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
|
4401
|
-
const parsed = JSON.parse(raw);
|
|
4402
|
-
if (parsed && typeof parsed === 'object') structured = parsed;
|
|
4403
|
-
} catch {}
|
|
4404
|
-
record.result = {
|
|
4405
|
-
runId: result.runId,
|
|
4406
|
-
status: structured?.status || result.status || 'completed',
|
|
4407
|
-
findings: Array.isArray(structured?.findings) ? structured.findings : [String(result.content || '').trim()].filter(Boolean),
|
|
4408
|
-
evidence: Array.isArray(structured?.evidence) ? structured.evidence : [],
|
|
4409
|
-
artifacts: Array.isArray(structured?.artifacts) ? structured.artifacts : [],
|
|
4410
|
-
confidence: ['low', 'medium', 'high'].includes(structured?.confidence) ? structured.confidence : 'medium',
|
|
4411
|
-
remainingBlockers: Array.isArray(structured?.remaining_blockers) ? structured.remaining_blockers : [],
|
|
4412
|
-
totalTokens: result.totalTokens,
|
|
4413
|
-
iterations: result.iterations,
|
|
4414
|
-
};
|
|
4415
|
-
this.emit(userId, 'run:subagent', {
|
|
4416
|
-
runId: parentRunId,
|
|
4417
|
-
handle,
|
|
4418
|
-
childRunId,
|
|
4419
|
-
status: record.status,
|
|
4420
|
-
result: record.result,
|
|
4421
|
-
});
|
|
4422
|
-
return record;
|
|
4423
|
-
} catch (err) {
|
|
4424
|
-
record.status = 'failed';
|
|
4425
|
-
record.error = err.message;
|
|
4426
|
-
this.emit(userId, 'run:subagent', {
|
|
4427
|
-
runId: parentRunId,
|
|
4428
|
-
handle,
|
|
4429
|
-
childRunId,
|
|
4430
|
-
status: 'failed',
|
|
4431
|
-
error: err.message,
|
|
4432
|
-
});
|
|
4433
|
-
throw err;
|
|
4434
|
-
}
|
|
4435
|
-
})();
|
|
4436
|
-
|
|
4437
|
-
return {
|
|
4438
|
-
handle,
|
|
4439
|
-
status: 'running',
|
|
4440
|
-
childRunId,
|
|
4441
|
-
task: clampRunContext(task, 180),
|
|
4442
|
-
};
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
async delegateToAgent({
|
|
4446
|
-
userId,
|
|
4447
|
-
parentAgentId,
|
|
4448
|
-
parentRunId,
|
|
4449
|
-
target,
|
|
4450
|
-
task,
|
|
4451
|
-
context = '',
|
|
4452
|
-
app = null,
|
|
4453
|
-
allowExternalSideEffects = false,
|
|
4454
|
-
} = {}) {
|
|
4455
|
-
const { agentCanDelegateTo, getAgentById, getAgentBySlug, resolveAgentId } = require('../agents/manager');
|
|
4456
|
-
const targetText = String(target || '').trim();
|
|
4457
|
-
const taskText = String(task || '').trim();
|
|
4458
|
-
if (!targetText || !taskText) {
|
|
4459
|
-
throw new Error('Target agent and task are required.');
|
|
4460
|
-
}
|
|
4461
|
-
|
|
4462
|
-
let targetAgent = getAgentById(userId, targetText) || getAgentBySlug(userId, targetText);
|
|
4463
|
-
if (!targetAgent) {
|
|
4464
|
-
targetAgent = db.prepare(
|
|
4465
|
-
"SELECT * FROM agents WHERE user_id = ? AND status = 'active' AND lower(display_name) = lower(?)"
|
|
4466
|
-
).get(userId, targetText);
|
|
4467
|
-
}
|
|
4468
|
-
if (!targetAgent || targetAgent.status !== 'active') {
|
|
4469
|
-
throw new Error(`No active specialist agent matches "${targetText}".`);
|
|
4470
|
-
}
|
|
4471
|
-
|
|
4472
|
-
const scopedParentAgentId = resolveAgentId(userId, parentAgentId);
|
|
4473
|
-
const parentAgent = getAgentById(userId, scopedParentAgentId);
|
|
4474
|
-
if (targetAgent.id === scopedParentAgentId) {
|
|
4475
|
-
throw new Error('An agent cannot delegate to itself.');
|
|
4476
|
-
}
|
|
4477
|
-
if (!agentCanDelegateTo(parentAgent, targetAgent)) {
|
|
4478
|
-
throw new Error(`${parentAgent?.display_name || 'This agent'} is not allowed to delegate tasks to ${targetAgent.display_name}.`);
|
|
4479
|
-
}
|
|
4480
|
-
|
|
4481
|
-
const delegationId = uuidv4();
|
|
4482
|
-
const childRunId = uuidv4();
|
|
4483
|
-
db.prepare(
|
|
4484
|
-
`INSERT INTO agent_delegations (
|
|
4485
|
-
id, user_id, parent_agent_id, target_agent_id, parent_run_id, child_run_id, task, context, status
|
|
4486
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running')`
|
|
4487
|
-
).run(
|
|
4488
|
-
delegationId,
|
|
4489
|
-
userId,
|
|
4490
|
-
scopedParentAgentId,
|
|
4491
|
-
targetAgent.id,
|
|
4492
|
-
parentRunId || null,
|
|
4493
|
-
childRunId,
|
|
4494
|
-
taskText,
|
|
4495
|
-
context || null,
|
|
4496
|
-
);
|
|
4497
|
-
|
|
4498
|
-
const delegatedPrompt = [
|
|
4499
|
-
'[SYSTEM: Delegated specialist-agent task]',
|
|
4500
|
-
`You are running as ${targetAgent.display_name} (${targetAgent.slug}).`,
|
|
4501
|
-
'Complete this delegated task using only your own agent memory, settings, credentials, and available tools.',
|
|
4502
|
-
allowExternalSideEffects
|
|
4503
|
-
? 'External side effects are allowed only when they directly satisfy the delegated task.'
|
|
4504
|
-
: 'Do not send external messages, make calls, or change external shared systems. Return findings and recommendations to the parent agent instead.',
|
|
4505
|
-
'',
|
|
4506
|
-
`Task:\n${taskText}`,
|
|
4507
|
-
context ? `\nContext from parent agent:\n${context}` : '',
|
|
4508
|
-
].filter(Boolean).join('\n');
|
|
4509
|
-
|
|
4510
|
-
try {
|
|
4511
|
-
const result = await this.runWithModel(
|
|
4512
|
-
userId,
|
|
4513
|
-
delegatedPrompt,
|
|
4514
|
-
{
|
|
4515
|
-
app: app || this.app,
|
|
4516
|
-
runId: childRunId,
|
|
4517
|
-
agentId: targetAgent.id,
|
|
4518
|
-
triggerType: 'agent_delegation',
|
|
4519
|
-
triggerSource: 'agent_delegation',
|
|
4520
|
-
skipConversationHistory: true,
|
|
4521
|
-
skipConversationMaintenance: true,
|
|
4522
|
-
context: { additionalContext: `Parent run: ${parentRunId || 'unknown'}` },
|
|
4523
|
-
allowExternalSideEffects,
|
|
4524
|
-
},
|
|
4525
|
-
null,
|
|
4526
|
-
);
|
|
4527
|
-
const summary = String(result?.content || '').trim();
|
|
4528
|
-
db.prepare(
|
|
4529
|
-
`UPDATE agent_delegations
|
|
4530
|
-
SET status = ?, result_summary = ?, updated_at = datetime('now'), completed_at = datetime('now')
|
|
4531
|
-
WHERE id = ?`
|
|
4532
|
-
).run(result?.status || 'completed', summary.slice(0, 20000), delegationId);
|
|
4533
|
-
return {
|
|
4534
|
-
delegationId,
|
|
4535
|
-
targetAgent: {
|
|
4536
|
-
id: targetAgent.id,
|
|
4537
|
-
slug: targetAgent.slug,
|
|
4538
|
-
name: targetAgent.display_name,
|
|
4539
|
-
},
|
|
4540
|
-
childRunId: result?.runId || childRunId,
|
|
4541
|
-
status: result?.status || 'completed',
|
|
4542
|
-
summary,
|
|
4543
|
-
totalTokens: result?.totalTokens || 0,
|
|
4544
|
-
};
|
|
4545
|
-
} catch (err) {
|
|
4546
|
-
db.prepare(
|
|
4547
|
-
`UPDATE agent_delegations
|
|
4548
|
-
SET status = 'failed', error = ?, updated_at = datetime('now'), completed_at = datetime('now')
|
|
4549
|
-
WHERE id = ?`
|
|
4550
|
-
).run(String(err?.message || err).slice(0, 20000), delegationId);
|
|
4551
|
-
throw err;
|
|
4552
|
-
}
|
|
4553
|
-
}
|
|
4554
|
-
|
|
4555
|
-
listSubagents(parentRunId = null) {
|
|
4556
|
-
return Array.from(this.subagents.values())
|
|
4557
|
-
.filter((record) => !parentRunId || record.parentRunId === parentRunId)
|
|
4558
|
-
.map((record) => ({
|
|
4559
|
-
handle: record.handle,
|
|
4560
|
-
parentRunId: record.parentRunId,
|
|
4561
|
-
childRunId: record.childRunId,
|
|
4562
|
-
status: record.status,
|
|
4563
|
-
task: clampRunContext(record.task, 180),
|
|
4564
|
-
result: record.result,
|
|
4565
|
-
error: record.error,
|
|
4566
|
-
createdAt: record.createdAt,
|
|
4567
|
-
}));
|
|
4568
|
-
}
|
|
4569
|
-
|
|
4570
|
-
cleanupSubagentsForRun(parentRunId, options = {}) {
|
|
4571
|
-
if (!parentRunId) return;
|
|
4572
|
-
const cancelRunning = options.cancelRunning !== false;
|
|
4573
|
-
for (const [handle, record] of this.subagents.entries()) {
|
|
4574
|
-
if (record.parentRunId !== parentRunId) continue;
|
|
4575
|
-
if (cancelRunning && record.status === 'running') {
|
|
4576
|
-
try {
|
|
4577
|
-
record.engine?.abort(record.childRunId);
|
|
4578
|
-
record.status = 'cancelled';
|
|
4579
|
-
} catch (err) {
|
|
4580
|
-
console.warn(`[AgentEngine] Failed to abort subagent ${handle}:`, err?.message);
|
|
4581
|
-
}
|
|
4582
|
-
}
|
|
4583
|
-
this.subagents.delete(handle);
|
|
4584
|
-
}
|
|
4585
|
-
}
|
|
4586
|
-
|
|
4587
|
-
async waitForSubagent(handle, options = {}) {
|
|
4588
|
-
const record = this.subagents.get(handle);
|
|
4589
|
-
if (!record) {
|
|
4590
|
-
return { error: `Unknown sub-agent handle: ${handle}` };
|
|
4591
|
-
}
|
|
4592
|
-
if (options.parentRunId && record.parentRunId !== options.parentRunId) {
|
|
4593
|
-
return { error: 'That sub-agent does not belong to the current parent run.' };
|
|
4594
|
-
}
|
|
4595
|
-
|
|
4596
|
-
if (record.status !== 'running' || !record.promise) {
|
|
4597
|
-
return {
|
|
4598
|
-
handle,
|
|
4599
|
-
status: record.status,
|
|
4600
|
-
result: record.result,
|
|
4601
|
-
error: record.error,
|
|
4602
|
-
};
|
|
4603
|
-
}
|
|
4604
|
-
|
|
4605
|
-
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 30000);
|
|
4606
|
-
const timeout = new Promise((resolve) => {
|
|
4607
|
-
setTimeout(() => resolve(null), timeoutMs);
|
|
4608
|
-
});
|
|
4609
|
-
const settled = await Promise.race([
|
|
4610
|
-
record.promise.then(() => record).catch(() => record),
|
|
4611
|
-
timeout,
|
|
4612
|
-
]);
|
|
4613
|
-
|
|
4614
|
-
if (!settled) {
|
|
4615
|
-
return {
|
|
4616
|
-
handle,
|
|
4617
|
-
status: 'running',
|
|
4618
|
-
timedOut: true,
|
|
4619
|
-
};
|
|
4620
|
-
}
|
|
4621
|
-
|
|
4622
|
-
return {
|
|
4623
|
-
handle,
|
|
4624
|
-
status: record.status,
|
|
4625
|
-
result: record.result,
|
|
4626
|
-
error: record.error,
|
|
4627
|
-
};
|
|
4628
|
-
}
|
|
4629
|
-
|
|
4630
|
-
async cancelSubagent(handle, options = {}) {
|
|
4631
|
-
const record = this.subagents.get(handle);
|
|
4632
|
-
if (!record) {
|
|
4633
|
-
return { error: `Unknown sub-agent handle: ${handle}` };
|
|
4634
|
-
}
|
|
4635
|
-
if (options.parentRunId && record.parentRunId !== options.parentRunId) {
|
|
4636
|
-
return { error: 'That sub-agent does not belong to the current parent run.' };
|
|
4637
|
-
}
|
|
4638
|
-
if (record.status !== 'running') {
|
|
4639
|
-
return {
|
|
4640
|
-
handle,
|
|
4641
|
-
status: record.status,
|
|
4642
|
-
result: record.result,
|
|
4643
|
-
error: record.error,
|
|
4644
|
-
};
|
|
4645
|
-
}
|
|
4646
|
-
|
|
4647
|
-
record.engine?.abort(record.childRunId);
|
|
4648
|
-
record.status = 'cancelled';
|
|
4649
|
-
this.emit(record.userId, 'run:subagent', {
|
|
4650
|
-
runId: record.parentRunId,
|
|
4651
|
-
handle,
|
|
4652
|
-
childRunId: record.childRunId,
|
|
4653
|
-
status: 'cancelled',
|
|
4654
|
-
});
|
|
4655
|
-
|
|
4656
|
-
return { handle, status: 'cancelled' };
|
|
4657
|
-
}
|
|
4658
|
-
|
|
4659
|
-
stopRun(runId) {
|
|
4660
|
-
const runMeta = this.activeRuns.get(runId);
|
|
4661
|
-
const delegatedChildren = db.prepare(
|
|
4662
|
-
"SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
|
|
4663
|
-
).all(runId);
|
|
4664
|
-
if (runMeta) {
|
|
4665
|
-
runMeta.status = 'stopped';
|
|
4666
|
-
runMeta.aborted = true;
|
|
4667
|
-
this.emit(runMeta.userId, 'run:stopping', { runId });
|
|
4668
|
-
for (const pid of runMeta.toolPids) {
|
|
4669
|
-
if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
|
|
4670
|
-
void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
runMeta.toolPids.clear();
|
|
4674
|
-
}
|
|
4675
|
-
for (const child of delegatedChildren) {
|
|
4676
|
-
if (child.child_run_id && child.child_run_id !== runId) {
|
|
4677
|
-
this.stopRun(child.child_run_id);
|
|
4678
|
-
}
|
|
4679
|
-
}
|
|
4680
|
-
db.prepare(
|
|
4681
|
-
"UPDATE agent_delegations SET status = 'stopped', updated_at = datetime('now'), completed_at = datetime('now') WHERE parent_run_id = ? AND status = 'running'"
|
|
4682
|
-
).run(runId);
|
|
4683
|
-
db.prepare("UPDATE agent_runs SET status = 'stopped', updated_at = datetime('now') WHERE id = ?").run(runId);
|
|
4684
|
-
}
|
|
4685
|
-
|
|
4686
|
-
abort(runId, { userId } = {}) {
|
|
4687
|
-
if (!runId) return false;
|
|
4688
|
-
if (userId != null) {
|
|
4689
|
-
// Ownership gate: never let one user abort another user's active run.
|
|
4690
|
-
const runMeta = this.activeRuns.get(runId);
|
|
4691
|
-
if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
|
|
4692
|
-
}
|
|
4693
|
-
this.stopRun(runId);
|
|
4694
|
-
return true;
|
|
4695
|
-
}
|
|
4696
|
-
|
|
4697
|
-
abortAll(userId) {
|
|
4698
|
-
for (const [runId, run] of this.activeRuns) {
|
|
4699
|
-
if (run.userId === userId) this.stopRun(runId);
|
|
4700
|
-
}
|
|
4701
|
-
}
|
|
4702
|
-
|
|
4703
|
-
getStepType(toolName) {
|
|
4704
|
-
if (toolName.startsWith('browser_')) return 'browser';
|
|
4705
|
-
if (toolName.startsWith('android_')) return 'android';
|
|
4706
|
-
if (toolName === 'execute_command') return 'cli';
|
|
4707
|
-
if (toolName.startsWith('memory_')) return 'memory';
|
|
4708
|
-
if (toolName === 'send_interim_update') return 'note';
|
|
4709
|
-
if (toolName === 'send_message') return 'messaging';
|
|
4710
|
-
if (toolName === 'make_call') return 'messaging';
|
|
4711
|
-
if (toolName.startsWith('mcp_') || toolName.includes('mcp')) return 'mcp';
|
|
4712
|
-
if (toolName === 'create_task' || toolName === 'update_task' || toolName === 'delete_task' || toolName === 'list_tasks' || toolName.includes('widget')) return 'tasks';
|
|
4713
|
-
if (toolName.includes('subagent')) return 'subagent';
|
|
4714
|
-
if (toolName === 'think') return 'thinking';
|
|
4715
|
-
return 'tool';
|
|
4716
|
-
}
|
|
4717
|
-
|
|
4718
|
-
emit(userId, event, data) {
|
|
4719
|
-
if (this.io) {
|
|
4720
|
-
this.io.to(`user:${userId}`).emit(event, data);
|
|
4721
|
-
}
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
|
|
4725
|
-
module.exports = { AgentEngine, buildInitialRunMetadata, getProviderForUser };
|
|
3
|
+
module.exports = require('./loop/agent_engine_core');
|