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.
- package/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +12 -6
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +20 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +452 -176
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +20 -45
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- package/server/services/ai/providers/google.js +135 -97
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- 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 +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -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 +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -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/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +159 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +110 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -14
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +13 -8
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/logger.js +19 -0
- 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
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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,
|
|
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
|
};
|