1959 lines
72 KiB
Rust
1959 lines
72 KiB
Rust
use super::*;
|
||
|
||
const PREVIEW_SERVE_USAGE: &str = "用法:--preview-serve <本地项目绝对路径>";
|
||
|
||
#[derive(Debug, Eq, PartialEq)]
|
||
pub(crate) enum CliCommand {
|
||
LlmStatus,
|
||
PreviewServe {
|
||
project_path: PathBuf,
|
||
},
|
||
AgentChat {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
prompt: String,
|
||
},
|
||
AgentTask {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
task: String,
|
||
initialize: bool,
|
||
},
|
||
SwarmChat {
|
||
project_path: PathBuf,
|
||
parent_agent_id: String,
|
||
initialize: bool,
|
||
run_profile: String,
|
||
},
|
||
AgentEnqueue {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
run_id: String,
|
||
task: String,
|
||
initialize: bool,
|
||
},
|
||
AgentRuntimeStatus {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
},
|
||
AgentContextCompact {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: Option<String>,
|
||
},
|
||
AgentGoalStatus {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
},
|
||
AgentGoalStart {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
run_id: String,
|
||
initialize: bool,
|
||
},
|
||
AgentGoalEdit {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
goal_id: String,
|
||
expected_revision: u64,
|
||
},
|
||
AgentGoalPause {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
goal_id: String,
|
||
expected_revision: u64,
|
||
},
|
||
AgentGoalResume {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
goal_id: String,
|
||
expected_revision: u64,
|
||
},
|
||
AgentGoalClear {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
goal_id: String,
|
||
expected_revision: u64,
|
||
},
|
||
AgentConfirm {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
run_id: String,
|
||
action_id: String,
|
||
},
|
||
AgentCancel {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
run_id: String,
|
||
},
|
||
AgentRetry {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
run_id: String,
|
||
next_run_id: String,
|
||
},
|
||
AgentSteer {
|
||
project_path: PathBuf,
|
||
agent_id: String,
|
||
session_id: String,
|
||
run_id: String,
|
||
steer_id: String,
|
||
},
|
||
AgentResume {
|
||
project_path: PathBuf,
|
||
},
|
||
RunnerStatus,
|
||
RunnerShutdownIfIdle,
|
||
AgentRun {
|
||
project_path: PathBuf,
|
||
prompt: String,
|
||
wait_for_enter: bool,
|
||
},
|
||
}
|
||
|
||
#[derive(Debug, Deserialize, Eq, PartialEq)]
|
||
#[serde(deny_unknown_fields)]
|
||
struct CliAgentGoalPayload {
|
||
outcome: String,
|
||
constraints: Vec<String>,
|
||
verification: Vec<String>,
|
||
}
|
||
|
||
impl CliCommand {
|
||
pub(crate) fn requires_external_agent_runner(&self) -> bool {
|
||
matches!(
|
||
self,
|
||
Self::AgentTask { .. }
|
||
| Self::SwarmChat { .. }
|
||
| Self::AgentEnqueue { .. }
|
||
| Self::AgentContextCompact { .. }
|
||
| Self::AgentConfirm { .. }
|
||
| Self::AgentCancel { .. }
|
||
| Self::AgentRetry { .. }
|
||
| Self::AgentSteer { .. }
|
||
| Self::AgentGoalStart { .. }
|
||
| Self::AgentGoalEdit { .. }
|
||
| Self::AgentGoalPause { .. }
|
||
| Self::AgentGoalResume { .. }
|
||
| Self::AgentGoalClear { .. }
|
||
| Self::AgentResume { .. }
|
||
| Self::RunnerShutdownIfIdle
|
||
)
|
||
}
|
||
|
||
pub(crate) fn is_read_only_status(&self) -> bool {
|
||
matches!(
|
||
self,
|
||
Self::AgentRuntimeStatus { .. } | Self::AgentGoalStatus { .. } | Self::RunnerStatus
|
||
)
|
||
}
|
||
|
||
pub(crate) fn requires_started_external_agent_runner(&self) -> bool {
|
||
self.requires_external_agent_runner()
|
||
&& !matches!(self, Self::AgentCancel { .. } | Self::RunnerShutdownIfIdle)
|
||
}
|
||
|
||
fn project_path_mut(&mut self) -> Option<(&mut PathBuf, bool)> {
|
||
match self {
|
||
Self::AgentTask {
|
||
project_path,
|
||
initialize,
|
||
..
|
||
}
|
||
| Self::SwarmChat {
|
||
project_path,
|
||
initialize,
|
||
..
|
||
}
|
||
| Self::AgentEnqueue {
|
||
project_path,
|
||
initialize,
|
||
..
|
||
}
|
||
| Self::AgentGoalStart {
|
||
project_path,
|
||
initialize,
|
||
..
|
||
} => Some((project_path, *initialize)),
|
||
Self::AgentChat { project_path, .. }
|
||
| Self::AgentRuntimeStatus { project_path, .. }
|
||
| Self::AgentContextCompact { project_path, .. }
|
||
| Self::AgentGoalStatus { project_path, .. }
|
||
| Self::AgentGoalEdit { project_path, .. }
|
||
| Self::AgentGoalPause { project_path, .. }
|
||
| Self::AgentGoalResume { project_path, .. }
|
||
| Self::AgentGoalClear { project_path, .. }
|
||
| Self::AgentConfirm { project_path, .. }
|
||
| Self::AgentCancel { project_path, .. }
|
||
| Self::AgentRetry { project_path, .. }
|
||
| Self::AgentSteer { project_path, .. }
|
||
| Self::AgentResume { project_path }
|
||
| Self::PreviewServe { project_path }
|
||
| Self::AgentRun { project_path, .. } => Some((project_path, false)),
|
||
Self::LlmStatus | Self::RunnerStatus | Self::RunnerShutdownIfIdle => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn canonicalize_cli_path(
|
||
path: &Path,
|
||
label: &str,
|
||
allow_missing_leaf: bool,
|
||
) -> Result<PathBuf, String> {
|
||
if !path.is_absolute() {
|
||
return Err(format!("{label} 必须是绝对路径"));
|
||
}
|
||
match fs::canonicalize(path) {
|
||
Ok(path) => Ok(path),
|
||
Err(error) if allow_missing_leaf && error.kind() == std::io::ErrorKind::NotFound => {
|
||
let parent = path
|
||
.parent()
|
||
.ok_or_else(|| format!("{label} 缺少可解析的父目录"))?;
|
||
let file_name = path
|
||
.file_name()
|
||
.ok_or_else(|| format!("{label} 缺少目录名"))?;
|
||
let parent = fs::canonicalize(parent).map_err(|parent_error| {
|
||
format!(
|
||
"解析 {label} 父目录失败:{}: {parent_error}",
|
||
parent.display()
|
||
)
|
||
})?;
|
||
Ok(parent.join(file_name))
|
||
}
|
||
Err(error) => Err(format!("解析 {label} 失败:{}: {error}", path.display())),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn prepare_cli_command_paths(
|
||
command: &mut CliCommand,
|
||
runtime_config_dir: Option<&Path>,
|
||
) -> Result<Option<PathBuf>, String> {
|
||
let project_path = if let Some((path, allow_missing_leaf)) = command.project_path_mut() {
|
||
let canonical = canonicalize_cli_path(path, "本地项目路径", allow_missing_leaf)?;
|
||
*path = canonical.clone();
|
||
Some(canonical)
|
||
} else {
|
||
None
|
||
};
|
||
|
||
let config_dir = runtime_config_dir
|
||
.map(|path| canonicalize_cli_path(path, "--config-dir", true))
|
||
.transpose()?;
|
||
if command.requires_external_agent_runner() && config_dir.is_none() {
|
||
return Err(
|
||
"Agent Runtime 写命令必须显式传入 --config-dir <项目外 AppData 绝对路径>".to_string(),
|
||
);
|
||
}
|
||
if let (Some(config_dir), Some(project_path)) = (&config_dir, &project_path) {
|
||
validate_game_creator_runtime_config_dir_outside_project(config_dir, project_path)?;
|
||
}
|
||
Ok(config_dir)
|
||
}
|
||
|
||
pub(crate) fn take_cli_runtime_config_dir(
|
||
args: &mut Vec<String>,
|
||
) -> Result<Option<PathBuf>, String> {
|
||
let positions = args
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(index, arg)| (arg == "--config-dir").then_some(index))
|
||
.collect::<Vec<_>>();
|
||
if positions.len() > 1 {
|
||
return Err("--config-dir 只能指定一次".to_string());
|
||
}
|
||
let Some(index) = positions.first().copied() else {
|
||
return Ok(None);
|
||
};
|
||
let value = args
|
||
.get(index + 1)
|
||
.map(String::as_str)
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.ok_or_else(|| "--config-dir 缺少目录路径".to_string())?;
|
||
let path = PathBuf::from(value);
|
||
if !path.is_absolute() {
|
||
return Err("--config-dir 必须是绝对路径".to_string());
|
||
}
|
||
args.drain(index..=index + 1);
|
||
Ok(Some(path))
|
||
}
|
||
|
||
pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus) -> Vec<String> {
|
||
let mut lines = vec![
|
||
format!("llm.configured={}", status.configured),
|
||
format!("llm.apiKeyPresent={}", status.api_key_present),
|
||
format!(
|
||
"llm.baseUrl={}",
|
||
status.base_url.as_deref().unwrap_or_default()
|
||
),
|
||
format!("llm.model={}", status.model.as_deref().unwrap_or_default()),
|
||
format!("llm.apiKind={}", status.api_kind),
|
||
format!("llm.reasoningEffort={}", status.reasoning_effort),
|
||
format!("llm.stream={}", status.stream),
|
||
format!("llm.webSearchEnabled={}", status.web_search_enabled),
|
||
format!("llm.contextWindowTokens={}", status.context_window_tokens),
|
||
format!(
|
||
"llm.autoCompactTokenLimit={}",
|
||
status.auto_compact_token_limit
|
||
),
|
||
format!(
|
||
"llm.toolOutputTokenLimit={}",
|
||
status.tool_output_token_limit
|
||
),
|
||
];
|
||
for agent in &status.agents {
|
||
lines.push(format!(
|
||
"llm.agent.{}.configured={}",
|
||
agent.agent_id, agent.configured
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.apiKeyPresent={}",
|
||
agent.agent_id, agent.api_key_present
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.baseUrl={}",
|
||
agent.agent_id,
|
||
agent.base_url.as_deref().unwrap_or_default()
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.model={}",
|
||
agent.agent_id,
|
||
agent.model.as_deref().unwrap_or_default()
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.apiKind={}",
|
||
agent.agent_id, agent.api_kind
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.reasoningEffort={}",
|
||
agent.agent_id, agent.reasoning_effort
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.stream={}",
|
||
agent.agent_id, agent.stream
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.webSearchEnabled={}",
|
||
agent.agent_id, agent.web_search_enabled
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.contextWindowTokens={}",
|
||
agent.agent_id, agent.context_window_tokens
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.autoCompactTokenLimit={}",
|
||
agent.agent_id, agent.auto_compact_token_limit
|
||
));
|
||
lines.push(format!(
|
||
"llm.agent.{}.toolOutputTokenLimit={}",
|
||
agent.agent_id, agent.tool_output_token_limit
|
||
));
|
||
if let Some(error) = agent.error.as_deref() {
|
||
lines.push(format!("llm.agent.{}.error={error}", agent.agent_id));
|
||
}
|
||
}
|
||
if let Some(error) = status.error.as_deref() {
|
||
lines.push(format!("llm.error={error}"));
|
||
}
|
||
lines
|
||
}
|
||
|
||
fn read_cli_agent_steer_instruction(reader: &mut impl Read) -> Result<String, String> {
|
||
const MAX_INSTRUCTION_BYTES: usize = 4 * 1024;
|
||
const MAX_STDIN_BYTES: u64 = 8 * 1024;
|
||
let mut bytes = Vec::new();
|
||
reader
|
||
.take(MAX_STDIN_BYTES + 1)
|
||
.read_to_end(&mut bytes)
|
||
.map_err(|error| format!("从 stdin 读取 Agent 追加指令失败:{error}"))?;
|
||
if bytes.len() as u64 > MAX_STDIN_BYTES {
|
||
return Err(format!(
|
||
"Agent 追加指令 stdin 超过 {MAX_STDIN_BYTES} 字节上限"
|
||
));
|
||
}
|
||
let instruction = String::from_utf8(bytes)
|
||
.map_err(|_| "Agent 追加指令 stdin 必须是 UTF-8 文本".to_string())?;
|
||
let instruction = instruction.trim();
|
||
if instruction.is_empty() {
|
||
return Err("Agent 追加指令 stdin 不能为空".to_string());
|
||
}
|
||
if instruction.len() > MAX_INSTRUCTION_BYTES {
|
||
return Err(format!(
|
||
"Agent 追加指令 stdin 超过 {MAX_INSTRUCTION_BYTES} 字节上限"
|
||
));
|
||
}
|
||
Ok(instruction.to_string())
|
||
}
|
||
|
||
fn read_cli_agent_goal_payload(reader: &mut impl Read) -> Result<CliAgentGoalPayload, String> {
|
||
const MAX_STDIN_BYTES: u64 = 64 * 1024;
|
||
let mut bytes = Vec::new();
|
||
reader
|
||
.take(MAX_STDIN_BYTES + 1)
|
||
.read_to_end(&mut bytes)
|
||
.map_err(|error| format!("从 stdin 读取 Agent Goal JSON 失败:{error}"))?;
|
||
if bytes.len() as u64 > MAX_STDIN_BYTES {
|
||
return Err(format!("Agent Goal stdin 超过 {MAX_STDIN_BYTES} 字节上限"));
|
||
}
|
||
if bytes.iter().all(u8::is_ascii_whitespace) {
|
||
return Err("Agent Goal stdin 不能为空".to_string());
|
||
}
|
||
let mut payload = serde_json::from_slice::<CliAgentGoalPayload>(&bytes)
|
||
.map_err(|error| format!("Agent Goal stdin 必须是结构化 JSON:{error}"))?;
|
||
payload.outcome = payload.outcome.trim().to_string();
|
||
if payload.outcome.is_empty() {
|
||
return Err("Agent Goal outcome 不能为空".to_string());
|
||
}
|
||
Ok(payload)
|
||
}
|
||
|
||
fn parse_cli_agent_goal_revision(value: &str, usage: &str) -> Result<u64, String> {
|
||
let revision = value.parse::<u64>().map_err(|_| usage.to_string())?;
|
||
if revision == 0 {
|
||
return Err(usage.to_string());
|
||
}
|
||
Ok(revision)
|
||
}
|
||
|
||
pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, String> {
|
||
if args.first().map(String::as_str) == Some("--preview-serve") {
|
||
if args.len() != 2 || args[1].trim().is_empty() {
|
||
return Err(PREVIEW_SERVE_USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::PreviewServe {
|
||
project_path: PathBuf::from(&args[1]),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--llm-status") {
|
||
return Ok(Some(CliCommand::LlmStatus));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--runner-status") {
|
||
if args.len() != 1 {
|
||
return Err("用法:--runner-status".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::RunnerStatus));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--runner-shutdown-if-idle") {
|
||
if args.len() != 1 {
|
||
return Err("用法:--runner-shutdown-if-idle".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::RunnerShutdownIfIdle));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-runtime-status") {
|
||
if args.len() != 3 {
|
||
return Err("用法:--agent-runtime-status <本地项目绝对路径> <agentId>".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentRuntimeStatus {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-context-compact") {
|
||
const USAGE: &str =
|
||
"用法:--agent-context-compact <本地项目绝对路径> <agentId> [sessionId]";
|
||
if !(args.len() == 3 || args.len() == 4)
|
||
|| args[1..].iter().any(|value| value.trim().is_empty())
|
||
{
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentContextCompact {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
session_id: args.get(3).map(|value| value.trim().to_string()),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-goal-status") {
|
||
const USAGE: &str = "用法:--agent-goal-status <本地项目绝对路径> <agentId> <sessionId>";
|
||
if args.len() != 4 || args[1..].iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentGoalStatus {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
session_id: args[3].trim().to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-goal-start") {
|
||
const USAGE: &str = "用法:--agent-goal-start [--init] <本地项目绝对路径> <agentId> <sessionId> <runId> --stdin";
|
||
let mut rest = args[1..].to_vec();
|
||
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
|
||
rest.remove(index);
|
||
true
|
||
} else {
|
||
false
|
||
};
|
||
if rest.len() != 5
|
||
|| rest.last().map(String::as_str) != Some("--stdin")
|
||
|| rest[..4].iter().any(|value| value.trim().is_empty())
|
||
{
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentGoalStart {
|
||
project_path: PathBuf::from(&rest[0]),
|
||
agent_id: rest[1].trim().to_string(),
|
||
session_id: rest[2].trim().to_string(),
|
||
run_id: rest[3].trim().to_string(),
|
||
initialize,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-goal-edit") {
|
||
const USAGE: &str = "用法:--agent-goal-edit <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision> --stdin";
|
||
if args.len() != 7
|
||
|| args.last().map(String::as_str) != Some("--stdin")
|
||
|| args[1..5].iter().any(|value| value.trim().is_empty())
|
||
{
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentGoalEdit {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
session_id: args[3].trim().to_string(),
|
||
goal_id: args[4].trim().to_string(),
|
||
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-goal-pause") {
|
||
const USAGE: &str =
|
||
"用法:--agent-goal-pause <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision>";
|
||
if args.len() != 6 || args[1..5].iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentGoalPause {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
session_id: args[3].trim().to_string(),
|
||
goal_id: args[4].trim().to_string(),
|
||
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-goal-resume") {
|
||
const USAGE: &str = "用法:--agent-goal-resume <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision>";
|
||
if args.len() != 6 || args[1..5].iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentGoalResume {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
session_id: args[3].trim().to_string(),
|
||
goal_id: args[4].trim().to_string(),
|
||
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-goal-clear") {
|
||
const USAGE: &str =
|
||
"用法:--agent-goal-clear <本地项目绝对路径> <agentId> <sessionId> <goalId> <revision>";
|
||
if args.len() != 6 || args[1..5].iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentGoalClear {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
session_id: args[3].trim().to_string(),
|
||
goal_id: args[4].trim().to_string(),
|
||
expected_revision: parse_cli_agent_goal_revision(&args[5], USAGE)?,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-confirm") {
|
||
if args.len() != 5 {
|
||
return Err(
|
||
"用法:--agent-confirm <本地项目绝对路径> <agentId> <runId> <actionId>".to_string(),
|
||
);
|
||
}
|
||
return Ok(Some(CliCommand::AgentConfirm {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
run_id: args[3].trim().to_string(),
|
||
action_id: args[4].trim().to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-cancel") {
|
||
const USAGE: &str = "用法:--agent-cancel <本地项目绝对路径> <agentId> <runId>";
|
||
if args.len() != 4 || args[1..].iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentCancel {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
run_id: args[3].trim().to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-retry") {
|
||
const USAGE: &str = "用法:--agent-retry <本地项目绝对路径> <agentId> <runId> <nextRunId>";
|
||
if args.len() != 5 || args[1..].iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentRetry {
|
||
project_path: PathBuf::from(&args[1]),
|
||
agent_id: args[2].trim().to_string(),
|
||
run_id: args[3].trim().to_string(),
|
||
next_run_id: args[4].trim().to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-steer") {
|
||
const USAGE: &str = "用法:--agent-steer <本地项目绝对路径> <agentId> <sessionId> <runId> <steerId> --stdin";
|
||
if args.len() != 7 || args.last().map(String::as_str) != Some("--stdin") {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
let values = &args[1..6];
|
||
if values.iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentSteer {
|
||
project_path: PathBuf::from(&values[0]),
|
||
agent_id: values[1].trim().to_string(),
|
||
session_id: values[2].trim().to_string(),
|
||
run_id: values[3].trim().to_string(),
|
||
steer_id: values[4].trim().to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-resume") {
|
||
if args.len() != 2 {
|
||
return Err("用法:--agent-resume <本地项目绝对路径>".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentResume {
|
||
project_path: PathBuf::from(&args[1]),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-enqueue") {
|
||
let mut rest = args[1..].to_vec();
|
||
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
|
||
rest.remove(index);
|
||
true
|
||
} else {
|
||
false
|
||
};
|
||
if rest.len() < 4 {
|
||
return Err(
|
||
"用法:--agent-enqueue [--init] <本地项目绝对路径> <agentId> <runId> <任务>"
|
||
.to_string(),
|
||
);
|
||
}
|
||
let task = rest[3..].join(" ");
|
||
if task.trim().is_empty() {
|
||
return Err("Agent 任务不能为空".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentEnqueue {
|
||
project_path: PathBuf::from(&rest[0]),
|
||
agent_id: rest[1].trim().to_string(),
|
||
run_id: rest[2].trim().to_string(),
|
||
task: task.trim().to_string(),
|
||
initialize,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-chat") {
|
||
let project_path = args.get(1).map(String::as_str).ok_or_else(|| {
|
||
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
|
||
})?;
|
||
let agent_id = args.get(2).map(String::as_str).ok_or_else(|| {
|
||
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
|
||
})?;
|
||
if args.len() < 4 {
|
||
return Err("用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string());
|
||
}
|
||
let prompt = args[3..].join(" ");
|
||
let prompt = prompt.trim();
|
||
if prompt.is_empty() {
|
||
return Err("聊天内容不能为空".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentChat {
|
||
project_path: PathBuf::from(project_path),
|
||
agent_id: agent_id.trim().to_string(),
|
||
prompt: prompt.to_string(),
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--swarm-chat") {
|
||
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] <本地项目绝对路径> [parentAgentId]";
|
||
let mut rest = args[1..].to_vec();
|
||
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
|
||
rest.remove(index);
|
||
true
|
||
} else {
|
||
false
|
||
};
|
||
let autonomous_game_build = match rest
|
||
.iter()
|
||
.filter(|arg| arg.as_str() == "--autonomous-game-build")
|
||
.count()
|
||
{
|
||
0 => false,
|
||
1 => {
|
||
let index = rest
|
||
.iter()
|
||
.position(|arg| arg == "--autonomous-game-build")
|
||
.expect("counted autonomous game build flag");
|
||
rest.remove(index);
|
||
true
|
||
}
|
||
_ => return Err(USAGE.to_string()),
|
||
};
|
||
if !(1..=2).contains(&rest.len()) || rest.iter().any(|value| value.trim().is_empty()) {
|
||
return Err(USAGE.to_string());
|
||
}
|
||
return Ok(Some(CliCommand::SwarmChat {
|
||
project_path: PathBuf::from(&rest[0]),
|
||
parent_agent_id: rest
|
||
.get(1)
|
||
.map(|value| value.trim().to_string())
|
||
.unwrap_or_else(|| GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
|
||
initialize,
|
||
run_profile: if autonomous_game_build {
|
||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string()
|
||
} else {
|
||
AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
|
||
},
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) == Some("--agent-task") {
|
||
let mut rest = args[1..].to_vec();
|
||
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
|
||
rest.remove(index);
|
||
true
|
||
} else {
|
||
false
|
||
};
|
||
let project_path = rest.first().map(String::as_str).ok_or_else(|| {
|
||
"用法:--agent-task [--init] <本地项目绝对路径> <agentId> <任务>".to_string()
|
||
})?;
|
||
let agent_id = rest.get(1).map(String::as_str).ok_or_else(|| {
|
||
"用法:--agent-task [--init] <本地项目绝对路径> <agentId> <任务>".to_string()
|
||
})?;
|
||
if rest.len() < 3 {
|
||
return Err(
|
||
"用法:--agent-task [--init] <本地项目绝对路径> <agentId> <任务>".to_string(),
|
||
);
|
||
}
|
||
let task = rest[2..].join(" ");
|
||
let task = task.trim();
|
||
if task.is_empty() {
|
||
return Err("Agent 任务不能为空".to_string());
|
||
}
|
||
return Ok(Some(CliCommand::AgentTask {
|
||
project_path: PathBuf::from(project_path),
|
||
agent_id: agent_id.trim().to_string(),
|
||
task: task.to_string(),
|
||
initialize,
|
||
}));
|
||
}
|
||
if args.first().map(String::as_str) != Some("--agent-run") {
|
||
return Ok(None);
|
||
}
|
||
let mut rest = args[1..].to_vec();
|
||
let wait_for_enter = if let Some(index) = rest.iter().position(|arg| arg == "--no-wait") {
|
||
rest.remove(index);
|
||
false
|
||
} else {
|
||
true
|
||
};
|
||
let project_path = rest
|
||
.first()
|
||
.map(String::as_str)
|
||
.ok_or_else(|| "用法:--agent-run [--no-wait] <本地项目绝对路径> <创作需求>".to_string())?;
|
||
if rest.len() < 2 {
|
||
return Err("用法:--agent-run [--no-wait] <本地项目绝对路径> <创作需求>".to_string());
|
||
}
|
||
let prompt = rest[1..].join(" ");
|
||
let prompt = prompt.trim();
|
||
if prompt.is_empty() {
|
||
return Err("创作需求不能为空".to_string());
|
||
}
|
||
Ok(Some(CliCommand::AgentRun {
|
||
project_path: PathBuf::from(project_path),
|
||
prompt: prompt.to_string(),
|
||
wait_for_enter,
|
||
}))
|
||
}
|
||
|
||
fn strip_agent_runtime_cli_private_paths(value: &mut serde_json::Value) {
|
||
match value {
|
||
serde_json::Value::Array(values) => {
|
||
for value in values {
|
||
strip_agent_runtime_cli_private_paths(value);
|
||
}
|
||
}
|
||
serde_json::Value::Object(object) => {
|
||
object.remove("sessionPath");
|
||
object.remove("eventPath");
|
||
object.remove("taskPath");
|
||
for value in object.values_mut() {
|
||
strip_agent_runtime_cli_private_paths(value);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn serialize_agent_runtime_cli_payload<T: serde::Serialize>(payload: &T) -> Result<String, String> {
|
||
let mut value = serde_json::to_value(payload)
|
||
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?;
|
||
strip_agent_runtime_cli_private_paths(&mut value);
|
||
serde_json::to_string(&value).map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))
|
||
}
|
||
|
||
pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
||
match command {
|
||
CliCommand::LlmStatus => {
|
||
let status = check_game_creator_llm_config_from_config();
|
||
for line in game_creator_llm_status_lines(&status) {
|
||
println!("{line}");
|
||
}
|
||
if status.configured {
|
||
Ok(())
|
||
} else {
|
||
Err("LLM 配置未就绪".to_string())
|
||
}
|
||
}
|
||
CliCommand::PreviewServe { project_path } => {
|
||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||
.enable_all()
|
||
.build()
|
||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||
let registry = game_creator_preview_registry();
|
||
let preview = start_local_game_preview_at(&project_path, ®istry)?;
|
||
println!("preview.running");
|
||
println!("projectPath={}", project_path.display());
|
||
println!("previewUrl={}", preview.url);
|
||
let wait_result = std::io::stdout()
|
||
.flush()
|
||
.map_err(|error| format!("刷新本地预览 CLI 输出失败:{error}"))
|
||
.and_then(|()| {
|
||
runtime
|
||
.block_on(tokio::signal::ctrl_c())
|
||
.map_err(|error| format!("等待 Ctrl+C 失败:{error}"))
|
||
});
|
||
let stop_result = stop_local_game_preview_for_root(Some(&project_path), ®istry)
|
||
.map(|_| ())
|
||
.map_err(|error| format!("停止本地预览失败:{error}"));
|
||
match (wait_result, stop_result) {
|
||
(Ok(()), Ok(())) => Ok(()),
|
||
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
|
||
(Err(wait_error), Err(stop_error)) => {
|
||
Err(format!("{wait_error};同时{stop_error}"))
|
||
}
|
||
}
|
||
}
|
||
CliCommand::AgentChat {
|
||
project_path,
|
||
agent_id,
|
||
prompt,
|
||
} => {
|
||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||
.enable_all()
|
||
.build()
|
||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||
let reply = runtime.block_on(chat_with_game_creator_role_agent_at(
|
||
&project_path,
|
||
&agent_id,
|
||
&prompt,
|
||
))?;
|
||
println!("agent.chat.completed");
|
||
println!("projectPath={}", project_path.display());
|
||
println!("agentId={agent_id}");
|
||
println!("replyText={}", reply.reply_text);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentTask {
|
||
project_path,
|
||
agent_id,
|
||
task,
|
||
initialize,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
if initialize && !project_path.join(".agent/manifest.json").is_file() {
|
||
let project_name = project_path
|
||
.file_name()
|
||
.and_then(|value| value.to_str())
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.unwrap_or("CLI Agent 项目");
|
||
init_local_game_project_at(
|
||
&project_path,
|
||
&format!("cli-agent-{}", unix_millis()),
|
||
project_name,
|
||
)?;
|
||
}
|
||
if !project_path.join(".agent/manifest.json").is_file() {
|
||
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
|
||
}
|
||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||
.enable_all()
|
||
.build()
|
||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||
let run_id = format!("cli-{agent_id}-{}", unix_millis());
|
||
let terminal = runtime.block_on(async {
|
||
let started = start_game_creator_agent_background_task_at(
|
||
&project_path,
|
||
&agent_id,
|
||
&task,
|
||
&run_id,
|
||
)?;
|
||
let canonical_run_id = started.state.run_id.clone();
|
||
let deadline = std::time::Instant::now() + Duration::from_secs(600);
|
||
loop {
|
||
let current = read_game_creator_agent_runtime_at(&project_path, &agent_id)?;
|
||
if current.state.run_id == canonical_run_id
|
||
&& (current.state.status == "idle"
|
||
|| current.state.status == "failed"
|
||
|| current.state.status == "waiting-for-confirmation"
|
||
|| current.state.status == "waiting-for-user-input")
|
||
{
|
||
break Ok::<AgentRuntimeState, String>(current.state);
|
||
}
|
||
if std::time::Instant::now() >= deadline {
|
||
break Err(format!(
|
||
"等待单 Agent 任务超时:agentId={agent_id} runId={canonical_run_id}"
|
||
));
|
||
}
|
||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||
}
|
||
})?;
|
||
println!("agent.task.terminal");
|
||
println!("projectPath={}", project_path.display());
|
||
println!("agentId={agent_id}");
|
||
println!("runId={}", terminal.run_id);
|
||
println!("status={}", terminal.status);
|
||
println!("phase={}", terminal.phase);
|
||
if let Some(reply) = terminal.last_response.as_deref() {
|
||
println!("replyText={reply}");
|
||
}
|
||
if let Some(pending) = terminal.pending_tool_action.as_ref() {
|
||
println!("pendingActionId={}", pending.action_id);
|
||
println!("pendingTool={}", pending.tool);
|
||
if let Some(summary) = pending.input_summary.as_deref() {
|
||
println!("pendingInput={summary}");
|
||
}
|
||
}
|
||
if let Some(error) = terminal.error.as_deref() {
|
||
println!("error={error}");
|
||
}
|
||
if terminal.status == "idle" && terminal.phase == "completed" {
|
||
Ok(())
|
||
} else if terminal.status == "waiting-for-confirmation" {
|
||
Err("单 Agent 任务正在等待开发者确认,请在开发窗口继续".to_string())
|
||
} else if terminal.status == "waiting-for-user-input" {
|
||
Err("单 Agent 任务正在等待用户回答,请使用 agc:chat 继续".to_string())
|
||
} else {
|
||
Err(format!(
|
||
"单 Agent 任务未完成:{} / {}",
|
||
terminal.status, terminal.phase
|
||
))
|
||
}
|
||
}
|
||
CliCommand::SwarmChat {
|
||
project_path,
|
||
parent_agent_id,
|
||
initialize,
|
||
run_profile,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
initialize_cli_agent_project(&project_path, initialize)?;
|
||
run_game_creator_swarm_chat_at(&project_path, &parent_agent_id, &run_profile)
|
||
}
|
||
CliCommand::AgentEnqueue {
|
||
project_path,
|
||
agent_id,
|
||
run_id,
|
||
task,
|
||
initialize,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
initialize_cli_agent_project(&project_path, initialize)?;
|
||
let runtime = start_game_creator_agent_background_task_at(
|
||
&project_path,
|
||
&agent_id,
|
||
&task,
|
||
&run_id,
|
||
)?;
|
||
println!("agent.enqueue.accepted");
|
||
println!("agentId={agent_id}");
|
||
println!("requestedRunId={run_id}");
|
||
println!(
|
||
"runtimeJson={}",
|
||
serialize_agent_runtime_cli_payload(&runtime)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentRuntimeStatus {
|
||
project_path,
|
||
agent_id,
|
||
} => {
|
||
let runtime = read_game_creator_agent_runtime_at(&project_path, &agent_id)?;
|
||
println!(
|
||
"runtimeJson={}",
|
||
serialize_agent_runtime_cli_payload(&runtime)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentContextCompact {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let result = compact_external_agent_runner_context(
|
||
&project_path,
|
||
&agent_id,
|
||
session_id.as_deref(),
|
||
)?;
|
||
println!(
|
||
"contextCompactionJson={}",
|
||
serde_json::to_string(&result)
|
||
.map_err(|error| format!("序列化上下文压缩结果失败:{error}"))?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentGoalStatus {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
let goal = read_game_creator_agent_goal(
|
||
project_path.display().to_string(),
|
||
agent_id,
|
||
session_id,
|
||
)?;
|
||
println!("agent.goal.status");
|
||
println!("goalJson={}", serialize_agent_runtime_cli_payload(&goal)?);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentGoalStart {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
run_id,
|
||
initialize,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
if initialize && !project_path.join(".agent/manifest.json").is_file() {
|
||
let project_name = project_path
|
||
.file_name()
|
||
.and_then(|value| value.to_str())
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.unwrap_or("CLI Goal 项目");
|
||
init_local_game_project_at(
|
||
&project_path,
|
||
&format!("cli-goal-{}", unix_millis()),
|
||
project_name,
|
||
)?;
|
||
}
|
||
if !project_path.join(".agent/manifest.json").is_file() {
|
||
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
|
||
}
|
||
let payload = read_cli_agent_goal_payload(&mut std::io::stdin().lock())?;
|
||
let result = start_game_creator_agent_goal(
|
||
project_path.display().to_string(),
|
||
agent_id,
|
||
Some(session_id),
|
||
payload.outcome,
|
||
payload.constraints,
|
||
payload.verification,
|
||
run_id,
|
||
)?;
|
||
println!("agent.goal.started");
|
||
println!(
|
||
"goalMutationJson={}",
|
||
serialize_agent_runtime_cli_payload(&result)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentGoalEdit {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let payload = read_cli_agent_goal_payload(&mut std::io::stdin().lock())?;
|
||
let result = edit_game_creator_agent_goal(
|
||
project_path.display().to_string(),
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
payload.outcome,
|
||
payload.constraints,
|
||
payload.verification,
|
||
)?;
|
||
println!("agent.goal.edited");
|
||
println!(
|
||
"goalMutationJson={}",
|
||
serialize_agent_runtime_cli_payload(&result)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentGoalPause {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let result = pause_game_creator_agent_goal(
|
||
project_path.display().to_string(),
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
)?;
|
||
println!("agent.goal.paused");
|
||
println!(
|
||
"goalMutationJson={}",
|
||
serialize_agent_runtime_cli_payload(&result)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentGoalResume {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let result = resume_game_creator_agent_goal(
|
||
project_path.display().to_string(),
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
)?;
|
||
println!("agent.goal.resumed");
|
||
println!(
|
||
"goalMutationJson={}",
|
||
serialize_agent_runtime_cli_payload(&result)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentGoalClear {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let result = clear_game_creator_agent_goal(
|
||
project_path.display().to_string(),
|
||
agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
)?;
|
||
println!("agent.goal.cleared");
|
||
println!(
|
||
"goalMutationJson={}",
|
||
serialize_agent_runtime_cli_payload(&result)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentConfirm {
|
||
project_path,
|
||
agent_id,
|
||
run_id,
|
||
action_id,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let runtime = confirm_game_creator_agent_runtime_task_at(
|
||
&project_path,
|
||
&agent_id,
|
||
&run_id,
|
||
&action_id,
|
||
"真实 E2E CLI 精确确认",
|
||
)?;
|
||
println!("agent.confirm.accepted");
|
||
println!(
|
||
"runtimeJson={}",
|
||
serialize_agent_runtime_cli_payload(&runtime)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentCancel {
|
||
project_path,
|
||
agent_id,
|
||
run_id,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
// Cancellation must remain available when a busy older Runner blocks a build
|
||
// handover. It writes the durable cancel tombstone/state locally; retry still
|
||
// requires the current executable's Runner after the old run becomes idle.
|
||
require_external_agent_runner_configured_for_cli_runtime_write(&project_path)?;
|
||
let runtime =
|
||
cancel_game_creator_agent_runtime_task_at(&project_path, &agent_id, &run_id)?;
|
||
println!("agent.cancel.accepted");
|
||
println!(
|
||
"runtimeJson={}",
|
||
serialize_agent_runtime_cli_payload(&runtime)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentRetry {
|
||
project_path,
|
||
agent_id,
|
||
run_id,
|
||
next_run_id,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let runtime = retry_game_creator_agent_runtime_task_at(
|
||
&project_path,
|
||
&agent_id,
|
||
&run_id,
|
||
&next_run_id,
|
||
)?;
|
||
println!("agent.retry.accepted");
|
||
println!(
|
||
"runtimeJson={}",
|
||
serialize_agent_runtime_cli_payload(&runtime)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentSteer {
|
||
project_path,
|
||
agent_id,
|
||
session_id,
|
||
run_id,
|
||
steer_id,
|
||
} => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let instruction = read_cli_agent_steer_instruction(&mut std::io::stdin().lock())?;
|
||
let mut result = steer_game_creator_agent_runtime_task_at(
|
||
&project_path,
|
||
&agent_id,
|
||
&session_id,
|
||
&run_id,
|
||
&steer_id,
|
||
&instruction,
|
||
"cli",
|
||
)?;
|
||
if !result.provider_interrupted {
|
||
result.provider_interrupted =
|
||
steer_external_agent_runner(&project_path, &agent_id, &run_id, &steer_id)?;
|
||
}
|
||
println!("agent.steer.accepted");
|
||
println!("agentId={agent_id}");
|
||
println!("sessionId={session_id}");
|
||
println!("runId={run_id}");
|
||
println!("steerId={steer_id}");
|
||
println!(
|
||
"steerJson={}",
|
||
serialize_agent_runtime_cli_payload(&result)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentResume { project_path } => {
|
||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
|
||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||
let runtimes = resume_game_creator_agent_background_tasks_at(&project_path)?;
|
||
println!("agent.resume.accepted");
|
||
println!(
|
||
"runtimesJson={}",
|
||
serialize_agent_runtime_cli_payload(&runtimes)?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::RunnerStatus => {
|
||
println!(
|
||
"runnerJson={}",
|
||
serde_json::to_string(&read_external_agent_runner_status())
|
||
.map_err(|error| format!("序列化 Agent Runner 状态失败:{error}"))?
|
||
);
|
||
Ok(())
|
||
}
|
||
CliCommand::RunnerShutdownIfIdle => {
|
||
let stopped = shutdown_external_agent_runner_if_idle()?;
|
||
println!("runner.stopped={stopped}");
|
||
Ok(())
|
||
}
|
||
CliCommand::AgentRun {
|
||
project_path,
|
||
prompt,
|
||
wait_for_enter,
|
||
} => {
|
||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||
.enable_all()
|
||
.build()
|
||
.map_err(|error| format!("创建 CLI runtime 失败:{error}"))?;
|
||
let result =
|
||
runtime.block_on(generate_local_game_draft_at(&project_path, &prompt, None))?;
|
||
let (preview, stop) = start_local_game_preview_for_project(&project_path)?;
|
||
record_preview_state(
|
||
&project_path,
|
||
GameCreationAppPreviewStatus::Running,
|
||
Some(preview.url.clone()),
|
||
Some(preview.port),
|
||
)?;
|
||
append_preview_log(&project_path, "running", Some(&preview.url))?;
|
||
append_preview_start_trace_step(&project_path, &preview)?;
|
||
println!("agent.run.completed");
|
||
println!("projectPath={}", result.project_path);
|
||
println!("gameIndexPath={}", result.game_index_path);
|
||
println!("designPath={}", result.design_path);
|
||
println!(
|
||
"tracePath={}",
|
||
project_path.join(".agent/run.latest.json").display()
|
||
);
|
||
println!("previewUrl={}", preview.url);
|
||
if wait_for_enter {
|
||
println!("按 Enter 停止本地预览。");
|
||
let mut line = String::new();
|
||
let _ = std::io::stdin().read_line(&mut line);
|
||
}
|
||
let _ = stop.send(());
|
||
let _ = record_preview_state(
|
||
&project_path,
|
||
GameCreationAppPreviewStatus::Stopped,
|
||
None,
|
||
None,
|
||
);
|
||
let _ = append_preview_log(&project_path, "stopped", None);
|
||
let _ = append_preview_stop_trace_step(&project_path);
|
||
Ok(())
|
||
}
|
||
}
|
||
}
|
||
|
||
fn initialize_cli_agent_project(project_path: &Path, initialize: bool) -> Result<(), String> {
|
||
if initialize && !project_path.join(".agent/manifest.json").is_file() {
|
||
let project_name = project_path
|
||
.file_name()
|
||
.and_then(|value| value.to_str())
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.unwrap_or("CLI Agent 项目");
|
||
init_local_game_project_at(
|
||
project_path,
|
||
&format!("cli-agent-{}", unix_millis()),
|
||
project_name,
|
||
)?;
|
||
}
|
||
if !project_path.join(".agent/manifest.json").is_file() {
|
||
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use std::io::Cursor;
|
||
|
||
#[test]
|
||
fn runtime_cli_payload_omits_private_storage_paths_recursively() {
|
||
let payload = serde_json::json!({
|
||
"sessionPath": "/tmp/private/session.json",
|
||
"runtime": {
|
||
"eventPath": "/tmp/private/events.jsonl",
|
||
"state": {
|
||
"sessionId": "session-7",
|
||
"runId": "run-9"
|
||
}
|
||
},
|
||
"runtimes": [{
|
||
"taskPath": "/tmp/private/tasks.jsonl",
|
||
"state": { "status": "running" }
|
||
}]
|
||
});
|
||
|
||
let encoded = serialize_agent_runtime_cli_payload(&payload)
|
||
.expect("serialize redacted Runtime CLI payload");
|
||
assert!(!encoded.contains("/tmp/private"));
|
||
let parsed = serde_json::from_str::<serde_json::Value>(&encoded)
|
||
.expect("parse redacted Runtime CLI payload");
|
||
assert!(parsed.get("sessionPath").is_none());
|
||
assert!(parsed["runtime"].get("eventPath").is_none());
|
||
assert!(parsed["runtimes"][0].get("taskPath").is_none());
|
||
assert_eq!(parsed["runtime"]["state"]["sessionId"], "session-7");
|
||
assert_eq!(parsed["runtimes"][0]["state"]["status"], "running");
|
||
}
|
||
|
||
#[test]
|
||
fn parses_preview_serve_without_runner_or_config() {
|
||
let project_path = std::env::current_dir().expect("current directory");
|
||
let mut command = parse_cli_command(&[
|
||
"--preview-serve".to_string(),
|
||
project_path.display().to_string(),
|
||
])
|
||
.expect("parse preview serve")
|
||
.expect("preview serve command");
|
||
|
||
assert_eq!(
|
||
command,
|
||
CliCommand::PreviewServe {
|
||
project_path: project_path.clone(),
|
||
}
|
||
);
|
||
assert!(!command.requires_external_agent_runner());
|
||
assert!(!command.requires_started_external_agent_runner());
|
||
assert!(!command.is_read_only_status());
|
||
assert_eq!(
|
||
prepare_cli_command_paths(&mut command, None)
|
||
.expect("prepare preview serve without config dir"),
|
||
None
|
||
);
|
||
assert_eq!(
|
||
command,
|
||
CliCommand::PreviewServe {
|
||
project_path: fs::canonicalize(project_path).expect("canonical project path"),
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn preview_serve_requires_exactly_one_non_empty_project_path() {
|
||
for args in [
|
||
vec!["--preview-serve"],
|
||
vec!["--preview-serve", " "],
|
||
vec!["--preview-serve", "/tmp/game-project", "/tmp/extra"],
|
||
] {
|
||
let args = args.into_iter().map(str::to_string).collect::<Vec<_>>();
|
||
assert_eq!(
|
||
parse_cli_command(&args),
|
||
Err(PREVIEW_SERVE_USAGE.to_string()),
|
||
"unexpected parse result for {args:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn parses_agent_steer_with_stdin_only_contract() {
|
||
let command = parse_cli_command(&[
|
||
"--agent-steer".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"run-9".to_string(),
|
||
"steer-11".to_string(),
|
||
"--stdin".to_string(),
|
||
])
|
||
.expect("parse agent steer")
|
||
.expect("agent steer command");
|
||
|
||
assert_eq!(
|
||
command,
|
||
CliCommand::AgentSteer {
|
||
project_path: PathBuf::from("/tmp/game-project"),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: "session-7".to_string(),
|
||
run_id: "run-9".to_string(),
|
||
steer_id: "steer-11".to_string(),
|
||
}
|
||
);
|
||
assert!(command.requires_external_agent_runner());
|
||
}
|
||
|
||
#[test]
|
||
fn parses_agent_cancel_and_retry_with_explicit_run_identity() {
|
||
let project_path = PathBuf::from("/tmp/game-project");
|
||
let cancel = parse_cli_command(&[
|
||
"--agent-cancel".to_string(),
|
||
project_path.display().to_string(),
|
||
" project-supervisor ".to_string(),
|
||
" run-9 ".to_string(),
|
||
])
|
||
.expect("parse agent cancel")
|
||
.expect("agent cancel command");
|
||
assert_eq!(
|
||
cancel,
|
||
CliCommand::AgentCancel {
|
||
project_path: project_path.clone(),
|
||
agent_id: "project-supervisor".to_string(),
|
||
run_id: "run-9".to_string(),
|
||
}
|
||
);
|
||
assert!(cancel.requires_external_agent_runner());
|
||
assert!(!cancel.requires_started_external_agent_runner());
|
||
assert!(!cancel.is_read_only_status());
|
||
|
||
let retry = parse_cli_command(&[
|
||
"--agent-retry".to_string(),
|
||
project_path.display().to_string(),
|
||
" project-supervisor ".to_string(),
|
||
" run-9 ".to_string(),
|
||
" run-10 ".to_string(),
|
||
])
|
||
.expect("parse agent retry")
|
||
.expect("agent retry command");
|
||
assert_eq!(
|
||
retry,
|
||
CliCommand::AgentRetry {
|
||
project_path,
|
||
agent_id: "project-supervisor".to_string(),
|
||
run_id: "run-9".to_string(),
|
||
next_run_id: "run-10".to_string(),
|
||
}
|
||
);
|
||
assert!(retry.requires_external_agent_runner());
|
||
assert!(retry.requires_started_external_agent_runner());
|
||
assert!(!retry.is_read_only_status());
|
||
}
|
||
|
||
#[test]
|
||
fn agent_cancel_and_retry_reject_missing_or_blank_identity() {
|
||
for args in [
|
||
vec!["--agent-cancel"],
|
||
vec![
|
||
"--agent-cancel",
|
||
"/tmp/game-project",
|
||
"project-supervisor",
|
||
" ",
|
||
],
|
||
vec![
|
||
"--agent-retry",
|
||
"/tmp/game-project",
|
||
"project-supervisor",
|
||
"run-9",
|
||
],
|
||
vec![
|
||
"--agent-retry",
|
||
"/tmp/game-project",
|
||
"project-supervisor",
|
||
"run-9",
|
||
"\t",
|
||
],
|
||
] {
|
||
let args = args.into_iter().map(str::to_string).collect::<Vec<_>>();
|
||
assert!(
|
||
parse_cli_command(&args).is_err(),
|
||
"args should fail: {args:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn agent_steer_rejects_missing_params_and_argv_instruction() {
|
||
assert!(parse_cli_command(&["--agent-steer".to_string()]).is_err());
|
||
assert!(parse_cli_command(&[
|
||
"--agent-steer".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"run-9".to_string(),
|
||
"steer-11".to_string(),
|
||
])
|
||
.is_err());
|
||
assert!(parse_cli_command(&[
|
||
"--agent-steer".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"run-9".to_string(),
|
||
"steer-11".to_string(),
|
||
"正文不能出现在 argv".to_string(),
|
||
])
|
||
.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn reads_agent_steer_instruction_only_from_stdin() {
|
||
let mut stdin = Cursor::new(" 先停下当前方案,改用键盘操作。\n");
|
||
assert_eq!(
|
||
read_cli_agent_steer_instruction(&mut stdin).as_deref(),
|
||
Ok("先停下当前方案,改用键盘操作。")
|
||
);
|
||
let mut empty = Cursor::new(" \r\n\t");
|
||
assert!(read_cli_agent_steer_instruction(&mut empty).is_err());
|
||
let mut exact_with_newline = Cursor::new(format!("{}\n", "x".repeat(4 * 1024)));
|
||
assert_eq!(
|
||
read_cli_agent_steer_instruction(&mut exact_with_newline)
|
||
.expect("read exact-size instruction")
|
||
.len(),
|
||
4 * 1024
|
||
);
|
||
let mut oversized = Cursor::new(vec![b'x'; 4 * 1024 + 1]);
|
||
assert!(read_cli_agent_steer_instruction(&mut oversized).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn agent_steer_requires_external_config_dir() {
|
||
let mut command = CliCommand::AgentSteer {
|
||
project_path: std::env::current_dir().expect("current directory"),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: "session-7".to_string(),
|
||
run_id: "run-9".to_string(),
|
||
steer_id: "steer-11".to_string(),
|
||
};
|
||
let error = prepare_cli_command_paths(&mut command, None)
|
||
.expect_err("agent steer must require config dir");
|
||
assert!(error.contains("--config-dir"));
|
||
}
|
||
|
||
#[test]
|
||
fn parses_all_non_interactive_goal_commands_with_explicit_cas_identity() {
|
||
let project_path = PathBuf::from("/tmp/game-project");
|
||
let status = parse_cli_command(&[
|
||
"--agent-goal-status".to_string(),
|
||
project_path.display().to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
])
|
||
.expect("parse goal status")
|
||
.expect("goal status command");
|
||
assert_eq!(
|
||
status,
|
||
CliCommand::AgentGoalStatus {
|
||
project_path: project_path.clone(),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: "session-7".to_string(),
|
||
}
|
||
);
|
||
assert!(status.is_read_only_status());
|
||
assert!(!status.requires_external_agent_runner());
|
||
|
||
let start = parse_cli_command(&[
|
||
"--agent-goal-start".to_string(),
|
||
project_path.display().to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"goal-run-9".to_string(),
|
||
"--stdin".to_string(),
|
||
])
|
||
.expect("parse goal start")
|
||
.expect("goal start command");
|
||
assert_eq!(
|
||
start,
|
||
CliCommand::AgentGoalStart {
|
||
project_path: project_path.clone(),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: "session-7".to_string(),
|
||
run_id: "goal-run-9".to_string(),
|
||
initialize: false,
|
||
}
|
||
);
|
||
|
||
let mut initialized_start = parse_cli_command(&[
|
||
"--agent-goal-start".to_string(),
|
||
"--init".to_string(),
|
||
project_path.display().to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-8".to_string(),
|
||
"goal-run-10".to_string(),
|
||
"--stdin".to_string(),
|
||
])
|
||
.expect("parse initialized goal start")
|
||
.expect("initialized goal start command");
|
||
assert_eq!(
|
||
initialized_start,
|
||
CliCommand::AgentGoalStart {
|
||
project_path: project_path.clone(),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: "session-8".to_string(),
|
||
run_id: "goal-run-10".to_string(),
|
||
initialize: true,
|
||
}
|
||
);
|
||
let (_, initialize) = initialized_start
|
||
.project_path_mut()
|
||
.expect("initialized Goal start project path");
|
||
assert!(initialize);
|
||
|
||
let edit = parse_cli_command(&[
|
||
"--agent-goal-edit".to_string(),
|
||
project_path.display().to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"goal-11".to_string(),
|
||
"3".to_string(),
|
||
"--stdin".to_string(),
|
||
])
|
||
.expect("parse goal edit")
|
||
.expect("goal edit command");
|
||
assert_eq!(
|
||
edit,
|
||
CliCommand::AgentGoalEdit {
|
||
project_path: project_path.clone(),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: "session-7".to_string(),
|
||
goal_id: "goal-11".to_string(),
|
||
expected_revision: 3,
|
||
}
|
||
);
|
||
|
||
let cas_commands = [
|
||
("--agent-goal-pause", "pause"),
|
||
("--agent-goal-resume", "resume"),
|
||
("--agent-goal-clear", "clear"),
|
||
];
|
||
for (flag, expected) in cas_commands {
|
||
let command = parse_cli_command(&[
|
||
flag.to_string(),
|
||
project_path.display().to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"goal-11".to_string(),
|
||
"3".to_string(),
|
||
])
|
||
.expect("parse Goal CAS command")
|
||
.expect("Goal CAS command");
|
||
match (expected, &command) {
|
||
(
|
||
"pause",
|
||
CliCommand::AgentGoalPause {
|
||
expected_revision, ..
|
||
},
|
||
)
|
||
| (
|
||
"resume",
|
||
CliCommand::AgentGoalResume {
|
||
expected_revision, ..
|
||
},
|
||
)
|
||
| (
|
||
"clear",
|
||
CliCommand::AgentGoalClear {
|
||
expected_revision, ..
|
||
},
|
||
) => {
|
||
assert_eq!(*expected_revision, 3)
|
||
}
|
||
_ => panic!("unexpected Goal CAS command: {command:?}"),
|
||
}
|
||
assert!(command.requires_external_agent_runner());
|
||
assert!(!command.is_read_only_status());
|
||
}
|
||
assert!(start.requires_external_agent_runner());
|
||
assert!(edit.requires_external_agent_runner());
|
||
}
|
||
|
||
#[test]
|
||
fn goal_cli_rejects_argv_bodies_bad_revisions_and_keeps_global_resume() {
|
||
assert!(parse_cli_command(&[
|
||
"--agent-goal-start".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"goal-run-9".to_string(),
|
||
"正文不能出现在 argv".to_string(),
|
||
])
|
||
.is_err());
|
||
assert!(parse_cli_command(&[
|
||
"--agent-goal-edit".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"goal-11".to_string(),
|
||
"0".to_string(),
|
||
"--stdin".to_string(),
|
||
])
|
||
.is_err());
|
||
assert!(parse_cli_command(&[
|
||
"--agent-goal-pause".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"session-7".to_string(),
|
||
"goal-11".to_string(),
|
||
"not-a-revision".to_string(),
|
||
])
|
||
.is_err());
|
||
|
||
assert_eq!(
|
||
parse_cli_command(&[
|
||
"--agent-resume".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
])
|
||
.expect("parse global agent resume")
|
||
.expect("global agent resume command"),
|
||
CliCommand::AgentResume {
|
||
project_path: PathBuf::from("/tmp/game-project"),
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn reads_strict_structured_goal_payload_from_stdin() {
|
||
let mut stdin = Cursor::new(
|
||
r#"{
|
||
"outcome": " 完成首个可玩版本 ",
|
||
"constraints": ["不新增平行 Runtime"],
|
||
"verification": ["键盘与触屏均可完成一局"]
|
||
}"#,
|
||
);
|
||
assert_eq!(
|
||
read_cli_agent_goal_payload(&mut stdin).expect("read Goal JSON"),
|
||
CliAgentGoalPayload {
|
||
outcome: "完成首个可玩版本".to_string(),
|
||
constraints: vec!["不新增平行 Runtime".to_string()],
|
||
verification: vec!["键盘与触屏均可完成一局".to_string()],
|
||
}
|
||
);
|
||
|
||
for invalid in [
|
||
r#"{"outcome":"目标","constraints":[]}"#,
|
||
r#"{"outcome":"目标","constraints":[],"verification":[],"extra":true}"#,
|
||
r#"{"outcome":" ","constraints":[],"verification":[]}"#,
|
||
"not-json",
|
||
" ",
|
||
] {
|
||
let mut stdin = Cursor::new(invalid);
|
||
assert!(
|
||
read_cli_agent_goal_payload(&mut stdin).is_err(),
|
||
"invalid payload should fail: {invalid}"
|
||
);
|
||
}
|
||
let mut oversized = Cursor::new(vec![b'x'; 64 * 1024 + 1]);
|
||
assert!(read_cli_agent_goal_payload(&mut oversized).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn parses_swarm_chat_and_requires_external_config_dir() {
|
||
let project_path = std::env::current_dir().expect("current directory");
|
||
let mut command = parse_cli_command(&[
|
||
"--swarm-chat".to_string(),
|
||
"--init".to_string(),
|
||
project_path.display().to_string(),
|
||
"code-prototype".to_string(),
|
||
])
|
||
.expect("parse swarm chat")
|
||
.expect("swarm chat command");
|
||
|
||
assert_eq!(
|
||
command,
|
||
CliCommand::SwarmChat {
|
||
project_path,
|
||
parent_agent_id: "code-prototype".to_string(),
|
||
initialize: true,
|
||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
|
||
}
|
||
);
|
||
assert!(command.requires_external_agent_runner());
|
||
let error = prepare_cli_command_paths(&mut command, None)
|
||
.expect_err("swarm chat must require config dir");
|
||
assert!(error.contains("--config-dir"));
|
||
}
|
||
|
||
#[test]
|
||
fn parses_idle_runner_shutdown_without_starting_a_new_runner() {
|
||
let mut command = parse_cli_command(&["--runner-shutdown-if-idle".to_string()])
|
||
.expect("parse idle Runner shutdown")
|
||
.expect("idle Runner shutdown command");
|
||
|
||
assert_eq!(command, CliCommand::RunnerShutdownIfIdle);
|
||
assert!(command.requires_external_agent_runner());
|
||
assert!(!command.requires_started_external_agent_runner());
|
||
let error = prepare_cli_command_paths(&mut command, None)
|
||
.expect_err("idle Runner shutdown must require config dir");
|
||
assert!(error.contains("--config-dir"));
|
||
assert!(parse_cli_command(
|
||
&["--runner-shutdown-if-idle".to_string(), "extra".to_string(),]
|
||
)
|
||
.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn parses_agent_context_compaction_with_optional_session() {
|
||
assert_eq!(
|
||
parse_cli_command(&[
|
||
"--agent-context-compact".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"agent-session-code-prototype".to_string(),
|
||
])
|
||
.expect("parse context compaction"),
|
||
Some(CliCommand::AgentContextCompact {
|
||
project_path: PathBuf::from("/tmp/game-project"),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: Some("agent-session-code-prototype".to_string()),
|
||
})
|
||
);
|
||
assert_eq!(
|
||
parse_cli_command(&[
|
||
"--agent-context-compact".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
])
|
||
.expect("parse active-session compaction"),
|
||
Some(CliCommand::AgentContextCompact {
|
||
project_path: PathBuf::from("/tmp/game-project"),
|
||
agent_id: "code-prototype".to_string(),
|
||
session_id: None,
|
||
})
|
||
);
|
||
assert!(parse_cli_command(&[
|
||
"--agent-context-compact".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
])
|
||
.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn swarm_chat_defaults_to_project_supervisor() {
|
||
let project_path = std::env::current_dir().expect("current directory");
|
||
let command = parse_cli_command(&[
|
||
"--swarm-chat".to_string(),
|
||
project_path.display().to_string(),
|
||
])
|
||
.expect("parse supervisor chat")
|
||
.expect("supervisor chat command");
|
||
|
||
assert_eq!(
|
||
command,
|
||
CliCommand::SwarmChat {
|
||
project_path,
|
||
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||
initialize: false,
|
||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn swarm_chat_autonomous_game_build_flag_selects_autonomous_profile() {
|
||
let project_path = std::env::current_dir().expect("current directory");
|
||
let command = parse_cli_command(&[
|
||
"--swarm-chat".to_string(),
|
||
project_path.display().to_string(),
|
||
"--autonomous-game-build".to_string(),
|
||
])
|
||
.expect("parse autonomous supervisor chat")
|
||
.expect("autonomous supervisor chat command");
|
||
|
||
assert_eq!(
|
||
command,
|
||
CliCommand::SwarmChat {
|
||
project_path,
|
||
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||
initialize: false,
|
||
run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn swarm_chat_rejects_missing_or_extra_arguments() {
|
||
assert!(parse_cli_command(&["--swarm-chat".to_string()]).is_err());
|
||
assert!(parse_cli_command(&[
|
||
"--swarm-chat".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
"code-prototype".to_string(),
|
||
"extra".to_string(),
|
||
])
|
||
.is_err());
|
||
assert!(parse_cli_command(&[
|
||
"--swarm-chat".to_string(),
|
||
"--autonomous-game-build".to_string(),
|
||
"--autonomous-game-build".to_string(),
|
||
"/tmp/game-project".to_string(),
|
||
])
|
||
.is_err());
|
||
}
|
||
}
|