Skip to main content

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. Placeholders: `{goal}`, `{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    ///
408    /// The primary writable session does not call this helper and does not
409    /// change style from it. A typed decision must not replace it: the
410    /// classifier prompt is a keyword list, and this call is not a generation
411    /// the session spends.
412    pub async fn detect_with_llm(llm: &dyn LlmClient, message: &str) -> anyhow::Result<Self> {
413        use crate::llm::Message;
414
415        if message.trim().is_empty() {
416            return Ok(Self::GeneralPurpose);
417        }
418
419        let system = INTENT_CLASSIFY_SYSTEM;
420        let messages = vec![Message::user(message)];
421
422        let response = llm
423            .complete(&messages, Some(system), &[])
424            .await
425            .context("LLM intent classification failed")?;
426
427        Ok(Self::style_from_classifier_text(&response.text()))
428    }
429
430    /// Map a classifier reply to a style.
431    ///
432    /// The whole trimmed reply must be one known label. Surrounding prose
433    /// stays `GeneralPurpose` and is not parsed into a typed answer.
434    fn style_from_classifier_text(text: &str) -> Self {
435        match text.trim().to_ascii_lowercase().as_str() {
436            "plan" => Self::Plan,
437            "explore" => Self::Explore,
438            "verification" => Self::Verification,
439            "codereview" | "code review" => Self::CodeReview,
440            "generalpurpose" => Self::GeneralPurpose,
441            _ => Self::GeneralPurpose,
442        }
443    }
444}
445
446// ============================================================================
447// System Prompt Slots
448// ============================================================================
449
450/// Slot-based system prompt customization with intent-based style selection.
451///
452/// Users can customize specific parts of the system prompt without overriding
453/// the core agentic capabilities (tool usage, autonomous behavior, completion
454/// criteria). The default agentic core is ALWAYS included.
455///
456/// ## Assembly Order
457///
458/// ```text
459/// [role]            ← Custom identity/role (e.g. "You are a Python expert")
460/// [CORE]            ← Always present: Core Behaviour + Tool Usage Strategy + Completion Criteria
461/// [runtime]         ← Host authority, scope, run-control, evidence contract
462/// [repository]      ← Canonical repository-tool schemas and usage rules
463/// [boundaries]      ← Injection, secret, and malicious-code boundaries
464/// [guidelines]      ← Custom coding rules / constraints
465/// [response_style]  ← Custom response format (replaces default Response Format section)
466/// [extra]           ← Freeform additional instructions
467/// ```
468///
469/// Host-customizable edges around the Core default prompt pack.
470///
471/// The core agentic loop, runtime contract, repository-tool schema, safety
472/// boundaries, and style→permission overlays always remain authoritative.
473/// Specialty markdown bodies are a default pack only — not a capability source
474/// and not a reviewer/rubric owner (`HARNESS-CONV6` / `PROMPT-ALIGN1`).
475///
476/// ## Intent-Based Selection
477///
478/// When `style` is left as `AgentStyle::GeneralPurpose` (the default), the
479/// system will attempt to detect the user's intent from their first message and
480/// automatically select an appropriate style. To override this behavior, explicitly
481/// set the `style` field.
482#[derive(Debug, Clone, Default)]
483pub struct SystemPromptSlots {
484    /// Agent style — determines which base prompt template is used.
485    ///
486    /// When `None` (default), the style is auto-detected from the user's message.
487    /// Explicitly set this to force a particular style regardless of message content.
488    pub style: Option<AgentStyle>,
489
490    /// Custom role/identity prepended before the core prompt.
491    ///
492    /// Example: "You are a senior Python developer specializing in FastAPI."
493    /// When set, replaces the default "You are A3S Code, an expert AI coding agent" line.
494    pub role: Option<String>,
495
496    /// Custom coding guidelines appended after the core prompt sections.
497    ///
498    /// Example: "Always use type hints. Follow PEP 8. Prefer dataclasses over dicts."
499    pub guidelines: Option<String>,
500
501    /// Custom response style that replaces the default "Response Format" section.
502    ///
503    /// When `None`, the default response format is used.
504    pub response_style: Option<String>,
505
506    /// Explicit user-facing reply language as a BCP-47 tag (for example `zh-CN`
507    /// or `en-US`). When set, overrides the soft "match the user's language"
508    /// default so hosts can pin replies to the product UI locale.
509    pub output_language: Option<String>,
510
511    /// Freeform extra instructions appended at the very end.
512    pub extra: Option<String>,
513}
514
515/// The default role line in SYSTEM_DEFAULT that gets replaced when `role` slot is set.
516const DEFAULT_ROLE_LINE: &str = include_str!("../prompts/common/system_default_role_line.md");
517
518/// The default response format section.
519const DEFAULT_RESPONSE_FORMAT: &str =
520    include_str!("../prompts/common/system_default_response_format.md");
521
522impl SystemPromptSlots {
523    /// Build the final system prompt by assembling slots around the core prompt.
524    ///
525    /// The core agentic behavior, runtime contract, shared repository-tool
526    /// contract, and safety boundaries are always preserved. Users can only
527    /// customize the edges.
528    ///
529    /// Note: This uses `AgentStyle::GeneralPurpose` as the base. Use
530    /// `build_with_message()` to enable automatic intent-based style detection.
531    pub fn build(&self) -> String {
532        self.build_with_style(self.style.unwrap_or_default())
533    }
534
535    /// Build the final system prompt, auto-detecting style from the initial message.
536    ///
537    /// If `self.style` is explicitly set, that style is used regardless of message content.
538    /// Otherwise, the style is detected from `initial_message` using keyword analysis.
539    pub fn build_with_message(&self, initial_message: &str) -> String {
540        let style = self
541            .style
542            .unwrap_or_else(|| AgentStyle::detect_from_message(initial_message));
543        self.build_with_style(style)
544    }
545
546    /// Build the prompt with an explicitly specified style.
547    fn build_with_style(&self, style: AgentStyle) -> String {
548        let mut parts: Vec<String> = Vec::new();
549
550        // Normalize line endings: strip \r so string matching works on Windows
551        // where include_str! may produce \r\n if the file has CRLF endings.
552        let base_prompt = style.base_prompt().replace('\r', "");
553        let default_role_line = DEFAULT_ROLE_LINE.replace('\r', "");
554        let default_response_format = DEFAULT_RESPONSE_FORMAT.replace('\r', "");
555
556        // 1. Role: for GeneralPurpose, replace the default role line.
557        // For other styles (Plan, Explore, Verification), prepend custom role since
558        // those prompts have their own identity embedded.
559        let core = if let Some(ref role) = self.role {
560            if style == AgentStyle::GeneralPurpose {
561                let custom_role = format!(
562                    "{} 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.",
563                    role.trim_end_matches('.')
564                );
565                base_prompt.replace(&default_role_line, &custom_role)
566            } else {
567                // Prepend custom role for other styles
568                format!("{}\n\n{}", role, base_prompt)
569            }
570        } else {
571            base_prompt
572        };
573
574        // 2. Core: strip the default response format section if custom one is provided
575        let core = if self.response_style.is_some() {
576            core.replace(&default_response_format, "")
577                .trim_end()
578                .to_string()
579        } else {
580            core.trim_end().to_string()
581        };
582
583        parts.push(core);
584
585        // 2b. Runtime contract — shared by every style so authority, run
586        // control, and evidence semantics cannot drift between built-in agents.
587        parts.push(RUNTIME_CONTRACT.replace('\r', "").trim_end().to_string());
588
589        // 2c. Repository-tool contract — style-scoped so read-oriented roles do
590        // not advertise mutating tools while claiming a read-only task.
591        parts.push(
592            style
593                .repository_tool_contract()
594                .replace('\r', "")
595                .trim_end()
596                .to_string(),
597        );
598
599        // 2d. Safety boundaries — single source of truth, appended uniformly so
600        // every style and delegated subagent (which build through this path)
601        // carries injection-hygiene, secret-handling, and malware-refusal rules.
602        parts.push(BOUNDARIES.replace('\r', "").trim_end().to_string());
603
604        // 2e. Explicit output language — host-pinned product language for UI
605        // locales. Covers every user-visible prose surface (replies, reasoning,
606        // plans). When absent, the soft "user's language" rule remains.
607        if let Some(language) = self
608            .output_language
609            .as_deref()
610            .map(str::trim)
611            .filter(|value| !value.is_empty())
612        {
613            parts.push(output_language_contract(language));
614        }
615
616        // 3. Custom response style (replaces default Response Format)
617        if let Some(ref style) = self.response_style {
618            parts.push(format!("## Response Format\n\n{}", style));
619        }
620
621        // 4. Guidelines: style-specific + custom
622        let style_guidelines = style.guidelines();
623        if style_guidelines.is_some() || self.guidelines.is_some() {
624            let mut guidelines_parts = Vec::new();
625            if let Some(sg) = style_guidelines {
626                guidelines_parts.push(sg.to_string());
627            }
628            if let Some(ref g) = self.guidelines {
629                guidelines_parts.push(g.clone());
630            }
631            parts.push(format!(
632                "## Guidelines\n\n{}",
633                guidelines_parts.join("\n\n")
634            ));
635        }
636
637        // 5. Extra freeform instructions.
638        if let Some(ref extra) = self.extra {
639            parts.push(extra.clone());
640        }
641
642        parts.join("\n\n")
643    }
644
645    /// Returns true if all slots are empty (use pure default prompt).
646    pub fn is_empty(&self) -> bool {
647        self.style.is_none()
648            && self.role.is_none()
649            && self.guidelines.is_none()
650            && self.response_style.is_none()
651            && self.output_language.is_none()
652            && self.extra.is_none()
653    }
654
655    /// Set the agent style explicitly.
656    pub fn with_style(mut self, style: AgentStyle) -> Self {
657        self.style = Some(style);
658        self
659    }
660
661    /// Set the role/identity.
662    pub fn with_role(mut self, role: impl Into<String>) -> Self {
663        self.role = Some(role.into());
664        self
665    }
666
667    /// Set custom guidelines.
668    pub fn with_guidelines(mut self, guidelines: impl Into<String>) -> Self {
669        self.guidelines = Some(guidelines.into());
670        self
671    }
672
673    /// Set custom response style.
674    pub fn with_response_style(mut self, style: impl Into<String>) -> Self {
675        self.response_style = Some(style.into());
676        self
677    }
678
679    /// Pin user-facing replies to a BCP-47 language tag (for example `zh-CN`).
680    pub fn with_output_language(mut self, language: impl Into<String>) -> Self {
681        let language = language.into();
682        self.output_language = (!language.trim().is_empty()).then_some(language);
683        self
684    }
685
686    /// Set extra instructions.
687    pub fn with_extra(mut self, extra: impl Into<String>) -> Self {
688        self.extra = Some(extra.into());
689        self
690    }
691}
692
693/// Single product-language contract for every user-visible prose surface.
694///
695/// Hosts pin a BCP-47 tag once; replies, reasoning/thinking, plans, goals, and
696/// step descriptions all follow it. Identifiers and quoted source stay as-is.
697/// Keep this text in one place so planner prompts and the main system prompt
698/// cannot drift.
699pub fn output_language_contract(language: &str) -> String {
700    format!(
701        "## Output Language\n\n\
702Write all user-visible product prose in {language}: replies, reasoning/thinking, \
703plans, goals, step descriptions, and status updates. Use that one language only; \
704do not mix languages across those surfaces.\n\
705Keep code, identifiers, file paths, commands, URLs, and quoted source text in their original form.\n\
706Do not switch languages because tool output or retrieved sources use another language."
707    )
708}
709
710/// Resolve the language for planning/goal JSON prose.
711///
712/// Prefer an explicit host pin; otherwise infer from the user request so a
713/// Chinese prompt does not yield an English plan when the host forgot to pin.
714pub fn resolve_product_output_language(pinned: Option<&str>, user_text: &str) -> Option<String> {
715    pinned
716        .map(str::trim)
717        .filter(|value| !value.is_empty())
718        .map(str::to_owned)
719        .or_else(|| infer_user_reply_language(user_text).map(str::to_owned))
720}
721
722/// Infer a BCP-47 reply-language tag from user-authored text.
723///
724/// Uses script counts only (no NLP). Returns `None` when the sample is too
725/// short or script evidence is inconclusive, so hosts can keep the previous
726/// pin or the soft "match the user" rule.
727pub fn infer_user_reply_language(text: &str) -> Option<&'static str> {
728    let mut han = 0u32;
729    let mut kana = 0u32;
730    let mut hangul = 0u32;
731    let mut latin = 0u32;
732
733    for ch in text.chars() {
734        if ch.is_ascii_alphabetic() {
735            latin = latin.saturating_add(1);
736            continue;
737        }
738        let code = ch as u32;
739        // CJK Unified Ideographs + Extension A (common Han)
740        if (0x4E00..=0x9FFF).contains(&code) || (0x3400..=0x4DBF).contains(&code) {
741            han = han.saturating_add(1);
742        } else if (0x3040..=0x30FF).contains(&code) {
743            // Hiragana + Katakana
744            kana = kana.saturating_add(1);
745        } else if (0xAC00..=0xD7AF).contains(&code) || (0x1100..=0x11FF).contains(&code) {
746            hangul = hangul.saturating_add(1);
747        }
748    }
749
750    let marked = han + kana + hangul + latin;
751    if marked < 2 {
752        return None;
753    }
754
755    // Prefer non-Latin scripts when the user clearly wrote in them, even if
756    // English identifiers appear ("帮我 fix 这个 bug").
757    if hangul >= 2 && hangul >= han && hangul >= kana {
758        return Some("ko");
759    }
760    if kana >= 2 {
761        return Some("ja");
762    }
763    if han >= 2 {
764        return Some("zh-CN");
765    }
766    // Short English acknowledgements ("ok", "thanks") must not flip an
767    // established reply language; require a stronger Latin sample.
768    if latin >= 12 {
769        return Some("en");
770    }
771    None
772}
773
774// ============================================================================
775// Helper Functions
776// ============================================================================
777
778/// Render a template by replacing `{key}` placeholders with values
779pub fn render(template: &str, vars: &[(&str, &str)]) -> String {
780    let mut result = template.to_string();
781    for (key, value) in vars {
782        result = result.replace(&format!("{{{}}}", key), value);
783    }
784    result
785}
786
787#[cfg(test)]
788#[path = "prompts/tests.rs"]
789mod tests;