feat: configure role-specific models
This commit is contained in:
@@ -213,6 +213,8 @@ pub struct MemoryExtractionProfileConfigPartial {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub model: Option<ModelManifest>,
|
pub model: Option<ModelManifest>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub reasoning: Option<ReasoningControl>,
|
||||||
|
#[serde(default)]
|
||||||
pub threshold: Option<u64>,
|
pub threshold: Option<u64>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub worker_max_turns: Option<u32>,
|
pub worker_max_turns: Option<u32>,
|
||||||
@@ -262,6 +264,7 @@ impl MemoryExtractionProfileConfigPartial {
|
|||||||
Self {
|
Self {
|
||||||
enabled: other.enabled.or(self.enabled),
|
enabled: other.enabled.or(self.enabled),
|
||||||
model: other.model.or(self.model),
|
model: other.model.or(self.model),
|
||||||
|
reasoning: other.reasoning.or(self.reasoning),
|
||||||
threshold: other.threshold.or(self.threshold),
|
threshold: other.threshold.or(self.threshold),
|
||||||
worker_max_turns: other.worker_max_turns.or(self.worker_max_turns),
|
worker_max_turns: other.worker_max_turns.or(self.worker_max_turns),
|
||||||
}
|
}
|
||||||
@@ -407,6 +410,7 @@ impl From<MemoryFeatureConfigPartial> for ResolvedMemoryFeatureConfig {
|
|||||||
extraction: MemoryExtractionProfileConfig {
|
extraction: MemoryExtractionProfileConfig {
|
||||||
enabled: extraction.enabled.unwrap_or(true),
|
enabled: extraction.enabled.unwrap_or(true),
|
||||||
model: extraction.model,
|
model: extraction.model,
|
||||||
|
reasoning: extraction.reasoning,
|
||||||
threshold: extraction.threshold.or(Some(50_000)),
|
threshold: extraction.threshold.or(Some(50_000)),
|
||||||
worker_max_turns: extraction
|
worker_max_turns: extraction
|
||||||
.worker_max_turns
|
.worker_max_turns
|
||||||
@@ -432,6 +436,7 @@ impl From<ResolvedMemoryFeatureConfig> for MemoryFeatureConfigPartial {
|
|||||||
extraction: Some(MemoryExtractionProfileConfigPartial {
|
extraction: Some(MemoryExtractionProfileConfigPartial {
|
||||||
enabled: Some(value.profile.extraction.enabled),
|
enabled: Some(value.profile.extraction.enabled),
|
||||||
model: value.profile.extraction.model,
|
model: value.profile.extraction.model,
|
||||||
|
reasoning: value.profile.extraction.reasoning,
|
||||||
threshold: value.profile.extraction.threshold,
|
threshold: value.profile.extraction.threshold,
|
||||||
worker_max_turns: value.profile.extraction.worker_max_turns,
|
worker_max_turns: value.profile.extraction.worker_max_turns,
|
||||||
}),
|
}),
|
||||||
@@ -1920,9 +1925,13 @@ inject_summary = false
|
|||||||
|
|
||||||
[feature.memory.extraction]
|
[feature.memory.extraction]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
reasoning = "medium"
|
||||||
threshold = 42000
|
threshold = 42000
|
||||||
worker_max_turns = 2
|
worker_max_turns = 2
|
||||||
|
|
||||||
|
[feature.memory.extraction.model]
|
||||||
|
ref = "codex-oauth/gpt-5.6-luna"
|
||||||
|
|
||||||
[feature.memory.consolidation]
|
[feature.memory.consolidation]
|
||||||
request_enabled = false
|
request_enabled = false
|
||||||
"#,
|
"#,
|
||||||
@@ -1935,6 +1944,14 @@ request_enabled = false
|
|||||||
assert_eq!(memory.consolidation.unwrap().request_enabled, Some(false));
|
assert_eq!(memory.consolidation.unwrap().request_enabled, Some(false));
|
||||||
let extraction = memory.extraction.unwrap();
|
let extraction = memory.extraction.unwrap();
|
||||||
assert_eq!(extraction.enabled, Some(true));
|
assert_eq!(extraction.enabled, Some(true));
|
||||||
|
assert_eq!(
|
||||||
|
extraction.reasoning,
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
extraction.model.and_then(|model| model.ref_),
|
||||||
|
Some("codex-oauth/gpt-5.6-luna".to_string())
|
||||||
|
);
|
||||||
assert_eq!(extraction.threshold, Some(42_000));
|
assert_eq!(extraction.threshold, Some(42_000));
|
||||||
assert_eq!(extraction.worker_max_turns, Some(2));
|
assert_eq!(extraction.worker_max_turns, Some(2));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ impl Default for MemoryResidentProfileConfig {
|
|||||||
pub struct MemoryExtractionProfileConfig {
|
pub struct MemoryExtractionProfileConfig {
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub model: Option<ModelManifest>,
|
pub model: Option<ModelManifest>,
|
||||||
|
pub reasoning: Option<ReasoningControl>,
|
||||||
pub threshold: Option<u64>,
|
pub threshold: Option<u64>,
|
||||||
pub worker_max_turns: Option<u32>,
|
pub worker_max_turns: Option<u32>,
|
||||||
}
|
}
|
||||||
@@ -276,6 +277,7 @@ impl Default for MemoryExtractionProfileConfig {
|
|||||||
Self {
|
Self {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
model: None,
|
model: None,
|
||||||
|
reasoning: None,
|
||||||
threshold: Some(50_000),
|
threshold: Some(50_000),
|
||||||
worker_max_turns: defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS,
|
worker_max_turns: defaults::MEMORY_EXTRACT_WORKER_MAX_TURNS,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1230,6 +1230,66 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_profiles_pin_role_models_and_reasoning() {
|
||||||
|
use crate::model::{ReasoningControl, ReasoningEffort};
|
||||||
|
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let resolve = |name: &str| {
|
||||||
|
ProfileResolver::new()
|
||||||
|
.with_workspace_base(tmp.path())
|
||||||
|
.resolve(
|
||||||
|
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, name),
|
||||||
|
ProfileResolveOptions::with_worker_name(format!("{name}-worker")),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let companion = resolve("companion");
|
||||||
|
assert_eq!(
|
||||||
|
companion.manifest.model.ref_.as_deref(),
|
||||||
|
Some("codex-oauth/gpt-6-astra")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
companion.manifest.engine.reasoning,
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::High))
|
||||||
|
);
|
||||||
|
|
||||||
|
for name in ["coder", "reviewer"] {
|
||||||
|
let role = resolve(name);
|
||||||
|
assert_eq!(
|
||||||
|
role.manifest.model.ref_.as_deref(),
|
||||||
|
Some("codex-oauth/gpt-5.6-sol")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
role.manifest.engine.reasoning,
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::High))
|
||||||
|
);
|
||||||
|
let extraction = &role.manifest.feature.memory.profile.extraction;
|
||||||
|
assert_eq!(
|
||||||
|
extraction
|
||||||
|
.model
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|model| model.ref_.as_deref()),
|
||||||
|
Some("codex-oauth/gpt-5.6-luna")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
extraction.reasoning,
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let consolidation = resolve("memory-consolidation");
|
||||||
|
assert_eq!(
|
||||||
|
consolidation.manifest.model.ref_.as_deref(),
|
||||||
|
Some("codex-oauth/gpt-5.6-luna")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
consolidation.manifest.engine.reasoning,
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn profile_rejects_workspace_memory_snapshot_authority_fields() {
|
fn profile_rejects_workspace_memory_snapshot_authority_fields() {
|
||||||
let tmp = TempDir::new().unwrap();
|
let tmp = TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -163,6 +163,17 @@ fn memory_lifecycle_task_spec() -> BackgroundTaskSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryLifecycleTask {
|
impl MemoryLifecycleTask {
|
||||||
|
fn extraction_manifest(&self) -> WorkerManifest {
|
||||||
|
let mut manifest = self.manifest.clone();
|
||||||
|
if let Some(model) = self.config.profile.extraction.model.clone() {
|
||||||
|
manifest.model = model;
|
||||||
|
}
|
||||||
|
if let Some(reasoning) = self.config.profile.extraction.reasoning.clone() {
|
||||||
|
manifest.engine.reasoning = Some(reasoning);
|
||||||
|
}
|
||||||
|
manifest
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_extraction(
|
async fn run_extraction(
|
||||||
&self,
|
&self,
|
||||||
context: BackgroundTaskContext,
|
context: BackgroundTaskContext,
|
||||||
@@ -365,10 +376,7 @@ impl MemoryLifecycleTask {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut manifest = self.manifest.clone();
|
let manifest = self.extraction_manifest();
|
||||||
if let Some(model) = self.config.profile.extraction.model.clone() {
|
|
||||||
manifest.model = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancel_observer = move |sender: tokio::sync::mpsc::Sender<()>| {
|
let cancel_observer = move |sender: tokio::sync::mpsc::Sender<()>| {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -1114,6 +1122,40 @@ permission = "write"
|
|||||||
registry.shutdown().await.unwrap();
|
registry.shutdown().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extraction_manifest_applies_memory_model_and_reasoning_overrides() {
|
||||||
|
use agen::llm_client::capability::{ReasoningControl, ReasoningEffort};
|
||||||
|
|
||||||
|
let client = ScriptClient::new(Vec::new());
|
||||||
|
let extension_writes = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let (event_tx, _) = broadcast::channel(16);
|
||||||
|
let workspace_client: Arc<dyn WorkspaceClient> =
|
||||||
|
Arc::new(RecordingWorkspaceClient::default());
|
||||||
|
let mut task = test_task(
|
||||||
|
capture(2, 250),
|
||||||
|
Box::new(client),
|
||||||
|
extension_writes,
|
||||||
|
event_tx,
|
||||||
|
workspace_client,
|
||||||
|
);
|
||||||
|
task.config.profile.extraction.model = Some(manifest::ModelManifest {
|
||||||
|
ref_: Some("codex-oauth/gpt-5.6-luna".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
task.config.profile.extraction.reasoning =
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::Medium));
|
||||||
|
|
||||||
|
let manifest = task.extraction_manifest();
|
||||||
|
assert_eq!(
|
||||||
|
manifest.model.ref_.as_deref(),
|
||||||
|
Some("codex-oauth/gpt-5.6-luna")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
manifest.engine.reasoning,
|
||||||
|
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn run_committed_background_task_finishes_empty_extraction_and_commits_pointer() {
|
async fn run_committed_background_task_finishes_empty_extraction_and_commits_pointer() {
|
||||||
let client = ScriptClient::new(vec![finish_empty_events("finish-1"), completed_events()]);
|
let client = ScriptClient::new(vec![finish_empty_events("finish-1"), completed_events()]);
|
||||||
|
|||||||
+3
-2
@@ -239,12 +239,13 @@ permission = "write"
|
|||||||
#
|
#
|
||||||
# [feature.memory.profile.extraction]
|
# [feature.memory.profile.extraction]
|
||||||
# enabled = true
|
# enabled = true
|
||||||
|
# reasoning = "medium"
|
||||||
# threshold = 30000
|
# threshold = 30000
|
||||||
# worker_max_turns = 8
|
# worker_max_turns = 8
|
||||||
#
|
#
|
||||||
# # 任意。省略時はmain modelをcloneする。
|
# # modelは任意。省略時はmain modelをcloneする。
|
||||||
# # [feature.memory.profile.extraction.model]
|
# # [feature.memory.profile.extraction.model]
|
||||||
# # ref = "anthropic/claude-haiku-4-5"
|
# # ref = "codex-oauth/gpt-5.6-luna"
|
||||||
#
|
#
|
||||||
# [feature.memory.profile.consolidation]
|
# [feature.memory.profile.consolidation]
|
||||||
# request_enabled = true
|
# request_enabled = true
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ context_window = 1050000
|
|||||||
max_context_window = 272000
|
max_context_window = 272000
|
||||||
capability = { tool_calling = "parallel", structured_output = "json_schema", reasoning = "effort", vision = true, prompt_caching = { kind = "auto" } }
|
capability = { tool_calling = "parallel", structured_output = "json_schema", reasoning = "effort", vision = true, prompt_caching = { kind = "auto" } }
|
||||||
|
|
||||||
|
[[model]]
|
||||||
|
id = "gpt-5.6-luna"
|
||||||
|
provider = "codex-oauth"
|
||||||
|
context_window = 1050000
|
||||||
|
max_context_window = 272000
|
||||||
|
capability = { tool_calling = "parallel", structured_output = "json_schema", reasoning = "effort", vision = true, prompt_caching = { kind = "auto" } }
|
||||||
|
|
||||||
[[model]]
|
[[model]]
|
||||||
id = "gpt-5.5"
|
id = "gpt-5.5"
|
||||||
provider = "codex-oauth"
|
provider = "codex-oauth"
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ feature = {
|
|||||||
resident = { inject_summary = true; };
|
resident = { inject_summary = true; };
|
||||||
extraction = {
|
extraction = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
|
model = { ref = "codex-oauth/gpt-5.6-luna"; };
|
||||||
|
reasoning = "medium";
|
||||||
threshold = 50000;
|
threshold = 50000;
|
||||||
};
|
};
|
||||||
consolidation = { request_enabled = true; };
|
consolidation = { request_enabled = true; };
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import "./base.dcdl" // {
|
|||||||
slug = "coder";
|
slug = "coder";
|
||||||
description = "Ticket implementation coder profile.";
|
description = "Ticket implementation coder profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
engine = { instruction = "role.coder"; };
|
model = { ref = "codex-oauth/gpt-5.6-sol"; };
|
||||||
|
engine = {
|
||||||
|
instruction = "role.coder";
|
||||||
|
reasoning = "high";
|
||||||
|
};
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import "./base.dcdl" // {
|
|||||||
slug = "companion";
|
slug = "companion";
|
||||||
description = "Workspace companion profile.";
|
description = "Workspace companion profile.";
|
||||||
scope = "workspace_write";
|
scope = "workspace_write";
|
||||||
|
model = { ref = "codex-oauth/gpt-6-astra"; };
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import "./base.dcdl" // {
|
|||||||
slug = "memory-consolidation";
|
slug = "memory-consolidation";
|
||||||
description = "Memory staging consolidation profile.";
|
description = "Memory staging consolidation profile.";
|
||||||
scope = "workspace_read";
|
scope = "workspace_read";
|
||||||
|
model = { ref = "codex-oauth/gpt-5.6-luna"; };
|
||||||
|
engine = { reasoning = "medium"; };
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = false; };
|
task = { enabled = false; };
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import "./base.dcdl" // {
|
|||||||
slug = "reviewer";
|
slug = "reviewer";
|
||||||
description = "Ticket review profile.";
|
description = "Ticket review profile.";
|
||||||
scope = "workspace_read";
|
scope = "workspace_read";
|
||||||
engine = { instruction = "role.reviewer"; };
|
model = { ref = "codex-oauth/gpt-5.6-sol"; };
|
||||||
|
engine = {
|
||||||
|
instruction = "role.reviewer";
|
||||||
|
reasoning = "high";
|
||||||
|
};
|
||||||
|
|
||||||
feature = {
|
feature = {
|
||||||
task = { enabled = true; };
|
task = { enabled = true; };
|
||||||
|
|||||||
Reference in New Issue
Block a user