6106b4e67c
- 删除应用侧 /history 精确匹配分支与 reloadHistory、chatPromptPolish 的 / 前缀绕过、chatCommandMetadata、memoryCommands、projectSummaryConstants 命令清单 - 删除只服务退役摘要面板的 project-summary/*Summaries.ts 与 agentTrace.ts 及对应测试 - 删除 /sync-canvas-project、/read、/trace 草稿回填死链(agentPresentation.ts 与 Rust suggested_canvas_tool_call) - 删除无人调用的 Tauri 命令 get_game_creation_agent_capabilities 与 get_limited_local_commands - 删除 --swarm-chat 入口、SwarmChat 变体、src/swarm_cli.rs 与整个 swarm_cli/ 目录 - 收敛 agent/interaction.rs 至自然语言 steer 决策路径,删除交互内核整层 - 删除 SWARM_TURN_*_ERROR、print_runtime_response_stream_status 及其专属测试 - 更新 ChatMarkdownMessage、chatPromptPolish、rememberCommand 与 appSurface 用例,移除斜杠命令断言
235 lines
8.8 KiB
Rust
235 lines
8.8 KiB
Rust
use super::*;
|
|
|
|
const AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS: u32 = 1_200;
|
|
pub(crate) const AGENT_RUNTIME_STEER_DECISION_TOOL: &str = "runtime_steer_decision";
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
|
struct AgentRuntimeSteerDecisionArguments {
|
|
reply: String,
|
|
interrupt_current_provider: bool,
|
|
reason: String,
|
|
}
|
|
|
|
fn agent_runtime_steer_decision_function_tool() -> platform_llm::LlmFunctionTool {
|
|
platform_llm::LlmFunctionTool::new(
|
|
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
|
prompt_text!("interaction.steer_decision_description"),
|
|
serde_json::json!({
|
|
"type": "object",
|
|
"required": ["reply", "interruptCurrentProvider", "reason"],
|
|
"additionalProperties": false,
|
|
"properties": {
|
|
"reply": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 2000
|
|
},
|
|
"interruptCurrentProvider": { "type": "boolean" },
|
|
"reason": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 500
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.with_strict(true)
|
|
}
|
|
|
|
fn build_agent_runtime_steer_decision_request(
|
|
root: &Path,
|
|
state: &AgentRuntimeState,
|
|
instruction: &str,
|
|
) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> {
|
|
let (llm, config_path, context) = build_game_creator_role_agent_context_for_session(
|
|
root,
|
|
&state.agent_id,
|
|
Some(&state.session_id),
|
|
)?;
|
|
let api_kind = parse_game_creator_llm_api_kind(&llm.api_kind)?;
|
|
let runtime_summary = serde_json::json!({
|
|
"status": state.status,
|
|
"phase": state.phase,
|
|
"currentAction": state.current_action,
|
|
"waitingOn": state.waiting_on,
|
|
"nextStep": state.next_step,
|
|
"plan": state.plan,
|
|
"appliedSteerCursor": state.applied_steer_cursor,
|
|
});
|
|
let system = format!(
|
|
prompt_text!("interaction.steer_decision_system"),
|
|
game_creator_project_supervisor_chat_system_prompt()
|
|
);
|
|
let user = format!(
|
|
prompt_text!("interaction.steer_decision_user"),
|
|
serde_json::to_string_pretty(&runtime_summary)
|
|
.map_err(|error| format!("序列化 steer Runtime 摘要失败:{error}"))?,
|
|
instruction.trim(),
|
|
context = context,
|
|
);
|
|
let request = LlmRunRequest::single_turn(system, user)
|
|
.with_api_kind(api_kind)
|
|
.with_max_output_tokens(AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS)
|
|
.with_function_tools(vec![agent_runtime_steer_decision_function_tool()])
|
|
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
|
let request = apply_game_creator_llm_reasoning_effort(request, &llm)?;
|
|
Ok((llm, config_path, request))
|
|
}
|
|
|
|
fn parse_agent_runtime_steer_decision_response(
|
|
response: &platform_llm::LlmRunResponse,
|
|
) -> Result<AgentRuntimeSteerDecision, String> {
|
|
if response.tool_calls.len() != 1 {
|
|
return Err("Supervisor steer decision 必须返回一个 runtime_steer_decision".to_string());
|
|
}
|
|
let call = &response.tool_calls[0];
|
|
if call.name != AGENT_RUNTIME_STEER_DECISION_TOOL {
|
|
return Err(format!(
|
|
"Supervisor steer decision 返回未知工具:{}",
|
|
call.name
|
|
));
|
|
}
|
|
let arguments = serde_json::from_str::<AgentRuntimeSteerDecisionArguments>(&call.arguments)
|
|
.map_err(|error| format!("runtime_steer_decision 参数无效:{error}"))?;
|
|
let reply = arguments.reply.trim();
|
|
let reason = arguments.reason.trim();
|
|
if reply.is_empty() || reason.is_empty() {
|
|
return Err("runtime_steer_decision reply/reason 不能为空".to_string());
|
|
}
|
|
Ok(AgentRuntimeSteerDecision {
|
|
reply: reply.to_string(),
|
|
interrupt_current_provider: arguments.interrupt_current_provider,
|
|
reason: reason.to_string(),
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn decide_game_creator_agent_runtime_steer_at(
|
|
root: &Path,
|
|
state: &AgentRuntimeState,
|
|
steer_id: &str,
|
|
sequence: u64,
|
|
instruction: &str,
|
|
) -> Result<AgentRuntimeSteerDecision, String> {
|
|
if let Some(decision) = read_game_creator_agent_runtime_steer_decision_at(
|
|
root,
|
|
&state.agent_id,
|
|
&state.run_id,
|
|
steer_id,
|
|
)? {
|
|
return Ok(decision);
|
|
}
|
|
if state.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
|
|| state.parent_agent_id.is_some()
|
|
|| state.parent_run_id.is_some()
|
|
{
|
|
return Err("只有根 Project Supervisor 支持运行中非终态回复".to_string());
|
|
}
|
|
let (llm, config_path, request) =
|
|
build_agent_runtime_steer_decision_request(root, state, instruction)?;
|
|
let decision_snapshot = {
|
|
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
|
root,
|
|
"runtime.steer_decision.snapshot",
|
|
)?;
|
|
let mut snapshot = capture_game_creator_agent_runtime_provider_request_snapshot_at_locked(
|
|
root,
|
|
&state.agent_id,
|
|
&state.session_id,
|
|
&state.run_id,
|
|
"steer-decision",
|
|
&format!("steer-{sequence}"),
|
|
state.applied_steer_cursor,
|
|
)?;
|
|
// The main Runtime Provider request must keep running while this
|
|
// independent LLM turn decides whether it is stale. A distinct node
|
|
// identity gives Codex app-server a separate process/thread gate.
|
|
snapshot.agent_id = format!("{}-steer-decision", state.agent_id);
|
|
snapshot.task_id = snapshot.agent_id.clone();
|
|
snapshot
|
|
};
|
|
let agent_mode =
|
|
normalize_game_creator_agent_mode(&load_game_creator_app_config()?.agent_mode)?;
|
|
let response = match agent_mode.as_str() {
|
|
GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER => {
|
|
request_game_creator_agent_codex_app_server(&decision_snapshot, &llm, request).await
|
|
}
|
|
GAME_CREATOR_AGENT_MODE_CODEX_CLI => request_game_creator_agent_codex_cli(request).await,
|
|
GAME_CREATOR_AGENT_MODE_PROVIDER => {
|
|
build_game_creator_agent_runtime_llm_client(&llm, &config_path)?
|
|
.run(request)
|
|
.await
|
|
}
|
|
_ => unreachable!("agent mode is normalized"),
|
|
}
|
|
.map_err(|error| format!("Supervisor steer decision 调用 LLM 失败:{error}"))?;
|
|
let decision = parse_agent_runtime_steer_decision_response(&response)?;
|
|
persist_game_creator_agent_runtime_steer_decision_and_reply_at(
|
|
root, state, steer_id, sequence, decision,
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn response(
|
|
text: &str,
|
|
tool_calls: Vec<platform_llm::LlmToolCall>,
|
|
) -> platform_llm::LlmRunResponse {
|
|
platform_llm::LlmRunResponse {
|
|
provider: LlmProvider::OpenAiCompatible,
|
|
model: "interaction-test".to_string(),
|
|
text: text.to_string(),
|
|
reasoning: String::new(),
|
|
finish_reason: Some("stop".to_string()),
|
|
response_id: Some("interaction-response".to_string()),
|
|
usage: None,
|
|
tool_calls,
|
|
responses_output: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn tool_call(name: &str, arguments: &str) -> platform_llm::LlmToolCall {
|
|
platform_llm::LlmToolCall {
|
|
id: "call-interaction".to_string(),
|
|
name: name.to_string(),
|
|
arguments: arguments.to_string(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn steer_decision_replies_without_interrupting_status_questions() {
|
|
let decision = parse_agent_runtime_steer_decision_response(&response(
|
|
"",
|
|
vec![tool_call(
|
|
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
|
r#"{"reply":"我正在完成玩法实现,当前任务会继续。","interruptCurrentProvider":false,"reason":"这是状态询问,不会使当前方案过期。"}"#,
|
|
)],
|
|
))
|
|
.expect("parse non-interrupting steer decision");
|
|
assert_eq!(
|
|
decision,
|
|
AgentRuntimeSteerDecision {
|
|
reply: "我正在完成玩法实现,当前任务会继续。".to_string(),
|
|
interrupt_current_provider: false,
|
|
reason: "这是状态询问,不会使当前方案过期。".to_string(),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn steer_decision_can_request_safe_interrupt_for_conflicting_change() {
|
|
let decision = parse_agent_runtime_steer_decision_response(&response(
|
|
"",
|
|
vec![tool_call(
|
|
AGENT_RUNTIME_STEER_DECISION_TOOL,
|
|
r#"{"reply":"明白,我会停止旧方向并改成回合制。","interruptCurrentProvider":true,"reason":"用户明确改向,旧 Provider 仍在生成过期方案。"}"#,
|
|
)],
|
|
))
|
|
.expect("parse interrupting steer decision");
|
|
assert!(decision.interrupt_current_provider);
|
|
assert!(decision.reply.contains("改成回合制"));
|
|
}
|
|
}
|