neoagent 3.2.1-beta.1 → 3.2.1-beta.11

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 (198) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +157 -24
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +7 -2
  8. package/flutter_app/lib/main_operations.dart +334 -370
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +342 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +97 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +12 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +81930 -80509
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/behavior.js +80 -0
  33. package/server/routes/browser.js +23 -15
  34. package/server/routes/desktop.js +18 -1
  35. package/server/routes/integrations.js +107 -1
  36. package/server/routes/memory.js +1 -0
  37. package/server/routes/settings.js +20 -5
  38. package/server/routes/social_reach.js +12 -3
  39. package/server/routes/social_video.js +4 -0
  40. package/server/services/agents/manager.js +1 -1
  41. package/server/services/ai/capabilityHealth.js +62 -96
  42. package/server/services/ai/compaction.js +7 -2
  43. package/server/services/ai/history.js +45 -6
  44. package/server/services/ai/integrated_tools/http_request.js +8 -0
  45. package/server/services/ai/loop/agent_engine_core.js +452 -176
  46. package/server/services/ai/loop/blank_recovery.js +5 -4
  47. package/server/services/ai/loop/callbacks.js +1 -0
  48. package/server/services/ai/loop/completion_judge.js +291 -8
  49. package/server/services/ai/loop/conversation_loop.js +515 -342
  50. package/server/services/ai/loop/messaging_delivery.js +176 -57
  51. package/server/services/ai/loop/model_call_guard.js +91 -0
  52. package/server/services/ai/loop/model_io.js +20 -45
  53. package/server/services/ai/loop/progress_classification.js +2 -0
  54. package/server/services/ai/loop/tool_dispatch.js +19 -8
  55. package/server/services/ai/loopPolicy.js +48 -21
  56. package/server/services/ai/messagingFallback.js +17 -17
  57. package/server/services/ai/model_discovery.js +227 -0
  58. package/server/services/ai/model_failure_cache.js +108 -0
  59. package/server/services/ai/model_identity.js +71 -0
  60. package/server/services/ai/models.js +68 -163
  61. package/server/services/ai/providerRetry.js +17 -59
  62. package/server/services/ai/provider_selector.js +166 -0
  63. package/server/services/ai/providers/anthropic.js +2 -2
  64. package/server/services/ai/providers/claudeCode.js +21 -33
  65. package/server/services/ai/providers/githubCopilot.js +41 -20
  66. package/server/services/ai/providers/google.js +135 -97
  67. package/server/services/ai/providers/grok.js +4 -3
  68. package/server/services/ai/providers/grokOauth.js +19 -27
  69. package/server/services/ai/providers/nvidia.js +10 -5
  70. package/server/services/ai/providers/ollama.js +111 -84
  71. package/server/services/ai/providers/ollama_stream.js +142 -0
  72. package/server/services/ai/providers/openai.js +39 -5
  73. package/server/services/ai/providers/openaiCodex.js +11 -4
  74. package/server/services/ai/providers/openrouter.js +29 -7
  75. package/server/services/ai/providers/provider_error.js +36 -0
  76. package/server/services/ai/settings.js +26 -2
  77. package/server/services/ai/systemPrompt.js +32 -123
  78. package/server/services/ai/taskAnalysis.js +104 -11
  79. package/server/services/ai/toolEvidence.js +256 -29
  80. package/server/services/ai/tools.js +248 -117
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/behavior/config.js +251 -0
  87. package/server/services/behavior/defaults.js +68 -0
  88. package/server/services/behavior/delivery.js +176 -0
  89. package/server/services/behavior/index.js +43 -0
  90. package/server/services/behavior/model_client.js +35 -0
  91. package/server/services/behavior/modules/agent_identity.js +37 -0
  92. package/server/services/behavior/modules/channel_style.js +28 -0
  93. package/server/services/behavior/modules/index.js +29 -0
  94. package/server/services/behavior/modules/norms.js +101 -0
  95. package/server/services/behavior/modules/persona.js +48 -0
  96. package/server/services/behavior/modules/persona_prompt.js +33 -0
  97. package/server/services/behavior/modules/social_memory.js +86 -0
  98. package/server/services/behavior/modules/social_observability.js +94 -0
  99. package/server/services/behavior/modules/social_signals.js +41 -0
  100. package/server/services/behavior/modules/theory_of_mind.js +110 -0
  101. package/server/services/behavior/modules/turn_taking.js +237 -0
  102. package/server/services/behavior/pipeline.js +285 -0
  103. package/server/services/behavior/registry.js +78 -0
  104. package/server/services/behavior/signals.js +107 -0
  105. package/server/services/behavior/state.js +99 -0
  106. package/server/services/behavior/system_prompt.js +75 -0
  107. package/server/services/browser/controller.js +843 -385
  108. package/server/services/browser/extension/gateway.js +40 -16
  109. package/server/services/browser/extension/protocol.js +15 -1
  110. package/server/services/browser/extension/provider.js +71 -47
  111. package/server/services/browser/extension/registry.js +155 -34
  112. package/server/services/cli/executor.js +62 -9
  113. package/server/services/credentials/bitwarden_cli.js +322 -0
  114. package/server/services/credentials/broker.js +594 -0
  115. package/server/services/desktop/gateway.js +41 -4
  116. package/server/services/desktop/protocol.js +3 -0
  117. package/server/services/desktop/provider.js +39 -42
  118. package/server/services/desktop/registry.js +137 -52
  119. package/server/services/integrations/bitwarden/constants.js +14 -0
  120. package/server/services/integrations/bitwarden/provider.js +197 -0
  121. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  122. package/server/services/integrations/figma/provider.js +78 -12
  123. package/server/services/integrations/github/common.js +11 -6
  124. package/server/services/integrations/github/provider.js +52 -53
  125. package/server/services/integrations/google/provider.js +55 -19
  126. package/server/services/integrations/home_assistant/network.js +17 -20
  127. package/server/services/integrations/home_assistant/provider.js +7 -5
  128. package/server/services/integrations/home_assistant/tools.js +17 -5
  129. package/server/services/integrations/http.js +51 -0
  130. package/server/services/integrations/manager.js +159 -53
  131. package/server/services/integrations/microsoft/provider.js +80 -13
  132. package/server/services/integrations/neoarchive/provider.js +55 -29
  133. package/server/services/integrations/neorecall/client.js +17 -10
  134. package/server/services/integrations/neorecall/provider.js +20 -11
  135. package/server/services/integrations/notion/provider.js +16 -13
  136. package/server/services/integrations/oauth_provider.js +115 -51
  137. package/server/services/integrations/registry.js +2 -0
  138. package/server/services/integrations/slack/provider.js +98 -9
  139. package/server/services/integrations/spotify/provider.js +67 -71
  140. package/server/services/integrations/trello/provider.js +21 -7
  141. package/server/services/integrations/weather/provider.js +18 -12
  142. package/server/services/integrations/whatsapp/provider.js +76 -16
  143. package/server/services/manager.js +110 -1
  144. package/server/services/memory/embedding_index.js +20 -8
  145. package/server/services/memory/embeddings.js +151 -90
  146. package/server/services/memory/ingestion.js +50 -9
  147. package/server/services/memory/ingestion_documents.js +13 -3
  148. package/server/services/memory/manager.js +66 -52
  149. package/server/services/messaging/access_policy.js +10 -6
  150. package/server/services/messaging/automation.js +240 -34
  151. package/server/services/messaging/discord.js +11 -1
  152. package/server/services/messaging/formatting_guides.js +5 -4
  153. package/server/services/messaging/http_platforms.js +37 -16
  154. package/server/services/messaging/inbound_queue.js +143 -28
  155. package/server/services/messaging/inbound_store.js +257 -0
  156. package/server/services/messaging/manager.js +344 -51
  157. package/server/services/messaging/telegram.js +10 -1
  158. package/server/services/messaging/typing_keepalive.js +5 -2
  159. package/server/services/messaging/whatsapp.js +33 -14
  160. package/server/services/network/http.js +210 -0
  161. package/server/services/network/safe_request.js +307 -0
  162. package/server/services/runtime/backends/local-vm.js +227 -67
  163. package/server/services/runtime/docker-vm-manager.js +9 -0
  164. package/server/services/runtime/guest_bootstrap.js +30 -4
  165. package/server/services/runtime/guest_image.js +43 -12
  166. package/server/services/runtime/manager.js +77 -23
  167. package/server/services/runtime/validation.js +7 -6
  168. package/server/services/security/tool_categories.js +6 -0
  169. package/server/services/social_reach/channels/github.js +10 -4
  170. package/server/services/social_reach/channels/reddit.js +4 -4
  171. package/server/services/social_reach/channels/rss.js +2 -2
  172. package/server/services/social_reach/channels/social_video.js +13 -8
  173. package/server/services/social_reach/channels/v2ex.js +21 -8
  174. package/server/services/social_reach/channels/x.js +2 -2
  175. package/server/services/social_reach/channels/xueqiu.js +5 -5
  176. package/server/services/social_reach/service.js +9 -6
  177. package/server/services/social_reach/utils.js +65 -14
  178. package/server/services/social_video/captions.js +2 -2
  179. package/server/services/social_video/service.js +343 -68
  180. package/server/services/tasks/integration_runtime.js +18 -8
  181. package/server/services/tasks/runtime.js +39 -4
  182. package/server/services/voice/agentBridge.js +17 -4
  183. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  184. package/server/services/voice/liveSession.js +31 -0
  185. package/server/services/voice/message.js +1 -1
  186. package/server/services/voice/openaiSpeech.js +33 -8
  187. package/server/services/voice/providers.js +233 -151
  188. package/server/services/voice/runtime.js +2 -2
  189. package/server/services/voice/runtimeManager.js +118 -20
  190. package/server/services/voice/turnRunner.js +6 -0
  191. package/server/services/wearable/firmware_manifest.js +51 -13
  192. package/server/services/wearable/service.js +1 -0
  193. package/server/utils/abort.js +96 -0
  194. package/server/utils/cloud-security.js +110 -3
  195. package/server/utils/files.js +31 -0
  196. package/server/utils/image_payload.js +95 -0
  197. package/server/utils/logger.js +19 -0
  198. package/server/utils/retry.js +107 -0
@@ -33,9 +33,9 @@ function shouldContinueAfterRecoverableToolFailure({
33
33
  if (Number(remainingIterations || 0) <= 0) return false;
34
34
  const failedExecution = latestFailedToolExecution(toolExecutions);
35
35
  if (!isRecoverableInternalToolFailure(failedExecution)) return false;
36
- const visible = normalizeOutgoingMessage(lastContent || '');
37
- if (!visible) return false;
38
- return /\b(blocked|could not|cannot|can't|do not have a confirmed finished result|not found|missing|internal tool issue)\b/i.test(visible);
36
+ // Structural only: recoverable internal tool failure + remaining budget.
37
+ // Do not phrase-match the draft reply.
38
+ return Boolean(normalizeOutgoingMessage(lastContent || '') || failedExecution);
39
39
  }
40
40
 
41
41
  function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
@@ -46,7 +46,8 @@ function buildBlankAfterToolFailureGuidance(toolExecutions = []) {
46
46
  return [
47
47
  `The previous tool "${toolName}" failed with: ${summarizeForLog(failure, 240)}.`,
48
48
  'The latest assistant turn returned no user-facing answer and no tool call, so the task is not terminal.',
49
- 'Continue with the next safe recovery action: retry with corrected arguments, use another available tool, verify from existing evidence, or report a real blocker only if no autonomous path remains.',
49
+ 'Continue with the next safe recovery action now in this same turn: retry with corrected arguments, use another available tool, verify from existing evidence, or report a real blocker only if no autonomous path remains.',
50
+ 'Do not invent a finished result. Prefer a concrete recovery step or a truthful partial answer over silence.',
50
51
  ].join(' ');
51
52
  }
52
53
 
@@ -65,6 +65,7 @@ async function publishInterimUpdate(engine, {
65
65
  persistConversation: true,
66
66
  metadata,
67
67
  deliveryKind: 'interim',
68
+ signal: runMeta.abortController?.signal || null,
68
69
  });
69
70
  requireSuccessfulMessagingDelivery(deliveryResult, 'Interim messaging delivery');
70
71
  } else if (triggerSource === 'voice_live') {
@@ -3,7 +3,10 @@
3
3
  const { normalizeCompletionConfidence } = require('../completion');
4
4
  const { normalizeOutgoingMessage } = require('../messagingFallback');
5
5
  const {
6
+ assessResearchAdequacy,
7
+ formatResearchAdequacyGuidance,
6
8
  summarizeAvailableTools,
9
+ summarizeResearchEvidenceCatalog,
7
10
  summarizeToolExecutions,
8
11
  } = require('../toolEvidence');
9
12
 
@@ -194,12 +197,19 @@ function buildCompletionDecisionPrompt({
194
197
  lastReply,
195
198
  iteration,
196
199
  maxIterations,
200
+ analysis = null,
201
+ researchAdequacy = null,
197
202
  }) {
198
203
  const draftReply = normalizeOutgoingMessage(lastReply) || '';
204
+ const adequacy = researchAdequacy || assessResearchAdequacy({
205
+ analysis,
206
+ goalContext,
207
+ toolExecutions,
208
+ });
199
209
  const lines = [
200
210
  'Return JSON only.',
201
211
  'Decide whether this run should continue autonomously or stop now.',
202
- '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"]}}',
203
213
  'Rules:',
204
214
  '- Use "continue" whenever any safe next step remains in this same run.',
205
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.',
@@ -210,13 +220,37 @@ function buildCompletionDecisionPrompt({
210
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.',
211
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.',
212
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.`,
213
- triggerSource === 'messaging' && messagingSent
214
- ? '- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.'
215
- : triggerSource === 'messaging'
216
- ? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.'
217
- : '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
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.',
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.',
218
233
  ];
219
234
 
235
+ if (adequacy.intensity !== 'none') {
236
+ lines.push(
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.`,
238
+ );
239
+ }
240
+ if (adequacy.structurallyReady === false) {
241
+ lines.push(
242
+ `- No source evidence is ready for review (${adequacy.reason}). Use "continue" unless the latest draft is a real external blocker.`,
243
+ );
244
+ }
245
+
246
+ if (triggerSource === 'messaging' && messagingSent) {
247
+ lines.push('- A final reply was already delivered via send_message. Use "complete" unless concrete task work remains.');
248
+ } else if (triggerSource === 'messaging') {
249
+ lines.push('- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked.');
250
+ } else {
251
+ lines.push('- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.');
252
+ }
253
+
220
254
  lines.push(
221
255
  goalContext.effectiveGoal ? `Goal: ${goalContext.effectiveGoal}` : '',
222
256
  goalContext.persistedGoalPrompt,
@@ -226,19 +260,215 @@ function buildCompletionDecisionPrompt({
226
260
  : '',
227
261
  `Current iteration: ${iteration} of ${maxIterations}.`,
228
262
  `Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
229
- `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
+ : '',
267
+ adequacy.intensity !== 'none'
268
+ ? `Research preflight: intensity=${adequacy.intensity}; structurally_ready=${adequacy.structurallyReady}; semantic_review_required=true; reason=${adequacy.reason}`
269
+ : '',
270
+ adequacy.targets.length
271
+ ? `Research targets: ${adequacy.targets.join('; ')}`
272
+ : '',
273
+ formatResearchAdequacyGuidance(adequacy),
230
274
  `Latest draft reply:\n${draftReply || '(empty)'}`,
231
275
  );
232
276
  return lines.filter(Boolean).join('\n');
233
277
  }
234
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
+
235
324
  function normalizeCompletionDecision(raw, fallbackStatus = 'continue') {
236
325
  const allowed = new Set(['continue', 'complete', 'blocked']);
237
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();
238
334
  return {
239
335
  status: allowed.has(requestedStatus) ? requestedStatus : fallbackStatus,
240
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
+ ),
356
+ };
357
+ }
358
+
359
+ function validateResearchReview(review, researchAdequacy, toolExecutions = []) {
360
+ if (!researchAdequacy || researchAdequacy.intensity === 'none') {
361
+ return { valid: true, reason: '' };
362
+ }
363
+ if (researchAdequacy.structurallyReady === false) {
364
+ return {
365
+ valid: false,
366
+ reason: researchAdequacy.reason || 'No successful source evidence is available.',
367
+ };
368
+ }
369
+ if (!review?.required || review.adequate !== true) {
370
+ return {
371
+ valid: false,
372
+ reason: 'The completion judge did not confirm adequate research evidence.',
373
+ };
374
+ }
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
+ );
241
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).
436
+ const researchAdequacy = options.researchAdequacy
437
+ || assessResearchAdequacy({
438
+ analysis: options.analysis || null,
439
+ goalContext: options.goalContext || null,
440
+ toolExecutions: options.toolExecutions || [],
441
+ });
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;
465
+ return {
466
+ status: 'continue',
467
+ reason: reviewValidation.reason,
468
+ research_review: decision.research_review,
469
+ };
470
+ }
471
+ return decision;
242
472
  }
243
473
 
244
474
  // Intentionally lightweight (200-token cap, self-contained) so the model can
@@ -249,11 +479,18 @@ function buildChurnAssessmentPrompt({
249
479
  goalContext,
250
480
  toolExecutions,
251
481
  iteration,
482
+ analysis = null,
483
+ researchAdequacy = null,
252
484
  }) {
485
+ const adequacy = researchAdequacy || assessResearchAdequacy({
486
+ analysis,
487
+ goalContext,
488
+ toolExecutions,
489
+ });
253
490
  const lines = [
254
491
  'Return JSON only.',
255
492
  'Self-assess your current loop state — are you making genuine progress or spinning?',
256
- '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}',
257
494
  '',
258
495
  `Context: ${readOnlyCount} consecutive iteration(s) with only read/search/inspect operations — no concrete state changes yet.`,
259
496
  alreadyRead ? `Already inspected: ${alreadyRead}.` : '',
@@ -263,21 +500,63 @@ function buildChurnAssessmentPrompt({
263
500
  : '',
264
501
  `Iteration: ${iteration}`,
265
502
  `Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 6) || 'none'}`,
503
+ adequacy.intensity !== 'none'
504
+ ? `Research preflight: intensity=${adequacy.intensity}; evidence_candidates=${adequacy.evidenceCandidateCount}; semantic target coverage must be judged from the evidence summaries.`
505
+ : '',
506
+ adequacy.targets.length
507
+ ? `Requested research targets: ${adequacy.targets.join('; ')}.`
508
+ : '',
266
509
  '',
267
510
  'Assessment rules:',
268
511
  '- "progressing": You are systematically gathering necessary context and the next concrete action is already determined — you know exactly what to do next.',
269
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.',
270
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.',
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.',
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
+ : '',
271
520
  ];
272
521
  return lines.filter(Boolean).join('\n');
273
522
  }
274
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
+
275
542
  function normalizeChurnAssessment(raw) {
276
543
  const allowed = new Set(['progressing', 'churn', 'blocked']);
277
544
  const assessment = String(raw?.assessment || '').trim().toLowerCase();
545
+ const blockerKind = String(raw?.blocker_kind || raw?.blockerKind || '').trim().toLowerCase();
278
546
  return {
279
547
  assessment: allowed.has(assessment) ? assessment : 'churn',
280
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,
281
560
  };
282
561
  }
283
562
 
@@ -285,11 +564,15 @@ module.exports = {
285
564
  buildChurnAssessmentPrompt,
286
565
  buildCompletionDecisionPrompt,
287
566
  buildGoalContractPrompt,
567
+ enforceTerminalReplyDecision,
288
568
  goalContractFromAnalysis,
289
569
  goalContractFromPlan,
290
570
  mergeGoalContracts,
571
+ enforceChurnAssessment,
291
572
  normalizeChurnAssessment,
292
573
  normalizeCompletionDecision,
574
+ normalizeResearchReview,
293
575
  normalizeGoalContract,
294
576
  resolveRunGoalContext,
577
+ validateResearchReview,
295
578
  };