neoagent 2.5.2-beta.20 → 2.5.2-beta.22
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/package.json +1 -1
- 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/loop/conversation_loop.js +150 -39
- package/server/services/ai/loop/messaging_delivery.js +46 -0
- package/server/services/ai/loop/progress_monitor.js +3 -3
- package/server/services/ai/loopPolicy.js +15 -6
- package/server/services/ai/messagingFallback.js +21 -0
- package/server/services/ai/taskAnalysis.js +1 -0
- package/server/services/runtime/docker-vm-manager.js +21 -1
- package/server/services/workspace/manager.js +1 -0
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7b68be842f70ebca5228682c4fe3dc90
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "1027370389" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -134794,7 +134794,7 @@ r===$&&A.b()
|
|
|
134794
134794
|
p.push(A.jP(q,A.j9(!1,new A.a_(B.uG,A.d8(new A.cA(B.jt,new A.a7N(r,q),q),q,q),q),!1,B.H,!0),q,q,0,0,0,q))}r=!1
|
|
134795
134795
|
if(!s.ay)if(!s.ch){r=s.e
|
|
134796
134796
|
r===$&&A.b()
|
|
134797
|
-
r=B.b.u("
|
|
134797
|
+
r=B.b.u("mqgt278r-971ca3d").length!==0&&r.b}if(r){r=s.d
|
|
134798
134798
|
r===$&&A.b()
|
|
134799
134799
|
r=r.aP&&!r.ai?84:0
|
|
134800
134800
|
s=s.e
|
|
@@ -140506,7 +140506,7 @@ $S:0}
|
|
|
140506
140506
|
A.a_6.prototype={}
|
|
140507
140507
|
A.SQ.prototype={
|
|
140508
140508
|
nb(a){var s=this
|
|
140509
|
-
if(B.b.u("
|
|
140509
|
+
if(B.b.u("mqgt278r-971ca3d").length===0||s.a!=null)return
|
|
140510
140510
|
s.AU()
|
|
140511
140511
|
s.a=A.on(B.RH,new A.bc8(s))},
|
|
140512
140512
|
AU(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -140524,7 +140524,7 @@ if(!t.f.b(k)){s=1
|
|
|
140524
140524
|
break}i=J.a3(k,"buildId")
|
|
140525
140525
|
h=i==null?null:B.b.u(J.p(i))
|
|
140526
140526
|
j=h==null?"":h
|
|
140527
|
-
if(J.bi(j)===0||J.d(j,"
|
|
140527
|
+
if(J.bi(j)===0||J.d(j,"mqgt278r-971ca3d")){s=1
|
|
140528
140528
|
break}n.b=!0
|
|
140529
140529
|
n.F()
|
|
140530
140530
|
p=2
|
|
@@ -140541,7 +140541,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
140541
140541
|
return A.k($async$AU,r)},
|
|
140542
140542
|
vE(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1
|
|
140543
140543
|
var $async$vE=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
140544
|
-
s=p}for(;;)switch(s){case 0:if(B.b.u("
|
|
140544
|
+
s=p}for(;;)switch(s){case 0:if(B.b.u("mqgt278r-971ca3d").length===0||n.c){s=1
|
|
140545
140545
|
break}n.c=!0
|
|
140546
140546
|
n.F()
|
|
140547
140547
|
p=4
|
|
@@ -132,6 +132,8 @@ const {
|
|
|
132
132
|
clampRunContext,
|
|
133
133
|
joinSentMessages,
|
|
134
134
|
buildBlankMessagingReplyPrompt,
|
|
135
|
+
buildMaxIterationWrapupPrompt,
|
|
136
|
+
buildProgressUpdatePrompt,
|
|
135
137
|
buildDeterministicMessagingFallback,
|
|
136
138
|
buildMessagingFailureScenario,
|
|
137
139
|
buildDeterministicMessagingErrorReply,
|
|
@@ -246,6 +248,33 @@ function fingerprintOutput(toolName, result, toolArgs = {}) {
|
|
|
246
248
|
return h >>> 0;
|
|
247
249
|
}
|
|
248
250
|
|
|
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 (typeof input.path === 'string' && input.path.trim()) {
|
|
261
|
+
target = input.path.trim();
|
|
262
|
+
} else if (typeof input.command === 'string') {
|
|
263
|
+
const files = input.command.match(/[\w./-]+\.(?:js|ts|tsx|jsx|py|dart|json|md|kt|c|h|ya?ml|sql|txt|sh)\b/g);
|
|
264
|
+
if (files && files.length) target = [...new Set(files)].slice(0, 2).join(', ');
|
|
265
|
+
} else if (typeof input.query === 'string' && input.query.trim()) {
|
|
266
|
+
target = `search:${input.query.trim().slice(0, 30)}`;
|
|
267
|
+
}
|
|
268
|
+
if (!target) continue;
|
|
269
|
+
const key = target.toLowerCase();
|
|
270
|
+
if (seen.has(key)) continue;
|
|
271
|
+
seen.add(key);
|
|
272
|
+
targets.push(target);
|
|
273
|
+
if (targets.length >= 8) break;
|
|
274
|
+
}
|
|
275
|
+
return targets.join('; ');
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
function cloneInterimHistory(history = []) {
|
|
250
279
|
if (!Array.isArray(history)) return [];
|
|
251
280
|
return history.map((item) => ({
|
|
@@ -775,6 +804,56 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
775
804
|
let compactionMetrics = [];
|
|
776
805
|
let analysis = null;
|
|
777
806
|
let plan = null;
|
|
807
|
+
|
|
808
|
+
// Model-authored progress updates: the messaging supervisor calls this to get a
|
|
809
|
+
// dynamic "what I'm doing right now" line instead of a hard-coded string. It runs
|
|
810
|
+
// a tiny tool-less model call over the current run activity. `provider`/`model` are
|
|
811
|
+
// `let` (closure tracks fallbacks) and `toolExecutions` is push-only.
|
|
812
|
+
if (triggerSource === 'messaging') {
|
|
813
|
+
const runMetaForCompose = engine.getRunMeta(runId);
|
|
814
|
+
if (runMetaForCompose) {
|
|
815
|
+
runMetaForCompose.composeProgressUpdate = async ({ stalled = false } = {}) => {
|
|
816
|
+
try {
|
|
817
|
+
const rm = engine.getRunMeta(runId);
|
|
818
|
+
const ledger = rm?.progressLedger || {};
|
|
819
|
+
// Real, specific evidence (actual commands + their output) so the update is
|
|
820
|
+
// grounded in what happened, not invented. Bare tool names make a weak model
|
|
821
|
+
// confabulate generic activity ("running the build", "training").
|
|
822
|
+
const recent = summarizeToolExecutions(toolExecutions, 5) || '(no tool activity yet)';
|
|
823
|
+
const priorUpdate = String(rm?.lastInterimMessage || '').trim();
|
|
824
|
+
const contextBlock = [
|
|
825
|
+
buildProgressUpdatePrompt(),
|
|
826
|
+
stalled ? 'This step has been running a while with no new progress; reassure the user you are still on it.' : '',
|
|
827
|
+
'',
|
|
828
|
+
`Original request: ${summarizeForLog(userMessage, 320)}`,
|
|
829
|
+
`Doing now: ${ledger.currentTool ? `using ${ledger.currentTool}` : (ledger.currentPhase || 'thinking')}`,
|
|
830
|
+
`Actual recent tool activity (newest last) — describe ONLY this, do not extrapolate:\n${recent}`,
|
|
831
|
+
priorUpdate ? `Your previous update (say something different): ${summarizeForLog(priorUpdate, 160)}` : '',
|
|
832
|
+
].filter(Boolean).join('\n');
|
|
833
|
+
// Reuse the run's real system prompt so the update follows the same voice and
|
|
834
|
+
// formatting guidelines as every other message (single source of truth).
|
|
835
|
+
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
836
|
+
|| 'You are a helpful assistant.';
|
|
837
|
+
const resp = await withModelCallTimeout(
|
|
838
|
+
provider.chat(
|
|
839
|
+
[
|
|
840
|
+
{ role: 'system', content: sysContent },
|
|
841
|
+
{ role: 'user', content: contextBlock },
|
|
842
|
+
],
|
|
843
|
+
[],
|
|
844
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
845
|
+
),
|
|
846
|
+
options,
|
|
847
|
+
'Progress update compose',
|
|
848
|
+
);
|
|
849
|
+
return sanitizeModelOutput(resp.content || '', { model });
|
|
850
|
+
} catch (composeErr) {
|
|
851
|
+
console.warn(`[Run ${shortenRunId(runId)}] progress_update_compose failed: ${summarizeForLog(composeErr?.message || composeErr, 140)}`);
|
|
852
|
+
return '';
|
|
853
|
+
}
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
}
|
|
778
857
|
let verification = null;
|
|
779
858
|
let deliverableWorkflow = null;
|
|
780
859
|
let deliverablePlan = null;
|
|
@@ -1073,9 +1152,17 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1073
1152
|
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1074
1153
|
if (readOnlyCount >= 3) {
|
|
1075
1154
|
const urgency = readOnlyCount >= 6 ? 'CRITICAL' : 'ACTION REQUIRED';
|
|
1155
|
+
const alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1076
1156
|
messages.push({
|
|
1077
1157
|
role: 'system',
|
|
1078
|
-
content:
|
|
1158
|
+
content: [
|
|
1159
|
+
`${urgency} — ${readOnlyCount} consecutive read-only turns with no concrete action.`,
|
|
1160
|
+
alreadyRead
|
|
1161
|
+
? `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.`
|
|
1162
|
+
: 'Do not re-read or re-search anything already in this conversation.',
|
|
1163
|
+
'Act on what you already know now: edit files, run a state-changing command, create the branch/PR, or send the result.',
|
|
1164
|
+
'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.',
|
|
1165
|
+
].join(' '),
|
|
1079
1166
|
});
|
|
1080
1167
|
}
|
|
1081
1168
|
}
|
|
@@ -1847,6 +1934,54 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1847
1934
|
const messagingSent = runMeta?.messagingSent || false;
|
|
1848
1935
|
const lastToolWasMessaging = runMeta?.lastToolName === 'send_message' || runMeta?.lastToolName === 'make_call';
|
|
1849
1936
|
|
|
1937
|
+
// Hermes _handle_max_iterations: if the run exhausted its step budget without a
|
|
1938
|
+
// judged completion, the model's last text is usually a mid-thought fragment
|
|
1939
|
+
// ("let me inline everything:"). Do one tool-less wrap-up call so the user gets a
|
|
1940
|
+
// real final answer instead of that fragment.
|
|
1941
|
+
const budgetExhaustedWithoutCompletion = triggerSource === 'messaging'
|
|
1942
|
+
&& !directAnswerEligible
|
|
1943
|
+
&& !messagingSent
|
|
1944
|
+
&& !runMeta?.terminalInterim
|
|
1945
|
+
&& iteration >= maxIterations;
|
|
1946
|
+
if (budgetExhaustedWithoutCompletion) {
|
|
1947
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup model=${model} iteration=${iteration}/${maxIterations}`);
|
|
1948
|
+
try {
|
|
1949
|
+
const wrapResponse = await withModelCallTimeout(
|
|
1950
|
+
provider.chat(
|
|
1951
|
+
sanitizeConversationMessages([
|
|
1952
|
+
...messages,
|
|
1953
|
+
{ role: 'system', content: buildMaxIterationWrapupPrompt(options?.source || null) },
|
|
1954
|
+
]),
|
|
1955
|
+
[],
|
|
1956
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
1957
|
+
),
|
|
1958
|
+
options,
|
|
1959
|
+
'Max-iteration wrap-up',
|
|
1960
|
+
);
|
|
1961
|
+
totalTokens += wrapResponse.usage?.totalTokens || 0;
|
|
1962
|
+
const wrapText = sanitizeModelOutput(wrapResponse.content || '', { model });
|
|
1963
|
+
// On budget exhaustion the model's last text is an untrustworthy mid-thought
|
|
1964
|
+
// fragment. Replace it with the wrap-up answer, or a clean deterministic
|
|
1965
|
+
// fallback if the wrap-up came back empty — never deliver the fragment.
|
|
1966
|
+
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null);
|
|
1967
|
+
lastContent = usableWrap
|
|
1968
|
+
? wrapText
|
|
1969
|
+
: buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1970
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1971
|
+
if (conversationId) {
|
|
1972
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1973
|
+
.run(conversationId, 'assistant', lastContent, usableWrap ? (wrapResponse.usage?.totalTokens || 0) : 0);
|
|
1974
|
+
}
|
|
1975
|
+
engine.recordRunEvent(userId, runId, 'max_iteration_wrapup_delivered', {
|
|
1976
|
+
iteration, maxIterations, stepIndex, source: usableWrap ? 'model' : 'deterministic',
|
|
1977
|
+
}, { agentId });
|
|
1978
|
+
} catch (wrapErr) {
|
|
1979
|
+
console.warn(`[Run ${shortenRunId(runId)}] max_iteration_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1980
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1981
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1850
1985
|
if (triggerSource === 'messaging' && !normalizeOutgoingMessage(lastContent, options?.source || null) && !messagingSent) {
|
|
1851
1986
|
// Simplified blank reply recovery: one model call with direct instruction,
|
|
1852
1987
|
// then fall back to a deterministic message. No multi-attempt LLM loop.
|
|
@@ -1877,48 +2012,24 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1877
2012
|
console.warn(`[Run ${shortenRunId(runId)}] blank_reply_recovery failed: ${summarizeForLog(recoverErr?.message || recoverErr, 180)}`);
|
|
1878
2013
|
}
|
|
1879
2014
|
totalTokens += recoveredTokens;
|
|
1880
|
-
|
|
2015
|
+
// The loop has already exited, so we cannot keep working: deliver the model's
|
|
2016
|
+
// own wrap-up (it summarizes what it tried / where it got blocked from the run
|
|
2017
|
+
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2018
|
+
// a deterministic message when the model returned nothing usable.
|
|
2019
|
+
const recoveredVisible = Boolean(normalizeOutgoingMessage(lastContent, options?.source || null));
|
|
2020
|
+
if (!recoveredVisible) {
|
|
1881
2021
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1882
2022
|
}
|
|
1883
2023
|
if (normalizeOutgoingMessage(lastContent, options?.source || null)) {
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
messages,
|
|
1889
|
-
analysis,
|
|
1890
|
-
plan,
|
|
1891
|
-
tools,
|
|
1892
|
-
toolExecutions,
|
|
1893
|
-
lastReply: lastContent,
|
|
1894
|
-
iteration,
|
|
1895
|
-
maxIterations,
|
|
1896
|
-
options: { ...options, triggerSource, runId, userId, agentId },
|
|
1897
|
-
messagingSent: false,
|
|
1898
|
-
});
|
|
1899
|
-
totalTokens += recoveryDecision.usage || 0;
|
|
1900
|
-
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_checked', {
|
|
1901
|
-
status: recoveryDecision.decision.status,
|
|
1902
|
-
reason: recoveryDecision.decision.reason,
|
|
2024
|
+
engine.recordRunEvent(userId, runId, 'blank_reply_recovery_delivered', {
|
|
2025
|
+
source: recoveredVisible ? 'model' : 'deterministic',
|
|
2026
|
+
stepIndex,
|
|
2027
|
+
failedStepCount,
|
|
1903
2028
|
}, { agentId });
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
failedStepCount,
|
|
1909
|
-
}, { agentId });
|
|
1910
|
-
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1911
|
-
messages.push({ role: 'assistant', content: lastContent });
|
|
1912
|
-
if (conversationId) {
|
|
1913
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1914
|
-
.run(conversationId, 'assistant', lastContent, 0);
|
|
1915
|
-
}
|
|
1916
|
-
} else {
|
|
1917
|
-
messages.push({ role: 'assistant', content: lastContent });
|
|
1918
|
-
if (conversationId) {
|
|
1919
|
-
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1920
|
-
.run(conversationId, 'assistant', lastContent, recoveredTokens);
|
|
1921
|
-
}
|
|
2029
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
2030
|
+
if (conversationId) {
|
|
2031
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
2032
|
+
.run(conversationId, 'assistant', lastContent, recoveredVisible ? recoveredTokens : 0);
|
|
1922
2033
|
}
|
|
1923
2034
|
}
|
|
1924
2035
|
}
|
|
@@ -42,6 +42,12 @@ function requireSuccessfulMessagingDelivery(result, label = 'Messaging delivery'
|
|
|
42
42
|
throw error;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Harness-driven progress: the originating chat must see autonomous updates during
|
|
46
|
+
// long runs even when a weak model never calls send_interim_update itself. The
|
|
47
|
+
// supervisor paces this call (FIRST/REPEAT thresholds + a repeat guard). The update
|
|
48
|
+
// text is always authored by the run's own agent loop (composeProgressUpdate); we
|
|
49
|
+
// never emit a hard-coded status line. If the loop can't compose one this tick, we
|
|
50
|
+
// skip the visible send and only nudge the model, then try again next tick.
|
|
45
51
|
async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
46
52
|
const runMeta = engine.getRunMeta(runId);
|
|
47
53
|
if (!runMeta || runMeta.aborted) return { sent: false, skipped: true };
|
|
@@ -54,6 +60,46 @@ async function sendRuntimeMessagingHeartbeat(engine, runId, options = {}) {
|
|
|
54
60
|
engine.updateRunProgress(runId, { heartbeatCount });
|
|
55
61
|
|
|
56
62
|
const ledger = runMeta.progressLedger || {};
|
|
63
|
+
const { platform, chatId } = runMeta.messagingContext || {};
|
|
64
|
+
|
|
65
|
+
if (engine.messagingManager && !runMeta.terminalInterim && platform && chatId
|
|
66
|
+
&& typeof runMeta.composeProgressUpdate === 'function') {
|
|
67
|
+
let statusMsg = '';
|
|
68
|
+
try {
|
|
69
|
+
const composed = await runMeta.composeProgressUpdate({ stalled: options.stalled === true });
|
|
70
|
+
if (normalizeOutgoingMessage(composed, platform)) statusMsg = String(composed).trim();
|
|
71
|
+
} catch { /* no dynamic update available this tick */ }
|
|
72
|
+
if (statusMsg) try {
|
|
73
|
+
const delivery = await engine.messagingManager.sendMessage(runMeta.userId, platform, chatId, statusMsg, {
|
|
74
|
+
runId,
|
|
75
|
+
agentId: runMeta.agentId,
|
|
76
|
+
});
|
|
77
|
+
if (delivery?.success === true && delivery?.suppressed !== true) {
|
|
78
|
+
const nowIso = isoNow();
|
|
79
|
+
runMeta.progressLedger = { ...ledger, lastUserVisibleUpdateAt: nowIso };
|
|
80
|
+
runMeta.lastInterimMessage = statusMsg;
|
|
81
|
+
engine.updateRunProgress(runId, { lastUserVisibleUpdateAt: nowIso });
|
|
82
|
+
engine.recordRunEvent(runMeta.userId, runId, 'progress_heartbeat_sent', {
|
|
83
|
+
stalled: options.stalled === true,
|
|
84
|
+
currentTool: ledger.currentTool || null,
|
|
85
|
+
currentStep: ledger.currentStep || null,
|
|
86
|
+
phase: ledger.currentPhase || 'idle',
|
|
87
|
+
userVisible: true,
|
|
88
|
+
createdAt,
|
|
89
|
+
}, { agentId: runMeta.agentId });
|
|
90
|
+
// Still nudge the model so its next turn can deliver a richer, real update.
|
|
91
|
+
engine.enqueueSystemSteering(
|
|
92
|
+
runId,
|
|
93
|
+
buildProgressNudge({ stalled: options.stalled === true }),
|
|
94
|
+
{ reason: options.stalled === true ? 'stalled_progress_check' : 'progress_check' },
|
|
95
|
+
);
|
|
96
|
+
return { sent: true, heartbeat: true };
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.warn('[Engine] Progress heartbeat send failed:', err?.message || err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
57
103
|
engine.recordRunEvent(runMeta.userId, runId, 'progress_heartbeat_sent', {
|
|
58
104
|
stalled: options.stalled === true,
|
|
59
105
|
currentTool: ledger.currentTool || null,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const FIRST_UPDATE_MS =
|
|
4
|
-
const REPEAT_UPDATE_MS =
|
|
5
|
-
const STALL_MS =
|
|
3
|
+
const FIRST_UPDATE_MS = 35 * 1000;
|
|
4
|
+
const REPEAT_UPDATE_MS = 75 * 1000;
|
|
5
|
+
const STALL_MS = 150 * 1000;
|
|
6
6
|
const TICK_MS = 15 * 1000;
|
|
7
7
|
|
|
8
8
|
function isoNow() {
|
|
@@ -14,15 +14,24 @@
|
|
|
14
14
|
* numbers only fire when something goes wrong.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
// The iteration count is intentionally NOT a real limit: the agent runs until it
|
|
18
|
+
// signals completion (task_complete / judged terminal), an external blocker, or a
|
|
19
|
+
// genuine stuck-state guard fires (consecutive tool failures, repetition guard,
|
|
20
|
+
// model-failure recoveries). This number only exists so arithmetic/logging have a
|
|
21
|
+
// finite sentinel; it is set far above any real task length so it never cuts a run.
|
|
22
|
+
const UNLIMITED_ITERATIONS = 1_000_000;
|
|
23
|
+
|
|
24
|
+
const DEFAULT_MAX_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
25
|
+
const DEFAULT_WIDGET_MAX_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
26
|
+
const DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
27
|
+
// Less aggressive than 0.60 so the model retains file contents it already read for
|
|
28
|
+
// longer, instead of losing them to compaction and re-reading the same files.
|
|
29
|
+
const DEFAULT_COMPACTION_THRESHOLD = 0.80;
|
|
21
30
|
const DEFAULT_MAX_CONSECUTIVE_TOOL_FAILURES = 5;
|
|
22
31
|
const DEFAULT_MAX_MODEL_FAILURE_RECOVERIES = 3;
|
|
23
32
|
|
|
24
|
-
// Hard
|
|
25
|
-
const MAX_ALLOWED_ITERATIONS =
|
|
33
|
+
// Hard ceiling — only guards against absurd/overflow config values, not task length.
|
|
34
|
+
const MAX_ALLOWED_ITERATIONS = UNLIMITED_ITERATIONS;
|
|
26
35
|
const MAX_ALLOWED_TOOL_FAILURES = 50;
|
|
27
36
|
const MAX_ALLOWED_MODEL_RECOVERIES = 10;
|
|
28
37
|
const MAX_ALLOWED_BUDGET_CHARS = 500_000;
|
|
@@ -49,6 +49,25 @@ function buildBlankMessagingReplyPrompt(attempt, platform = null) {
|
|
|
49
49
|
return `Your previous reply was empty. Return one non-empty message now. Do not call tools. If needed, apologize briefly and explain the blocker in one sentence. Use the run evidence already in the conversation instead of asking the user to restate the task. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function buildProgressUpdatePrompt() {
|
|
53
|
+
// Intentionally carries NO voice/formatting rules of its own: this prompt runs with
|
|
54
|
+
// the run's real system prompt as context, so the update inherits the same voice and
|
|
55
|
+
// formatting guidelines as every other message and stays maintainable in one place.
|
|
56
|
+
return [
|
|
57
|
+
'You are mid-task and working autonomously while the user waits.',
|
|
58
|
+
'Send ONE brief progress ping saying what you are doing right now, grounded ONLY in the actual recent tool activity below.',
|
|
59
|
+
'Describe what the evidence literally shows (e.g. reading files, searching the repo, cloning, editing). Do NOT invent or guess steps, and never mention builds, compiling, gradle, tests, training, models, evaluation, pipelines, batch jobs, or deployments unless they actually appear in the activity.',
|
|
60
|
+
'This is not the final answer: do not claim the task is done and do not summarize results.',
|
|
61
|
+
'No greeting, no question, no sign-off; vary the wording from your previous update.',
|
|
62
|
+
'Follow your normal voice and formatting rules. Output only the message text.',
|
|
63
|
+
].join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildMaxIterationWrapupPrompt(platform = null) {
|
|
67
|
+
const formattingGuide = buildPlatformFormattingGuide(platform);
|
|
68
|
+
return `You have reached the step limit for this run, so this is your final turn. Stop here and do NOT call any tools. Write the single best, most complete answer you can for the user from the work already done in this conversation: lead with the concrete results and what you accomplished, then clearly name anything you could not finish and the specific blocker. Do not output a half-finished thought, a plan for what to do next, or a "let me…" fragment, this message is the final reply. Do not promise future work unless it already happened in this run.\n\n${formattingGuide}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
function parseToolExecutionSummary(item) {
|
|
53
72
|
if (!item?.summary) return null;
|
|
54
73
|
try {
|
|
@@ -216,6 +235,8 @@ module.exports = {
|
|
|
216
235
|
joinSentMessages,
|
|
217
236
|
normalizeInterimText,
|
|
218
237
|
buildBlankMessagingReplyPrompt,
|
|
238
|
+
buildMaxIterationWrapupPrompt,
|
|
239
|
+
buildProgressUpdatePrompt,
|
|
219
240
|
parseToolExecutionSummary,
|
|
220
241
|
toolWorkDescription,
|
|
221
242
|
summarizeRecentWork,
|
|
@@ -82,6 +82,7 @@ const VERIFIER_PROMPT_INSTRUCTIONS = [
|
|
|
82
82
|
const EXECUTION_GUIDANCE_ACTION_LINES = [
|
|
83
83
|
'Act end-to-end. Run independent searches or inspections in parallel when possible. Prefer native integration tools and structured APIs over browser automation or shell scraping. Use exact IDs and required parameters; list or search first when you do not have them.',
|
|
84
84
|
'For GitHub issue implementation or PR work, fetch the issue once, then establish or reuse a writable local checkout, create a task branch, inspect/edit/test locally, and push/open the PR. Use direct GitHub file mutation tools only as a fallback when a local checkout is unavailable.',
|
|
85
|
+
'Your shell (execute_command) starts in your workspace, and the file tools (read_file, write_file, list_directory, search_files) operate on that same workspace. Clone or create files into the current directory and then inspect or edit them with either the shell or the file tools interchangeably. Clone a repo once and reuse it; do not re-clone or re-list the same tree.',
|
|
85
86
|
'Tool results are already present in the conversation as tool output. Do not assume a tool result was persisted to a readable /tmp path unless that exact path was explicitly returned by the tool result.',
|
|
86
87
|
'Use send_interim_update sparingly when a short real update or question would help.',
|
|
87
88
|
'When you must ask for missing required user input, ask once, then wait for the reply instead of re-asking in the same run.',
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spawnSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
4
6
|
const http = require('http');
|
|
5
7
|
const net = require('net');
|
|
8
|
+
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
9
|
+
const { sanitizeWorkspaceKey } = require('../workspace/manager');
|
|
6
10
|
|
|
7
11
|
const CONTAINER_IMAGE = 'mcr.microsoft.com/playwright:v1.44.0-focal';
|
|
8
12
|
const CONTAINER_LABEL = 'neoagent.managed=1';
|
|
13
|
+
// The per-user host workspace is bind-mounted here so the shell (execute_command)
|
|
14
|
+
// and the workspace file tools (read_file/write_file/list_directory/search_files)
|
|
15
|
+
// operate on the SAME files. The guest agent defaults its shell cwd to this path.
|
|
16
|
+
const GUEST_WORKSPACE = '/workspace';
|
|
17
|
+
|
|
18
|
+
// Host path of a user's workspace — must match WorkspaceManager's layout exactly
|
|
19
|
+
// (AGENT_DATA_DIR/workspaces/<sanitized key>) so both sides see one directory.
|
|
20
|
+
function hostWorkspaceDir(key) {
|
|
21
|
+
return path.join(AGENT_DATA_DIR, 'workspaces', sanitizeWorkspaceKey(key));
|
|
22
|
+
}
|
|
9
23
|
|
|
10
24
|
// ─── Guest agent ─────────────────────────────────────────────────────────────
|
|
11
25
|
// Injected into every container. Pure Node.js — only built-in modules + playwright
|
|
@@ -104,7 +118,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
104
118
|
if (req.method === 'POST' && url === '/exec') {
|
|
105
119
|
const timeoutMs = Math.min(Number(b.timeout) || 15 * 60 * 1000, 20 * 60 * 1000);
|
|
106
120
|
const child = spawn('sh', ['-c', b.command || 'true'], {
|
|
107
|
-
cwd: b.cwd || '/
|
|
121
|
+
cwd: b.cwd || '/workspace',
|
|
108
122
|
env: { ...process.env, ...b.env },
|
|
109
123
|
});
|
|
110
124
|
const pid = child.pid;
|
|
@@ -325,6 +339,10 @@ class DockerVMManager {
|
|
|
325
339
|
const port = await findAvailablePort();
|
|
326
340
|
console.log(`[DockerVM:${this.profile}] Starting container for user ${key} on port ${port}`);
|
|
327
341
|
|
|
342
|
+
// Bind-mount the user's host workspace so shell and file tools share one place.
|
|
343
|
+
const workspaceDir = hostWorkspaceDir(key);
|
|
344
|
+
try { fs.mkdirSync(workspaceDir, { recursive: true }); } catch { /* best effort */ }
|
|
345
|
+
|
|
328
346
|
const containerId = docker([
|
|
329
347
|
'run', '-d',
|
|
330
348
|
'--memory', `${this.memoryMb}m`,
|
|
@@ -334,6 +352,8 @@ class DockerVMManager {
|
|
|
334
352
|
...(this.guestToken ? ['-e', `NEOAGENT_VM_GUEST_TOKEN=${this.guestToken}`] : []),
|
|
335
353
|
'--shm-size=2g',
|
|
336
354
|
'--security-opt', 'no-new-privileges',
|
|
355
|
+
'-v', `${workspaceDir}:${GUEST_WORKSPACE}`,
|
|
356
|
+
'-w', GUEST_WORKSPACE,
|
|
337
357
|
'--label', CONTAINER_LABEL,
|
|
338
358
|
'--label', `neoagent.profile=${this.profile}`,
|
|
339
359
|
'--label', `neoagent.user=${key}`,
|