生成設定のmanifest化の実装

This commit is contained in:
2026-04-28 09:37:22 +09:00
parent 7a0ed7d744
commit bdf2a08459
8 changed files with 206 additions and 16 deletions
+1 -1
View File
@@ -9,6 +9,6 @@ Pod の宣言的設定を TOML マニフェストとして定義・パースす
- `ModelConfig` — LLM モデル設定(scheme、base_url、model_id、auth
- `SchemeKind` — wire scheme 種別(`Anthropic`, `OpenaiChat`, `OpenaiResponses`, `Gemini`
- `AuthRef` — 認証参照(`None`, `ApiKey { env, file }`, `CodexOAuth`
- `WorkerManifest` — ワーカー設定(システムプロンプト、max_tokens、temperature
- `WorkerManifest` — ワーカー設定(システムプロンプト、生成設定、reasoning
- `ScopeConfig` / `ScopeRule` / `Permission` — allow / deny の宣言的スコープ設定
- `Scope` — 実行時スコープ。`from_config(&ScopeConfig, pwd)` で構築し、`is_readable` / `is_writable` / `permission_at` で問い合わせる
+59
View File
@@ -65,6 +65,12 @@ pub struct WorkerManifestConfig {
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub top_k: Option<u32>,
#[serde(default)]
pub stop_sequences: Option<Vec<String>>,
#[serde(default)]
pub reasoning: Option<ReasoningControl>,
#[serde(default)]
pub tool_output: ToolOutputLimitsPartial,
@@ -226,6 +232,9 @@ impl WorkerManifestConfig {
max_tokens: upper.max_tokens.or(self.max_tokens),
max_turns: upper.max_turns.or(self.max_turns),
temperature: upper.temperature.or(self.temperature),
top_p: upper.top_p.or(self.top_p),
top_k: upper.top_k.or(self.top_k),
stop_sequences: upper.stop_sequences.or(self.stop_sequences),
reasoning: upper.reasoning.or(self.reasoning),
tool_output: self.tool_output.merge(upper.tool_output),
}
@@ -339,6 +348,9 @@ impl TryFrom<PodManifestConfig> for PodManifest {
max_tokens: cfg.worker.max_tokens,
max_turns: cfg.worker.max_turns,
temperature: cfg.worker.temperature,
top_p: cfg.worker.top_p,
top_k: cfg.worker.top_k,
stop_sequences: cfg.worker.stop_sequences.unwrap_or_default(),
reasoning: cfg.worker.reasoning,
tool_output: ToolOutputLimits {
default_max_bytes: cfg
@@ -586,6 +598,33 @@ mod tests {
);
}
#[test]
fn merge_worker_generation_settings_upper_wins() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
top_p: Some(0.8),
top_k: Some(20),
stop_sequences: Some(vec!["lower".into()]),
..Default::default()
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
top_p: Some(0.9),
stop_sequences: Some(vec!["upper".into()]),
..Default::default()
},
..Default::default()
};
let merged = lower.merge(upper);
assert_eq!(merged.worker.top_p, Some(0.9));
assert_eq!(merged.worker.top_k, Some(20));
assert_eq!(merged.worker.stop_sequences, Some(vec!["upper".into()]));
}
#[test]
fn merge_scope_accumulates_allow_and_deny() {
let lower = PodManifestConfig {
@@ -732,6 +771,26 @@ reasoning = -1
);
}
#[test]
fn from_toml_accepts_worker_generation_settings() {
let cfg = PodManifestConfig::from_toml(
r#"
[worker]
top_p = 0.9
top_k = 40
stop_sequences = ["\n\n", "</stop>"]
"#,
)
.unwrap();
assert_eq!(cfg.worker.top_p, Some(0.9));
assert_eq!(cfg.worker.top_k, Some(40));
assert_eq!(
cfg.worker.stop_sequences,
Some(vec!["\n\n".into(), "</stop>".into()])
);
}
#[test]
fn from_toml_partial_layer_succeeds() {
// A project-layer manifest with only scope set must parse fine.
+15
View File
@@ -102,6 +102,12 @@ pub struct WorkerManifest {
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub top_p: Option<f32>,
#[serde(default)]
pub top_k: Option<u32>,
#[serde(default)]
pub stop_sequences: Vec<String>,
#[serde(default)]
pub reasoning: Option<ReasoningControl>,
/// Byte-size caps applied to tool `content` before it reaches the
/// conversation history. The section is optional in TOML — when
@@ -299,6 +305,9 @@ permission = "write"
assert_eq!(manifest.scope.allow.len(), 1);
assert!(manifest.scope.deny.is_empty());
assert_eq!(manifest.worker.instruction, defaults::DEFAULT_INSTRUCTION);
assert!(manifest.worker.top_p.is_none());
assert!(manifest.worker.top_k.is_none());
assert!(manifest.worker.stop_sequences.is_empty());
}
#[test]
@@ -316,6 +325,9 @@ auth = { kind = "api_key", file = "/abs/keys/anthropic" }
instruction = "$user/reviewer"
max_tokens = 4096
temperature = 0.3
top_p = 0.9
top_k = 40
stop_sequences = ["\n\n", "</stop>"]
reasoning = "medium"
[[scope.allow]]
@@ -341,6 +353,9 @@ permission = "write"
assert_eq!(manifest.worker.instruction, "$user/reviewer");
assert_eq!(manifest.worker.max_tokens, Some(4096));
assert_eq!(manifest.worker.temperature, Some(0.3));
assert_eq!(manifest.worker.top_p, Some(0.9));
assert_eq!(manifest.worker.top_k, Some(40));
assert_eq!(manifest.worker.stop_sequences, vec!["\n\n", "</stop>"]);
assert_eq!(
manifest.worker.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
+40 -6
View File
@@ -1384,6 +1384,15 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
/// minijinja template that is parsed by `Pod::from_manifest` and
/// rendered once at first turn in `ensure_system_prompt_materialized`.
pub fn apply_worker_manifest<C: LlmClient>(worker: &mut Worker<C>, wm: &WorkerManifest) {
worker.set_request_config(request_config_from_worker_manifest(wm));
worker.set_max_turns(wm.max_turns.map(|n| n.get()));
worker.set_tool_output_limits(Some(ToolOutputLimits {
default_max_bytes: wm.tool_output.default_max_bytes,
per_tool: wm.tool_output.per_tool.clone(),
}));
}
fn request_config_from_worker_manifest(wm: &WorkerManifest) -> RequestConfig {
let mut config = RequestConfig::new();
if let Some(max_tokens) = wm.max_tokens {
config.max_tokens = Some(max_tokens);
@@ -1391,13 +1400,15 @@ pub fn apply_worker_manifest<C: LlmClient>(worker: &mut Worker<C>, wm: &WorkerMa
if let Some(temperature) = wm.temperature {
config.temperature = Some(temperature);
}
if let Some(top_p) = wm.top_p {
config.top_p = Some(top_p);
}
if let Some(top_k) = wm.top_k {
config.top_k = Some(top_k);
}
config.stop_sequences = wm.stop_sequences.clone();
config.reasoning = wm.reasoning.clone();
worker.set_request_config(config);
worker.set_max_turns(wm.max_turns.map(|n| n.get()));
worker.set_tool_output_limits(Some(ToolOutputLimits {
default_max_bytes: wm.tool_output.default_max_bytes,
per_tool: wm.tool_output.per_tool.clone(),
}));
config
}
/// Result of a Pod run.
@@ -1616,6 +1627,29 @@ mod build_summary_prompt_tests {
assert!(!prompt.contains("deliberation"));
}
#[test]
fn worker_manifest_generation_settings_become_request_config() {
let manifest = WorkerManifest {
instruction: "unused".into(),
max_tokens: Some(1024),
max_turns: None,
temperature: Some(0.2),
top_p: Some(0.9),
top_k: Some(40),
stop_sequences: vec!["\n\n".into(), "</stop>".into()],
reasoning: None,
tool_output: manifest::ToolOutputLimits::default(),
};
let config = request_config_from_worker_manifest(&manifest);
assert_eq!(config.max_tokens, Some(1024));
assert_eq!(config.temperature, Some(0.2));
assert_eq!(config.top_p, Some(0.9));
assert_eq!(config.top_k, Some(40));
assert_eq!(config.stop_sequences, vec!["\n\n", "</stop>"]);
}
#[test]
fn keeps_user_and_assistant_messages() {
let items = vec![