neoagent 3.0.1-beta.2 → 3.0.1-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +19 -0
- package/README.md +20 -3
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +224 -0
- package/docs/configuration.md +22 -0
- package/docs/getting-started.md +10 -8
- package/docs/index.md +1 -0
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +4 -1
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +316 -0
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1594 -224
- package/flutter_app/lib/main_controller.dart +263 -26
- package/flutter_app/lib/main_devices.dart +1 -1
- package/flutter_app/lib/main_install.dart +1147 -0
- package/flutter_app/lib/main_navigation.dart +12 -0
- package/flutter_app/lib/main_operations.dart +134 -9
- package/flutter_app/lib/main_settings.dart +148 -52
- package/flutter_app/lib/main_shared.dart +1 -0
- package/flutter_app/lib/src/backend_client.dart +86 -1
- package/landing/index.html +2 -2
- package/lib/manager.js +184 -66
- package/lib/schema_migrations.js +84 -0
- package/package.json +10 -4
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +511 -23
- package/server/admin/billing.js +415 -0
- package/server/admin/index.html +120 -13
- package/server/admin/users.js +15 -15
- package/server/db/database.js +13 -21
- package/server/db/sessions_db.js +8 -0
- package/server/http/middleware.js +4 -4
- package/server/http/routes.js +9 -0
- package/server/http/static.js +4 -2
- package/server/middleware/requireBilling.js +12 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +89347 -85449
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +515 -63
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +127 -0
- package/server/routes/billing_webhook.js +43 -0
- package/server/routes/memory.js +1 -4
- package/server/routes/settings.js +1 -2
- package/server/routes/skills.js +1 -1
- package/server/routes/triggers.js +2 -2
- package/server/services/account/erasure.js +263 -0
- package/server/services/account/sessions.js +1 -9
- package/server/services/ai/loop/agent_engine_core.js +42 -0
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/completion_judge.js +42 -0
- package/server/services/ai/loop/conversation_loop.js +248 -34
- package/server/services/ai/loop/progress_monitor.js +6 -6
- package/server/services/ai/loopPolicy.js +37 -44
- package/server/services/ai/messagingFallback.js +25 -1
- package/server/services/ai/models.js +18 -0
- package/server/services/ai/preModelCompaction.js +25 -5
- package/server/services/ai/rate_limits.js +28 -4
- package/server/services/ai/systemPrompt.js +23 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/tools.js +231 -20
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/billing_email.js +106 -0
- package/server/services/billing/config.js +17 -0
- package/server/services/billing/plans.js +121 -0
- package/server/services/billing/stripe_client.js +21 -0
- package/server/services/billing/subscriptions.js +462 -0
- package/server/services/billing/trial_guard.js +111 -0
- package/server/services/browser/contentExtractor.js +629 -0
- package/server/services/browser/controller.js +4 -8
- package/server/services/desktop/gateway.js +7 -4
- package/server/services/integrations/google/calendar.js +30 -2
- package/server/services/integrations/secrets.js +7 -4
- package/server/services/manager.js +11 -0
- package/server/services/messaging/automation.js +1 -1
- package/server/services/messaging/telnyx.js +12 -11
- package/server/services/messaging/whatsapp.js +1 -1
- package/server/services/recordings/manager.js +13 -7
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +182 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/tasks/runtime.js +103 -15
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/voice/runtimeManager.js +19 -10
- package/server/services/wearable/gateway.js +8 -5
- package/server/services/websocket.js +26 -8
- package/server/services/workspace/manager.js +37 -3
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
selectDeliverableWorkflow,
|
|
50
50
|
validateDeliverableExecution,
|
|
51
51
|
} = require('../deliverables');
|
|
52
|
-
const { buildLoopPolicy, resolveToolResultLimits } = require('../loopPolicy');
|
|
52
|
+
const { buildLoopPolicy, resolveToolResultLimits, resolveChurnNudgeThreshold } = require('../loopPolicy');
|
|
53
53
|
const { globalHooks } = require('../hooks');
|
|
54
54
|
const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('../completion');
|
|
55
55
|
const { enforceRateLimits } = require('../rate_limits');
|
|
@@ -58,7 +58,10 @@ const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
|
58
58
|
const { IterationBudget } = require('./iteration_budget');
|
|
59
59
|
const {
|
|
60
60
|
buildBlankAfterToolFailureGuidance,
|
|
61
|
+
buildRecoverableToolFailureGuidance,
|
|
62
|
+
isRecoverableInternalToolFailure,
|
|
61
63
|
shouldContinueAfterBlankToolFailure,
|
|
64
|
+
shouldContinueAfterRecoverableToolFailure,
|
|
62
65
|
} = require('./blank_recovery');
|
|
63
66
|
const {
|
|
64
67
|
shouldSendMessagingErrorFallback,
|
|
@@ -210,6 +213,9 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
210
213
|
// multiple iterations before self-correcting.
|
|
211
214
|
const immediateGuides = {
|
|
212
215
|
eisdir: 'That path is a directory or outside the workspace file-tool boundary. Use list_directory for workspace directories. Keep source files in the shared workspace before reading them with file tools.',
|
|
216
|
+
enoent: 'That path does not exist. Do not keep retrying the same missing file. Locate the correct file first with list_directory/search_files or verify whether the evidence only exists in the user-provided logs.',
|
|
217
|
+
outside_workspace: 'That path is outside the shared workspace. Use the workspace root and its file tools, or rely on the user-provided evidence if the file only exists on another server.',
|
|
218
|
+
bad_cwd: 'The current working directory is wrong for that path. Reconfirm the workspace root with pwd/list_directory before reading files.',
|
|
213
219
|
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.',
|
|
214
220
|
};
|
|
215
221
|
if (immediateGuides[key]) {
|
|
@@ -284,6 +290,24 @@ function summarizeReadTargets(toolExecutions = []) {
|
|
|
284
290
|
return targets.join('; ');
|
|
285
291
|
}
|
|
286
292
|
|
|
293
|
+
function buildNoProgressWrapupPrompt({ readOnlyCount = 0, alreadyRead = '', platform = null } = {}) {
|
|
294
|
+
return [
|
|
295
|
+
`No-progress limit reached after ${Math.max(0, Number(readOnlyCount) || 0)} consecutive turns without substantive progress.`,
|
|
296
|
+
alreadyRead ? `Already inspected/searched: ${alreadyRead}.` : '',
|
|
297
|
+
'This is the final turn for this run. Do not call tools.',
|
|
298
|
+
'From the existing evidence, return the concrete result, a no-op/no-change answer, or a clear blocker.',
|
|
299
|
+
'If an external site, service, permission, unavailable target, or missing user input prevents completion, say that clearly and stop.',
|
|
300
|
+
buildMaxIterationWrapupPrompt(platform),
|
|
301
|
+
].filter(Boolean).join('\n\n');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isDeliveryTerminated(runMeta, deliveryState) {
|
|
305
|
+
return runMeta?.noResponse === true
|
|
306
|
+
|| deliveryState?.noResponse === true
|
|
307
|
+
|| runMeta?.finalDeliverySent === true
|
|
308
|
+
|| deliveryState?.finalDeliverySent === true;
|
|
309
|
+
}
|
|
310
|
+
|
|
287
311
|
function cloneInterimHistory(history = []) {
|
|
288
312
|
if (!Array.isArray(history)) return [];
|
|
289
313
|
return history.map((item) => ({
|
|
@@ -483,6 +507,7 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
|
|
|
483
507
|
const pool = enabledIds.length > 0
|
|
484
508
|
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
485
509
|
: availableModels;
|
|
510
|
+
const fallbackSearchPool = pool.length > 0 ? pool : availableModels;
|
|
486
511
|
const currentModel = pool.find((model) => model.id === currentModelId)
|
|
487
512
|
|| availableModels.find((model) => model.id === currentModelId)
|
|
488
513
|
|| null;
|
|
@@ -493,27 +518,26 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
|
|
|
493
518
|
const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
|
|
494
519
|
|
|
495
520
|
if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
|
|
496
|
-
const preferred =
|
|
521
|
+
const preferred = fallbackSearchPool.find((model) => model.id === preferredFallbackId)
|
|
497
522
|
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
498
523
|
if (preferred) return preferred.id;
|
|
499
524
|
}
|
|
500
525
|
|
|
501
526
|
if (currentModel?.provider) {
|
|
502
|
-
const differentProvider =
|
|
503
|
-
|
|
527
|
+
const differentProvider = fallbackSearchPool.find((model) =>
|
|
528
|
+
model.id !== currentModelId && model.provider !== currentModel.provider);
|
|
504
529
|
if (differentProvider) return differentProvider.id;
|
|
505
530
|
}
|
|
506
531
|
|
|
507
532
|
// If no different-provider model exists, still try the preferred fallback
|
|
508
533
|
// even on rate limits (it's better than nothing).
|
|
509
534
|
if (preferredFallbackId && preferredFallbackId !== currentModelId) {
|
|
510
|
-
const preferred =
|
|
535
|
+
const preferred = fallbackSearchPool.find((model) => model.id === preferredFallbackId)
|
|
511
536
|
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
512
537
|
if (preferred) return preferred.id;
|
|
513
538
|
}
|
|
514
539
|
|
|
515
|
-
const differentModel =
|
|
516
|
-
|| availableModels.find((model) => model.id !== currentModelId);
|
|
540
|
+
const differentModel = fallbackSearchPool.find((model) => model.id !== currentModelId);
|
|
517
541
|
return differentModel?.id || null;
|
|
518
542
|
}
|
|
519
543
|
|
|
@@ -529,13 +553,25 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
529
553
|
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
530
554
|
ensureDefaultAiSettings(userId, agentId);
|
|
531
555
|
const aiSettings = getAiSettings(userId, agentId);
|
|
532
|
-
|
|
533
|
-
enforceRateLimits(userId);
|
|
534
|
-
|
|
535
556
|
const runId = options.runId || uuidv4();
|
|
536
557
|
const conversationId = options.conversationId;
|
|
537
558
|
const app = options.app || engine.app;
|
|
538
559
|
const triggerSource = options.triggerSource || 'web';
|
|
560
|
+
let provider = null;
|
|
561
|
+
let model = null;
|
|
562
|
+
let providerName = null;
|
|
563
|
+
let messages = [];
|
|
564
|
+
let iteration = 0;
|
|
565
|
+
let totalTokens = 0;
|
|
566
|
+
let lastContent = '';
|
|
567
|
+
let stepIndex = 0;
|
|
568
|
+
let failedStepCount = 0;
|
|
569
|
+
let toolExecutions = [];
|
|
570
|
+
let deliverableWorkflow = null;
|
|
571
|
+
|
|
572
|
+
const { releaseReservation } = enforceRateLimits(userId);
|
|
573
|
+
|
|
574
|
+
try {
|
|
539
575
|
const historyWindow = Math.max(
|
|
540
576
|
1,
|
|
541
577
|
Number(options.historyWindow || aiSettings.chat_history_window) || aiSettings.chat_history_window,
|
|
@@ -563,9 +599,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
563
599
|
_modelOverride,
|
|
564
600
|
providerStatusConfig
|
|
565
601
|
);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
602
|
+
provider = selectedProvider.provider;
|
|
603
|
+
model = selectedProvider.model;
|
|
604
|
+
providerName = selectedProvider.providerName;
|
|
569
605
|
const switchToFallbackModel = async (failedModel, error, phase) => {
|
|
570
606
|
const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
|
|
571
607
|
if (!fallbackModelId || fallbackModelId === failedModel) return false;
|
|
@@ -779,7 +815,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
779
815
|
historyMessages = (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
780
816
|
}
|
|
781
817
|
|
|
782
|
-
|
|
818
|
+
messages = engine.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
|
|
783
819
|
if (capabilitySummary) {
|
|
784
820
|
messages.push({ role: 'system', content: `[Capability health]\n${capabilitySummary}` });
|
|
785
821
|
}
|
|
@@ -809,14 +845,14 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
809
845
|
.run(conversationId, 'user', userMessage);
|
|
810
846
|
}
|
|
811
847
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
848
|
+
iteration = 0;
|
|
849
|
+
totalTokens = 0;
|
|
850
|
+
lastContent = '';
|
|
851
|
+
stepIndex = 0;
|
|
852
|
+
failedStepCount = 0;
|
|
817
853
|
let modelFailureRecoveries = 0;
|
|
818
854
|
let promptMetrics = {};
|
|
819
|
-
|
|
855
|
+
toolExecutions = [];
|
|
820
856
|
let compactionMetrics = [];
|
|
821
857
|
let analysis = null;
|
|
822
858
|
let plan = null;
|
|
@@ -877,15 +913,14 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
877
913
|
}
|
|
878
914
|
}
|
|
879
915
|
let verification = null;
|
|
880
|
-
|
|
916
|
+
deliverableWorkflow = null;
|
|
881
917
|
let deliverablePlan = null;
|
|
882
918
|
let deliverableArtifacts = [];
|
|
883
919
|
let deliverableValidation = null;
|
|
884
920
|
let directAnswerEligible = false;
|
|
885
921
|
let analysisUsage = 0;
|
|
886
922
|
|
|
887
|
-
|
|
888
|
-
if (options.skipTaskAnalysis === true) {
|
|
923
|
+
if (options.skipTaskAnalysis === true) {
|
|
889
924
|
analysis = buildSkipTaskAnalysisResult(options.forceMode);
|
|
890
925
|
} else {
|
|
891
926
|
const analysisResult = await runWithModelFallback('task analysis', () => engine.analyzeTask({
|
|
@@ -1169,17 +1204,163 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1169
1204
|
messages = steeringAtLoopStart.messages;
|
|
1170
1205
|
messages = sanitizeConversationMessages(messages);
|
|
1171
1206
|
|
|
1172
|
-
// Analysis-paralysis gate:
|
|
1173
|
-
//
|
|
1174
|
-
// taking any concrete action. Escalates in urgency each turn.
|
|
1207
|
+
// Analysis-paralysis gate: AI self-assesses at churnNudgeThreshold; hard
|
|
1208
|
+
// force-wrap-up fires at maxConsecutiveReadOnlyIterations unconditionally.
|
|
1175
1209
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1176
1210
|
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1211
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1212
|
+
const latestFailedExecution = toolExecutions.length > 0
|
|
1213
|
+
? [...toolExecutions].reverse().find((item) => item && item.ok === false) || null
|
|
1214
|
+
: null;
|
|
1215
|
+
|
|
1216
|
+
if (readOnlyCount >= 2) {
|
|
1217
|
+
const runGoalCtx = resolveRunGoalContext(engine.getRunMeta(runId), analysis, plan);
|
|
1218
|
+
const churnNudgeThreshold = resolveChurnNudgeThreshold(runGoalCtx.goalContract);
|
|
1219
|
+
|
|
1220
|
+
let triggerForceWrapup = false;
|
|
1221
|
+
let forceWrapupSource = 'hard_limit';
|
|
1222
|
+
let alreadyRead = '';
|
|
1223
|
+
|
|
1224
|
+
if (readOnlyCount >= loopPolicy.maxConsecutiveReadOnlyIterations) {
|
|
1225
|
+
if (
|
|
1226
|
+
isRecoverableInternalToolFailure(latestFailedExecution)
|
|
1227
|
+
&& iterMeta
|
|
1228
|
+
&& iterMeta.recoverableReadOnlyDeferralUsed !== true
|
|
1229
|
+
) {
|
|
1230
|
+
iterMeta.recoverableReadOnlyDeferralUsed = true;
|
|
1231
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, loopPolicy.maxConsecutiveReadOnlyIterations - 2);
|
|
1232
|
+
messages.push({
|
|
1233
|
+
role: 'system',
|
|
1234
|
+
content: buildRecoverableToolFailureGuidance(toolExecutions),
|
|
1235
|
+
});
|
|
1236
|
+
engine.recordRunEvent(userId, runId, 'read_only_wrapup_deferred_for_recovery', {
|
|
1237
|
+
iteration,
|
|
1238
|
+
readOnlyCount,
|
|
1239
|
+
toolName: latestFailedExecution?.toolName || null,
|
|
1240
|
+
}, { agentId });
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1244
|
+
triggerForceWrapup = true;
|
|
1245
|
+
} else if (readOnlyCount >= churnNudgeThreshold) {
|
|
1246
|
+
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1247
|
+
let churnResult;
|
|
1248
|
+
try {
|
|
1249
|
+
churnResult = await engine.assessChurnState({
|
|
1250
|
+
provider,
|
|
1251
|
+
providerName,
|
|
1252
|
+
model,
|
|
1253
|
+
messages,
|
|
1254
|
+
analysis,
|
|
1255
|
+
plan,
|
|
1256
|
+
toolExecutions,
|
|
1257
|
+
readOnlyCount,
|
|
1258
|
+
alreadyRead,
|
|
1259
|
+
iteration,
|
|
1260
|
+
options: { ...options, triggerSource, runId, userId, agentId },
|
|
1261
|
+
});
|
|
1262
|
+
} catch (churnErr) {
|
|
1263
|
+
console.warn(`[Run ${shortenRunId(runId)}] churn_assessment failed: ${summarizeForLog(churnErr?.message || churnErr, 120)}`);
|
|
1264
|
+
churnResult = { assessment: { assessment: 'churn', reason: '' }, usage: 0 };
|
|
1265
|
+
}
|
|
1266
|
+
totalTokens += churnResult.usage || 0;
|
|
1267
|
+
engine.recordRunEvent(userId, runId, 'churn_assessment', {
|
|
1268
|
+
assessment: churnResult.assessment.assessment,
|
|
1269
|
+
reason: churnResult.assessment.reason,
|
|
1270
|
+
readOnlyCount,
|
|
1271
|
+
churnNudgeThreshold,
|
|
1272
|
+
iteration,
|
|
1273
|
+
}, { agentId });
|
|
1274
|
+
|
|
1275
|
+
const churnVerdict = churnResult.assessment.assessment;
|
|
1276
|
+
if (churnVerdict === 'blocked') {
|
|
1277
|
+
triggerForceWrapup = true;
|
|
1278
|
+
forceWrapupSource = 'ai_blocked';
|
|
1279
|
+
} else if (churnVerdict === 'progressing') {
|
|
1280
|
+
// Model is genuinely on track — partially reset so it gets
|
|
1281
|
+
// re-assessed after one more read-only turn rather than immediately.
|
|
1282
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1283
|
+
if (iterMeta) {
|
|
1284
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, churnNudgeThreshold - 1);
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
// 'churn' — model acknowledges it is spinning; inject the nudge
|
|
1288
|
+
// so it can course-correct in the next iteration.
|
|
1289
|
+
messages.push({
|
|
1290
|
+
role: 'system',
|
|
1291
|
+
content: buildReadOnlyChurnGuidance({ readOnlyCount, alreadyRead }),
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (triggerForceWrapup) {
|
|
1297
|
+
console.warn(
|
|
1298
|
+
`[Run ${shortenRunId(runId)}] no_progress_wrapup source=${forceWrapupSource} readOnlyCount=${readOnlyCount}`
|
|
1299
|
+
);
|
|
1300
|
+
engine.updateRunProgress(runId, {
|
|
1301
|
+
currentPhase: 'model',
|
|
1302
|
+
currentStep: 'model:no_progress_wrapup',
|
|
1303
|
+
currentTool: null,
|
|
1304
|
+
currentStepStartedAt: isoNow(),
|
|
1305
|
+
});
|
|
1306
|
+
let wrapTokens = 0;
|
|
1307
|
+
try {
|
|
1308
|
+
const wrapResponse = await withModelCallTimeout(
|
|
1309
|
+
provider.chat(
|
|
1310
|
+
sanitizeConversationMessages([
|
|
1311
|
+
...messages,
|
|
1312
|
+
{
|
|
1313
|
+
role: 'system',
|
|
1314
|
+
content: buildNoProgressWrapupPrompt({
|
|
1315
|
+
readOnlyCount,
|
|
1316
|
+
alreadyRead,
|
|
1317
|
+
platform: options?.source || null,
|
|
1318
|
+
}),
|
|
1319
|
+
},
|
|
1320
|
+
]),
|
|
1321
|
+
[],
|
|
1322
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
1323
|
+
),
|
|
1324
|
+
options,
|
|
1325
|
+
'No-progress wrap-up',
|
|
1326
|
+
);
|
|
1327
|
+
wrapTokens = wrapResponse.usage?.totalTokens || 0;
|
|
1328
|
+
lastContent = sanitizeModelOutput(wrapResponse.content || '', { model });
|
|
1329
|
+
} catch (wrapErr) {
|
|
1330
|
+
console.warn(`[Run ${shortenRunId(runId)}] no_progress_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1331
|
+
}
|
|
1332
|
+
totalTokens += wrapTokens;
|
|
1333
|
+
const usableWrap = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
1334
|
+
if (!usableWrap) {
|
|
1335
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1336
|
+
}
|
|
1337
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1338
|
+
if (conversationId) {
|
|
1339
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1340
|
+
.run(conversationId, 'assistant', lastContent, usableWrap ? wrapTokens : 0);
|
|
1341
|
+
}
|
|
1342
|
+
engine.recordRunEvent(userId, runId, 'no_progress_wrapup_delivered', {
|
|
1343
|
+
iteration,
|
|
1344
|
+
readOnlyCount,
|
|
1345
|
+
source: usableWrap ? 'model' : 'deterministic',
|
|
1346
|
+
forceWrapupSource,
|
|
1347
|
+
stepIndex,
|
|
1348
|
+
}, { agentId });
|
|
1349
|
+
// This wrap-up is a forced, tool-less terminal answer: the model had no
|
|
1350
|
+
// way to call send_message itself. On automatic background runs the plain
|
|
1351
|
+
// result is normally gated, which would silently drop this. Mark it so the
|
|
1352
|
+
// task runtime delivers it — a stuck/blocked scheduled task must still
|
|
1353
|
+
// surface its result instead of going silent.
|
|
1354
|
+
if (
|
|
1355
|
+
(triggerSource === 'schedule' || triggerSource === 'tasks')
|
|
1356
|
+
&& options.deliveryState
|
|
1357
|
+
&& !engine.activeRuns.get(runId)?.messagingSent
|
|
1358
|
+
) {
|
|
1359
|
+
options.deliveryState.terminalWrapup = true;
|
|
1360
|
+
}
|
|
1361
|
+
directAnswerEligible = true;
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1183
1364
|
}
|
|
1184
1365
|
}
|
|
1185
1366
|
|
|
@@ -1410,6 +1591,22 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1410
1591
|
lastContent = '';
|
|
1411
1592
|
continue;
|
|
1412
1593
|
}
|
|
1594
|
+
if (shouldContinueAfterRecoverableToolFailure({
|
|
1595
|
+
lastContent,
|
|
1596
|
+
remainingIterations: iterationBudget.remaining,
|
|
1597
|
+
toolExecutions,
|
|
1598
|
+
})) {
|
|
1599
|
+
engine.recordRunEvent(userId, runId, 'recoverable_tool_failure_continued', {
|
|
1600
|
+
iteration,
|
|
1601
|
+
remainingIterations: iterationBudget.remaining,
|
|
1602
|
+
}, { agentId });
|
|
1603
|
+
messages.push({
|
|
1604
|
+
role: 'system',
|
|
1605
|
+
content: buildRecoverableToolFailureGuidance(toolExecutions),
|
|
1606
|
+
});
|
|
1607
|
+
lastContent = '';
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1413
1610
|
const loopDecision = await engine.decideLoopState({
|
|
1414
1611
|
provider,
|
|
1415
1612
|
providerName,
|
|
@@ -1908,6 +2105,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1908
2105
|
if (runMeta?.terminalInterim) {
|
|
1909
2106
|
break;
|
|
1910
2107
|
}
|
|
2108
|
+
if (isDeliveryTerminated(runMeta, options.deliveryState)) {
|
|
2109
|
+
break;
|
|
2110
|
+
}
|
|
1911
2111
|
}
|
|
1912
2112
|
|
|
1913
2113
|
// Update analysis-paralysis counter after each iteration's tool calls.
|
|
@@ -1924,6 +2124,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1924
2124
|
|
|
1925
2125
|
if (engine.isRunStopped(runId)) break;
|
|
1926
2126
|
if (engine.getRunMeta(runId)?.terminalInterim) break;
|
|
2127
|
+
if (isDeliveryTerminated(engine.getRunMeta(runId), options.deliveryState)) break;
|
|
1927
2128
|
if (engine.getRunMeta(runId)?.widgetSnapshotSaved) break;
|
|
1928
2129
|
if (!engine.activeRuns.has(runId)) break;
|
|
1929
2130
|
}
|
|
@@ -1974,6 +2175,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1974
2175
|
lastContent = 'Widget snapshot updated.';
|
|
1975
2176
|
}
|
|
1976
2177
|
const messagingSent = runMeta?.messagingSent || false;
|
|
2178
|
+
const stagedProactiveReply = normalizeOutgoingMessage(
|
|
2179
|
+
runMeta?.stagedProactiveMessage?.content
|
|
2180
|
+
|| options?.deliveryState?.stagedProactiveMessage?.content
|
|
2181
|
+
|| '',
|
|
2182
|
+
options?.source || null,
|
|
2183
|
+
);
|
|
1977
2184
|
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
1978
2185
|
|
|
1979
2186
|
// Hermes _handle_max_iterations: if the run exhausted its step budget without a
|
|
@@ -2079,6 +2286,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2079
2286
|
if (
|
|
2080
2287
|
!normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
2081
2288
|
&& !messagingSent
|
|
2289
|
+
&& !stagedProactiveReply
|
|
2082
2290
|
&& runMeta?.widgetSnapshotSaved !== true
|
|
2083
2291
|
) {
|
|
2084
2292
|
const explicitNoResponse = (
|
|
@@ -2128,10 +2336,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2128
2336
|
const normalizedLastContent = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
2129
2337
|
let finalResponseText = messagingSent
|
|
2130
2338
|
? (sentMessageText || (normalizedLastContent ? lastContent.trim() : ''))
|
|
2131
|
-
: (normalizedLastContent ? lastContent.trim() : sentMessageText);
|
|
2339
|
+
: (normalizedLastContent ? lastContent.trim() : (stagedProactiveReply || sentMessageText));
|
|
2132
2340
|
const lastFinalDeliveryMessage = normalizeOutgoingMessage(
|
|
2133
2341
|
runMeta?.lastSentMessage
|
|
2134
2342
|
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
2343
|
+
|| runMeta?.stagedProactiveMessage?.content
|
|
2135
2344
|
|| '',
|
|
2136
2345
|
options?.source || null
|
|
2137
2346
|
);
|
|
@@ -2469,7 +2678,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2469
2678
|
}
|
|
2470
2679
|
|
|
2471
2680
|
throw err;
|
|
2681
|
+
} finally {
|
|
2682
|
+
releaseReservation();
|
|
2472
2683
|
}
|
|
2473
2684
|
}
|
|
2474
2685
|
|
|
2475
|
-
module.exports = {
|
|
2686
|
+
module.exports = {
|
|
2687
|
+
getFailureFallbackModelId,
|
|
2688
|
+
runConversation,
|
|
2689
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const FIRST_UPDATE_MS = 30 * 1000;
|
|
4
|
-
const REPEAT_UPDATE_MS =
|
|
5
|
-
const STALL_MS =
|
|
4
|
+
const REPEAT_UPDATE_MS = 120 * 1000;
|
|
5
|
+
const STALL_MS = 240 * 1000;
|
|
6
6
|
const TICK_MS = 15 * 1000;
|
|
7
7
|
|
|
8
8
|
function isoNow() {
|
|
@@ -60,13 +60,13 @@ function evaluateProgressLiveness(runMeta, now = Date.now()) {
|
|
|
60
60
|
|
|
61
61
|
function buildProgressNudge({ stalled = false } = {}) {
|
|
62
62
|
return [
|
|
63
|
-
'
|
|
63
|
+
'Internal progress check for the active messaging run.',
|
|
64
64
|
stalled
|
|
65
65
|
? 'No verified progress has been recorded for the stall threshold.'
|
|
66
66
|
: 'The originating chat has not received a user-visible update for the progress threshold.',
|
|
67
|
-
'Before starting more tool work,
|
|
68
|
-
'
|
|
69
|
-
'
|
|
67
|
+
'Before starting more tool work, decide whether the user has a materially useful new status, a real blocker, a final answer, or no user-visible update is needed.',
|
|
68
|
+
'Only send_interim_update when there is new user-relevant progress or a blocker. Continue silently if nothing materially changed.',
|
|
69
|
+
'Never expose this progress check, internal state, or tool bookkeeping to the user, and do not treat an interim update as final delivery.',
|
|
70
70
|
].join(' ');
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -1,59 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* exit signal. The AI signals completion via task_complete; these
|
|
14
|
-
* numbers only fire when something goes wrong.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
// The iteration count is intentionally NOT a real limit: the agent runs until it
|
|
18
|
-
// signals completion (task_complete / judged terminal), an external blocker, or a
|
|
19
|
-
// genuine stuck-state guard fires (consecutive tool failures, repetition guard,
|
|
20
|
-
// model-failure recoveries). This number only exists so arithmetic/logging have a
|
|
21
|
-
// finite sentinel; it is set far above any real task length so it never cuts a run.
|
|
22
|
-
const UNLIMITED_ITERATIONS = 1_000_000;
|
|
23
|
-
|
|
24
|
-
const DEFAULT_MAX_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
25
|
-
const DEFAULT_WIDGET_MAX_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
26
|
-
const DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
1
|
+
// Limits resolve in priority order: per-run options → agent AI settings → hardcoded defaults.
|
|
2
|
+
// They are safety nets only; task_complete / progress guards are the real exit signals.
|
|
3
|
+
|
|
4
|
+
// The iteration ceiling is a pure runaway safety net, NOT the primary stop signal.
|
|
5
|
+
// A run stops when it makes no real progress (consecutiveReadOnlyIterations cap,
|
|
6
|
+
// which resets the moment the agent does anything state-changing), or when the
|
|
7
|
+
// repetition / tool-failure / model-recovery guards fire, or when the model signals
|
|
8
|
+
// task_complete. These ceilings are set high so they only ever catch a genuine
|
|
9
|
+
// runaway and never guillotine a long, legitimately-progressing complex task.
|
|
10
|
+
const DEFAULT_MAX_ITERATIONS = 250;
|
|
11
|
+
const DEFAULT_WIDGET_MAX_ITERATIONS = 150;
|
|
12
|
+
const DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS = 250;
|
|
27
13
|
// Less aggressive than 0.60 so the model retains file contents it already read for
|
|
28
14
|
// longer, instead of losing them to compaction and re-reading the same files.
|
|
29
15
|
const DEFAULT_COMPACTION_THRESHOLD = 0.80;
|
|
16
|
+
// The real "stop when stuck" guard. Counts consecutive turns of pure reading/
|
|
17
|
+
// searching/inspecting with zero state change; resets to 0 on any concrete progress.
|
|
18
|
+
const DEFAULT_MAX_CONSECUTIVE_READ_ONLY_ITERATIONS = 8;
|
|
30
19
|
const DEFAULT_MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
31
20
|
const DEFAULT_MAX_MODEL_FAILURE_RECOVERIES = 3;
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
const
|
|
22
|
+
const MAX_ALLOWED_ITERATIONS = 400;
|
|
23
|
+
const MAX_ALLOWED_READ_ONLY_ITERATIONS = 25;
|
|
35
24
|
const MAX_ALLOWED_TOOL_FAILURES = 50;
|
|
36
25
|
const MAX_ALLOWED_MODEL_RECOVERIES = 10;
|
|
37
26
|
const MAX_ALLOWED_BUDGET_CHARS = 500_000;
|
|
38
27
|
|
|
39
|
-
/** Return n if finite and positive, otherwise fallback. */
|
|
40
28
|
function finitePositive(n, fallback) {
|
|
41
29
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
/** Clamp n to [lo, hi]; return fallback if not finite. */
|
|
45
32
|
function clampFinite(n, lo, hi, fallback) {
|
|
46
33
|
if (!Number.isFinite(n)) return fallback;
|
|
47
34
|
return Math.min(Math.max(n, lo), hi);
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
/**
|
|
51
|
-
* @param {object} aiSettings - from getAiSettings()
|
|
52
|
-
* @param {string} triggerType - 'chat' | 'schedule' | 'subagent' | etc.
|
|
53
|
-
* @param {string} analysisMode - 'direct_answer' | 'execute' | 'plan_execute'
|
|
54
|
-
* @param {object} options - per-run options (may override anything)
|
|
55
|
-
* @returns {LoopPolicy}
|
|
56
|
-
*/
|
|
57
37
|
function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = 'execute', options = {}) {
|
|
58
38
|
const autonomyPolicy = options.autonomyPolicy && typeof options.autonomyPolicy === 'object'
|
|
59
39
|
? options.autonomyPolicy
|
|
@@ -112,6 +92,16 @@ function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = '
|
|
|
112
92
|
DEFAULT_MAX_MODEL_FAILURE_RECOVERIES,
|
|
113
93
|
);
|
|
114
94
|
|
|
95
|
+
const rawReadOnlyIterations = options.maxConsecutiveReadOnlyIterations != null
|
|
96
|
+
? Number(options.maxConsecutiveReadOnlyIterations)
|
|
97
|
+
: Number(aiSettings.max_consecutive_read_only_iterations);
|
|
98
|
+
const maxConsecutiveReadOnlyIterations = clampFinite(
|
|
99
|
+
Math.floor(rawReadOnlyIterations),
|
|
100
|
+
3,
|
|
101
|
+
MAX_ALLOWED_READ_ONLY_ITERATIONS,
|
|
102
|
+
DEFAULT_MAX_CONSECUTIVE_READ_ONLY_ITERATIONS,
|
|
103
|
+
);
|
|
104
|
+
|
|
115
105
|
// compactionThreshold must be in (0, 1]; clamp to [0.1, 1].
|
|
116
106
|
const compactionThreshold = clampFinite(
|
|
117
107
|
Number(aiSettings.compaction_threshold),
|
|
@@ -122,6 +112,7 @@ function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = '
|
|
|
122
112
|
|
|
123
113
|
return {
|
|
124
114
|
maxIterations,
|
|
115
|
+
maxConsecutiveReadOnlyIterations,
|
|
125
116
|
maxConsecutiveToolFailures,
|
|
126
117
|
maxModelFailureRecoveries,
|
|
127
118
|
|
|
@@ -142,9 +133,6 @@ function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = '
|
|
|
142
133
|
};
|
|
143
134
|
}
|
|
144
135
|
|
|
145
|
-
/**
|
|
146
|
-
* Map a tool name to its result-size category.
|
|
147
|
-
*/
|
|
148
136
|
function getToolCategory(toolName) {
|
|
149
137
|
if (!toolName) return 'default';
|
|
150
138
|
if (/^(read_file|write_file|search_files|list_directory|file_)/.test(toolName)) return 'file';
|
|
@@ -153,9 +141,6 @@ function getToolCategory(toolName) {
|
|
|
153
141
|
return 'default';
|
|
154
142
|
}
|
|
155
143
|
|
|
156
|
-
/**
|
|
157
|
-
* Resolve soft + hard limits for a specific tool from the policy.
|
|
158
|
-
*/
|
|
159
144
|
function resolveToolResultLimits(toolName, policy) {
|
|
160
145
|
const category = getToolCategory(toolName);
|
|
161
146
|
const soft = policy.toolResultBudget[category] ?? policy.toolResultBudget.default;
|
|
@@ -163,4 +148,12 @@ function resolveToolResultLimits(toolName, policy) {
|
|
|
163
148
|
return { softLimit: soft, hardLimit: hard };
|
|
164
149
|
}
|
|
165
150
|
|
|
166
|
-
|
|
151
|
+
function resolveChurnNudgeThreshold(goalContract) {
|
|
152
|
+
const complexity = String(goalContract?.complexity || 'standard').toLowerCase();
|
|
153
|
+
const autonomyLevel = String(goalContract?.autonomyLevel || goalContract?.autonomy_level || 'normal').toLowerCase();
|
|
154
|
+
if (complexity === 'simple') return 2;
|
|
155
|
+
if (complexity === 'complex' || autonomyLevel === 'high') return 5;
|
|
156
|
+
return 3;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { buildLoopPolicy, getToolCategory, resolveToolResultLimits, resolveChurnNudgeThreshold };
|