neoagent 3.2.1-beta.10 → 3.2.1-beta.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/flutter_app/lib/main_controller.dart +46 -4
- package/flutter_app/lib/main_models.dart +4 -2
- package/flutter_app/lib/main_operations.dart +0 -49
- package/flutter_app/lib/main_settings.dart +338 -0
- package/flutter_app/lib/src/backend_client.dart +19 -0
- package/package.json +1 -1
- package/server/http/routes.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65635 -65088
- package/server/routes/behavior.js +80 -0
- package/server/routes/settings.js +4 -0
- package/server/services/ai/history.js +1 -1
- package/server/services/ai/loop/agent_engine_core.js +97 -16
- package/server/services/ai/loop/completion_judge.js +211 -44
- package/server/services/ai/loop/conversation_loop.js +6 -11
- package/server/services/ai/loop/messaging_delivery.js +47 -0
- package/server/services/ai/messagingFallback.js +2 -2
- package/server/services/ai/model_failure_cache.js +7 -0
- package/server/services/ai/systemPrompt.js +30 -128
- package/server/services/ai/taskAnalysis.js +47 -2
- package/server/services/ai/toolEvidence.js +65 -159
- package/server/services/ai/tools.js +60 -8
- package/server/services/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +172 -0
- package/server/services/behavior/modules/persona_prompt.js +238 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +118 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +324 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/memory/manager.js +14 -33
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +158 -27
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +2 -4
- package/server/services/messaging/http_platforms.js +4 -3
- package/server/services/messaging/inbound_queue.js +74 -13
- package/server/services/messaging/inbound_store.js +33 -0
- package/server/services/messaging/manager.js +18 -0
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/whatsapp.js +11 -0
- package/server/services/social_reach/channels/social_video.js +1 -1
- package/server/services/social_video/captions.js +2 -2
- package/server/services/social_video/service.js +194 -29
- package/server/services/voice/message.js +1 -1
- package/server/services/voice/runtime.js +2 -2
- package/server/utils/logger.js +19 -0
- package/server/services/ai/terminal_reply.js +0 -18
|
@@ -0,0 +1,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
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../../db/database');
|
|
4
|
+
const { buildAgentRosterPrompt } = require('../../agents/manager');
|
|
5
|
+
const { isModuleEnabled } = require('../config');
|
|
6
|
+
|
|
7
|
+
function clamp(text, maxChars) {
|
|
8
|
+
const value = String(text || '').trim();
|
|
9
|
+
if (!value || value.length <= maxChars) return value;
|
|
10
|
+
return `${value.slice(0, maxChars)}\n...[trimmed]`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildSystemPromptContribution(ctx) {
|
|
14
|
+
if (!ctx.agentId || !isModuleEnabled(ctx.config, 'agent_identity')) return '';
|
|
15
|
+
const agent = db.prepare(
|
|
16
|
+
`SELECT display_name, slug, description, responsibilities, instructions
|
|
17
|
+
FROM agents
|
|
18
|
+
WHERE user_id = ? AND id = ?`,
|
|
19
|
+
).get(ctx.userId, ctx.agentId);
|
|
20
|
+
if (!agent) return '';
|
|
21
|
+
const active = [
|
|
22
|
+
'## Active Agent',
|
|
23
|
+
`Name: ${agent.display_name} (${agent.slug})`,
|
|
24
|
+
agent.description ? `Description: ${clamp(agent.description, 600)}` : '',
|
|
25
|
+
agent.responsibilities ? `Responsibilities: ${clamp(agent.responsibilities, 1000)}` : '',
|
|
26
|
+
agent.instructions ? `Agent instructions: ${clamp(agent.instructions, 1600)}` : '',
|
|
27
|
+
].filter(Boolean).join('\n');
|
|
28
|
+
const roster = ctx.triggerSource === 'agent_delegation'
|
|
29
|
+
? ''
|
|
30
|
+
: buildAgentRosterPrompt(ctx.userId, ctx.agentId);
|
|
31
|
+
return [active, roster].filter(Boolean).join('\n\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
id: 'agent_identity',
|
|
36
|
+
composeSystemPrompt: buildSystemPromptContribution,
|
|
37
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isModuleEnabled } = require('../config');
|
|
4
|
+
|
|
5
|
+
function buildSystemPromptContribution(ctx) {
|
|
6
|
+
if (!isModuleEnabled(ctx.config, 'channel_style')) return '';
|
|
7
|
+
if (ctx.widgetId) {
|
|
8
|
+
return 'CHANNEL RESPONSE GUIDE: Widget refreshes should produce structured snapshot data, not conversational filler.';
|
|
9
|
+
}
|
|
10
|
+
if (ctx.triggerSource === 'voice_live' || ctx.latencyProfile === 'voice') {
|
|
11
|
+
return 'CHANNEL RESPONSE GUIDE: Voice replies should usually fit in one or two concise spoken sentences unless detail is necessary.';
|
|
12
|
+
}
|
|
13
|
+
if (ctx.triggerSource === 'messaging' && ctx.audience === 'shared') {
|
|
14
|
+
return 'CHANNEL RESPONSE GUIDE: The turn-taking gate already decided this shared room needs a response. Make one brief, natural contribution, address the relevant participant or room rather than the owner, and do not dominate or send progress chatter.';
|
|
15
|
+
}
|
|
16
|
+
if (ctx.triggerSource === 'messaging') {
|
|
17
|
+
return 'CHANNEL RESPONSE GUIDE: Text like a natural contact. Prefer one concise reply, or a few short bubbles when the thought genuinely benefits from separate beats. A blank line marks an intentional bubble break, so do not add one mechanically. Keep dense information and lists together, and expand only when the task needs detail.';
|
|
18
|
+
}
|
|
19
|
+
if (ctx.triggerSource === 'wearable') {
|
|
20
|
+
return 'CHANNEL RESPONSE GUIDE: Wearable replies should be one or two short sentences with the result first.';
|
|
21
|
+
}
|
|
22
|
+
return 'CHANNEL RESPONSE GUIDE: Web chat may use short paragraphs and compact lists. Avoid padding and lead with the result.';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
id: 'channel_style',
|
|
27
|
+
composeSystemPrompt: buildSystemPromptContribution,
|
|
28
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const turnTaking = require('./turn_taking');
|
|
4
|
+
const socialMemory = require('./social_memory');
|
|
5
|
+
const norms = require('./norms');
|
|
6
|
+
const persona = require('./persona');
|
|
7
|
+
const agentIdentity = require('./agent_identity');
|
|
8
|
+
const channelStyle = require('./channel_style');
|
|
9
|
+
const theoryOfMind = require('./theory_of_mind');
|
|
10
|
+
const socialSignals = require('./social_signals');
|
|
11
|
+
const socialObservability = require('./social_observability');
|
|
12
|
+
const delivery = require('../delivery');
|
|
13
|
+
|
|
14
|
+
const BEHAVIOR_MODULES = Object.freeze([
|
|
15
|
+
turnTaking,
|
|
16
|
+
socialMemory,
|
|
17
|
+
norms,
|
|
18
|
+
persona,
|
|
19
|
+
agentIdentity,
|
|
20
|
+
channelStyle,
|
|
21
|
+
theoryOfMind,
|
|
22
|
+
socialSignals,
|
|
23
|
+
socialObservability,
|
|
24
|
+
delivery,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
BEHAVIOR_MODULES,
|
|
29
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { requestStructuredJson } = require('../model_client');
|
|
4
|
+
const { loadRecentRoomMessages, truncate } = require('../signals');
|
|
5
|
+
const { getThreadState, setThreadState } = require('../state');
|
|
6
|
+
const { isModuleEnabled } = require('../config');
|
|
7
|
+
|
|
8
|
+
const SYSTEM_PROMPT = `You extract a compact group-chat norms profile for an AI participant.
|
|
9
|
+
Return JSON with keys:
|
|
10
|
+
promptBlock (short instructions the agent can follow to match the room's voice),
|
|
11
|
+
notes (array of short observations).
|
|
12
|
+
No phrase blacklists. Describe style, formality, humor, length, and participation norms.`;
|
|
13
|
+
|
|
14
|
+
function getNormsPromptBlock(ctx) {
|
|
15
|
+
if (!isModuleEnabled(ctx.config, 'norms')) return '';
|
|
16
|
+
const state = getThreadState(ctx.userId, ctx.agentId, ctx.msg.platform, ctx.msg.chatId);
|
|
17
|
+
return String(state.normsPromptBlock || '').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function maybeRefreshNorms(ctx) {
|
|
21
|
+
const { userId, agentId, msg, config, signal = null } = ctx;
|
|
22
|
+
if (!msg?.isGroup || !isModuleEnabled(config, 'norms')) {
|
|
23
|
+
return { refreshed: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const state = getThreadState(userId, agentId, msg.platform, msg.chatId);
|
|
27
|
+
const count = Number(state.messageCountSinceNorms || 0) + 1;
|
|
28
|
+
const gap = Number(config.normsRefreshMessageGap || 18);
|
|
29
|
+
if (count < gap) {
|
|
30
|
+
setThreadState(userId, agentId, msg.platform, msg.chatId, {
|
|
31
|
+
messageCountSinceNorms: count,
|
|
32
|
+
});
|
|
33
|
+
return { refreshed: false, deferred: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const roomMessages = loadRecentRoomMessages({
|
|
37
|
+
userId,
|
|
38
|
+
agentId,
|
|
39
|
+
platform: msg.platform,
|
|
40
|
+
chatId: msg.chatId,
|
|
41
|
+
limit: 24,
|
|
42
|
+
});
|
|
43
|
+
if (roomMessages.length < 4) {
|
|
44
|
+
setThreadState(userId, agentId, msg.platform, msg.chatId, {
|
|
45
|
+
messageCountSinceNorms: count,
|
|
46
|
+
});
|
|
47
|
+
return { refreshed: false, reason: 'insufficient_history' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await requestStructuredJson({
|
|
52
|
+
agentEngine: ctx.agentEngine,
|
|
53
|
+
userId,
|
|
54
|
+
agentId,
|
|
55
|
+
purpose: 'fast',
|
|
56
|
+
system: SYSTEM_PROMPT,
|
|
57
|
+
prompt: JSON.stringify({
|
|
58
|
+
platform: msg.platform,
|
|
59
|
+
chatId: msg.chatId,
|
|
60
|
+
recentMessages: roomMessages.map((item) => ({
|
|
61
|
+
sender: item.sender,
|
|
62
|
+
content: item.content,
|
|
63
|
+
})),
|
|
64
|
+
}),
|
|
65
|
+
signal,
|
|
66
|
+
maxTokens: 280,
|
|
67
|
+
});
|
|
68
|
+
const promptBlock = truncate(result.parsed?.promptBlock || '', 900);
|
|
69
|
+
if (!promptBlock) {
|
|
70
|
+
setThreadState(userId, agentId, msg.platform, msg.chatId, {
|
|
71
|
+
messageCountSinceNorms: 0,
|
|
72
|
+
});
|
|
73
|
+
return { refreshed: false, reason: 'empty_block' };
|
|
74
|
+
}
|
|
75
|
+
setThreadState(userId, agentId, msg.platform, msg.chatId, {
|
|
76
|
+
normsPromptBlock: `## Group norms\n${promptBlock}`,
|
|
77
|
+
normsUpdatedAt: new Date().toISOString(),
|
|
78
|
+
messageCountSinceNorms: 0,
|
|
79
|
+
});
|
|
80
|
+
return { refreshed: true };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (signal?.aborted) throw error;
|
|
83
|
+
setThreadState(userId, agentId, msg.platform, msg.chatId, {
|
|
84
|
+
messageCountSinceNorms: count,
|
|
85
|
+
});
|
|
86
|
+
return { refreshed: false, error: error?.message || String(error) };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function composeContext(ctx) {
|
|
91
|
+
const content = getNormsPromptBlock(ctx);
|
|
92
|
+
return content ? { key: 'norms', priority: 50, content } : null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
id: 'norms',
|
|
97
|
+
composeContext,
|
|
98
|
+
afterTurn: maybeRefreshNorms,
|
|
99
|
+
getNormsPromptBlock,
|
|
100
|
+
maybeRefreshNorms,
|
|
101
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isModuleEnabled } = require('../config');
|
|
4
|
+
const { requestStructuredJson } = require('../model_client');
|
|
5
|
+
const { truncate } = require('../signals');
|
|
6
|
+
const { BASELINE_PERSONA_PROMPT } = require('./persona_prompt');
|
|
7
|
+
|
|
8
|
+
const INTERACTION_VOICE_RULES = `Mandatory interaction-voice editing rules:
|
|
9
|
+
- Preserve every fact, number, name, date, URL, citation, command, result, uncertainty, warning, and user-relevant blocker. Never add a fact.
|
|
10
|
+
- Preserve requested formatting and substantive detail. A detailed or multi-part deliverable may remain long.
|
|
11
|
+
- Match the actual user's casing, punctuation, vocabulary, directness, and established emoji register.
|
|
12
|
+
- Short social messages should usually become one original line.
|
|
13
|
+
- Everyday advice should be direct and at most three short sentences unless nuance or safety requires more.
|
|
14
|
+
- Straightforward factual answers should usually be one compact paragraph, not a headed article or exhaustive list.
|
|
15
|
+
- Keep sensitive replies restrained. If a serious draft is already one or two attentive sentences with no advice menu, send it unchanged. Never add an offer to talk, vent, get support, or use distraction. Remove therapy language, support-option menus, unsolicited coping advice, and "I'm here if you want to talk" closers unless the user explicitly requested that support.
|
|
16
|
+
- Remove canned praise, question-grading, repeated acknowledgements, preambles, postambles, automatic follow-up questions, generic offers, and service sign-offs.
|
|
17
|
+
- If the user did not ask a question and the draft's question is only there to prolong the exchange, remove it.
|
|
18
|
+
- Keep at most one contextual joke. Never make serious material witty.
|
|
19
|
+
- A requested short draft should contain only the draft.
|
|
20
|
+
- In a group, make one brief contribution and never dominate the room.`;
|
|
21
|
+
|
|
22
|
+
const INTERACTION_EDITOR_PROMPT = `You are the final Interaction Voice editor for a personal AI messaging thread.
|
|
23
|
+
Return JSON with exactly these keys:
|
|
24
|
+
action ("send" or "revise"),
|
|
25
|
+
revisedContent (string; empty when action is send),
|
|
26
|
+
reasonCodes (array of short strings),
|
|
27
|
+
rationale (one short sentence).
|
|
28
|
+
|
|
29
|
+
The draft may be factually correct but sound like a generic assistant. You are an editor, not a second conversational partner: make the smallest necessary edit and do not replace a good draft merely to write your own response.
|
|
30
|
+
|
|
31
|
+
${INTERACTION_VOICE_RULES}
|
|
32
|
+
|
|
33
|
+
If the draft already satisfies these rules, return action "send". Otherwise return action "revise" with the complete replacement. Do not explain the edit inside revisedContent.`;
|
|
34
|
+
|
|
35
|
+
function buildSystemPromptContribution(ctx) {
|
|
36
|
+
if (!isModuleEnabled(ctx.config, 'persona')) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const dynamic = [];
|
|
40
|
+
const behaviorNotes = ctx.memoryManager && ctx.userId != null
|
|
41
|
+
? ctx.memoryManager.getAssistantBehaviorNotes(
|
|
42
|
+
ctx.userId,
|
|
43
|
+
{ agentId: ctx.agentId },
|
|
44
|
+
)
|
|
45
|
+
: '';
|
|
46
|
+
if (behaviorNotes) {
|
|
47
|
+
dynamic.push([
|
|
48
|
+
'## Assistant Behavior Notes',
|
|
49
|
+
'These are durable preferences for how the agent should usually behave. System rules and the current request take priority.',
|
|
50
|
+
behaviorNotes,
|
|
51
|
+
].join('\n'));
|
|
52
|
+
}
|
|
53
|
+
const selfState = ctx.memoryManager && ctx.userId != null
|
|
54
|
+
? ctx.memoryManager.getAssistantSelfState(
|
|
55
|
+
ctx.userId,
|
|
56
|
+
{ agentId: ctx.agentId },
|
|
57
|
+
)
|
|
58
|
+
: null;
|
|
59
|
+
const identity = selfState?.identity || {};
|
|
60
|
+
const focus = ctx.audience === 'shared' ? {} : (selfState?.focus || {});
|
|
61
|
+
if (Object.keys(identity).length || Object.keys(focus).length) {
|
|
62
|
+
dynamic.push([
|
|
63
|
+
'## Assistant Self State',
|
|
64
|
+
Object.keys(identity).length ? `Identity: ${JSON.stringify(identity)}` : '',
|
|
65
|
+
Object.keys(focus).length ? `Focus: ${JSON.stringify(focus)}` : '',
|
|
66
|
+
].filter(Boolean).join('\n'));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
stable: [BASELINE_PERSONA_PROMPT],
|
|
70
|
+
dynamic,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function refineDraft(ctx) {
|
|
75
|
+
const {
|
|
76
|
+
userId,
|
|
77
|
+
agentId,
|
|
78
|
+
msg,
|
|
79
|
+
config,
|
|
80
|
+
draft,
|
|
81
|
+
signal = null,
|
|
82
|
+
runId = null,
|
|
83
|
+
} = ctx;
|
|
84
|
+
const content = String(draft || '').trim();
|
|
85
|
+
if (
|
|
86
|
+
!content
|
|
87
|
+
|| content.toUpperCase() === '[NO RESPONSE]'
|
|
88
|
+
|| !isModuleEnabled(config, 'persona')
|
|
89
|
+
) {
|
|
90
|
+
return {
|
|
91
|
+
action: 'send',
|
|
92
|
+
content,
|
|
93
|
+
reasonCodes: ['persona_refine_skip'],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Large deliverables need their full information and structure preserved. The
|
|
98
|
+
// main prompt owns their voice; the lightweight interaction pass owns messages.
|
|
99
|
+
if (content.length > 2800) {
|
|
100
|
+
return {
|
|
101
|
+
action: 'send',
|
|
102
|
+
content,
|
|
103
|
+
reasonCodes: ['persona_refine_large_passthrough'],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const runModelId = runId
|
|
108
|
+
? ctx.agentEngine?.getRunMeta?.(runId)?.modelSelectionId || null
|
|
109
|
+
: null;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const result = await requestStructuredJson({
|
|
113
|
+
agentEngine: ctx.agentEngine,
|
|
114
|
+
userId,
|
|
115
|
+
agentId,
|
|
116
|
+
modelId: config.decisionModelId || runModelId,
|
|
117
|
+
purpose: runModelId ? 'general' : config.decisionModelPurpose,
|
|
118
|
+
system: INTERACTION_EDITOR_PROMPT,
|
|
119
|
+
prompt: JSON.stringify({
|
|
120
|
+
channel: {
|
|
121
|
+
platform: msg.platform,
|
|
122
|
+
audience: msg.isGroup ? 'shared' : 'direct',
|
|
123
|
+
},
|
|
124
|
+
inbound: truncate(msg.content, 900),
|
|
125
|
+
draft: content,
|
|
126
|
+
}),
|
|
127
|
+
signal,
|
|
128
|
+
maxTokens: 1200,
|
|
129
|
+
});
|
|
130
|
+
const parsed = result.parsed || {};
|
|
131
|
+
if (
|
|
132
|
+
parsed.action === 'revise'
|
|
133
|
+
&& String(parsed.revisedContent || '').trim()
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
action: 'revise',
|
|
137
|
+
content: String(parsed.revisedContent).trim(),
|
|
138
|
+
reasonCodes: Array.isArray(parsed.reasonCodes)
|
|
139
|
+
? parsed.reasonCodes
|
|
140
|
+
: ['persona_revise'],
|
|
141
|
+
rationale: String(parsed.rationale || '').trim(),
|
|
142
|
+
model: result.modelSelectionId || result.model || null,
|
|
143
|
+
usage: result.usage || 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
action: 'send',
|
|
148
|
+
content,
|
|
149
|
+
reasonCodes: Array.isArray(parsed.reasonCodes)
|
|
150
|
+
? parsed.reasonCodes
|
|
151
|
+
: ['persona_send'],
|
|
152
|
+
rationale: String(parsed.rationale || '').trim(),
|
|
153
|
+
model: result.modelSelectionId || result.model || null,
|
|
154
|
+
usage: result.usage || 0,
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (signal?.aborted) throw error;
|
|
158
|
+
return {
|
|
159
|
+
action: 'send',
|
|
160
|
+
content,
|
|
161
|
+
reasonCodes: ['persona_error_passthrough'],
|
|
162
|
+
failureCode: 'model_unavailable',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = {
|
|
168
|
+
id: 'persona',
|
|
169
|
+
composeSystemPrompt: buildSystemPromptContribution,
|
|
170
|
+
refineDraft,
|
|
171
|
+
INTERACTION_VOICE_RULES,
|
|
172
|
+
};
|