mioku-plugin-chat 2.3.1 → 2.4.1

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/core/prompt.ts CHANGED
@@ -19,7 +19,6 @@ export interface PromptContext {
19
19
  triggerSkillRole?: SkillPermissionRole;
20
20
  aiService: AIService;
21
21
  isGroup: boolean;
22
- // Humanize context (computed once per processChat)
23
22
  memoryContext?: string;
24
23
  topicContext?: string;
25
24
  expressionContext?: string;
@@ -28,31 +27,152 @@ export interface PromptContext {
28
27
  targetMessage: TargetMessage;
29
28
  currentEmotion?: string;
30
29
  plannerThoughts?: string;
31
- // Reply context - tells AI what type of reply this is
32
30
  replyContext?: {
33
31
  type: "reply" | "comment" | "idle" | "review" | "poked";
34
32
  targetUser?: string;
35
33
  targetMessage?: string;
36
34
  };
37
- // Review context - messages collected during cooldown period
38
35
  reviewMessages?: {
39
36
  contents: string[];
40
37
  userNames: string[];
41
38
  messageIds: number[];
42
39
  };
43
40
  promptInjections?: ChatRuntimePromptInjection[];
44
- // Emoji agent for dynamic meme info
45
41
  emojiAgent?: EmojiAgent;
46
- // Skill session manager for on-demand features
47
42
  skillManager?: SkillSessionManager;
48
43
  sessionId?: string;
49
44
  }
50
45
 
51
- /**
52
- * Build system prompt — called each iteration with updated context
53
- */
46
+ type Strength = "low" | "medium" | "high";
47
+ type ReplyType = "reply" | "comment" | "idle" | "review" | "poked";
48
+
49
+ function normalizeConstraintStrength(value: unknown): Strength {
50
+ if (value === "low" || value === "high" || value === "medium") return value;
51
+ return "medium";
52
+ }
53
+
54
+ function isMultiUserInteraction(
55
+ reviewMsgs?: PromptContext["reviewMessages"],
56
+ ): boolean {
57
+ return (
58
+ !!reviewMsgs &&
59
+ reviewMsgs.userNames.length > 1 &&
60
+ new Set(reviewMsgs.userNames).size > 1
61
+ );
62
+ }
63
+
64
+ // ==================== Strength → guidance string tables ====================
65
+ // Replaces the if/else-if/else strength chains with table lookups.
66
+
67
+ const REPLY_MULTIUSER_LENGTH: Record<Strength, string> = {
68
+ high: "Keep it extremely brief. Prefer one short sentence; max two short lines.",
69
+ medium:
70
+ "Keep it brief and conversational. One or two sentences max. Don't try to be comprehensive - just pick one thing to respond to or make a general comment that fits the vibe.",
71
+ low: "Keep it natural and focused on one key point instead of covering everything.",
72
+ };
73
+
74
+ const REPLY_SINGLE_TOOL: Record<Strength, string> = {
75
+ high: "If the user asks for facts, verification, or external info, proactively use suitable tools. Avoid guessing when tools can validate.",
76
+ medium:
77
+ "If the user asks for help, use recent chat history and suitable tools when needed to answer accurately. Avoid vague or incorrect info.",
78
+ low: "If the user asks for help, prioritize direct conversational replies first. Use tools only when clearly necessary.",
79
+ };
80
+
81
+ const REPLY_SINGLE_LENGTH: Record<Strength, string> = {
82
+ high: "Length target: one short sentence preferred, max two short lines.",
83
+ medium: "Length target: concise reply, usually within 1-2 short paragraphs.",
84
+ low: "",
85
+ };
86
+
87
+ const COMMENT_LENGTH: Record<Strength, string> = {
88
+ high: "Length target: keep it very short, ideally one sentence, max two short lines. If there are multiple messages, summarize into one brief reply.",
89
+ medium:
90
+ "Important! Messages must be concise and impactful, not exceeding two sentences. If there are multiple messages, summarize and reply concisely.",
91
+ low: "If there are multiple messages, prefer one merged response instead of replying one by one.",
92
+ };
93
+
94
+ const IDLE_LENGTH: Record<Strength, string> = {
95
+ high: `Length target: one short sentence only. Do NOT say things like "群里好久没人说话了" or "大家怎么都不说话了".`,
96
+ medium: `Important!! Please keep your messages extremely concise. Use no more than one sentence to reply to the person you most want to reply to, or two short paragraphs for a brief group-level comment. Do NOT say things like "群里好久没人说话了" or "大家怎么都不说话了".`,
97
+ low: "Reply naturally and quickly; avoid mentioning that the group was quiet.",
98
+ };
99
+
100
+ const REVIEW_MULTI_LENGTH: Record<Strength, string> = {
101
+ high: "CRITICAL: Reply once only, and keep it to one short sentence (max two short lines).",
102
+ medium:
103
+ "CRITICAL: Do NOT try to reply to each message or each person separately. Give ONE brief, casual response that fits the overall conversation. Pick one thing to comment on or just say something general. Keep it to a single sentence or two at most.",
104
+ low: "Reply once for the whole group instead of replying person-by-person.",
105
+ };
106
+
107
+ const REVIEW_SINGLE_LENGTH: Record<Strength, string> = {
108
+ high: "Respond naturally in one short message, preferably one sentence.",
109
+ medium:
110
+ "Please respond reasonably and naturally in context. Keep the message concise, since you've already said it, and it must fit in a single message.",
111
+ low: "Respond naturally in context and avoid repeating old wording.",
112
+ };
113
+
114
+ const POKED_LENGTH: Record<Strength, string> = {
115
+ high: "Keep this very short: one brief sentence.",
116
+ medium: "",
117
+ low: "",
118
+ };
119
+
120
+ const REPLY_STYLE_LENGTH: Record<Strength, string> = {
121
+ high: "Keep replies very short. Prefer one short sentence; max two short lines.",
122
+ medium:
123
+ "Keep replies concise and conversational. Avoid long paragraphs unless the topic demands it.",
124
+ low: "Keep replies natural and conversational. Do not be verbose without purpose.",
125
+ };
126
+
127
+ const AUDIO_MODE_LINE: Record<Strength, string> = {
128
+ high:
129
+ "- Use voice sparingly. Only use it when spoken delivery is clearly better than text, such as a greeting, a sharp emotional reaction, or a daily phrase.",
130
+ medium:
131
+ "- You may use voice for greetings, reactions, calls, confirmations, or comforting words, but stay selective.",
132
+ low: "- When a short spoken reaction would make the conversation feel more natural or vivid, you can use voice more freely.",
133
+ };
134
+
135
+ const MARKDOWN_MODE_LINE: Record<Strength, string> = {
136
+ high:
137
+ "- Prefer normal chat text. Use Markdown only when the reply truly needs structured presentation, such as a tutorial, comparison, detailed explanation, code sample or processing large amounts of data, such as after a web search or viewing a webpage.",
138
+ medium: "- Use Markdown when your responses require a structured presentation.",
139
+ low: "- Use Markdown freely where it can make your responses clearer.",
140
+ };
141
+
142
+ const TOOL_INTENSITY_BLOCK: Record<Strength, string> = {
143
+ high: `
144
+ ### Tool Usage Intensity
145
+ - Be proactive with tools for uncertain facts, external info, verification, and current events.
146
+ - Prefer validating with tools over guessing.
147
+ - If web searches fail to produce a useful answer after about 2-3 attempts, stop searching and reply directly based on what you already know or what you have already found.`,
148
+ medium: `
149
+ ### Tool Usage Intensity
150
+ - Use tools when clearly useful for correctness, verification, or missing context.
151
+ - If web searches still do not produce a useful answer after about 2-3 attempts, stop searching and give a direct reply instead of continuing to try more keywords.`,
152
+ low: `
153
+ ### Tool Usage Intensity
154
+ - Prefer direct chat responses first.
155
+ - Use tools only when strictly necessary.`,
156
+ };
157
+
158
+ const EMOJI_MODE_LINE: Record<Strength, string> = {
159
+ high:
160
+ "- Keep stickers rare. Use one only when it clearly strengthens a strong emotional beat or punchline.",
161
+ medium:
162
+ "- You may use a sticker for obvious emotional beats, reactions, jokes, teasing, or celebrations, but do not overuse it.",
163
+ low: "- When it helps the emotional effect of the reply, you can use a matching sticker more freely.",
164
+ };
165
+
166
+ const WEB_SEARCH_LINE: Record<Strength, string> = {
167
+ high:
168
+ "- When facts may be outdated or uncertain, proactively call web_search instead of guessing.",
169
+ medium: "- Use web_search when current or external info is needed.",
170
+ low: "- Use web_search only when the user explicitly needs external/current information.",
171
+ };
172
+
173
+ // ==================== System prompt assembly ====================
174
+
54
175
  export function buildSystemPrompt(ctx: PromptContext): string {
55
- const sections: string[] = [];
56
176
  const lengthStrength = normalizeConstraintStrength(
57
177
  ctx.config.outputLengthConstraintStrength,
58
178
  );
@@ -69,12 +189,10 @@ export function buildSystemPrompt(ctx: PromptContext): string {
69
189
  ctx.config.markdownUsageConstraintStrength,
70
190
  );
71
191
 
72
- // 1. Extra Info — loaded external skills
73
- if (ctx.activeSkillsInfo) {
74
- sections.push(ctx.activeSkillsInfo);
75
- }
192
+ const sections: string[] = [];
193
+
194
+ if (ctx.activeSkillsInfo) sections.push(ctx.activeSkillsInfo);
76
195
 
77
- // 2. Expression Habits
78
196
  if (ctx.expressionContext) {
79
197
  logger.info(
80
198
  `[buildSystemPrompt] Adding expressionContext (${ctx.expressionContext.length} chars) for user`,
@@ -86,7 +204,6 @@ export function buildSystemPrompt(ctx: PromptContext): string {
86
204
  );
87
205
  }
88
206
 
89
- // 3. Memory Retrieval Results
90
207
  if (ctx.memoryContext) {
91
208
  logger.info(
92
209
  `[buildSystemPrompt] Adding memoryContext (${ctx.memoryContext.length} chars)`,
@@ -100,27 +217,15 @@ export function buildSystemPrompt(ctx: PromptContext): string {
100
217
  );
101
218
  }
102
219
 
103
- // 4. Background Topics Outside Visible History
104
- if (ctx.topicContext) {
105
- sections.push(ctx.topicContext);
106
- }
107
-
108
- // 5. Slang Dictionary (placeholder)
109
- // TODO: slang dictionary injection
220
+ if (ctx.topicContext) sections.push(ctx.topicContext);
110
221
 
111
- // 6. Current Time & Environment
112
222
  sections.push(buildEnvironmentSection(ctx));
113
-
114
- // 7. Chat History
115
223
  sections.push(buildChatHistorySection(ctx));
116
-
117
- // 8. Target Message
118
224
  sections.push(
119
225
  buildTargetMessageSection(ctx.targetMessage, ctx.reviewMessages),
120
226
  );
121
227
  sections.push(...buildInjectedSections(ctx.promptInjections));
122
228
 
123
- // 9. Reply Context - tells AI what kind of reply this is
124
229
  if (ctx.replyContext) {
125
230
  sections.push(
126
231
  buildReplyContextSection(
@@ -132,21 +237,13 @@ export function buildSystemPrompt(ctx: PromptContext): string {
132
237
  );
133
238
  }
134
239
 
135
- // 10. Planner's Thoughts
136
240
  if (ctx.plannerThoughts) {
137
241
  sections.push(`## Planner's Analysis\n${ctx.plannerThoughts}`);
138
242
  }
139
243
 
140
- // 11. Persona
141
244
  sections.push(buildPersonaSection(ctx));
142
-
143
- // 12. Emotion
144
245
  sections.push(buildEmotionSection(ctx));
145
-
146
- // 13. Reply Style + Behavior + Self-Protection
147
246
  sections.push(buildReplyStyleSection(ctx, lengthStrength));
148
-
149
- // 14. Available Tools & Response Format
150
247
  sections.push(
151
248
  buildResponseFormatSection(
152
249
  ctx,
@@ -161,198 +258,99 @@ export function buildSystemPrompt(ctx: PromptContext): string {
161
258
  return sections.join("\n\n");
162
259
  }
163
260
 
164
- // ==================== Section Builders ====================
165
-
166
- type ConstraintStrength = "low" | "medium" | "high";
167
-
168
261
  function buildInjectedSections(
169
262
  injections: ChatRuntimePromptInjection[] | undefined,
170
263
  ): string[] {
171
- if (!injections || injections.length === 0) {
172
- return [];
173
- }
174
-
264
+ if (!injections || injections.length === 0) return [];
175
265
  return injections.map((injection, index) => {
176
266
  const title = injection.title || `Runtime Instruction ${index + 1}`;
177
267
  return `## ${title}\n${injection.content}`;
178
268
  });
179
269
  }
180
270
 
181
- function normalizeConstraintStrength(value: unknown): ConstraintStrength {
182
- if (value === "low" || value === "high" || value === "medium") {
183
- return value;
184
- }
185
- return "medium";
186
- }
271
+ // ==================== Reply context ====================
187
272
 
188
273
  function buildReplyContextSection(
189
274
  replyCtx: PromptContext["replyContext"],
190
- reviewMsgs?: PromptContext["reviewMessages"],
191
- lengthStrength: ConstraintStrength = "medium",
192
- toolStrength: ConstraintStrength = "medium",
275
+ reviewMsgs: PromptContext["reviewMessages"],
276
+ lengthStrength: Strength,
277
+ toolStrength: Strength,
193
278
  ): string {
194
279
  if (!replyCtx) return "";
280
+ const isMulti = isMultiUserInteraction(reviewMsgs);
281
+ const builders: Record<ReplyType, () => string[]> = {
282
+ reply: () => buildReplyGuidance(isMulti, lengthStrength, toolStrength),
283
+ comment: () => buildCommentGuidance(lengthStrength),
284
+ idle: () => buildIdleGuidance(lengthStrength),
285
+ review: () => buildReviewGuidance(isMulti, lengthStrength),
286
+ poked: () => buildPokedGuidance(lengthStrength),
287
+ };
288
+ return ["## This Response Context", ...builders[replyCtx.type]()].join("\n");
289
+ }
195
290
 
196
- const lines = [`## This Response Context`];
291
+ function buildReplyGuidance(
292
+ isMulti: boolean,
293
+ length: Strength,
294
+ tool: Strength,
295
+ ): string[] {
296
+ if (isMulti) {
297
+ return [
298
+ "Multiple people are interacting with you at the same time. You see messages from several group members directed at you.",
299
+ "IMPORTANT: Do NOT reply to each person individually or try to address every single message. Instead, give a SINGLE, unified response that acknowledges the group as a whole. Be casual and natural - like you're talking to a group of friends, not giving individual responses.",
300
+ REPLY_MULTIUSER_LENGTH[length],
301
+ ];
302
+ }
303
+ return [
304
+ "Someone mentioned you in the group, maybe like you asked a certain question, or just wanted to tease you.",
305
+ REPLY_SINGLE_TOOL[tool],
306
+ "If a user doesn't have a real problem and is just trying to tease you, don't get annoyed. Use the group chat history to infer intent and join naturally. If a user is provocative or insulting, respond humorously but politely.",
307
+ REPLY_SINGLE_LENGTH[length],
308
+ ].filter(Boolean);
309
+ }
197
310
 
198
- // Check if this is a multi-user interaction (reviewMessages has multiple different users)
199
- const isMultiUserInteraction =
200
- reviewMsgs &&
201
- reviewMsgs.userNames.length > 1 &&
202
- Array.from(new Set(reviewMsgs.userNames)).length > 1;
203
-
204
- switch (replyCtx.type) {
205
- case "reply":
206
- if (isMultiUserInteraction) {
207
- lines.push(
208
- `Multiple people are interacting with you at the same time. You see messages from several group members directed at you.`,
209
- );
210
- lines.push(
211
- `IMPORTANT: Do NOT reply to each person individually or try to address every single message. Instead, give a SINGLE, unified response that acknowledges the group as a whole. Be casual and natural - like you're talking to a group of friends, not giving individual responses.`,
212
- );
213
- if (lengthStrength === "high") {
214
- lines.push(
215
- `Keep it extremely brief. Prefer one short sentence; max two short lines.`,
216
- );
217
- } else if (lengthStrength === "medium") {
218
- lines.push(
219
- `Keep it brief and conversational. One or two sentences max. Don't try to be comprehensive - just pick one thing to respond to or make a general comment that fits the vibe.`,
220
- );
221
- } else {
222
- lines.push(
223
- `Keep it natural and focused on one key point instead of covering everything.`,
224
- );
225
- }
226
- } else {
227
- lines.push(
228
- `Someone mentioned you in the group, maybe like you asked a certain question, or just wanted to tease you.`,
229
- );
230
- if (toolStrength === "high") {
231
- lines.push(
232
- `If the user asks for facts, verification, or external info, proactively use suitable tools. Avoid guessing when tools can validate.`,
233
- );
234
- } else if (toolStrength === "medium") {
235
- lines.push(
236
- `If the user asks for help, use recent chat history and suitable tools when needed to answer accurately. Avoid vague or incorrect info.`,
237
- );
238
- } else {
239
- lines.push(
240
- `If the user asks for help, prioritize direct conversational replies first. Use tools only when clearly necessary.`,
241
- );
242
- }
243
- lines.push(
244
- `If a user doesn't have a real problem and is just trying to tease you, don't get annoyed. Use the group chat history to infer intent and join naturally. If a user is provocative or insulting, respond humorously but politely.`,
245
- );
246
- if (lengthStrength === "high") {
247
- lines.push(
248
- `Length target: one short sentence preferred, max two short lines.`,
249
- );
250
- } else if (lengthStrength === "medium") {
251
- lines.push(
252
- `Length target: concise reply, usually within 1-2 short paragraphs.`,
253
- );
254
- }
255
- }
256
- break;
257
- case "comment":
258
- lines.push(
259
- `If someone adds or comments after you reply to the previous message, please carefully read the group chat history and analyze your reply. Provide a reasonable and natural response to the user's comment, and do not repeat what you already said or a particular viewpoint.`,
260
- );
261
- if (lengthStrength === "high") {
262
- lines.push(
263
- `Length target: keep it very short, ideally one sentence, max two short lines. If there are multiple messages, summarize into one brief reply.`,
264
- );
265
- } else if (lengthStrength === "medium") {
266
- lines.push(
267
- `Important! Messages must be concise and impactful, not exceeding two sentences. If there are multiple messages, summarize and reply concisely.`,
268
- );
269
- } else {
270
- lines.push(
271
- `If there are multiple messages, prefer one merged response instead of replying one by one.`,
272
- );
273
- }
274
- break;
275
- case "idle":
276
- lines.push(
277
- `No one spoke in the group for a long time, so you decided to chime in.`,
278
- );
279
- lines.push(
280
- `First, observe the chat history in the group. If there is any content related to your persona that you are interested in, consider replying. Next, observe if any group members have unresolved questions. If not, then observe the chat style of the group members and send messages that naturally blend into their conversations. You can even repeat a funny message sent by a group member or a phrase that appears repeatedly in the chat history.`,
281
- );
282
- if (lengthStrength === "high") {
283
- lines.push(
284
- `Length target: one short sentence only. Do NOT say things like "群里好久没人说话了" or "大家怎么都不说话了".`,
285
- );
286
- } else if (lengthStrength === "medium") {
287
- lines.push(
288
- `Important!! Please keep your messages extremely concise. Use no more than one sentence to reply to the person you most want to reply to, or two short paragraphs for a brief group-level comment. Do NOT say things like "群里好久没人说话了" or "大家怎么都不说话了".`,
289
- );
290
- } else {
291
- lines.push(
292
- `Reply naturally and quickly; avoid mentioning that the group was quiet.`,
293
- );
294
- }
295
- break;
296
- case "review":
297
- if (isMultiUserInteraction) {
298
- lines.push(
299
- `Multiple people have sent you messages while you were away. You see a batch of messages from different group members.`,
300
- );
301
- if (lengthStrength === "high") {
302
- lines.push(
303
- `CRITICAL: Reply once only, and keep it to one short sentence (max two short lines).`,
304
- );
305
- } else if (lengthStrength === "medium") {
306
- lines.push(
307
- `CRITICAL: Do NOT try to reply to each message or each person separately. Give ONE brief, casual response that fits the overall conversation. Pick one thing to comment on or just say something general. Keep it to a single sentence or two at most.`,
308
- );
309
- } else {
310
- lines.push(
311
- `Reply once for the whole group instead of replying person-by-person.`,
312
- );
313
- }
314
- } else {
315
- lines.push(
316
- `After you reply to other group members' messages, some people have new questions or replies to your answers.`,
317
- );
318
- if (lengthStrength === "high") {
319
- lines.push(
320
- `Respond naturally in one short message, preferably one sentence.`,
321
- );
322
- } else if (lengthStrength === "medium") {
323
- lines.push(
324
- `Please respond reasonably and naturally in context. Keep the message concise, since you've already said it, and it must fit in a single message.`,
325
- );
326
- } else {
327
- lines.push(
328
- `Respond naturally in context and avoid repeating old wording.`,
329
- );
330
- }
331
- }
332
- break;
333
- case "poked":
334
- lines.push(
335
- `Someone pokes you in a group, probably out of non-malicious play or to draw your attention to what happened in the group chat.`,
336
- );
337
- lines.push(
338
- `Don't make a fuss about replying, just observe whether the chat history in the group has noteworthy content, and if not, simply say hello or express concern to the user.`,
339
- );
340
- lines.push(
341
- `Reply naturally in combination with the context, don't say something like "怎么又来戳我了"`,
342
- );
343
- if (lengthStrength === "high") {
344
- lines.push(`Keep this very short: one brief sentence.`);
345
- }
346
- break;
311
+ function buildCommentGuidance(length: Strength): string[] {
312
+ return [
313
+ "If someone adds or comments after you reply to the previous message, please carefully read the group chat history and analyze your reply. Provide a reasonable and natural response to the user's comment, and do not repeat what you already said or a particular viewpoint.",
314
+ COMMENT_LENGTH[length],
315
+ ].filter(Boolean);
316
+ }
317
+
318
+ function buildIdleGuidance(length: Strength): string[] {
319
+ return [
320
+ "No one spoke in the group for a long time, so you decided to chime in.",
321
+ "First, observe the chat history in the group. If there is any content related to your persona that you are interested in, consider replying. Next, observe if any group members have unresolved questions. If not, then observe the chat style of the group members and send messages that naturally blend into their conversations. You can even repeat a funny message sent by a group member or a phrase that appears repeatedly in the chat history.",
322
+ IDLE_LENGTH[length],
323
+ ].filter(Boolean);
324
+ }
325
+
326
+ function buildReviewGuidance(isMulti: boolean, length: Strength): string[] {
327
+ if (isMulti) {
328
+ return [
329
+ "Multiple people have sent you messages while you were away. You see a batch of messages from different group members.",
330
+ REVIEW_MULTI_LENGTH[length],
331
+ ];
347
332
  }
333
+ return [
334
+ "After you reply to other group members' messages, some people have new questions or replies to your answers.",
335
+ REVIEW_SINGLE_LENGTH[length],
336
+ ];
337
+ }
348
338
 
349
- return lines.join("\n");
339
+ function buildPokedGuidance(length: Strength): string[] {
340
+ return [
341
+ "Someone pokes you in a group, probably out of non-malicious play or to draw your attention to what happened in the group chat.",
342
+ "Don't make a fuss about replying, just observe whether the chat history in the group has noteworthy content, and if not, simply say hello or express concern to the user.",
343
+ "Reply naturally in combination with the context, don't say something like \"怎么又来戳我了\"",
344
+ POKED_LENGTH[length],
345
+ ].filter(Boolean);
350
346
  }
351
347
 
348
+ // ==================== Other sections ====================
349
+
352
350
  function buildEnvironmentSection(ctx: PromptContext): string {
353
351
  const now = new Date();
354
352
  const timeStr = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")} ${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
355
- const dayNames = [
353
+ const dayOfWeek = [
356
354
  "Sunday",
357
355
  "Monday",
358
356
  "Tuesday",
@@ -360,8 +358,7 @@ function buildEnvironmentSection(ctx: PromptContext): string {
360
358
  "Thursday",
361
359
  "Friday",
362
360
  "Saturday",
363
- ];
364
- const dayOfWeek = dayNames[now.getDay()];
361
+ ][now.getDay()];
365
362
 
366
363
  const lines = [
367
364
  `## Current Time & Environment`,
@@ -369,12 +366,12 @@ function buildEnvironmentSection(ctx: PromptContext): string {
369
366
  ];
370
367
 
371
368
  if (ctx.isGroup) {
372
- lines.push(`Chat type: Group chat`);
369
+ lines.push("Chat type: Group chat");
373
370
  if (ctx.groupName) lines.push(`Group name: ${ctx.groupName}`);
374
371
  if (ctx.memberCount) lines.push(`Member count: ${ctx.memberCount}`);
375
372
  lines.push(`Your role in group: ${ctx.botRole}`);
376
373
  } else {
377
- lines.push(`Chat type: Private chat`);
374
+ lines.push("Chat type: Private chat");
378
375
  }
379
376
 
380
377
  return lines.join("\n");
@@ -388,94 +385,71 @@ function buildChatHistorySection(ctx: PromptContext): string {
388
385
  let currentAssistantBlock: { timeStr: string; contents: string[] } | null =
389
386
  null;
390
387
 
388
+ const flushAssistant = () => {
389
+ if (!currentAssistantBlock) return;
390
+ mergedLines.push(
391
+ `[${currentAssistantBlock.timeStr}] ${ctx.botNickname}: ${currentAssistantBlock.contents.join(" | ")}`,
392
+ );
393
+ currentAssistantBlock = null;
394
+ };
395
+
391
396
  for (const msg of chatHistory) {
392
397
  const time = new Date(msg.timestamp);
393
398
  const timeStr = `${String(time.getMonth() + 1).padStart(2, "0")}-${String(time.getDate()).padStart(2, "0")} ${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
394
399
 
395
400
  if (msg.role === "assistant") {
396
401
  if (currentAssistantBlock && currentAssistantBlock.timeStr === timeStr) {
397
- // Same timestamp, add to current block
398
402
  currentAssistantBlock.contents.push(msg.content);
399
403
  } else {
400
- // New assistant block
401
- if (currentAssistantBlock) {
402
- // Flush previous block
403
- const mergedContent = currentAssistantBlock.contents.join(" | ");
404
- mergedLines.push(
405
- `[${currentAssistantBlock.timeStr}] ${ctx.botNickname}: ${mergedContent}`,
406
- );
407
- }
404
+ flushAssistant();
408
405
  currentAssistantBlock = { timeStr, contents: [msg.content] };
409
406
  }
410
- } else {
411
- // Flush assistant block if exists
412
- if (currentAssistantBlock) {
413
- const mergedContent = currentAssistantBlock.contents.join(" | ");
414
- mergedLines.push(
415
- `[${currentAssistantBlock.timeStr}] ${ctx.botNickname}: ${mergedContent}`,
416
- );
417
- currentAssistantBlock = null;
418
- }
419
-
420
- const name = msg.userName || "unknown";
421
- const roleLabel =
422
- msg.userRole === "owner"
423
- ? "Owner"
424
- : msg.userRole === "admin"
425
- ? "Admin"
426
- : "Member";
427
- const titleStr = msg.userTitle ? `, ${msg.userTitle}` : "";
428
- const qqStr = msg.userId ? `${msg.userId}` : "";
429
- const msgIdStr = msg.messageId ? ` #${msg.messageId}` : "";
430
-
431
- mergedLines.push(
432
- `[${timeStr}] ${name}(${qqStr}, ${roleLabel}${titleStr})${msgIdStr}): ${msg.content}`,
433
- );
407
+ continue;
434
408
  }
435
- }
436
409
 
437
- if (currentAssistantBlock) {
438
- const mergedContent = currentAssistantBlock.contents.join(" | ");
410
+ flushAssistant();
411
+ const name = msg.userName || "unknown";
412
+ const roleLabel =
413
+ msg.userRole === "owner"
414
+ ? "Owner"
415
+ : msg.userRole === "admin"
416
+ ? "Admin"
417
+ : "Member";
418
+ const titleStr = msg.userTitle ? `, ${msg.userTitle}` : "";
419
+ const qqStr = msg.userId ? `${msg.userId}` : "";
420
+ const msgIdStr = msg.messageId ? ` #${msg.messageId}` : "";
439
421
  mergedLines.push(
440
- `[${currentAssistantBlock.timeStr}] ${ctx.botNickname}: ${mergedContent}`,
422
+ `[${timeStr}] ${name}(${qqStr}, ${roleLabel}${titleStr})${msgIdStr}): ${msg.content}`,
441
423
  );
442
424
  }
425
+ flushAssistant();
443
426
 
444
427
  return `## Recent Context (Only reference if directly relevant)
445
428
  Just the last few messages - don't overthink it or dig into old conversations:
446
429
 
447
430
  ${mergedLines.join("\n")}
448
431
 
449
- Note: Messages may contain media tags like [meme:描述], [image:描述], [video:描述], [forward:摘要], [card:摘要], or [group_notice:摘要]. These are brief processed summaries. If you need detailed information about an image, use the view_image tool with the message ID.
432
+ Note: Messages may contain media tags like [meme:描述], [image:描述], [video:描述], [forward:摘要], [card:摘要], or [group_notice:摘要]. These are brief processed summaries. If you need detailed information about an image or video, use the view_media tool with the message ID.
450
433
 
451
434
  -- DON'T repeat yourself or bring up old topics - focus on what's being said right now. --`;
452
435
  }
453
436
 
454
437
  function buildTargetMessageSection(
455
438
  target: TargetMessage,
456
- reviewMsgs?: PromptContext["reviewMessages"],
439
+ reviewMsgs: PromptContext["reviewMessages"],
457
440
  ): string {
458
441
  const time = new Date(target.timestamp);
459
442
  const timeStr = `${String(time.getMonth() + 1).padStart(2, "0")}-${String(time.getDate()).padStart(2, "0")} ${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
460
443
  const msgIdStr = target.messageId ? ` #${target.messageId}` : "";
461
444
 
462
- const isMultiUserInteraction =
463
- reviewMsgs &&
464
- reviewMsgs.userNames.length > 1 &&
465
- new Set(reviewMsgs.userNames).size > 1;
466
-
467
- if (isMultiUserInteraction && reviewMsgs) {
445
+ if (isMultiUserInteraction(reviewMsgs) && reviewMsgs) {
468
446
  const uniqueUsers = Array.from(new Set(reviewMsgs.userNames));
469
447
  const userList = uniqueUsers.join(", ");
470
-
471
- const messageBlocks: string[] = [];
472
- for (let i = 0; i < reviewMsgs.contents.length; i++) {
473
- const userName = reviewMsgs.userNames[i];
474
- const content = reviewMsgs.contents[i];
448
+ const messageBlocks = reviewMsgs.contents.map((content, i) => {
475
449
  const msgId = reviewMsgs.messageIds[i];
476
450
  const msgIdLabel = msgId ? ` #${msgId}` : "";
477
- messageBlocks.push(`[${userName}${msgIdLabel}]: ${content}`);
478
- }
451
+ return `[${reviewMsgs.userNames[i]}${msgIdLabel}]: ${content}`;
452
+ });
479
453
 
480
454
  return `## >>> Multiple People Are Interacting With You <<<
481
455
  ${userList} sent you messages at around ${timeStr}:
@@ -490,12 +464,8 @@ IMPORTANT: You do NOT need to reply to each person or each message above. Give O
490
464
  }
491
465
 
492
466
  function buildPersonaSection(ctx: PromptContext): string {
493
- const lines = [`## Persona`];
494
-
495
- if (ctx.config.persona) {
496
- lines.push(ctx.config.persona);
497
- }
498
-
467
+ const lines = ["## Persona"];
468
+ if (ctx.config.persona) lines.push(ctx.config.persona);
499
469
  return lines.join("\n");
500
470
  }
501
471
 
@@ -519,20 +489,19 @@ function buildEmotionSection(ctx: PromptContext): string {
519
489
  );
520
490
  const examples =
521
491
  currentExamples.length > 0 ? currentExamples : fallbackExamples;
522
- const lines = [`## Emotion State`, `Current emotion: ${currentEmotion}`];
492
+ const lines = ["## Emotion State", `Current emotion: ${currentEmotion}`];
523
493
 
524
494
  if (availableEmotions.length > 0) {
525
495
  lines.push(`Available emotions: ${availableEmotions.join(", ")}`);
526
496
  }
527
-
528
497
  lines.push(
529
- `You may switch your emotion state by writing [emotion:emotion_name]. The marker is not sent to the chat. Only use available emotions.`,
498
+ "You may switch your emotion state by writing [emotion:emotion_name]. The marker is not sent to the chat. Only use available emotions.",
530
499
  );
531
500
 
532
501
  if (examples.length > 0) {
533
502
  lines.push(
534
- `For examples of responses to current emotions, please refer to their tone and speech characteristics.`,
535
- `Be sure to imitate their tone and speaking characteristics, including sentence length, pauses within sentences, and the use of punctuation`,
503
+ "For examples of responses to current emotions, please refer to their tone and speech characteristics.",
504
+ "Be sure to imitate their tone and speaking characteristics, including sentence length, pauses within sentences, and the use of punctuation",
536
505
  examples.map((example) => `- ${example}`).join("\n"),
537
506
  );
538
507
  }
@@ -541,9 +510,7 @@ function buildEmotionSection(ctx: PromptContext): string {
541
510
  }
542
511
 
543
512
  function normalizeEmotionName(value: unknown): string {
544
- return String(value || "")
545
- .trim()
546
- .toLowerCase();
513
+ return String(value || "").trim().toLowerCase();
547
514
  }
548
515
 
549
516
  function normalizeEmotionExamples(value: unknown): string[] {
@@ -553,25 +520,16 @@ function normalizeEmotionExamples(value: unknown): string[] {
553
520
 
554
521
  function buildReplyStyleSection(
555
522
  ctx: PromptContext,
556
- lengthStrength: ConstraintStrength,
523
+ lengthStrength: Strength,
557
524
  ): string {
558
525
  const style = pickReplyStyle(ctx.config);
559
- const lines = [`## Reply Style`];
560
-
561
- if (style) {
562
- lines.push(`Current style: ${style}`);
563
- }
526
+ const lines = ["## Reply Style"];
527
+ if (style) lines.push(`Current style: ${style}`);
564
528
 
565
529
  lines.push(`
566
530
  ### Behavior Guidelines
567
531
  - You are a participant in this chat, not an assistant. Chat naturally like a real person.
568
- - ${
569
- lengthStrength === "high"
570
- ? "Keep replies very short. Prefer one short sentence; max two short lines."
571
- : lengthStrength === "medium"
572
- ? "Keep replies concise and conversational. Avoid long paragraphs unless the topic demands it."
573
- : "Keep replies natural and conversational. Do not be verbose without purpose."
574
- }
532
+ - ${REPLY_STYLE_LENGTH[lengthStrength]}
575
533
  - Match the language used by others in the chat (if they speak Chinese, reply in Chinese, etc.).
576
534
  - Don't repeat yourself or echo what others just said.
577
535
  - **NEVER use action descriptions like *xxx* or (xxx) — just speak as a normal person would**
@@ -590,11 +548,11 @@ function buildReplyStyleSection(
590
548
 
591
549
  function buildResponseFormatSection(
592
550
  ctx: PromptContext,
593
- lengthStrength: ConstraintStrength,
594
- toolStrength: ConstraintStrength,
595
- emojiStrength: ConstraintStrength,
596
- audioStrength: ConstraintStrength,
597
- markdownStrength: ConstraintStrength,
551
+ _lengthStrength: Strength,
552
+ toolStrength: Strength,
553
+ emojiStrength: Strength,
554
+ audioStrength: Strength,
555
+ markdownStrength: Strength,
598
556
  ): string {
599
557
  const lines = [`## Response Format`];
600
558
 
@@ -606,7 +564,7 @@ function buildResponseFormatSection(
606
564
  - If you want to send multiple messages, just press Enter and write the next line
607
565
  - Each line = one message sent to the chat
608
566
  - **If your reply has multiple sentences or different points, ALWAYS use real line breaks to separate them**
609
- - NEVER use "\" or literal "\\n" to simulate a new line
567
+ - NEVER use "\\" or literal "\\n" to simulate a new line
610
568
  - **MESSAGE ORDER MATTERS**: messages are sent top-to-bottom, one line at a time.
611
569
  - For action markers like [meme:...] or [audio:...], put them on their own line when they are meant to be a separate action.
612
570
 
@@ -617,14 +575,7 @@ function buildResponseFormatSection(
617
575
  - **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**
618
576
  - These markers will be automatically parsed and removed from your sent message`);
619
577
 
620
- // Audio section - always attached when enabled
621
578
  if (ctx.config.audio?.enabled && ctx.config.audio.baseUrl?.trim()) {
622
- const audioModeLine =
623
- audioStrength === "high"
624
- ? "- Use voice sparingly. Only use it when spoken delivery is clearly better than text, such as a greeting, a sharp emotional reaction, or a daily phrase."
625
- : audioStrength === "medium"
626
- ? "- You may use voice for greetings, reactions, calls, confirmations, or comforting words, but stay selective."
627
- : "- When a short spoken reaction would make the conversation feel more natural or vivid, you can use voice more freely.";
628
579
  lines.push(`
629
580
  ### Optional Voice Message Format
630
581
  - You MAY optionally send one voice message by writing [audio:content]
@@ -632,44 +583,21 @@ function buildResponseFormatSection(
632
583
  The voice message function sends plain text and cannot be used for singing. If a user needs you to sing, other skills should be considered first.
633
584
  - Put [audio:...] on its own line when you want it sent as a separate message in sequence
634
585
  - Example: "[audio:おはようー]"
635
- ${audioModeLine}`);
586
+ ${AUDIO_MODE_LINE[audioStrength]}`);
636
587
  }
637
588
 
638
- // Markdown section - always attached when enabled
639
589
  if (ctx.config.enableMarkdownScreenshot) {
640
- const markdownModeLine =
641
- markdownStrength === "high"
642
- ? "- Prefer normal chat text. Use Markdown only when the reply truly needs structured presentation, such as a tutorial, comparison, detailed explanation, code sample or processing large amounts of data, such as after a web search or viewing a webpage."
643
- : markdownStrength === "medium"
644
- ? "- Use Markdown when your responses require a structured presentation."
645
- : "- Use Markdown freely where it can make your responses clearer.";
646
590
  lines.push(`
647
591
  ### Optional Markdown Screenshot Format
648
592
  - You MAY optionally send one rendered Markdown screenshot by wrapping content with exact tags: <MARKDOWN> ... </MARKDOWN>
649
593
  - Put the Markdown block on its own message whenever possible.
650
594
  - It is forbidden to use Markdown syntax or formulas in plain text; they must be rendered using <MARKDOWN> blocks.
651
- ${markdownModeLine}
595
+ ${MARKDOWN_MODE_LINE[markdownStrength]}
652
596
  - Inside <MARKDOWN>...</MARKDOWN>, there is NO length limit. If the user needs detail, explain clearly and thoroughly instead of over-compressing.
653
597
  `);
654
598
  }
655
599
 
656
- if (toolStrength === "high") {
657
- lines.push(`
658
- ### Tool Usage Intensity
659
- - Be proactive with tools for uncertain facts, external info, verification, and current events.
660
- - Prefer validating with tools over guessing.
661
- - If web searches fail to produce a useful answer after about 2-3 attempts, stop searching and reply directly based on what you already know or what you have already found.`);
662
- } else if (toolStrength === "medium") {
663
- lines.push(`
664
- ### Tool Usage Intensity
665
- - Use tools when clearly useful for correctness, verification, or missing context.
666
- - If web searches still do not produce a useful answer after about 2-3 attempts, stop searching and give a direct reply instead of continuing to try more keywords.`);
667
- } else {
668
- lines.push(`
669
- ### Tool Usage Intensity
670
- - Prefer direct chat responses first.
671
- - Use tools only when strictly necessary.`);
672
- }
600
+ lines.push(TOOL_INTENSITY_BLOCK[toolStrength]);
673
601
 
674
602
  lines.push(`
675
603
  ### Tool Calling Format
@@ -677,7 +605,6 @@ ${markdownModeLine}
677
605
  - Do NOT output tool calls, tool names, or tool arguments in your reply text under any circumstances
678
606
  - Do NOT use XML, JSON, or any text format to describe tool calls — only use the API's tool_calls field`);
679
607
 
680
- // Memory Recall section - only when "recall_memory" feature is loaded
681
608
  const activeFeatures = getActiveFeatureNames(ctx);
682
609
  if (activeFeatures.includes("recall_memory") && ctx.config.memory?.enabled) {
683
610
  lines.push(`
@@ -688,62 +615,16 @@ ${markdownModeLine}
688
615
  - The worker returns historical logs with timestamps; treat them as past records, not newly sent messages.`);
689
616
  }
690
617
 
691
- const emojiAgent = ctx.emojiAgent;
692
- if (emojiAgent && ctx.config.emoji?.enabled) {
693
- const configChars = ctx.config.emoji.characters || [];
694
- let availableEmotions: string[] = [];
695
-
696
- if (configChars.length > 0) {
697
- for (const char of configChars) {
698
- const emotions = emojiAgent.getAvailableEmotions(char);
699
- availableEmotions.push(...emotions);
700
- }
701
- } else {
702
- const allChars = emojiAgent.getAvailableCharacters();
703
- for (const char of allChars) {
704
- const emotions = emojiAgent.getAvailableEmotions(char);
705
- availableEmotions.push(...emotions);
706
- }
707
- }
618
+ appendEmojiSection(lines, ctx, emojiStrength);
708
619
 
709
- const uniqueEmotions = [...new Set(availableEmotions)].sort();
710
- if (uniqueEmotions.length > 0) {
711
- const emojiModeLine =
712
- emojiStrength === "high"
713
- ? "- Keep stickers rare. Use one only when it clearly strengthens a strong emotional beat or punchline."
714
- : emojiStrength === "medium"
715
- ? "- You may use a sticker for obvious emotional beats, reactions, jokes, teasing, or celebrations, but do not overuse it."
716
- : "- When it helps the emotional effect of the reply, you can use a matching sticker more freely.";
717
- lines.push(`
718
- ### Optional Sticker / Emoji Format
719
- - You MAY optionally send one matching sticker by writing [meme:emotion]
720
- ${emojiModeLine}
721
- - Do NOT send a sticker in every reply, and do not force one when the mood is plain
722
- - Prefer one matching sticker at most. It should enhance the text instead of replacing meaningful content
723
- - Put [meme:...] on its own line when it should be a separate action message after text
724
- - Available emotions: ${uniqueEmotions.join(", ")}`);
725
- }
726
- }
727
-
728
- // Web search tool note - only when "web_search" feature is loaded
729
620
  if (activeFeatures.includes("web_search") && ctx.config.searxng?.enabled) {
730
- const searxngLine =
731
- toolStrength === "high"
732
- ? "- When facts may be outdated or uncertain, proactively call web_search instead of guessing."
733
- : toolStrength === "medium"
734
- ? "- Use web_search when current or external info is needed."
735
- : "- Use web_search only when the user explicitly needs external/current information.";
736
621
  lines.push(`
737
622
  ### Web Search Tool
738
623
  - web_search: Use this when you need current or external information that is not in chat history.
739
- ${searxngLine}`);
624
+ ${WEB_SEARCH_LINE[toolStrength]}`);
740
625
  }
741
626
 
742
- // Web reading tool note - only when "web_read_page" feature is loaded
743
- if (
744
- activeFeatures.includes("web_read_page") &&
745
- ctx.config.webReader?.enabled
746
- ) {
627
+ if (activeFeatures.includes("web_read_page") && ctx.config.webReader?.enabled) {
747
628
  const independentUseLine = ctx.config.searxng?.enabled
748
629
  ? "- web_search and web_read_page are independent. Use web_search when you need to discover URLs; use web_read_page directly when the user already gave a URL."
749
630
  : "- web_read_page can be used directly when the user provides a URL.";
@@ -754,60 +635,85 @@ ${independentUseLine}
754
635
  - Only set render_js=true when the page clearly needs JavaScript rendering, because it costs much more CPU and memory.`);
755
636
  }
756
637
 
757
- // External skills note
758
- if (ctx.config.enableExternalSkills) {
759
- const skillsMap = ctx.aiService.getAllSkills?.();
760
- const skillEntries = skillsMap
761
- ? filterAllowedExternalSkills(
762
- ctx.config,
763
- [...skillsMap.values()],
764
- ctx.triggerSkillRole ?? "member",
765
- )
766
- : [];
767
-
768
- const builtinFeatureNames: string[] = [];
769
- const builtinFeatureDescs: string[] = [];
770
-
771
- if (ctx.config.searxng?.enabled) {
772
- builtinFeatureNames.push("web_search");
773
- builtinFeatureDescs.push("- web_search: 进行网页搜索");
774
- }
775
- if (ctx.config.webReader?.enabled) {
776
- builtinFeatureNames.push("web_read_page");
777
- builtinFeatureDescs.push("- web_read_page: 读取某个网页URL的内容");
778
- }
779
- if (ctx.config.memory?.enabled) {
780
- builtinFeatureNames.push("recall_memory");
781
- builtinFeatureDescs.push("- recall_memory: 回忆某内容,也可用于历史查询");
782
- }
638
+ appendExternalSkillsSection(lines, ctx);
639
+
640
+ return lines.join("\n");
641
+ }
642
+
643
+ function appendEmojiSection(
644
+ lines: string[],
645
+ ctx: PromptContext,
646
+ emojiStrength: Strength,
647
+ ): void {
648
+ const emojiAgent = ctx.emojiAgent;
649
+ if (!emojiAgent || !ctx.config.emoji?.enabled) return;
650
+
651
+ const configChars = ctx.config.emoji.characters || [];
652
+ const chars =
653
+ configChars.length > 0 ? configChars : emojiAgent.getAvailableCharacters();
654
+ const availableEmotions: string[] = [];
655
+ for (const char of chars) {
656
+ availableEmotions.push(...emojiAgent.getAvailableEmotions(char));
657
+ }
658
+ const uniqueEmotions = [...new Set(availableEmotions)].sort();
659
+ if (uniqueEmotions.length === 0) return;
660
+
661
+ lines.push(`
662
+ ### Optional Sticker / Emoji Format
663
+ - You MAY optionally send one matching sticker by writing [meme:emotion]
664
+ ${EMOJI_MODE_LINE[emojiStrength]}
665
+ - Do NOT send a sticker in every reply, and do not force one when the mood is plain
666
+ - Prefer one matching sticker at most. It should enhance the text instead of replacing meaningful content
667
+ - Put [meme:...] on its own line when it should be a separate action message after text
668
+ - Available emotions: ${uniqueEmotions.join(", ")}`);
669
+ }
783
670
 
784
- const pluginSkillList =
785
- skillEntries.length > 0
786
- ? skillEntries.map((s) => `- ${s.name}: ${s.description}`).join("\n")
787
- : "";
671
+ function appendExternalSkillsSection(
672
+ lines: string[],
673
+ ctx: PromptContext,
674
+ ): void {
675
+ if (!ctx.config.enableExternalSkills) return;
676
+
677
+ const skillsMap = ctx.aiService.getAllSkills?.();
678
+ const skillEntries = skillsMap
679
+ ? filterAllowedExternalSkills(
680
+ ctx.config,
681
+ [...skillsMap.values()],
682
+ ctx.triggerSkillRole ?? "member",
683
+ )
684
+ : [];
685
+
686
+ const builtinFeatureDescs: string[] = [];
687
+ if (ctx.config.searxng?.enabled) {
688
+ builtinFeatureDescs.push("- web_search: 进行网页搜索");
689
+ }
690
+ if (ctx.config.webReader?.enabled) {
691
+ builtinFeatureDescs.push("- web_read_page: 读取某个网页URL的内容");
692
+ }
693
+ if (ctx.config.memory?.enabled) {
694
+ builtinFeatureDescs.push("- recall_memory: 回忆某内容,也可用于历史查询");
695
+ }
788
696
 
789
- const builtinList = builtinFeatureDescs.join("\n");
790
- const combinedList = pluginSkillList
791
- ? pluginSkillList + "\n" + builtinList
792
- : builtinList;
697
+ const pluginSkillList = skillEntries.length
698
+ ? skillEntries.map((s) => `- ${s.name}: ${s.description}`).join("\n")
699
+ : "";
700
+ const builtinList = builtinFeatureDescs.join("\n");
701
+ const combinedList = pluginSkillList
702
+ ? pluginSkillList + "\n" + builtinList
703
+ : builtinList;
793
704
 
794
- if (combinedList) {
795
- lines.push(`
705
+ if (combinedList) {
706
+ lines.push(`
796
707
  ### External Skills
797
708
  You can load external skills to gain additional capabilities. Use load_skill to load the allowed skills below.
798
709
  You prefer to use extra skills to complete the user's tasks like an assistant
799
710
  Allowed skills:
800
711
  ${combinedList}`);
801
- }
802
712
  }
803
-
804
- return lines.join("\n");
805
713
  }
806
714
 
807
715
  function getActiveFeatureNames(ctx: PromptContext): string[] {
808
- if (!ctx.skillManager || !ctx.sessionId) {
809
- return [];
810
- }
716
+ if (!ctx.skillManager || !ctx.sessionId) return [];
811
717
  return ctx.skillManager.getActiveFeatureNames(ctx.sessionId);
812
718
  }
813
719
 
@@ -815,16 +721,9 @@ function markdownBehaviorLine(ctx: PromptContext): string {
815
721
  const activeFeatures = getActiveFeatureNames(ctx);
816
722
  const hasMarkdownFeature = activeFeatures.includes("markdown");
817
723
 
818
- if (!ctx.config.enableMarkdownScreenshot) {
819
- return "**DO NOT use markdown formatting, lists, or bullet points. Plain text only.**";
820
- }
821
-
822
- if (!hasMarkdownFeature) {
823
- // Markdown enabled in config but feature not loaded — tell AI markdown is NOT available
724
+ if (!ctx.config.enableMarkdownScreenshot || !hasMarkdownFeature) {
824
725
  return "**DO NOT use markdown formatting, lists, or bullet points. Plain text only.**";
825
726
  }
826
-
827
- // Feature loaded — allow markdown usage (full instructions in markdown section below)
828
727
  return "**Normal chat should stay plain text. Only use markdown when you intentionally want to send a rendered Markdown screenshot with the special <MARKDOWN>...</MARKDOWN> format.**";
829
728
  }
830
729
 
@@ -833,30 +732,20 @@ function markdownBehaviorLine(ctx: PromptContext): string {
833
732
 
834
733
  export function buildWebSearchFeatureSection(
835
734
  config: ChatConfig,
836
- toolStrength: ConstraintStrength = "medium",
735
+ toolStrength: Strength = "medium",
837
736
  ): string {
838
- if (!config.searxng?.enabled) {
839
- return "";
840
- }
841
- const searxngLine =
842
- toolStrength === "high"
843
- ? "- When facts may be outdated or uncertain, proactively call web_search instead of guessing."
844
- : toolStrength === "medium"
845
- ? "- Use web_search when current or external info is needed."
846
- : "- Use web_search only when the user explicitly needs external/current information.";
737
+ if (!config.searxng?.enabled) return "";
847
738
  return `
848
739
  ### Web Search Tool
849
740
  - web_search: Use this when you need current or external information that is not in chat history.
850
- ${searxngLine}`;
741
+ ${WEB_SEARCH_LINE[toolStrength]}`;
851
742
  }
852
743
 
853
744
  export function buildWebReadFeatureSection(
854
745
  config: ChatConfig,
855
- toolStrength: ConstraintStrength = "medium",
746
+ _toolStrength: Strength = "medium",
856
747
  ): string {
857
- if (!config.webReader?.enabled) {
858
- return "";
859
- }
748
+ if (!config.webReader?.enabled) return "";
860
749
  const independentUseLine = config.searxng?.enabled
861
750
  ? "- web_search and web_read_page are independent. Use web_search when you need to discover URLs; use web_read_page directly when the user already gave a URL."
862
751
  : "- web_read_page can be used directly when the user provides a URL.";
@@ -868,9 +757,7 @@ ${independentUseLine}
868
757
  }
869
758
 
870
759
  export function buildRecallMemoryFeatureSection(config: ChatConfig): string {
871
- if (!config.memory?.enabled) {
872
- return "";
873
- }
760
+ if (!config.memory?.enabled) return "";
874
761
  return `
875
762
  ### Memory Recall Tools
876
763
  - recall_memory: Delegate recall to a memory worker model. Pass a clear recall question and let the worker search historical logs.