neoagent 3.2.1-beta.8 → 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.
- package/flutter_app/lib/main_app_shell.dart +178 -34
- package/flutter_app/lib/main_chat.dart +542 -80
- package/flutter_app/lib/main_controller.dart +59 -4
- package/flutter_app/lib/main_models.dart +171 -22
- 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/db/database.js +2 -2
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +85079 -83904
- package/server/routes/behavior.js +80 -0
- package/server/routes/settings.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/history.js +1 -1
- package/server/services/ai/loop/agent_engine_core.js +131 -18
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/completion_judge.js +280 -18
- package/server/services/ai/loop/conversation_loop.js +92 -34
- package/server/services/ai/loop/messaging_delivery.js +47 -0
- package/server/services/ai/loop/progress_classification.js +2 -0
- package/server/services/ai/loopPolicy.js +24 -2
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_failure_cache.js +7 -0
- package/server/services/ai/systemPrompt.js +31 -122
- package/server/services/ai/taskAnalysis.js +101 -12
- package/server/services/ai/toolEvidence.js +198 -0
- package/server/services/ai/tools.js +60 -19
- 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 +238 -0
- package/server/services/behavior/pipeline.js +341 -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 +269 -74
- package/server/services/messaging/automation.js +158 -27
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +73 -28
- package/server/services/messaging/inbound_queue.js +74 -13
- package/server/services/messaging/inbound_store.js +33 -0
- package/server/services/messaging/manager.js +57 -5
- 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 -57
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveBehaviorConfig, isModuleEnabled } = require('./config');
|
|
4
|
+
const {
|
|
5
|
+
bumpTurnEpoch,
|
|
6
|
+
getThreadState,
|
|
7
|
+
isTurnCurrent,
|
|
8
|
+
markSpoke,
|
|
9
|
+
} = require('./state');
|
|
10
|
+
const { createBehaviorRegistry } = require('./registry');
|
|
11
|
+
const { BEHAVIOR_MODULES } = require('./modules');
|
|
12
|
+
const { createServiceLogger } = require('../../utils/logger');
|
|
13
|
+
|
|
14
|
+
const logger = createServiceLogger('Behavior');
|
|
15
|
+
|
|
16
|
+
function createBehaviorPipeline(deps = {}) {
|
|
17
|
+
const memoryManager = deps.memoryManager || null;
|
|
18
|
+
const agentEngine = deps.agentEngine || null;
|
|
19
|
+
const io = deps.io || null;
|
|
20
|
+
const registry = createBehaviorRegistry(BEHAVIOR_MODULES);
|
|
21
|
+
|
|
22
|
+
function effectiveConfig(userId, agentId, msg) {
|
|
23
|
+
const config = resolveBehaviorConfig(userId, agentId, {
|
|
24
|
+
platform: msg.platform,
|
|
25
|
+
chatId: msg.chatId,
|
|
26
|
+
isGroup: Boolean(msg.isGroup),
|
|
27
|
+
});
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function noteInbound({ userId, agentId, msg }) {
|
|
32
|
+
const state = bumpTurnEpoch(userId, agentId, msg.platform, msg.chatId);
|
|
33
|
+
msg.behaviorTurnEpoch = state.turnEpoch;
|
|
34
|
+
return state.turnEpoch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scheduleBackground(baseCtx) {
|
|
38
|
+
const task = async (backgroundSignal) => {
|
|
39
|
+
const ctx = { ...baseCtx, signal: backgroundSignal };
|
|
40
|
+
await registry.run('afterTurn', ctx);
|
|
41
|
+
};
|
|
42
|
+
const key = [
|
|
43
|
+
'social-background',
|
|
44
|
+
baseCtx.userId,
|
|
45
|
+
baseCtx.agentId || 'main',
|
|
46
|
+
baseCtx.msg.platform,
|
|
47
|
+
baseCtx.msg.chatId,
|
|
48
|
+
].join(':');
|
|
49
|
+
const promise = agentEngine?.trackBackgroundTask
|
|
50
|
+
? agentEngine.trackBackgroundTask(task, { key, coalesce: true, signal: baseCtx.signal })
|
|
51
|
+
: Promise.resolve().then(() => task(baseCtx.signal));
|
|
52
|
+
promise.catch((error) => {
|
|
53
|
+
if (!baseCtx.signal?.aborted) logger.warn('background analysis failed:', error?.message || error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handleInbound({ userId, agentId, msg, signal = null }) {
|
|
58
|
+
const config = effectiveConfig(userId, agentId, msg);
|
|
59
|
+
const turnEpoch = Number(msg.behaviorTurnEpoch)
|
|
60
|
+
|| noteInbound({ userId, agentId, msg });
|
|
61
|
+
if (
|
|
62
|
+
msg.isGroup
|
|
63
|
+
&& msg.accessPolicyAllowUntagged === false
|
|
64
|
+
&& !msg.wasMentioned
|
|
65
|
+
&& !msg.repliedToAgent
|
|
66
|
+
) {
|
|
67
|
+
return {
|
|
68
|
+
engage: false,
|
|
69
|
+
decision: {
|
|
70
|
+
decision: 'stay_silent',
|
|
71
|
+
needScore: 0,
|
|
72
|
+
confidence: 1,
|
|
73
|
+
reasonCodes: ['untagged_disabled_for_shared_space'],
|
|
74
|
+
urgency: 'low',
|
|
75
|
+
rationale: 'Untagged responses are disabled for this shared space.',
|
|
76
|
+
tokenPath: 'gate_skip',
|
|
77
|
+
latencyMs: 0,
|
|
78
|
+
turnEpoch,
|
|
79
|
+
},
|
|
80
|
+
config,
|
|
81
|
+
promptBlocks: [],
|
|
82
|
+
observeResult: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (config.enabled === false) {
|
|
86
|
+
return {
|
|
87
|
+
engage: true,
|
|
88
|
+
decision: {
|
|
89
|
+
decision: 'speak',
|
|
90
|
+
needScore: 1,
|
|
91
|
+
confidence: 1,
|
|
92
|
+
reasonCodes: ['behavior_disabled'],
|
|
93
|
+
urgency: 'medium',
|
|
94
|
+
rationale: 'Behavior modules are disabled; using the standard response path.',
|
|
95
|
+
tokenPath: 'gate_skip',
|
|
96
|
+
latencyMs: 0,
|
|
97
|
+
turnEpoch,
|
|
98
|
+
},
|
|
99
|
+
config,
|
|
100
|
+
promptBlocks: [],
|
|
101
|
+
observeResult: null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const baseCtx = {
|
|
106
|
+
userId,
|
|
107
|
+
agentId,
|
|
108
|
+
msg,
|
|
109
|
+
config,
|
|
110
|
+
signal,
|
|
111
|
+
memoryManager,
|
|
112
|
+
agentEngine,
|
|
113
|
+
turnEpoch,
|
|
114
|
+
isModuleEnabled: (moduleId) => isModuleEnabled(config, moduleId),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const observations = await registry.run('observe', baseCtx);
|
|
118
|
+
const observeResult = observations.find((item) => item.moduleId === 'social_memory')?.value || null;
|
|
119
|
+
if (msg.isGroup) scheduleBackground(baseCtx);
|
|
120
|
+
|
|
121
|
+
const memoryHints = [];
|
|
122
|
+
if (observeResult?.scopeId) memoryHints.push(`channel:${observeResult.scopeId}`);
|
|
123
|
+
|
|
124
|
+
let decision;
|
|
125
|
+
if (!isModuleEnabled(config, 'turn_taking')) {
|
|
126
|
+
decision = {
|
|
127
|
+
decision: 'speak',
|
|
128
|
+
needScore: 1,
|
|
129
|
+
confidence: 1,
|
|
130
|
+
reasonCodes: ['turn_taking_disabled'],
|
|
131
|
+
urgency: 'medium',
|
|
132
|
+
rationale: 'Turn-taking is disabled; using the standard response path.',
|
|
133
|
+
tokenPath: 'gate_skip',
|
|
134
|
+
latencyMs: 0,
|
|
135
|
+
turnEpoch,
|
|
136
|
+
};
|
|
137
|
+
} else {
|
|
138
|
+
decision = (await registry.run('decide', {
|
|
139
|
+
...baseCtx,
|
|
140
|
+
memoryHints,
|
|
141
|
+
})).find((item) => item.moduleId === 'turn_taking')?.value;
|
|
142
|
+
}
|
|
143
|
+
if (!decision) {
|
|
144
|
+
throw new Error('The turn-taking module did not return a decision.');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (io && userId) {
|
|
148
|
+
io.to(`user:${userId}`).emit('behavior:decision', {
|
|
149
|
+
platform: msg.platform,
|
|
150
|
+
chatId: msg.chatId,
|
|
151
|
+
agentId,
|
|
152
|
+
isGroup: Boolean(msg.isGroup),
|
|
153
|
+
decision: decision.decision,
|
|
154
|
+
confidence: decision.confidence,
|
|
155
|
+
needScore: decision.needScore,
|
|
156
|
+
reasonCodes: decision.reasonCodes || [],
|
|
157
|
+
urgency: decision.urgency,
|
|
158
|
+
rationale: decision.rationale || '',
|
|
159
|
+
tokenPath: decision.tokenPath || 'gate_only',
|
|
160
|
+
turnEpoch: decision.turnEpoch,
|
|
161
|
+
model: decision.model || null,
|
|
162
|
+
at: new Date().toISOString(),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (decision.decision !== 'speak') {
|
|
167
|
+
return {
|
|
168
|
+
engage: false,
|
|
169
|
+
decision,
|
|
170
|
+
config,
|
|
171
|
+
promptBlocks: [],
|
|
172
|
+
observeResult,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const promptBlocks = await registry.composeContext(baseCtx);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
engage: true,
|
|
180
|
+
decision,
|
|
181
|
+
config,
|
|
182
|
+
promptBlocks,
|
|
183
|
+
observeResult,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function refineAndMaybeDeliver({
|
|
188
|
+
userId,
|
|
189
|
+
agentId,
|
|
190
|
+
msg,
|
|
191
|
+
config,
|
|
192
|
+
draft,
|
|
193
|
+
messagingManager,
|
|
194
|
+
runId = null,
|
|
195
|
+
signal = null,
|
|
196
|
+
mediaPath = null,
|
|
197
|
+
deliver = false,
|
|
198
|
+
turnEpoch = null,
|
|
199
|
+
}) {
|
|
200
|
+
const expectedEpoch = Number(turnEpoch || msg.behaviorTurnEpoch || 0);
|
|
201
|
+
if (msg.isGroup && !isTurnCurrent(
|
|
202
|
+
userId,
|
|
203
|
+
agentId,
|
|
204
|
+
msg.platform,
|
|
205
|
+
msg.chatId,
|
|
206
|
+
expectedEpoch,
|
|
207
|
+
)) {
|
|
208
|
+
return {
|
|
209
|
+
action: 'suppress',
|
|
210
|
+
content: '[NO RESPONSE]',
|
|
211
|
+
delivered: false,
|
|
212
|
+
suppressed: true,
|
|
213
|
+
reasonCodes: ['stale_turn'],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const combinedGroupReview = msg.isGroup
|
|
217
|
+
&& isModuleEnabled(config, 'persona')
|
|
218
|
+
&& isModuleEnabled(config, 'theory_of_mind');
|
|
219
|
+
const persona = combinedGroupReview
|
|
220
|
+
? {
|
|
221
|
+
action: 'send',
|
|
222
|
+
content: draft,
|
|
223
|
+
reasonCodes: ['persona_refine_combined_with_tom'],
|
|
224
|
+
}
|
|
225
|
+
: await registry.get('persona').refineDraft({
|
|
226
|
+
userId,
|
|
227
|
+
agentId,
|
|
228
|
+
msg,
|
|
229
|
+
config,
|
|
230
|
+
draft,
|
|
231
|
+
signal,
|
|
232
|
+
agentEngine,
|
|
233
|
+
runId,
|
|
234
|
+
});
|
|
235
|
+
const tom = await registry.get('theory_of_mind').refineDraft({
|
|
236
|
+
userId,
|
|
237
|
+
agentId,
|
|
238
|
+
msg,
|
|
239
|
+
config,
|
|
240
|
+
draft: persona.content,
|
|
241
|
+
signal,
|
|
242
|
+
agentEngine,
|
|
243
|
+
runId,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const content = tom.content;
|
|
247
|
+
const reasonCodes = [
|
|
248
|
+
...(persona.reasonCodes || []),
|
|
249
|
+
...(tom.reasonCodes || []),
|
|
250
|
+
];
|
|
251
|
+
if (!deliver || !messagingManager) {
|
|
252
|
+
return {
|
|
253
|
+
...tom,
|
|
254
|
+
delivered: false,
|
|
255
|
+
content,
|
|
256
|
+
reasonCodes,
|
|
257
|
+
personaAction: persona.action,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!content || content.toUpperCase() === '[NO RESPONSE]') {
|
|
262
|
+
return {
|
|
263
|
+
...tom,
|
|
264
|
+
delivered: false,
|
|
265
|
+
suppressed: true,
|
|
266
|
+
content,
|
|
267
|
+
reasonCodes,
|
|
268
|
+
personaAction: persona.action,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const deliveryConfig = isModuleEnabled(config, 'delivery')
|
|
273
|
+
? config
|
|
274
|
+
: { ...config, deliveryStyle: 'single' };
|
|
275
|
+
const delivery = await registry.get('delivery').deliver({
|
|
276
|
+
messagingManager,
|
|
277
|
+
userId,
|
|
278
|
+
agentId,
|
|
279
|
+
platform: msg.platform,
|
|
280
|
+
chatId: msg.chatId,
|
|
281
|
+
content,
|
|
282
|
+
config: deliveryConfig,
|
|
283
|
+
runId,
|
|
284
|
+
signal,
|
|
285
|
+
mediaPath,
|
|
286
|
+
turnEpoch: expectedEpoch,
|
|
287
|
+
beforeBubble: () => !msg.isGroup || isTurnCurrent(
|
|
288
|
+
userId,
|
|
289
|
+
agentId,
|
|
290
|
+
msg.platform,
|
|
291
|
+
msg.chatId,
|
|
292
|
+
expectedEpoch,
|
|
293
|
+
),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (
|
|
297
|
+
(delivery?.success !== false && delivery?.suppressed !== true)
|
|
298
|
+
|| Number(delivery?.deliveredBubbles || 0) > 0
|
|
299
|
+
) {
|
|
300
|
+
markSpoke(userId, agentId, msg.platform, msg.chatId);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
...tom,
|
|
305
|
+
delivered: delivery?.success !== false && delivery?.suppressed !== true,
|
|
306
|
+
suppressed: delivery?.suppressed === true,
|
|
307
|
+
delivery,
|
|
308
|
+
content,
|
|
309
|
+
reasonCodes,
|
|
310
|
+
personaAction: persona.action,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function getDiagnostics(userId, agentId, platform, chatId) {
|
|
315
|
+
const config = resolveBehaviorConfig(userId, agentId, {
|
|
316
|
+
platform,
|
|
317
|
+
chatId,
|
|
318
|
+
isGroup: true,
|
|
319
|
+
});
|
|
320
|
+
const state = getThreadState(userId, agentId, platform, chatId);
|
|
321
|
+
return {
|
|
322
|
+
config,
|
|
323
|
+
state,
|
|
324
|
+
modules: Object.fromEntries(
|
|
325
|
+
Object.keys(config.modules || {}).map((id) => [id, isModuleEnabled(config, id)]),
|
|
326
|
+
),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
registry,
|
|
332
|
+
noteInbound,
|
|
333
|
+
handleInbound,
|
|
334
|
+
refineAndMaybeDeliver,
|
|
335
|
+
getDiagnostics,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
createBehaviorPipeline,
|
|
341
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const LIFECYCLE_STAGES = Object.freeze([
|
|
4
|
+
'observe',
|
|
5
|
+
'decide',
|
|
6
|
+
'composeContext',
|
|
7
|
+
'composeSystemPrompt',
|
|
8
|
+
'refineDraft',
|
|
9
|
+
'deliver',
|
|
10
|
+
'afterTurn',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function createBehaviorRegistry(modules) {
|
|
14
|
+
const byId = new Map();
|
|
15
|
+
for (const module of modules) {
|
|
16
|
+
const id = String(module?.id || '').trim();
|
|
17
|
+
if (!id) throw new Error('Behavior modules require a stable id.');
|
|
18
|
+
if (byId.has(id)) throw new Error(`Duplicate behavior module id: ${id}`);
|
|
19
|
+
byId.set(id, module);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function list() {
|
|
23
|
+
return [...byId.values()];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function get(id) {
|
|
27
|
+
return byId.get(id) || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function run(stage, ctx) {
|
|
31
|
+
if (!LIFECYCLE_STAGES.includes(stage)) {
|
|
32
|
+
throw new Error(`Unknown behavior lifecycle stage: ${stage}`);
|
|
33
|
+
}
|
|
34
|
+
const results = [];
|
|
35
|
+
for (const module of byId.values()) {
|
|
36
|
+
if (ctx.isModuleEnabled && !ctx.isModuleEnabled(module.id)) continue;
|
|
37
|
+
if (typeof module[stage] !== 'function') continue;
|
|
38
|
+
results.push({ moduleId: module.id, value: await module[stage](ctx) });
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function composeContext(ctx) {
|
|
44
|
+
const contributions = [];
|
|
45
|
+
const keys = new Set();
|
|
46
|
+
for (const { moduleId, value } of await run('composeContext', ctx)) {
|
|
47
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (!entry?.content) continue;
|
|
50
|
+
const key = String(entry.key || moduleId).trim();
|
|
51
|
+
if (keys.has(key)) {
|
|
52
|
+
throw new Error(`Duplicate behavior prompt contribution: ${key}`);
|
|
53
|
+
}
|
|
54
|
+
keys.add(key);
|
|
55
|
+
contributions.push({
|
|
56
|
+
key,
|
|
57
|
+
priority: Number(entry.priority || 0),
|
|
58
|
+
content: String(entry.content).trim(),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return contributions
|
|
63
|
+
.sort((left, right) => right.priority - left.priority)
|
|
64
|
+
.map((entry) => entry.content);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
get,
|
|
69
|
+
list,
|
|
70
|
+
run,
|
|
71
|
+
composeContext,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
LIFECYCLE_STAGES,
|
|
77
|
+
createBehaviorRegistry,
|
|
78
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
|
|
5
|
+
function truncate(text, max = 280) {
|
|
6
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
7
|
+
if (value.length <= max) return value;
|
|
8
|
+
return `${value.slice(0, max - 1)}…`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildChannelScopeId(platform, chatId) {
|
|
12
|
+
return `${String(platform || '').trim()}:${String(chatId || '').trim()}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadRecentRoomMessages({ userId, agentId, platform, chatId, limit = 12 }) {
|
|
16
|
+
const rows = db.prepare(
|
|
17
|
+
`SELECT role, content, created_at, metadata
|
|
18
|
+
FROM messages
|
|
19
|
+
WHERE user_id = ?
|
|
20
|
+
AND agent_id IS ?
|
|
21
|
+
AND platform = ?
|
|
22
|
+
AND platform_chat_id = ?
|
|
23
|
+
ORDER BY created_at DESC
|
|
24
|
+
LIMIT ?`,
|
|
25
|
+
).all(userId, agentId, platform, String(chatId), Math.max(1, Math.min(Number(limit) || 12, 30)));
|
|
26
|
+
|
|
27
|
+
return rows.reverse().map((row) => {
|
|
28
|
+
let metadata = null;
|
|
29
|
+
try {
|
|
30
|
+
metadata = row.metadata ? JSON.parse(row.metadata) : null;
|
|
31
|
+
} catch {
|
|
32
|
+
metadata = null;
|
|
33
|
+
}
|
|
34
|
+
const sender = row.role === 'assistant'
|
|
35
|
+
? 'assistant'
|
|
36
|
+
: (metadata?.senderDisplayName || metadata?.senderName || metadata?.sender || 'participant');
|
|
37
|
+
return {
|
|
38
|
+
role: row.role,
|
|
39
|
+
sender,
|
|
40
|
+
content: truncate(row.content, 320),
|
|
41
|
+
createdAt: row.created_at,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildDecisionPacket({
|
|
47
|
+
msg,
|
|
48
|
+
config,
|
|
49
|
+
threadState,
|
|
50
|
+
roomMessages = [],
|
|
51
|
+
localMemoryHints = [],
|
|
52
|
+
}) {
|
|
53
|
+
const recent = Array.isArray(msg.channelContext) && msg.channelContext.length
|
|
54
|
+
? msg.channelContext.slice(-12).map((item) => ({
|
|
55
|
+
sender: item.author || item.sender || 'participant',
|
|
56
|
+
content: truncate(item.content, 280),
|
|
57
|
+
}))
|
|
58
|
+
: roomMessages.slice(-12).map((item) => ({
|
|
59
|
+
sender: item.sender,
|
|
60
|
+
content: item.content,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
const secondsSinceSpoke = threadState?.lastSpokeAt
|
|
64
|
+
? Math.max(0, Math.round((Date.now() - Date.parse(threadState.lastSpokeAt)) / 1000))
|
|
65
|
+
: null;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
chat: {
|
|
69
|
+
platform: msg.platform,
|
|
70
|
+
chatId: String(msg.chatId || ''),
|
|
71
|
+
isGroup: Boolean(msg.isGroup),
|
|
72
|
+
groupName: msg.groupName || msg.guildName || msg.channelName || null,
|
|
73
|
+
},
|
|
74
|
+
sender: {
|
|
75
|
+
id: msg.sender || null,
|
|
76
|
+
name: msg.senderName || msg.senderDisplayName || msg.senderUsername || null,
|
|
77
|
+
username: msg.senderUsername || null,
|
|
78
|
+
tag: msg.senderTag || null,
|
|
79
|
+
},
|
|
80
|
+
event: {
|
|
81
|
+
content: truncate(msg.content, 800),
|
|
82
|
+
hasMedia: Boolean(msg.localMediaPath || msg.mediaType),
|
|
83
|
+
mediaType: msg.mediaType || null,
|
|
84
|
+
wasMentioned: msg.wasMentioned === true,
|
|
85
|
+
repliedToAgent: msg.repliedToAgent === true,
|
|
86
|
+
timestamp: msg.timestamp || new Date().toISOString(),
|
|
87
|
+
},
|
|
88
|
+
room: {
|
|
89
|
+
recentMessages: recent,
|
|
90
|
+
secondsSinceAgentSpoke: secondsSinceSpoke,
|
|
91
|
+
recentSilenceCount: Number(threadState?.recentSilenceCount || 0),
|
|
92
|
+
},
|
|
93
|
+
policy: {
|
|
94
|
+
participationMode: config.participationMode || 'automatic',
|
|
95
|
+
minimumNeedScore: Number(config.minimumNeedScore ?? 0.72),
|
|
96
|
+
groupDefaultPosture: 'prefer_hold_back',
|
|
97
|
+
},
|
|
98
|
+
roomHints: Array.isArray(localMemoryHints) ? localMemoryHints.slice(0, 4) : [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
truncate,
|
|
104
|
+
buildChannelScopeId,
|
|
105
|
+
loadRecentRoomMessages,
|
|
106
|
+
buildDecisionPacket,
|
|
107
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MAX_THREAD_STATES = 1000;
|
|
4
|
+
const THREAD_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
const threadStates = new Map();
|
|
6
|
+
|
|
7
|
+
function stateKey(userId, agentId, platform, chatId) {
|
|
8
|
+
return [
|
|
9
|
+
String(userId || ''),
|
|
10
|
+
String(agentId || 'main'),
|
|
11
|
+
String(platform || ''),
|
|
12
|
+
String(chatId || ''),
|
|
13
|
+
].join('::');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function defaultState() {
|
|
17
|
+
return {
|
|
18
|
+
turnEpoch: 0,
|
|
19
|
+
lastDecision: null,
|
|
20
|
+
lastDecisionAt: null,
|
|
21
|
+
lastSpokeAt: null,
|
|
22
|
+
messageCountSinceNorms: 0,
|
|
23
|
+
normsPromptBlock: '',
|
|
24
|
+
normsUpdatedAt: null,
|
|
25
|
+
lastObservabilityAt: null,
|
|
26
|
+
lastObservabilitySummary: null,
|
|
27
|
+
messageCountSinceObservability: 0,
|
|
28
|
+
recentSilenceCount: 0,
|
|
29
|
+
recentSignals: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function evictThreadStates() {
|
|
34
|
+
const cutoff = Date.now() - THREAD_STATE_TTL_MS;
|
|
35
|
+
for (const [key, entry] of threadStates.entries()) {
|
|
36
|
+
if (entry.touchedAt < cutoff) threadStates.delete(key);
|
|
37
|
+
}
|
|
38
|
+
while (threadStates.size > MAX_THREAD_STATES) {
|
|
39
|
+
const oldest = threadStates.keys().next().value;
|
|
40
|
+
if (oldest == null) break;
|
|
41
|
+
threadStates.delete(oldest);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getThreadState(userId, agentId, platform, chatId) {
|
|
46
|
+
const key = stateKey(userId, agentId, platform, chatId);
|
|
47
|
+
const entry = threadStates.get(key);
|
|
48
|
+
if (!entry) return defaultState();
|
|
49
|
+
threadStates.delete(key);
|
|
50
|
+
threadStates.set(key, entry);
|
|
51
|
+
entry.touchedAt = Date.now();
|
|
52
|
+
return { ...defaultState(), ...entry.value };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function setThreadState(userId, agentId, platform, chatId, patch = {}) {
|
|
56
|
+
const key = stateKey(userId, agentId, platform, chatId);
|
|
57
|
+
const current = getThreadState(userId, agentId, platform, chatId);
|
|
58
|
+
const next = {
|
|
59
|
+
...current,
|
|
60
|
+
...patch,
|
|
61
|
+
};
|
|
62
|
+
threadStates.delete(key);
|
|
63
|
+
threadStates.set(key, { value: next, touchedAt: Date.now() });
|
|
64
|
+
evictThreadStates();
|
|
65
|
+
return next;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function bumpTurnEpoch(userId, agentId, platform, chatId) {
|
|
69
|
+
const current = getThreadState(userId, agentId, platform, chatId);
|
|
70
|
+
return setThreadState(userId, agentId, platform, chatId, {
|
|
71
|
+
turnEpoch: Number(current.turnEpoch || 0) + 1,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isTurnCurrent(userId, agentId, platform, chatId, turnEpoch) {
|
|
76
|
+
return Number(getThreadState(userId, agentId, platform, chatId).turnEpoch || 0)
|
|
77
|
+
=== Number(turnEpoch || 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function markSpoke(userId, agentId, platform, chatId) {
|
|
81
|
+
return setThreadState(userId, agentId, platform, chatId, {
|
|
82
|
+
lastSpokeAt: new Date().toISOString(),
|
|
83
|
+
recentSilenceCount: 0,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function clearThreadStates() {
|
|
88
|
+
threadStates.clear();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
getThreadState,
|
|
93
|
+
setThreadState,
|
|
94
|
+
bumpTurnEpoch,
|
|
95
|
+
isTurnCurrent,
|
|
96
|
+
markSpoke,
|
|
97
|
+
clearThreadStates,
|
|
98
|
+
stateKey,
|
|
99
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { resolveBehaviorConfig, isModuleEnabled } = require('./config');
|
|
4
|
+
const { createBehaviorRegistry } = require('./registry');
|
|
5
|
+
const { BEHAVIOR_MODULES } = require('./modules');
|
|
6
|
+
|
|
7
|
+
async function buildBehaviorSystemPrompt({
|
|
8
|
+
userId,
|
|
9
|
+
agentId,
|
|
10
|
+
triggerSource,
|
|
11
|
+
context,
|
|
12
|
+
memoryManager,
|
|
13
|
+
}) {
|
|
14
|
+
const config = resolveBehaviorConfig(userId, agentId, {
|
|
15
|
+
platform: context.source || null,
|
|
16
|
+
chatId: context.chatId || null,
|
|
17
|
+
isGroup: context.memoryAudience === 'shared' || context.socialIntelligence?.isGroup === true,
|
|
18
|
+
});
|
|
19
|
+
const ctx = {
|
|
20
|
+
...context,
|
|
21
|
+
userId,
|
|
22
|
+
agentId,
|
|
23
|
+
triggerSource,
|
|
24
|
+
audience: context.memoryAudience || 'owner',
|
|
25
|
+
config,
|
|
26
|
+
memoryManager,
|
|
27
|
+
};
|
|
28
|
+
const registry = createBehaviorRegistry(BEHAVIOR_MODULES);
|
|
29
|
+
const results = await registry.run('composeSystemPrompt', {
|
|
30
|
+
...ctx,
|
|
31
|
+
isModuleEnabled: (moduleId) => isModuleEnabled(config, moduleId),
|
|
32
|
+
});
|
|
33
|
+
const priorities = {
|
|
34
|
+
channel_style: 100,
|
|
35
|
+
persona: 80,
|
|
36
|
+
agent_identity: 60,
|
|
37
|
+
};
|
|
38
|
+
const entries = [];
|
|
39
|
+
for (const entry of results) {
|
|
40
|
+
if (!entry.value) continue;
|
|
41
|
+
const priority = priorities[entry.moduleId] || 0;
|
|
42
|
+
if (typeof entry.value === 'string') {
|
|
43
|
+
entries.push({
|
|
44
|
+
moduleId: entry.moduleId,
|
|
45
|
+
content: entry.value,
|
|
46
|
+
section: entry.moduleId === 'channel_style' ? 'stable' : 'dynamic',
|
|
47
|
+
priority,
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
for (const section of ['stable', 'dynamic']) {
|
|
52
|
+
const values = Array.isArray(entry.value[section])
|
|
53
|
+
? entry.value[section]
|
|
54
|
+
: [entry.value[section]];
|
|
55
|
+
for (const content of values) {
|
|
56
|
+
if (!content) continue;
|
|
57
|
+
entries.push({
|
|
58
|
+
moduleId: entry.moduleId,
|
|
59
|
+
content,
|
|
60
|
+
section,
|
|
61
|
+
priority,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
entries.sort((left, right) => right.priority - left.priority);
|
|
67
|
+
return {
|
|
68
|
+
stable: entries.filter((entry) => entry.section === 'stable').map((entry) => entry.content),
|
|
69
|
+
dynamic: entries.filter((entry) => entry.section === 'dynamic').map((entry) => entry.content),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
buildBehaviorSystemPrompt,
|
|
75
|
+
};
|