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.
Files changed (62) hide show
  1. package/flutter_app/lib/main_controller.dart +46 -4
  2. package/flutter_app/lib/main_models.dart +4 -2
  3. package/flutter_app/lib/main_operations.dart +0 -49
  4. package/flutter_app/lib/main_settings.dart +338 -0
  5. package/flutter_app/lib/src/backend_client.dart +19 -0
  6. package/package.json +1 -1
  7. package/server/http/routes.js +1 -0
  8. package/server/public/.last_build_id +1 -1
  9. package/server/public/flutter_bootstrap.js +1 -1
  10. package/server/public/main.dart.js +65635 -65088
  11. package/server/routes/behavior.js +80 -0
  12. package/server/routes/settings.js +4 -0
  13. package/server/services/ai/history.js +1 -1
  14. package/server/services/ai/loop/agent_engine_core.js +97 -16
  15. package/server/services/ai/loop/completion_judge.js +211 -44
  16. package/server/services/ai/loop/conversation_loop.js +6 -11
  17. package/server/services/ai/loop/messaging_delivery.js +47 -0
  18. package/server/services/ai/messagingFallback.js +2 -2
  19. package/server/services/ai/model_failure_cache.js +7 -0
  20. package/server/services/ai/systemPrompt.js +30 -128
  21. package/server/services/ai/taskAnalysis.js +47 -2
  22. package/server/services/ai/toolEvidence.js +65 -159
  23. package/server/services/ai/tools.js +60 -8
  24. package/server/services/behavior/config.js +251 -0
  25. package/server/services/behavior/defaults.js +68 -0
  26. package/server/services/behavior/delivery.js +176 -0
  27. package/server/services/behavior/index.js +43 -0
  28. package/server/services/behavior/model_client.js +35 -0
  29. package/server/services/behavior/modules/agent_identity.js +37 -0
  30. package/server/services/behavior/modules/channel_style.js +28 -0
  31. package/server/services/behavior/modules/index.js +29 -0
  32. package/server/services/behavior/modules/norms.js +101 -0
  33. package/server/services/behavior/modules/persona.js +172 -0
  34. package/server/services/behavior/modules/persona_prompt.js +238 -0
  35. package/server/services/behavior/modules/social_memory.js +86 -0
  36. package/server/services/behavior/modules/social_observability.js +94 -0
  37. package/server/services/behavior/modules/social_signals.js +41 -0
  38. package/server/services/behavior/modules/theory_of_mind.js +118 -0
  39. package/server/services/behavior/modules/turn_taking.js +237 -0
  40. package/server/services/behavior/pipeline.js +324 -0
  41. package/server/services/behavior/registry.js +78 -0
  42. package/server/services/behavior/signals.js +107 -0
  43. package/server/services/behavior/state.js +99 -0
  44. package/server/services/behavior/system_prompt.js +75 -0
  45. package/server/services/memory/manager.js +14 -33
  46. package/server/services/messaging/access_policy.js +10 -6
  47. package/server/services/messaging/automation.js +158 -27
  48. package/server/services/messaging/discord.js +11 -1
  49. package/server/services/messaging/formatting_guides.js +2 -4
  50. package/server/services/messaging/http_platforms.js +4 -3
  51. package/server/services/messaging/inbound_queue.js +74 -13
  52. package/server/services/messaging/inbound_store.js +33 -0
  53. package/server/services/messaging/manager.js +18 -0
  54. package/server/services/messaging/telegram.js +10 -1
  55. package/server/services/messaging/whatsapp.js +11 -0
  56. package/server/services/social_reach/channels/social_video.js +1 -1
  57. package/server/services/social_video/captions.js +2 -2
  58. package/server/services/social_video/service.js +194 -29
  59. package/server/services/voice/message.js +1 -1
  60. package/server/services/voice/runtime.js +2 -2
  61. package/server/utils/logger.js +19 -0
  62. package/server/services/ai/terminal_reply.js +0 -18
@@ -198,7 +198,10 @@ class DiscordPlatform extends BasePlatform {
198
198
  : `${senderDisplayName} in #${message.channel.name || channelId}${message.guild ? ` (${message.guild.name})` : ''}`;
199
199
 
200
200
  // Fetch recent channel history for context on guild/channel mentions
201
- const channelContext = (!isDM && this._isMentioned(message)) ? await this._fetchContext(message.channel, 20) : null;
201
+ const channelContext = !isDM ? await this._fetchContext(message.channel, 20) : null;
202
+ const repliedToAgent = !isDM
203
+ && message.reference?.messageId
204
+ && message.mentions?.repliedUser?.id === this._botUser?.id;
202
205
 
203
206
  this.emit('message', {
204
207
  platform: 'discord',
@@ -209,6 +212,13 @@ class DiscordPlatform extends BasePlatform {
209
212
  senderUsername,
210
213
  senderTag,
211
214
  guildId,
215
+ serverId: guildId,
216
+ groupId: isDM ? null : channelId,
217
+ channelId: isDM ? null : channelId,
218
+ roleIds: !isDM && message.member ? [...message.member.roles.cache.keys()] : [],
219
+ wasMentioned: !isDM && this._isMentioned(message),
220
+ repliedToAgent: Boolean(repliedToAgent),
221
+ replyToMessageId: message.reference?.messageId || null,
212
222
  content,
213
223
  mediaType: null,
214
224
  isGroup: !isDM,
@@ -31,11 +31,10 @@ function buildPlatformFormattingGuide(_platform, options = {}) {
31
31
  ? ''
32
32
  : 'Reply formatting guide:';
33
33
  const body = [
34
- 'Write like a favorite contact texting back: compact, natural, human.',
35
34
  'Prefer short paragraphs or multi-line chat bursts over document structure.',
36
35
  'Use simple single-level lists only when they genuinely improve clarity.',
37
36
  'Avoid tables, raw HTML, and formal report formatting in chat replies.',
38
- 'Lead with the useful result. Skip helpdesk filler and throat-clearing.',
37
+ 'A blank line may be delivered as a separate message bubble, so use one only for an intentional conversational beat.',
39
38
  'The runtime will adapt the final text to the destination platform.'
40
39
  ].map((line) => `- ${line}`).join('\n');
41
40
  return [intro, body].filter(Boolean).join('\n');
@@ -43,8 +42,7 @@ function buildPlatformFormattingGuide(_platform, options = {}) {
43
42
 
44
43
  function buildSendMessageFormattingReference() {
45
44
  return [
46
- 'Use one plain chat-style reply, like a favorite contact texting.',
47
- 'Lead with the result and keep filler out.',
45
+ 'Use one plain chat-style reply unless intentional bubble breaks improve it.',
48
46
  'The runtime adapts final formatting for the destination platform.',
49
47
  'For WhatsApp, media attachments still use media_path.'
50
48
  ].join(' ');
@@ -345,9 +345,6 @@ class SlackPlatform extends BasePlatform {
345
345
  const isGroup = String(event.channel_type || '') !== 'im';
346
346
  const wasMentioned = event.type === 'app_mention'
347
347
  || (this._botUserId && String(event.text || '').includes(`<@${this._botUserId}>`));
348
- if (isGroup && !wasMentioned) {
349
- return { handled: true, status: 202, body: 'ignored' };
350
- }
351
348
  const content = this._botUserId
352
349
  ? String(event.text).replace(new RegExp(`<@${this._botUserId}>`, 'g'), '').trim()
353
350
  : String(event.text);
@@ -365,8 +362,12 @@ class SlackPlatform extends BasePlatform {
365
362
  messageId: String(event.client_msg_id || event.ts || crypto.randomUUID()),
366
363
  timestamp: event.event_ts ? new Date(Number(event.event_ts) * 1000).toISOString() : new Date().toISOString(),
367
364
  threadTs: event.thread_ts || null,
365
+ threadId: event.thread_ts || null,
366
+ channelId: isGroup ? String(event.channel || '') : null,
367
+ groupId: isGroup ? String(event.channel || '') : null,
368
368
  rawMessage: body,
369
369
  wasMentioned,
370
+ repliedToAgent: Boolean(event.thread_ts && event.parent_user_id === this._botUserId),
370
371
  };
371
372
  const access = this._checkInboundAccess({
372
373
  platform: 'slack',
@@ -21,25 +21,72 @@ function cancelPending(queue) {
21
21
  for (const item of queue.pending.splice(0)) settleWaiters(item, result);
22
22
  }
23
23
 
24
+ function queueKeyForMessage(userId, msg) {
25
+ return [
26
+ String(userId),
27
+ String(msg.agentId || 'main'),
28
+ String(msg.platform || ''),
29
+ String(msg.chatId || ''),
30
+ ].join(':');
31
+ }
32
+
33
+ function batchEntry(msg) {
34
+ return {
35
+ sender: msg.sender || null,
36
+ senderName: msg.senderDisplayName || msg.senderName || msg.senderUsername || null,
37
+ content: String(msg.content || ''),
38
+ messageId: msg.messageId || null,
39
+ timestamp: msg.timestamp || null,
40
+ wasMentioned: msg.wasMentioned === true,
41
+ repliedToAgent: msg.repliedToAgent === true,
42
+ };
43
+ }
44
+
45
+ function mergeMessage(target, msg) {
46
+ const existingBatch = Array.isArray(target.message.messageBatch)
47
+ ? target.message.messageBatch
48
+ : [batchEntry(target.message)];
49
+ const nextBatch = [...existingBatch, batchEntry(msg)];
50
+ target.message.messageBatch = nextBatch;
51
+ target.message.content = target.message.isGroup
52
+ ? nextBatch.map((entry) => (
53
+ `[${entry.senderName || entry.sender || 'participant'}]: ${entry.content}`
54
+ )).join('\n')
55
+ : nextBatch.map((entry) => entry.content).join('\n');
56
+ target.message.messageId = msg.messageId || target.message.messageId;
57
+ target.message.timestamp = msg.timestamp || target.message.timestamp;
58
+ target.message.wasMentioned = target.message.wasMentioned === true || msg.wasMentioned === true;
59
+ target.message.repliedToAgent = target.message.repliedToAgent === true || msg.repliedToAgent === true;
60
+ target.message.behaviorTurnEpoch = Math.max(
61
+ Number(target.message.behaviorTurnEpoch || 0),
62
+ Number(msg.behaviorTurnEpoch || 0),
63
+ );
64
+ target.message.inboundJobIds = Array.from(new Set([
65
+ ...(target.message.inboundJobIds || []),
66
+ target.message.inboundJobId,
67
+ ...(msg.inboundJobIds || []),
68
+ msg.inboundJobId,
69
+ ].filter(Boolean)));
70
+ }
71
+
24
72
  function queuedResult(queue, msg) {
25
73
  let resolveCompletion;
26
74
  const completion = new Promise((resolve) => {
27
75
  resolveCompletion = resolve;
28
76
  });
29
- const last = queue.pending[queue.pending.length - 1];
77
+ const last = queue.collecting && queue.activeItem
78
+ ? queue.activeItem
79
+ : queue.pending[queue.pending.length - 1];
30
80
  if (
31
81
  last
32
82
  && last.message.platform === msg.platform
33
83
  && last.message.chatId === msg.chatId
34
- && String(last.message.sender || '') === String(msg.sender || '')
84
+ && (
85
+ last.message.isGroup === true
86
+ || String(last.message.sender || '') === String(msg.sender || '')
87
+ )
35
88
  ) {
36
- last.message.content += `\n${msg.content}`;
37
- last.message.messageId = msg.messageId;
38
- last.message.inboundJobIds = Array.from(new Set([
39
- ...(last.message.inboundJobIds || []),
40
- ...(msg.inboundJobIds || []),
41
- msg.inboundJobId,
42
- ].filter(Boolean)));
89
+ mergeMessage(last, msg);
43
90
  last.waiters.push(resolveCompletion);
44
91
  } else {
45
92
  queue.pending.push({
@@ -57,14 +104,16 @@ async function processInboundQueue({
57
104
  userId,
58
105
  msg,
59
106
  executeMessage,
60
- onProcessingError = null
107
+ onProcessingError = null,
108
+ batchWindowMs = 0,
61
109
  }) {
62
- const agentId = msg.agentId || null;
63
- const queueKey = `${userId}:${agentId || 'main'}`;
110
+ const queueKey = queueKeyForMessage(userId, msg);
64
111
  if (!userQueues[queueKey]) {
65
112
  userQueues[queueKey] = {
66
113
  running: false,
67
114
  pending: [],
115
+ activeItem: null,
116
+ collecting: false,
68
117
  cancelRequested: false,
69
118
  cancelPending() {
70
119
  cancelPending(this);
@@ -91,6 +140,15 @@ async function processInboundQueue({
91
140
 
92
141
  try {
93
142
  while (currentItem) {
143
+ queue.activeItem = currentItem;
144
+ const delayMs = currentItem.message.isGroup
145
+ ? Math.max(0, Math.min(5000, Number(batchWindowMs) || 0))
146
+ : 0;
147
+ if (delayMs > 0) {
148
+ queue.collecting = true;
149
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
150
+ queue.collecting = false;
151
+ }
94
152
  let outcome;
95
153
  try {
96
154
  outcome = await executeMessage(currentItem.message);
@@ -123,6 +181,8 @@ async function processInboundQueue({
123
181
  }
124
182
  } finally {
125
183
  queue.running = false;
184
+ queue.collecting = false;
185
+ queue.activeItem = null;
126
186
  cancelPending(queue);
127
187
  queue.cancelRequested = false;
128
188
  if (userQueues[queueKey] === queue) {
@@ -161,5 +221,6 @@ async function notifyProcessingError(handler, details) {
161
221
  }
162
222
 
163
223
  module.exports = {
164
- processInboundQueue
224
+ processInboundQueue,
225
+ queueKeyForMessage,
165
226
  };
@@ -156,6 +156,38 @@ function attachRunToInboundJobs(jobIds, runId) {
156
156
  ))();
157
157
  }
158
158
 
159
+ function annotateInboundJobs(jobIds, annotation) {
160
+ const ids = Array.from(new Set(
161
+ (Array.isArray(jobIds) ? jobIds : [jobIds])
162
+ .map((value) => String(value || '').trim())
163
+ .filter(Boolean),
164
+ ));
165
+ if (!ids.length) return 0;
166
+ const read = db.prepare(
167
+ `SELECT messages.id, messages.metadata
168
+ FROM messaging_inbound_jobs
169
+ JOIN messages ON messages.id = messaging_inbound_jobs.message_id
170
+ WHERE messaging_inbound_jobs.id = ?`,
171
+ );
172
+ const update = db.prepare('UPDATE messages SET metadata = ? WHERE id = ?');
173
+ return db.transaction(() => {
174
+ let count = 0;
175
+ for (const id of ids) {
176
+ const row = read.get(id);
177
+ if (!row) continue;
178
+ let metadata = {};
179
+ try {
180
+ metadata = row.metadata ? JSON.parse(row.metadata) : {};
181
+ } catch {
182
+ metadata = {};
183
+ }
184
+ update.run(JSON.stringify({ ...metadata, ...annotation }), row.id);
185
+ count += 1;
186
+ }
187
+ return count;
188
+ })();
189
+ }
190
+
159
191
  function reconcileInterruptedInboundJobs() {
160
192
  return db.transaction(() => {
161
193
  const completed = db.prepare(
@@ -214,6 +246,7 @@ function payloadForInboundJob(job) {
214
246
  }
215
247
 
216
248
  module.exports = {
249
+ annotateInboundJobs,
217
250
  attachRunToInboundJobs,
218
251
  claimInboundJob,
219
252
  enqueueInboundMessage,
@@ -187,6 +187,13 @@ class MessagingManager extends EventEmitter {
187
187
  senderUsername: msg.senderUsername,
188
188
  senderTag: msg.senderTag,
189
189
  isGroup: msg.isGroup,
190
+ wasMentioned: msg.wasMentioned === true,
191
+ repliedToAgent: msg.repliedToAgent === true,
192
+ groupId: msg.groupId || null,
193
+ channelId: msg.channelId || null,
194
+ serverId: msg.serverId || msg.guildId || null,
195
+ roomId: msg.roomId || null,
196
+ roleIds: Array.isArray(msg.roleIds) ? msg.roleIds.map(String) : [],
190
197
  mediaType: msg.mediaType,
191
198
  localMediaPath: msg.localMediaPath || null,
192
199
  ...(msg.metadata && typeof msg.metadata === 'object' ? msg.metadata : {}),
@@ -201,6 +208,8 @@ class MessagingManager extends EventEmitter {
201
208
  senderDisplayName: msg.senderDisplayName || null,
202
209
  senderUsername: msg.senderUsername || null,
203
210
  senderTag: msg.senderTag || null,
211
+ wasMentioned: msg.wasMentioned === true,
212
+ repliedToAgent: msg.repliedToAgent === true,
204
213
  content: normalizedIncomingContent,
205
214
  mediaType: msg.mediaType || null,
206
215
  localMediaPath: msg.localMediaPath || null,
@@ -211,6 +220,15 @@ class MessagingManager extends EventEmitter {
211
220
  groupName: msg.groupName || null,
212
221
  guildName: msg.guildName || null,
213
222
  roomName: msg.roomName || null,
223
+ groupId: msg.groupId || null,
224
+ channelId: msg.channelId || null,
225
+ serverId: msg.serverId || msg.guildId || null,
226
+ guildId: msg.guildId || msg.serverId || null,
227
+ roomId: msg.roomId || null,
228
+ roleIds: Array.isArray(msg.roleIds) ? msg.roleIds.map(String) : [],
229
+ replyToMessageId: msg.replyToMessageId || null,
230
+ threadId: msg.threadId || msg.threadTs || null,
231
+ eventType: msg.eventType || 'message',
214
232
  metadata: msg.metadata && typeof msg.metadata === 'object' ? msg.metadata : null,
215
233
  };
216
234
  const queued = enqueueInboundMessage({
@@ -223,7 +223,10 @@ class TelegramPlatform extends BasePlatform {
223
223
  ? senderName
224
224
  : `${senderName} in ${msg.chat.title || rawChatId}`;
225
225
 
226
- const channelContext = (!isPrivate && this._isMentioned(msg)) ? this._getContext(rawChatId) : null;
226
+ const channelContext = !isPrivate ? this._getContext(rawChatId) : null;
227
+ const repliedToAgent = !isPrivate
228
+ && msg.reply_to_message?.from?.id
229
+ && String(msg.reply_to_message.from.id) === String(this._botUser?.id || '');
227
230
 
228
231
  this.emit('message', {
229
232
  platform: 'telegram',
@@ -233,6 +236,12 @@ class TelegramPlatform extends BasePlatform {
233
236
  senderDisplayName,
234
237
  senderUsername,
235
238
  senderTag: senderUsername,
239
+ wasMentioned: !isPrivate && this._isMentioned(msg),
240
+ repliedToAgent: Boolean(repliedToAgent),
241
+ replyToMessageId: msg.reply_to_message?.message_id
242
+ ? String(msg.reply_to_message.message_id)
243
+ : null,
244
+ groupId: isPrivate ? null : rawChatId,
236
245
  content,
237
246
  mediaType: null,
238
247
  isGroup: !isPrivate,
@@ -172,6 +172,7 @@ class WhatsAppPlatform extends BasePlatform {
172
172
  const isGroup = chatId?.endsWith('@g.us');
173
173
  const sender = isGroup ? msg.key.participant : chatId;
174
174
  const pushName = msg.pushName || '';
175
+ const wasMentioned = isGroup && this._isGroupAddressedToBot(msg.message || {});
175
176
 
176
177
  let content = '';
177
178
  let mediaType = null;
@@ -258,6 +259,16 @@ class WhatsAppPlatform extends BasePlatform {
258
259
  senderName: pushName,
259
260
  senderDisplayName: pushName || null,
260
261
  senderTag: normalizeWhatsAppId(sender) || sender,
262
+ wasMentioned,
263
+ repliedToAgent: Boolean(
264
+ msg.message?.extendedTextMessage?.contextInfo?.stanzaId
265
+ && msg.message?.extendedTextMessage?.contextInfo?.participant
266
+ && this._ownIds().has(normalizeWhatsAppId(
267
+ msg.message.extendedTextMessage.contextInfo.participant
268
+ ))
269
+ ),
270
+ replyToMessageId: msg.message?.extendedTextMessage?.contextInfo?.stanzaId || null,
271
+ groupId: isGroup ? chatId : null,
261
272
  content,
262
273
  mediaType,
263
274
  localMediaPath,
@@ -47,7 +47,7 @@ class SocialVideoReachChannel extends SocialReachChannel {
47
47
  };
48
48
  });
49
49
  const missing = (health.dependencies || [])
50
- .filter((item) => !item.available)
50
+ .filter((item) => item.required !== false && !item.available)
51
51
  .map((item) => item.name)
52
52
  .filter(Boolean);
53
53
 
@@ -27,7 +27,7 @@ function pickCaptionTrack(captionGroups = {}, preferredLanguages = []) {
27
27
 
28
28
  const preferred = preferredLanguages.map(normalizeLanguageCode).filter(Boolean);
29
29
  const preferredOrder = preferred.length > 0 ? preferred : ['en', 'en-us', 'en-gb'];
30
- const preferredExt = ['vtt', 'webvtt', 'srt', 'json3'];
30
+ const preferredExt = ['vtt', 'webvtt', 'srt', 'json3', 'ttml', 'xml', 'json'];
31
31
 
32
32
  const scoreTrack = (track) => {
33
33
  const langIdx = preferredOrder.findIndex((code) => track.language === code || track.language.startsWith(`${code}-`));
@@ -148,7 +148,7 @@ function parseCaptionText(raw, extension = '') {
148
148
  if (ext === 'vtt' || ext === 'webvtt') {
149
149
  return parseWebVttToText(raw);
150
150
  }
151
- if (ext === 'ttml') {
151
+ if (ext === 'ttml' || ext === 'xml') {
152
152
  return parseTtmlToText(raw);
153
153
  }
154
154
  if (ext === 'srt') {