neoagent 3.0.1-beta.5 → 3.0.1-beta.6
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/agent_engine_core.js +42 -0
- package/server/services/ai/loop/completion_judge.js +49 -0
- package/server/services/ai/loop/conversation_loop.js +131 -64
- package/server/services/ai/loopPolicy.js +18 -1
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
504d057957ef960afb5bd462407c2c9f
|
|
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"77e2e94772b6eb43759e34ed1ad7da4674e19c
|
|
|
37
37
|
|
|
38
38
|
_flutter.loader.load({
|
|
39
39
|
serviceWorkerSettings: {
|
|
40
|
-
serviceWorkerVersion: "
|
|
40
|
+
serviceWorkerVersion: "4074570140" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
|
|
41
41
|
}
|
|
42
42
|
});
|
|
@@ -135597,7 +135597,7 @@ r===$&&A.b()
|
|
|
135597
135597
|
p.push(A.jV(q,A.jd(!1,new A.a_(B.uM,A.db(new A.cC(B.jA,new A.a89(r,q),q),q,q),q),!1,B.I,!0),q,q,0,0,0,q))}r=!1
|
|
135598
135598
|
if(!s.ay)if(!s.ch){r=s.e
|
|
135599
135599
|
r===$&&A.b()
|
|
135600
|
-
r=B.b.u("
|
|
135600
|
+
r=B.b.u("mqin9jwm-c5e3286").length!==0&&r.b}if(r){r=s.d
|
|
135601
135601
|
r===$&&A.b()
|
|
135602
135602
|
r=r.aP&&!r.ai?84:0
|
|
135603
135603
|
s=s.e
|
|
@@ -141319,7 +141319,7 @@ $S:0}
|
|
|
141319
141319
|
A.a_p.prototype={}
|
|
141320
141320
|
A.T7.prototype={
|
|
141321
141321
|
nd(a){var s=this
|
|
141322
|
-
if(B.b.u("
|
|
141322
|
+
if(B.b.u("mqin9jwm-c5e3286").length===0||s.a!=null)return
|
|
141323
141323
|
s.B_()
|
|
141324
141324
|
s.a=A.ot(B.S2,new A.bde(s))},
|
|
141325
141325
|
B_(){var s=0,r=A.l(t.H),q,p=2,o=[],n=this,m,l,k,j,i,h,g,f
|
|
@@ -141337,7 +141337,7 @@ if(!t.f.b(k)){s=1
|
|
|
141337
141337
|
break}i=J.a3(k,"buildId")
|
|
141338
141338
|
h=i==null?null:B.b.u(J.p(i))
|
|
141339
141339
|
j=h==null?"":h
|
|
141340
|
-
if(J.bj(j)===0||J.e(j,"
|
|
141340
|
+
if(J.bj(j)===0||J.e(j,"mqin9jwm-c5e3286")){s=1
|
|
141341
141341
|
break}n.b=!0
|
|
141342
141342
|
n.F()
|
|
141343
141343
|
p=2
|
|
@@ -141354,7 +141354,7 @@ case 2:return A.i(o.at(-1),r)}})
|
|
|
141354
141354
|
return A.k($async$B_,r)},
|
|
141355
141355
|
vI(){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
|
|
141356
141356
|
var $async$vI=A.h(function(a2,a3){if(a2===1){o.push(a3)
|
|
141357
|
-
s=p}for(;;)switch(s){case 0:if(B.b.u("
|
|
141357
|
+
s=p}for(;;)switch(s){case 0:if(B.b.u("mqin9jwm-c5e3286").length===0||n.c){s=1
|
|
141358
141358
|
break}n.c=!0
|
|
141359
141359
|
n.F()
|
|
141360
141360
|
p=4
|
|
@@ -21,7 +21,9 @@ const { shouldAcceptTaskComplete } = require('../completion');
|
|
|
21
21
|
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
22
22
|
const { runConversation } = require('./conversation_loop');
|
|
23
23
|
const {
|
|
24
|
+
buildChurnAssessmentPrompt,
|
|
24
25
|
buildCompletionDecisionPrompt,
|
|
26
|
+
normalizeChurnAssessment,
|
|
25
27
|
normalizeCompletionDecision,
|
|
26
28
|
resolveRunGoalContext,
|
|
27
29
|
} = require('./completion_judge');
|
|
@@ -812,6 +814,46 @@ class AgentEngine {
|
|
|
812
814
|
};
|
|
813
815
|
}
|
|
814
816
|
|
|
817
|
+
async assessChurnState({
|
|
818
|
+
provider,
|
|
819
|
+
providerName,
|
|
820
|
+
model,
|
|
821
|
+
messages,
|
|
822
|
+
analysis,
|
|
823
|
+
plan,
|
|
824
|
+
toolExecutions,
|
|
825
|
+
readOnlyCount,
|
|
826
|
+
alreadyRead,
|
|
827
|
+
iteration,
|
|
828
|
+
options,
|
|
829
|
+
}) {
|
|
830
|
+
const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
|
|
831
|
+
const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
|
|
832
|
+
const response = await this.requestStructuredJson({
|
|
833
|
+
provider,
|
|
834
|
+
providerName,
|
|
835
|
+
model,
|
|
836
|
+
messages,
|
|
837
|
+
prompt: buildChurnAssessmentPrompt({
|
|
838
|
+
readOnlyCount,
|
|
839
|
+
alreadyRead,
|
|
840
|
+
goalContext,
|
|
841
|
+
toolExecutions,
|
|
842
|
+
iteration,
|
|
843
|
+
}),
|
|
844
|
+
maxTokens: 200,
|
|
845
|
+
normalize: (raw) => normalizeChurnAssessment(raw),
|
|
846
|
+
fallback: { assessment: 'churn', reason: 'churn assessment unavailable' },
|
|
847
|
+
reasoningEffort: this.getReasoningEffort(providerName, options),
|
|
848
|
+
telemetry: options,
|
|
849
|
+
phase: 'churn_assessment',
|
|
850
|
+
});
|
|
851
|
+
return {
|
|
852
|
+
assessment: response.value,
|
|
853
|
+
usage: response.usage,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
815
857
|
async refreshConversationState({
|
|
816
858
|
conversationId,
|
|
817
859
|
runId,
|
|
@@ -241,12 +241,61 @@ function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
|
|
|
241
241
|
};
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Ask the model to self-assess whether it is making genuine progress or
|
|
246
|
+
* spinning in analysis-paralysis. This replaces the hardcoded nudge threshold
|
|
247
|
+
* with an AI-controlled signal: the model knows its own plan better than any
|
|
248
|
+
* regex over tool names can infer.
|
|
249
|
+
*
|
|
250
|
+
* Intentionally lightweight — the response is capped at 200 tokens and the
|
|
251
|
+
* prompt is self-contained so the model can answer without re-reading history.
|
|
252
|
+
*/
|
|
253
|
+
function buildChurnAssessmentPrompt({
|
|
254
|
+
readOnlyCount,
|
|
255
|
+
alreadyRead,
|
|
256
|
+
goalContext,
|
|
257
|
+
toolExecutions,
|
|
258
|
+
iteration,
|
|
259
|
+
}) {
|
|
260
|
+
const lines = [
|
|
261
|
+
'Return JSON only.',
|
|
262
|
+
'Self-assess your current loop state — are you making genuine progress or spinning?',
|
|
263
|
+
'Schema: {"assessment":"progressing|churn|blocked","reason":"one short concrete sentence"}',
|
|
264
|
+
'',
|
|
265
|
+
`Context: ${readOnlyCount} consecutive iteration(s) with only read/search/inspect operations — no concrete state changes yet.`,
|
|
266
|
+
alreadyRead ? `Already inspected: ${alreadyRead}.` : '',
|
|
267
|
+
goalContext.effectiveGoal ? `Goal: ${goalContext.effectiveGoal}` : '',
|
|
268
|
+
goalContext.successCriteria.length > 0
|
|
269
|
+
? `Success criteria:\n${goalContext.successCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}`
|
|
270
|
+
: '',
|
|
271
|
+
`Iteration: ${iteration}`,
|
|
272
|
+
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 6) || 'none'}`,
|
|
273
|
+
'',
|
|
274
|
+
'Assessment rules:',
|
|
275
|
+
'- "progressing": You are systematically gathering necessary context and the next concrete action is already determined — you know exactly what to do next.',
|
|
276
|
+
'- "churn": You are re-reading/re-searching information already in context, or exploring without a clear next concrete step. Accept the nudge and act.',
|
|
277
|
+
'- "blocked": No concrete action is available in this run. You have all the evidence needed to deliver a truthful final answer or a specific external blocker.',
|
|
278
|
+
];
|
|
279
|
+
return lines.filter(Boolean).join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeChurnAssessment(raw) {
|
|
283
|
+
const allowed = new Set(['progressing', 'churn', 'blocked']);
|
|
284
|
+
const assessment = String(raw?.assessment || '').trim().toLowerCase();
|
|
285
|
+
return {
|
|
286
|
+
assessment: allowed.has(assessment) ? assessment : 'churn',
|
|
287
|
+
reason: String(raw?.reason || '').trim().slice(0, 300),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
244
291
|
module.exports = {
|
|
292
|
+
buildChurnAssessmentPrompt,
|
|
245
293
|
buildCompletionDecisionPrompt,
|
|
246
294
|
buildGoalContractPrompt,
|
|
247
295
|
goalContractFromAnalysis,
|
|
248
296
|
goalContractFromPlan,
|
|
249
297
|
mergeGoalContracts,
|
|
298
|
+
normalizeChurnAssessment,
|
|
250
299
|
normalizeCompletionDecision,
|
|
251
300
|
normalizeGoalContract,
|
|
252
301
|
resolveRunGoalContext,
|
|
@@ -49,7 +49,7 @@ const {
|
|
|
49
49
|
selectDeliverableWorkflow,
|
|
50
50
|
validateDeliverableExecution,
|
|
51
51
|
} = require('../deliverables');
|
|
52
|
-
const { buildLoopPolicy, resolveToolResultLimits } = require('../loopPolicy');
|
|
52
|
+
const { buildLoopPolicy, resolveToolResultLimits, resolveChurnNudgeThreshold } = require('../loopPolicy');
|
|
53
53
|
const { globalHooks } = require('../hooks');
|
|
54
54
|
const { normalizeCompletionConfidence, shouldAcceptTaskComplete } = require('../completion');
|
|
55
55
|
const { enforceRateLimits } = require('../rate_limits');
|
|
@@ -1180,73 +1180,140 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1180
1180
|
messages = steeringAtLoopStart.messages;
|
|
1181
1181
|
messages = sanitizeConversationMessages(messages);
|
|
1182
1182
|
|
|
1183
|
-
// Analysis-paralysis gate:
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1183
|
+
// Analysis-paralysis gate (EVE-style: AI self-assessment instead of
|
|
1184
|
+
// hardcoded counters). The nudge threshold is derived from the goal
|
|
1185
|
+
// contract complexity/autonomy_level that the model set during task
|
|
1186
|
+
// analysis, making the loop budget sensitivity AI-controlled.
|
|
1187
|
+
//
|
|
1188
|
+
// Flow:
|
|
1189
|
+
// readOnlyCount >= maxConsecutiveReadOnlyIterations → hard safety net,
|
|
1190
|
+
// force wrap-up unconditionally (same as before).
|
|
1191
|
+
// readOnlyCount >= churnNudgeThreshold → ask the model to self-assess:
|
|
1192
|
+
// "progressing" → give grace (partial counter reset, loop continues).
|
|
1193
|
+
// "churn" → inject the nudge so the model can course-correct.
|
|
1194
|
+
// "blocked" → trigger the force wrap-up early (AI-authorised).
|
|
1186
1195
|
if (analysis.mode === 'execute' || analysis.mode === 'plan_execute') {
|
|
1187
1196
|
const readOnlyCount = engine.getRunMeta(runId)?.consecutiveReadOnlyIterations || 0;
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1197
|
+
|
|
1198
|
+
if (readOnlyCount > 0) {
|
|
1199
|
+
const runGoalCtx = resolveRunGoalContext(engine.getRunMeta(runId), analysis, plan);
|
|
1200
|
+
const churnNudgeThreshold = resolveChurnNudgeThreshold(runGoalCtx.goalContract);
|
|
1201
|
+
|
|
1202
|
+
let triggerForceWrapup = false;
|
|
1203
|
+
let forceWrapupSource = 'hard_limit';
|
|
1204
|
+
// Compute alreadyRead lazily — only needed at or above the nudge threshold.
|
|
1205
|
+
let alreadyRead = '';
|
|
1206
|
+
|
|
1207
|
+
if (readOnlyCount >= loopPolicy.maxConsecutiveReadOnlyIterations) {
|
|
1208
|
+
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1209
|
+
triggerForceWrapup = true;
|
|
1210
|
+
} else if (readOnlyCount >= churnNudgeThreshold) {
|
|
1211
|
+
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1212
|
+
let churnResult;
|
|
1213
|
+
try {
|
|
1214
|
+
churnResult = await engine.assessChurnState({
|
|
1215
|
+
provider,
|
|
1216
|
+
providerName,
|
|
1217
|
+
model,
|
|
1218
|
+
messages,
|
|
1219
|
+
analysis,
|
|
1220
|
+
plan,
|
|
1221
|
+
toolExecutions,
|
|
1222
|
+
readOnlyCount,
|
|
1223
|
+
alreadyRead,
|
|
1224
|
+
iteration,
|
|
1225
|
+
options: { ...options, triggerSource, runId, userId, agentId },
|
|
1226
|
+
});
|
|
1227
|
+
} catch (churnErr) {
|
|
1228
|
+
console.warn(`[Run ${shortenRunId(runId)}] churn_assessment failed: ${summarizeForLog(churnErr?.message || churnErr, 120)}`);
|
|
1229
|
+
churnResult = { assessment: { assessment: 'churn', reason: '' }, usage: 0 };
|
|
1230
|
+
}
|
|
1231
|
+
totalTokens += churnResult.usage || 0;
|
|
1232
|
+
engine.recordRunEvent(userId, runId, 'churn_assessment', {
|
|
1233
|
+
assessment: churnResult.assessment.assessment,
|
|
1234
|
+
reason: churnResult.assessment.reason,
|
|
1235
|
+
readOnlyCount,
|
|
1236
|
+
churnNudgeThreshold,
|
|
1237
|
+
iteration,
|
|
1238
|
+
}, { agentId });
|
|
1239
|
+
|
|
1240
|
+
const churnVerdict = churnResult.assessment.assessment;
|
|
1241
|
+
if (churnVerdict === 'blocked') {
|
|
1242
|
+
triggerForceWrapup = true;
|
|
1243
|
+
forceWrapupSource = 'ai_blocked';
|
|
1244
|
+
} else if (churnVerdict === 'progressing') {
|
|
1245
|
+
// Model is genuinely on track — partially reset so it gets
|
|
1246
|
+
// re-assessed after one more read-only turn rather than immediately.
|
|
1247
|
+
const iterMeta = engine.getRunMeta(runId);
|
|
1248
|
+
if (iterMeta) {
|
|
1249
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, churnNudgeThreshold - 1);
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
// 'churn' — model acknowledges it is spinning; inject the nudge
|
|
1253
|
+
// so it can course-correct in the next iteration.
|
|
1254
|
+
messages.push({
|
|
1255
|
+
role: 'system',
|
|
1256
|
+
content: buildReadOnlyChurnGuidance({ readOnlyCount, alreadyRead }),
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1236
1259
|
}
|
|
1237
|
-
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1260
|
+
|
|
1261
|
+
if (triggerForceWrapup) {
|
|
1262
|
+
console.warn(
|
|
1263
|
+
`[Run ${shortenRunId(runId)}] no_progress_wrapup source=${forceWrapupSource} readOnlyCount=${readOnlyCount}`
|
|
1264
|
+
);
|
|
1265
|
+
engine.updateRunProgress(runId, {
|
|
1266
|
+
currentPhase: 'model',
|
|
1267
|
+
currentStep: 'model:no_progress_wrapup',
|
|
1268
|
+
currentTool: null,
|
|
1269
|
+
currentStepStartedAt: isoNow(),
|
|
1270
|
+
});
|
|
1271
|
+
let wrapTokens = 0;
|
|
1272
|
+
try {
|
|
1273
|
+
const wrapResponse = await withModelCallTimeout(
|
|
1274
|
+
provider.chat(
|
|
1275
|
+
sanitizeConversationMessages([
|
|
1276
|
+
...messages,
|
|
1277
|
+
{
|
|
1278
|
+
role: 'system',
|
|
1279
|
+
content: buildNoProgressWrapupPrompt({
|
|
1280
|
+
readOnlyCount,
|
|
1281
|
+
alreadyRead,
|
|
1282
|
+
platform: options?.source || null,
|
|
1283
|
+
}),
|
|
1284
|
+
},
|
|
1285
|
+
]),
|
|
1286
|
+
[],
|
|
1287
|
+
{ model, reasoningEffort: engine.getReasoningEffort(providerName, options) },
|
|
1288
|
+
),
|
|
1289
|
+
options,
|
|
1290
|
+
'No-progress wrap-up',
|
|
1291
|
+
);
|
|
1292
|
+
wrapTokens = wrapResponse.usage?.totalTokens || 0;
|
|
1293
|
+
lastContent = sanitizeModelOutput(wrapResponse.content || '', { model });
|
|
1294
|
+
} catch (wrapErr) {
|
|
1295
|
+
console.warn(`[Run ${shortenRunId(runId)}] no_progress_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1296
|
+
}
|
|
1297
|
+
totalTokens += wrapTokens;
|
|
1298
|
+
const usableWrap = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
1299
|
+
if (!usableWrap) {
|
|
1300
|
+
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1301
|
+
}
|
|
1302
|
+
messages.push({ role: 'assistant', content: lastContent });
|
|
1303
|
+
if (conversationId) {
|
|
1304
|
+
db.prepare('INSERT INTO conversation_messages (conversation_id, role, content, tokens) VALUES (?, ?, ?, ?)')
|
|
1305
|
+
.run(conversationId, 'assistant', lastContent, usableWrap ? wrapTokens : 0);
|
|
1306
|
+
}
|
|
1307
|
+
engine.recordRunEvent(userId, runId, 'no_progress_wrapup_delivered', {
|
|
1308
|
+
iteration,
|
|
1309
|
+
readOnlyCount,
|
|
1310
|
+
source: usableWrap ? 'model' : 'deterministic',
|
|
1311
|
+
forceWrapupSource,
|
|
1312
|
+
stepIndex,
|
|
1313
|
+
}, { agentId });
|
|
1314
|
+
directAnswerEligible = true;
|
|
1315
|
+
break;
|
|
1241
1316
|
}
|
|
1242
|
-
engine.recordRunEvent(userId, runId, 'no_progress_wrapup_delivered', {
|
|
1243
|
-
iteration,
|
|
1244
|
-
readOnlyCount,
|
|
1245
|
-
source: usableWrap ? 'model' : 'deterministic',
|
|
1246
|
-
stepIndex,
|
|
1247
|
-
}, { agentId });
|
|
1248
|
-
directAnswerEligible = true;
|
|
1249
|
-
break;
|
|
1250
1317
|
}
|
|
1251
1318
|
}
|
|
1252
1319
|
|
|
@@ -176,4 +176,21 @@ function resolveToolResultLimits(toolName, policy) {
|
|
|
176
176
|
return { softLimit: soft, hardLimit: hard };
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Resolve the read-only churn nudge threshold from the current goal contract.
|
|
181
|
+
* The AI itself sets complexity and autonomy_level during task analysis, so
|
|
182
|
+
* this threshold is indirectly AI-controlled rather than a hardcoded constant.
|
|
183
|
+
*
|
|
184
|
+
* Simple tasks get nudged sooner (2 read-only turns) because exploration is
|
|
185
|
+
* rarely needed. Complex/high-autonomy tasks get more latitude (5 turns) before
|
|
186
|
+
* the churn self-assessment fires.
|
|
187
|
+
*/
|
|
188
|
+
function resolveChurnNudgeThreshold(goalContract) {
|
|
189
|
+
const complexity = String(goalContract?.complexity || 'standard').toLowerCase();
|
|
190
|
+
const autonomyLevel = String(goalContract?.autonomyLevel || goalContract?.autonomy_level || 'normal').toLowerCase();
|
|
191
|
+
if (complexity === 'simple') return 2;
|
|
192
|
+
if (complexity === 'complex' || autonomyLevel === 'high') return 5;
|
|
193
|
+
return 3;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = { buildLoopPolicy, getToolCategory, resolveToolResultLimits, resolveChurnNudgeThreshold };
|