neoagent 3.2.1-beta.9 → 3.3.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/flutter_app/lib/main_app_shell.dart +178 -34
- package/flutter_app/lib/main_chat.dart +542 -80
- package/flutter_app/lib/main_controller.dart +59 -4
- package/flutter_app/lib/main_models.dart +171 -22
- package/flutter_app/lib/main_operations.dart +0 -49
- package/flutter_app/lib/main_settings.dart +338 -0
- package/flutter_app/lib/src/backend_client.dart +19 -0
- package/package.json +1 -1
- package/server/db/database.js +2 -2
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +85079 -83904
- package/server/routes/behavior.js +80 -0
- package/server/routes/settings.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/history.js +1 -1
- package/server/services/ai/loop/agent_engine_core.js +110 -17
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/completion_judge.js +226 -33
- package/server/services/ai/loop/conversation_loop.js +92 -34
- package/server/services/ai/loop/messaging_delivery.js +47 -0
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loopPolicy.js +24 -2
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_failure_cache.js +7 -0
- package/server/services/ai/systemPrompt.js +31 -122
- package/server/services/ai/taskAnalysis.js +86 -12
- package/server/services/ai/toolEvidence.js +68 -224
- package/server/services/ai/tools.js +60 -19
- package/server/services/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +172 -0
- package/server/services/behavior/modules/persona_prompt.js +238 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +118 -0
- package/server/services/behavior/modules/turn_taking.js +238 -0
- package/server/services/behavior/pipeline.js +341 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/memory/manager.js +14 -33
- package/server/services/messaging/access_policy.js +269 -74
- package/server/services/messaging/automation.js +158 -27
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +73 -28
- package/server/services/messaging/inbound_queue.js +74 -13
- package/server/services/messaging/inbound_store.js +33 -0
- package/server/services/messaging/manager.js +57 -5
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/whatsapp.js +11 -0
- package/server/services/social_reach/channels/social_video.js +1 -1
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +194 -29
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/runtime.js +2 -2
- package/server/utils/logger.js +19 -0
- package/server/services/ai/terminal_reply.js +0 -57
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
const { normalizeCompletionConfidence } = require('../completion');
|
|
4
4
|
const { normalizeOutgoingMessage } = require('../messagingFallback');
|
|
5
|
-
const {
|
|
6
|
-
isDeferredWorkReply,
|
|
7
|
-
isTerminalQuestionOrBlockerReply,
|
|
8
|
-
} = require('../terminal_reply');
|
|
9
5
|
const {
|
|
10
6
|
assessResearchAdequacy,
|
|
11
7
|
formatResearchAdequacyGuidance,
|
|
12
8
|
summarizeAvailableTools,
|
|
9
|
+
summarizeResearchEvidenceCatalog,
|
|
13
10
|
summarizeToolExecutions,
|
|
14
11
|
} = require('../toolEvidence');
|
|
15
12
|
|
|
@@ -212,7 +209,7 @@ function buildCompletionDecisionPrompt({
|
|
|
212
209
|
const lines = [
|
|
213
210
|
'Return JSON only.',
|
|
214
211
|
'Decide whether this run should continue autonomously or stop now.',
|
|
215
|
-
'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason"}',
|
|
212
|
+
'Schema: {"status":"continue|complete|blocked","reason":"short concrete reason","blocker_review":{"kind":"user_input|permission|external_dependency|unavailable_capability|none","resolvable_in_run":true,"required_value":"specific missing dependency"},"research_review":{"required":true,"adequate":false,"overall_support":"primary|secondary|context|none","evidence_indexes":[1],"target_coverage":[{"target":"exact requested target","support":"primary|secondary|context|none","evidence_indexes":[1]}],"missing_targets":["exact missing target"]}}',
|
|
216
213
|
'Rules:',
|
|
217
214
|
'- Use "continue" whenever any safe next step remains in this same run.',
|
|
218
215
|
'- Use "complete" when the requested outcome is achieved, already done, or a no-op because there is nothing matching the request to change, and the latest draft is the finished user-facing answer.',
|
|
@@ -223,18 +220,26 @@ function buildCompletionDecisionPrompt({
|
|
|
223
220
|
'- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
|
|
224
221
|
'- Repeated read-only inspection that has already established the relevant object is absent or unchanged is not progress. Accept a concise complete/blocker reply instead of requiring more searching.',
|
|
225
222
|
`- If completion_confidence_required is ${goalContext.effectiveCompletionConfidence} and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.`,
|
|
226
|
-
'- When research intensity is light or deep,
|
|
223
|
+
'- When research intensity is light or deep, fill research_review from the numbered evidence entries. Do not mark complete unless every requested target has concrete support.',
|
|
227
224
|
'- Search snippets, memory, and model priors are leads, not completion evidence. Prefer opened/fetched primary sources before "complete".',
|
|
225
|
+
'- Judge source quality semantically from the actual evidence summary. Tool names, call counts, local notes, and target words in tool arguments do not by themselves make evidence primary or prove a claim.',
|
|
226
|
+
'- evidence_indexes are the numeric parts of E identifiers from the Research evidence catalog. Never cite an entry that does not support the target.',
|
|
227
|
+
'- A real external blocker or genuinely required user input may be "blocked" even when research is incomplete. Name the missing dependency precisely; do not pretend more autonomous research can resolve user-only information.',
|
|
228
|
+
'- For "blocked", blocker_review.kind must identify the external dependency, resolvable_in_run must be false, and required_value must state what is missing. Otherwise choose "continue".',
|
|
229
|
+
'- If the latest draft invents entities, products, people, files, results, or actions that are not supported by tool evidence, use "continue" so the run can gather evidence or rewrite into a truthful partial/blocker answer.',
|
|
230
|
+
'- A polished-sounding answer is not complete if key requested targets still lack direct evidence.',
|
|
231
|
+
'- If the latest draft only announces unfinished work, promises a future update, or asks the user to wait without a concrete result or blocker, use "continue" so the run keeps acting.',
|
|
232
|
+
'- If the latest draft asks for missing required user input, confirmation, or a choice needed to proceed, use "blocked" so the run waits instead of repeating the same ask.',
|
|
228
233
|
];
|
|
229
234
|
|
|
230
235
|
if (adequacy.intensity !== 'none') {
|
|
231
236
|
lines.push(
|
|
232
|
-
`- Research intensity for this run is ${adequacy.intensity}.
|
|
237
|
+
`- Research intensity for this run is ${adequacy.intensity}. Successful evidence candidates=${adequacy.evidenceCandidateCount}; unique calls=${adequacy.uniqueEvidenceCandidateCount}. Semantic target coverage has not been precomputed.`,
|
|
233
238
|
);
|
|
234
239
|
}
|
|
235
|
-
if (adequacy.
|
|
240
|
+
if (adequacy.structurallyReady === false) {
|
|
236
241
|
lines.push(
|
|
237
|
-
`-
|
|
242
|
+
`- No source evidence is ready for review (${adequacy.reason}). Use "continue" unless the latest draft is a real external blocker.`,
|
|
238
243
|
);
|
|
239
244
|
}
|
|
240
245
|
|
|
@@ -255,61 +260,212 @@ function buildCompletionDecisionPrompt({
|
|
|
255
260
|
: '',
|
|
256
261
|
`Current iteration: ${iteration} of ${maxIterations}.`,
|
|
257
262
|
`Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
|
|
258
|
-
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions,
|
|
263
|
+
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 12) || 'none'}`,
|
|
264
|
+
adequacy.intensity !== 'none'
|
|
265
|
+
? `Research evidence catalog:\n${summarizeResearchEvidenceCatalog(toolExecutions) || 'none'}`
|
|
266
|
+
: '',
|
|
259
267
|
adequacy.intensity !== 'none'
|
|
260
|
-
? `Research
|
|
268
|
+
? `Research preflight: intensity=${adequacy.intensity}; structurally_ready=${adequacy.structurallyReady}; semantic_review_required=true; reason=${adequacy.reason}`
|
|
261
269
|
: '',
|
|
262
270
|
adequacy.targets.length
|
|
263
271
|
? `Research targets: ${adequacy.targets.join('; ')}`
|
|
264
272
|
: '',
|
|
265
|
-
adequacy.uncoveredTargets.length
|
|
266
|
-
? `Uncovered research targets: ${adequacy.uncoveredTargets.join('; ')}`
|
|
267
|
-
: '',
|
|
268
273
|
formatResearchAdequacyGuidance(adequacy),
|
|
269
274
|
`Latest draft reply:\n${draftReply || '(empty)'}`,
|
|
270
275
|
);
|
|
271
276
|
return lines.filter(Boolean).join('\n');
|
|
272
277
|
}
|
|
273
278
|
|
|
279
|
+
function normalizeEvidenceIndexes(value) {
|
|
280
|
+
if (!Array.isArray(value)) return [];
|
|
281
|
+
return [...new Set(value
|
|
282
|
+
.map((entry) => Number(entry))
|
|
283
|
+
.filter((entry) => Number.isInteger(entry) && entry > 0))]
|
|
284
|
+
.slice(0, 20);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function normalizeResearchReview(raw = null) {
|
|
288
|
+
const review = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
289
|
+
const targetCoverage = Array.isArray(review.target_coverage || review.targetCoverage)
|
|
290
|
+
? (review.target_coverage || review.targetCoverage)
|
|
291
|
+
.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry))
|
|
292
|
+
.map((entry) => ({
|
|
293
|
+
target: String(entry.target || '').trim(),
|
|
294
|
+
support: ['primary', 'secondary', 'context', 'none'].includes(
|
|
295
|
+
String(entry.support || '').trim().toLowerCase(),
|
|
296
|
+
)
|
|
297
|
+
? String(entry.support).trim().toLowerCase()
|
|
298
|
+
: 'none',
|
|
299
|
+
evidence_indexes: normalizeEvidenceIndexes(
|
|
300
|
+
entry.evidence_indexes || entry.evidenceIndexes,
|
|
301
|
+
),
|
|
302
|
+
}))
|
|
303
|
+
.filter((entry) => entry.target)
|
|
304
|
+
.slice(0, 12)
|
|
305
|
+
: [];
|
|
306
|
+
return {
|
|
307
|
+
required: review.required === true,
|
|
308
|
+
adequate: review.adequate === true,
|
|
309
|
+
overall_support: ['primary', 'secondary', 'context', 'none'].includes(
|
|
310
|
+
String(review.overall_support || review.overallSupport || '').trim().toLowerCase(),
|
|
311
|
+
)
|
|
312
|
+
? String(review.overall_support || review.overallSupport).trim().toLowerCase()
|
|
313
|
+
: 'none',
|
|
314
|
+
evidence_indexes: normalizeEvidenceIndexes(
|
|
315
|
+
review.evidence_indexes || review.evidenceIndexes,
|
|
316
|
+
),
|
|
317
|
+
target_coverage: targetCoverage,
|
|
318
|
+
missing_targets: normalizeGoalCriteria(
|
|
319
|
+
review.missing_targets || review.missingTargets || [],
|
|
320
|
+
),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
274
324
|
function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
|
|
275
325
|
const allowed = new Set(['continue', 'complete', 'blocked']);
|
|
276
326
|
const requestedStatus = String(raw.status || '').trim().toLowerCase();
|
|
327
|
+
const rawBlockerReview = raw.blocker_review || raw.blockerReview;
|
|
328
|
+
const blockerReview = rawBlockerReview
|
|
329
|
+
&& typeof rawBlockerReview === 'object'
|
|
330
|
+
&& !Array.isArray(rawBlockerReview)
|
|
331
|
+
? rawBlockerReview
|
|
332
|
+
: {};
|
|
333
|
+
const blockerKind = String(blockerReview.kind || '').trim().toLowerCase();
|
|
277
334
|
return {
|
|
278
335
|
status: allowed.has(requestedStatus) ? requestedStatus : fallbackStatus,
|
|
279
336
|
reason: String(raw.reason || '').trim().slice(0, 400),
|
|
337
|
+
blocker_review: {
|
|
338
|
+
kind: [
|
|
339
|
+
'user_input',
|
|
340
|
+
'permission',
|
|
341
|
+
'external_dependency',
|
|
342
|
+
'unavailable_capability',
|
|
343
|
+
'none',
|
|
344
|
+
].includes(blockerKind)
|
|
345
|
+
? blockerKind
|
|
346
|
+
: 'none',
|
|
347
|
+
resolvable_in_run: blockerReview.resolvable_in_run !== false
|
|
348
|
+
&& blockerReview.resolvableInRun !== false,
|
|
349
|
+
required_value: String(
|
|
350
|
+
blockerReview.required_value || blockerReview.requiredValue || '',
|
|
351
|
+
).trim().slice(0, 300),
|
|
352
|
+
},
|
|
353
|
+
research_review: normalizeResearchReview(
|
|
354
|
+
raw.research_review || raw.researchReview,
|
|
355
|
+
),
|
|
280
356
|
};
|
|
281
357
|
}
|
|
282
358
|
|
|
283
|
-
function
|
|
284
|
-
if (
|
|
359
|
+
function validateResearchReview(review, researchAdequacy, toolExecutions = []) {
|
|
360
|
+
if (!researchAdequacy || researchAdequacy.intensity === 'none') {
|
|
361
|
+
return { valid: true, reason: '' };
|
|
362
|
+
}
|
|
363
|
+
if (researchAdequacy.structurallyReady === false) {
|
|
285
364
|
return {
|
|
286
|
-
|
|
287
|
-
reason:
|
|
365
|
+
valid: false,
|
|
366
|
+
reason: researchAdequacy.reason || 'No successful source evidence is available.',
|
|
288
367
|
};
|
|
289
368
|
}
|
|
290
|
-
if (
|
|
369
|
+
if (!review?.required || review.adequate !== true) {
|
|
291
370
|
return {
|
|
292
|
-
|
|
293
|
-
reason: 'The
|
|
371
|
+
valid: false,
|
|
372
|
+
reason: 'The completion judge did not confirm adequate research evidence.',
|
|
294
373
|
};
|
|
295
374
|
}
|
|
296
375
|
|
|
376
|
+
const isValidEvidenceIndex = (index) => {
|
|
377
|
+
const execution = (Array.isArray(toolExecutions) ? toolExecutions : [])[index - 1];
|
|
378
|
+
return Boolean(
|
|
379
|
+
execution
|
|
380
|
+
&& execution.ok === true
|
|
381
|
+
&& execution.evidenceRelevant !== false
|
|
382
|
+
&& execution.evidenceSource !== 'messaging',
|
|
383
|
+
);
|
|
384
|
+
};
|
|
385
|
+
const hasValidEvidence = (indexes) => (
|
|
386
|
+
Array.isArray(indexes) && indexes.some(isValidEvidenceIndex)
|
|
387
|
+
);
|
|
388
|
+
const requiredTargets = Array.isArray(researchAdequacy.targets)
|
|
389
|
+
? researchAdequacy.targets
|
|
390
|
+
: [];
|
|
391
|
+
|
|
392
|
+
if (requiredTargets.length === 0) {
|
|
393
|
+
const reviewIndexes = [
|
|
394
|
+
...(review.evidence_indexes || []),
|
|
395
|
+
...(review.target_coverage || []).flatMap((entry) => entry.evidence_indexes || []),
|
|
396
|
+
];
|
|
397
|
+
if (!hasValidEvidence(reviewIndexes)) {
|
|
398
|
+
return { valid: false, reason: 'The research review cites no successful evidence entry.' };
|
|
399
|
+
}
|
|
400
|
+
if (researchAdequacy.intensity === 'deep' && review.overall_support !== 'primary') {
|
|
401
|
+
return { valid: false, reason: 'Deep research lacks primary source support.' };
|
|
402
|
+
}
|
|
403
|
+
if (review.overall_support === 'none') {
|
|
404
|
+
return { valid: false, reason: 'The research review did not classify its source support.' };
|
|
405
|
+
}
|
|
406
|
+
return { valid: true, reason: '' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const coverageByTarget = new Map(
|
|
410
|
+
(review.target_coverage || []).map((entry) => [
|
|
411
|
+
String(entry.target || '').trim().toLowerCase(),
|
|
412
|
+
entry,
|
|
413
|
+
]),
|
|
414
|
+
);
|
|
415
|
+
for (const target of requiredTargets) {
|
|
416
|
+
const coverage = coverageByTarget.get(String(target).trim().toLowerCase());
|
|
417
|
+
if (!coverage || coverage.support === 'none' || !hasValidEvidence(coverage.evidence_indexes)) {
|
|
418
|
+
return {
|
|
419
|
+
valid: false,
|
|
420
|
+
reason: `The research review does not cite supporting evidence for "${target}".`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (researchAdequacy.intensity === 'deep' && coverage.support !== 'primary') {
|
|
424
|
+
return {
|
|
425
|
+
valid: false,
|
|
426
|
+
reason: `Deep research still lacks primary evidence for "${target}".`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return { valid: true, reason: '' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function enforceTerminalReplyDecision(decision, lastReply, options = {}) {
|
|
434
|
+
// Natural-language terminality is judged by the model completion decision.
|
|
435
|
+
// Runtime only enforces structural evidence contracts (research adequacy).
|
|
297
436
|
const researchAdequacy = options.researchAdequacy
|
|
298
437
|
|| assessResearchAdequacy({
|
|
299
438
|
analysis: options.analysis || null,
|
|
300
439
|
goalContext: options.goalContext || null,
|
|
301
440
|
toolExecutions: options.toolExecutions || [],
|
|
302
441
|
});
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
442
|
+
if (decision?.status === 'blocked') {
|
|
443
|
+
const blockerReview = decision.blocker_review;
|
|
444
|
+
if (
|
|
445
|
+
blockerReview
|
|
446
|
+
&& blockerReview.kind !== 'none'
|
|
447
|
+
&& blockerReview.resolvable_in_run === false
|
|
448
|
+
&& blockerReview.required_value
|
|
449
|
+
) {
|
|
450
|
+
return decision;
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
...decision,
|
|
454
|
+
status: 'continue',
|
|
455
|
+
reason: 'The completion judge did not substantiate a blocker that requires external input or state.',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (decision?.status === 'complete') {
|
|
459
|
+
const reviewValidation = validateResearchReview(
|
|
460
|
+
decision.research_review,
|
|
461
|
+
researchAdequacy,
|
|
462
|
+
options.toolExecutions || [],
|
|
463
|
+
);
|
|
464
|
+
if (reviewValidation.valid) return decision;
|
|
309
465
|
return {
|
|
310
466
|
status: 'continue',
|
|
311
|
-
reason:
|
|
312
|
-
|
|
467
|
+
reason: reviewValidation.reason,
|
|
468
|
+
research_review: decision.research_review,
|
|
313
469
|
};
|
|
314
470
|
}
|
|
315
471
|
return decision;
|
|
@@ -334,7 +490,7 @@ function buildChurnAssessmentPrompt({
|
|
|
334
490
|
const lines = [
|
|
335
491
|
'Return JSON only.',
|
|
336
492
|
'Self-assess your current loop state — are you making genuine progress or spinning?',
|
|
337
|
-
'Schema: {"assessment":"progressing|churn|blocked","reason":"one short concrete sentence"}',
|
|
493
|
+
'Schema: {"assessment":"progressing|churn|blocked","reason":"one short concrete sentence","blocker_kind":"user_input|permission|external_dependency|unavailable_capability|none","resolvable_in_run":true}',
|
|
338
494
|
'',
|
|
339
495
|
`Context: ${readOnlyCount} consecutive iteration(s) with only read/search/inspect operations — no concrete state changes yet.`,
|
|
340
496
|
alreadyRead ? `Already inspected: ${alreadyRead}.` : '',
|
|
@@ -345,28 +501,62 @@ function buildChurnAssessmentPrompt({
|
|
|
345
501
|
`Iteration: ${iteration}`,
|
|
346
502
|
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 6) || 'none'}`,
|
|
347
503
|
adequacy.intensity !== 'none'
|
|
348
|
-
? `Research
|
|
504
|
+
? `Research preflight: intensity=${adequacy.intensity}; evidence_candidates=${adequacy.evidenceCandidateCount}; semantic target coverage must be judged from the evidence summaries.`
|
|
349
505
|
: '',
|
|
350
|
-
adequacy.
|
|
351
|
-
? `
|
|
506
|
+
adequacy.targets.length
|
|
507
|
+
? `Requested research targets: ${adequacy.targets.join('; ')}.`
|
|
352
508
|
: '',
|
|
353
509
|
'',
|
|
354
510
|
'Assessment rules:',
|
|
355
511
|
'- "progressing": You are systematically gathering necessary context and the next concrete action is already determined — you know exactly what to do next.',
|
|
356
512
|
'- "churn": You are re-reading/re-searching information already in context, or exploring without a clear next concrete step. Accept the nudge and act.',
|
|
357
513
|
'- "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.',
|
|
514
|
+
'- Use "blocked" only with a non-none blocker_kind and resolvable_in_run=false. Partial evidence, a failed attempt, or uncertainty is not by itself an external blocker.',
|
|
358
515
|
'- For multi-target research, keep "progressing" while uncovered targets remain and a fresh primary source can still be opened. Do not mark "blocked" just because you have partial notes.',
|
|
359
516
|
'- Re-querying the same snippet source for an already covered target is "churn". Opening a different primary source for an uncovered target is "progressing".',
|
|
517
|
+
adequacy.structurallyReady === false
|
|
518
|
+
? '- No successful source evidence exists yet. Prefer "progressing" if a concrete source-gathering step remains, but use "blocked" for a genuine external dependency or required user input.'
|
|
519
|
+
: '',
|
|
360
520
|
];
|
|
361
521
|
return lines.filter(Boolean).join('\n');
|
|
362
522
|
}
|
|
363
523
|
|
|
524
|
+
function enforceChurnAssessment(assessment) {
|
|
525
|
+
const normalized = normalizeChurnAssessment(assessment);
|
|
526
|
+
if (
|
|
527
|
+
normalized.assessment === 'blocked'
|
|
528
|
+
&& (
|
|
529
|
+
normalized.blocker_kind === 'none'
|
|
530
|
+
|| normalized.resolvable_in_run !== false
|
|
531
|
+
)
|
|
532
|
+
) {
|
|
533
|
+
return {
|
|
534
|
+
...normalized,
|
|
535
|
+
assessment: 'churn',
|
|
536
|
+
reason: 'The churn assessment did not identify a concrete external blocker.',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return normalized;
|
|
540
|
+
}
|
|
541
|
+
|
|
364
542
|
function normalizeChurnAssessment(raw) {
|
|
365
543
|
const allowed = new Set(['progressing', 'churn', 'blocked']);
|
|
366
544
|
const assessment = String(raw?.assessment || '').trim().toLowerCase();
|
|
545
|
+
const blockerKind = String(raw?.blocker_kind || raw?.blockerKind || '').trim().toLowerCase();
|
|
367
546
|
return {
|
|
368
547
|
assessment: allowed.has(assessment) ? assessment : 'churn',
|
|
369
548
|
reason: String(raw?.reason || '').trim().slice(0, 300),
|
|
549
|
+
blocker_kind: [
|
|
550
|
+
'user_input',
|
|
551
|
+
'permission',
|
|
552
|
+
'external_dependency',
|
|
553
|
+
'unavailable_capability',
|
|
554
|
+
'none',
|
|
555
|
+
].includes(blockerKind)
|
|
556
|
+
? blockerKind
|
|
557
|
+
: 'none',
|
|
558
|
+
resolvable_in_run: raw?.resolvable_in_run !== false
|
|
559
|
+
&& raw?.resolvableInRun !== false,
|
|
370
560
|
};
|
|
371
561
|
}
|
|
372
562
|
|
|
@@ -378,8 +568,11 @@ module.exports = {
|
|
|
378
568
|
goalContractFromAnalysis,
|
|
379
569
|
goalContractFromPlan,
|
|
380
570
|
mergeGoalContracts,
|
|
571
|
+
enforceChurnAssessment,
|
|
381
572
|
normalizeChurnAssessment,
|
|
382
573
|
normalizeCompletionDecision,
|
|
574
|
+
normalizeResearchReview,
|
|
383
575
|
normalizeGoalContract,
|
|
384
576
|
resolveRunGoalContext,
|
|
577
|
+
validateResearchReview,
|
|
385
578
|
};
|
|
@@ -153,8 +153,8 @@ const {
|
|
|
153
153
|
buildDeterministicMessagingErrorReply,
|
|
154
154
|
buildModelFailureLoopPrompt,
|
|
155
155
|
} = require('../messagingFallback');
|
|
156
|
-
const { isDeferredWorkReply } = require('../terminal_reply');
|
|
157
156
|
const {
|
|
157
|
+
assessResearchAdequacy,
|
|
158
158
|
classifyToolExecution,
|
|
159
159
|
gatheredNewEvidence,
|
|
160
160
|
isSubstantiveProgressToolName,
|
|
@@ -303,13 +303,34 @@ function summarizeReadTargets(toolExecutions = []) {
|
|
|
303
303
|
return targets.join('; ');
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
function buildNoProgressWrapupPrompt({
|
|
306
|
+
function buildNoProgressWrapupPrompt({
|
|
307
|
+
readOnlyCount = 0,
|
|
308
|
+
alreadyRead = '',
|
|
309
|
+
platform = null,
|
|
310
|
+
researchAdequacy = null,
|
|
311
|
+
} = {}) {
|
|
312
|
+
const adequacy = researchAdequacy && typeof researchAdequacy === 'object'
|
|
313
|
+
? researchAdequacy
|
|
314
|
+
: null;
|
|
315
|
+
const researchIncomplete = adequacy
|
|
316
|
+
&& adequacy.adequate === false
|
|
317
|
+
&& adequacy.intensity
|
|
318
|
+
&& adequacy.intensity !== 'none';
|
|
307
319
|
return [
|
|
308
320
|
`This is the final turn for this run (no further tool calls; ${Math.max(0, Number(readOnlyCount) || 0)} read-only turns without a state change).`,
|
|
309
321
|
alreadyRead ? `Already gathered: ${alreadyRead}.` : '',
|
|
322
|
+
researchIncomplete
|
|
323
|
+
? `Research coverage is incomplete: ${adequacy.reason || 'requested targets still lack primary evidence.'}`
|
|
324
|
+
: '',
|
|
325
|
+
researchIncomplete && adequacy.uncoveredTargets?.length
|
|
326
|
+
? `Uncovered research targets: ${adequacy.uncoveredTargets.join('; ')}.`
|
|
327
|
+
: '',
|
|
310
328
|
'Write the answer now from everything you have already gathered in this conversation. Deliver the useful result you DO have — calendar, weather, emails, search findings, whatever was collected — formatted as the actual answer to the original request.',
|
|
311
329
|
'If one part could not be retrieved, still deliver everything else and note the missing part in at most one short clause. Never withhold a useful answer because a single detail is missing.',
|
|
312
|
-
'
|
|
330
|
+
'Do not invent entities, products, people, files, outcomes, or next-step work that is not already supported by tool evidence in this conversation.',
|
|
331
|
+
researchIncomplete
|
|
332
|
+
? 'Because research is incomplete, clearly label assumptions and missing targets. Prefer a partial verified answer or a concrete blocker over a confident fabricated comparison.'
|
|
333
|
+
: 'Only report a pure blocker if you genuinely gathered nothing usable at all. Do not describe the result as unfinished, unconfirmed, "blocked", or "still working" when you have something useful — this IS the final answer.',
|
|
313
334
|
buildMaxIterationWrapupPrompt(platform),
|
|
314
335
|
].filter(Boolean).join('\n\n');
|
|
315
336
|
}
|
|
@@ -770,6 +791,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
770
791
|
? {
|
|
771
792
|
platform: options.source || null,
|
|
772
793
|
chatId: options.chatId || null,
|
|
794
|
+
behavior: options.context?.socialIntelligence || null,
|
|
773
795
|
}
|
|
774
796
|
: null,
|
|
775
797
|
goalContract: carriedGoalContract,
|
|
@@ -784,7 +806,9 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
784
806
|
progressLedger,
|
|
785
807
|
goalContract: carriedGoalContract,
|
|
786
808
|
});
|
|
787
|
-
|
|
809
|
+
if (options.context?.socialIntelligence?.isGroup !== true) {
|
|
810
|
+
engine.startMessagingProgressSupervisor(runId);
|
|
811
|
+
}
|
|
788
812
|
engine.emit(userId, 'run:start', {
|
|
789
813
|
runId,
|
|
790
814
|
agentId,
|
|
@@ -821,6 +845,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
821
845
|
userMessage,
|
|
822
846
|
agentId,
|
|
823
847
|
triggerSource,
|
|
848
|
+
memoryAudience: options.memoryAudience || 'owner',
|
|
824
849
|
});
|
|
825
850
|
// Pass short descriptions so the model always knows every available tool.
|
|
826
851
|
// compactToolDefinition caps tool desc at 120 chars, param desc at 70 chars.
|
|
@@ -971,7 +996,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
971
996
|
// Reuse the run's real system prompt so the update follows the same voice and
|
|
972
997
|
// formatting guidelines as every other message (single source of truth).
|
|
973
998
|
const sysContent = [systemPrompt?.stable, systemPrompt?.dynamic].filter(Boolean).join('\n\n')
|
|
974
|
-
|| '
|
|
999
|
+
|| 'Respond directly and accurately using only the verified progress evidence.';
|
|
975
1000
|
const resp = await runAbortableModelCall(
|
|
976
1001
|
(signal) => provider.chat(
|
|
977
1002
|
[
|
|
@@ -1373,6 +1398,47 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1373
1398
|
continue;
|
|
1374
1399
|
}
|
|
1375
1400
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
1401
|
+
const researchAdequacyAtCap = assessResearchAdequacy({
|
|
1402
|
+
analysis,
|
|
1403
|
+
goalContext: runGoalCtx,
|
|
1404
|
+
toolExecutions,
|
|
1405
|
+
});
|
|
1406
|
+
// Incomplete multi-target research is not "stuck" yet if primary sources
|
|
1407
|
+
// remain uncovered. Give one guided research continuation instead of
|
|
1408
|
+
// force-finishing with a guessed comparison.
|
|
1409
|
+
if (
|
|
1410
|
+
researchAdequacyAtCap.adequate === false
|
|
1411
|
+
&& researchAdequacyAtCap.intensity !== 'none'
|
|
1412
|
+
&& iterMeta
|
|
1413
|
+
&& iterMeta.researchAdequacyDeferralUsed !== true
|
|
1414
|
+
&& (
|
|
1415
|
+
researchAdequacyAtCap.uncoveredTargets.length > 0
|
|
1416
|
+
|| researchAdequacyAtCap.primarySourceCount < researchAdequacyAtCap.requiredPrimarySources
|
|
1417
|
+
)
|
|
1418
|
+
) {
|
|
1419
|
+
iterMeta.researchAdequacyDeferralUsed = true;
|
|
1420
|
+
iterMeta.consecutiveReadOnlyIterations = Math.max(0, churnNudgeThreshold - 1);
|
|
1421
|
+
messages.push({
|
|
1422
|
+
role: 'system',
|
|
1423
|
+
content: [
|
|
1424
|
+
'Research self-check: evidence is still incomplete, so this run must not force-finish yet.',
|
|
1425
|
+
researchAdequacyAtCap.reason ? `Gap: ${researchAdequacyAtCap.reason}` : '',
|
|
1426
|
+
researchAdequacyAtCap.nextActions?.length
|
|
1427
|
+
? `Next safe steps:\n- ${researchAdequacyAtCap.nextActions.join('\n- ')}`
|
|
1428
|
+
: '',
|
|
1429
|
+
'Open or fetch primary sources for uncovered targets now. Do not invent the missing comparison from memory or search snippets alone.',
|
|
1430
|
+
].filter(Boolean).join('\n'),
|
|
1431
|
+
});
|
|
1432
|
+
engine.recordRunEvent(userId, runId, 'read_only_wrapup_deferred_for_research', {
|
|
1433
|
+
iteration,
|
|
1434
|
+
readOnlyCount,
|
|
1435
|
+
intensity: researchAdequacyAtCap.intensity,
|
|
1436
|
+
uncoveredTargets: researchAdequacyAtCap.uncoveredTargets,
|
|
1437
|
+
primarySourceCount: researchAdequacyAtCap.primarySourceCount,
|
|
1438
|
+
requiredPrimarySources: researchAdequacyAtCap.requiredPrimarySources,
|
|
1439
|
+
}, { agentId });
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1376
1442
|
triggerForceWrapup = true;
|
|
1377
1443
|
} else if (readOnlyCount >= churnNudgeThreshold) {
|
|
1378
1444
|
alreadyRead = summarizeReadTargets(toolExecutions);
|
|
@@ -1447,6 +1513,11 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1447
1513
|
readOnlyCount,
|
|
1448
1514
|
alreadyRead,
|
|
1449
1515
|
platform: options?.source || null,
|
|
1516
|
+
researchAdequacy: assessResearchAdequacy({
|
|
1517
|
+
analysis,
|
|
1518
|
+
goalContext: runGoalCtx,
|
|
1519
|
+
toolExecutions,
|
|
1520
|
+
}),
|
|
1450
1521
|
}),
|
|
1451
1522
|
},
|
|
1452
1523
|
]),
|
|
@@ -1466,8 +1537,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1466
1537
|
console.warn(`[Run ${shortenRunId(runId)}] no_progress_wrapup failed: ${summarizeForLog(wrapErr?.message || wrapErr, 180)}`);
|
|
1467
1538
|
}
|
|
1468
1539
|
totalTokens += wrapTokens;
|
|
1469
|
-
const usableWrap = normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
1470
|
-
&& !isDeferredWorkReply(lastContent);
|
|
1540
|
+
const usableWrap = normalizeOutgoingMessage(lastContent, options?.source || null);
|
|
1471
1541
|
if (!usableWrap) {
|
|
1472
1542
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
1473
1543
|
}
|
|
@@ -1697,15 +1767,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1697
1767
|
lastContent = '';
|
|
1698
1768
|
continue;
|
|
1699
1769
|
}
|
|
1700
|
-
if (engine.shouldFastCompleteVoiceReply({
|
|
1701
|
-
options,
|
|
1702
|
-
toolExecutions,
|
|
1703
|
-
failedStepCount,
|
|
1704
|
-
messagingSent: engine.activeRuns.get(runId)?.messagingSent || false,
|
|
1705
|
-
lastReply: lastContent,
|
|
1706
|
-
})) {
|
|
1707
|
-
break;
|
|
1708
|
-
}
|
|
1709
1770
|
if (shouldContinueAfterBlankToolFailure({
|
|
1710
1771
|
lastContent,
|
|
1711
1772
|
failedStepCount,
|
|
@@ -1761,13 +1822,23 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
1761
1822
|
iteration,
|
|
1762
1823
|
}, { agentId });
|
|
1763
1824
|
if (loopDecision.decision.status === 'continue') {
|
|
1825
|
+
const researchGuidance = loopDecision.researchAdequacy?.adequate === false
|
|
1826
|
+
? [
|
|
1827
|
+
`Research still incomplete: ${loopDecision.researchAdequacy.reason || 'gather primary evidence for remaining targets.'}`,
|
|
1828
|
+
loopDecision.researchAdequacy.nextActions?.length
|
|
1829
|
+
? `Next safe steps: ${loopDecision.researchAdequacy.nextActions.join(' ')}`
|
|
1830
|
+
: '',
|
|
1831
|
+
].filter(Boolean).join(' ')
|
|
1832
|
+
: '';
|
|
1764
1833
|
messages.push({
|
|
1765
1834
|
role: 'system',
|
|
1766
1835
|
content: [
|
|
1767
1836
|
'The run self-check determined the latest assistant text is not terminal.',
|
|
1768
1837
|
'Continue with the next safe tool/model step.',
|
|
1769
1838
|
'If the text was only a progress note, do not repeat it; either make progress or provide a real final/blocker reply.',
|
|
1770
|
-
|
|
1839
|
+
'Do not invent missing targets, outcomes, or tool results. Gather evidence or return a truthful partial/blocker answer.',
|
|
1840
|
+
researchGuidance,
|
|
1841
|
+
].filter(Boolean).join(' '),
|
|
1771
1842
|
});
|
|
1772
1843
|
lastContent = '';
|
|
1773
1844
|
continue;
|
|
@@ -2399,8 +2470,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2399
2470
|
// On budget exhaustion the model's last text is an untrustworthy mid-thought
|
|
2400
2471
|
// fragment. Replace it with the wrap-up answer, or a clean deterministic
|
|
2401
2472
|
// fallback if the wrap-up came back empty — never deliver the fragment.
|
|
2402
|
-
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null)
|
|
2403
|
-
&& !isDeferredWorkReply(wrapText);
|
|
2473
|
+
const usableWrap = normalizeOutgoingMessage(wrapText, options?.source || null);
|
|
2404
2474
|
lastContent = usableWrap
|
|
2405
2475
|
? wrapText
|
|
2406
2476
|
: buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
@@ -2455,8 +2525,7 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2455
2525
|
// evidence) instead of second-guessing it into a generic blob. Only fall back to
|
|
2456
2526
|
// a deterministic message when the model returned nothing usable.
|
|
2457
2527
|
const recoveredVisible = Boolean(
|
|
2458
|
-
normalizeOutgoingMessage(lastContent, options?.source || null)
|
|
2459
|
-
&& !isDeferredWorkReply(lastContent),
|
|
2528
|
+
normalizeOutgoingMessage(lastContent, options?.source || null),
|
|
2460
2529
|
);
|
|
2461
2530
|
if (!recoveredVisible) {
|
|
2462
2531
|
lastContent = buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
@@ -2603,18 +2672,8 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2603
2672
|
});
|
|
2604
2673
|
}
|
|
2605
2674
|
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
iteration,
|
|
2609
|
-
contentPreview: String(finalResponseText || '').slice(0, 240),
|
|
2610
|
-
}, { agentId });
|
|
2611
|
-
finalResponseText = buildDeterministicMessagingFallback({
|
|
2612
|
-
failedStepCount,
|
|
2613
|
-
stepIndex,
|
|
2614
|
-
toolExecutions,
|
|
2615
|
-
});
|
|
2616
|
-
lastContent = finalResponseText;
|
|
2617
|
-
}
|
|
2675
|
+
// Final reply terminality is decided by the AI completion judge / task_complete path.
|
|
2676
|
+
// Do not phrase-match natural-language drafts here.
|
|
2618
2677
|
|
|
2619
2678
|
if (deliverableWorkflow && deliverablePlan) {
|
|
2620
2679
|
engine.recordRunEvent(userId, runId, 'deliverable_validation_started', {
|
|
@@ -2883,7 +2942,6 @@ async function runConversation(engine, userId, userMessage, options = {}, _model
|
|
|
2883
2942
|
const drafted = sanitizeModelOutput(modelReply.content || '', { model });
|
|
2884
2943
|
if (
|
|
2885
2944
|
normalizeOutgoingMessage(drafted, options?.source || null)
|
|
2886
|
-
&& !isDeferredWorkReply(drafted)
|
|
2887
2945
|
) {
|
|
2888
2946
|
messagingFailureContent = drafted.trim();
|
|
2889
2947
|
}
|