Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs
T
k88936 6106b4e67c 退役AGC项目对话斜杠命令与终端swarm chat入口:应用与Rust实现删除
- 删除应用侧 /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 用例,移除斜杠命令断言
2026-09-23 11:12:39 +08:00

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("改成回合制"));
}
}