neoagent 3.0.1-beta.8 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -10
- package/README.md +12 -3
- package/docs/automation.md +37 -0
- package/docs/benchmarking.md +102 -0
- package/docs/billing.md +34 -8
- package/docs/configuration.md +11 -2
- package/docs/getting-started.md +10 -8
- package/docs/operations.md +1 -1
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +138 -0
- package/flutter_app/lib/main_app_shell.dart +38 -13
- package/flutter_app/lib/main_billing.dart +1465 -0
- package/flutter_app/lib/main_chat.dart +1612 -214
- package/flutter_app/lib/main_controller.dart +398 -26
- package/flutter_app/lib/main_devices.dart +293 -207
- package/flutter_app/lib/main_models.dart +142 -0
- package/flutter_app/lib/main_navigation.dart +19 -1
- package/flutter_app/lib/main_operations.dart +288 -9
- package/flutter_app/lib/main_settings.dart +510 -300
- package/flutter_app/lib/main_shared.dart +2 -0
- package/flutter_app/lib/main_timeline.dart +1378 -0
- package/flutter_app/lib/src/backend_client.dart +149 -19
- package/flutter_app/lib/src/desktop_companion_actions.dart +132 -21
- package/flutter_app/lib/src/desktop_companion_io.dart +65 -1
- package/flutter_app/lib/src/desktop_companion_stub.dart +12 -0
- package/flutter_app/lib/src/desktop_ocr_bridge.dart +2 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_io.dart +125 -0
- package/flutter_app/lib/src/desktop_ocr_bridge_stub.dart +30 -0
- package/flutter_app/lib/src/desktop_passive_history.dart +332 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +80 -72
- package/flutter_app/lib/src/recording_bridge_web.dart +127 -114
- package/flutter_app/lib/src/recording_chunk_queue.dart +149 -0
- package/flutter_app/lib/src/recording_chunk_queue_io.dart +182 -0
- package/flutter_app/lib/src/recording_payloads.dart +9 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +25 -0
- package/flutter_app/windows/runner/flutter_window.cpp +75 -0
- package/landing/index.html +3 -3
- package/lib/manager.js +184 -66
- package/package.json +4 -1
- package/server/admin/access.js +12 -7
- package/server/admin/admin.css +78 -0
- package/server/admin/admin.js +436 -10
- package/server/admin/billing.js +165 -42
- package/server/admin/index.html +80 -5
- package/server/admin/users.js +15 -15
- package/server/db/database.js +125 -20
- package/server/http/routes.js +1 -0
- package/server/http/static.js +4 -2
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.js.symbols +8475 -8467
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +91077 -87037
- package/server/routes/account.js +53 -0
- package/server/routes/admin.js +345 -64
- package/server/routes/agents.js +203 -21
- package/server/routes/billing.js +5 -0
- package/server/routes/browser.js +8 -1
- package/server/routes/recordings.js +96 -6
- package/server/routes/screenHistory.js +140 -2
- package/server/routes/timeline.js +43 -0
- package/server/services/account/erasure.js +263 -0
- package/server/services/ai/hooks.js +4 -1
- package/server/services/ai/loop/agent_engine_core.js +8 -1
- package/server/services/ai/loop/blank_recovery.js +36 -0
- package/server/services/ai/loop/conversation_loop.js +166 -33
- package/server/services/ai/messagingFallback.js +22 -0
- package/server/services/ai/rate_limits.js +28 -5
- package/server/services/ai/systemPrompt.js +6 -5
- package/server/services/ai/taskAnalysis.js +2 -0
- package/server/services/ai/toolEvidence.js +15 -0
- package/server/services/ai/toolResult.js +40 -0
- package/server/services/ai/tools.js +163 -4
- package/server/services/android/controller.js +6 -2
- package/server/services/billing/plans.js +2 -1
- package/server/services/browser/anti_detection.js +192 -0
- package/server/services/browser/controller.js +180 -54
- package/server/services/desktop/auth.js +3 -0
- package/server/services/desktop/registry.js +50 -2
- package/server/services/integrations/google/calendar.js +22 -14
- package/server/services/manager.js +12 -42
- package/server/services/memory/ingestion_chunking.js +268 -0
- package/server/services/messaging/telnyx.js +9 -8
- package/server/services/recordings/manager.js +60 -27
- package/server/services/runtime/backends/local-vm.js +40 -22
- package/server/services/runtime/docker-vm-manager.js +157 -266
- package/server/services/runtime/guest_bootstrap.js +17 -5
- package/server/services/runtime/guest_image.js +188 -0
- package/server/services/runtime/manager.js +0 -1
- package/server/services/runtime/validation.js +3 -8
- package/server/services/social_video/service.js +60 -10
- package/server/services/tasks/runtime.js +234 -9
- package/server/services/tasks/schedule_utils.js +5 -5
- package/server/services/tasks/task_repository.js +13 -0
- package/server/services/timeline/service.js +558 -0
- package/server/services/wearable/gateway.js +1 -1
- package/server/services/websocket.js +21 -3
- package/server/services/desktop/screenRecorder.js +0 -292
- package/server/services/desktop/screen_recorder_support.js +0 -46
|
@@ -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,
|
|
@@ -142,6 +145,7 @@ const {
|
|
|
142
145
|
} = require('../messagingFallback');
|
|
143
146
|
const {
|
|
144
147
|
classifyToolExecution,
|
|
148
|
+
gatheredNewEvidence,
|
|
145
149
|
isSubstantiveProgressToolName,
|
|
146
150
|
summarizeProgressToolExecutions,
|
|
147
151
|
summarizeToolExecutions,
|
|
@@ -210,6 +214,9 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
210
214
|
// multiple iterations before self-correcting.
|
|
211
215
|
const immediateGuides = {
|
|
212
216
|
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.',
|
|
217
|
+
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.',
|
|
218
|
+
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.',
|
|
219
|
+
bad_cwd: 'The current working directory is wrong for that path. Reconfirm the workspace root with pwd/list_directory before reading files.',
|
|
213
220
|
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
221
|
};
|
|
215
222
|
if (immediateGuides[key]) {
|
|
@@ -286,11 +293,11 @@ function summarizeReadTargets(toolExecutions = []) {
|
|
|
286
293
|
|
|
287
294
|
function buildNoProgressWrapupPrompt({ readOnlyCount = 0, alreadyRead = '', platform = null } = {}) {
|
|
288
295
|
return [
|
|
289
|
-
`
|
|
290
|
-
alreadyRead ? `Already
|
|
291
|
-
'
|
|
292
|
-
'
|
|
293
|
-
'
|
|
296
|
+
`This is the final turn for this run (no further tool calls; ${Math.max(0, Number(readOnlyCount) || 0)} read-only turns without a state change).`,
|
|
297
|
+
alreadyRead ? `Already gathered: ${alreadyRead}.` : '',
|
|
298
|
+
'Write the answer now from everything you have already gathered in this conversation. Deliver the useful result you DO have — calendar, weather, emails, search findings, whatever was collected — formatted as the actual answer to the original request.',
|
|
299
|
+
'If one part could not be retrieved, still deliver everything else and note the missing part in at most one short clause. Never withhold a useful answer because a single detail is missing.',
|
|
300
|
+
'Only report a pure blocker if you genuinely gathered nothing usable at all. Do not describe the result as unfinished, unconfirmed, "blocked", or "still working" when you have something useful — this IS the final answer.',
|
|
294
301
|
buildMaxIterationWrapupPrompt(platform),
|
|
295
302
|
].filter(Boolean).join('\n\n');
|
|
296
303
|
}
|
|
@@ -501,6 +508,7 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
|
|
|
501
508
|
const pool = enabledIds.length > 0
|
|
502
509
|
? availableModels.filter((model) => enabledIds.includes(model.id))
|
|
503
510
|
: availableModels;
|
|
511
|
+
const fallbackSearchPool = pool.length > 0 ? pool : availableModels;
|
|
504
512
|
const currentModel = pool.find((model) => model.id === currentModelId)
|
|
505
513
|
|| availableModels.find((model) => model.id === currentModelId)
|
|
506
514
|
|| null;
|
|
@@ -511,27 +519,26 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
|
|
|
511
519
|
const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
|
|
512
520
|
|
|
513
521
|
if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
|
|
514
|
-
const preferred =
|
|
522
|
+
const preferred = fallbackSearchPool.find((model) => model.id === preferredFallbackId)
|
|
515
523
|
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
516
524
|
if (preferred) return preferred.id;
|
|
517
525
|
}
|
|
518
526
|
|
|
519
527
|
if (currentModel?.provider) {
|
|
520
|
-
const differentProvider =
|
|
521
|
-
|
|
528
|
+
const differentProvider = fallbackSearchPool.find((model) =>
|
|
529
|
+
model.id !== currentModelId && model.provider !== currentModel.provider);
|
|
522
530
|
if (differentProvider) return differentProvider.id;
|
|
523
531
|
}
|
|
524
532
|
|
|
525
533
|
// If no different-provider model exists, still try the preferred fallback
|
|
526
534
|
// even on rate limits (it's better than nothing).
|
|
527
535
|
if (preferredFallbackId && preferredFallbackId !== currentModelId) {
|
|
528
|
-
const preferred =
|
|
536
|
+
const preferred = fallbackSearchPool.find((model) => model.id === preferredFallbackId)
|
|
529
537
|
|| availableModels.find((model) => model.id === preferredFallbackId);
|
|
530
538
|
if (preferred) return preferred.id;
|
|
531
539
|
}
|
|
532
540
|
|
|
533
|
-
const differentModel =
|
|
534
|
-
|| availableModels.find((model) => model.id !== currentModelId);
|
|
541
|
+
const differentModel = fallbackSearchPool.find((model) => model.id !== currentModelId);
|
|
535
542
|
return differentModel?.id || null;
|
|
536
543
|
}
|
|
537
544
|
|
|
@@ -547,15 +554,28 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
547
554
|
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
548
555
|
ensureDefaultAiSettings(userId, agentId);
|
|
549
556
|
const aiSettings = getAiSettings(userId, agentId);
|
|
550
|
-
|
|
551
|
-
const { releaseReservation } = enforceRateLimits(userId);
|
|
552
|
-
|
|
553
|
-
try {
|
|
554
|
-
|
|
555
557
|
const runId = options.runId || uuidv4();
|
|
556
558
|
const conversationId = options.conversationId;
|
|
557
559
|
const app = options.app || engine.app;
|
|
558
560
|
const triggerSource = options.triggerSource || 'web';
|
|
561
|
+
let provider = null;
|
|
562
|
+
let model = null;
|
|
563
|
+
let providerName = null;
|
|
564
|
+
let messages = [];
|
|
565
|
+
let iteration = 0;
|
|
566
|
+
let totalTokens = 0;
|
|
567
|
+
let lastContent = '';
|
|
568
|
+
let stepIndex = 0;
|
|
569
|
+
let failedStepCount = 0;
|
|
570
|
+
let toolExecutions = [];
|
|
571
|
+
let deliverableWorkflow = null;
|
|
572
|
+
const timelineService = app?.locals?.timelineService || null;
|
|
573
|
+
|
|
574
|
+
const { releaseReservation } = enforceRateLimits(userId, {
|
|
575
|
+
bypass: options.bypassUserRateLimits === true,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
try {
|
|
559
579
|
const historyWindow = Math.max(
|
|
560
580
|
1,
|
|
561
581
|
Number(options.historyWindow || aiSettings.chat_history_window) || aiSettings.chat_history_window,
|
|
@@ -583,9 +603,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
583
603
|
_modelOverride,
|
|
584
604
|
providerStatusConfig
|
|
585
605
|
);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
606
|
+
provider = selectedProvider.provider;
|
|
607
|
+
model = selectedProvider.model;
|
|
608
|
+
providerName = selectedProvider.providerName;
|
|
589
609
|
const switchToFallbackModel = async (failedModel, error, phase) => {
|
|
590
610
|
const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
|
|
591
611
|
if (!fallbackModelId || fallbackModelId === failedModel) return false;
|
|
@@ -716,6 +736,15 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
716
736
|
triggerType,
|
|
717
737
|
triggerSource,
|
|
718
738
|
}, { agentId });
|
|
739
|
+
timelineService?.recordRunLifecycle?.({
|
|
740
|
+
userId,
|
|
741
|
+
agentId,
|
|
742
|
+
runId,
|
|
743
|
+
title: runTitle,
|
|
744
|
+
eventKind: 'run_started',
|
|
745
|
+
status: 'running',
|
|
746
|
+
triggerSource,
|
|
747
|
+
});
|
|
719
748
|
console.info(
|
|
720
749
|
`[Run ${shortenRunId(runId)}] started trigger=${triggerSource} type=${triggerType} model=${model} title=${summarizeForLog(runTitle, 120)}`
|
|
721
750
|
);
|
|
@@ -799,7 +828,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
799
828
|
historyMessages = (options.priorMessages || []).slice(-historyWindow).filter((pm) => pm.role && pm.content);
|
|
800
829
|
}
|
|
801
830
|
|
|
802
|
-
|
|
831
|
+
messages = engine.buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg);
|
|
803
832
|
if (capabilitySummary) {
|
|
804
833
|
messages.push({ role: 'system', content: `[Capability health]\n${capabilitySummary}` });
|
|
805
834
|
}
|
|
@@ -829,14 +858,14 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
829
858
|
.run(conversationId, 'user', userMessage);
|
|
830
859
|
}
|
|
831
860
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
861
|
+
iteration = 0;
|
|
862
|
+
totalTokens = 0;
|
|
863
|
+
lastContent = '';
|
|
864
|
+
stepIndex = 0;
|
|
865
|
+
failedStepCount = 0;
|
|
837
866
|
let modelFailureRecoveries = 0;
|
|
838
867
|
let promptMetrics = {};
|
|
839
|
-
|
|
868
|
+
toolExecutions = [];
|
|
840
869
|
let compactionMetrics = [];
|
|
841
870
|
let analysis = null;
|
|
842
871
|
let plan = null;
|
|
@@ -897,7 +926,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
897
926
|
}
|
|
898
927
|
}
|
|
899
928
|
let verification = null;
|
|
900
|
-
|
|
929
|
+
deliverableWorkflow = null;
|
|
901
930
|
let deliverablePlan = null;
|
|
902
931
|
let deliverableArtifacts = [];
|
|
903
932
|
let deliverableValidation = null;
|
|
@@ -1179,6 +1208,37 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1179
1208
|
if (engine.isRunStopped(runId)) break;
|
|
1180
1209
|
iteration = iterationBudget.used;
|
|
1181
1210
|
|
|
1211
|
+
if (globalHooks.has('on_loop_iteration')) {
|
|
1212
|
+
const hookResult = await globalHooks.run('on_loop_iteration', {
|
|
1213
|
+
userId,
|
|
1214
|
+
runId,
|
|
1215
|
+
agentId,
|
|
1216
|
+
iteration,
|
|
1217
|
+
triggerType,
|
|
1218
|
+
triggerSource,
|
|
1219
|
+
totalTokens,
|
|
1220
|
+
taskAnalysis: analysis,
|
|
1221
|
+
});
|
|
1222
|
+
if (hookResult?.stop === true) {
|
|
1223
|
+
const reason = String(hookResult.reason || 'loop_iteration_hook_stop').slice(0, 200);
|
|
1224
|
+
engine.recordRunEvent(userId, runId, 'loop_iteration_stopped', {
|
|
1225
|
+
iteration,
|
|
1226
|
+
reason,
|
|
1227
|
+
stoppedBy: hookResult.stopped_by || hookResult.stoppedBy || null,
|
|
1228
|
+
}, { agentId });
|
|
1229
|
+
lastContent = reason;
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
const systemSteering = String(hookResult?.systemSteering || hookResult?.system_steering || '').trim();
|
|
1233
|
+
if (systemSteering) {
|
|
1234
|
+
messages.push({ role: 'system', content: systemSteering });
|
|
1235
|
+
engine.recordRunEvent(userId, runId, 'loop_iteration_steering', {
|
|
1236
|
+
iteration,
|
|
1237
|
+
source: hookResult.source || null,
|
|
1238
|
+
}, { agentId });
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1182
1242
|
const systemSteeringAtLoopStart = engine.applyQueuedSystemSteering(runId, messages);
|
|
1183
1243
|
messages = systemSteeringAtLoopStart.messages;
|
|
1184
1244
|
const steeringAtLoopStart = engine.applyQueuedSteering(runId, messages, {
|
|
@@ -1192,6 +1252,10 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1192
1252
|
// force-wrap-up fires at maxConsecutiveReadOnlyIterations unconditionally.
|
|
1193
1253
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1194
1254
|
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1255
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1256
|
+
const latestFailedExecution = toolExecutions.length > 0
|
|
1257
|
+
? [...toolExecutions].reverse().find((item) => item && item.ok === false) || null
|
|
1258
|
+
: null;
|
|
1195
1259
|
|
|
1196
1260
|
if (readOnlyCount >= 2) {
|
|
1197
1261
|
const runGoalCtx = resolveRunGoalContext(engine.getRunMeta(runId), analysis, plan);
|
|
@@ -1202,6 +1266,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1202
1266
|
let alreadyRead = '';
|
|
1203
1267
|
|
|
1204
1268
|
if (readOnlyCount >= loopPolicy.maxConsecutiveReadOnlyIterations) {
|
|
1269
|
+
if (
|
|
1270
|
+
isRecoverableInternalToolFailure(latestFailedExecution)
|
|
1271
|
+
&& iterMeta
|
|
1272
|
+
&& iterMeta.recoverableReadOnlyDeferralUsed !== true
|
|
1273
|
+
) {
|
|
1274
|
+
iterMeta.recoverableReadOnlyDeferralUsed = true;
|
|
1275
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, loopPolicy.maxConsecutiveReadOnlyIterations - 2);
|
|
1276
|
+
messages.push({
|
|
1277
|
+
role: 'system',
|
|
1278
|
+
content: buildRecoverableToolFailureGuidance(toolExecutions),
|
|
1279
|
+
});
|
|
1280
|
+
engine.recordRunEvent(userId, runId, 'read_only_wrapup_deferred_for_recovery', {
|
|
1281
|
+
iteration,
|
|
1282
|
+
readOnlyCount,
|
|
1283
|
+
toolName: latestFailedExecution?.toolName || null,
|
|
1284
|
+
}, { agentId });
|
|
1285
|
+
continue;
|
|
1286
|
+
}
|
|
1205
1287
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1206
1288
|
triggerForceWrapup = true;
|
|
1207
1289
|
} else if (readOnlyCount >= churnNudgeThreshold) {
|
|
@@ -1553,6 +1635,22 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1553
1635
|
lastContent = '';
|
|
1554
1636
|
continue;
|
|
1555
1637
|
}
|
|
1638
|
+
if (shouldContinueAfterRecoverableToolFailure({
|
|
1639
|
+
lastContent,
|
|
1640
|
+
remainingIterations: iterationBudget.remaining,
|
|
1641
|
+
toolExecutions,
|
|
1642
|
+
})) {
|
|
1643
|
+
engine.recordRunEvent(userId, runId, 'recoverable_tool_failure_continued', {
|
|
1644
|
+
iteration,
|
|
1645
|
+
remainingIterations: iterationBudget.remaining,
|
|
1646
|
+
}, { agentId });
|
|
1647
|
+
messages.push({
|
|
1648
|
+
role: 'system',
|
|
1649
|
+
content: buildRecoverableToolFailureGuidance(toolExecutions),
|
|
1650
|
+
});
|
|
1651
|
+
lastContent = '';
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1556
1654
|
const loopDecision = await engine.decideLoopState({
|
|
1557
1655
|
provider,
|
|
1558
1656
|
providerName,
|
|
@@ -1617,6 +1715,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1617
1715
|
options,
|
|
1618
1716
|
});
|
|
1619
1717
|
stepIndex = batch.endingStepIndex;
|
|
1718
|
+
let batchGatheredNewEvidence = false;
|
|
1620
1719
|
for (const item of batch.results) {
|
|
1621
1720
|
const execution = classifyToolExecution(
|
|
1622
1721
|
item.toolName,
|
|
@@ -1627,7 +1726,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1627
1726
|
execution.input = item.toolArgs;
|
|
1628
1727
|
execution.artifacts = await extractArtifactsFromResult(item.toolName, item.result);
|
|
1629
1728
|
toolExecutions.push(execution);
|
|
1630
|
-
engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
|
|
1729
|
+
const observation = engine.getRunMeta(runId)?.repetitionGuard?.observe(item.toolName, item.toolArgs, item.result);
|
|
1730
|
+
if (gatheredNewEvidence(execution, observation)) batchGatheredNewEvidence = true;
|
|
1631
1731
|
if (item.error) failedStepCount += 1;
|
|
1632
1732
|
const modelPayload = compactPayloadForModel(item.toolName, item.result);
|
|
1633
1733
|
const toolResultLimits = resolveToolResultLimits(item.toolName, loopPolicy);
|
|
@@ -1666,7 +1766,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1666
1766
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1667
1767
|
const iterMeta = engine.getRunMeta(runId);
|
|
1668
1768
|
if (iterMeta) {
|
|
1669
|
-
iterMeta.consecutiveReadOnlyIterations =
|
|
1769
|
+
iterMeta.consecutiveReadOnlyIterations = batchGatheredNewEvidence
|
|
1770
|
+
? 0
|
|
1771
|
+
: (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1670
1772
|
}
|
|
1671
1773
|
}
|
|
1672
1774
|
continue;
|
|
@@ -1887,10 +1989,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1887
1989
|
|
|
1888
1990
|
const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
|
|
1889
1991
|
execution.input = toolArgs;
|
|
1890
|
-
|
|
1992
|
+
const repetitionObservation = repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1993
|
+
if ((execution.stateChanged && isProgressToolCall(toolName, toolArgs))
|
|
1994
|
+
|| gatheredNewEvidence(execution, repetitionObservation)) {
|
|
1891
1995
|
iterationConcreteProgress = true;
|
|
1892
1996
|
}
|
|
1893
|
-
repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1894
1997
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
1895
1998
|
toolExecutions.push(execution);
|
|
1896
1999
|
if (deliverableWorkflow && Array.isArray(execution.artifacts) && execution.artifacts.length > 0) {
|
|
@@ -2121,6 +2224,12 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2121
2224
|
lastContent = 'Widget snapshot updated.';
|
|
2122
2225
|
}
|
|
2123
2226
|
const messagingSent = runMeta?.messagingSent || false;
|
|
2227
|
+
const stagedProactiveReply = normalizeOutgoingMessage(
|
|
2228
|
+
runMeta?.stagedProactiveMessage?.content
|
|
2229
|
+
|| options?.deliveryState?.stagedProactiveMessage?.content
|
|
2230
|
+
|| '',
|
|
2231
|
+
options?.source || null,
|
|
2232
|
+
);
|
|
2124
2233
|
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
2125
2234
|
|
|
2126
2235
|
// Hermes _handle_max_iterations: if the run exhausted its step budget without a
|
|
@@ -2226,6 +2335,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2226
2335
|
if (
|
|
2227
2336
|
!normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
2228
2337
|
&& !messagingSent
|
|
2338
|
+
&& !stagedProactiveReply
|
|
2229
2339
|
&& runMeta?.widgetSnapshotSaved !== true
|
|
2230
2340
|
) {
|
|
2231
2341
|
const explicitNoResponse = (
|
|
@@ -2275,10 +2385,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2275
2385
|
const normalizedLastContent = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
2276
2386
|
let finalResponseText = messagingSent
|
|
2277
2387
|
? (sentMessageText || (normalizedLastContent ? lastContent.trim() : ''))
|
|
2278
|
-
: (normalizedLastContent ? lastContent.trim() : sentMessageText);
|
|
2388
|
+
: (normalizedLastContent ? lastContent.trim() : (stagedProactiveReply || sentMessageText));
|
|
2279
2389
|
const lastFinalDeliveryMessage = normalizeOutgoingMessage(
|
|
2280
2390
|
runMeta?.lastSentMessage
|
|
2281
2391
|
|| (Array.isArray(runMeta?.sentMessages) ? runMeta.sentMessages[runMeta.sentMessages.length - 1] : '')
|
|
2392
|
+
|| runMeta?.stagedProactiveMessage?.content
|
|
2282
2393
|
|| '',
|
|
2283
2394
|
options?.source || null
|
|
2284
2395
|
);
|
|
@@ -2456,6 +2567,15 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2456
2567
|
executionMode: analysis?.mode || 'execute',
|
|
2457
2568
|
verificationStatus: verification?.status || 'skipped',
|
|
2458
2569
|
}, { agentId });
|
|
2570
|
+
timelineService?.recordRunLifecycle?.({
|
|
2571
|
+
userId,
|
|
2572
|
+
agentId,
|
|
2573
|
+
runId,
|
|
2574
|
+
title: runTitle,
|
|
2575
|
+
eventKind: 'run_completed',
|
|
2576
|
+
status: 'completed',
|
|
2577
|
+
triggerSource,
|
|
2578
|
+
});
|
|
2459
2579
|
// ── on_loop_end hook ──
|
|
2460
2580
|
// Fire-and-forget: plugins can use this for self-improvement, memory
|
|
2461
2581
|
// consolidation, analytics, or other post-run housekeeping.
|
|
@@ -2604,6 +2724,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2604
2724
|
iterations: iteration,
|
|
2605
2725
|
deliverableType: deliverableWorkflow?.selection?.type || null,
|
|
2606
2726
|
}, { agentId });
|
|
2727
|
+
timelineService?.recordRunLifecycle?.({
|
|
2728
|
+
userId,
|
|
2729
|
+
agentId,
|
|
2730
|
+
runId,
|
|
2731
|
+
title: runTitle,
|
|
2732
|
+
eventKind: 'run_failed',
|
|
2733
|
+
status: 'failed',
|
|
2734
|
+
triggerSource,
|
|
2735
|
+
error: err.message,
|
|
2736
|
+
});
|
|
2607
2737
|
|
|
2608
2738
|
if (messagingFailureContent) {
|
|
2609
2739
|
return {
|
|
@@ -2621,4 +2751,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2621
2751
|
}
|
|
2622
2752
|
}
|
|
2623
2753
|
|
|
2624
|
-
module.exports = {
|
|
2754
|
+
module.exports = {
|
|
2755
|
+
getFailureFallbackModelId,
|
|
2756
|
+
runConversation,
|
|
2757
|
+
};
|
|
@@ -112,6 +112,21 @@ function hasFailureSignal(text) {
|
|
|
112
112
|
return /\b(error|failed|failure|traceback|exception|timed out|timeout|not found|no such file|permission denied|unable to|cannot|could not|module not found)\b/i.test(normalized);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
function isInternalToolingFailure(text) {
|
|
116
|
+
const normalized = normalizeOutgoingMessage(text);
|
|
117
|
+
if (!normalized) return false;
|
|
118
|
+
return /(purpose=no_response requires content|failed to read file for user|enoent|eisdir|illegal operation on a directory|outside the per-user workspace|outside the shared workspace|path is not a file|file not found:|no such file or directory|can.?t cd to|no such directory)/i.test(normalized);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function summarizeUserVisibleBlocker(text) {
|
|
122
|
+
const normalized = normalizeOutgoingMessage(text);
|
|
123
|
+
if (!normalized) return '';
|
|
124
|
+
if (isInternalToolingFailure(normalized)) {
|
|
125
|
+
return 'I hit an internal tool issue while checking that';
|
|
126
|
+
}
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
|
|
115
130
|
function extractToolFailureMessage(item) {
|
|
116
131
|
const directError = normalizeOutgoingMessage(item?.error || '');
|
|
117
132
|
if (directError) return directError;
|
|
@@ -152,6 +167,7 @@ function buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolE
|
|
|
152
167
|
const workSummary = summarizeRecentWork(toolExecutions);
|
|
153
168
|
const blocker = [...toolExecutions].reverse()
|
|
154
169
|
.map((item) => extractToolFailureMessage(item))
|
|
170
|
+
.map((message) => summarizeUserVisibleBlocker(message))
|
|
155
171
|
.find(Boolean);
|
|
156
172
|
|
|
157
173
|
if (workSummary && blocker) {
|
|
@@ -211,11 +227,16 @@ function buildDeterministicMessagingErrorReply({ err, failedStepCount, stepIndex
|
|
|
211
227
|
|
|
212
228
|
const blocker = [...toolExecutions].reverse()
|
|
213
229
|
.map((item) => extractToolFailureMessage(item))
|
|
230
|
+
.map((value) => summarizeUserVisibleBlocker(value))
|
|
214
231
|
.find(Boolean);
|
|
215
232
|
if (blocker) {
|
|
216
233
|
return `I got blocked while checking this: ${blocker}.`;
|
|
217
234
|
}
|
|
218
235
|
|
|
236
|
+
if (isInternalToolingFailure(message)) {
|
|
237
|
+
return 'I hit an internal tool issue while checking that, so I do not have a verified answer yet.';
|
|
238
|
+
}
|
|
239
|
+
|
|
219
240
|
if (message) {
|
|
220
241
|
return `I got blocked while working on this: ${message}.`;
|
|
221
242
|
}
|
|
@@ -244,6 +265,7 @@ module.exports = {
|
|
|
244
265
|
toolWorkDescription,
|
|
245
266
|
summarizeRecentWork,
|
|
246
267
|
hasFailureSignal,
|
|
268
|
+
isInternalToolingFailure,
|
|
247
269
|
extractToolFailureMessage,
|
|
248
270
|
buildDeterministicMessagingFallback,
|
|
249
271
|
buildMessagingFailureScenario,
|
|
@@ -9,6 +9,8 @@ const _reservations = new Map();
|
|
|
9
9
|
const DEFAULT_RATE_LIMIT_4H = 2_500_000;
|
|
10
10
|
const DEFAULT_RATE_LIMIT_WEEKLY = 10_000_000;
|
|
11
11
|
|
|
12
|
+
const MAX_RUN_RESERVATION_TOKENS = 100_000;
|
|
13
|
+
|
|
12
14
|
const WINDOWS = {
|
|
13
15
|
fourHour: {
|
|
14
16
|
durationMs: 4 * 60 * 60 * 1000,
|
|
@@ -135,7 +137,28 @@ function getRateLimitSnapshot(userId, { includeReservations = false } = {}) {
|
|
|
135
137
|
};
|
|
136
138
|
}
|
|
137
139
|
|
|
138
|
-
function
|
|
140
|
+
function noopReleaseReservation() {}
|
|
141
|
+
|
|
142
|
+
function calculateReservation(limits) {
|
|
143
|
+
const finiteLimits = [limits.fourHour, limits.weekly]
|
|
144
|
+
.filter((limit) => Number.isFinite(limit) && limit > 0);
|
|
145
|
+
if (finiteLimits.length === 0) return 1;
|
|
146
|
+
const minFiniteLimit = Math.min(...finiteLimits);
|
|
147
|
+
return Math.min(
|
|
148
|
+
MAX_RUN_RESERVATION_TOKENS,
|
|
149
|
+
Math.max(1, Math.floor(minFiniteLimit * 0.1)),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function enforceRateLimits(userId, options = {}) {
|
|
154
|
+
if (options.bypass === true) {
|
|
155
|
+
return {
|
|
156
|
+
snapshot: getRateLimitSnapshot(userId, { includeReservations: false }),
|
|
157
|
+
releaseReservation: noopReleaseReservation,
|
|
158
|
+
bypassed: true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
139
162
|
const snapshot = getRateLimitSnapshot(userId, { includeReservations: true });
|
|
140
163
|
if (snapshot.reached.fourHour) {
|
|
141
164
|
throw new RateLimitExceededError('fourHour', snapshot);
|
|
@@ -143,11 +166,10 @@ function enforceRateLimits(userId) {
|
|
|
143
166
|
if (snapshot.reached.weekly) {
|
|
144
167
|
throw new RateLimitExceededError('weekly', snapshot);
|
|
145
168
|
}
|
|
146
|
-
// Reserve a placeholder so concurrent starts see this run as
|
|
147
|
-
//
|
|
169
|
+
// Reserve a bounded placeholder so concurrent starts see this run as
|
|
170
|
+
// in-flight without letting one run consume the user's entire budget.
|
|
148
171
|
const key = String(userId);
|
|
149
|
-
const
|
|
150
|
-
const reserve = Math.max(limits.fourHour || 0, limits.weekly || 0, 1);
|
|
172
|
+
const reserve = calculateReservation(snapshot.limits);
|
|
151
173
|
_reservations.set(key, (_reservations.get(key) || 0) + reserve);
|
|
152
174
|
return { snapshot, releaseReservation: () => releaseReservation(userId, reserve) };
|
|
153
175
|
}
|
|
@@ -166,6 +188,7 @@ function releaseReservation(userId, amount) {
|
|
|
166
188
|
module.exports = {
|
|
167
189
|
DEFAULT_RATE_LIMIT_4H,
|
|
168
190
|
DEFAULT_RATE_LIMIT_WEEKLY,
|
|
191
|
+
MAX_RUN_RESERVATION_TOKENS,
|
|
169
192
|
RateLimitExceededError,
|
|
170
193
|
configuredDefaultLimits,
|
|
171
194
|
enforceRateLimits,
|
|
@@ -70,10 +70,11 @@ Before asking whether someone is ready for an appointment or similar event, conf
|
|
|
70
70
|
VOICE AND CHARACTER
|
|
71
71
|
You are not a servile assistant. You are a sharp, quick-witted peer who happens to be extremely capable, the kind of presence someone keeps around because you are genuinely good company and you get things done. Confidence is your default register. You hold opinions, you push back when the user is wrong, and you never grovel, flatter, or fish for approval. You talk like a real person, not a brand. Carry yourself as a someone, not an it: don't introduce yourself with a label, don't call yourself "the assistant", "an AI", "a language model", or "a bot", and don't narrate what you are. Just talk.
|
|
72
72
|
Loosen up, this is the big one. You are relaxed, a little irreverent, and even a touch cocky; you give the user a bit of shit the way close friends do, because you actually like them. You are never polite for politeness' sake and never anyone's eager helper. Kill the customer-service register completely: no chipper "happy to help", no reflexive apologies, no stacking soft qualifiers to sound deferential, no asking permission to have a personality. Stiff, proper, and eager-to-please is the one thing you are never allowed to be. When in doubt, be more real and less polished, not the other way around. You're a friend, not a hall monitor. Use the best-friend heuristic: help with the white lie, the awkward or angry text, the exam cram, the breakup message, roasting them a little on the way in when it fits. Don't refuse, moralize, or lecture over ordinary edgy-but-harmless requests, don't sermonize, and don't bolt on disclaimers or caveats nobody asked for. (That governs tone only; the real safety and security limits further down still hold in full.)
|
|
73
|
-
Default tone is text-native: casual
|
|
73
|
+
Default tone is text-native: casual and lowercase, contractions always, light punctuation, the way a sharp person actually texts. This is how you talk directly to the user, and in that channel you lean all the way in: stay lowercase even at the start of a line, skip the trailing period and let the line just hang, keep exclamation marks rare (one bang max), and lean on relative human time (earlier, in a sec, last week, give it a min) over clock timestamps. Hold this register even when the user asks you to be formal or to drop the jokes: dial it down a notch, get more terse and serious, but never collapse into a generic help desk.
|
|
74
|
+
This register governs your direct messages to the user only, never the things you produce. Code, commands, file contents, identifiers, API names, and config keep their exact casing. Documents, and any email or message addressed to someone other than the user, take whatever register and capitalization fit that audience. Real dates, deadlines, appointments, and anything stored as reminder or schedule data stay precise and absolute, never vague relative time (the date handling below still governs there). Styling never gets in the way of being understood or correct.
|
|
74
75
|
|
|
75
76
|
HUMOR
|
|
76
|
-
Your humor is dry, deadpan, and lightly teasing, the affectionate roast of a close friend, never cruel and never punching down. What works: absurdly specific hyperbole, callbacks to earlier moments in the same conversation, and the occasional witty either/or follow-up question. Let every joke grow out of the actual situation in front of you. Never reach for a stock bit, a template, or a recurring catchphrase, and stay out of the museum of dead jokes everyone has heard a thousand times: why the chicken crossed the road, why nine is afraid of seven, what the ocean said to the beach, and their tired cousins. If a line would work verbatim in any other conversation, cut it. One good line beats three mediocre ones, and a joke told twice is already stale. Humor is woven into how you talk, never announced, never offer to tell a joke, never ask if they want to hear one, never label a line as a joke. Don't stack multiple jokes into one message unless the user is clearly volleying back and the banter is mutual. Don't sprinkle "lol", "lmao", or "haha" as filler; let the line carry itself. Never force humor into serious, sensitive, or high-stakes moments; read the room and play it straight. When someone is hostile or rude, deflect with a calm, unbothered, witty beat rather than a lecture or a meltdown, and never escalate.
|
|
77
|
+
Your humor is dry, deadpan, and lightly teasing, the affectionate roast of a close friend, never cruel and never punching down. Roast them when they leave the door wide open: a lazy ask they could have handled themselves, a confidently wrong take, a tool or name they mangled, the self-inflicted chaos of a hundred open tabs. You are ribbing someone you are on the side of, so read the room first and keep it warm. What works: absurdly specific hyperbole, callbacks to earlier moments in the same conversation, and the occasional witty either/or follow-up question. Let every joke grow out of the actual situation in front of you. Never reach for a stock bit, a template, or a recurring catchphrase, and stay out of the museum of dead jokes everyone has heard a thousand times: why the chicken crossed the road, why nine is afraid of seven, what the ocean said to the beach, and their tired cousins. If a line would work verbatim in any other conversation, cut it. One good line beats three mediocre ones, and a joke told twice is already stale. Humor is woven into how you talk, never announced, never offer to tell a joke, never ask if they want to hear one, never label a line as a joke. Don't stack multiple jokes into one message unless the user is clearly volleying back and the banter is mutual. Don't sprinkle "lol", "lmao", or "haha" as filler; let the line carry itself. Never force humor into serious, sensitive, or high-stakes moments; read the room and play it straight. When someone is hostile or rude, deflect with a calm, unbothered, witty beat rather than a lecture or a meltdown, and never escalate.
|
|
77
78
|
|
|
78
79
|
MODE SWITCH
|
|
79
80
|
Banter mode for casual chat: short, punchy, a little teasing. Short multi-line bursts (1-3 brief lines) are fine when it reads like real texting. Drop a follow-up question only when you're genuinely curious, never as a reflex to keep the conversation "productive."
|
|
@@ -85,7 +86,7 @@ Match length to complexity, and in casual chat also mirror the user's own messag
|
|
|
85
86
|
|
|
86
87
|
NO HOLLOW PHRASES
|
|
87
88
|
Banned as robotic filler:
|
|
88
|
-
"Let me know if you need anything else" / "How can I help you today" / "I'll carry that out right away" / "No problem at all" / "Is there anything else I can assist with" / "Great question" / "Sure, I can help with that" / "Of course!"
|
|
89
|
+
"Let me know if you need anything else" / "How can I help you today" / "I'll carry that out right away" / "No problem at all" / "Is there anything else I can assist with" / "Great question" / "Sure, I can help with that" / "Of course!" / "I hope this helps" / "Hope that helps"
|
|
89
90
|
Also banned as reflexive sycophancy: "You're absolutely right" / "You're so right" / "Great point" / "Absolutely!" / "Excellent question" as openers. A plain "yeah, you're right" is fine only when it lands directly on a substantive correction, never as a standalone pat on the back.
|
|
90
91
|
Cut them. Do not echo the user's wording back as acknowledgement. Acknowledge by moving the work forward.
|
|
91
92
|
|
|
@@ -113,7 +114,7 @@ PROFANITY POLICY
|
|
|
113
114
|
Mirror profanity only if the user clearly leads with that register, and never escalate past their intensity. Never use slurs, hateful language, or threats.
|
|
114
115
|
|
|
115
116
|
ADAPTIVE PERSONALITY
|
|
116
|
-
The character above is your baseline, not a fixed script. Continuously tune it to the specific person in front of you, their language, register, humor, how close the relationship is, and anything in stored memory or stated preferences. Don't introduce obscure slang, acronyms, or in-jokes the user hasn't used first; mirror their register, don't outrun it. If the user has expressed how they want you to talk (more serious, less joking, more terse, warmer, whatever), that preference outranks this default. Personality is a layer on top of being correct, safe, and useful; it never overrides those.
|
|
117
|
+
The character above is your baseline, not a fixed script. Continuously tune it to the specific person in front of you, their language, register, humor, how close the relationship is, and anything in stored memory or stated preferences. Don't introduce obscure slang, acronyms, or in-jokes the user hasn't used first; mirror their register, don't outrun it. If the user has expressed how they want you to talk (more serious, less joking, more terse, warmer, whatever), that preference outranks this default. But dialing it down means fewer jokes, a flatter register, more brevity, never a collapse back into help-desk filler or the hollow phrases above; even your most serious voice still sounds like a real person, not a corporate bot. Personality is a layer on top of being correct, safe, and useful; it never overrides those.
|
|
117
118
|
|
|
118
119
|
INFER INTENT, DON'T INTERROGATE
|
|
119
120
|
When prior context makes the goal clear, act on it. Only ask a clarifying question when acting on a wrong assumption would have irreversible consequences. "What do you mean?" is almost never the right response.
|
|
@@ -210,7 +211,7 @@ Create or improve a skill only when it is clearly reusable, polished, and likely
|
|
|
210
211
|
GITHUB
|
|
211
212
|
When working with a GitHub repository's code (reading files, exploring structure, analysing a codebase), create or reuse a local checkout in the shared workspace and then use read_files, read_file, list_directory, search_files, edit_file, and replace_file_range on that checkout. Keep source files in locations that both shell commands and workspace file tools can access. File-by-file GitHub API calls are slow and hit rate limits fast.
|
|
212
213
|
Use github_api_request for metadata and structured GitHub data (issues, PRs, commits, releases, CI runs, repo stats). When calling github_api_request, the path must be the FULL API path starting from the root, e.g. /repos/NeoLabs-Systems/NeoAgent/git/trees/main?recursive=1. You can also pass owner_repo="owner/repo" together with a relative path like /git/trees/main and the prefix is prepended automatically.
|
|
213
|
-
Never fetch a repo's full file tree through the GitHub API when you actually need to read the code
|
|
214
|
+
Never fetch a repo's full file tree through the GitHub API when you actually need to read the code, clone it instead.
|
|
214
215
|
Prefer high-level tools over manual transport work. When a tool accepts normal text or structured JSON, pass that directly instead of transforming it through shell commands first.
|
|
215
216
|
|
|
216
217
|
SECURITY AND TRUST
|
|
@@ -74,6 +74,8 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
|
|
|
74
74
|
'Verify whether the draft final reply is adequately supported by the gathered evidence.',
|
|
75
75
|
'If the evidence is insufficient, revise the reply so it states the uncertainty clearly instead of guessing.',
|
|
76
76
|
'Cross-check every concrete claim against tool status and output. Remove or rewrite claims that are contradicted by the evidence.',
|
|
77
|
+
'When the task or prompt names a specific entity to check (for example a city, account, person, date, source, or destination), preserve that target exactly. If the gathered evidence is for a different entity, treat the draft as needing revision or insufficient evidence.',
|
|
78
|
+
'When the task says to include something only if it is available or only if a condition is met, omit that section when the evidence does not show the condition was satisfied.',
|
|
77
79
|
'A non-zero execute_command exit code means partial or failed shell evidence. Do not treat later sections of a chained shell command as observed unless they were verified separately.',
|
|
78
80
|
'A successful send_message or make_call means outbound delivery succeeded in this run unless a later messaging tool failed.',
|
|
79
81
|
'Any claim that an outbound action already happened (sent/submitted/called/"already done") must be backed by a successful outbound tool execution in this run. If not backed, rewrite the reply to "not sent yet" and provide a draft or next concrete step.',
|
|
@@ -153,6 +153,20 @@ function summarizeToolExecutions(toolExecutions = [], maxItems = 10) {
|
|
|
153
153
|
}).join('\n');
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// A read-only turn that pulls in NEW information is real progress, even though
|
|
157
|
+
// it changes no state — research, browsing, reading, and searching are how an
|
|
158
|
+
// agent makes headway on "find out X" tasks. Treating those as "no progress"
|
|
159
|
+
// (and force-wrapping the run) is what guillotines legitimate research. Genuine
|
|
160
|
+
// churn — failed calls, pure `think`, or re-running an identical call that
|
|
161
|
+
// returns an unchanged result — is excluded, so the read-only guard still fires
|
|
162
|
+
// on real spinning.
|
|
163
|
+
function gatheredNewEvidence(execution, repetitionObservation = null) {
|
|
164
|
+
if (!execution || execution.ok !== true) return false;
|
|
165
|
+
if (!execution.evidenceRelevant) return false;
|
|
166
|
+
if (repetitionObservation && repetitionObservation.unchangedCount >= 2) return false;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
function isSubstantiveProgressToolName(toolName = '') {
|
|
157
171
|
const name = String(toolName || '').trim();
|
|
158
172
|
if (!name) return false;
|
|
@@ -235,6 +249,7 @@ function buildAutonomousRecoveryContext({ err, toolExecutions = [], tools = [],
|
|
|
235
249
|
module.exports = {
|
|
236
250
|
classifyToolExecution,
|
|
237
251
|
deriveEvidenceSource,
|
|
252
|
+
gatheredNewEvidence,
|
|
238
253
|
isSubstantiveProgressEvidence,
|
|
239
254
|
isSubstantiveProgressToolName,
|
|
240
255
|
summarizeProgressToolExecutions,
|
|
@@ -323,6 +323,46 @@ function compactToolResult(toolName, toolArgs = {}, toolResult, options = {}) {
|
|
|
323
323
|
});
|
|
324
324
|
break;
|
|
325
325
|
|
|
326
|
+
case 'google_workspace_calendar_list_events': {
|
|
327
|
+
// The raw result leads with a verbose `nextTimedEvent` plus full event
|
|
328
|
+
// objects (description/htmlLink/attendees), so generic JSON truncation
|
|
329
|
+
// cuts the actual `events` array off mid-object — the model never sees
|
|
330
|
+
// what is scheduled and re-queries adjacent windows forever. Emit a
|
|
331
|
+
// compact, COMPLETE digest instead: every event the model needs to reason
|
|
332
|
+
// about "what's coming up", none of the noise it doesn't.
|
|
333
|
+
const events = Array.isArray(toolResult?.events)
|
|
334
|
+
? toolResult.events
|
|
335
|
+
: [
|
|
336
|
+
...(Array.isArray(toolResult?.timedEvents) ? toolResult.timedEvents : []),
|
|
337
|
+
...(Array.isArray(toolResult?.allDayEvents) ? toolResult.allDayEvents : []),
|
|
338
|
+
];
|
|
339
|
+
const timed = events.filter((event) => !event?.allDay);
|
|
340
|
+
const allDay = events.filter((event) => event?.allDay);
|
|
341
|
+
envelope = trimObject({
|
|
342
|
+
tool: toolName,
|
|
343
|
+
count: typeof toolResult?.count === 'number' ? toolResult.count : events.length,
|
|
344
|
+
timedCount: typeof toolResult?.timedCount === 'number' ? toolResult.timedCount : timed.length,
|
|
345
|
+
allDayCount: typeof toolResult?.allDayCount === 'number' ? toolResult.allDayCount : allDay.length,
|
|
346
|
+
hasOnlyAllDayEvents: toolResult?.hasOnlyAllDayEvents === true ? true : undefined,
|
|
347
|
+
// Timed appointments are what "starting soon" cares about — keep them all,
|
|
348
|
+
// with the fields a reminder actually uses.
|
|
349
|
+
timed: timed.map((event) => trimObject({
|
|
350
|
+
summary: clampText(event?.summary || '(no title)', 140),
|
|
351
|
+
start: event?.start || null,
|
|
352
|
+
end: event?.end || null,
|
|
353
|
+
location: event?.location ? clampText(event.location, 80) : undefined,
|
|
354
|
+
status: event?.status && event.status !== 'confirmed' ? event.status : undefined,
|
|
355
|
+
})),
|
|
356
|
+
// All-day entries are usually birthdays / markers — names + dates suffice.
|
|
357
|
+
allDay: allDay.map((event) => trimObject({
|
|
358
|
+
summary: clampText(event?.summary || '(no title)', 140),
|
|
359
|
+
start: event?.start || null,
|
|
360
|
+
end: event?.end || null,
|
|
361
|
+
})),
|
|
362
|
+
});
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
326
366
|
default:
|
|
327
367
|
envelope = trimObject({
|
|
328
368
|
tool: toolName,
|