neoagent 3.2.1-beta.10 → 3.2.1-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/flutter_app/lib/main_controller.dart +46 -4
- package/flutter_app/lib/main_models.dart +4 -2
- package/flutter_app/lib/main_operations.dart +0 -49
- package/flutter_app/lib/main_settings.dart +338 -0
- package/flutter_app/lib/src/backend_client.dart +19 -0
- package/package.json +1 -1
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65635 -65088
- package/server/routes/behavior.js +80 -0
- package/server/routes/settings.js +4 -0
- package/server/services/ai/history.js +1 -1
- package/server/services/ai/loop/agent_engine_core.js +97 -16
- package/server/services/ai/loop/completion_judge.js +211 -44
- package/server/services/ai/loop/conversation_loop.js +6 -11
- package/server/services/ai/loop/messaging_delivery.js +47 -0
- package/server/services/ai/messagingFallback.js +2 -2
- package/server/services/ai/model_failure_cache.js +7 -0
- package/server/services/ai/systemPrompt.js +30 -128
- package/server/services/ai/taskAnalysis.js +47 -2
- package/server/services/ai/toolEvidence.js +65 -159
- package/server/services/ai/tools.js +60 -8
- package/server/services/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +172 -0
- package/server/services/behavior/modules/persona_prompt.js +238 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +118 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +324 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/memory/manager.js +14 -33
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +158 -27
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +2 -4
- package/server/services/messaging/http_platforms.js +4 -3
- package/server/services/messaging/inbound_queue.js +74 -13
- package/server/services/messaging/inbound_store.js +33 -0
- package/server/services/messaging/manager.js +18 -0
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/whatsapp.js +11 -0
- package/server/services/social_reach/channels/social_video.js +1 -1
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +194 -29
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/runtime.js +2 -2
- package/server/utils/logger.js +19 -0
- package/server/services/ai/terminal_reply.js +0 -18
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const { requireAuth } = require('../middleware/auth');
|
|
5
|
+
const {
|
|
6
|
+
MODULE_IDS,
|
|
7
|
+
cloneDefaults,
|
|
8
|
+
getBehaviorConfig,
|
|
9
|
+
setBehaviorConfig,
|
|
10
|
+
resolveBehaviorConfig,
|
|
11
|
+
} = require('../services/behavior');
|
|
12
|
+
const {
|
|
13
|
+
getAgentIdFromRequest,
|
|
14
|
+
resolveAgentId,
|
|
15
|
+
} = require('../services/agents/manager');
|
|
16
|
+
const { invalidateSystemPromptCache } = require('../services/ai/systemPrompt');
|
|
17
|
+
|
|
18
|
+
const router = express.Router();
|
|
19
|
+
router.use(requireAuth);
|
|
20
|
+
|
|
21
|
+
function requestAgentId(req) {
|
|
22
|
+
return resolveAgentId(req.session.userId, getAgentIdFromRequest(req));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
router.get('/', (req, res) => {
|
|
26
|
+
const userId = req.session.userId;
|
|
27
|
+
const agentId = requestAgentId(req);
|
|
28
|
+
res.json({
|
|
29
|
+
agentId,
|
|
30
|
+
modules: MODULE_IDS,
|
|
31
|
+
defaults: cloneDefaults(),
|
|
32
|
+
config: getBehaviorConfig(userId, agentId),
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
router.put('/', (req, res) => {
|
|
37
|
+
if (!req.body?.config || typeof req.body.config !== 'object' || Array.isArray(req.body.config)) {
|
|
38
|
+
return res.status(400).json({ error: 'config must be an object' });
|
|
39
|
+
}
|
|
40
|
+
const userId = req.session.userId;
|
|
41
|
+
const agentId = requestAgentId(req);
|
|
42
|
+
const config = setBehaviorConfig(userId, agentId, req.body.config);
|
|
43
|
+
invalidateSystemPromptCache(userId, agentId);
|
|
44
|
+
return res.json({ agentId, config });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
router.get('/effective', (req, res) => {
|
|
48
|
+
const userId = req.session.userId;
|
|
49
|
+
const agentId = requestAgentId(req);
|
|
50
|
+
const platform = String(req.query.platform || '').trim();
|
|
51
|
+
const chatId = String(req.query.chatId || '').trim();
|
|
52
|
+
if (!platform || !chatId) {
|
|
53
|
+
return res.status(400).json({ error: 'platform and chatId are required' });
|
|
54
|
+
}
|
|
55
|
+
return res.json({
|
|
56
|
+
agentId,
|
|
57
|
+
config: resolveBehaviorConfig(userId, agentId, {
|
|
58
|
+
platform,
|
|
59
|
+
chatId,
|
|
60
|
+
isGroup: req.query.isGroup !== 'false',
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
router.get('/diagnostics', (req, res) => {
|
|
66
|
+
const userId = req.session.userId;
|
|
67
|
+
const agentId = requestAgentId(req);
|
|
68
|
+
const platform = String(req.query.platform || '').trim();
|
|
69
|
+
const chatId = String(req.query.chatId || '').trim();
|
|
70
|
+
if (!platform || !chatId) {
|
|
71
|
+
return res.status(400).json({ error: 'platform and chatId are required' });
|
|
72
|
+
}
|
|
73
|
+
const pipeline = req.app?.locals?.behaviorPipeline;
|
|
74
|
+
if (!pipeline?.getDiagnostics) {
|
|
75
|
+
return res.status(503).json({ error: 'Behavior runtime is not initialized' });
|
|
76
|
+
}
|
|
77
|
+
return res.json(pipeline.getDiagnostics(userId, agentId, platform, chatId));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
module.exports = router;
|
|
@@ -346,6 +346,10 @@ router.put('/', async (req, res) => {
|
|
|
346
346
|
});
|
|
347
347
|
|
|
348
348
|
tx(Object.entries(normalizedBody));
|
|
349
|
+
if (Object.prototype.hasOwnProperty.call(normalizedBody, 'assistant_behavior_notes')) {
|
|
350
|
+
const { invalidateSystemPromptCache } = require('../services/ai/systemPrompt');
|
|
351
|
+
invalidateSystemPromptCache(userId, agentId);
|
|
352
|
+
}
|
|
349
353
|
|
|
350
354
|
if (Object.keys(normalizedBody).some((key) => VOICE_SETTING_KEYS.has(key))) {
|
|
351
355
|
const manager = req.app?.locals?.messagingManager;
|
|
@@ -126,7 +126,7 @@ async function summarizeMessages(
|
|
|
126
126
|
const prompt = [
|
|
127
127
|
{
|
|
128
128
|
role: 'system',
|
|
129
|
-
content: 'Compress conversation context. Preserve user goals, constraints, preferences, decisions, promised follow-ups, recurring schedules, important facts, tool outcomes, and
|
|
129
|
+
content: 'Compress conversation context. Preserve user goals, constraints, preferences, decisions, promised follow-ups, recurring schedules, important facts, tool outcomes, unresolved issues, and any user-stated communication preferences. Keep concrete details (names, dates, times, statuses) and avoid vague wording. Output plain text only.'
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
role: 'user',
|
|
@@ -20,6 +20,10 @@ const { summarizeCapabilityHealth } = require('../capabilityHealth');
|
|
|
20
20
|
const { shouldAcceptTaskComplete } = require('../completion');
|
|
21
21
|
const { shortenRunId, summarizeForLog } = require('../logFormat');
|
|
22
22
|
const { getProviderForUser } = require('../provider_selector');
|
|
23
|
+
const {
|
|
24
|
+
recordModelFailure,
|
|
25
|
+
recordModelSuccess,
|
|
26
|
+
} = require('../model_failure_cache');
|
|
23
27
|
const { runConversation } = require('./conversation_loop');
|
|
24
28
|
const {
|
|
25
29
|
TERMINAL_STATUSES,
|
|
@@ -638,6 +642,82 @@ class AgentEngine {
|
|
|
638
642
|
});
|
|
639
643
|
}
|
|
640
644
|
|
|
645
|
+
async inferStructured({
|
|
646
|
+
userId,
|
|
647
|
+
agentId = null,
|
|
648
|
+
modelId = null,
|
|
649
|
+
purpose = 'fast',
|
|
650
|
+
system,
|
|
651
|
+
prompt,
|
|
652
|
+
maxTokens = 400,
|
|
653
|
+
fallback = {},
|
|
654
|
+
signal = null,
|
|
655
|
+
}) {
|
|
656
|
+
const { getFailureFallbackModelId } = require('./conversation_loop');
|
|
657
|
+
const configuredFallbackId = getAiSettings(userId, agentId).fallback_model_id;
|
|
658
|
+
const failedModelIds = new Set();
|
|
659
|
+
let requestedModelId = modelId;
|
|
660
|
+
let selected = null;
|
|
661
|
+
let result = null;
|
|
662
|
+
|
|
663
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
664
|
+
selected = await getProviderForUser(
|
|
665
|
+
userId,
|
|
666
|
+
prompt,
|
|
667
|
+
false,
|
|
668
|
+
requestedModelId,
|
|
669
|
+
{
|
|
670
|
+
agentId,
|
|
671
|
+
signal,
|
|
672
|
+
selectionHint: {
|
|
673
|
+
purpose: purpose === 'general' ? 'general' : 'fast',
|
|
674
|
+
costMode: purpose === 'fast' ? 'economy' : 'balanced_auto',
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
);
|
|
678
|
+
try {
|
|
679
|
+
result = await this.requestStructuredJson({
|
|
680
|
+
provider: selected.provider,
|
|
681
|
+
providerName: selected.providerName,
|
|
682
|
+
model: selected.model,
|
|
683
|
+
messages: system ? [{ role: 'system', content: String(system) }] : [],
|
|
684
|
+
prompt: String(prompt || ''),
|
|
685
|
+
maxTokens,
|
|
686
|
+
normalize: (value) => value,
|
|
687
|
+
fallback,
|
|
688
|
+
telemetry: { userId, agentId, signal },
|
|
689
|
+
phase: 'behavior_inference',
|
|
690
|
+
});
|
|
691
|
+
recordModelSuccess(userId, agentId, selected.modelSelectionId);
|
|
692
|
+
break;
|
|
693
|
+
} catch (error) {
|
|
694
|
+
if (isAbortError(error, signal) || signal?.aborted) throw error;
|
|
695
|
+
recordModelFailure(userId, agentId, selected.modelSelectionId, error);
|
|
696
|
+
failedModelIds.add(selected.modelSelectionId);
|
|
697
|
+
if (attempt >= 2) throw error;
|
|
698
|
+
requestedModelId = await getFailureFallbackModelId(
|
|
699
|
+
userId,
|
|
700
|
+
agentId,
|
|
701
|
+
selected.modelSelectionId,
|
|
702
|
+
configuredFallbackId,
|
|
703
|
+
error,
|
|
704
|
+
signal,
|
|
705
|
+
failedModelIds,
|
|
706
|
+
);
|
|
707
|
+
if (!requestedModelId) throw error;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
parsed: result.value,
|
|
713
|
+
raw: result.raw,
|
|
714
|
+
usage: result.usage,
|
|
715
|
+
model: selected.model,
|
|
716
|
+
modelSelectionId: selected.modelSelectionId,
|
|
717
|
+
providerName: selected.providerName,
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
641
721
|
async requestModelResponse({
|
|
642
722
|
provider,
|
|
643
723
|
providerName,
|
|
@@ -973,6 +1053,15 @@ class AgentEngine {
|
|
|
973
1053
|
content: [
|
|
974
1054
|
'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
|
|
975
1055
|
'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
|
|
1056
|
+
options.memoryScope?.scopeType === 'channel'
|
|
1057
|
+
? [
|
|
1058
|
+
'This is a shared room. Extract only durable room or participant facts that are safe and useful in this room.',
|
|
1059
|
+
'Do not infer or store facts about the authenticated owner from another participant’s message.',
|
|
1060
|
+
options.context?.socialIntelligence?.message?.sender
|
|
1061
|
+
? `For facts about the current speaker, use the canonical subject "${options.source}:${options.context.socialIntelligence.message.sender}".`
|
|
1062
|
+
: '',
|
|
1063
|
+
].filter(Boolean).join(' ')
|
|
1064
|
+
: '',
|
|
976
1065
|
buildMemoryConsolidationInstructions(new Date().toISOString()),
|
|
977
1066
|
'Schema:',
|
|
978
1067
|
JSON.stringify({
|
|
@@ -1049,6 +1138,14 @@ class AgentEngine {
|
|
|
1049
1138
|
agentId: options.agentId || null,
|
|
1050
1139
|
conversationId,
|
|
1051
1140
|
runId,
|
|
1141
|
+
scope: options.memoryScope || undefined,
|
|
1142
|
+
metadata: options.memoryScope
|
|
1143
|
+
? {
|
|
1144
|
+
trustLevel: 'shared_room_conversation',
|
|
1145
|
+
platform: options.source || null,
|
|
1146
|
+
chatId: options.chatId || null,
|
|
1147
|
+
}
|
|
1148
|
+
: undefined,
|
|
1052
1149
|
signal: options.signal,
|
|
1053
1150
|
},
|
|
1054
1151
|
);
|
|
@@ -1245,22 +1342,6 @@ class AgentEngine {
|
|
|
1245
1342
|
return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
|
|
1246
1343
|
}
|
|
1247
1344
|
|
|
1248
|
-
shouldFastCompleteVoiceReply({
|
|
1249
|
-
options = {},
|
|
1250
|
-
toolExecutions = [],
|
|
1251
|
-
failedStepCount = 0,
|
|
1252
|
-
messagingSent = false,
|
|
1253
|
-
lastReply = '',
|
|
1254
|
-
}) {
|
|
1255
|
-
// Voice fast-path is structural only: short reply, no tools/failures.
|
|
1256
|
-
// Content terminality is left to the model completion judge.
|
|
1257
|
-
return options.latencyProfile === 'voice'
|
|
1258
|
-
&& toolExecutions.length === 0
|
|
1259
|
-
&& failedStepCount === 0
|
|
1260
|
-
&& !messagingSent
|
|
1261
|
-
&& Boolean(String(lastReply || '').trim());
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
1345
|
getMessagingRetryLimit(maxIterations) {
|
|
1265
1346
|
// Cap at 3: more than 3 autonomous messaging retries indicates a structural
|
|
1266
1347
|
// problem (model unavailable, bad config) that more retries won't solve.
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
assessResearchAdequacy,
|
|
7
7
|
formatResearchAdequacyGuidance,
|
|
8
8
|
summarizeAvailableTools,
|
|
9
|
+
summarizeResearchEvidenceCatalog,
|
|
9
10
|
summarizeToolExecutions,
|
|
10
11
|
} = require('../toolEvidence');
|
|
11
12
|
|
|
@@ -208,7 +209,7 @@ function buildCompletionDecisionPrompt({
|
|
|
208
209
|
const lines = [
|
|
209
210
|
'Return JSON only.',
|
|
210
211
|
'Decide whether this run should continue autonomously or stop now.',
|
|
211
|
-
'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"]}}',
|
|
212
213
|
'Rules:',
|
|
213
214
|
'- Use "continue" whenever any safe next step remains in this same run.',
|
|
214
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.',
|
|
@@ -219,8 +220,12 @@ function buildCompletionDecisionPrompt({
|
|
|
219
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.',
|
|
220
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.',
|
|
221
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.`,
|
|
222
|
-
'- When research intensity is light or deep,
|
|
223
|
+
'- When research intensity is light or deep, fill research_review from the numbered evidence entries. Do not mark complete unless every requested target has concrete support.',
|
|
223
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".',
|
|
224
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.',
|
|
225
230
|
'- A polished-sounding answer is not complete if key requested targets still lack direct evidence.',
|
|
226
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.',
|
|
@@ -229,12 +234,12 @@ function buildCompletionDecisionPrompt({
|
|
|
229
234
|
|
|
230
235
|
if (adequacy.intensity !== 'none') {
|
|
231
236
|
lines.push(
|
|
232
|
-
`- Research intensity for this run is ${adequacy.intensity}.
|
|
237
|
+
`- Research intensity for this run is ${adequacy.intensity}. Successful evidence candidates=${adequacy.evidenceCandidateCount}; unique calls=${adequacy.uniqueEvidenceCandidateCount}. Semantic target coverage has not been precomputed.`,
|
|
233
238
|
);
|
|
234
239
|
}
|
|
235
|
-
if (adequacy.
|
|
240
|
+
if (adequacy.structurallyReady === false) {
|
|
236
241
|
lines.push(
|
|
237
|
-
`-
|
|
242
|
+
`- No source evidence is ready for review (${adequacy.reason}). Use "continue" unless the latest draft is a real external blocker.`,
|
|
238
243
|
);
|
|
239
244
|
}
|
|
240
245
|
|
|
@@ -255,31 +260,176 @@ function buildCompletionDecisionPrompt({
|
|
|
255
260
|
: '',
|
|
256
261
|
`Current iteration: ${iteration} of ${maxIterations}.`,
|
|
257
262
|
`Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
|
|
258
|
-
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions,
|
|
263
|
+
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 12) || 'none'}`,
|
|
259
264
|
adequacy.intensity !== 'none'
|
|
260
|
-
? `Research
|
|
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}`
|
|
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
|
|
|
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
|
+
);
|
|
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
|
+
|
|
283
433
|
function enforceTerminalReplyDecision(decision, lastReply, options = {}) {
|
|
284
434
|
// Natural-language terminality is judged by the model completion decision.
|
|
285
435
|
// Runtime only enforces structural evidence contracts (research adequacy).
|
|
@@ -289,25 +439,33 @@ function enforceTerminalReplyDecision(decision, lastReply, options = {}) {
|
|
|
289
439
|
goalContext: options.goalContext || null,
|
|
290
440
|
toolExecutions: options.toolExecutions || [],
|
|
291
441
|
});
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
&& researchAdequacy.adequate === false
|
|
295
|
-
&& (decision?.status === 'complete' || decision?.status === 'blocked')
|
|
296
|
-
) {
|
|
297
|
-
// Allow true blocked outcomes only when the model already chose blocked AND
|
|
298
|
-
// research cannot progress further (no remaining targets/primary gap that
|
|
299
|
-
// tools could still cover). Otherwise force continue for evidence gathering.
|
|
442
|
+
if (decision?.status === 'blocked') {
|
|
443
|
+
const blockerReview = decision.blocker_review;
|
|
300
444
|
if (
|
|
301
|
-
|
|
302
|
-
&&
|
|
303
|
-
&&
|
|
445
|
+
blockerReview
|
|
446
|
+
&& blockerReview.kind !== 'none'
|
|
447
|
+
&& blockerReview.resolvable_in_run === false
|
|
448
|
+
&& blockerReview.required_value
|
|
304
449
|
) {
|
|
305
450
|
return decision;
|
|
306
451
|
}
|
|
307
452
|
return {
|
|
453
|
+
...decision,
|
|
308
454
|
status: 'continue',
|
|
309
|
-
reason:
|
|
310
|
-
|
|
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,
|
|
311
469
|
};
|
|
312
470
|
}
|
|
313
471
|
return decision;
|
|
@@ -332,7 +490,7 @@ function buildChurnAssessmentPrompt({
|
|
|
332
490
|
const lines = [
|
|
333
491
|
'Return JSON only.',
|
|
334
492
|
'Self-assess your current loop state — are you making genuine progress or spinning?',
|
|
335
|
-
'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}',
|
|
336
494
|
'',
|
|
337
495
|
`Context: ${readOnlyCount} consecutive iteration(s) with only read/search/inspect operations — no concrete state changes yet.`,
|
|
338
496
|
alreadyRead ? `Already inspected: ${alreadyRead}.` : '',
|
|
@@ -343,44 +501,39 @@ function buildChurnAssessmentPrompt({
|
|
|
343
501
|
`Iteration: ${iteration}`,
|
|
344
502
|
`Recent tool evidence:\n${summarizeToolExecutions(toolExecutions, 6) || 'none'}`,
|
|
345
503
|
adequacy.intensity !== 'none'
|
|
346
|
-
? `Research
|
|
504
|
+
? `Research preflight: intensity=${adequacy.intensity}; evidence_candidates=${adequacy.evidenceCandidateCount}; semantic target coverage must be judged from the evidence summaries.`
|
|
347
505
|
: '',
|
|
348
|
-
adequacy.
|
|
349
|
-
? `
|
|
506
|
+
adequacy.targets.length
|
|
507
|
+
? `Requested research targets: ${adequacy.targets.join('; ')}.`
|
|
350
508
|
: '',
|
|
351
509
|
'',
|
|
352
510
|
'Assessment rules:',
|
|
353
511
|
'- "progressing": You are systematically gathering necessary context and the next concrete action is already determined — you know exactly what to do next.',
|
|
354
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.',
|
|
355
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.',
|
|
356
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.',
|
|
357
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".',
|
|
358
|
-
adequacy.
|
|
359
|
-
? '-
|
|
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.'
|
|
360
519
|
: '',
|
|
361
520
|
];
|
|
362
521
|
return lines.filter(Boolean).join('\n');
|
|
363
522
|
}
|
|
364
523
|
|
|
365
|
-
function enforceChurnAssessment(assessment
|
|
524
|
+
function enforceChurnAssessment(assessment) {
|
|
366
525
|
const normalized = normalizeChurnAssessment(assessment);
|
|
367
|
-
const researchAdequacy = options.researchAdequacy
|
|
368
|
-
|| assessResearchAdequacy({
|
|
369
|
-
analysis: options.analysis || null,
|
|
370
|
-
goalContext: options.goalContext || null,
|
|
371
|
-
toolExecutions: options.toolExecutions || [],
|
|
372
|
-
});
|
|
373
526
|
if (
|
|
374
|
-
|
|
375
|
-
&&
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
527
|
+
normalized.assessment === 'blocked'
|
|
528
|
+
&& (
|
|
529
|
+
normalized.blocker_kind === 'none'
|
|
530
|
+
|| normalized.resolvable_in_run !== false
|
|
531
|
+
)
|
|
379
532
|
) {
|
|
380
533
|
return {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
534
|
+
...normalized,
|
|
535
|
+
assessment: 'churn',
|
|
536
|
+
reason: 'The churn assessment did not identify a concrete external blocker.',
|
|
384
537
|
};
|
|
385
538
|
}
|
|
386
539
|
return normalized;
|
|
@@ -389,9 +542,21 @@ function enforceChurnAssessment(assessment, options = {}) {
|
|
|
389
542
|
function normalizeChurnAssessment(raw) {
|
|
390
543
|
const allowed = new Set(['progressing', 'churn', 'blocked']);
|
|
391
544
|
const assessment = String(raw?.assessment || '').trim().toLowerCase();
|
|
545
|
+
const blockerKind = String(raw?.blocker_kind || raw?.blockerKind || '').trim().toLowerCase();
|
|
392
546
|
return {
|
|
393
547
|
assessment: allowed.has(assessment) ? assessment : 'churn',
|
|
394
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,
|
|
395
560
|
};
|
|
396
561
|
}
|
|
397
562
|
|
|
@@ -406,6 +571,8 @@ module.exports = {
|
|
|
406
571
|
enforceChurnAssessment,
|
|
407
572
|
normalizeChurnAssessment,
|
|
408
573
|
normalizeCompletionDecision,
|
|
574
|
+
normalizeResearchReview,
|
|
409
575
|
normalizeGoalContract,
|
|
410
576
|
resolveRunGoalContext,
|
|
577
|
+
validateResearchReview,
|
|
411
578
|
};
|