a3s_code_core/prompts.rs
1// Prompt Registry
2//
3// Central registry for default system prompts and prompt templates used in
4// A3S Code. Every LLM-facing default pack is externalized here as a
5// compile-time `include_str!` so the shipping defaults are visible in one place.
6//
7// Ownership (`HARNESS-CONV6`):
8// - Core owns AgentStyle → hard permission overlays, runtime/tool contracts,
9// and `SystemPromptSlots` assembly. Prompt text never grants a capability.
10// - Specialty markdown bodies under `prompts/` are a replaceable default pack.
11// Hosts may replace role/guidelines/extra (and eventually the whole pack);
12// reviewer rubrics and product prompts remain host-owned.
13//
14// Directory layout:
15// prompts/
16// ├── common/ — shared runtime prompts used by multiple subsystems
17// ├── analysis/ — intent classification and pre-analysis prompts
18// ├── planning/ — planner and plan execution prompts
19// └── agents/ — built-in delegated-agent role prompts
20
21// ============================================================================
22// Default System Prompt
23// ============================================================================
24
25use crate::llm::LlmClient;
26use anyhow::Context;
27
28/// Default agentic system prompt — injected when no system prompt is configured.
29///
30/// Instructs the LLM to behave as an autonomous coding agent: use tools to act,
31/// verify results, and keep working until the task is fully complete.
32pub const SYSTEM_DEFAULT: &str = include_str!("../prompts/common/system_default.md");
33
34/// Continuation message — injected as a user turn when the LLM stops without
35/// completing the task (i.e. stops calling tools mid-task).
36pub const CONTINUATION: &str = include_str!("../prompts/common/continuation.md");
37
38/// Safety boundaries (injection hygiene, secret handling, malicious-code refusal).
39///
40/// Single source of truth, appended to every assembled system prompt by
41/// [`SystemPromptSlots::build_with_style`] so it applies uniformly across all
42/// agent styles and delegated subagents (which build through the same path).
43pub const BOUNDARIES: &str = include_str!("../prompts/common/boundaries.md");
44
45/// Canonical repository-tool arguments and context-efficient usage strategy.
46///
47/// Appended to writable / general styles so parameter and pagination guidance
48/// stay aligned with the registered schemas.
49pub const REPOSITORY_TOOL_CONTRACT: &str =
50 include_str!("../prompts/common/repository_tool_contract.md");
51
52/// Read-oriented repository-tool contract for specialty styles that must not
53/// advertise mutating tools while claiming a read-only role.
54pub const REPOSITORY_TOOL_CONTRACT_READONLY: &str =
55 include_str!("../prompts/common/repository_tool_contract_readonly.md");
56
57/// Shared runtime contract for authority, scope, run control, evidence, and
58/// completion semantics.
59///
60/// This contract is appended to every assembled prompt. It describes the
61/// model-facing boundary; enforcement remains in the host runtime and tool
62/// implementations.
63pub const RUNTIME_CONTRACT: &str = include_str!("../prompts/common/runtime_contract.md");
64
65// ============================================================================
66// Delegated Run Prompts
67// ============================================================================
68
69/// Explore delegated run — read-only codebase exploration
70pub const AGENT_EXPLORE: &str = include_str!("../prompts/agents/explore.md");
71
72/// Plan delegated run — read-only planning and analysis
73pub const AGENT_PLAN: &str = include_str!("../prompts/agents/plan.md");
74
75/// Code review delegated run — issue finding and review focus
76pub const AGENT_CODE_REVIEW: &str = include_str!("../prompts/agents/code_review.md");
77
78// ============================================================================
79// Session — Context Compaction
80// ============================================================================
81
82/// User template for context compaction. Placeholder: `{conversation}`
83pub const CONTEXT_COMPACT: &str = include_str!("../prompts/common/context_compact.md");
84
85/// Prefix for compacted summary messages
86pub const CONTEXT_SUMMARY_PREFIX: &str =
87 include_str!("../prompts/common/context_summary_prefix.md");
88
89// ============================================================================
90// LLM Planner — JSON-structured prompts
91// ============================================================================
92
93/// System prompt for LLM planner: plan creation (JSON output)
94pub const LLM_PLAN_SYSTEM: &str = include_str!("../prompts/planning/llm_plan_system.md");
95
96/// System prompt for LLM planner: goal extraction (JSON output)
97pub const LLM_GOAL_EXTRACT_SYSTEM: &str =
98 include_str!("../prompts/planning/llm_goal_extract_system.md");
99
100/// System prompt for LLM planner: goal achievement check (JSON output)
101pub const LLM_GOAL_CHECK_SYSTEM: &str =
102 include_str!("../prompts/planning/llm_goal_check_system.md");
103
104/// System prompt for pre-analysis: combined intent + goal + plan + input optimization.
105pub const PRE_ANALYSIS_SYSTEM: &str = include_str!("../prompts/analysis/pre_analysis_system.md");
106
107// ============================================================================
108// Plan Execution Templates
109// ============================================================================
110
111/// Template for initial plan execution message
112pub const PLAN_EXECUTE_GOAL: &str = include_str!("../prompts/planning/plan_execute_goal.md");
113
114/// Template for per-step execution prompt
115pub const PLAN_EXECUTE_STEP: &str = include_str!("../prompts/planning/plan_execute_step.md");
116
117/// Skill catalog header injected before listing available skill names/descriptions.
118pub const SKILLS_CATALOG_HEADER: &str = include_str!("../prompts/common/skills_catalog_header.md");
119
120// ============================================================================
121// Verification Agent
122// ============================================================================
123
124/// Verification agent — adversarial specialist that tries to break code
125pub const AGENT_VERIFICATION: &str = include_str!("../prompts/agents/verification.md");
126
127// ============================================================================
128// Intent Classification
129// ============================================================================
130
131/// System prompt for LLM-based intent classification
132pub const INTENT_CLASSIFY_SYSTEM: &str =
133 include_str!("../prompts/analysis/intent_classify_system.md");
134
135// ============================================================================
136// Planning Mode (Auto-Detection)
137// ============================================================================
138
139use serde::{Deserialize, Serialize};
140
141/// Planning mode — controls when planning phase is used.
142///
143/// When set to `Auto` (the default), the system detects from the user's
144/// message whether planning should be enabled. When explicitly `Enabled`,
145/// planning runs on every execution. When `Disabled`, planning is skipped.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
147pub enum PlanningMode {
148 /// Automatically detect from message content — enables planning when the
149 /// message benefits from structured pre-analysis. Local keyword detection is
150 /// only a fallback when pre-analysis is unavailable.
151 #[default]
152 Auto,
153 /// Explicitly disabled — never use planning phase.
154 Disabled,
155 /// Explicitly enabled — always use planning phase.
156 Enabled,
157}
158
159impl PlanningMode {
160 /// Returns true for the local no-LLM fallback path.
161 ///
162 /// Normal agent execution runs pre-analysis in `Auto` mode and uses its
163 /// structured `requires_planning` decision instead of this heuristic.
164 pub fn should_plan(&self, message: &str) -> bool {
165 match self {
166 PlanningMode::Auto => AgentStyle::detect_from_message(message).requires_planning(),
167 PlanningMode::Enabled => true,
168 PlanningMode::Disabled => false,
169 }
170 }
171}
172
173// ============================================================================
174// Agent Style (Intent-Based Prompt Selection)
175// ============================================================================
176
177/// Agent style — determines which system prompt template is used.
178///
179/// Each style has a different focus and behavior, selected based on the user's
180/// apparent intent from their message.
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
182pub enum AgentStyle {
183 /// Default — general purpose coding agent for research and multi-step tasks.
184 #[default]
185 GeneralPurpose,
186 /// Read-only planning and architecture analysis.
187 /// Prohibited from modifying files, focuses on design and planning.
188 Plan,
189 /// Adversarial verification specialist — tries to break code, not confirm it works.
190 Verification,
191 /// Fast file search and codebase exploration.
192 /// Read-only, optimized for finding files and patterns quickly.
193 Explore,
194 /// Code review focused — analyzes code quality, best practices, potential issues.
195 CodeReview,
196}
197
198/// Detection confidence level for style detection.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum DetectionConfidence {
201 /// High confidence — very specific keywords, skip LLM classification.
202 High,
203 /// Medium confidence — some indicators present, LLM classification helpful.
204 Medium,
205 /// Low confidence — no clear indicators, LLM classification recommended.
206 Low,
207}
208
209impl AgentStyle {
210 /// Returns true if this style is a specialty read-oriented role whose
211 /// prompt must not advertise workspace mutations as normal work.
212 pub fn uses_readonly_tool_contract(&self) -> bool {
213 matches!(
214 self,
215 AgentStyle::Plan
216 | AgentStyle::Explore
217 | AgentStyle::Verification
218 | AgentStyle::CodeReview
219 )
220 }
221
222 /// Repository-tool contract fragment for this style.
223 pub fn repository_tool_contract(&self) -> &'static str {
224 if self.uses_readonly_tool_contract() {
225 REPOSITORY_TOOL_CONTRACT_READONLY
226 } else {
227 REPOSITORY_TOOL_CONTRACT
228 }
229 }
230
231 /// Returns the base system prompt for this style.
232 pub fn base_prompt(&self) -> &'static str {
233 match self {
234 AgentStyle::GeneralPurpose => SYSTEM_DEFAULT,
235 AgentStyle::Plan => AGENT_PLAN,
236 AgentStyle::Verification => AGENT_VERIFICATION,
237 AgentStyle::Explore => AGENT_EXPLORE,
238 AgentStyle::CodeReview => AGENT_CODE_REVIEW,
239 }
240 }
241
242 /// Returns style-specific guidelines if any.
243 pub fn guidelines(&self) -> Option<&'static str> {
244 match self {
245 AgentStyle::GeneralPurpose => None,
246 AgentStyle::Plan => None, // Already embedded in agents/plan.md
247 AgentStyle::Verification => None, // Already embedded in agents/verification.md
248 AgentStyle::Explore => None, // Already embedded in agents/explore.md
249 AgentStyle::CodeReview => None,
250 }
251 }
252
253 /// Returns a one-line description of this style.
254 pub fn description(&self) -> &'static str {
255 match self {
256 AgentStyle::GeneralPurpose => {
257 "General purpose coding agent for research and multi-step tasks"
258 }
259 AgentStyle::Plan => "Read-only planning and architecture analysis agent",
260 AgentStyle::Verification => "Adversarial verification specialist — tries to break code",
261 AgentStyle::Explore => "Fast read-only file search and codebase exploration agent",
262 AgentStyle::CodeReview => "Code review focused — analyzes quality and best practices",
263 }
264 }
265
266 /// Returns the canonical built-in delegated-agent name for this style.
267 pub fn builtin_agent_name(&self) -> &'static str {
268 match self {
269 AgentStyle::GeneralPurpose => "general",
270 AgentStyle::Plan => "plan",
271 AgentStyle::Verification => "verification",
272 AgentStyle::Explore => "explore",
273 AgentStyle::CodeReview => "review",
274 }
275 }
276
277 /// Returns the stable runtime mode label for UI/event consumers.
278 pub fn runtime_mode(&self) -> &'static str {
279 match self {
280 AgentStyle::GeneralPurpose => "general",
281 AgentStyle::Plan => "planning",
282 AgentStyle::Verification => "verification",
283 AgentStyle::Explore => "explore",
284 AgentStyle::CodeReview => "code_review",
285 }
286 }
287
288 /// Returns true if this style benefits from a planning phase.
289 ///
290 /// Planning is beneficial for styles that involve multi-step execution
291 /// or where a structured approach improves outcomes.
292 pub fn requires_planning(&self) -> bool {
293 matches!(self, AgentStyle::Plan)
294 }
295
296 /// Detects the most appropriate agent style based on user message content,
297 /// along with a confidence level.
298 ///
299 /// This is a local fallback for environments where LLM pre-analysis is
300 /// unavailable. Normal execution uses structured pre-analysis first.
301 pub fn detect_with_confidence(message: &str) -> (Self, DetectionConfidence) {
302 // Chinese text has high ambiguity in intent classification due to
303 // compound verb structures and context-dependent meaning.
304 // Bypass keyword matching entirely and route to LLM classification.
305 if message
306 .chars()
307 .any(|c| ('\u{4e00}'..='\u{9fff}').contains(&c))
308 {
309 return (AgentStyle::GeneralPurpose, DetectionConfidence::Low);
310 }
311
312 let lower = message.to_lowercase();
313
314 // === HIGH CONFIDENCE: Very specific patterns ===
315
316 // Strong verification indicators
317 if lower.contains("try to break")
318 || lower.contains("find vulnerabilities")
319 || lower.contains("adversarial")
320 || lower.contains("security audit")
321 {
322 return (AgentStyle::Verification, DetectionConfidence::High);
323 }
324
325 // Strong plan indicators
326 if lower.contains("help me plan")
327 || lower.contains("help me design")
328 || lower.contains("create a plan")
329 || lower.contains("implementation plan")
330 || lower.contains("step-by-step plan")
331 {
332 return (AgentStyle::Plan, DetectionConfidence::High);
333 }
334
335 // Strong exploration indicators
336 if lower.contains("find all files")
337 || lower.contains("search for all")
338 || lower.contains("locate all")
339 {
340 return (AgentStyle::Explore, DetectionConfidence::High);
341 }
342
343 // === MEDIUM CONFIDENCE: Specific but less definitive ===
344
345 // Verification keywords
346 if lower.contains("verify")
347 || lower.contains("verification")
348 || lower.contains("break")
349 || lower.contains("debug")
350 || lower.contains("test")
351 || lower.contains("check if")
352 {
353 return (AgentStyle::Verification, DetectionConfidence::Medium);
354 }
355
356 // Plan keywords
357 if lower.contains("plan")
358 || lower.contains("design")
359 || lower.contains("architecture")
360 || lower.contains("approach")
361 {
362 return (AgentStyle::Plan, DetectionConfidence::Medium);
363 }
364
365 // Explore keywords
366 if lower.contains("find")
367 || lower.contains("search")
368 || lower.contains("where is")
369 || lower.contains("where's")
370 || lower.contains("locate")
371 || lower.contains("explore")
372 || lower.contains("look for")
373 {
374 return (AgentStyle::Explore, DetectionConfidence::Medium);
375 }
376
377 // Code review keywords
378 if lower.contains("review")
379 || lower.contains("code review")
380 || lower.contains("analyze")
381 || lower.contains("assess")
382 || lower.contains("quality")
383 || lower.contains("best practice")
384 {
385 return (AgentStyle::CodeReview, DetectionConfidence::Medium);
386 }
387
388 // No clear indicators
389 (AgentStyle::GeneralPurpose, DetectionConfidence::Low)
390 }
391
392 /// Detects the most appropriate agent style based on user message content.
393 ///
394 /// This is a local fallback heuristic. Normal execution uses structured
395 /// pre-analysis first; users can also explicitly set the style via
396 /// `SystemPromptSlots::with_style()`.
397 pub fn detect_from_message(message: &str) -> Self {
398 Self::detect_with_confidence(message).0
399 }
400
401 /// Classifies user intent using LLM when keyword confidence is low.
402 ///
403 /// This helper is available to callers that want explicit one-shot intent
404 /// classification outside the main pre-analysis path.
405 ///
406 /// Uses a lightweight classification prompt that returns a single word.
407 pub async fn detect_with_llm(llm: &dyn LlmClient, message: &str) -> anyhow::Result<Self> {
408 use crate::llm::Message;
409
410 let system = INTENT_CLASSIFY_SYSTEM;
411 let messages = vec![Message::user(message)];
412
413 let response = llm
414 .complete(&messages, Some(system), &[])
415 .await
416 .context("LLM intent classification failed")?;
417
418 let text = response.text().trim().to_lowercase();
419
420 let style = match text.as_str() {
421 "plan" => AgentStyle::Plan,
422 "explore" => AgentStyle::Explore,
423 "verification" => AgentStyle::Verification,
424 "codereview" | "code review" => AgentStyle::CodeReview,
425 _ => AgentStyle::GeneralPurpose,
426 };
427
428 Ok(style)
429 }
430}
431
432// ============================================================================
433// System Prompt Slots
434// ============================================================================
435
436/// Slot-based system prompt customization with intent-based style selection.
437///
438/// Users can customize specific parts of the system prompt without overriding
439/// the core agentic capabilities (tool usage, autonomous behavior, completion
440/// criteria). The default agentic core is ALWAYS included.
441///
442/// ## Assembly Order
443///
444/// ```text
445/// [role] ← Custom identity/role (e.g. "You are a Python expert")
446/// [CORE] ← Always present: Core Behaviour + Tool Usage Strategy + Completion Criteria
447/// [runtime] ← Host authority, scope, run-control, evidence contract
448/// [repository] ← Canonical repository-tool schemas and usage rules
449/// [boundaries] ← Injection, secret, and malicious-code boundaries
450/// [guidelines] ← Custom coding rules / constraints
451/// [response_style] ← Custom response format (replaces default Response Format section)
452/// [extra] ← Freeform additional instructions
453/// ```
454///
455/// Host-customizable edges around the Core default prompt pack.
456///
457/// The core agentic loop, runtime contract, repository-tool schema, safety
458/// boundaries, and style→permission overlays always remain authoritative.
459/// Specialty markdown bodies are a default pack only — not a capability source
460/// and not a reviewer/rubric owner (`HARNESS-CONV6` / `PROMPT-ALIGN1`).
461///
462/// ## Intent-Based Selection
463///
464/// When `style` is left as `AgentStyle::GeneralPurpose` (the default), the
465/// system will attempt to detect the user's intent from their first message and
466/// automatically select an appropriate style. To override this behavior, explicitly
467/// set the `style` field.
468#[derive(Debug, Clone, Default)]
469pub struct SystemPromptSlots {
470 /// Agent style — determines which base prompt template is used.
471 ///
472 /// When `None` (default), the style is auto-detected from the user's message.
473 /// Explicitly set this to force a particular style regardless of message content.
474 pub style: Option<AgentStyle>,
475
476 /// Custom role/identity prepended before the core prompt.
477 ///
478 /// Example: "You are a senior Python developer specializing in FastAPI."
479 /// When set, replaces the default "You are A3S Code, an expert AI coding agent" line.
480 pub role: Option<String>,
481
482 /// Custom coding guidelines appended after the core prompt sections.
483 ///
484 /// Example: "Always use type hints. Follow PEP 8. Prefer dataclasses over dicts."
485 pub guidelines: Option<String>,
486
487 /// Custom response style that replaces the default "Response Format" section.
488 ///
489 /// When `None`, the default response format is used.
490 pub response_style: Option<String>,
491
492 /// Explicit user-facing reply language as a BCP-47 tag (for example `zh-CN`
493 /// or `en-US`). When set, overrides the soft "match the user's language"
494 /// default so hosts can pin replies to the product UI locale.
495 pub output_language: Option<String>,
496
497 /// Freeform extra instructions appended at the very end.
498 pub extra: Option<String>,
499}
500
501/// The default role line in SYSTEM_DEFAULT that gets replaced when `role` slot is set.
502const DEFAULT_ROLE_LINE: &str = include_str!("../prompts/common/system_default_role_line.md");
503
504/// The default response format section.
505const DEFAULT_RESPONSE_FORMAT: &str =
506 include_str!("../prompts/common/system_default_response_format.md");
507
508impl SystemPromptSlots {
509 /// Build the final system prompt by assembling slots around the core prompt.
510 ///
511 /// The core agentic behavior, runtime contract, shared repository-tool
512 /// contract, and safety boundaries are always preserved. Users can only
513 /// customize the edges.
514 ///
515 /// Note: This uses `AgentStyle::GeneralPurpose` as the base. Use
516 /// `build_with_message()` to enable automatic intent-based style detection.
517 pub fn build(&self) -> String {
518 self.build_with_style(self.style.unwrap_or_default())
519 }
520
521 /// Build the final system prompt, auto-detecting style from the initial message.
522 ///
523 /// If `self.style` is explicitly set, that style is used regardless of message content.
524 /// Otherwise, the style is detected from `initial_message` using keyword analysis.
525 pub fn build_with_message(&self, initial_message: &str) -> String {
526 let style = self
527 .style
528 .unwrap_or_else(|| AgentStyle::detect_from_message(initial_message));
529 self.build_with_style(style)
530 }
531
532 /// Build the prompt with an explicitly specified style.
533 fn build_with_style(&self, style: AgentStyle) -> String {
534 let mut parts: Vec<String> = Vec::new();
535
536 // Normalize line endings: strip \r so string matching works on Windows
537 // where include_str! may produce \r\n if the file has CRLF endings.
538 let base_prompt = style.base_prompt().replace('\r', "");
539 let default_role_line = DEFAULT_ROLE_LINE.replace('\r', "");
540 let default_response_format = DEFAULT_RESPONSE_FORMAT.replace('\r', "");
541
542 // 1. Role: for GeneralPurpose, replace the default role line.
543 // For other styles (Plan, Explore, Verification), prepend custom role since
544 // those prompts have their own identity embedded.
545 let core = if let Some(ref role) = self.role {
546 if style == AgentStyle::GeneralPurpose {
547 let custom_role = format!(
548 "{} Working inside the user's workspace, use the tools exposed in the current turn to inspect, change, and verify the requested work; continue until the user's request is genuinely complete.",
549 role.trim_end_matches('.')
550 );
551 base_prompt.replace(&default_role_line, &custom_role)
552 } else {
553 // Prepend custom role for other styles
554 format!("{}\n\n{}", role, base_prompt)
555 }
556 } else {
557 base_prompt
558 };
559
560 // 2. Core: strip the default response format section if custom one is provided
561 let core = if self.response_style.is_some() {
562 core.replace(&default_response_format, "")
563 .trim_end()
564 .to_string()
565 } else {
566 core.trim_end().to_string()
567 };
568
569 parts.push(core);
570
571 // 2b. Runtime contract — shared by every style so authority, run
572 // control, and evidence semantics cannot drift between built-in agents.
573 parts.push(RUNTIME_CONTRACT.replace('\r', "").trim_end().to_string());
574
575 // 2c. Repository-tool contract — style-scoped so read-oriented roles do
576 // not advertise mutating tools while claiming a read-only task.
577 parts.push(
578 style
579 .repository_tool_contract()
580 .replace('\r', "")
581 .trim_end()
582 .to_string(),
583 );
584
585 // 2d. Safety boundaries — single source of truth, appended uniformly so
586 // every style and delegated subagent (which build through this path)
587 // carries injection-hygiene, secret-handling, and malware-refusal rules.
588 parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
589
590 // 2e. Explicit output language — host-pinned product language for UI
591 // locales. Covers every user-visible prose surface (replies, reasoning,
592 // plans). When absent, the soft "user's language" rule remains.
593 if let Some(language) = self
594 .output_language
595 .as_deref()
596 .map(str::trim)
597 .filter(|value| !value.is_empty())
598 {
599 parts.push(output_language_contract(language));
600 }
601
602 // 3. Custom response style (replaces default Response Format)
603 if let Some(ref style) = self.response_style {
604 parts.push(format!("## Response Format\n\n{}", style));
605 }
606
607 // 4. Guidelines: style-specific + custom
608 let style_guidelines = style.guidelines();
609 if style_guidelines.is_some() || self.guidelines.is_some() {
610 let mut guidelines_parts = Vec::new();
611 if let Some(sg) = style_guidelines {
612 guidelines_parts.push(sg.to_string());
613 }
614 if let Some(ref g) = self.guidelines {
615 guidelines_parts.push(g.clone());
616 }
617 parts.push(format!(
618 "## Guidelines\n\n{}",
619 guidelines_parts.join("\n\n")
620 ));
621 }
622
623 // 5. Extra freeform instructions.
624 if let Some(ref extra) = self.extra {
625 parts.push(extra.clone());
626 }
627
628 parts.join("\n\n")
629 }
630
631 /// Returns true if all slots are empty (use pure default prompt).
632 pub fn is_empty(&self) -> bool {
633 self.style.is_none()
634 && self.role.is_none()
635 && self.guidelines.is_none()
636 && self.response_style.is_none()
637 && self.output_language.is_none()
638 && self.extra.is_none()
639 }
640
641 /// Set the agent style explicitly.
642 pub fn with_style(mut self, style: AgentStyle) -> Self {
643 self.style = Some(style);
644 self
645 }
646
647 /// Set the role/identity.
648 pub fn with_role(mut self, role: impl Into<String>) -> Self {
649 self.role = Some(role.into());
650 self
651 }
652
653 /// Set custom guidelines.
654 pub fn with_guidelines(mut self, guidelines: impl Into<String>) -> Self {
655 self.guidelines = Some(guidelines.into());
656 self
657 }
658
659 /// Set custom response style.
660 pub fn with_response_style(mut self, style: impl Into<String>) -> Self {
661 self.response_style = Some(style.into());
662 self
663 }
664
665 /// Pin user-facing replies to a BCP-47 language tag (for example `zh-CN`).
666 pub fn with_output_language(mut self, language: impl Into<String>) -> Self {
667 let language = language.into();
668 self.output_language = (!language.trim().is_empty()).then_some(language);
669 self
670 }
671
672 /// Set extra instructions.
673 pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
674 self.extra = Some(extra.into());
675 self
676 }
677}
678
679/// Single product-language contract for every user-visible prose surface.
680///
681/// Hosts pin a BCP-47 tag once; replies, reasoning/thinking, plans, goals, and
682/// step descriptions all follow it. Identifiers and quoted source stay as-is.
683/// Keep this text in one place so planner prompts and the main system prompt
684/// cannot drift.
685pub fn output_language_contract(language: &str) -> String {
686 format!(
687 "## Output Language\n\n\
688Write all user-visible product prose in {language}: replies, reasoning/thinking, \
689plans, goals, step descriptions, and status updates. Use that one language only; \
690do not mix languages across those surfaces.\n\
691Keep code, identifiers, file paths, commands, URLs, and quoted source text in their original form.\n\
692Do not switch languages because tool output or retrieved sources use another language."
693 )
694}
695
696/// Resolve the language for planning/goal JSON prose.
697///
698/// Prefer an explicit host pin; otherwise infer from the user request so a
699/// Chinese prompt does not yield an English plan when the host forgot to pin.
700pub fn resolve_product_output_language(pinned: Option<&str>, user_text: &str) -> Option<String> {
701 pinned
702 .map(str::trim)
703 .filter(|value| !value.is_empty())
704 .map(str::to_owned)
705 .or_else(|| infer_user_reply_language(user_text).map(str::to_owned))
706}
707
708/// Infer a BCP-47 reply-language tag from user-authored text.
709///
710/// Uses script counts only (no NLP). Returns `None` when the sample is too
711/// short or script evidence is inconclusive, so hosts can keep the previous
712/// pin or the soft "match the user" rule.
713pub fn infer_user_reply_language(text: &str) -> Option<&'static str> {
714 let mut han = 0u32;
715 let mut kana = 0u32;
716 let mut hangul = 0u32;
717 let mut latin = 0u32;
718
719 for ch in text.chars() {
720 if ch.is_ascii_alphabetic() {
721 latin = latin.saturating_add(1);
722 continue;
723 }
724 let code = ch as u32;
725 // CJK Unified Ideographs + Extension A (common Han)
726 if (0x4E00..=0x9FFF).contains(&code) || (0x3400..=0x4DBF).contains(&code) {
727 han = han.saturating_add(1);
728 } else if (0x3040..=0x30FF).contains(&code) {
729 // Hiragana + Katakana
730 kana = kana.saturating_add(1);
731 } else if (0xAC00..=0xD7AF).contains(&code) || (0x1100..=0x11FF).contains(&code) {
732 hangul = hangul.saturating_add(1);
733 }
734 }
735
736 let marked = han + kana + hangul + latin;
737 if marked < 2 {
738 return None;
739 }
740
741 // Prefer non-Latin scripts when the user clearly wrote in them, even if
742 // English identifiers appear ("帮我 fix 这个 bug").
743 if hangul >= 2 && hangul >= han && hangul >= kana {
744 return Some("ko");
745 }
746 if kana >= 2 {
747 return Some("ja");
748 }
749 if han >= 2 {
750 return Some("zh-CN");
751 }
752 // Short English acknowledgements ("ok", "thanks") must not flip an
753 // established reply language; require a stronger Latin sample.
754 if latin >= 12 {
755 return Some("en");
756 }
757 None
758}
759
760// ============================================================================
761// Helper Functions
762// ============================================================================
763
764/// Render a template by replacing `{key}` placeholders with values
765pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
766 let mut result = template.to_string();
767 for (key, value) in vars {
768 result = result.replace(&format!("{{{}}}", key), value);
769 }
770 result
771}
772
773#[cfg(test)]
774#[path = "prompts/tests.rs"]
775mod tests;