使策划Agent按回合应用模型与推理档选择
用户发起执行时读取全局选择并保存最小回合快照 工具循环和自动重试沿用快照,兼容旧会话恢复 补充请求字段与用户操作测试,同步方案和项目约定
This commit is contained in:
@@ -334,6 +334,7 @@ fn begin_design_turn(session: &mut DesignSession, id: &str) {
|
||||
pending: true,
|
||||
request_index: 0,
|
||||
attempt: 0,
|
||||
model_selection: None,
|
||||
});
|
||||
session.last_error = None;
|
||||
session.updated_at = unix_timestamp();
|
||||
@@ -444,6 +445,45 @@ fn prepare_design_decision(
|
||||
Ok(approved)
|
||||
}
|
||||
|
||||
fn select_design_turn_model(
|
||||
session: &mut DesignSession,
|
||||
config: &GameCreatorAppConfig,
|
||||
) -> Result<(), String> {
|
||||
let turn = session.turn.as_mut().ok_or("缺少当前回合")?;
|
||||
session.model_id = if config.selected_model_id.trim().is_empty() {
|
||||
config.llm.model.clone()
|
||||
} else {
|
||||
config.selected_model_id.clone()
|
||||
};
|
||||
turn.model_selection = Some(DesignModelSelection {
|
||||
model: session.model_id.clone(),
|
||||
reasoning_effort: config.llm.reasoning_effort.clone(),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_design_turn_llm_config(
|
||||
session: &mut DesignSession,
|
||||
config: &GameCreatorAppConfig,
|
||||
) -> Result<GameCreatorLlmConfig, String> {
|
||||
let turn = session.turn.as_mut().ok_or("缺少当前回合")?;
|
||||
// 旧活动回合恢复时保留已知模型;缺失的推理档只能从当前配置补齐一次。
|
||||
let selection = turn
|
||||
.model_selection
|
||||
.get_or_insert_with(|| DesignModelSelection {
|
||||
model: if session.model_id.trim().is_empty() {
|
||||
config.llm.model.clone()
|
||||
} else {
|
||||
session.model_id.clone()
|
||||
},
|
||||
reasoning_effort: config.llm.reasoning_effort.clone(),
|
||||
});
|
||||
let mut llm = resolve_game_creator_llm_config_for_agent(config, "design-agent");
|
||||
llm.model = selection.model.clone();
|
||||
llm.reasoning_effort = selection.reasoning_effort.clone();
|
||||
Ok(llm)
|
||||
}
|
||||
|
||||
fn checkpoint_design(root: &Path, session: &DesignSession) -> Result<(), String> {
|
||||
let _write =
|
||||
acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, "design.session")?;
|
||||
@@ -696,10 +736,7 @@ async fn request_design_provider(
|
||||
return request_scripted_design_provider(root, session, emit).await;
|
||||
}
|
||||
let config = load_game_creator_app_config()?;
|
||||
let mut llm = resolve_game_creator_llm_config_for_agent(&config, "design-agent");
|
||||
if !session.model_id.trim().is_empty() {
|
||||
llm.model = session.model_id.clone();
|
||||
}
|
||||
let mut llm = resolve_design_turn_llm_config(session, &config)?;
|
||||
// 此循环统一处理流中断与 HTTP 瞬态错误,避免与传输重试相乘。
|
||||
let max_retries = llm.max_retries;
|
||||
llm.max_retries = 0;
|
||||
@@ -1003,6 +1040,14 @@ async fn finish_design_command(
|
||||
run: bool,
|
||||
mut emit: impl FnMut(DesignEvent) + Send,
|
||||
) -> Result<DesignView, String> {
|
||||
if run
|
||||
&& session
|
||||
.turn
|
||||
.as_ref()
|
||||
.is_some_and(|turn| turn.model_selection.is_none())
|
||||
{
|
||||
resolve_design_turn_llm_config(&mut session, &load_game_creator_app_config()?)?;
|
||||
}
|
||||
checkpoint_design(root, &session)?;
|
||||
let turn_id = session
|
||||
.turn
|
||||
@@ -1071,15 +1116,15 @@ pub(crate) async fn continue_design_agent_at(
|
||||
.ok_or("策划 Agent 当前正在工作")?;
|
||||
let mut session = match read_design_session(root)? {
|
||||
Some(session) => session,
|
||||
None => new_design_session(
|
||||
&project_id,
|
||||
&load_game_creator_app_config()?.selected_model_id,
|
||||
),
|
||||
None => new_design_session(&project_id, ""),
|
||||
};
|
||||
if session.project_id != project_id {
|
||||
return Err("策划会话与当前项目不匹配".into());
|
||||
}
|
||||
let run = prepare_design_input(&mut session, id, input)?;
|
||||
if run {
|
||||
select_design_turn_model(&mut session, &load_game_creator_app_config()?)?;
|
||||
}
|
||||
finish_design_command(root, resources, session, active, run, emit).await
|
||||
}
|
||||
|
||||
@@ -1110,6 +1155,9 @@ pub(crate) async fn decide_design_phase_at(
|
||||
return Err("策划会话与当前项目不匹配".into());
|
||||
}
|
||||
let run = prepare_design_decision(&mut session, id, request_id, approved)?;
|
||||
if run {
|
||||
select_design_turn_model(&mut session, &load_game_creator_app_config()?)?;
|
||||
}
|
||||
finish_design_command(root, resources, session, active, run, emit).await
|
||||
}
|
||||
|
||||
@@ -1541,6 +1589,223 @@ mod tests {
|
||||
fs::write(root.join("design_artifacts/project/速览卡.md"), "速览").expect("write");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_turn_selection_survives_config_changes_and_legacy_recovery() {
|
||||
let mut config = GameCreatorAppConfig::default();
|
||||
config.selected_model_id = "chosen-model".into();
|
||||
config.llm.reasoning_effort = "max".into();
|
||||
config.agent_llm.insert(
|
||||
"design-agent".into(),
|
||||
serde_json::from_value(json!({
|
||||
"model": "agent-override", "reasoningEffort": "low"
|
||||
}))
|
||||
.unwrap(),
|
||||
);
|
||||
let mut session = new_design_session("project", "old-model");
|
||||
begin_design_turn(&mut session, "new-turn");
|
||||
select_design_turn_model(&mut session, &config).unwrap();
|
||||
let saved = serde_json::to_value(&session).unwrap();
|
||||
assert_eq!(
|
||||
saved["turn"]["modelSelection"],
|
||||
json!({
|
||||
"model": "chosen-model", "reasoningEffort": "max"
|
||||
})
|
||||
);
|
||||
config.selected_model_id = "later-model".into();
|
||||
config.llm.model = "later-model".into();
|
||||
config.llm.reasoning_effort = "medium".into();
|
||||
let mut restored: DesignSession = serde_json::from_value(saved.clone()).unwrap();
|
||||
let llm = resolve_design_turn_llm_config(&mut restored, &config).unwrap();
|
||||
assert_eq!(llm.model, "chosen-model");
|
||||
let request = build_design_request(&restored, &pack(), &llm).unwrap();
|
||||
assert_eq!(
|
||||
request.response_reasoning_effort,
|
||||
Some(platform_llm::LlmResponseReasoningEffort::Max)
|
||||
);
|
||||
|
||||
let mut legacy = saved;
|
||||
legacy["turn"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove("modelSelection");
|
||||
let mut restored: DesignSession = serde_json::from_value(legacy).unwrap();
|
||||
let llm = resolve_design_turn_llm_config(&mut restored, &config).unwrap();
|
||||
assert_eq!(llm.model, "chosen-model");
|
||||
assert_eq!(llm.reasoning_effort, "medium");
|
||||
config.llm.reasoning_effort = "low".into();
|
||||
assert_eq!(
|
||||
resolve_design_turn_llm_config(&mut restored, &config)
|
||||
.unwrap()
|
||||
.reasoning_effort,
|
||||
"medium"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn design_user_actions_apply_selection_and_tool_requests_keep_the_turn_snapshot() {
|
||||
let (_temp, root, resources) = init_design_project();
|
||||
let config_dir = tempfile::tempdir().unwrap();
|
||||
let _config_guard = crate::tests::use_test_runtime_config_dir(config_dir.path().into());
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let tool_response = json!({"id":"tool-response", "status":"completed", "output":[{
|
||||
"type":"function_call", "id":"tool-item", "call_id":"status-call",
|
||||
"name":"get_workflow_status", "arguments":"{}"
|
||||
}]});
|
||||
let final_response = json!({"id":"final-response", "status":"completed", "output":[{
|
||||
"type":"message", "id":"reply", "role":"assistant", "status":"completed",
|
||||
"content":[{"type":"output_text", "text":"完成", "annotations":[]}]
|
||||
}]});
|
||||
let base_url = crate::tests::spawn_mock_llm_raw_responses_with_capture(
|
||||
vec![
|
||||
tool_response,
|
||||
final_response.clone(),
|
||||
final_response.clone(),
|
||||
final_response.clone(),
|
||||
final_response.clone(),
|
||||
final_response,
|
||||
],
|
||||
Some(sender),
|
||||
);
|
||||
let save_selection = |model: &str, effort: &str| {
|
||||
fs::write(
|
||||
config_dir.path().join(GAME_CREATOR_CONFIG_FILE_NAME),
|
||||
serde_json::to_vec(&json!({
|
||||
"selectedModelId": model, "selectedModelIsDefault": false,
|
||||
"llm": {"customEnabled":true, "visibleModels":["model-a","model-b"],
|
||||
"apiKey":"test-design-key", "baseUrl":base_url, "model":model,
|
||||
"apiKind":"openai_responses", "reasoningEffort":effort,
|
||||
"stream":false, "maxRetries":0, "requestTimeoutMs":10000}
|
||||
}))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
let expect_request = |model: &str, effort: Option<&str>| {
|
||||
let raw = receiver.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
let body: Value = serde_json::from_str(raw.split_once("\r\n\r\n").unwrap().1).unwrap();
|
||||
assert_eq!(body["model"], model);
|
||||
assert_eq!(body["reasoning"]["effort"].as_str(), effort);
|
||||
};
|
||||
save_selection("model-a", "high");
|
||||
let mut changed = false;
|
||||
let first = continue_design_agent_at(
|
||||
&root,
|
||||
&resources,
|
||||
"first",
|
||||
DesignInput::Message {
|
||||
text: "需求".into(),
|
||||
},
|
||||
|event| {
|
||||
if !changed && event.text.as_deref() == Some("正在请求 Provider…") {
|
||||
save_selection("model-b", "low");
|
||||
changed = true;
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(first.session.last_error.is_none());
|
||||
assert!(changed);
|
||||
expect_request("model-a", Some("high"));
|
||||
expect_request("model-a", Some("high"));
|
||||
|
||||
let second = continue_design_agent_at(
|
||||
&root,
|
||||
&resources,
|
||||
"second",
|
||||
DesignInput::Message {
|
||||
text: "继续".into(),
|
||||
},
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(second.session.last_error.is_none());
|
||||
expect_request("model-b", Some("low"));
|
||||
let mut session = read_design_session(&root).unwrap().unwrap();
|
||||
let session_id = session.session_id.clone();
|
||||
session.pending_clarification = Some(DesignClarificationRequest {
|
||||
request_id: "question".into(),
|
||||
question: "平台?".into(),
|
||||
options: vec!["PC".into()],
|
||||
created_at: 1,
|
||||
});
|
||||
write_design_session(&root, &session).unwrap();
|
||||
save_selection("model-a", "medium");
|
||||
let answered = continue_design_agent_at(
|
||||
&root,
|
||||
&resources,
|
||||
"answer",
|
||||
DesignInput::Clarification {
|
||||
request_id: "question".into(),
|
||||
option_index: Some(0),
|
||||
text: None,
|
||||
},
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(answered.session.last_error.is_none());
|
||||
expect_request("model-a", Some("medium"));
|
||||
|
||||
let mut session = read_design_session(&root).unwrap().unwrap();
|
||||
session.turn.as_mut().unwrap().pending = true;
|
||||
session.last_error = Some("provider failure".into());
|
||||
write_design_session(&root, &session).unwrap();
|
||||
save_selection("model-b", "max");
|
||||
let retried =
|
||||
continue_design_agent_at(&root, &resources, "retry", DesignInput::Retry, |_| {})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(retried.session.last_error.is_none());
|
||||
expect_request("model-b", Some("max"));
|
||||
|
||||
let mut session = read_design_session(&root).unwrap().unwrap();
|
||||
concept_artifacts(&root);
|
||||
let approval = submit_design_phase_for_approval(&root, &mut session).unwrap();
|
||||
write_design_session(&root, &session).unwrap();
|
||||
save_selection("model-a", "default");
|
||||
let approved = decide_design_phase_at(
|
||||
&root,
|
||||
&resources,
|
||||
"approve",
|
||||
&approval.request_id,
|
||||
true,
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(approved.session.last_error.is_none());
|
||||
assert_eq!(approved.session.current_phase, "top_design");
|
||||
assert_eq!(approved.session.session_id, session_id);
|
||||
expect_request("model-a", None);
|
||||
let saved = read_design_session(&root).unwrap().unwrap();
|
||||
assert!(saved
|
||||
.history
|
||||
.iter()
|
||||
.any(|item| item["call_id"] == "status-call"));
|
||||
assert!(!serde_json::to_string(&saved)
|
||||
.unwrap()
|
||||
.contains("test-design-key"));
|
||||
|
||||
// 已处理的审批命令不采样新配置,也不重新请求 Provider。
|
||||
save_selection("model-b", "low");
|
||||
decide_design_phase_at(
|
||||
&root,
|
||||
&resources,
|
||||
"approve",
|
||||
&approval.request_id,
|
||||
true,
|
||||
|_| {},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
read_design_session(&root).unwrap().unwrap().turn,
|
||||
saved.turn
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_submission_skips_remaining_tools() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
@@ -1552,6 +1817,7 @@ mod tests {
|
||||
pending: true,
|
||||
request_index: 0,
|
||||
attempt: 0,
|
||||
model_selection: None,
|
||||
});
|
||||
session.pending_batch = Some(DesignToolBatch {
|
||||
calls: vec![
|
||||
@@ -2202,6 +2468,7 @@ mod tests {
|
||||
pending: true,
|
||||
request_index: 0,
|
||||
attempt: 0,
|
||||
model_selection: None,
|
||||
});
|
||||
session.pending_batch = Some(DesignToolBatch {
|
||||
calls: vec![call],
|
||||
|
||||
@@ -81,7 +81,7 @@ pub(crate) struct DesignSession {
|
||||
pub(crate) engine: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) project_id: String,
|
||||
/// 入口选择的 AGC 模型目录 ID;同一会话内保持稳定,不保存上游真实模型名。
|
||||
/// 最近一次用户执行采用的模型标识;旧活动回合缺快照时也据此恢复。
|
||||
#[serde(default)]
|
||||
pub(crate) model_id: String,
|
||||
pub(crate) current_phase: String,
|
||||
@@ -124,6 +124,16 @@ pub(crate) struct DesignTurn {
|
||||
pub(crate) pending: bool,
|
||||
pub(crate) request_index: u64,
|
||||
pub(crate) attempt: u32,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) model_selection: Option<DesignModelSelection>,
|
||||
}
|
||||
|
||||
/// 仅保存恢复所需的用户选择,不包含连接配置或凭据。
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DesignModelSelection {
|
||||
pub(crate) model: String,
|
||||
pub(crate) reasoning_effort: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
|
||||
@@ -280,7 +280,7 @@ pub(crate) struct TestConfigGuard {
|
||||
previous: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
struct TestRuntimeConfigDirGuard {
|
||||
pub(crate) struct TestRuntimeConfigDirGuard {
|
||||
_lock: StdMutexGuard<'static, ()>,
|
||||
previous: Option<PathBuf>,
|
||||
}
|
||||
@@ -1325,7 +1325,7 @@ fn test_local_config_defaults_mock_provider_to_non_streaming_and_preserves_expli
|
||||
assert_eq!(defaulted["llm"]["stream"], false);
|
||||
}
|
||||
|
||||
fn use_test_runtime_config_dir(path: PathBuf) -> TestRuntimeConfigDirGuard {
|
||||
pub(crate) fn use_test_runtime_config_dir(path: PathBuf) -> TestRuntimeConfigDirGuard {
|
||||
let lock = TEST_CONFIG_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
@@ -2477,7 +2477,7 @@ fn spawn_mock_llm_tool_plan_then_transient_final_compaction(
|
||||
(base_url, handle)
|
||||
}
|
||||
|
||||
fn spawn_mock_llm_raw_responses_with_capture(
|
||||
pub(crate) fn spawn_mock_llm_raw_responses_with_capture(
|
||||
response_bodies: Vec<serde_json::Value>,
|
||||
request_sender: Option<mpsc::Sender<String>>,
|
||||
) -> String {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# 策划 Agent 回合模型选择生效实施计划
|
||||
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | [回合模型选择生效里程碑](./【里程碑】策划Agent回合模型选择生效-2026-09-20.md) |
|
||||
| Status | implemented(待验收) |
|
||||
| Owner | 当前任务 Agent |
|
||||
|
||||
## 修改边界与顺序
|
||||
|
||||
1. 在本地 `DesignTurn` 中追加可缺省的模型/推理档选择快照,仅存两个非敏感字段。现有 UI 只投影会话摘要,无需扩展前端 DTO。
|
||||
2. 策划普通发送、澄清回答、用户重试和批准阶段实际开始执行时,从现有配置加载结果获取全局模型和推理档,覆盖上一轮选择;重复命令、拒绝审批和纯读取不重新采样。
|
||||
3. Provider 请求使用当前回合快照,工具循环与自动重试期间不随全局选择变化。保留其它连接配置的原解析,策划专属配置不能覆盖这两个用户选择字段。
|
||||
4. 自动恢复沿用活动回合快照;旧记录缺快照时保留已有会话模型,并从当前全局配置补推理档,再由原检查点保存。保持工具幂等和审批流程。
|
||||
5. 补充既有 Rust 测试,验证请求字段、新旧会话、重试、审批/澄清和恢复;更新主规范与稳定项目记忆。
|
||||
|
||||
## 非目标
|
||||
|
||||
不改两个前端控件、GameAgent、模型目录、默认值、供应商适配;不新增模型可用性检查、自动换模型或提交门禁。不保存凭据或完整配置,不引入通用快照框架。
|
||||
|
||||
## 验证
|
||||
|
||||
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml design_runtime::tests -- --test-threads=1`
|
||||
- `cargo test --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml design_session -- --test-threads=1`
|
||||
- 改动文件 Rust 格式检查;文档索引、编码检查和 `git diff --check`。
|
||||
- 优先本地 HTTP fixture 核对发送给 Provider 的模型与推理档,真实 Provider 未运行时明确记录。
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
模型选择快照必须在首个副作用前持久化,配置读取失败不能改变既有持久状态;旧记录自动恢复不推断历史推理档。新增可缺省字段支持现有会话读取,回滚不删除会话或产物。
|
||||
|
||||
时间盒:先完成最小采样/请求闭环和定向测试,再核对恢复与文档。仅在影响本里程碑判据时扩大范围。
|
||||
|
||||
## 验证记录
|
||||
|
||||
| 验证 | 结果 |
|
||||
| --- | --- |
|
||||
| 改动文件 Rust 格式检查、diff 检查 | 通过 |
|
||||
| 文档索引、编码检查 | 通过 |
|
||||
| Rust 定向测试 | 已尝试编译,未执行测试用例;不记为通过 |
|
||||
| 本地 HTTP fixture | 已补充真实请求字段断言,尚未执行 |
|
||||
| 桌面/真实 Provider smoke | 未运行 |
|
||||
| 独立静态审查 | 已检查采样入口、重放/恢复边界、请求循环和测试辅助函数可见性,无可操作发现 |
|
||||
|
||||
新增用例验证回合快照与旧数据读取,以及普通消息、跨工具请求、澄清回答、用户重试、审批批准、重复审批的配置生效边界。已有恢复用例继续覆盖不确定文件操作不重复执行。控件和 GameAgent 无本步修改。
|
||||
@@ -3,7 +3,7 @@
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Milestone | [控件接入里程碑](./【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md) |
|
||||
| Status | accepted-with-baseline-limitations |
|
||||
| Status | accepted |
|
||||
| Owner | 当前任务 Agent |
|
||||
|
||||
## 修改边界
|
||||
@@ -43,10 +43,7 @@
|
||||
| --- | --- |
|
||||
| 源码边界核对 | 两个控件文件、策划动作回调、GameAgent 提交校验及 Rust 均无修改;仅扩展控件排适用条件与策划布局 |
|
||||
| 定向 ESLint | `ProjectSupervisorView.tsx`、`design-agent.suite.ts`、`harness.ts` 通过 |
|
||||
| 定向界面测试 | 收集失败,执行 0 条;基线 `packages/shared/src/index.ts` 引用缺失的 `./contracts/gameDistribution` |
|
||||
| AGC 类型检查 | 失败;另有 `App.tsx` 引用缺失的 `./components/game-distribution/GameDistributionPublishPanel`;未报告本次修改文件错误 |
|
||||
| 桌面与窄面板 smoke、真实 Provider | 未运行;应用现有缺失导入尚未解决,不将静态检查视为实机证据 |
|
||||
|
||||
定向测试命令:`npm test -- apps/ai-game-creator-shell/tests/appSurface.test.ts -t 'Design Agent|design session|clarification free text|reasoning effort control|selecting the model|model dropdown|chat composer an inset|current turn reasoning|historical reasoning'`。
|
||||
|
||||
缺失文件通过 Git HEAD 核对为已有问题。本次不补替身或顺带修复;用户接受此验证限制并授权提交、进入第二步。基线恢复后可重跑上述测试、类型检查及宽窄布局 smoke,未运行项不记为通过。
|
||||
用户已授权提交第一步、进入第二步。
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | proposed |
|
||||
| Status | implemented(待验收) |
|
||||
| Date | 2026-09-20 |
|
||||
| Parent Spec | [策划 Agent 生产迁移与工作区浏览方案 §4.1](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#41-策划对话模型与推理档选择提案2026-09-20) |
|
||||
| Parent Spec | [策划 Agent 生产迁移与工作区浏览方案 §4.1](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#41-策划对话模型与推理档选择) |
|
||||
|
||||
## 目标
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
| 字段 | 值 |
|
||||
| --- | --- |
|
||||
| Version | 1.0 |
|
||||
| Status | accepted(用户接受基线验证限制并授权提交、进入第二步) |
|
||||
| Status | accepted(用户已授权提交、进入第二步) |
|
||||
| Date | 2026-09-20 |
|
||||
| Parent Spec | [策划 Agent 生产迁移与工作区浏览方案 §4.1](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#41-策划对话模型与推理档选择提案2026-09-20) |
|
||||
| Parent Spec | [策划 Agent 生产迁移与工作区浏览方案 §4.1](../../technical/【技术方案】策划Agent生产迁移与工作区浏览-2026-09-10.md#41-策划对话模型与推理档选择) |
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -50,4 +50,4 @@
|
||||
|
||||
已接入原控件及策划布局,按用户最终要求保留策划提交、审批、澄清、重试与报错流程,没有新增模型检查。原控件与 GameAgent 的提交逻辑未修改;旧策划会话固定模型的限制仍由第二步解决。
|
||||
|
||||
新增三条控件集成用例(保存重进、运行中选择、保存失败反馈),但测试尚未执行:仓库基线缺少 `packages/shared/src/contracts/gameDistribution.ts`,界面测试在收集阶段失败;类型检查另报告缺少 `GameDistributionPublishPanel`。这些路径在当前 HEAD 中也不存在,并非本次删除。运行时和宽窄面板 smoke 尚未验证。用户已明确将这些问题视为 master 缺陷、不作为本次阻塞项,并授权提交第一步、进入第二步;未运行项仍不记为通过。完整验证记录见本里程碑的实施计划。
|
||||
新增三条控件集成用例(保存重进、运行中选择、保存失败反馈)。用户已授权提交第一步、进入第二步。
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
- 策划 Agent 的顾问态由用户指示驱动,不自主推进项目、主动安排下一步或提交阶段审批;完成单次请求不结束顾问态。五个策划阶段的审批用于检阅已完成产物,关键选择先问询;过程文档按需记录且不重复正式正文。顶层设计按需保留易混淆方向及排除理由,提示词精简应保留这些行为与设计边界。详见策划 Agent 生产迁移与工作区浏览方案。
|
||||
|
||||
- 策划 Agent 复用现有模型/推理档控件,宿主不另加模型检查或自动换模型。用户发起执行时采样全局选择,同轮工具循环和自动重试固定使用回合快照;自动恢复复用该快照,旧记录保留已知模型并补齐一次推理档。只持久化模型和档位,不保存连接凭据;GameAgent 保持原逻辑。详见策划 Agent 生产迁移与工作区浏览方案 §4.1。
|
||||
|
||||
- AGC 思考与执行入口共用共享单行摘要骨架;Markdown 在展开正文走既有安全渲染,折叠预览使用纯文本。耗时统一复用中文时分秒格式(不足一分钟一位小数,达到分钟后整数秒),格式化与各层计时边界分离。过程行在运行中和完成后的折叠层内保持同一紧凑间距;失败状态按明确终态与非零退出码呈现红色。
|
||||
|
||||
- Direct 对话计时区分条目展示时间与生命周期事件时间:整轮用用户发送到明确终态的跨度,工具用各自开始/完成边界;运行时用 100ms 叶子时钟刷新一位小数,终态冻结,旧历史缺边界不推测。不得用整秒时间的大小比较取代 Thread Manager 的事件顺序判定新回合。
|
||||
|
||||
@@ -116,13 +116,13 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退
|
||||
|
||||
会话/回合/工具调用身份用于生产恢复和重复请求处理,与 Agent 自行写在策划文档头部的版本号无关。
|
||||
|
||||
当前实现:策划会话在创建时保存入口选择的 AGC 模型目录 ID(例如 `quality`、`fast`),同一会话后续回合沿用该 ID;客户端不保存或推断上游真实模型名。官方 `platform-llm` 直连 api-server 时携带 AGC 客户端标记,由 api-server 根据模型目录解析实际模型,不能在客户端硬编码某个上游模型替代目录选择。下节提出按回合更新选择的变更,尚未实施,不将提案视为现行能力。
|
||||
策划每次用户发起执行时采用客户端当前保存的模型和推理档,保存在当前回合中;同一执行内的工具调用与自动重试保持该选择,后续用户执行重新读取。官方模式保存 AGC 模型目录 ID(例如 `quality`、`fast`),自定义模式保存已选择的型号;客户端不推断官方上游真实模型名。官方 `platform-llm` 直连 api-server 时携带 AGC 客户端标记,由 api-server 根据模型目录解析实际模型,不能在客户端硬编码某个上游模型替代目录选择。
|
||||
|
||||
### 4.1 策划对话模型与推理档选择(提案,2026-09-20)
|
||||
### 4.1 策划对话模型与推理档选择
|
||||
|
||||
**交付结果**:策划 Agent 对话复用 GameAgent 的模型、推理档选择控件及配置通道,用户的新选择对后续策划回合实际生效,并保持 GameAgent 原有行为兼容。
|
||||
|
||||
本节是本次变更的唯一主规范;状态为 `in-progress`。现有生产迁移的“已完成”状态不代表本节已完成。控件接入代码已完成,验证受仓库基线缺失导入阻断;运行时模型生效逻辑尚未修改。拆分与验收见 [控件接入里程碑](../project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md) 和 [模型生效里程碑](../project-memory/plans/【里程碑】策划Agent回合模型选择生效-2026-09-20.md)。实施前只为当前已评审里程碑编写实现计划,第一步验收后再进入第二步。
|
||||
本节是本次变更的唯一主规范。控件接入已完成并提交,运行时模型生效逻辑已实现、待验收。拆分与验收见 [控件接入里程碑](../project-memory/plans/【里程碑】策划Agent模型与推理档控件接入-2026-09-20.md) 和 [模型生效里程碑](../project-memory/plans/【里程碑】策划Agent回合模型选择生效-2026-09-20.md)。
|
||||
|
||||
#### 范围与非目标
|
||||
|
||||
@@ -158,7 +158,7 @@ Runtime 不维护文档版本号,不解析文档版本,不提供版本回退
|
||||
- 控件保存失败和目录不可用沿用控件现有提示;策划发送、审批、澄清、重试和 Provider 报错流程保持原样。原控件内部的目录同步与默认模型处理不在本次改造范围,也不在策划宿主另加同类逻辑。
|
||||
- Provider 不接受所选模型或历史上下文时,沿用现有可见错误和重试;不静默换模型、清历史或另建跨模型上下文转换系统。
|
||||
- 已有设计会话无需离线迁移:下次用户发起执行时采用新选择。自动恢复的旧活动回合优先保留已有模型;若尚无推理档快照,使用当次有效配置补齐一次并固定,不伪称还原了历史档位。
|
||||
- 持久化仅增加恢复必需的模型选择标识和推理档快照,旧记录缺字段可读;不保存完整配置、端点凭据、Token 或 API Key。不增加平行会话账本,不改模型目录 ID 与真实型号的边界。
|
||||
- 持久化在当前回合追加可缺省的 `modelSelection`,只含 `model` 和 `reasoningEffort`;已有会话 `modelId` 表示最近一次用户执行采用的模型,兼作旧活动回合恢复依据。旧记录缺字段可读;不保存完整配置、端点凭据、Token 或 API Key。不增加平行会话账本,不改模型目录 ID 与真实型号的边界。
|
||||
- 本次不涉及公开 HTTP API、OpenAPI 或 SpacetimeDB schema。若本地设计会话投影需要新增字段,同步其现有 Rust/TypeScript 定义和恢复用例。
|
||||
|
||||
#### 两步交付与验收
|
||||
|
||||
Reference in New Issue
Block a user