From 023de0f58d3df15bb551d62124ce6527fc2a81d3 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 13 May 2026 02:27:30 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Language=E3=82=A4=E3=83=B3=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=83=A9=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/manifest/src/config.rs | 7 +++++++ crates/manifest/src/defaults.rs | 5 +++++ crates/manifest/src/lib.rs | 20 ++++++++++++++++++++ crates/pod/src/factory.rs | 1 + crates/pod/src/pod.rs | 12 ++++++++++++ crates/pod/src/prompt/system.rs | 8 ++++++++ resources/prompts/common/language.md | 6 ++++++ resources/prompts/default.md | 2 ++ 8 files changed, 61 insertions(+) create mode 100644 resources/prompts/common/language.md diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index 7a471792..ab266182 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -66,6 +66,8 @@ pub struct WorkerManifestConfig { #[serde(default)] pub instruction: Option, #[serde(default)] + pub language: Option, + #[serde(default)] pub max_tokens: Option, #[serde(default)] pub max_turns: Option, @@ -291,6 +293,7 @@ impl WorkerManifestConfig { fn merge(self, upper: Self) -> Self { Self { instruction: upper.instruction.or(self.instruction), + language: upper.language.or(self.language), max_tokens: upper.max_tokens.or(self.max_tokens), max_turns: upper.max_turns.or(self.max_turns), temperature: upper.temperature.or(self.temperature), @@ -429,6 +432,10 @@ impl TryFrom for PodManifest { .worker .instruction .unwrap_or_else(|| defaults::DEFAULT_INSTRUCTION.to_string()), + language: cfg + .worker + .language + .unwrap_or_else(|| defaults::WORKER_LANGUAGE.to_string()), max_tokens: cfg.worker.max_tokens, max_turns: cfg.worker.max_turns, temperature: cfg.worker.temperature, diff --git a/crates/manifest/src/defaults.rs b/crates/manifest/src/defaults.rs index e4ef0478..68123e92 100644 --- a/crates/manifest/src/defaults.rs +++ b/crates/manifest/src/defaults.rs @@ -33,6 +33,11 @@ pub const COMPACT_RETAINED_TOKENS: u64 = 8000; /// `$insomnia/` / `$user/` / `$workspace/` namespaces. pub const DEFAULT_INSTRUCTION: &str = "$insomnia/default"; +/// Default language policy used by the main worker for normal prose +/// responses. See [`crate::WorkerManifest::language`]. +pub const WORKER_LANGUAGE: &str = + "match the user's language unless they explicitly request another language"; + /// Token budget for auto-read file contents injected into the new /// session after compaction. Limits how much raw file text the /// compact worker can pull into the compacted context via diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index 06ff906a..9e1a5339 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -173,6 +173,12 @@ pub struct WorkerManifest { /// unset manifests fall through to [`defaults::DEFAULT_INSTRUCTION`]. #[serde(default = "default_instruction")] pub instruction: String, + /// Language policy used by the main worker for normal prose responses. + /// Free-form so workspaces can use names like `English`, `Japanese`, + /// locale tags, or a policy phrase. Unset manifests fall through to + /// [`defaults::WORKER_LANGUAGE`]. + #[serde(default = "default_worker_language")] + pub language: String, #[serde(default)] pub max_tokens: Option, #[serde(default)] @@ -241,6 +247,10 @@ fn default_instruction() -> String { defaults::DEFAULT_INSTRUCTION.to_string() } +fn default_worker_language() -> String { + defaults::WORKER_LANGUAGE.to_string() +} + impl Default for ToolOutputLimits { fn default() -> Self { Self { @@ -689,6 +699,16 @@ model_id = "claude-sonnet-4-20250514" ); } + #[test] + fn worker_language_defaults_and_parses() { + let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap(); + assert_eq!(manifest.worker.language, defaults::WORKER_LANGUAGE); + + let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nlanguage = \"Japanese\"\n"); + let manifest = PodManifest::from_toml(&toml).unwrap(); + assert_eq!(manifest.worker.language, "Japanese"); + } + #[test] fn parse_worker_output_limits() { let toml = MINIMAL_REQUIRED.replace( diff --git a/crates/pod/src/factory.rs b/crates/pod/src/factory.rs index 897e07d2..59a79e8f 100644 --- a/crates/pod/src/factory.rs +++ b/crates/pod/src/factory.rs @@ -643,6 +643,7 @@ permission = "write" let ctx = SystemPromptContext { now: chrono::Utc::now(), cwd: &root, + language: manifest::defaults::WORKER_LANGUAGE, scope: &scope, tool_names: Vec::new(), agents_md: None, diff --git a/crates/pod/src/pod.rs b/crates/pod/src/pod.rs index 32240685..9367b14e 100644 --- a/crates/pod/src/pod.rs +++ b/crates/pod/src/pod.rs @@ -879,10 +879,12 @@ impl Pod { }; let resident_exposure_snapshots = self.resident_exposure_snapshots(&resident, &resident_workflows); + let worker_language = worker_language(&self.manifest.worker); let scope_snapshot = self.scope.snapshot(); let ctx = SystemPromptContext { now: chrono::Utc::now(), cwd: &self.pwd, + language: worker_language, scope: &scope_snapshot, tool_names, agents_md: agents_md_read.body, @@ -2342,6 +2344,15 @@ fn memory_language(cfg: &manifest::MemoryConfig) -> &str { .unwrap_or(manifest::defaults::MEMORY_LANGUAGE) } +fn worker_language(cfg: &manifest::WorkerManifest) -> &str { + let language = cfg.language.trim(); + if language.is_empty() { + manifest::defaults::WORKER_LANGUAGE + } else { + language + } +} + /// Outcome of a single extract iteration. Internal to /// `try_post_run_extract` / `run_extract_once`. enum ExtractDecision { @@ -3152,6 +3163,7 @@ mod build_summary_prompt_tests { fn worker_manifest_generation_settings_become_request_config() { let manifest = WorkerManifest { instruction: "unused".into(), + language: manifest::defaults::WORKER_LANGUAGE.into(), max_tokens: Some(1024), max_turns: None, temperature: Some(0.2), diff --git a/crates/pod/src/prompt/system.rs b/crates/pod/src/prompt/system.rs index 00bf4121..e11fcd1e 100644 --- a/crates/pod/src/prompt/system.rs +++ b/crates/pod/src/prompt/system.rs @@ -144,6 +144,8 @@ impl std::fmt::Debug for SystemPromptTemplate { pub struct SystemPromptContext<'a> { pub now: DateTime, pub cwd: &'a Path, + /// Language policy exposed to instruction templates as `{{ language }}`. + pub language: &'a str, pub scope: &'a Scope, pub tool_names: Vec, /// Project-level instructions read from the nearest `AGENTS.md`. @@ -181,6 +183,7 @@ impl<'a> SystemPromptContext<'a> { Value::from(self.now.to_rfc3339_opts(SecondsFormat::Secs, true)), ); root.insert("cwd".into(), Value::from(self.cwd.display().to_string())); + root.insert("language".into(), Value::from(self.language)); root.insert( "tools".into(), Value::from( @@ -328,6 +331,7 @@ mod tests { SystemPromptContext { now: fixed_now(), cwd, + language: manifest::defaults::WORKER_LANGUAGE, scope, tool_names: tools, agents_md, @@ -345,6 +349,7 @@ mod tests { SystemPromptContext { now: fixed_now(), cwd, + language: manifest::defaults::WORKER_LANGUAGE, scope, tool_names: Vec::new(), agents_md: None, @@ -380,6 +385,9 @@ mod tests { let rendered = tmpl .render(&ctx(dir.path(), &scope, vec!["Read".into()], None)) .unwrap(); + // Builtin default body must expose the language policy. + assert!(rendered.contains("## Language")); + assert!(rendered.contains("`language`: `match the user's language")); // Trailing section must be present. assert!(rendered.contains("## Working boundaries")); assert!(rendered.contains("Readable:")); diff --git a/resources/prompts/common/language.md b/resources/prompts/common/language.md new file mode 100644 index 00000000..23d65f48 --- /dev/null +++ b/resources/prompts/common/language.md @@ -0,0 +1,6 @@ +## Language + +- `language`: `{{ language }}`. +- Follow this language policy for normal prose responses unless the user explicitly requests another language. +- Preserve code identifiers, file paths, command names, logs, quoted text, and external proper nouns when translation would reduce fidelity. +- Do not translate file contents, command output, or protocol literals unless the user asks for translation. diff --git a/resources/prompts/default.md b/resources/prompts/default.md index 17bc305a..5d7aa68d 100644 --- a/resources/prompts/default.md +++ b/resources/prompts/default.md @@ -6,4 +6,6 @@ Stay precise, edit code directly when asked, and avoid speculative refactoring. {% include "common/tool-usage" %} +{% include "common/language" %} + {% include "common/writing" %}