neoagent 3.2.1-beta.10 → 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/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 +64 -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/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 +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/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
|
@@ -200,41 +200,6 @@ function isSubstantiveProgressToolName(toolName = '') {
|
|
|
200
200
|
return true;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
const PRIMARY_RESEARCH_SOURCES = new Set([
|
|
204
|
-
'browser',
|
|
205
|
-
'http',
|
|
206
|
-
'integration',
|
|
207
|
-
'files',
|
|
208
|
-
'command',
|
|
209
|
-
'mcp',
|
|
210
|
-
'android',
|
|
211
|
-
'data',
|
|
212
|
-
'vision',
|
|
213
|
-
'skills',
|
|
214
|
-
'subagent',
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
const SECONDARY_RESEARCH_SOURCES = new Set([
|
|
218
|
-
'search',
|
|
219
|
-
'memory',
|
|
220
|
-
]);
|
|
221
|
-
|
|
222
|
-
// External/source-backed tools only. Local file/edit/shell work must not
|
|
223
|
-
// inherit a research burden just because inspection tools are available.
|
|
224
|
-
const RESEARCH_TOOL_HINTS = new Set([
|
|
225
|
-
'web_search',
|
|
226
|
-
'http_request',
|
|
227
|
-
'browser_navigate',
|
|
228
|
-
'browser_open',
|
|
229
|
-
'browser_click',
|
|
230
|
-
'browser_type',
|
|
231
|
-
'browser_snapshot',
|
|
232
|
-
'browser_evaluate',
|
|
233
|
-
'analyze_image',
|
|
234
|
-
'spawn_subagent',
|
|
235
|
-
'delegate_to_agent',
|
|
236
|
-
]);
|
|
237
|
-
|
|
238
203
|
function clampResearchText(value, maxChars = 220) {
|
|
239
204
|
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
240
205
|
if (!text) return '';
|
|
@@ -266,18 +231,20 @@ function collectResearchTargets(analysis = null, goalContext = null) {
|
|
|
266
231
|
], { limit: 8 });
|
|
267
232
|
}
|
|
268
233
|
|
|
269
|
-
function hasResearchToolHint(analysis = null) {
|
|
270
|
-
const tools = Array.isArray(analysis?.suggested_tools) ? analysis.suggested_tools : [];
|
|
271
|
-
return tools.some((name) => {
|
|
272
|
-
const tool = String(name || '').trim().toLowerCase();
|
|
273
|
-
if (!tool) return false;
|
|
274
|
-
if (RESEARCH_TOOL_HINTS.has(tool)) return true;
|
|
275
|
-
// Tool-schema identity only: family prefixes, not free-text phrase matching.
|
|
276
|
-
return tool.startsWith('browser_') || tool.startsWith('mcp_');
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
234
|
function resolveResearchIntensity(analysis = null, goalContext = null) {
|
|
235
|
+
const declaredDepth = String(
|
|
236
|
+
analysis?.research_depth
|
|
237
|
+
|| analysis?.researchDepth
|
|
238
|
+
|| '',
|
|
239
|
+
).trim().toLowerCase();
|
|
240
|
+
const declaredTargets = collectResearchTargets(analysis, goalContext);
|
|
241
|
+
if (
|
|
242
|
+
['none', 'light', 'deep'].includes(declaredDepth)
|
|
243
|
+
&& !(declaredDepth === 'none' && declaredTargets.length > 0)
|
|
244
|
+
) {
|
|
245
|
+
return declaredDepth;
|
|
246
|
+
}
|
|
247
|
+
|
|
281
248
|
const mode = String(analysis?.mode || '').trim().toLowerCase();
|
|
282
249
|
const complexity = String(
|
|
283
250
|
goalContext?.effectiveComplexity
|
|
@@ -297,21 +264,16 @@ function resolveResearchIntensity(analysis = null, goalContext = null) {
|
|
|
297
264
|
const verificationNeed = String(analysis?.verification_need || '').trim().toLowerCase();
|
|
298
265
|
const freshnessRisk = String(analysis?.freshness_risk || '').trim().toLowerCase();
|
|
299
266
|
const planningDepth = String(analysis?.planning_depth || '').trim().toLowerCase();
|
|
300
|
-
const targets =
|
|
301
|
-
const researchToolHint = hasResearchToolHint(analysis);
|
|
267
|
+
const targets = declaredTargets;
|
|
302
268
|
|
|
303
269
|
if (mode === 'direct_answer' && verificationNeed === 'none' && freshnessRisk === 'none') {
|
|
304
270
|
return 'none';
|
|
305
271
|
}
|
|
306
272
|
|
|
307
|
-
// External/source-backed work only. Pure local implementation tasks must not
|
|
308
|
-
// inherit a research burden just because they are complex or multi-step.
|
|
309
273
|
const needsExternalEvidence = (
|
|
310
274
|
targets.length > 0
|
|
311
|
-
|| researchToolHint
|
|
312
275
|
|| freshnessRisk === 'possible'
|
|
313
276
|
|| freshnessRisk === 'high'
|
|
314
|
-
|| verificationNeed === 'required'
|
|
315
277
|
);
|
|
316
278
|
if (!needsExternalEvidence) {
|
|
317
279
|
return 'none';
|
|
@@ -342,32 +304,32 @@ function isSuccessfulResearchExecution(item = {}) {
|
|
|
342
304
|
return Boolean(item.evidenceRelevant || item.dependsOnOutput || item.stateChanged);
|
|
343
305
|
}
|
|
344
306
|
|
|
345
|
-
function
|
|
346
|
-
|
|
307
|
+
function selectResearchEvidenceCandidates(toolExecutions = [], maxItems = 80) {
|
|
308
|
+
const candidates = (Array.isArray(toolExecutions) ? toolExecutions : [])
|
|
309
|
+
.map((execution, index) => ({ execution, evidenceIndex: index + 1 }))
|
|
310
|
+
.filter(({ execution }) => isSuccessfulResearchExecution(execution));
|
|
311
|
+
if (candidates.length <= maxItems) return candidates;
|
|
312
|
+
|
|
313
|
+
const firstCount = Math.floor(maxItems / 4);
|
|
314
|
+
return [
|
|
315
|
+
...candidates.slice(0, firstCount),
|
|
316
|
+
...candidates.slice(-(maxItems - firstCount)),
|
|
317
|
+
];
|
|
347
318
|
}
|
|
348
319
|
|
|
349
|
-
function
|
|
350
|
-
return
|
|
320
|
+
function summarizeResearchEvidenceCatalog(toolExecutions = [], maxItems = 80) {
|
|
321
|
+
return selectResearchEvidenceCandidates(toolExecutions, maxItems)
|
|
322
|
+
.map(({ execution, evidenceIndex }) => (
|
|
323
|
+
`E${evidenceIndex}. ${execution.toolName} [${execution.evidenceSource || 'tool'}] :: ${clampRunContext(execution.summary || '', 160)}`
|
|
324
|
+
))
|
|
325
|
+
.join('\n');
|
|
351
326
|
}
|
|
352
327
|
|
|
353
|
-
function
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
execution.toolName,
|
|
359
|
-
execution.evidenceSource,
|
|
360
|
-
JSON.stringify(execution.input || {}),
|
|
361
|
-
].join(' ').toLowerCase();
|
|
362
|
-
if (haystack.includes(needle)) return true;
|
|
363
|
-
|
|
364
|
-
const tokens = needle
|
|
365
|
-
.split(/[^a-z0-9+]+/i)
|
|
366
|
-
.map((token) => token.trim())
|
|
367
|
-
.filter((token) => token.length >= 3);
|
|
368
|
-
if (tokens.length === 0) return false;
|
|
369
|
-
const matched = tokens.filter((token) => haystack.includes(token)).length;
|
|
370
|
-
return matched >= Math.min(2, tokens.length);
|
|
328
|
+
function researchExecutionSignature(execution = {}) {
|
|
329
|
+
return JSON.stringify([
|
|
330
|
+
String(execution.toolName || '').trim(),
|
|
331
|
+
execution.input && typeof execution.input === 'object' ? execution.input : {},
|
|
332
|
+
]);
|
|
371
333
|
}
|
|
372
334
|
|
|
373
335
|
function assessResearchAdequacy({
|
|
@@ -380,111 +342,53 @@ function assessResearchAdequacy({
|
|
|
380
342
|
|
|
381
343
|
const successful = (Array.isArray(toolExecutions) ? toolExecutions : [])
|
|
382
344
|
.filter(isSuccessfulResearchExecution);
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
));
|
|
391
|
-
const uncoveredTargets = targets.filter((target) => !coveredTargets.includes(target));
|
|
392
|
-
const primaryUncoveredTargets = targets.filter((target) => !primaryCoveredTargets.includes(target));
|
|
393
|
-
|
|
394
|
-
const requiredPrimarySources = intensity === 'deep'
|
|
395
|
-
? (targets.length > 0 ? Math.min(4, targets.length) : 2)
|
|
396
|
-
: intensity === 'light'
|
|
397
|
-
? 1
|
|
398
|
-
: 0;
|
|
399
|
-
const requiredSecondarySources = intensity === 'deep' ? 1 : 0;
|
|
400
|
-
const requiredTargetCoverage = intensity === 'deep'
|
|
401
|
-
? targets.length
|
|
402
|
-
: intensity === 'light'
|
|
403
|
-
? Math.min(targets.length, 1)
|
|
404
|
-
: 0;
|
|
405
|
-
const requiredPrimaryTargetCoverage = intensity === 'deep'
|
|
406
|
-
? targets.length
|
|
407
|
-
: 0;
|
|
408
|
-
|
|
409
|
-
const missing = [];
|
|
410
|
-
if (primary.length < requiredPrimarySources) {
|
|
411
|
-
missing.push(
|
|
412
|
-
`Need ${requiredPrimarySources} primary source open/fetch/inspect step(s); have ${primary.length}.`,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
if (secondary.length < requiredSecondarySources && primary.length < requiredPrimarySources) {
|
|
416
|
-
missing.push('Need at least one search lead before finishing deep research.');
|
|
417
|
-
}
|
|
418
|
-
if (targets.length > 0 && coveredTargets.length < requiredTargetCoverage) {
|
|
419
|
-
missing.push(
|
|
420
|
-
`Need evidence for: ${uncoveredTargets.slice(0, 4).join('; ') || targets.slice(0, 4).join('; ')}.`,
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
if (targets.length > 0 && primaryCoveredTargets.length < requiredPrimaryTargetCoverage) {
|
|
424
|
-
missing.push(
|
|
425
|
-
`Need primary-source evidence for: ${primaryUncoveredTargets.slice(0, 4).join('; ') || targets.slice(0, 4).join('; ')}.`,
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
if (intensity === 'deep' && primary.length === 0 && secondary.length > 0) {
|
|
429
|
-
missing.push('Search snippets alone are not enough; open primary sources for the key claims.');
|
|
430
|
-
}
|
|
431
|
-
if (intensity === 'light' && primary.length === 0 && secondary.length === 0) {
|
|
432
|
-
missing.push('Need at least one successful search or primary-source check before finishing.');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const adequate = intensity === 'none' ? true : missing.length === 0;
|
|
345
|
+
const uniqueEvidenceCandidateCount = new Set(
|
|
346
|
+
successful.map((item) => researchExecutionSignature(item)),
|
|
347
|
+
).size;
|
|
348
|
+
const structurallyReady = intensity === 'none' || uniqueEvidenceCandidateCount > 0;
|
|
349
|
+
const missing = structurallyReady
|
|
350
|
+
? []
|
|
351
|
+
: ['No successful source-bearing tool evidence is available for semantic review.'];
|
|
436
352
|
const nextActions = [];
|
|
437
|
-
if (!
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
);
|
|
442
|
-
} else if (uncoveredTargets.length > 0) {
|
|
443
|
-
nextActions.push(
|
|
444
|
-
`Research each remaining target separately: ${uncoveredTargets.slice(0, 4).join('; ')}.`,
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
if (primary.length < requiredPrimarySources) {
|
|
448
|
-
nextActions.push('Open or fetch primary pages/docs for the remaining claims instead of guessing from memory or snippets.');
|
|
449
|
-
}
|
|
450
|
-
if (secondary.length === 0 && intensity === 'deep') {
|
|
451
|
-
nextActions.push('Run targeted searches, then open the strongest sources.');
|
|
452
|
-
}
|
|
353
|
+
if (!structurallyReady) {
|
|
354
|
+
nextActions.push('Gather source-backed evidence before requesting completion.');
|
|
355
|
+
} else if (intensity !== 'none') {
|
|
356
|
+
nextActions.push('Have the completion judge map each requested target to concrete evidence entries and assess source quality.');
|
|
453
357
|
}
|
|
454
358
|
|
|
455
359
|
return {
|
|
456
360
|
intensity,
|
|
457
|
-
adequate,
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
primarySourceCount: primary.length,
|
|
463
|
-
secondarySourceCount: secondary.length,
|
|
361
|
+
adequate: structurallyReady,
|
|
362
|
+
structurallyReady,
|
|
363
|
+
semanticReviewRequired: intensity !== 'none',
|
|
364
|
+
evidenceCandidateCount: successful.length,
|
|
365
|
+
uniqueEvidenceCandidateCount,
|
|
464
366
|
targets,
|
|
465
|
-
coveredTargets,
|
|
466
|
-
primaryCoveredTargets,
|
|
467
|
-
uncoveredTargets,
|
|
468
|
-
primaryUncoveredTargets,
|
|
367
|
+
coveredTargets: [],
|
|
368
|
+
primaryCoveredTargets: [],
|
|
369
|
+
uncoveredTargets: intensity === 'none' ? [] : targets,
|
|
370
|
+
primaryUncoveredTargets: intensity === 'none' ? [] : targets,
|
|
469
371
|
missing,
|
|
470
372
|
nextActions,
|
|
471
|
-
reason:
|
|
373
|
+
reason: structurallyReady
|
|
472
374
|
? (intensity === 'none'
|
|
473
375
|
? 'No research burden for this run.'
|
|
474
|
-
: '
|
|
376
|
+
: 'Source candidates exist; the AI completion judge must still validate target coverage and source quality.')
|
|
475
377
|
: clampResearchText(missing.join(' ') || 'Research evidence is still incomplete.', 320),
|
|
476
378
|
};
|
|
477
379
|
}
|
|
478
380
|
|
|
479
381
|
function formatResearchAdequacyGuidance(assessment = null) {
|
|
480
|
-
if (!assessment || assessment.
|
|
382
|
+
if (!assessment || assessment.intensity === 'none') return '';
|
|
481
383
|
const lines = [
|
|
482
|
-
|
|
384
|
+
assessment.structurallyReady
|
|
385
|
+
? 'Research self-check: source candidates exist, but semantic target coverage is not inferred from tool names, arguments, or token overlap.'
|
|
386
|
+
: 'Research self-check: no successful source evidence is available yet.',
|
|
483
387
|
assessment.reason ? `Gap: ${assessment.reason}` : '',
|
|
484
388
|
assessment.nextActions?.length
|
|
485
389
|
? `Next safe steps:\n- ${assessment.nextActions.join('\n- ')}`
|
|
486
390
|
: '',
|
|
487
|
-
'
|
|
391
|
+
'The completion judge must explicitly map every requested target to supporting evidence and judge whether that evidence is primary, secondary, or merely contextual.',
|
|
488
392
|
];
|
|
489
393
|
return lines.filter(Boolean).join('\n');
|
|
490
394
|
}
|
|
@@ -569,6 +473,8 @@ module.exports = {
|
|
|
569
473
|
isSubstantiveProgressEvidence,
|
|
570
474
|
isSubstantiveProgressToolName,
|
|
571
475
|
resolveResearchIntensity,
|
|
476
|
+
selectResearchEvidenceCandidates,
|
|
477
|
+
summarizeResearchEvidenceCatalog,
|
|
572
478
|
summarizeProgressToolExecutions,
|
|
573
479
|
summarizeToolExecutions,
|
|
574
480
|
summarizeAvailableTools,
|
|
@@ -2400,6 +2400,13 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2400
2400
|
if (!engine || !runId) {
|
|
2401
2401
|
return { error: 'Interim updates require an active run.' };
|
|
2402
2402
|
}
|
|
2403
|
+
if (engine.getRunMeta(runId)?.messagingContext?.behavior?.isGroup === true) {
|
|
2404
|
+
return {
|
|
2405
|
+
sent: false,
|
|
2406
|
+
skipped: true,
|
|
2407
|
+
reason: 'Interim progress updates are suppressed in shared rooms.',
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2403
2410
|
const interimContent = typeof args.content === 'string' ? args.content : '';
|
|
2404
2411
|
const expectsReply = args.expects_reply === true;
|
|
2405
2412
|
const deferFollowUp = args.defer_follow_up === true;
|
|
@@ -2495,13 +2502,58 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2495
2502
|
if (triggerSource === 'messaging' && originDelivery) {
|
|
2496
2503
|
await engine?.stopMessagingProgressSupervisor?.(runId);
|
|
2497
2504
|
}
|
|
2498
|
-
const
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
+
const behavior = runState?.messagingContext?.behavior;
|
|
2506
|
+
const behaviorPipeline = app?.locals?.behaviorPipeline;
|
|
2507
|
+
let deliveredContent = normalizedMessage;
|
|
2508
|
+
let sendResult;
|
|
2509
|
+
if (
|
|
2510
|
+
originDelivery
|
|
2511
|
+
&& behavior
|
|
2512
|
+
&& behavior.enabled !== false
|
|
2513
|
+
&& behaviorPipeline
|
|
2514
|
+
&& typeof behaviorPipeline.refineAndMaybeDeliver === 'function'
|
|
2515
|
+
) {
|
|
2516
|
+
const behaviorResult = await behaviorPipeline.refineAndMaybeDeliver({
|
|
2517
|
+
userId,
|
|
2518
|
+
agentId,
|
|
2519
|
+
msg: behavior.message,
|
|
2520
|
+
config: behavior.config,
|
|
2521
|
+
draft: normalizedMessage,
|
|
2522
|
+
messagingManager: manager,
|
|
2523
|
+
runId,
|
|
2524
|
+
signal,
|
|
2525
|
+
mediaPath: args.media_path,
|
|
2526
|
+
turnEpoch: behavior.turnEpoch,
|
|
2527
|
+
deliver: true,
|
|
2528
|
+
});
|
|
2529
|
+
deliveredContent = behaviorResult.content;
|
|
2530
|
+
if (behaviorResult.delivered) {
|
|
2531
|
+
sendResult = { success: true, behavior: true, result: behaviorResult.delivery };
|
|
2532
|
+
} else if (behaviorResult.suppressed) {
|
|
2533
|
+
markProactiveNoResponse({ runState, deliveryState });
|
|
2534
|
+
sendResult = {
|
|
2535
|
+
success: true,
|
|
2536
|
+
sent: false,
|
|
2537
|
+
suppressed: true,
|
|
2538
|
+
reason: behaviorResult.reasonCodes?.[0] || 'behavior_suppressed',
|
|
2539
|
+
};
|
|
2540
|
+
} else {
|
|
2541
|
+
sendResult = {
|
|
2542
|
+
success: false,
|
|
2543
|
+
error: behaviorResult.delivery?.error
|
|
2544
|
+
|| behaviorResult.delivery?.reason
|
|
2545
|
+
|| 'Behavior delivery was not confirmed.',
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
} else {
|
|
2549
|
+
sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
|
|
2550
|
+
agentId,
|
|
2551
|
+
mediaPath: args.media_path,
|
|
2552
|
+
runId,
|
|
2553
|
+
persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks',
|
|
2554
|
+
signal,
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2505
2557
|
// Track that the agent explicitly sent a message during this run
|
|
2506
2558
|
if (
|
|
2507
2559
|
!suppressReply
|
|
@@ -2509,7 +2561,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
2509
2561
|
&& sendResult?.suppressed !== true
|
|
2510
2562
|
&& originDelivery
|
|
2511
2563
|
) {
|
|
2512
|
-
markProactiveMessageSent({ runState, deliveryState, content:
|
|
2564
|
+
markProactiveMessageSent({ runState, deliveryState, content: deliveredContent });
|
|
2513
2565
|
if (runState && triggerSource === 'messaging') {
|
|
2514
2566
|
runState.explicitMessageSent = true;
|
|
2515
2567
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
const { isMainAgent } = require('../agents/manager');
|
|
5
|
+
const { MODULE_IDS, cloneDefaults, DEFAULT_MODULE_CONFIG } = require('./defaults');
|
|
6
|
+
|
|
7
|
+
const SETTINGS_KEY = 'behavior_modules_config';
|
|
8
|
+
const PARTICIPATION_MODES = new Set(['automatic', 'mention_only', 'always']);
|
|
9
|
+
const MODEL_PURPOSES = new Set(['fast', 'general']);
|
|
10
|
+
const DELIVERY_STYLES = new Set(['single', 'natural_bubbles']);
|
|
11
|
+
|
|
12
|
+
function clampNumber(value, min, max, fallback) {
|
|
13
|
+
const number = Number(value);
|
|
14
|
+
if (!Number.isFinite(number)) return fallback;
|
|
15
|
+
return Math.min(max, Math.max(min, number));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asObject(value) {
|
|
19
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeModules(rawModules, fallbackEnabled = true, sparse = false) {
|
|
23
|
+
const source = asObject(rawModules);
|
|
24
|
+
const modules = {};
|
|
25
|
+
for (const id of MODULE_IDS) {
|
|
26
|
+
if (sparse && !Object.prototype.hasOwnProperty.call(source, id)) continue;
|
|
27
|
+
const entry = asObject(source[id]);
|
|
28
|
+
modules[id] = {
|
|
29
|
+
enabled: entry.enabled == null ? fallbackEnabled : entry.enabled !== false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return modules;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeOptionalString(value, fallback = null) {
|
|
36
|
+
if (value == null) return fallback;
|
|
37
|
+
const normalized = String(value).trim();
|
|
38
|
+
return normalized || null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeLeafConfig(raw = {}, base = cloneDefaults(), sparse = false) {
|
|
42
|
+
const input = asObject(raw);
|
|
43
|
+
const output = {};
|
|
44
|
+
const assign = (key, value) => {
|
|
45
|
+
if (!sparse || Object.prototype.hasOwnProperty.call(input, key)) output[key] = value;
|
|
46
|
+
};
|
|
47
|
+
const enabled = input.enabled == null ? base.enabled !== false : input.enabled !== false;
|
|
48
|
+
assign('enabled', enabled);
|
|
49
|
+
if (!sparse || Object.prototype.hasOwnProperty.call(input, 'modules')) {
|
|
50
|
+
output.modules = normalizeModules(input.modules, enabled, sparse);
|
|
51
|
+
}
|
|
52
|
+
assign(
|
|
53
|
+
'participationMode',
|
|
54
|
+
PARTICIPATION_MODES.has(String(input.participationMode || ''))
|
|
55
|
+
? String(input.participationMode)
|
|
56
|
+
: (base.participationMode || DEFAULT_MODULE_CONFIG.participationMode),
|
|
57
|
+
);
|
|
58
|
+
assign('minimumNeedScore', clampNumber(
|
|
59
|
+
input.minimumNeedScore,
|
|
60
|
+
0,
|
|
61
|
+
1,
|
|
62
|
+
base.minimumNeedScore ?? DEFAULT_MODULE_CONFIG.minimumNeedScore,
|
|
63
|
+
));
|
|
64
|
+
assign('batchWindowMs', Math.round(clampNumber(
|
|
65
|
+
input.batchWindowMs,
|
|
66
|
+
0,
|
|
67
|
+
5000,
|
|
68
|
+
base.batchWindowMs ?? DEFAULT_MODULE_CONFIG.batchWindowMs,
|
|
69
|
+
)));
|
|
70
|
+
assign('decisionContextMessageLimit', Math.round(clampNumber(
|
|
71
|
+
input.decisionContextMessageLimit,
|
|
72
|
+
4,
|
|
73
|
+
30,
|
|
74
|
+
base.decisionContextMessageLimit ?? DEFAULT_MODULE_CONFIG.decisionContextMessageLimit,
|
|
75
|
+
)));
|
|
76
|
+
assign(
|
|
77
|
+
'decisionModelId',
|
|
78
|
+
normalizeOptionalString(input.decisionModelId, base.decisionModelId || null),
|
|
79
|
+
);
|
|
80
|
+
assign(
|
|
81
|
+
'decisionModelPurpose',
|
|
82
|
+
MODEL_PURPOSES.has(String(input.decisionModelPurpose || ''))
|
|
83
|
+
? String(input.decisionModelPurpose)
|
|
84
|
+
: (base.decisionModelPurpose || DEFAULT_MODULE_CONFIG.decisionModelPurpose),
|
|
85
|
+
);
|
|
86
|
+
assign(
|
|
87
|
+
'deliveryStyle',
|
|
88
|
+
DELIVERY_STYLES.has(String(input.deliveryStyle || ''))
|
|
89
|
+
? String(input.deliveryStyle)
|
|
90
|
+
: (base.deliveryStyle || DEFAULT_MODULE_CONFIG.deliveryStyle),
|
|
91
|
+
);
|
|
92
|
+
assign('maxBubbles', Math.round(clampNumber(
|
|
93
|
+
input.maxBubbles,
|
|
94
|
+
1,
|
|
95
|
+
5,
|
|
96
|
+
base.maxBubbles ?? DEFAULT_MODULE_CONFIG.maxBubbles,
|
|
97
|
+
)));
|
|
98
|
+
assign('bubbleGapMs', Math.round(clampNumber(
|
|
99
|
+
input.bubbleGapMs,
|
|
100
|
+
0,
|
|
101
|
+
5000,
|
|
102
|
+
base.bubbleGapMs ?? DEFAULT_MODULE_CONFIG.bubbleGapMs,
|
|
103
|
+
)));
|
|
104
|
+
assign('normsRefreshMessageGap', Math.round(clampNumber(
|
|
105
|
+
input.normsRefreshMessageGap,
|
|
106
|
+
3,
|
|
107
|
+
200,
|
|
108
|
+
base.normsRefreshMessageGap ?? DEFAULT_MODULE_CONFIG.normsRefreshMessageGap,
|
|
109
|
+
)));
|
|
110
|
+
assign('observabilityIntervalMinutes', Math.round(clampNumber(
|
|
111
|
+
input.observabilityIntervalMinutes,
|
|
112
|
+
30,
|
|
113
|
+
10080,
|
|
114
|
+
base.observabilityIntervalMinutes ?? DEFAULT_MODULE_CONFIG.observabilityIntervalMinutes,
|
|
115
|
+
)));
|
|
116
|
+
return output;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeStoredConfig(raw) {
|
|
120
|
+
const base = cloneDefaults();
|
|
121
|
+
const input = asObject(raw);
|
|
122
|
+
const normalized = {
|
|
123
|
+
schemaVersion: DEFAULT_MODULE_CONFIG.schemaVersion,
|
|
124
|
+
...normalizeLeafConfig(input, base),
|
|
125
|
+
};
|
|
126
|
+
const platformOverrides = {};
|
|
127
|
+
for (const [platform, value] of Object.entries(asObject(input.platformOverrides))) {
|
|
128
|
+
const key = String(platform || '').trim();
|
|
129
|
+
if (!key) continue;
|
|
130
|
+
platformOverrides[key] = normalizeLeafConfig(value, normalized, true);
|
|
131
|
+
}
|
|
132
|
+
const roomOverrides = {};
|
|
133
|
+
const rawRoomOverrides = Object.keys(asObject(input.roomOverrides)).length
|
|
134
|
+
? asObject(input.roomOverrides)
|
|
135
|
+
: asObject(input.groupOverrides);
|
|
136
|
+
for (const [roomKey, value] of Object.entries(rawRoomOverrides)) {
|
|
137
|
+
const key = String(roomKey || '').trim();
|
|
138
|
+
if (!key) continue;
|
|
139
|
+
roomOverrides[key] = normalizeLeafConfig(value, normalized, true);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...normalized,
|
|
143
|
+
platformOverrides,
|
|
144
|
+
roomOverrides,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readSettingRow(userId, agentId) {
|
|
149
|
+
const row = db.prepare(
|
|
150
|
+
'SELECT value FROM agent_settings WHERE user_id = ? AND agent_id = ? AND key = ?',
|
|
151
|
+
).get(userId, agentId, SETTINGS_KEY);
|
|
152
|
+
if (row?.value != null) return row.value;
|
|
153
|
+
if (isMainAgent(userId, agentId)) {
|
|
154
|
+
const legacy = db.prepare(
|
|
155
|
+
'SELECT value FROM user_settings WHERE user_id = ? AND key = ?',
|
|
156
|
+
).get(userId, SETTINGS_KEY);
|
|
157
|
+
return legacy?.value;
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseStoredValue(value) {
|
|
163
|
+
if (value == null || value === '') return cloneDefaults();
|
|
164
|
+
if (typeof value === 'object') return normalizeStoredConfig(value);
|
|
165
|
+
try {
|
|
166
|
+
return normalizeStoredConfig(JSON.parse(value));
|
|
167
|
+
} catch {
|
|
168
|
+
return cloneDefaults();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getBehaviorConfig(userId, agentId = null) {
|
|
173
|
+
return parseStoredValue(readSettingRow(userId, agentId));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function setBehaviorConfig(userId, agentId, config) {
|
|
177
|
+
const normalized = normalizeStoredConfig(config);
|
|
178
|
+
db.prepare(
|
|
179
|
+
`INSERT INTO agent_settings (user_id, agent_id, key, value)
|
|
180
|
+
VALUES (?, ?, ?, ?)
|
|
181
|
+
ON CONFLICT(user_id, agent_id, key) DO UPDATE SET value = excluded.value`,
|
|
182
|
+
).run(userId, agentId, SETTINGS_KEY, JSON.stringify(normalized));
|
|
183
|
+
return normalized;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function roomConfigKey(platform, chatId) {
|
|
187
|
+
return `${String(platform || '').trim()}::${String(chatId || '').trim()}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function mergeConfigs(base, override) {
|
|
191
|
+
if (!override) return base;
|
|
192
|
+
return {
|
|
193
|
+
...base,
|
|
194
|
+
...override,
|
|
195
|
+
modules: {
|
|
196
|
+
...asObject(base.modules),
|
|
197
|
+
...asObject(override.modules),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolveBehaviorConfig(userId, agentId, { platform = null, chatId = null, isGroup = false } = {}) {
|
|
203
|
+
const storedValue = readSettingRow(userId, agentId);
|
|
204
|
+
const stored = parseStoredValue(storedValue);
|
|
205
|
+
let effective = normalizeLeafConfig(stored, cloneDefaults());
|
|
206
|
+
let participationModeSource = storedValue == null ? 'default' : 'agent';
|
|
207
|
+
const platformKey = String(platform || '').trim();
|
|
208
|
+
if (platformKey && stored.platformOverrides?.[platformKey]) {
|
|
209
|
+
effective = mergeConfigs(effective, stored.platformOverrides[platformKey]);
|
|
210
|
+
if (Object.prototype.hasOwnProperty.call(stored.platformOverrides[platformKey], 'participationMode')) {
|
|
211
|
+
participationModeSource = 'platform';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (isGroup && platformKey && chatId) {
|
|
215
|
+
const key = roomConfigKey(platformKey, chatId);
|
|
216
|
+
if (stored.roomOverrides?.[key]) {
|
|
217
|
+
effective = mergeConfigs(effective, stored.roomOverrides[key]);
|
|
218
|
+
if (Object.prototype.hasOwnProperty.call(stored.roomOverrides[key], 'participationMode')) {
|
|
219
|
+
participationModeSource = 'room';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
effective.modules = normalizeModules(effective.modules, effective.enabled !== false);
|
|
224
|
+
effective.platformOverrides = stored.platformOverrides || {};
|
|
225
|
+
effective.roomOverrides = stored.roomOverrides || {};
|
|
226
|
+
effective.participationModeSource = participationModeSource;
|
|
227
|
+
effective.scope = {
|
|
228
|
+
platform: platformKey || null,
|
|
229
|
+
chatId: chatId ? String(chatId) : null,
|
|
230
|
+
isGroup: Boolean(isGroup),
|
|
231
|
+
roomKey: isGroup && platformKey && chatId ? roomConfigKey(platformKey, chatId) : null,
|
|
232
|
+
};
|
|
233
|
+
return effective;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isModuleEnabled(config, moduleId) {
|
|
237
|
+
if (!config || config.enabled === false) return false;
|
|
238
|
+
const entry = config.modules?.[moduleId];
|
|
239
|
+
if (!entry) return false;
|
|
240
|
+
return entry.enabled !== false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = {
|
|
244
|
+
SETTINGS_KEY,
|
|
245
|
+
getBehaviorConfig,
|
|
246
|
+
setBehaviorConfig,
|
|
247
|
+
resolveBehaviorConfig,
|
|
248
|
+
normalizeStoredConfig,
|
|
249
|
+
roomConfigKey,
|
|
250
|
+
isModuleEnabled,
|
|
251
|
+
};
|