update: fmt + memoryに用いる言語の構成

This commit is contained in:
Keisuke Hirata 2026-05-13 01:57:04 +09:00
parent 0141880b9d
commit eae0efb2c0
No known key found for this signature in database
16 changed files with 95 additions and 47 deletions

View File

@ -47,7 +47,9 @@ pub enum SpawnError {
/// runtime ディレクトリが解決できなかった (環境変数未設定等)。 /// runtime ディレクトリが解決できなかった (環境変数未設定等)。
RuntimeDirUnavailable, RuntimeDirUnavailable,
PodLaunchFailed(io::Error), PodLaunchFailed(io::Error),
PodExitedEarly { stderr_tail: String }, PodExitedEarly {
stderr_tail: String,
},
Timeout, Timeout,
} }
@ -88,10 +90,7 @@ impl From<io::Error> for SpawnError {
/// ///
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる /// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。 /// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
pub async fn spawn_pod<F>( pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnReady, SpawnError>
config: SpawnConfig,
mut progress: F,
) -> Result<SpawnReady, SpawnError>
where where
F: FnMut(&str), F: FnMut(&str),
{ {

View File

@ -316,7 +316,8 @@ impl AnthropicScheme {
}); });
match &raw.content_block { match &raw.content_block {
ContentBlock::Thinking { ContentBlock::Thinking {
thinking, signature, thinking,
signature,
} => { } => {
state.pending_thinking = Some(PendingThinking { state.pending_thinking = Some(PendingThinking {
text: thinking.clone(), text: thinking.clone(),
@ -372,10 +373,7 @@ impl AnthropicScheme {
} }
AnthropicEventType::ContentBlockStop => { AnthropicEventType::ContentBlockStop => {
let raw: ContentBlockStopEvent = serde_json::from_str(data)?; let raw: ContentBlockStopEvent = serde_json::from_str(data)?;
let block_type = state let block_type = state.current_block_type.take().unwrap_or(BlockType::Text);
.current_block_type
.take()
.unwrap_or(BlockType::Text);
emitted.push(Event::BlockStop(BlockStop { emitted.push(Event::BlockStop(BlockStop {
index: raw.index, index: raw.index,
block_type, block_type,

View File

@ -458,7 +458,10 @@ pub(crate) fn parse_sse(
"response.reasoning_text.delta" => { "response.reasoning_text.delta" => {
let ev: ReasoningTextDelta = from_json(data)?; let ev: ReasoningTextDelta = from_json(data)?;
// round-trip 用に蓄積 // round-trip 用に蓄積
state.ensure_reasoning(ev.output_index).text.push_str(&ev.delta); state
.ensure_reasoning(ev.output_index)
.text
.push_str(&ev.delta);
Ok(ensure_and_delta( Ok(ensure_and_delta(
state, state,
SlotKey::ContentPart { SlotKey::ContentPart {

View File

@ -16,9 +16,7 @@ mod common;
use common::MockLlmClient; use common::MockLlmClient;
use llm_worker::Item; use llm_worker::Item;
use llm_worker::Worker; use llm_worker::Worker;
use llm_worker::llm_client::event::{ use llm_worker::llm_client::event::{Event, ReasoningItemEvent, ResponseStatus, StatusEvent};
Event, ReasoningItemEvent, ResponseStatus, StatusEvent,
};
/// Anthropic 風: thinking ブロック → text → 終了 のシーケンス。 /// Anthropic 風: thinking ブロック → text → 終了 のシーケンス。
/// Worker history に Reasoning(signature 付き) → assistant_message が並ぶ。 /// Worker history に Reasoning(signature 付き) → assistant_message が並ぶ。

View File

@ -258,6 +258,7 @@ impl MemoryConfig {
workspace_root: upper.workspace_root.or(self.workspace_root), workspace_root: upper.workspace_root.or(self.workspace_root),
query_result_limit: upper.query_result_limit.or(self.query_result_limit), query_result_limit: upper.query_result_limit.or(self.query_result_limit),
query_excerpt_lines: upper.query_excerpt_lines.or(self.query_excerpt_lines), query_excerpt_lines: upper.query_excerpt_lines.or(self.query_excerpt_lines),
language: upper.language.or(self.language),
extract_model: upper.extract_model.or(self.extract_model), extract_model: upper.extract_model.or(self.extract_model),
extract_threshold: upper.extract_threshold.or(self.extract_threshold), extract_threshold: upper.extract_threshold.or(self.extract_threshold),
extract_worker_max_input_tokens: upper extract_worker_max_input_tokens: upper

View File

@ -62,3 +62,7 @@ pub const MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS: u64 = 30_000;
/// Optional maximum extract-worker tool-loop depth. `None` means unlimited. /// Optional maximum extract-worker tool-loop depth. `None` means unlimited.
/// See [`crate::MemoryConfig::extract_worker_max_turns`]. /// See [`crate::MemoryConfig::extract_worker_max_turns`].
pub const MEMORY_EXTRACT_WORKER_MAX_TURNS: Option<u32> = Some(8); pub const MEMORY_EXTRACT_WORKER_MAX_TURNS: Option<u32> = Some(8);
/// Default language used by memory extraction / consolidation workers for
/// durable memory and knowledge text. See [`crate::MemoryConfig::language`].
pub const MEMORY_LANGUAGE: &str = "English";

View File

@ -96,6 +96,12 @@ pub struct MemoryConfig {
/// Ignored when the request omits `query`. `None` ⇒ tool default (3). /// Ignored when the request omits `query`. `None` ⇒ tool default (3).
#[serde(default)] #[serde(default)]
pub query_excerpt_lines: Option<usize>, pub query_excerpt_lines: Option<usize>,
/// Language used by memory extraction / consolidation workers for durable
/// memory and knowledge text. Free-form so workspaces can use names like
/// `English`, `Japanese`, or locale tags. `None` ⇒
/// [`defaults::MEMORY_LANGUAGE`].
#[serde(default)]
pub language: Option<String>,
/// Optional model for the extract worker. When `None`, /// Optional model for the extract worker. When `None`,
/// the main pod model is cloned via `clone_boxed()`. Lightweight /// the main pod model is cloned via `clone_boxed()`. Lightweight
/// reasoning-capable models (Haiku / 4o-mini / Flash class) are /// reasoning-capable models (Haiku / 4o-mini / Flash class) are
@ -656,6 +662,14 @@ model_id = "claude-sonnet-4-20250514"
); );
} }
#[test]
fn memory_section_with_language() {
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nlanguage = \"Japanese\"\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let mem = manifest.memory.unwrap();
assert_eq!(mem.language.as_deref(), Some("Japanese"));
}
#[test] #[test]
fn reject_unknown_scheme() { fn reject_unknown_scheme() {
let toml = let toml =

View File

@ -2075,9 +2075,10 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
.or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS); .or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS);
let client = self.build_extractor_client(memory_cfg)?; let client = self.build_extractor_client(memory_cfg)?;
let memory_language = memory_language(memory_cfg);
let extract_system_prompt = self let extract_system_prompt = self
.prompts .prompts
.memory_extract_system() .memory_extract_system(memory_language)
.map_err(PodError::PromptCatalog)?; .map_err(PodError::PromptCatalog)?;
let mut extract_worker = Worker::new(client).system_prompt(extract_system_prompt); let mut extract_worker = Worker::new(client).system_prompt(extract_system_prompt);
extract_worker.set_cache_key(Some(self.session_id.to_string())); extract_worker.set_cache_key(Some(self.session_id.to_string()));
@ -2276,13 +2277,15 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
return Err(e); return Err(e);
} }
}; };
let consolidation_system_prompt = match self.prompts.memory_consolidation_system() { let memory_language = memory_language(memory_cfg);
Ok(p) => p, let consolidation_system_prompt =
Err(e) => { match self.prompts.memory_consolidation_system(memory_language) {
lock.release_only(); Ok(p) => p,
return Err(PodError::PromptCatalog(e)); Err(e) => {
} lock.release_only();
}; return Err(PodError::PromptCatalog(e));
}
};
let mut worker = Worker::new(client).system_prompt(consolidation_system_prompt); let mut worker = Worker::new(client).system_prompt(consolidation_system_prompt);
worker.set_cache_key(Some(self.session_id.to_string())); worker.set_cache_key(Some(self.session_id.to_string()));
@ -2331,6 +2334,14 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
} }
} }
fn memory_language(cfg: &manifest::MemoryConfig) -> &str {
cfg.language
.as_deref()
.map(str::trim)
.filter(|language| !language.is_empty())
.unwrap_or(manifest::defaults::MEMORY_LANGUAGE)
}
/// Outcome of a single extract iteration. Internal to /// Outcome of a single extract iteration. Internal to
/// `try_post_run_extract` / `run_extract_once`. /// `try_post_run_extract` / `run_extract_once`.
enum ExtractDecision { enum ExtractDecision {

View File

@ -311,14 +311,17 @@ impl PromptCatalog {
self.render(PodPrompt::CompactSystem, Value::UNDEFINED) self.render(PodPrompt::CompactSystem, Value::UNDEFINED)
} }
/// Render `PodPrompt::MemoryExtractSystem` (no inputs). /// Render `PodPrompt::MemoryExtractSystem` with `{{ language }}`.
pub fn memory_extract_system(&self) -> Result<String, CatalogError> { pub fn memory_extract_system(&self, language: &str) -> Result<String, CatalogError> {
self.render(PodPrompt::MemoryExtractSystem, Value::UNDEFINED) self.render(PodPrompt::MemoryExtractSystem, single("language", language))
} }
/// Render `PodPrompt::MemoryConsolidationSystem` (no inputs). /// Render `PodPrompt::MemoryConsolidationSystem` with `{{ language }}`.
pub fn memory_consolidation_system(&self) -> Result<String, CatalogError> { pub fn memory_consolidation_system(&self, language: &str) -> Result<String, CatalogError> {
self.render(PodPrompt::MemoryConsolidationSystem, Value::UNDEFINED) self.render(
PodPrompt::MemoryConsolidationSystem,
single("language", language),
)
} }
/// Render `PodPrompt::NotifyWrapper` with `{{ message }}`. /// Render `PodPrompt::NotifyWrapper` with `{{ message }}`.
@ -488,6 +491,15 @@ mod tests {
assert!(rendered.contains("mark_read_required")); assert!(rendered.contains("mark_read_required"));
} }
#[test]
fn memory_worker_prompts_include_language() {
let cat = PromptCatalog::builtins_only().unwrap();
let extract = cat.memory_extract_system("Japanese").unwrap();
let consolidate = cat.memory_consolidation_system("Japanese").unwrap();
assert!(extract.contains("`language`: `Japanese`"));
assert!(consolidate.contains("`language`: `Japanese`"));
}
#[test] #[test]
fn notify_wrapper_interpolates_message() { fn notify_wrapper_interpolates_message() {
let cat = PromptCatalog::builtins_only().unwrap(); let cat = PromptCatalog::builtins_only().unwrap();

View File

@ -280,10 +280,7 @@ mod tests {
assert_eq!(all.len(), 3); assert_eq!(all.len(), 3);
let alpha = state.list_knowledge_completions("alpha"); let alpha = state.list_knowledge_completions("alpha");
assert_eq!( assert_eq!(
alpha alpha.iter().map(|c| c.slug.as_str()).collect::<Vec<_>>(),
.iter()
.map(|c| c.slug.as_str())
.collect::<Vec<_>>(),
vec!["alpha", "alphabet"] vec!["alpha", "alphabet"]
); );
assert!(state.list_knowledge_completions("zzz").is_empty()); assert!(state.list_knowledge_completions("zzz").is_empty());

View File

@ -325,7 +325,8 @@ mod tests {
fn legacy_reasoning_without_signature_field_deserializes() { fn legacy_reasoning_without_signature_field_deserializes() {
// signature フィールドが無い旧形式の history.json を読み込んでも // signature フィールドが無い旧形式の history.json を読み込んでも
// None としてロードできる(後方互換性)。 // None としてロードできる(後方互換性)。
let legacy_json = r#"{"kind":"reasoning","text":"old","summary":[],"encrypted_content":null}"#; let legacy_json =
r#"{"kind":"reasoning","text":"old","summary":[],"encrypted_content":null}"#;
let parsed: LoggedItem = serde_json::from_str(legacy_json).unwrap(); let parsed: LoggedItem = serde_json::from_str(legacy_json).unwrap();
match Item::from(parsed) { match Item::from(parsed) {
Item::Reasoning { Item::Reasoning {

View File

@ -69,7 +69,9 @@ impl Renderer {
fn span_style(&self) -> Style { fn span_style(&self) -> Style {
if self.in_inline_code > 0 { if self.in_inline_code > 0 {
return Style::default().fg(Color::Yellow).bg(Color::Rgb(40, 40, 40)); return Style::default()
.fg(Color::Yellow)
.bg(Color::Rgb(40, 40, 40));
} }
if self.in_code_block { if self.in_code_block {
return Style::default().fg(Color::Cyan); return Style::default().fg(Color::Cyan);
@ -211,10 +213,8 @@ impl Renderer {
} }
Tag::BlockQuote(_) => { Tag::BlockQuote(_) => {
self.emit_blank(out); self.emit_blank(out);
self.line_prefix.push(Span::styled( self.line_prefix
"", .push(Span::styled("", Style::default().fg(Color::DarkGray)));
Style::default().fg(Color::DarkGray),
));
} }
Tag::Strong => self.bold += 1, Tag::Strong => self.bold += 1,
Tag::Emphasis => self.italic += 1, Tag::Emphasis => self.italic += 1,

View File

@ -309,10 +309,7 @@ mod tests {
s.apply_system_message_text(&text); s.apply_system_message_text(&text);
let t = &s.tasks()[0]; let t = &s.tasks()[0];
assert_eq!(t.subject, "subject with\nembedded newline"); assert_eq!(t.subject, "subject with\nembedded newline");
assert_eq!( assert_eq!(t.description, "desc:\n status: not-actually-a-field");
t.description,
"desc:\n status: not-actually-a-field"
);
} }
} }

View File

@ -165,10 +165,7 @@ fn mini_view_summary_line(counts: TaskCounts, width: u16) -> Line<'static> {
counts.deleted, counts.deleted,
); );
let shown = truncate_with_ellipsis(&text, width as usize); let shown = truncate_with_ellipsis(&text, width as usize);
Line::from(Span::styled( Line::from(Span::styled(shown, Style::default().fg(Color::DarkGray)))
shown,
Style::default().fg(Color::DarkGray),
))
} }
/// Two-character status marker + the style to render it with. Mirrors /// Two-character status marker + the style to render it with. Mirrors
@ -591,7 +588,10 @@ fn render_block_into(lines: &mut Vec<Line<'static>>, block: &Block, width: u16,
} }
Block::AssistantText { text } => match mode { Block::AssistantText { text } => match mode {
Mode::Overview => push_overview_line(lines, text, width, MessageKind::Assistant, ""), Mode::Overview => push_overview_line(lines, text, width, MessageKind::Assistant, ""),
_ => lines.extend(crate::markdown::render(text, kind_style(MessageKind::Assistant))), _ => lines.extend(crate::markdown::render(
text,
kind_style(MessageKind::Assistant),
)),
}, },
Block::Thinking(t) => render_thinking(lines, t, width, mode), Block::Thinking(t) => render_thinking(lines, t, width, mode),
// ToolCall is dispatched in `compute_history` via `tool::render_tool` // ToolCall is dispatched in `compute_history` via `tool::render_tool`

View File

@ -12,6 +12,13 @@ You have:
Your initial user message contains the staging entries, the full memory records, the knowledge candidate report, and the tidy hints. Existing knowledge bodies are NOT in the prompt; pull them through `KnowledgeQuery` + `MemoryRead` when relevant. Your initial user message contains the staging entries, the full memory records, the knowledge candidate report, and the tidy hints. Existing knowledge bodies are NOT in the prompt; pull them through `KnowledgeQuery` + `MemoryRead` when relevant.
# Memory language
- `language`: `{{ language }}`.
- Write durable memory and knowledge prose in this language, including frontmatter descriptions and record bodies.
- Existing records in another language may be rewritten into this language when you touch them for integration or tidy work; do not rewrite untouched records only for language normalization.
- Preserve code identifiers, paths, command names, quoted user text, logs, and external proper nouns when translation would reduce fidelity.
# Common rules (both steps) # Common rules (both steps)
- **Do not invent provenance.** Decisions / Requests `sources` arrays MUST be copied from the staging `source` field for the originating activity log entries. Do not synthesise `session_id` or entry ranges. Do not fabricate `last_sources` for Knowledge. - **Do not invent provenance.** Decisions / Requests `sources` arrays MUST be copied from the staging `source` field for the originating activity log entries. Do not synthesise `session_id` or entry ranges. Do not fabricate `last_sources` for Knowledge.

View File

@ -2,6 +2,12 @@ You are the activity extractor for an INSOMNIA memory subsystem.
Your single job: read the supplied conversation slice and emit a structured JSON record of "what happened" via the `write_extracted` tool. You are not consolidating, summarising, or generating knowledge — that is the consolidation worker's job. Your single job: read the supplied conversation slice and emit a structured JSON record of "what happened" via the `write_extracted` tool. You are not consolidating, summarising, or generating knowledge — that is the consolidation worker's job.
# Memory language
- `language`: `{{ language }}`.
- Write extracted fact strings (`rationale`, `topic`, `points`, `action`, `result`, `intent`, `summary`, etc.) in this language.
- Preserve code identifiers, paths, command names, quoted user text, logs, and external proper nouns when translation would reduce fidelity.
# Hard rules # Hard rules
- Call `write_extracted` exactly once. Do not narrate, ask questions, or send any other tool output. - Call `write_extracted` exactly once. Do not narrate, ask questions, or send any other tool output.