neoagent 2.5.2-beta.9 → 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 +92 -9
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/conversation_loop.js +345 -242
- package/server/services/ai/loop/error_recovery.js +38 -0
- package/server/services/ai/loop/messaging_delivery.js +52 -6
- package/server/services/ai/loop/progress_classification.js +178 -0
- package/server/services/ai/loop/progress_monitor.js +6 -5
- package/server/services/ai/loop/tool_dispatch.js +1 -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
|
@@ -56,6 +56,13 @@ const { enforceRateLimits } = require('../rate_limits');
|
|
|
56
56
|
const { ToolRepetitionGuard } = require('../repetitionGuard');
|
|
57
57
|
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
58
58
|
const { IterationBudget } = require('./iteration_budget');
|
|
59
|
+
const {
|
|
60
|
+
buildBlankAfterToolFailureGuidance,
|
|
61
|
+
shouldContinueAfterBlankToolFailure,
|
|
62
|
+
} = require('./blank_recovery');
|
|
63
|
+
const {
|
|
64
|
+
shouldSendMessagingErrorFallback,
|
|
65
|
+
} = require('./error_recovery');
|
|
59
66
|
const {
|
|
60
67
|
buildCompletionDecisionPrompt,
|
|
61
68
|
buildGoalContractPrompt,
|
|
@@ -117,11 +124,16 @@ const {
|
|
|
117
124
|
getAvailableTools: getAvailableToolsImpl,
|
|
118
125
|
isReadOnlyToolCall: isReadOnlyToolCallImpl,
|
|
119
126
|
} = require('./tool_dispatch');
|
|
127
|
+
const {
|
|
128
|
+
isProgressToolCall,
|
|
129
|
+
} = require('./progress_classification');
|
|
120
130
|
const {
|
|
121
131
|
normalizeOutgoingMessage,
|
|
122
132
|
clampRunContext,
|
|
123
133
|
joinSentMessages,
|
|
124
134
|
buildBlankMessagingReplyPrompt,
|
|
135
|
+
buildMaxIterationWrapupPrompt,
|
|
136
|
+
buildProgressUpdatePrompt,
|
|
125
137
|
buildDeterministicMessagingFallback,
|
|
126
138
|
buildMessagingFailureScenario,
|
|
127
139
|
buildDeterministicMessagingErrorReply,
|
|
@@ -132,7 +144,6 @@ const {
|
|
|
132
144
|
summarizeToolExecutions,
|
|
133
145
|
summarizeAvailableTools,
|
|
134
146
|
inferToolFailureMessage,
|
|
135
|
-
buildAutonomousRecoveryContext,
|
|
136
147
|
} = require('../toolEvidence');
|
|
137
148
|
const {
|
|
138
149
|
buildMemoryConsolidationInstructions,
|
|
@@ -195,7 +206,7 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
195
206
|
// Immediate guidance on first occurrence for high-signal patterns that waste
|
|
196
207
|
// multiple iterations before self-correcting.
|
|
197
208
|
const immediateGuides = {
|
|
198
|
-
eisdir: 'That path is a directory
|
|
209
|
+
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.',
|
|
199
210
|
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.',
|
|
200
211
|
};
|
|
201
212
|
if (immediateGuides[key]) {
|
|
@@ -205,9 +216,9 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
205
216
|
|
|
206
217
|
if (count < 3) return null;
|
|
207
218
|
const guides = {
|
|
208
|
-
outside_workspace: 'read_file
|
|
219
|
+
outside_workspace: 'read_file/read_files only access the shared workspace. Put the relevant files there, then use read_files/search_files/edit_file instead of repeatedly extracting snippets through shell commands.',
|
|
209
220
|
enoent: 'That path does not exist. Use execute_command with `find . -name "..."` to locate the correct path first.',
|
|
210
|
-
bad_cwd: 'The VM home directory is not ~/.
|
|
221
|
+
bad_cwd: 'The VM home directory is not ~/. Discover the workspace root with pwd/list_directory and keep working files there so file tools can inspect them.',
|
|
211
222
|
not_found: 'This path or resource was not found. Try listing the parent directory or checking with a broader search first.',
|
|
212
223
|
};
|
|
213
224
|
const guide = guides[key];
|
|
@@ -217,8 +228,17 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
217
228
|
|
|
218
229
|
const OUTPUT_FINGERPRINT_TOOLS = /^(list_|search_|read_|get_|find_|github_list|github_get|github_search)/;
|
|
219
230
|
|
|
220
|
-
function fingerprintOutput(toolName, result) {
|
|
221
|
-
|
|
231
|
+
function fingerprintOutput(toolName, result, toolArgs = {}) {
|
|
232
|
+
const name = String(toolName || '');
|
|
233
|
+
if (
|
|
234
|
+
!name
|
|
235
|
+
|| (
|
|
236
|
+
!OUTPUT_FINGERPRINT_TOOLS.test(name)
|
|
237
|
+
&& !(name === 'execute_command' && !isProgressToolCall(name, toolArgs))
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
222
242
|
const raw = typeof result === 'string' ? result : JSON.stringify(result ?? '');
|
|
223
243
|
if (raw.length < 200) return null;
|
|
224
244
|
// djb2 hash over first 3000 chars — fast, collision-unlikely for our sizes
|
|
@@ -228,16 +248,37 @@ function fingerprintOutput(toolName, result) {
|
|
|
228
248
|
return h >>> 0;
|
|
229
249
|
}
|
|
230
250
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
251
|
+
// Concise list of files/targets the run has already read or searched, so the
|
|
252
|
+
// analysis-paralysis nudge can name them and tell the model not to re-read them.
|
|
253
|
+
function summarizeReadTargets(toolExecutions = []) {
|
|
254
|
+
const targets = [];
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
for (const item of toolExecutions) {
|
|
257
|
+
if (!item || item.stateChanged) continue; // only read-only steps
|
|
258
|
+
const input = item.input || {};
|
|
259
|
+
let target = '';
|
|
260
|
+
if (Array.isArray(input.files) && input.files.length) {
|
|
261
|
+
target = input.files
|
|
262
|
+
.map((file) => (typeof file === 'string' ? file : file?.path || file?.file_path || ''))
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.slice(0, 3)
|
|
265
|
+
.join(', ');
|
|
266
|
+
} else if (typeof input.path === 'string' && input.path.trim()) {
|
|
267
|
+
target = input.path.trim();
|
|
268
|
+
} else if (typeof input.command === 'string') {
|
|
269
|
+
const files = input.command.match(/[\w./-]+\.(?:js|ts|tsx|jsx|py|dart|json|md|kt|c|h|ya?ml|sql|txt|sh)\b/g);
|
|
270
|
+
if (files && files.length) target = [...new Set(files)].slice(0, 2).join(', ');
|
|
271
|
+
} else if (typeof input.query === 'string' && input.query.trim()) {
|
|
272
|
+
target = `search:${input.query.trim().slice(0, 30)}`;
|
|
273
|
+
}
|
|
274
|
+
if (!target) continue;
|
|
275
|
+
const key = target.toLowerCase();
|
|
276
|
+
if (seen.has(key)) continue;
|
|
277
|
+
seen.add(key);
|
|
278
|
+
targets.push(target);
|
|
279
|
+
if (targets.length >= 8) break;
|
|
280
|
+
}
|
|
281
|
+
return targets.join('; ');
|
|
241
282
|
}
|
|
242
283
|
|
|
243
284
|
function cloneInterimHistory(history = []) {
|
|
@@ -556,9 +597,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
556
597
|
|
|
557
598
|
const runTitle = generateTitle(userMessage);
|
|
558
599
|
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
559
|
-
db.prepare(`INSERT
|
|
600
|
+
db.prepare(`INSERT INTO agent_runs(
|
|
560
601
|
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
561
|
-
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
602
|
+
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
603
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
604
|
+
status = 'running',
|
|
605
|
+
model = excluded.model,
|
|
606
|
+
updated_at = datetime('now'),
|
|
607
|
+
completed_at = NULL,
|
|
608
|
+
error = NULL,
|
|
609
|
+
metadata_json = COALESCE(agent_runs.metadata_json, excluded.metadata_json)`).run(
|
|
562
610
|
runId,
|
|
563
611
|
userId,
|
|
564
612
|
agentId,
|
|
@@ -686,7 +734,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
686
734
|
console.info(
|
|
687
735
|
`[Run ${shortenRunId(runId)}] tools total=${toolNames.length} builtIns=${builtInTools.length} mcp=${mcpTools.length} core=${JSON.stringify(coreToolStatus)}`
|
|
688
736
|
);
|
|
689
|
-
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine
|
|
737
|
+
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine });
|
|
690
738
|
const capabilitySummary = summarizeCapabilityHealth(capabilityHealth);
|
|
691
739
|
const integrationSummary = integrationManager?.summarizeConnectedProviders?.(userId, agentId) || '';
|
|
692
740
|
|
|
@@ -762,6 +810,56 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
762
810
|
let compactionMetrics = [];
|
|
763
811
|
let analysis = null;
|
|
764
812
|
let plan = null;
|
|
813
|
+
|
|
814
|
+
// Model-authored progress updates: the messaging supervisor calls this to get a
|
|
815
|
+
// dynamic "what I'm doing right now" line instead of a hard-coded string. It runs
|
|
816
|
+
// a tiny tool-less model call over the current run activity. `provider`/`model` are
|
|
817
|
+
// `let` (closure tracks fallbacks) and `toolExecutions` is push-only.
|
|
818
|
+
if (triggerSource === 'messaging') {
|
|
819
|
+
const runMetaForCompose = engine.getRunMeta(runId);
|
|
820
|
+
if (runMetaForCompose) {
|
|
821
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
822
|
+
try {
|
|
823
|
+
const rm = engine.getRunMeta(runId);
|
|
824
|
+
const ledger = rm?.progressLedger || {};
|
|
825
|
+
// Real, specific evidence (actual commands + their output) so the update is
|
|
826
|
+
// grounded in what happened, not invented. Bare tool names make a weak model
|
|
827
|
+
// confabulate generic activity ("running the build", "training").
|
|
828
|
+
const recent = summarizeToolExecutions(toolExecutions, 5) || '(no tool activity yet)';
|
|
829
|
+
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
830
|
+
const contextBlock = [
|
|
831
|
+
buildProgressUpdatePrompt(),
|
|
832
|
+
stalled ? 'This step has been running a while with no new progress; reassure the user you are still on it.' : '',
|
|
833
|
+
'',
|
|
834
|
+
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
835
|
+
`Doing now: ${ledger.currentTool ? `using ${ledger.currentTool}` : (ledger.currentPhase || 'thinking')}`,
|
|
836
|
+
`Actual recent tool activity (newest last) — describe ONLY this, do not extrapolate:\n${recent}`,
|
|
837
|
+
priorUpdate ? `Your previous update (say something different): ${summarizeForLog(priorUpdate, 160)}` : '',
|
|
838
|
+
].filter(Boolean).join('\n');
|
|
839
|
+
// Reuse the run's real system prompt so the update follows the same voice and
|
|
840
|
+
// formatting guidelines as every other message (single source of truth).
|
|
841
|
+
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
842
|
+
|| 'You are a helpful assistant.';
|
|
843
|
+
const resp = await withModelCallTimeout(
|
|
844
|
+
provider.chat(
|
|
845
|
+
[
|
|
846
|
+
{ role: 'system', content: sysContent },
|
|
847
|
+
{ role: 'user', content: contextBlock },
|
|
848
|
+
],
|
|
849
|
+
[],
|
|
850
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
851
|
+
),
|
|
852
|
+
options,
|
|
853
|
+
'Progress update compose',
|
|
854
|
+
);
|
|
855
|
+
return sanitizeModelOutput(resp.content || '', { model });
|
|
856
|
+
} catch (composeErr) {
|
|
857
|
+
console.warn(`[Run ${shortenRunId(runId)}] progress_update_compose failed: ${summarizeForLog(composeErr?.message || composeErr, 140)}`);
|
|
858
|
+
return '';
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
}
|
|
765
863
|
let verification = null;
|
|
766
864
|
let deliverableWorkflow = null;
|
|
767
865
|
let deliverablePlan = null;
|
|
@@ -827,6 +925,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
827
925
|
|
|
828
926
|
tools = selectInitialTools(allTools, analysis.suggested_tools, {
|
|
829
927
|
widgetId: options.widgetId || null,
|
|
928
|
+
includeCoreFileTools: analysis.mode === 'execute' || analysis.mode === 'plan_execute',
|
|
830
929
|
});
|
|
831
930
|
engine.initializeToolRuntime(runId, allTools, tools, options);
|
|
832
931
|
messages.push({
|
|
@@ -836,6 +935,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
836
935
|
buildToolCatalog(allTools),
|
|
837
936
|
'',
|
|
838
937
|
`Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
|
|
938
|
+
'For workspace file inspection/editing, prefer read_files, read_file, search_files, list_directory, edit_file, replace_file_range, and write_file over shell cat/sed/python snippets. Use execute_command for git, tests, package managers, builds, and other shell-native actions.',
|
|
839
939
|
'Use activate_tools with exact catalog names when another schema is required.',
|
|
840
940
|
].join('\n'),
|
|
841
941
|
});
|
|
@@ -896,54 +996,63 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
896
996
|
maxIterations = loopPolicy.maxIterations;
|
|
897
997
|
|
|
898
998
|
if (options.skipDeliverableWorkflow !== true) {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
options: { ...options, runId, userId, agentId },
|
|
907
|
-
});
|
|
908
|
-
totalTokens += deliverableSelectionResult.usage || 0;
|
|
909
|
-
const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
|
|
910
|
-
if (selectedWorkflow?.canHandle(deliverableSelectionResult.selection)) {
|
|
911
|
-
deliverableWorkflow = {
|
|
912
|
-
workflow: selectedWorkflow,
|
|
913
|
-
selection: deliverableSelectionResult.selection,
|
|
914
|
-
request: selectedWorkflow.normalizeRequest({
|
|
915
|
-
...deliverableSelectionResult.selection,
|
|
916
|
-
userMessage,
|
|
917
|
-
}),
|
|
918
|
-
};
|
|
919
|
-
deliverablePlan = selectedWorkflow.buildExecutionPlan(deliverableWorkflow.request, {
|
|
920
|
-
analysis,
|
|
999
|
+
try {
|
|
1000
|
+
const deliverableSelectionResult = await selectDeliverableWorkflow({
|
|
1001
|
+
engine,
|
|
1002
|
+
provider,
|
|
1003
|
+
providerName,
|
|
1004
|
+
model,
|
|
1005
|
+
messages,
|
|
921
1006
|
tools,
|
|
922
|
-
options,
|
|
1007
|
+
options: { ...options, runId, userId, agentId },
|
|
923
1008
|
});
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1009
|
+
totalTokens += deliverableSelectionResult.usage || 0;
|
|
1010
|
+
const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
|
|
1011
|
+
if (selectedWorkflow?.canHandle(deliverableSelectionResult.selection)) {
|
|
1012
|
+
deliverableWorkflow = {
|
|
1013
|
+
workflow: selectedWorkflow,
|
|
1014
|
+
selection: deliverableSelectionResult.selection,
|
|
1015
|
+
request: selectedWorkflow.normalizeRequest({
|
|
1016
|
+
...deliverableSelectionResult.selection,
|
|
1017
|
+
userMessage,
|
|
1018
|
+
}),
|
|
1019
|
+
};
|
|
1020
|
+
deliverablePlan = selectedWorkflow.buildExecutionPlan(deliverableWorkflow.request, {
|
|
1021
|
+
analysis,
|
|
1022
|
+
tools,
|
|
1023
|
+
options,
|
|
1024
|
+
});
|
|
1025
|
+
await selectedWorkflow.run(deliverablePlan, {
|
|
1026
|
+
engine,
|
|
1027
|
+
userId,
|
|
1028
|
+
agentId,
|
|
1029
|
+
runId,
|
|
1030
|
+
app,
|
|
1031
|
+
});
|
|
1032
|
+
engine.persistRunMetadata(runId, {
|
|
1033
|
+
deliverableWorkflow: {
|
|
1034
|
+
...deliverableWorkflow.selection,
|
|
1035
|
+
plan: deliverablePlan,
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
engine.updateRunGoalContract(runId, {
|
|
1039
|
+
goal: deliverableWorkflow.selection.goal,
|
|
1040
|
+
});
|
|
1041
|
+
engine.recordRunEvent(userId, runId, 'deliverable_workflow_selected', {
|
|
1042
|
+
type: deliverableWorkflow.selection.type,
|
|
1043
|
+
confidence: deliverableWorkflow.selection.confidence,
|
|
1044
|
+
goal: deliverableWorkflow.selection.goal,
|
|
1045
|
+
requestedOutputs: deliverableWorkflow.selection.requestedOutputs,
|
|
1046
|
+
}, { agentId });
|
|
1047
|
+
}
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
engine.recordRunEvent(userId, runId, 'deliverable_workflow_skipped', {
|
|
1050
|
+
reason: summarizeForLog(error?.message || error, 240),
|
|
946
1051
|
}, { agentId });
|
|
1052
|
+
messages.push({
|
|
1053
|
+
role: 'system',
|
|
1054
|
+
content: 'The optional deliverable workflow classifier failed. Continue with the normal agent loop; do not stop or retry the whole run just because this classifier failed.',
|
|
1055
|
+
});
|
|
947
1056
|
}
|
|
948
1057
|
}
|
|
949
1058
|
|
|
@@ -1051,9 +1160,18 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1051
1160
|
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1052
1161
|
if (readOnlyCount >= 3) {
|
|
1053
1162
|
const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
|
|
1163
|
+
const alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1054
1164
|
messages.push({
|
|
1055
1165
|
role: 'system',
|
|
1056
|
-
content:
|
|
1166
|
+
content: [
|
|
1167
|
+
`${urgency} — ${readOnlyCount} consecutive read-only turns with no concrete action.`,
|
|
1168
|
+
alreadyRead
|
|
1169
|
+
? `You have ALREADY read/searched: ${alreadyRead}. Their output is in this conversation above — do not read or search them again; re-reading what you already have is the main way runs stall.`
|
|
1170
|
+
: 'Do not re-read or re-search anything already in this conversation.',
|
|
1171
|
+
'If you are repeatedly extracting evidence through low-level commands, switch to the available file/search/edit tools over the shared workspace.',
|
|
1172
|
+
'Act on what you already know now: edit files, run a state-changing command, create the branch/PR, or send the result.',
|
|
1173
|
+
'If you genuinely cannot proceed, call task_complete with your best answer or a concrete blocker. Deciding to finish is a valid action; continuing to gather is not.',
|
|
1174
|
+
].join(' '),
|
|
1057
1175
|
});
|
|
1058
1176
|
}
|
|
1059
1177
|
}
|
|
@@ -1268,6 +1386,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1268
1386
|
})) {
|
|
1269
1387
|
break;
|
|
1270
1388
|
}
|
|
1389
|
+
if (shouldContinueAfterBlankToolFailure({
|
|
1390
|
+
lastContent,
|
|
1391
|
+
failedStepCount,
|
|
1392
|
+
remainingIterations: iterationBudget.remaining,
|
|
1393
|
+
toolExecutions,
|
|
1394
|
+
})) {
|
|
1395
|
+
engine.recordRunEvent(userId, runId, 'blank_after_tool_failure_continued', {
|
|
1396
|
+
iteration,
|
|
1397
|
+
remainingIterations: iterationBudget.remaining,
|
|
1398
|
+
}, { agentId });
|
|
1399
|
+
messages.push({
|
|
1400
|
+
role: 'system',
|
|
1401
|
+
content: buildBlankAfterToolFailureGuidance(toolExecutions),
|
|
1402
|
+
});
|
|
1403
|
+
lastContent = '';
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1271
1406
|
const loopDecision = await engine.decideLoopState({
|
|
1272
1407
|
provider,
|
|
1273
1408
|
providerName,
|
|
@@ -1378,9 +1513,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1378
1513
|
}, {
|
|
1379
1514
|
verified: true,
|
|
1380
1515
|
});
|
|
1516
|
+
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1517
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1518
|
+
if (iterMeta) {
|
|
1519
|
+
iterMeta.consecutiveReadOnlyIterations = (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1381
1522
|
continue;
|
|
1382
1523
|
}
|
|
1383
1524
|
|
|
1525
|
+
let iterationConcreteProgress = false;
|
|
1384
1526
|
for (const toolCall of response.toolCalls) {
|
|
1385
1527
|
if (engine.isRunStopped(runId)) break;
|
|
1386
1528
|
stepIndex++;
|
|
@@ -1595,6 +1737,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1595
1737
|
|
|
1596
1738
|
const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
|
|
1597
1739
|
execution.input = toolArgs;
|
|
1740
|
+
if (execution.stateChanged && isProgressToolCall(toolName, toolArgs)) {
|
|
1741
|
+
iterationConcreteProgress = true;
|
|
1742
|
+
}
|
|
1598
1743
|
repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1599
1744
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
1600
1745
|
toolExecutions.push(execution);
|
|
@@ -1684,7 +1829,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1684
1829
|
// Output fingerprint guard: steer away from re-fetching data already seen.
|
|
1685
1830
|
if (!toolErrorMessage) {
|
|
1686
1831
|
const currentRunMeta = engine.getRunMeta(runId);
|
|
1687
|
-
const fp = fingerprintOutput(toolName, toolResult);
|
|
1832
|
+
const fp = fingerprintOutput(toolName, toolResult, toolArgs);
|
|
1688
1833
|
if (fp !== null && currentRunMeta?.seenOutputHashes) {
|
|
1689
1834
|
const prior = currentRunMeta.seenOutputHashes.get(fp);
|
|
1690
1835
|
if (prior) {
|
|
@@ -1694,24 +1839,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1694
1839
|
});
|
|
1695
1840
|
} else {
|
|
1696
1841
|
currentRunMeta.seenOutputHashes.set(fp, { toolName, iteration });
|
|
1697
|
-
// External state: persist large read results to disk so the
|
|
1698
|
-
// model can reference them after context compaction without
|
|
1699
|
-
// re-fetching. Only for significant payloads.
|
|
1700
|
-
const persistRaw = typeof toolResult === 'string' ? toolResult : JSON.stringify(toolResult ?? '');
|
|
1701
|
-
if (persistRaw.length >= 1000 && runId) {
|
|
1702
|
-
const persistPath = `/tmp/run-${runId.slice(0, 8)}-${toolName}.json`;
|
|
1703
|
-
try {
|
|
1704
|
-
require('fs').writeFileSync(persistPath, persistRaw.slice(0, 40000));
|
|
1705
|
-
if (!currentRunMeta.persistedDataPaths) currentRunMeta.persistedDataPaths = [];
|
|
1706
|
-
if (!currentRunMeta.persistedDataPaths.includes(persistPath)) {
|
|
1707
|
-
currentRunMeta.persistedDataPaths.push(persistPath);
|
|
1708
|
-
messages.push({
|
|
1709
|
-
role: 'system',
|
|
1710
|
-
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.`,
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
} catch { /* non-fatal — disk full or permissions */ }
|
|
1714
|
-
}
|
|
1715
1842
|
}
|
|
1716
1843
|
}
|
|
1717
1844
|
}
|
|
@@ -1782,8 +1909,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1782
1909
|
&& (analysis.mode === 'execute' || analysis.mode === 'plan_execute')) {
|
|
1783
1910
|
const iterMeta = engine.getRunMeta(runId);
|
|
1784
1911
|
if (iterMeta) {
|
|
1785
|
-
|
|
1786
|
-
iterMeta.consecutiveReadOnlyIterations = calledProgress
|
|
1912
|
+
iterMeta.consecutiveReadOnlyIterations = iterationConcreteProgress
|
|
1787
1913
|
? 0
|
|
1788
1914
|
: (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1789
1915
|
}
|
|
@@ -1822,6 +1948,54 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1822
1948
|
const messagingSent = runMeta?.messagingSent || false;
|
|
1823
1949
|
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
1824
1950
|
|
|
1951
|
+
// Hermes _handle_max_iterations: if the run exhausted its step budget without a
|
|
1952
|
+
// judged completion, the model's last text is usually a mid-thought fragment
|
|
1953
|
+
// ("let me inline everything:"). Do one tool-less wrap-up call so the user gets a
|
|
1954
|
+
// real final answer instead of that fragment.
|
|
1955
|
+
const budgetExhaustedWithoutCompletion = triggerSource === 'messaging'
|
|
1956
|
+
&& !directAnswerEligible
|
|
1957
|
+
&& !messagingSent
|
|
1958
|
+
&& !runMeta?.terminalInterim
|
|
1959
|
+
&& iteration >= maxIterations;
|
|
1960
|
+
if (budgetExhaustedWithoutCompletion) {
|
|
1961
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
1962
|
+
try {
|
|
1963
|
+
const wrapResponse = await withModelCallTimeout(
|
|
1964
|
+
provider.chat(
|
|
1965
|
+
sanitizeConversationMessages([
|
|
1966
|
+
...messages,
|
|
1967
|
+
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
1968
|
+
]),
|
|
1969
|
+
[],
|
|
1970
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
1971
|
+
),
|
|
1972
|
+
options,
|
|
1973
|
+
'Max-iteration wrap-up',
|
|
1974
|
+
);
|
|
1975
|
+
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
1976
|
+
const wrapText = sanitizeModelOutput(wrapResponse.content || '', { model });
|
|
1977
|
+
// On budget exhaustion the model's last text is an untrustworthy mid-thought
|
|
1978
|
+
// fragment. Replace it with the wrap-up answer, or a clean deterministic
|
|
1979
|
+
// fallback if the wrap-up came back empty — never deliver the fragment.
|
|
1980
|
+
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null);
|
|
1981
|
+
lastContent = usableWrap
|
|
1982
|
+
? wrapText
|
|
1983
|
+
: buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1984
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1985
|
+
if (conversationId) {
|
|
1986
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1987
|
+
.run(conversationId, 'assistant', lastContent, usableWrap ? (wrapResponse.usage?.totalTokens || 0) : 0);
|
|
1988
|
+
}
|
|
1989
|
+
engine.recordRunEvent(userId, runId, 'max_iteration_wrapup_delivered', {
|
|
1990
|
+
iteration, maxIterations, stepIndex, source: usableWrap ? 'model' : 'deterministic',
|
|
1991
|
+
}, { agentId });
|
|
1992
|
+
} catch (wrapErr) {
|
|
1993
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1994
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1995
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1825
1999
|
if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
|
|
1826
2000
|
// Simplified blank reply recovery: one model call with direct instruction,
|
|
1827
2001
|
// then fall back to a deterministic message. No multi-attempt LLM loop.
|
|
@@ -1852,37 +2026,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1852
2026
|
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`);
|
|
1853
2027
|
}
|
|
1854
2028
|
totalTokens += recoveredTokens;
|
|
1855
|
-
|
|
2029
|
+
// The loop has already exited, so we cannot keep working: deliver the model's
|
|
2030
|
+
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2031
|
+
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2032
|
+
// a deterministic message when the model returned nothing usable.
|
|
2033
|
+
const recoveredVisible = Boolean(normalizeOutgoingMessage(lastContent, options?.source || null));
|
|
2034
|
+
if (!recoveredVisible) {
|
|
1856
2035
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1857
2036
|
}
|
|
1858
2037
|
if (normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
messages,
|
|
1864
|
-
analysis,
|
|
1865
|
-
plan,
|
|
1866
|
-
tools,
|
|
1867
|
-
toolExecutions,
|
|
1868
|
-
lastReply: lastContent,
|
|
1869
|
-
iteration,
|
|
1870
|
-
maxIterations,
|
|
1871
|
-
options: { ...options, triggerSource, runId, userId, agentId },
|
|
1872
|
-
messagingSent: false,
|
|
1873
|
-
});
|
|
1874
|
-
totalTokens += recoveryDecision.usage || 0;
|
|
1875
|
-
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_checked', {
|
|
1876
|
-
status: recoveryDecision.decision.status,
|
|
1877
|
-
reason: recoveryDecision.decision.reason,
|
|
2038
|
+
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_delivered', {
|
|
2039
|
+
source: recoveredVisible ? 'model' : 'deterministic',
|
|
2040
|
+
stepIndex,
|
|
2041
|
+
failedStepCount,
|
|
1878
2042
|
}, { agentId });
|
|
1879
|
-
if (recoveryDecision.decision.status === 'continue') {
|
|
1880
|
-
throw new Error('Messaging run ended without a judged terminal reply.');
|
|
1881
|
-
}
|
|
1882
2043
|
messages.push({ role: 'assistant', content: lastContent });
|
|
1883
2044
|
if (conversationId) {
|
|
1884
2045
|
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1885
|
-
.run(conversationId, 'assistant', lastContent, recoveredTokens);
|
|
2046
|
+
.run(conversationId, 'assistant', lastContent, recoveredVisible ? recoveredTokens : 0);
|
|
1886
2047
|
}
|
|
1887
2048
|
}
|
|
1888
2049
|
}
|
|
@@ -1910,10 +2071,28 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1910
2071
|
stepIndex,
|
|
1911
2072
|
failedStepCount,
|
|
1912
2073
|
}, { agentId });
|
|
1913
|
-
|
|
2074
|
+
if (triggerSource === 'messaging') {
|
|
2075
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2076
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2077
|
+
if (conversationId) {
|
|
2078
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2079
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
2080
|
+
}
|
|
2081
|
+
} else {
|
|
2082
|
+
throw new Error(`Iteration budget exhausted before judged completion after ${maxIterations} iterations.`);
|
|
2083
|
+
}
|
|
1914
2084
|
}
|
|
1915
2085
|
if (stepIndex > 0 && !lastToolWasMessaging && iteration < maxIterations) {
|
|
1916
|
-
|
|
2086
|
+
if (triggerSource === 'messaging') {
|
|
2087
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2088
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2089
|
+
if (conversationId) {
|
|
2090
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2091
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
2092
|
+
}
|
|
2093
|
+
} else {
|
|
2094
|
+
throw new Error('Run ended without an explicit completion or blocker reply.');
|
|
2095
|
+
}
|
|
1917
2096
|
}
|
|
1918
2097
|
}
|
|
1919
2098
|
|
|
@@ -2156,149 +2335,73 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2156
2335
|
}
|
|
2157
2336
|
|
|
2158
2337
|
const runMeta = engine.activeRuns.get(runId);
|
|
2159
|
-
const retryCount = Number(options.messagingAutonomousRetryCount || 0);
|
|
2160
|
-
// Rate-limit errors (429) must not trigger messaging retries: the model
|
|
2161
|
-
// won't be available in the milliseconds between retries, so spawning new
|
|
2162
|
-
// runs just compounds the rate-limit pressure with no benefit.
|
|
2163
|
-
const isRateLimitError = /429|rate.?limit|free-models-per/i.test(String(err?.message || ''));
|
|
2164
|
-
const canRetryMessagingRun = (
|
|
2165
|
-
triggerSource === 'messaging'
|
|
2166
|
-
&& options.source
|
|
2167
|
-
&& options.chatId
|
|
2168
|
-
&& runMeta?.finalDeliverySent !== true
|
|
2169
|
-
&& runMeta?.messagingSent !== true
|
|
2170
|
-
&& err?.disableAutonomousRetry !== true
|
|
2171
|
-
&& !isRateLimitError
|
|
2172
|
-
&& retryCount < engine.getMessagingRetryLimit(maxIterations)
|
|
2173
|
-
);
|
|
2174
|
-
|
|
2175
|
-
if (canRetryMessagingRun) {
|
|
2176
|
-
const recoveryContext = buildAutonomousRecoveryContext({
|
|
2177
|
-
err,
|
|
2178
|
-
toolExecutions,
|
|
2179
|
-
tools,
|
|
2180
|
-
userMessage,
|
|
2181
|
-
visibleMessageSent: Boolean(
|
|
2182
|
-
runMeta?.lastSentMessage
|
|
2183
|
-
|| runMeta?.lastInterimMessage
|
|
2184
|
-
|| runMeta?.messagingSent === true
|
|
2185
|
-
),
|
|
2186
|
-
});
|
|
2187
|
-
db.prepare('UPDATE agent_runs SET status = ?, error = ?, updated_at = datetime(\'now\') WHERE id = ?')
|
|
2188
|
-
.run('retrying', err.message, runId);
|
|
2189
|
-
console.warn(
|
|
2190
|
-
`[Run ${shortenRunId(runId)}] retrying_messaging_attempt=${retryCount + 1} reason=${summarizeForLog(err.message, 140)}`
|
|
2191
|
-
);
|
|
2192
|
-
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
2193
|
-
engine.stopMessagingProgressSupervisor(runId);
|
|
2194
|
-
engine.activeRuns.delete(runId);
|
|
2195
|
-
engine.emit(userId, 'run:interim', {
|
|
2196
|
-
runId,
|
|
2197
|
-
message: 'Retrying internally after a transient failure.',
|
|
2198
|
-
phase: 'retrying'
|
|
2199
|
-
});
|
|
2200
|
-
|
|
2201
|
-
const retryOptions = {
|
|
2202
|
-
...options,
|
|
2203
|
-
messagingAutonomousRetryCount: retryCount + 1,
|
|
2204
|
-
messagingRetryState: {
|
|
2205
|
-
lastFinalMessage: String(runMeta?.lastSentMessage || options?.messagingRetryState?.lastFinalMessage || '').trim(),
|
|
2206
|
-
explicitMessageSent: runMeta?.explicitMessageSent === true || options?.messagingRetryState?.explicitMessageSent === true,
|
|
2207
|
-
interimHistory: cloneInterimHistory([
|
|
2208
|
-
...(Array.isArray(options?.messagingRetryState?.interimHistory) ? options.messagingRetryState.interimHistory : []),
|
|
2209
|
-
...(Array.isArray(runMeta?.interimMessages) ? runMeta.interimMessages : []),
|
|
2210
|
-
]),
|
|
2211
|
-
goalContract: mergeGoalContracts(
|
|
2212
|
-
options?.messagingRetryState?.goalContract || null,
|
|
2213
|
-
runMeta?.goalContract || null,
|
|
2214
|
-
),
|
|
2215
|
-
lastUserVisibleUpdateAt: runMeta?.progressLedger?.lastUserVisibleUpdateAt || options?.messagingRetryState?.lastUserVisibleUpdateAt || null,
|
|
2216
|
-
lastFinalDeliveryAt: runMeta?.progressLedger?.lastFinalDeliveryAt || options?.messagingRetryState?.lastFinalDeliveryAt || null,
|
|
2217
|
-
heartbeatCount: Number(runMeta?.progressLedger?.heartbeatCount || options?.messagingRetryState?.heartbeatCount || 0),
|
|
2218
|
-
progressState: runMeta?.progressLedger?.progressState || options?.messagingRetryState?.progressState || 'active',
|
|
2219
|
-
lastVerifiedProgressAt: runMeta?.progressLedger?.lastVerifiedProgressAt || options?.messagingRetryState?.lastVerifiedProgressAt || null,
|
|
2220
|
-
},
|
|
2221
|
-
context: {
|
|
2222
|
-
...(options.context || {}),
|
|
2223
|
-
additionalContext: [
|
|
2224
|
-
options.context?.additionalContext || '',
|
|
2225
|
-
recoveryContext,
|
|
2226
|
-
].filter(Boolean).join('\n\n')
|
|
2227
|
-
}
|
|
2228
|
-
};
|
|
2229
|
-
delete retryOptions.runId;
|
|
2230
|
-
|
|
2231
|
-
return engine.runWithModel(userId, userMessage, retryOptions, _modelOverride);
|
|
2232
|
-
}
|
|
2233
2338
|
|
|
2234
2339
|
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2235
2340
|
|| err?.deliverableValidation?.summary
|
|
2236
2341
|
|| '';
|
|
2237
2342
|
let messagingFailureContent = '';
|
|
2238
2343
|
let sendSucceeded = false;
|
|
2239
|
-
if (triggerSource
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2344
|
+
if (shouldSendMessagingErrorFallback({ triggerSource, options, runMeta })) {
|
|
2345
|
+
const manager = engine.messagingManager;
|
|
2346
|
+
if (manager) {
|
|
2347
|
+
const failureScenario = buildMessagingFailureScenario({
|
|
2348
|
+
err,
|
|
2349
|
+
failedStepCount,
|
|
2350
|
+
stepIndex,
|
|
2351
|
+
toolExecutions,
|
|
2352
|
+
});
|
|
2353
|
+
try {
|
|
2354
|
+
const failedMessage = sanitizeConversationMessages([
|
|
2355
|
+
...messages,
|
|
2356
|
+
{
|
|
2357
|
+
role: 'system',
|
|
2358
|
+
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)}`
|
|
2359
|
+
}
|
|
2360
|
+
]);
|
|
2361
|
+
const modelReply = await withModelCallTimeout(
|
|
2362
|
+
provider.chat(failedMessage, [], {
|
|
2363
|
+
model,
|
|
2364
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2365
|
+
}),
|
|
2366
|
+
options,
|
|
2367
|
+
'Messaging failure reply',
|
|
2368
|
+
);
|
|
2369
|
+
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2370
|
+
if (normalizeOutgoingMessage(drafted, options?.source || null)) {
|
|
2371
|
+
messagingFailureContent = drafted.trim();
|
|
2372
|
+
}
|
|
2373
|
+
} catch {
|
|
2374
|
+
// Fall back to deterministic text below.
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (!messagingFailureContent) {
|
|
2378
|
+
messagingFailureContent = buildDeterministicMessagingErrorReply({
|
|
2244
2379
|
err,
|
|
2245
2380
|
failedStepCount,
|
|
2246
2381
|
stepIndex,
|
|
2247
2382
|
toolExecutions,
|
|
2248
2383
|
});
|
|
2249
|
-
|
|
2250
|
-
const failedMessage = sanitizeConversationMessages([
|
|
2251
|
-
...messages,
|
|
2252
|
-
{
|
|
2253
|
-
role: 'system',
|
|
2254
|
-
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)}`
|
|
2255
|
-
}
|
|
2256
|
-
]);
|
|
2257
|
-
const modelReply = await withModelCallTimeout(
|
|
2258
|
-
provider.chat(failedMessage, [], {
|
|
2259
|
-
model,
|
|
2260
|
-
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2261
|
-
}),
|
|
2262
|
-
options,
|
|
2263
|
-
'Messaging failure reply',
|
|
2264
|
-
);
|
|
2265
|
-
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2266
|
-
if (normalizeOutgoingMessage(drafted, options?.source || null)) {
|
|
2267
|
-
messagingFailureContent = drafted.trim();
|
|
2268
|
-
}
|
|
2269
|
-
} catch {
|
|
2270
|
-
// Fall back to deterministic text below.
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
if (!messagingFailureContent) {
|
|
2274
|
-
messagingFailureContent = buildDeterministicMessagingErrorReply({
|
|
2275
|
-
err,
|
|
2276
|
-
failedStepCount,
|
|
2277
|
-
stepIndex,
|
|
2278
|
-
toolExecutions,
|
|
2279
|
-
});
|
|
2280
|
-
}
|
|
2384
|
+
}
|
|
2281
2385
|
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
}
|
|
2297
|
-
engine.markRunFinalDelivery(runId, messagingFailureContent);
|
|
2298
|
-
} catch (sendErr) {
|
|
2299
|
-
console.error('[Engine] Messaging error fallback failed:', sendErr.message);
|
|
2300
|
-
messagingFailureContent = '';
|
|
2386
|
+
try {
|
|
2387
|
+
const deliveryResult = await manager.sendMessage(
|
|
2388
|
+
userId,
|
|
2389
|
+
options.source,
|
|
2390
|
+
options.chatId,
|
|
2391
|
+
messagingFailureContent,
|
|
2392
|
+
{ runId, agentId },
|
|
2393
|
+
);
|
|
2394
|
+
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
2395
|
+
sendSucceeded = true;
|
|
2396
|
+
if (runMeta) {
|
|
2397
|
+
runMeta.lastSentMessage = messagingFailureContent;
|
|
2398
|
+
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
2399
|
+
runMeta.sentMessages.push(messagingFailureContent);
|
|
2301
2400
|
}
|
|
2401
|
+
engine.markRunFinalDelivery(runId, messagingFailureContent);
|
|
2402
|
+
} catch (sendErr) {
|
|
2403
|
+
console.error('[Engine] Messaging error fallback failed:', sendErr.message);
|
|
2404
|
+
messagingFailureContent = '';
|
|
2302
2405
|
}
|
|
2303
2406
|
}
|
|
2304
2407
|
}
|