neoagent 3.2.1-beta.9 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/flutter_app/lib/main_app_shell.dart +178 -34
  2. package/flutter_app/lib/main_chat.dart +542 -80
  3. package/flutter_app/lib/main_controller.dart +59 -4
  4. package/flutter_app/lib/main_models.dart +171 -22
  5. package/flutter_app/lib/main_operations.dart +0 -49
  6. package/flutter_app/lib/main_settings.dart +338 -0
  7. package/flutter_app/lib/src/backend_client.dart +19 -0
  8. package/package.json +1 -1
  9. package/server/db/database.js +2 -2
  10. package/server/http/routes.js +1 -0
  11. package/server/public/.last_build_id +1 -1
  12. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  13. package/server/public/flutter_bootstrap.js +1 -1
  14. package/server/public/main.dart.js +85079 -83904
  15. package/server/routes/behavior.js +80 -0
  16. package/server/routes/settings.js +4 -0
  17. package/server/services/agents/manager.js +1 -1
  18. package/server/services/ai/history.js +1 -1
  19. package/server/services/ai/loop/agent_engine_core.js +110 -17
  20. package/server/services/ai/loop/blank_recovery.js +5 -4
  21. package/server/services/ai/loop/completion_judge.js +226 -33
  22. package/server/services/ai/loop/conversation_loop.js +92 -34
  23. package/server/services/ai/loop/messaging_delivery.js +47 -0
  24. package/server/services/ai/loop/progress_classification.js +2 -0
  25. package/server/services/ai/loopPolicy.js +24 -2
  26. package/server/services/ai/messagingFallback.js +17 -17
  27. package/server/services/ai/model_failure_cache.js +7 -0
  28. package/server/services/ai/systemPrompt.js +31 -122
  29. package/server/services/ai/taskAnalysis.js +86 -12
  30. package/server/services/ai/toolEvidence.js +68 -224
  31. package/server/services/ai/tools.js +60 -19
  32. package/server/services/behavior/config.js +251 -0
  33. package/server/services/behavior/defaults.js +68 -0
  34. package/server/services/behavior/delivery.js +176 -0
  35. package/server/services/behavior/index.js +43 -0
  36. package/server/services/behavior/model_client.js +35 -0
  37. package/server/services/behavior/modules/agent_identity.js +37 -0
  38. package/server/services/behavior/modules/channel_style.js +28 -0
  39. package/server/services/behavior/modules/index.js +29 -0
  40. package/server/services/behavior/modules/norms.js +101 -0
  41. package/server/services/behavior/modules/persona.js +172 -0
  42. package/server/services/behavior/modules/persona_prompt.js +238 -0
  43. package/server/services/behavior/modules/social_memory.js +86 -0
  44. package/server/services/behavior/modules/social_observability.js +94 -0
  45. package/server/services/behavior/modules/social_signals.js +41 -0
  46. package/server/services/behavior/modules/theory_of_mind.js +118 -0
  47. package/server/services/behavior/modules/turn_taking.js +238 -0
  48. package/server/services/behavior/pipeline.js +341 -0
  49. package/server/services/behavior/registry.js +78 -0
  50. package/server/services/behavior/signals.js +107 -0
  51. package/server/services/behavior/state.js +99 -0
  52. package/server/services/behavior/system_prompt.js +75 -0
  53. package/server/services/memory/manager.js +14 -33
  54. package/server/services/messaging/access_policy.js +269 -74
  55. package/server/services/messaging/automation.js +158 -27
  56. package/server/services/messaging/discord.js +11 -1
  57. package/server/services/messaging/formatting_guides.js +5 -4
  58. package/server/services/messaging/http_platforms.js +73 -28
  59. package/server/services/messaging/inbound_queue.js +74 -13
  60. package/server/services/messaging/inbound_store.js +33 -0
  61. package/server/services/messaging/manager.js +57 -5
  62. package/server/services/messaging/telegram.js +10 -1
  63. package/server/services/messaging/whatsapp.js +11 -0
  64. package/server/services/social_reach/channels/social_video.js +1 -1
  65. package/server/services/social_video/captions.js +2 -2
  66. package/server/services/social_video/service.js +194 -29
  67. package/server/services/voice/message.js +1 -1
  68. package/server/services/voice/runtime.js +2 -2
  69. package/server/utils/logger.js +19 -0
  70. package/server/services/ai/terminal_reply.js +0 -57
@@ -12,7 +12,6 @@ const {
12
12
  normalizeOutgoingMessageForPlatform,
13
13
  } = require('../messaging/formatting_guides');
14
14
  const { INTERIM_KINDS, normalizeInterimKind } = require('./interim');
15
- const { isDeferredWorkReply } = require('./terminal_reply');
16
15
  const { normalizeWhatsAppId } = require('../../utils/whatsapp');
17
16
  const {
18
17
  executeIntegratedTool,
@@ -2401,6 +2400,13 @@ async function executeTool(toolName, args, context, engine) {
2401
2400
  if (!engine || !runId) {
2402
2401
  return { error: 'Interim updates require an active run.' };
2403
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
+ }
2404
2410
  const interimContent = typeof args.content === 'string' ? args.content : '';
2405
2411
  const expectsReply = args.expects_reply === true;
2406
2412
  const deferFollowUp = args.defer_follow_up === true;
@@ -2438,16 +2444,6 @@ async function executeTool(toolName, args, context, engine) {
2438
2444
  platform: args.platform,
2439
2445
  to: args.to,
2440
2446
  });
2441
- if (
2442
- !suppressReply
2443
- && triggerSource === 'messaging'
2444
- && originDelivery
2445
- && isDeferredWorkReply(normalizedMessage)
2446
- ) {
2447
- return {
2448
- error: 'send_message cannot end the run with a promise or progress-only reply. Continue the work, or use send_interim_update for a factual interim update.',
2449
- };
2450
- }
2451
2447
  if (isProactiveTrigger(triggerSource)) {
2452
2448
  const proactiveValidation = validateProactiveSendMessageArgs({
2453
2449
  purpose: args.purpose,
@@ -2506,13 +2502,58 @@ async function executeTool(toolName, args, context, engine) {
2506
2502
  if (triggerSource === 'messaging' && originDelivery) {
2507
2503
  await engine?.stopMessagingProgressSupervisor?.(runId);
2508
2504
  }
2509
- const sendResult = await manager.sendMessage(userId, args.platform, args.to, args.content, {
2510
- agentId,
2511
- mediaPath: args.media_path,
2512
- runId,
2513
- persistConversation: triggerSource === 'schedule' || triggerSource === 'tasks',
2514
- signal,
2515
- });
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
+ }
2516
2557
  // Track that the agent explicitly sent a message during this run
2517
2558
  if (
2518
2559
  !suppressReply
@@ -2520,7 +2561,7 @@ async function executeTool(toolName, args, context, engine) {
2520
2561
  && sendResult?.suppressed !== true
2521
2562
  && originDelivery
2522
2563
  ) {
2523
- markProactiveMessageSent({ runState, deliveryState, content: normalizedMessage });
2564
+ markProactiveMessageSent({ runState, deliveryState, content: deliveredContent });
2524
2565
  if (runState && triggerSource === 'messaging') {
2525
2566
  runState.explicitMessageSent = true;
2526
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
+ };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const MODULE_IDS = Object.freeze([
4
+ 'turn_taking',
5
+ 'social_memory',
6
+ 'norms',
7
+ 'persona',
8
+ 'agent_identity',
9
+ 'channel_style',
10
+ 'theory_of_mind',
11
+ 'social_signals',
12
+ 'social_observability',
13
+ 'delivery',
14
+ ]);
15
+
16
+ const DEFAULT_MODULE_CONFIG = Object.freeze({
17
+ schemaVersion: 1,
18
+ enabled: true,
19
+ participationMode: 'automatic',
20
+ minimumNeedScore: 0.72,
21
+ batchWindowMs: 900,
22
+ decisionContextMessageLimit: 12,
23
+ decisionModelId: null,
24
+ decisionModelPurpose: 'general',
25
+ deliveryStyle: 'natural_bubbles',
26
+ maxBubbles: 4,
27
+ bubbleGapMs: 650,
28
+ normsRefreshMessageGap: 18,
29
+ observabilityIntervalMinutes: 360,
30
+ });
31
+
32
+ function cloneDefaults() {
33
+ return {
34
+ schemaVersion: DEFAULT_MODULE_CONFIG.schemaVersion,
35
+ enabled: DEFAULT_MODULE_CONFIG.enabled,
36
+ modules: {
37
+ turn_taking: { enabled: true },
38
+ social_memory: { enabled: true },
39
+ norms: { enabled: true },
40
+ persona: { enabled: true },
41
+ agent_identity: { enabled: true },
42
+ channel_style: { enabled: true },
43
+ theory_of_mind: { enabled: true },
44
+ social_signals: { enabled: true },
45
+ social_observability: { enabled: true },
46
+ delivery: { enabled: true },
47
+ },
48
+ participationMode: DEFAULT_MODULE_CONFIG.participationMode,
49
+ minimumNeedScore: DEFAULT_MODULE_CONFIG.minimumNeedScore,
50
+ batchWindowMs: DEFAULT_MODULE_CONFIG.batchWindowMs,
51
+ decisionContextMessageLimit: DEFAULT_MODULE_CONFIG.decisionContextMessageLimit,
52
+ decisionModelId: DEFAULT_MODULE_CONFIG.decisionModelId,
53
+ decisionModelPurpose: DEFAULT_MODULE_CONFIG.decisionModelPurpose,
54
+ deliveryStyle: DEFAULT_MODULE_CONFIG.deliveryStyle,
55
+ maxBubbles: DEFAULT_MODULE_CONFIG.maxBubbles,
56
+ bubbleGapMs: DEFAULT_MODULE_CONFIG.bubbleGapMs,
57
+ normsRefreshMessageGap: DEFAULT_MODULE_CONFIG.normsRefreshMessageGap,
58
+ observabilityIntervalMinutes: DEFAULT_MODULE_CONFIG.observabilityIntervalMinutes,
59
+ platformOverrides: {},
60
+ roomOverrides: {},
61
+ };
62
+ }
63
+
64
+ module.exports = {
65
+ MODULE_IDS,
66
+ DEFAULT_MODULE_CONFIG,
67
+ cloneDefaults,
68
+ };
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ splitOutgoingMessageForPlatform,
5
+ normalizeOutgoingMessageForPlatform,
6
+ } = require('../messaging/formatting_guides');
7
+
8
+ function sleep(ms, signal = null) {
9
+ const delay = Math.max(0, Number(ms) || 0);
10
+ if (!delay) return Promise.resolve();
11
+ return new Promise((resolve, reject) => {
12
+ const timer = setTimeout(() => {
13
+ cleanup();
14
+ resolve();
15
+ }, delay);
16
+ const onAbort = () => {
17
+ cleanup();
18
+ const error = new Error('Delivery aborted.');
19
+ error.name = 'AbortError';
20
+ reject(error);
21
+ };
22
+ const cleanup = () => {
23
+ clearTimeout(timer);
24
+ if (signal) signal.removeEventListener('abort', onAbort);
25
+ };
26
+ if (signal) {
27
+ if (signal.aborted) {
28
+ cleanup();
29
+ const error = new Error('Delivery aborted.');
30
+ error.name = 'AbortError';
31
+ reject(error);
32
+ return;
33
+ }
34
+ signal.addEventListener('abort', onAbort, { once: true });
35
+ }
36
+ });
37
+ }
38
+
39
+ function splitIntoNaturalBubbles(platform, content, { maxBubbles = 4 } = {}) {
40
+ const normalized = normalizeOutgoingMessageForPlatform(platform, content, {
41
+ stripNoResponseMarker: false,
42
+ });
43
+ if (!normalized) return [];
44
+ if (normalized.toUpperCase() === '[NO RESPONSE]') return [normalized];
45
+
46
+ const paragraphChunks = splitOutgoingMessageForPlatform(platform, normalized);
47
+ const bubbles = [];
48
+ for (const chunk of paragraphChunks) {
49
+ const bubble = String(chunk).trim();
50
+ if (bubble) bubbles.push(bubble);
51
+ }
52
+
53
+ const limited = [];
54
+ for (const bubble of bubbles) {
55
+ if (!bubble) continue;
56
+ if (limited.length < maxBubbles) {
57
+ limited.push(bubble);
58
+ } else {
59
+ limited[limited.length - 1] = `${limited[limited.length - 1]} ${bubble}`.trim();
60
+ }
61
+ }
62
+ return limited.length ? limited : [normalized];
63
+ }
64
+
65
+ async function deliverSocialReply({
66
+ messagingManager,
67
+ userId,
68
+ agentId,
69
+ platform,
70
+ chatId,
71
+ content,
72
+ config,
73
+ runId = null,
74
+ signal = null,
75
+ mediaPath = null,
76
+ turnEpoch = null,
77
+ beforeBubble = null,
78
+ }) {
79
+ const style = config?.deliveryStyle === 'single' ? 'single' : 'natural_bubbles';
80
+ if (style === 'single' || mediaPath) {
81
+ if (beforeBubble && !(await beforeBubble(0))) {
82
+ return { success: false, suppressed: true, reason: 'stale_turn', turnEpoch };
83
+ }
84
+ return messagingManager.sendMessage(userId, platform, chatId, content, {
85
+ agentId,
86
+ runId,
87
+ mediaPath,
88
+ signal,
89
+ deliveryKind: 'final',
90
+ idempotencyKey: runId ? `${runId}:social:0` : undefined,
91
+ });
92
+ }
93
+
94
+ const bubbles = splitIntoNaturalBubbles(platform, content, {
95
+ maxBubbles: config?.maxBubbles ?? 4,
96
+ });
97
+ if (bubbles.length <= 1) {
98
+ if (beforeBubble && !(await beforeBubble(0))) {
99
+ return { success: false, suppressed: true, reason: 'stale_turn', turnEpoch };
100
+ }
101
+ return messagingManager.sendMessage(userId, platform, chatId, bubbles[0] || content, {
102
+ agentId,
103
+ runId,
104
+ signal,
105
+ deliveryKind: 'final',
106
+ idempotencyKey: runId ? `${runId}:social:0` : undefined,
107
+ });
108
+ }
109
+
110
+ try {
111
+ let lastResult = null;
112
+ for (let index = 0; index < bubbles.length; index += 1) {
113
+ if (signal?.aborted) {
114
+ const error = new Error('Delivery aborted.');
115
+ error.name = 'AbortError';
116
+ throw error;
117
+ }
118
+ if (index > 0) {
119
+ await sleep(config?.bubbleGapMs ?? 650, signal);
120
+ }
121
+ if (beforeBubble && !(await beforeBubble(index))) {
122
+ return {
123
+ success: false,
124
+ suppressed: true,
125
+ reason: 'stale_turn',
126
+ turnEpoch,
127
+ deliveredBubbles: index,
128
+ };
129
+ }
130
+ try {
131
+ await messagingManager.sendTyping?.(userId, platform, chatId, true, { agentId, runId, signal });
132
+ } catch {
133
+ // typing is best-effort
134
+ }
135
+ lastResult = await messagingManager.sendMessage(userId, platform, chatId, bubbles[index], {
136
+ agentId,
137
+ runId,
138
+ signal,
139
+ idempotencyKey: runId ? `${runId}:social:${index}` : undefined,
140
+ deliveryKind: index === bubbles.length - 1 ? 'final' : 'partial',
141
+ metadata: {
142
+ socialDelivery: true,
143
+ bubbleIndex: index,
144
+ bubbleCount: bubbles.length,
145
+ },
146
+ });
147
+ if (lastResult?.success === false || lastResult?.suppressed === true) {
148
+ return {
149
+ success: false,
150
+ error: lastResult?.error || lastResult?.reason || 'Messaging platform rejected delivery.',
151
+ result: lastResult,
152
+ deliveredBubbles: index,
153
+ };
154
+ }
155
+ }
156
+ return {
157
+ success: true,
158
+ bubbled: true,
159
+ bubbleCount: bubbles.length,
160
+ result: lastResult,
161
+ };
162
+ } finally {
163
+ try {
164
+ await messagingManager.sendTyping?.(userId, platform, chatId, false, { agentId, runId, signal });
165
+ } catch {
166
+ // typing is best-effort
167
+ }
168
+ }
169
+ }
170
+
171
+ module.exports = {
172
+ id: 'delivery',
173
+ deliver: deliverSocialReply,
174
+ splitIntoNaturalBubbles,
175
+ deliverSocialReply,
176
+ };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ getBehaviorConfig,
5
+ setBehaviorConfig,
6
+ resolveBehaviorConfig,
7
+ normalizeStoredConfig,
8
+ roomConfigKey,
9
+ isModuleEnabled,
10
+ SETTINGS_KEY,
11
+ } = require('./config');
12
+ const { MODULE_IDS, cloneDefaults, DEFAULT_MODULE_CONFIG } = require('./defaults');
13
+ const { createBehaviorPipeline } = require('./pipeline');
14
+ const {
15
+ getThreadState,
16
+ setThreadState,
17
+ isTurnCurrent,
18
+ markSpoke,
19
+ } = require('./state');
20
+ const { createBehaviorRegistry, LIFECYCLE_STAGES } = require('./registry');
21
+ const { splitIntoNaturalBubbles, deliverSocialReply } = require('./delivery');
22
+
23
+ module.exports = {
24
+ MODULE_IDS,
25
+ DEFAULT_MODULE_CONFIG,
26
+ SETTINGS_KEY,
27
+ cloneDefaults,
28
+ getBehaviorConfig,
29
+ setBehaviorConfig,
30
+ resolveBehaviorConfig,
31
+ normalizeStoredConfig,
32
+ roomConfigKey,
33
+ isModuleEnabled,
34
+ createBehaviorPipeline,
35
+ createBehaviorRegistry,
36
+ LIFECYCLE_STAGES,
37
+ getThreadState,
38
+ setThreadState,
39
+ isTurnCurrent,
40
+ markSpoke,
41
+ splitIntoNaturalBubbles,
42
+ deliverSocialReply,
43
+ };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ async function requestStructuredJson({
4
+ agentEngine,
5
+ userId,
6
+ agentId,
7
+ modelId = null,
8
+ purpose = 'fast',
9
+ system,
10
+ prompt,
11
+ signal = null,
12
+ maxTokens = 220,
13
+ fallback = {},
14
+ }) {
15
+ if (!agentEngine || typeof agentEngine.inferStructured !== 'function') {
16
+ const error = new Error('Behavior inference requires the central AI engine.');
17
+ error.code = 'BEHAVIOR_ENGINE_UNAVAILABLE';
18
+ throw error;
19
+ }
20
+ return agentEngine.inferStructured({
21
+ userId,
22
+ agentId,
23
+ modelId,
24
+ purpose,
25
+ system,
26
+ prompt,
27
+ maxTokens,
28
+ fallback,
29
+ signal,
30
+ });
31
+ }
32
+
33
+ module.exports = {
34
+ requestStructuredJson,
35
+ };