neoagent 2.5.2-beta.9 → 3.0.1-beta.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/flutter_app/lib/main_controller.dart +34 -0
- package/flutter_app/lib/main_security.dart +19 -0
- package/lib/schema_migrations.js +224 -0
- package/package.json +2 -2
- package/server/admin/admin.css +29 -0
- package/server/admin/admin.js +81 -1
- package/server/admin/index.html +97 -9
- package/server/db/database.js +57 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +32788 -32752
- package/server/routes/security.js +19 -0
- package/server/routes/settings.js +1 -0
- package/server/routes/skills.js +9 -5
- package/server/services/ai/deliverables/artifact_helpers.js +92 -9
- package/server/services/ai/deliverables/selector.js +17 -0
- package/server/services/ai/hooks.js +3 -2
- package/server/services/ai/learning.js +8 -3
- package/server/services/ai/loop/agent_engine_core.js +67 -0
- package/server/services/ai/loop/blank_recovery.js +37 -0
- package/server/services/ai/loop/conversation_loop.js +391 -252
- 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/settings.js +8 -0
- package/server/services/ai/systemPrompt.js +24 -0
- package/server/services/ai/taskAnalysis.js +10 -0
- package/server/services/ai/toolEvidence.js +39 -3
- package/server/services/ai/toolResult.js +29 -0
- package/server/services/ai/toolRunner.js +262 -55
- package/server/services/ai/toolSelector.js +20 -3
- package/server/services/ai/tools.js +201 -31
- package/server/services/cli/shell_worker_pool.js +87 -5
- package/server/services/integrations/github/common.js +2 -2
- package/server/services/integrations/github/repos.js +123 -55
- package/server/services/manager.js +16 -0
- package/server/services/memory/manager.js +39 -24
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/runtime/manager.js +6 -2
- package/server/services/security/approval_gate_service.js +108 -5
- package/server/services/security/tool_categories.js +113 -4
- package/server/services/security/tool_security_hook.js +7 -2
- package/server/services/skills/runtime.js +2 -0
- 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,
|
|
@@ -129,10 +141,11 @@ const {
|
|
|
129
141
|
} = require('../messagingFallback');
|
|
130
142
|
const {
|
|
131
143
|
classifyToolExecution,
|
|
144
|
+
isSubstantiveProgressToolName,
|
|
145
|
+
summarizeProgressToolExecutions,
|
|
132
146
|
summarizeToolExecutions,
|
|
133
147
|
summarizeAvailableTools,
|
|
134
148
|
inferToolFailureMessage,
|
|
135
|
-
buildAutonomousRecoveryContext,
|
|
136
149
|
} = require('../toolEvidence');
|
|
137
150
|
const {
|
|
138
151
|
buildMemoryConsolidationInstructions,
|
|
@@ -195,7 +208,7 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
195
208
|
// Immediate guidance on first occurrence for high-signal patterns that waste
|
|
196
209
|
// multiple iterations before self-correcting.
|
|
197
210
|
const immediateGuides = {
|
|
198
|
-
eisdir: 'That path is a directory
|
|
211
|
+
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
212
|
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
213
|
};
|
|
201
214
|
if (immediateGuides[key]) {
|
|
@@ -205,9 +218,9 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
205
218
|
|
|
206
219
|
if (count < 3) return null;
|
|
207
220
|
const guides = {
|
|
208
|
-
outside_workspace: 'read_file
|
|
221
|
+
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
222
|
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 ~/.
|
|
223
|
+
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
224
|
not_found: 'This path or resource was not found. Try listing the parent directory or checking with a broader search first.',
|
|
212
225
|
};
|
|
213
226
|
const guide = guides[key];
|
|
@@ -217,8 +230,17 @@ function buildErrorPatternGuidance(key, count) {
|
|
|
217
230
|
|
|
218
231
|
const OUTPUT_FINGERPRINT_TOOLS = /^(list_|search_|read_|get_|find_|github_list|github_get|github_search)/;
|
|
219
232
|
|
|
220
|
-
function fingerprintOutput(toolName, result) {
|
|
221
|
-
|
|
233
|
+
function fingerprintOutput(toolName, result, toolArgs = {}) {
|
|
234
|
+
const name = String(toolName || '');
|
|
235
|
+
if (
|
|
236
|
+
!name
|
|
237
|
+
|| (
|
|
238
|
+
!OUTPUT_FINGERPRINT_TOOLS.test(name)
|
|
239
|
+
&& !(name === 'execute_command' && !isProgressToolCall(name, toolArgs))
|
|
240
|
+
)
|
|
241
|
+
) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
222
244
|
const raw = typeof result === 'string' ? result : JSON.stringify(result ?? '');
|
|
223
245
|
if (raw.length < 200) return null;
|
|
224
246
|
// djb2 hash over first 3000 chars — fast, collision-unlikely for our sizes
|
|
@@ -228,16 +250,37 @@ function fingerprintOutput(toolName, result) {
|
|
|
228
250
|
return h >>> 0;
|
|
229
251
|
}
|
|
230
252
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
253
|
+
// Concise list of files/targets the run has already read or searched, so the
|
|
254
|
+
// analysis-paralysis nudge can name them and tell the model not to re-read them.
|
|
255
|
+
function summarizeReadTargets(toolExecutions = []) {
|
|
256
|
+
const targets = [];
|
|
257
|
+
const seen = new Set();
|
|
258
|
+
for (const item of toolExecutions) {
|
|
259
|
+
if (!item || item.stateChanged) continue; // only read-only steps
|
|
260
|
+
const input = item.input || {};
|
|
261
|
+
let target = '';
|
|
262
|
+
if (Array.isArray(input.files) && input.files.length) {
|
|
263
|
+
target = input.files
|
|
264
|
+
.map((file) => (typeof file === 'string' ? file : file?.path || file?.file_path || ''))
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.slice(0, 3)
|
|
267
|
+
.join(', ');
|
|
268
|
+
} else if (typeof input.path === 'string' && input.path.trim()) {
|
|
269
|
+
target = input.path.trim();
|
|
270
|
+
} else if (typeof input.command === 'string') {
|
|
271
|
+
const files = input.command.match(/[\w./-]+\.(?:js|ts|tsx|jsx|py|dart|json|md|kt|c|h|ya?ml|sql|txt|sh)\b/g);
|
|
272
|
+
if (files && files.length) target = [...new Set(files)].slice(0, 2).join(', ');
|
|
273
|
+
} else if (typeof input.query === 'string' && input.query.trim()) {
|
|
274
|
+
target = `search:${input.query.trim().slice(0, 30)}`;
|
|
275
|
+
}
|
|
276
|
+
if (!target) continue;
|
|
277
|
+
const key = target.toLowerCase();
|
|
278
|
+
if (seen.has(key)) continue;
|
|
279
|
+
seen.add(key);
|
|
280
|
+
targets.push(target);
|
|
281
|
+
if (targets.length >= 8) break;
|
|
282
|
+
}
|
|
283
|
+
return targets.join('; ');
|
|
241
284
|
}
|
|
242
285
|
|
|
243
286
|
function cloneInterimHistory(history = []) {
|
|
@@ -556,9 +599,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
556
599
|
|
|
557
600
|
const runTitle = generateTitle(userMessage);
|
|
558
601
|
const initialRunMetadata = buildInitialRunMetadata(options);
|
|
559
|
-
db.prepare(`INSERT
|
|
602
|
+
db.prepare(`INSERT INTO agent_runs(
|
|
560
603
|
id, user_id, agent_id, title, status, trigger_type, trigger_source, model, metadata_json
|
|
561
|
-
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
604
|
+
) VALUES(?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
605
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
606
|
+
status = 'running',
|
|
607
|
+
model = excluded.model,
|
|
608
|
+
updated_at = datetime('now'),
|
|
609
|
+
completed_at = NULL,
|
|
610
|
+
error = NULL,
|
|
611
|
+
metadata_json = COALESCE(agent_runs.metadata_json, excluded.metadata_json)`).run(
|
|
562
612
|
runId,
|
|
563
613
|
userId,
|
|
564
614
|
agentId,
|
|
@@ -615,6 +665,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
615
665
|
steeringQueue: [],
|
|
616
666
|
systemSteeringQueue: [],
|
|
617
667
|
toolPids: new Set(),
|
|
668
|
+
subagentDepth: Math.max(0, Number(options.subagentDepth) || 0),
|
|
618
669
|
repetitionGuard: new ToolRepetitionGuard(),
|
|
619
670
|
seenOutputHashes: new Map(),
|
|
620
671
|
consecutiveReadOnlyIterations: 0,
|
|
@@ -667,7 +718,13 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
667
718
|
const mcpManager = app?.locals?.mcpManager || app?.locals?.mcpClient || engine.mcpManager;
|
|
668
719
|
const integrationManager = app?.locals?.integrationManager || null;
|
|
669
720
|
const mcpTools = mcpManager ? mcpManager.getAllTools(userId, { agentId }) : [];
|
|
670
|
-
const
|
|
721
|
+
const disallowedToolNames = new Set(
|
|
722
|
+
(Array.isArray(options.disallowedToolNames) ? options.disallowedToolNames : [])
|
|
723
|
+
.map((name) => String(name || '').trim())
|
|
724
|
+
.filter(Boolean),
|
|
725
|
+
);
|
|
726
|
+
const allTools = selectToolsForTask(userMessage, builtInTools, mcpTools, options)
|
|
727
|
+
.filter((tool) => !disallowedToolNames.has(tool?.name));
|
|
671
728
|
let tools = allTools;
|
|
672
729
|
const toolNames = allTools.map((tool) => tool.name).filter(Boolean);
|
|
673
730
|
const coreToolStatus = {
|
|
@@ -686,7 +743,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
686
743
|
console.info(
|
|
687
744
|
`[Run ${shortenRunId(runId)}] tools total=${toolNames.length} builtIns=${builtInTools.length} mcp=${mcpTools.length} core=${JSON.stringify(coreToolStatus)}`
|
|
688
745
|
);
|
|
689
|
-
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine
|
|
746
|
+
const capabilityHealth = await getCapabilityHealth({ userId, agentId, app, engine });
|
|
690
747
|
const capabilitySummary = summarizeCapabilityHealth(capabilityHealth);
|
|
691
748
|
const integrationSummary = integrationManager?.summarizeConnectedProviders?.(userId, agentId) || '';
|
|
692
749
|
|
|
@@ -762,6 +819,62 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
762
819
|
let compactionMetrics = [];
|
|
763
820
|
let analysis = null;
|
|
764
821
|
let plan = null;
|
|
822
|
+
|
|
823
|
+
// Model-authored progress updates: the messaging supervisor calls this to get a
|
|
824
|
+
// dynamic "what I'm doing right now" line instead of a hard-coded string. It runs
|
|
825
|
+
// a tiny tool-less model call over the current run activity. `provider`/`model` are
|
|
826
|
+
// `let` (closure tracks fallbacks) and `toolExecutions` is push-only.
|
|
827
|
+
if (triggerSource === 'messaging') {
|
|
828
|
+
const runMetaForCompose = engine.getRunMeta(runId);
|
|
829
|
+
if (runMetaForCompose) {
|
|
830
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
831
|
+
try {
|
|
832
|
+
const rm = engine.getRunMeta(runId);
|
|
833
|
+
const ledger = rm?.progressLedger || {};
|
|
834
|
+
// Real, specific evidence (actual commands + their output) so the update is
|
|
835
|
+
// grounded in what happened, not invented. Bare tool names make a weak model
|
|
836
|
+
// confabulate generic activity ("running the build", "training").
|
|
837
|
+
const recent = summarizeProgressToolExecutions(toolExecutions, 5);
|
|
838
|
+
const currentTool = isSubstantiveProgressToolName(ledger.currentTool)
|
|
839
|
+
? ledger.currentTool
|
|
840
|
+
: '';
|
|
841
|
+
if (!recent && !currentTool) {
|
|
842
|
+
return '';
|
|
843
|
+
}
|
|
844
|
+
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
845
|
+
const contextBlock = [
|
|
846
|
+
buildProgressUpdatePrompt(),
|
|
847
|
+
stalled ? 'This step has been running a while with no new progress; reassure the user you are still on it.' : '',
|
|
848
|
+
'',
|
|
849
|
+
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
850
|
+
currentTool ? `Doing now: using ${currentTool}` : '',
|
|
851
|
+
recent ? `Actual recent tool activity (newest last) — describe ONLY this, do not extrapolate:\n${recent}` : '',
|
|
852
|
+
priorUpdate ? `Your previous update (say something different): ${summarizeForLog(priorUpdate, 160)}` : '',
|
|
853
|
+
].filter(Boolean).join('\n');
|
|
854
|
+
// Reuse the run's real system prompt so the update follows the same voice and
|
|
855
|
+
// formatting guidelines as every other message (single source of truth).
|
|
856
|
+
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
857
|
+
|| 'You are a helpful assistant.';
|
|
858
|
+
const resp = await withModelCallTimeout(
|
|
859
|
+
provider.chat(
|
|
860
|
+
[
|
|
861
|
+
{ role: 'system', content: sysContent },
|
|
862
|
+
{ role: 'user', content: contextBlock },
|
|
863
|
+
],
|
|
864
|
+
[],
|
|
865
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
866
|
+
),
|
|
867
|
+
options,
|
|
868
|
+
'Progress update compose',
|
|
869
|
+
);
|
|
870
|
+
return sanitizeModelOutput(resp.content || '', { model });
|
|
871
|
+
} catch (composeErr) {
|
|
872
|
+
console.warn(`[Run ${shortenRunId(runId)}] progress_update_compose failed: ${summarizeForLog(composeErr?.message || composeErr, 140)}`);
|
|
873
|
+
return '';
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
765
878
|
let verification = null;
|
|
766
879
|
let deliverableWorkflow = null;
|
|
767
880
|
let deliverablePlan = null;
|
|
@@ -827,6 +940,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
827
940
|
|
|
828
941
|
tools = selectInitialTools(allTools, analysis.suggested_tools, {
|
|
829
942
|
widgetId: options.widgetId || null,
|
|
943
|
+
includeCoreFileTools: analysis.mode === 'execute' || analysis.mode === 'plan_execute',
|
|
830
944
|
});
|
|
831
945
|
engine.initializeToolRuntime(runId, allTools, tools, options);
|
|
832
946
|
messages.push({
|
|
@@ -836,6 +950,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
836
950
|
buildToolCatalog(allTools),
|
|
837
951
|
'',
|
|
838
952
|
`Active tools: ${tools.map((tool) => tool.name).join(', ')}`,
|
|
953
|
+
'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
954
|
'Use activate_tools with exact catalog names when another schema is required.',
|
|
840
955
|
].join('\n'),
|
|
841
956
|
});
|
|
@@ -896,54 +1011,63 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
896
1011
|
maxIterations = loopPolicy.maxIterations;
|
|
897
1012
|
|
|
898
1013
|
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,
|
|
1014
|
+
try {
|
|
1015
|
+
const deliverableSelectionResult = await selectDeliverableWorkflow({
|
|
1016
|
+
engine,
|
|
1017
|
+
provider,
|
|
1018
|
+
providerName,
|
|
1019
|
+
model,
|
|
1020
|
+
messages,
|
|
921
1021
|
tools,
|
|
922
|
-
options,
|
|
923
|
-
});
|
|
924
|
-
await selectedWorkflow.run(deliverablePlan, {
|
|
925
|
-
engine: this,
|
|
926
|
-
userId,
|
|
927
|
-
agentId,
|
|
928
|
-
runId,
|
|
929
|
-
agentId,
|
|
930
|
-
app,
|
|
1022
|
+
options: { ...options, runId, userId, agentId },
|
|
931
1023
|
});
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
1024
|
+
totalTokens += deliverableSelectionResult.usage || 0;
|
|
1025
|
+
const selectedWorkflow = getDeliverableWorkflow(deliverableSelectionResult.selection.type);
|
|
1026
|
+
if (selectedWorkflow?.canHandle(deliverableSelectionResult.selection)) {
|
|
1027
|
+
deliverableWorkflow = {
|
|
1028
|
+
workflow: selectedWorkflow,
|
|
1029
|
+
selection: deliverableSelectionResult.selection,
|
|
1030
|
+
request: selectedWorkflow.normalizeRequest({
|
|
1031
|
+
...deliverableSelectionResult.selection,
|
|
1032
|
+
userMessage,
|
|
1033
|
+
}),
|
|
1034
|
+
};
|
|
1035
|
+
deliverablePlan = selectedWorkflow.buildExecutionPlan(deliverableWorkflow.request, {
|
|
1036
|
+
analysis,
|
|
1037
|
+
tools,
|
|
1038
|
+
options,
|
|
1039
|
+
});
|
|
1040
|
+
await selectedWorkflow.run(deliverablePlan, {
|
|
1041
|
+
engine,
|
|
1042
|
+
userId,
|
|
1043
|
+
agentId,
|
|
1044
|
+
runId,
|
|
1045
|
+
app,
|
|
1046
|
+
});
|
|
1047
|
+
engine.persistRunMetadata(runId, {
|
|
1048
|
+
deliverableWorkflow: {
|
|
1049
|
+
...deliverableWorkflow.selection,
|
|
1050
|
+
plan: deliverablePlan,
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
engine.updateRunGoalContract(runId, {
|
|
1054
|
+
goal: deliverableWorkflow.selection.goal,
|
|
1055
|
+
});
|
|
1056
|
+
engine.recordRunEvent(userId, runId, 'deliverable_workflow_selected', {
|
|
1057
|
+
type: deliverableWorkflow.selection.type,
|
|
1058
|
+
confidence: deliverableWorkflow.selection.confidence,
|
|
1059
|
+
goal: deliverableWorkflow.selection.goal,
|
|
1060
|
+
requestedOutputs: deliverableWorkflow.selection.requestedOutputs,
|
|
1061
|
+
}, { agentId });
|
|
1062
|
+
}
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
engine.recordRunEvent(userId, runId, 'deliverable_workflow_skipped', {
|
|
1065
|
+
reason: summarizeForLog(error?.message || error, 240),
|
|
946
1066
|
}, { agentId });
|
|
1067
|
+
messages.push({
|
|
1068
|
+
role: 'system',
|
|
1069
|
+
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.',
|
|
1070
|
+
});
|
|
947
1071
|
}
|
|
948
1072
|
}
|
|
949
1073
|
|
|
@@ -1051,9 +1175,18 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1051
1175
|
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1052
1176
|
if (readOnlyCount >= 3) {
|
|
1053
1177
|
const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
|
|
1178
|
+
const alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1054
1179
|
messages.push({
|
|
1055
1180
|
role: 'system',
|
|
1056
|
-
content:
|
|
1181
|
+
content: [
|
|
1182
|
+
`${urgency} — ${readOnlyCount} consecutive read-only turns with no concrete action.`,
|
|
1183
|
+
alreadyRead
|
|
1184
|
+
? `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.`
|
|
1185
|
+
: 'Do not re-read or re-search anything already in this conversation.',
|
|
1186
|
+
'If you are repeatedly extracting evidence through low-level commands, switch to the available file/search/edit tools over the shared workspace.',
|
|
1187
|
+
'Act on what you already know now: edit files, run a state-changing command, create the branch/PR, or send the result.',
|
|
1188
|
+
'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.',
|
|
1189
|
+
].join(' '),
|
|
1057
1190
|
});
|
|
1058
1191
|
}
|
|
1059
1192
|
}
|
|
@@ -1268,6 +1401,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1268
1401
|
})) {
|
|
1269
1402
|
break;
|
|
1270
1403
|
}
|
|
1404
|
+
if (shouldContinueAfterBlankToolFailure({
|
|
1405
|
+
lastContent,
|
|
1406
|
+
failedStepCount,
|
|
1407
|
+
remainingIterations: iterationBudget.remaining,
|
|
1408
|
+
toolExecutions,
|
|
1409
|
+
})) {
|
|
1410
|
+
engine.recordRunEvent(userId, runId, 'blank_after_tool_failure_continued', {
|
|
1411
|
+
iteration,
|
|
1412
|
+
remainingIterations: iterationBudget.remaining,
|
|
1413
|
+
}, { agentId });
|
|
1414
|
+
messages.push({
|
|
1415
|
+
role: 'system',
|
|
1416
|
+
content: buildBlankAfterToolFailureGuidance(toolExecutions),
|
|
1417
|
+
});
|
|
1418
|
+
lastContent = '';
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1271
1421
|
const loopDecision = await engine.decideLoopState({
|
|
1272
1422
|
provider,
|
|
1273
1423
|
providerName,
|
|
@@ -1378,9 +1528,16 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1378
1528
|
}, {
|
|
1379
1529
|
verified: true,
|
|
1380
1530
|
});
|
|
1531
|
+
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1532
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1533
|
+
if (iterMeta) {
|
|
1534
|
+
iterMeta.consecutiveReadOnlyIterations = (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1381
1537
|
continue;
|
|
1382
1538
|
}
|
|
1383
1539
|
|
|
1540
|
+
let iterationConcreteProgress = false;
|
|
1384
1541
|
for (const toolCall of response.toolCalls) {
|
|
1385
1542
|
if (engine.isRunStopped(runId)) break;
|
|
1386
1543
|
stepIndex++;
|
|
@@ -1595,6 +1752,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1595
1752
|
|
|
1596
1753
|
const execution = classifyToolExecution(toolName, toolArgs, toolResult, toolErrorMessage);
|
|
1597
1754
|
execution.input = toolArgs;
|
|
1755
|
+
if (execution.stateChanged && isProgressToolCall(toolName, toolArgs)) {
|
|
1756
|
+
iterationConcreteProgress = true;
|
|
1757
|
+
}
|
|
1598
1758
|
repetitionGuard?.observe(toolName, toolArgs, toolResult);
|
|
1599
1759
|
execution.artifacts = await extractArtifactsFromResult(toolName, toolResult);
|
|
1600
1760
|
toolExecutions.push(execution);
|
|
@@ -1684,7 +1844,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1684
1844
|
// Output fingerprint guard: steer away from re-fetching data already seen.
|
|
1685
1845
|
if (!toolErrorMessage) {
|
|
1686
1846
|
const currentRunMeta = engine.getRunMeta(runId);
|
|
1687
|
-
const fp = fingerprintOutput(toolName, toolResult);
|
|
1847
|
+
const fp = fingerprintOutput(toolName, toolResult, toolArgs);
|
|
1688
1848
|
if (fp !== null && currentRunMeta?.seenOutputHashes) {
|
|
1689
1849
|
const prior = currentRunMeta.seenOutputHashes.get(fp);
|
|
1690
1850
|
if (prior) {
|
|
@@ -1694,24 +1854,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1694
1854
|
});
|
|
1695
1855
|
} else {
|
|
1696
1856
|
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
1857
|
}
|
|
1716
1858
|
}
|
|
1717
1859
|
}
|
|
@@ -1782,8 +1924,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1782
1924
|
&& (analysis.mode === 'execute' || analysis.mode === 'plan_execute')) {
|
|
1783
1925
|
const iterMeta = engine.getRunMeta(runId);
|
|
1784
1926
|
if (iterMeta) {
|
|
1785
|
-
|
|
1786
|
-
iterMeta.consecutiveReadOnlyIterations = calledProgress
|
|
1927
|
+
iterMeta.consecutiveReadOnlyIterations = iterationConcreteProgress
|
|
1787
1928
|
? 0
|
|
1788
1929
|
: (iterMeta.consecutiveReadOnlyIterations || 0) + 1;
|
|
1789
1930
|
}
|
|
@@ -1796,20 +1937,41 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1796
1937
|
}
|
|
1797
1938
|
|
|
1798
1939
|
if (engine.isRunStopped(runId)) {
|
|
1799
|
-
|
|
1800
|
-
|
|
1940
|
+
const stoppedRunMeta = engine.getRunMeta(runId);
|
|
1941
|
+
const terminalStatus = stoppedRunMeta?.status === 'interrupted' ? 'interrupted' : 'stopped';
|
|
1942
|
+
const stopReason = stoppedRunMeta?.stopReason || null;
|
|
1943
|
+
db.prepare(
|
|
1944
|
+
`UPDATE agent_runs
|
|
1945
|
+
SET status = ?,
|
|
1946
|
+
error = COALESCE(?, error),
|
|
1947
|
+
updated_at = datetime('now'),
|
|
1948
|
+
completed_at = datetime('now')
|
|
1949
|
+
WHERE id = ?`
|
|
1950
|
+
).run(terminalStatus, stopReason, runId);
|
|
1801
1951
|
console.warn(
|
|
1802
|
-
`[Run ${shortenRunId(runId)}]
|
|
1952
|
+
`[Run ${shortenRunId(runId)}] ${terminalStatus} trigger=${triggerSource} steps=${stepIndex} tokens=${totalTokens}`
|
|
1803
1953
|
);
|
|
1954
|
+
engine.cleanupSubagentsForRun(runId, { cancelRunning: true });
|
|
1804
1955
|
engine.stopMessagingProgressSupervisor(runId);
|
|
1805
1956
|
engine.activeRuns.delete(runId);
|
|
1806
|
-
engine.emit(userId, 'run:stopped', {
|
|
1807
|
-
|
|
1957
|
+
engine.emit(userId, terminalStatus === 'interrupted' ? 'run:interrupted' : 'run:stopped', {
|
|
1958
|
+
runId,
|
|
1808
1959
|
triggerSource,
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1960
|
+
reason: stopReason,
|
|
1961
|
+
});
|
|
1962
|
+
engine.recordRunEvent(
|
|
1963
|
+
userId,
|
|
1964
|
+
runId,
|
|
1965
|
+
terminalStatus === 'interrupted' ? 'run_interrupted' : 'run_stopped',
|
|
1966
|
+
{
|
|
1967
|
+
triggerSource,
|
|
1968
|
+
totalTokens,
|
|
1969
|
+
iterations: iteration,
|
|
1970
|
+
reason: stopReason,
|
|
1971
|
+
},
|
|
1972
|
+
{ agentId },
|
|
1973
|
+
);
|
|
1974
|
+
return { runId, content: '', totalTokens, iterations: iteration, status: terminalStatus };
|
|
1813
1975
|
}
|
|
1814
1976
|
|
|
1815
1977
|
const runMeta = engine.activeRuns.get(runId);
|
|
@@ -1822,6 +1984,54 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1822
1984
|
const messagingSent = runMeta?.messagingSent || false;
|
|
1823
1985
|
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
1824
1986
|
|
|
1987
|
+
// Hermes _handle_max_iterations: if the run exhausted its step budget without a
|
|
1988
|
+
// judged completion, the model's last text is usually a mid-thought fragment
|
|
1989
|
+
// ("let me inline everything:"). Do one tool-less wrap-up call so the user gets a
|
|
1990
|
+
// real final answer instead of that fragment.
|
|
1991
|
+
const budgetExhaustedWithoutCompletion = triggerSource === 'messaging'
|
|
1992
|
+
&& !directAnswerEligible
|
|
1993
|
+
&& !messagingSent
|
|
1994
|
+
&& !runMeta?.terminalInterim
|
|
1995
|
+
&& iteration >= maxIterations;
|
|
1996
|
+
if (budgetExhaustedWithoutCompletion) {
|
|
1997
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
1998
|
+
try {
|
|
1999
|
+
const wrapResponse = await withModelCallTimeout(
|
|
2000
|
+
provider.chat(
|
|
2001
|
+
sanitizeConversationMessages([
|
|
2002
|
+
...messages,
|
|
2003
|
+
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
2004
|
+
]),
|
|
2005
|
+
[],
|
|
2006
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
2007
|
+
),
|
|
2008
|
+
options,
|
|
2009
|
+
'Max-iteration wrap-up',
|
|
2010
|
+
);
|
|
2011
|
+
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
2012
|
+
const wrapText = sanitizeModelOutput(wrapResponse.content || '', { model });
|
|
2013
|
+
// On budget exhaustion the model's last text is an untrustworthy mid-thought
|
|
2014
|
+
// fragment. Replace it with the wrap-up answer, or a clean deterministic
|
|
2015
|
+
// fallback if the wrap-up came back empty — never deliver the fragment.
|
|
2016
|
+
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null);
|
|
2017
|
+
lastContent = usableWrap
|
|
2018
|
+
? wrapText
|
|
2019
|
+
: buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2020
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2021
|
+
if (conversationId) {
|
|
2022
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2023
|
+
.run(conversationId, 'assistant', lastContent, usableWrap ? (wrapResponse.usage?.totalTokens || 0) : 0);
|
|
2024
|
+
}
|
|
2025
|
+
engine.recordRunEvent(userId, runId, 'max_iteration_wrapup_delivered', {
|
|
2026
|
+
iteration, maxIterations, stepIndex, source: usableWrap ? 'model' : 'deterministic',
|
|
2027
|
+
}, { agentId });
|
|
2028
|
+
} catch (wrapErr) {
|
|
2029
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
2030
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2031
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1825
2035
|
if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
|
|
1826
2036
|
// Simplified blank reply recovery: one model call with direct instruction,
|
|
1827
2037
|
// then fall back to a deterministic message. No multi-attempt LLM loop.
|
|
@@ -1852,37 +2062,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1852
2062
|
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`);
|
|
1853
2063
|
}
|
|
1854
2064
|
totalTokens += recoveredTokens;
|
|
1855
|
-
|
|
2065
|
+
// The loop has already exited, so we cannot keep working: deliver the model's
|
|
2066
|
+
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2067
|
+
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2068
|
+
// a deterministic message when the model returned nothing usable.
|
|
2069
|
+
const recoveredVisible = Boolean(normalizeOutgoingMessage(lastContent, options?.source || null));
|
|
2070
|
+
if (!recoveredVisible) {
|
|
1856
2071
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1857
2072
|
}
|
|
1858
2073
|
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,
|
|
2074
|
+
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_delivered', {
|
|
2075
|
+
source: recoveredVisible ? 'model' : 'deterministic',
|
|
2076
|
+
stepIndex,
|
|
2077
|
+
failedStepCount,
|
|
1878
2078
|
}, { agentId });
|
|
1879
|
-
if (recoveryDecision.decision.status === 'continue') {
|
|
1880
|
-
throw new Error('Messaging run ended without a judged terminal reply.');
|
|
1881
|
-
}
|
|
1882
2079
|
messages.push({ role: 'assistant', content: lastContent });
|
|
1883
2080
|
if (conversationId) {
|
|
1884
2081
|
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1885
|
-
.run(conversationId, 'assistant', lastContent, recoveredTokens);
|
|
2082
|
+
.run(conversationId, 'assistant', lastContent, recoveredVisible ? recoveredTokens : 0);
|
|
1886
2083
|
}
|
|
1887
2084
|
}
|
|
1888
2085
|
}
|
|
@@ -1910,10 +2107,28 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1910
2107
|
stepIndex,
|
|
1911
2108
|
failedStepCount,
|
|
1912
2109
|
}, { agentId });
|
|
1913
|
-
|
|
2110
|
+
if (triggerSource === 'messaging') {
|
|
2111
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2112
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2113
|
+
if (conversationId) {
|
|
2114
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2115
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
2116
|
+
}
|
|
2117
|
+
} else {
|
|
2118
|
+
throw new Error(`Iteration budget exhausted before judged completion after ${maxIterations} iterations.`);
|
|
2119
|
+
}
|
|
1914
2120
|
}
|
|
1915
2121
|
if (stepIndex > 0 && !lastToolWasMessaging && iteration < maxIterations) {
|
|
1916
|
-
|
|
2122
|
+
if (triggerSource === 'messaging') {
|
|
2123
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
2124
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2125
|
+
if (conversationId) {
|
|
2126
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2127
|
+
.run(conversationId, 'assistant', lastContent, 0);
|
|
2128
|
+
}
|
|
2129
|
+
} else {
|
|
2130
|
+
throw new Error('Run ended without an explicit completion or blocker reply.');
|
|
2131
|
+
}
|
|
1917
2132
|
}
|
|
1918
2133
|
}
|
|
1919
2134
|
|
|
@@ -2156,149 +2371,73 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2156
2371
|
}
|
|
2157
2372
|
|
|
2158
2373
|
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
2374
|
|
|
2234
2375
|
const deliverableFailureResponse = err?.deliverableResult?.summary
|
|
2235
2376
|
|| err?.deliverableValidation?.summary
|
|
2236
2377
|
|| '';
|
|
2237
2378
|
let messagingFailureContent = '';
|
|
2238
2379
|
let sendSucceeded = false;
|
|
2239
|
-
if (triggerSource
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2380
|
+
if (shouldSendMessagingErrorFallback({ triggerSource, options, runMeta })) {
|
|
2381
|
+
const manager = engine.messagingManager;
|
|
2382
|
+
if (manager) {
|
|
2383
|
+
const failureScenario = buildMessagingFailureScenario({
|
|
2384
|
+
err,
|
|
2385
|
+
failedStepCount,
|
|
2386
|
+
stepIndex,
|
|
2387
|
+
toolExecutions,
|
|
2388
|
+
});
|
|
2389
|
+
try {
|
|
2390
|
+
const failedMessage = sanitizeConversationMessages([
|
|
2391
|
+
...messages,
|
|
2392
|
+
{
|
|
2393
|
+
role: 'system',
|
|
2394
|
+
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)}`
|
|
2395
|
+
}
|
|
2396
|
+
]);
|
|
2397
|
+
const modelReply = await withModelCallTimeout(
|
|
2398
|
+
provider.chat(failedMessage, [], {
|
|
2399
|
+
model,
|
|
2400
|
+
reasoningEffort: engine.getReasoningEffort(providerName, options)
|
|
2401
|
+
}),
|
|
2402
|
+
options,
|
|
2403
|
+
'Messaging failure reply',
|
|
2404
|
+
);
|
|
2405
|
+
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2406
|
+
if (normalizeOutgoingMessage(drafted, options?.source || null)) {
|
|
2407
|
+
messagingFailureContent = drafted.trim();
|
|
2408
|
+
}
|
|
2409
|
+
} catch {
|
|
2410
|
+
// Fall back to deterministic text below.
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (!messagingFailureContent) {
|
|
2414
|
+
messagingFailureContent = buildDeterministicMessagingErrorReply({
|
|
2244
2415
|
err,
|
|
2245
2416
|
failedStepCount,
|
|
2246
2417
|
stepIndex,
|
|
2247
2418
|
toolExecutions,
|
|
2248
2419
|
});
|
|
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
|
-
}
|
|
2420
|
+
}
|
|
2281
2421
|
|
|
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 = '';
|
|
2422
|
+
try {
|
|
2423
|
+
const deliveryResult = await manager.sendMessage(
|
|
2424
|
+
userId,
|
|
2425
|
+
options.source,
|
|
2426
|
+
options.chatId,
|
|
2427
|
+
messagingFailureContent,
|
|
2428
|
+
{ runId, agentId },
|
|
2429
|
+
);
|
|
2430
|
+
requireSuccessfulMessagingDelivery(deliveryResult, 'Messaging failure delivery');
|
|
2431
|
+
sendSucceeded = true;
|
|
2432
|
+
if (runMeta) {
|
|
2433
|
+
runMeta.lastSentMessage = messagingFailureContent;
|
|
2434
|
+
if (!Array.isArray(runMeta.sentMessages)) runMeta.sentMessages = [];
|
|
2435
|
+
runMeta.sentMessages.push(messagingFailureContent);
|
|
2301
2436
|
}
|
|
2437
|
+
engine.markRunFinalDelivery(runId, messagingFailureContent);
|
|
2438
|
+
} catch (sendErr) {
|
|
2439
|
+
console.error('[Engine] Messaging error fallback failed:', sendErr.message);
|
|
2440
|
+
messagingFailureContent = '';
|
|
2302
2441
|
}
|
|
2303
2442
|
}
|
|
2304
2443
|
}
|