Compare commits
3 Commits
2b5da965ca
...
023de0f58d
| Author | SHA1 | Date | |
|---|---|---|---|
| 023de0f58d | |||
| eae0efb2c0 | |||
| 0141880b9d |
|
|
@ -47,7 +47,9 @@ pub enum SpawnError {
|
|||
/// runtime ディレクトリが解決できなかった (環境変数未設定等)。
|
||||
RuntimeDirUnavailable,
|
||||
PodLaunchFailed(io::Error),
|
||||
PodExitedEarly { stderr_tail: String },
|
||||
PodExitedEarly {
|
||||
stderr_tail: String,
|
||||
},
|
||||
Timeout,
|
||||
}
|
||||
|
||||
|
|
@ -88,10 +90,7 @@ impl From<io::Error> for SpawnError {
|
|||
///
|
||||
/// `progress` は ready 行を見つけるまでに観測した stderr の各行で呼ばれる
|
||||
/// (ready 行自体は除外される)。UI の表示更新や E2E ログ取得に使う。
|
||||
pub async fn spawn_pod<F>(
|
||||
config: SpawnConfig,
|
||||
mut progress: F,
|
||||
) -> Result<SpawnReady, SpawnError>
|
||||
pub async fn spawn_pod<F>(config: SpawnConfig, mut progress: F) -> Result<SpawnReady, SpawnError>
|
||||
where
|
||||
F: FnMut(&str),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -316,7 +316,8 @@ impl AnthropicScheme {
|
|||
});
|
||||
match &raw.content_block {
|
||||
ContentBlock::Thinking {
|
||||
thinking, signature,
|
||||
thinking,
|
||||
signature,
|
||||
} => {
|
||||
state.pending_thinking = Some(PendingThinking {
|
||||
text: thinking.clone(),
|
||||
|
|
@ -372,10 +373,7 @@ impl AnthropicScheme {
|
|||
}
|
||||
AnthropicEventType::ContentBlockStop => {
|
||||
let raw: ContentBlockStopEvent = serde_json::from_str(data)?;
|
||||
let block_type = state
|
||||
.current_block_type
|
||||
.take()
|
||||
.unwrap_or(BlockType::Text);
|
||||
let block_type = state.current_block_type.take().unwrap_or(BlockType::Text);
|
||||
emitted.push(Event::BlockStop(BlockStop {
|
||||
index: raw.index,
|
||||
block_type,
|
||||
|
|
|
|||
|
|
@ -458,7 +458,10 @@ pub(crate) fn parse_sse(
|
|||
"response.reasoning_text.delta" => {
|
||||
let ev: ReasoningTextDelta = from_json(data)?;
|
||||
// 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(
|
||||
state,
|
||||
SlotKey::ContentPart {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ mod common;
|
|||
use common::MockLlmClient;
|
||||
use llm_worker::Item;
|
||||
use llm_worker::Worker;
|
||||
use llm_worker::llm_client::event::{
|
||||
Event, ReasoningItemEvent, ResponseStatus, StatusEvent,
|
||||
};
|
||||
use llm_worker::llm_client::event::{Event, ReasoningItemEvent, ResponseStatus, StatusEvent};
|
||||
|
||||
/// Anthropic 風: thinking ブロック → text → 終了 のシーケンス。
|
||||
/// Worker history に Reasoning(signature 付き) → assistant_message が並ぶ。
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ pub struct WorkerManifestConfig {
|
|||
#[serde(default)]
|
||||
pub instruction: Option<String>,
|
||||
#[serde(default)]
|
||||
pub language: Option<String>,
|
||||
#[serde(default)]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub max_turns: Option<NonZeroU32>,
|
||||
|
|
@ -258,6 +260,7 @@ impl MemoryConfig {
|
|||
workspace_root: upper.workspace_root.or(self.workspace_root),
|
||||
query_result_limit: upper.query_result_limit.or(self.query_result_limit),
|
||||
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_threshold: upper.extract_threshold.or(self.extract_threshold),
|
||||
extract_worker_max_input_tokens: upper
|
||||
|
|
@ -290,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),
|
||||
|
|
@ -428,6 +432,10 @@ impl TryFrom<PodManifestConfig> 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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -62,3 +67,7 @@ pub const MEMORY_EXTRACT_WORKER_MAX_INPUT_TOKENS: u64 = 30_000;
|
|||
/// Optional maximum extract-worker tool-loop depth. `None` means unlimited.
|
||||
/// See [`crate::MemoryConfig::extract_worker_max_turns`].
|
||||
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";
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ pub struct MemoryConfig {
|
|||
/// Ignored when the request omits `query`. `None` ⇒ tool default (3).
|
||||
#[serde(default)]
|
||||
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`,
|
||||
/// the main pod model is cloned via `clone_boxed()`. Lightweight
|
||||
/// reasoning-capable models (Haiku / 4o-mini / Flash class) are
|
||||
|
|
@ -167,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<u32>,
|
||||
#[serde(default)]
|
||||
|
|
@ -235,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 {
|
||||
|
|
@ -656,6 +672,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]
|
||||
fn reject_unknown_scheme() {
|
||||
let toml =
|
||||
|
|
@ -675,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(
|
||||
|
|
|
|||
|
|
@ -47,22 +47,71 @@ fn split_for_retained_impl(history: &[Item], records: &[UsageRecord], retained:
|
|||
// (内部で毎回再計算すると O(n²) になる)。将来ボトルネックになれば
|
||||
// record 境界で二分探索に置き換える。
|
||||
let mut chosen_source = current.source;
|
||||
let mut cut_index = history.len();
|
||||
for idx in 1..=history.len() {
|
||||
let est = tokens_at(history, records, idx, &prefix);
|
||||
if est.tokens >= target {
|
||||
chosen_source = est.source;
|
||||
return SplitPoint {
|
||||
index: idx,
|
||||
source: chosen_source,
|
||||
};
|
||||
cut_index = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
SplitPoint {
|
||||
index: history.len(),
|
||||
index: balance_to_pair_boundary(history, cut_index),
|
||||
source: chosen_source,
|
||||
}
|
||||
}
|
||||
|
||||
/// `history[cut..]` が `ToolCall` / `ToolResult` のペア境界を尊重するよう
|
||||
/// `cut` を後退させる。
|
||||
///
|
||||
/// LLM API は「`ToolResult` を送るならその `ToolCall` も同じ request に
|
||||
/// 含まれていなければならない」というバリデーションを持つ。トークン数
|
||||
/// だけで切った `cut` は並列 tool 呼び出しの途中に落ちうるので、retained
|
||||
/// 側の先頭に対応 `ToolCall` を持たない `ToolResult`(orphan)が残ると
|
||||
/// 次セッション初回 request が API バリデーションで弾かれる。
|
||||
///
|
||||
/// 対策は「retained に入る `ToolResult` について、対応 `ToolCall` も
|
||||
/// retained に含まれる位置まで `cut` を引き下げる」こと。retained_tokens
|
||||
/// 予算は超えうるが、ここでは直接 LLM に投げる訳ではなく次の
|
||||
/// `pre_llm_request` で再評価されるだけなので safe。
|
||||
///
|
||||
/// アルゴリズム: history を末尾から走査し、retained 範囲内の `ToolResult`
|
||||
/// に出会うたびに対応 `ToolCall` の位置で `cut` を min 更新する。`cut` が
|
||||
/// 下がると以前は要約側だった位置が retained に入るので、後続走査で連鎖的
|
||||
/// に正しい位置まで引き下がる。`ToolCall` の `call_id` はユニークなので
|
||||
/// 事前にマップ化して O(n) で済ます。
|
||||
fn balance_to_pair_boundary(history: &[Item], cut: usize) -> usize {
|
||||
let mut idx = cut.min(history.len());
|
||||
if idx == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let call_positions: std::collections::HashMap<&str, usize> = history
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, item)| match item {
|
||||
Item::ToolCall { call_id, .. } => Some((call_id.as_str(), i)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut k = history.len();
|
||||
while k > 0 {
|
||||
k -= 1;
|
||||
if k >= idx {
|
||||
if let Item::ToolResult { call_id, .. } = &history[k] {
|
||||
if let Some(&call_pos) = call_positions.get(call_id.as_str()) {
|
||||
if call_pos < idx {
|
||||
idx = call_pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
/// 1 つの ToolResult 項目について、`content` を `None` に射影したとき
|
||||
/// 減少するシリアライズ後バイト数。ToolResult 以外や既に content=None
|
||||
/// の item は 0 を返す。
|
||||
|
|
@ -305,4 +354,109 @@ mod tests {
|
|||
let est = savings_for_prune_impl(&history, &[record(1, 100)], &[99]);
|
||||
assert_eq!(est.tokens, 0);
|
||||
}
|
||||
|
||||
fn tc(call_id: &str) -> Item {
|
||||
Item::tool_call(call_id, "Read", "{}")
|
||||
}
|
||||
|
||||
fn tr(call_id: &str) -> Item {
|
||||
Item::tool_result(call_id, "summary")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_noop_on_clean_message_boundary() {
|
||||
let history = vec![msg("a"), msg("b"), msg("c")];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 2), 2);
|
||||
assert_eq!(balance_to_pair_boundary(&history, 0), 0);
|
||||
assert_eq!(balance_to_pair_boundary(&history, 3), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_retreats_from_inside_parallel_tool_results() {
|
||||
// [Msg, TC_a, TC_b, TC_c, TR_a, TR_b, TR_c]
|
||||
// cut=5 → retained=[TR_b, TR_c]。TR_c の TC は idx=3、TR_b は idx=2 →
|
||||
// idx=2 まで後退。だが retained に TR_a (idx=4) が新たに入り、その TC_a
|
||||
// は idx=1 でまだ外 → 連鎖後退で最終的に idx=1。retained は
|
||||
// [TC_a, TC_b, TC_c, TR_a, TR_b, TR_c]。
|
||||
let history = vec![
|
||||
msg("u"),
|
||||
tc("a"),
|
||||
tc("b"),
|
||||
tc("c"),
|
||||
tr("a"),
|
||||
tr("b"),
|
||||
tr("c"),
|
||||
];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_retreats_between_call_and_result() {
|
||||
// [TC_a, TR_a, TC_b, TR_b]。cut=3 → retained=[TR_b] orphan。
|
||||
// TC_b は idx=2 → cut=2。retained=[TC_b, TR_b]。
|
||||
let history = vec![tc("a"), tr("a"), tc("b"), tr("b")];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 3), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_cascades_through_nested_pairs() {
|
||||
// [TC_a, TC_b, TR_b, TR_a, TC_c, TR_c]。cut=3 → retained=[TR_a, TC_c, TR_c]。
|
||||
// TR_a の TC は idx=0 → cut=0。retained=full。
|
||||
let history = vec![tc("a"), tc("b"), tr("b"), tr("a"), tc("c"), tr("c")];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 3), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_noop_when_cut_at_pair_boundary() {
|
||||
// [TC_a, TR_a, Msg, TC_b, TR_b]。cut=2 → retained=[Msg, TC_b, TR_b] balanced。
|
||||
let history = vec![tc("a"), tr("a"), msg("u"), tc("b"), tr("b")];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 2), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_handles_orphan_result_without_matching_call() {
|
||||
// ToolCall がそもそも存在しない ToolResult は触らない(壊れた history は
|
||||
// ここでは直しようがない)。cut=1 → そのまま 1 を返す。
|
||||
let history = vec![msg("u"), tr("zombie")];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn balance_keeps_cut_when_call_is_inside_retained() {
|
||||
// [Msg, TC_a, TR_a]。cut=1 → retained=[TC_a, TR_a]。TR_a の call_pos=1 >= idx=1。OK。
|
||||
let history = vec![msg("u"), tc("a"), tr("a")];
|
||||
assert_eq!(balance_to_pair_boundary(&history, 1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_for_retained_aligns_to_pair_boundary() {
|
||||
// 並列 TC*3 / TR*3 ターン後に Msg を 1 件足し、retained=Msg のサイズ相当に
|
||||
// 設定。トークン的には cut=末尾近くだが、orphan を避けるため TC 群の手前
|
||||
// まで後退するはず。
|
||||
let history = vec![
|
||||
msg("user"),
|
||||
tc("a"),
|
||||
tc("b"),
|
||||
tc("c"),
|
||||
tr("a"),
|
||||
tr("b"),
|
||||
tr("c"),
|
||||
msg("tail"),
|
||||
];
|
||||
let total_bytes: u64 = history.iter().map(item_bytes).sum();
|
||||
let records = vec![record(history.len(), total_bytes)]; // rate = 1 tok/byte
|
||||
// tail の item_bytes 相当のみ retain したい。
|
||||
let tail_tokens = item_bytes(&history[7]);
|
||||
let cut = split_for_retained_impl(&history, &records, tail_tokens);
|
||||
// token 単独だと cut は 7(tail のみ retained)になるが、retained 先頭が
|
||||
// Msg なら balance しなくて OK。balance helper の no-op を確認する意味も込めて
|
||||
// index == 7 を期待する。
|
||||
assert_eq!(cut.index, 7);
|
||||
|
||||
// 逆に retained をやや増やしてトークン的に cut=6(TR_c のみ retained)に
|
||||
// させると、TR_c は orphan なので balance が 1 まで後退するはず。
|
||||
let big_retain = tail_tokens + item_bytes(&history[6]);
|
||||
let cut = split_for_retained_impl(&history, &records, big_retain);
|
||||
assert_eq!(cut.index, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -879,10 +879,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
};
|
||||
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,
|
||||
|
|
@ -2075,9 +2077,10 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
.or(manifest::defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS);
|
||||
|
||||
let client = self.build_extractor_client(memory_cfg)?;
|
||||
let memory_language = memory_language(memory_cfg);
|
||||
let extract_system_prompt = self
|
||||
.prompts
|
||||
.memory_extract_system()
|
||||
.memory_extract_system(memory_language)
|
||||
.map_err(PodError::PromptCatalog)?;
|
||||
let mut extract_worker = Worker::new(client).system_prompt(extract_system_prompt);
|
||||
extract_worker.set_cache_key(Some(self.session_id.to_string()));
|
||||
|
|
@ -2276,7 +2279,9 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
|||
return Err(e);
|
||||
}
|
||||
};
|
||||
let consolidation_system_prompt = match self.prompts.memory_consolidation_system() {
|
||||
let memory_language = memory_language(memory_cfg);
|
||||
let consolidation_system_prompt =
|
||||
match self.prompts.memory_consolidation_system(memory_language) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
lock.release_only();
|
||||
|
|
@ -2331,6 +2336,23 @@ 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -3141,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),
|
||||
|
|
|
|||
|
|
@ -311,14 +311,17 @@ impl PromptCatalog {
|
|||
self.render(PodPrompt::CompactSystem, Value::UNDEFINED)
|
||||
}
|
||||
|
||||
/// Render `PodPrompt::MemoryExtractSystem` (no inputs).
|
||||
pub fn memory_extract_system(&self) -> Result<String, CatalogError> {
|
||||
self.render(PodPrompt::MemoryExtractSystem, Value::UNDEFINED)
|
||||
/// Render `PodPrompt::MemoryExtractSystem` with `{{ language }}`.
|
||||
pub fn memory_extract_system(&self, language: &str) -> Result<String, CatalogError> {
|
||||
self.render(PodPrompt::MemoryExtractSystem, single("language", language))
|
||||
}
|
||||
|
||||
/// Render `PodPrompt::MemoryConsolidationSystem` (no inputs).
|
||||
pub fn memory_consolidation_system(&self) -> Result<String, CatalogError> {
|
||||
self.render(PodPrompt::MemoryConsolidationSystem, Value::UNDEFINED)
|
||||
/// Render `PodPrompt::MemoryConsolidationSystem` with `{{ language }}`.
|
||||
pub fn memory_consolidation_system(&self, language: &str) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
PodPrompt::MemoryConsolidationSystem,
|
||||
single("language", language),
|
||||
)
|
||||
}
|
||||
|
||||
/// Render `PodPrompt::NotifyWrapper` with `{{ message }}`.
|
||||
|
|
@ -488,6 +491,15 @@ mod tests {
|
|||
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]
|
||||
fn notify_wrapper_interpolates_message() {
|
||||
let cat = PromptCatalog::builtins_only().unwrap();
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ impl std::fmt::Debug for SystemPromptTemplate {
|
|||
pub struct SystemPromptContext<'a> {
|
||||
pub now: DateTime<Utc>,
|
||||
pub cwd: &'a Path,
|
||||
/// Language policy exposed to instruction templates as `{{ language }}`.
|
||||
pub language: &'a str,
|
||||
pub scope: &'a Scope,
|
||||
pub tool_names: Vec<String>,
|
||||
/// 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:"));
|
||||
|
|
|
|||
|
|
@ -280,10 +280,7 @@ mod tests {
|
|||
assert_eq!(all.len(), 3);
|
||||
let alpha = state.list_knowledge_completions("alpha");
|
||||
assert_eq!(
|
||||
alpha
|
||||
.iter()
|
||||
.map(|c| c.slug.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
alpha.iter().map(|c| c.slug.as_str()).collect::<Vec<_>>(),
|
||||
vec!["alpha", "alphabet"]
|
||||
);
|
||||
assert!(state.list_knowledge_completions("zzz").is_empty());
|
||||
|
|
|
|||
|
|
@ -325,7 +325,8 @@ mod tests {
|
|||
fn legacy_reasoning_without_signature_field_deserializes() {
|
||||
// signature フィールドが無い旧形式の history.json を読み込んでも
|
||||
// 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();
|
||||
match Item::from(parsed) {
|
||||
Item::Reasoning {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ impl Renderer {
|
|||
|
||||
fn span_style(&self) -> Style {
|
||||
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 {
|
||||
return Style::default().fg(Color::Cyan);
|
||||
|
|
@ -211,10 +213,8 @@ impl Renderer {
|
|||
}
|
||||
Tag::BlockQuote(_) => {
|
||||
self.emit_blank(out);
|
||||
self.line_prefix.push(Span::styled(
|
||||
"│ ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
));
|
||||
self.line_prefix
|
||||
.push(Span::styled("│ ", Style::default().fg(Color::DarkGray)));
|
||||
}
|
||||
Tag::Strong => self.bold += 1,
|
||||
Tag::Emphasis => self.italic += 1,
|
||||
|
|
|
|||
|
|
@ -309,10 +309,7 @@ mod tests {
|
|||
s.apply_system_message_text(&text);
|
||||
let t = &s.tasks()[0];
|
||||
assert_eq!(t.subject, "subject with\nembedded newline");
|
||||
assert_eq!(
|
||||
t.description,
|
||||
"desc:\n status: not-actually-a-field"
|
||||
);
|
||||
assert_eq!(t.description, "desc:\n status: not-actually-a-field");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -165,10 +165,7 @@ fn mini_view_summary_line(counts: TaskCounts, width: u16) -> Line<'static> {
|
|||
counts.deleted,
|
||||
);
|
||||
let shown = truncate_with_ellipsis(&text, width as usize);
|
||||
Line::from(Span::styled(
|
||||
shown,
|
||||
Style::default().fg(Color::DarkGray),
|
||||
))
|
||||
Line::from(Span::styled(shown, Style::default().fg(Color::DarkGray)))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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),
|
||||
// ToolCall is dispatched in `compute_history` via `tool::render_tool`
|
||||
|
|
|
|||
6
resources/prompts/common/language.md
Normal file
6
resources/prompts/common/language.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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" %}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
# 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)
|
||||
|
||||
- **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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
# 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
|
||||
|
||||
- Call `write_extracted` exactly once. Do not narrate, ask questions, or send any other tool output.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user