neoagent 3.2.1-beta.9 → 3.3.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.
Files changed (78) hide show
  1. package/LICENSE +67 -80
  2. package/README.md +7 -1
  3. package/docs/licensing.md +44 -0
  4. package/flutter_app/lib/main.dart +1 -0
  5. package/flutter_app/lib/main_app_shell.dart +186 -40
  6. package/flutter_app/lib/main_chat.dart +542 -80
  7. package/flutter_app/lib/main_controller.dart +66 -5
  8. package/flutter_app/lib/main_install.dart +9 -7
  9. package/flutter_app/lib/main_models.dart +171 -22
  10. package/flutter_app/lib/main_operations.dart +0 -49
  11. package/flutter_app/lib/main_settings.dart +338 -0
  12. package/flutter_app/lib/src/backend_client.dart +19 -0
  13. package/flutter_app/lib/src/local_runtime_paths.dart +60 -0
  14. package/lib/manager.js +10 -2
  15. package/package.json +1 -1
  16. package/runtime/paths.js +70 -0
  17. package/server/db/database.js +2 -2
  18. package/server/http/routes.js +1 -0
  19. package/server/public/.last_build_id +1 -1
  20. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  21. package/server/public/flutter_bootstrap.js +1 -1
  22. package/server/public/main.dart.js +84940 -83737
  23. package/server/routes/behavior.js +80 -0
  24. package/server/routes/settings.js +4 -0
  25. package/server/services/agents/manager.js +1 -1
  26. package/server/services/ai/history.js +1 -1
  27. package/server/services/ai/loop/agent_engine_core.js +110 -17
  28. package/server/services/ai/loop/blank_recovery.js +5 -4
  29. package/server/services/ai/loop/completion_judge.js +226 -33
  30. package/server/services/ai/loop/conversation_loop.js +92 -34
  31. package/server/services/ai/loop/messaging_delivery.js +47 -0
  32. package/server/services/ai/loop/progress_classification.js +2 -0
  33. package/server/services/ai/loopPolicy.js +24 -2
  34. package/server/services/ai/messagingFallback.js +17 -17
  35. package/server/services/ai/model_failure_cache.js +7 -0
  36. package/server/services/ai/systemPrompt.js +31 -122
  37. package/server/services/ai/taskAnalysis.js +86 -12
  38. package/server/services/ai/toolEvidence.js +68 -224
  39. package/server/services/ai/tools.js +60 -19
  40. package/server/services/behavior/config.js +251 -0
  41. package/server/services/behavior/defaults.js +68 -0
  42. package/server/services/behavior/delivery.js +176 -0
  43. package/server/services/behavior/index.js +43 -0
  44. package/server/services/behavior/model_client.js +35 -0
  45. package/server/services/behavior/modules/agent_identity.js +37 -0
  46. package/server/services/behavior/modules/channel_style.js +28 -0
  47. package/server/services/behavior/modules/index.js +29 -0
  48. package/server/services/behavior/modules/norms.js +101 -0
  49. package/server/services/behavior/modules/persona.js +172 -0
  50. package/server/services/behavior/modules/persona_prompt.js +238 -0
  51. package/server/services/behavior/modules/social_memory.js +86 -0
  52. package/server/services/behavior/modules/social_observability.js +94 -0
  53. package/server/services/behavior/modules/social_signals.js +41 -0
  54. package/server/services/behavior/modules/theory_of_mind.js +118 -0
  55. package/server/services/behavior/modules/turn_taking.js +238 -0
  56. package/server/services/behavior/pipeline.js +341 -0
  57. package/server/services/behavior/registry.js +78 -0
  58. package/server/services/behavior/signals.js +107 -0
  59. package/server/services/behavior/state.js +99 -0
  60. package/server/services/behavior/system_prompt.js +75 -0
  61. package/server/services/memory/manager.js +14 -33
  62. package/server/services/messaging/access_policy.js +269 -74
  63. package/server/services/messaging/automation.js +158 -27
  64. package/server/services/messaging/discord.js +11 -1
  65. package/server/services/messaging/formatting_guides.js +5 -4
  66. package/server/services/messaging/http_platforms.js +73 -28
  67. package/server/services/messaging/inbound_queue.js +74 -13
  68. package/server/services/messaging/inbound_store.js +33 -0
  69. package/server/services/messaging/manager.js +57 -5
  70. package/server/services/messaging/telegram.js +10 -1
  71. package/server/services/messaging/whatsapp.js +11 -0
  72. package/server/services/social_reach/channels/social_video.js +1 -1
  73. package/server/services/social_video/captions.js +2 -2
  74. package/server/services/social_video/service.js +194 -29
  75. package/server/services/voice/message.js +1 -1
  76. package/server/services/voice/runtime.js +2 -2
  77. package/server/utils/logger.js +19 -0
  78. 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, use "continue" until the required primary/source coverage and target coverage are met, or the draft is an explicit blocker naming exactly what could not be verified.',
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}. Current coverage: primary=${adequacy.primarySourceCount}/${adequacy.requiredPrimarySources}, secondary=${adequacy.secondarySourceCount}, targets_covered=${adequacy.coveredTargets.length}/${Math.max(adequacy.requiredTargetCoverage, adequacy.targets.length)}.`,
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.adequate === false) {
240
+ if (adequacy.structurallyReady === false) {
236
241
  lines.push(
237
- `- Research is still incomplete (${adequacy.reason}). Use "continue" unless the latest draft is an explicit blocker naming the exact missing evidence.`,
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, 8) || 'none'}`,
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 adequacy: intensity=${adequacy.intensity}; adequate=${adequacy.adequate}; reason=${adequacy.reason}`
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 enforceTerminalReplyDecision(decision, lastReply, options = {}) {
284
- if (isDeferredWorkReply(lastReply)) {
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
- status: 'continue',
287
- reason: 'The latest reply only announces or promises unfinished work; the run must continue or return a concrete blocker.',
365
+ valid: false,
366
+ reason: researchAdequacy.reason || 'No successful source evidence is available.',
288
367
  };
289
368
  }
290
- if (decision?.status === 'continue' && isTerminalQuestionOrBlockerReply(lastReply)) {
369
+ if (!review?.required || review.adequate !== true) {
291
370
  return {
292
- status: 'blocked',
293
- reason: 'The latest reply asks for user input or states a concrete blocker, so the run must wait instead of repeating it.',
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
- researchAdequacy
305
- && researchAdequacy.adequate === false
306
- && (decision?.status === 'complete' || decision?.status === 'blocked')
307
- && !isTerminalQuestionOrBlockerReply(lastReply)
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: researchAdequacy.reason
312
- || 'Research evidence is still incomplete for the requested targets; continue gathering sources before finishing.',
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 adequacy: intensity=${adequacy.intensity}; adequate=${adequacy.adequate}; covered=${adequacy.coveredTargets.length}/${Math.max(adequacy.requiredTargetCoverage, adequacy.targets.length)}; primary=${adequacy.primarySourceCount}/${adequacy.requiredPrimarySources}.`
504
+ ? `Research preflight: intensity=${adequacy.intensity}; evidence_candidates=${adequacy.evidenceCandidateCount}; semantic target coverage must be judged from the evidence summaries.`
349
505
  : '',
350
- adequacy.uncoveredTargets.length
351
- ? `Still uncovered research targets: ${adequacy.uncoveredTargets.join('; ')}.`
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({ readOnlyCount = 0, alreadyRead = '', platform = null } = {}) {
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
- '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.',
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
- engine.startMessagingProgressSupervisor(runId);
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
- || 'You are a helpful assistant.';
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
- ].join(' '),
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
- if (!messagingSent && isDeferredWorkReply(finalResponseText)) {
2607
- engine.recordRunEvent(userId, runId, 'non_terminal_final_reply_rejected', {
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
  }