ded3253b08
按四不写原则移除 PR #159 引入、已被策划 V2 取代的整条 V1 链路 删除 runtime_protocol 下六个 V1 模块(planning_storage/submit/approval/coordinator/hydrate/provider_usage) V1 与 V2 共用的 GDD 数据模型抽到新模块 planning_gdd_model.rs 供 V2 继续复用 删除 prompt manifest 中 planning Agent 目录、role overlay、plan sections 与 supervisorPlan 组合及对应生成常量 删除四个 V1 提示词文件(roles/project-planning.md、plan/common.md、plan/supervisor-identity.md、plan/supervisor-playbook.md) 删除 game-creator.config.json 与配置代码中的 planning 能力开关 删除 CLI --swarm-chat 的 --plan 入口与 swarm_cli 中的 plan source 分支 删除 provider_retry 的 planning session binding 与 plan 专用请求指纹链路 删除 provider_action_batch 的 plan.submit_gdd v4 批次形状校验与 planning 绑定字段 删除 tool_policy_snapshot / agent_native_tools / tool_plan_protocol 中的 plan 根阶段收窄与 plan.submit_gdd 身份门 删除 main_loop 的 plan_gdd blocker 投影、plan 信封修复回路与 plan submit 业务拒绝限流 删除 pending_recovery 与 recovery_scan 的 plan submit 锚点恢复、planning session 投影恢复与审批投影恢复 删除 acceptance_graph 的 Fast GDD 取证覆盖校验与 plan 根验收前置门 删除 agent_db 的 plan.provider_usage、plan.gdd_decided、plan_submit_gdd.committed 三条专用持久车道及其预留配额 删除 AgentRuntimeState 的 plan_submit_gdd_rejection_count 字段 runtime_tools 的委派、run_status、goal_contract 恢复为通用路径(移除 plan 根对称性守卫与 acceptance gate 钩子) 同步删除只覆盖 V1 行为的测试用例(planning 澄清、锚点恢复、plan 根委派门、plan 提示词组合等)
391 lines
15 KiB
Rust
391 lines
15 KiB
Rust
use super::*;
|
||
|
||
#[derive(Debug, Eq, PartialEq)]
|
||
pub(super) enum SwarmChatInput {
|
||
Help,
|
||
Agents,
|
||
Status,
|
||
History,
|
||
Compact,
|
||
Goal(SwarmGoalCommand),
|
||
InvalidGoal(String),
|
||
Resume,
|
||
Quit,
|
||
Message(String),
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||
pub(super) enum SwarmChatFlow {
|
||
Continue,
|
||
Exit,
|
||
}
|
||
|
||
pub(super) enum SwarmInputEvent {
|
||
Line(String),
|
||
Eof,
|
||
Error(String),
|
||
}
|
||
|
||
pub(super) enum SwarmPromptDecision {
|
||
Approve,
|
||
Reject,
|
||
Deferred,
|
||
InputClosed,
|
||
Quit,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||
pub(super) enum SwarmNewRunLaunch<'a> {
|
||
ProjectSupervisor {
|
||
source: &'static str,
|
||
run_profile: &'a str,
|
||
},
|
||
ExplicitParentDebug,
|
||
}
|
||
|
||
impl SwarmNewRunLaunch<'_> {
|
||
pub(super) fn expected_parent_source(self) -> Option<&'static str> {
|
||
match self {
|
||
Self::ProjectSupervisor { .. } | Self::ExplicitParentDebug => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(super) fn resolve_swarm_new_run_launch<'a>(
|
||
parent_agent_id: &str,
|
||
run_profile: &'a str,
|
||
supervisor_source: &'static str,
|
||
) -> Result<SwarmNewRunLaunch<'a>, String> {
|
||
if !matches!(
|
||
run_profile,
|
||
AGENT_RUNTIME_RUN_PROFILE_STANDARD | AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||
) {
|
||
return Err(format!("不支持的 Agent Runtime Run Profile:{run_profile}"));
|
||
}
|
||
if parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||
if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE {
|
||
return Err(format!(
|
||
"不支持的 Project Supervisor source:{supervisor_source}"
|
||
));
|
||
}
|
||
return Ok(SwarmNewRunLaunch::ProjectSupervisor {
|
||
source: supervisor_source,
|
||
run_profile,
|
||
});
|
||
}
|
||
if supervisor_source != AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE {
|
||
return Err(format!(
|
||
"受限 Supervisor source 仅支持 project-supervisor 总控入口:{supervisor_source}"
|
||
));
|
||
}
|
||
if run_profile != AGENT_RUNTIME_RUN_PROFILE_STANDARD {
|
||
return Err("--autonomous-game-build 仅支持 project-supervisor 总控入口".to_string());
|
||
}
|
||
Ok(SwarmNewRunLaunch::ExplicitParentDebug)
|
||
}
|
||
|
||
pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||
root: &Path,
|
||
parent_agent_id: &str,
|
||
run_profile: &str,
|
||
supervisor_source: &'static str,
|
||
input: &Receiver<SwarmInputEvent>,
|
||
output: &mut W,
|
||
) -> Result<(), String> {
|
||
let parent_agent_id = parent_agent_id.trim();
|
||
if parent_agent_id.is_empty() {
|
||
return Err("parentAgentId 不能为空".to_string());
|
||
}
|
||
let new_run_launch =
|
||
resolve_swarm_new_run_launch(parent_agent_id, run_profile, supervisor_source)?;
|
||
let expected_parent_source = new_run_launch.expected_parent_source();
|
||
enforce_project_permission_policy(root, "conversation.read")?;
|
||
enforce_project_permission_policy(root, "conversation.write")?;
|
||
enforce_project_permission_policy(root, "agent.run_status")?;
|
||
enforce_project_permission_policy(root, "agent.compact")?;
|
||
enforce_project_permission_policy(root, "agent.resume")?;
|
||
let _ = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
|
||
let existing_runtimes = read_game_creator_agent_runtimes_at(root)?;
|
||
|
||
writeln!(output, "Agent Swarm Chat")
|
||
.and_then(|_| writeln!(output, "项目:{}", root.display()))
|
||
.and_then(|_| writeln!(output, "父 Agent:{parent_agent_id}"))
|
||
.and_then(|_| writeln!(output, "输入 /help 查看命令。"))
|
||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||
print_conversation_history(root, parent_agent_id, output)?;
|
||
|
||
let active_conversation =
|
||
read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
|
||
let active_session_id = active_conversation
|
||
.session_id
|
||
.as_deref()
|
||
.ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
|
||
let matching_parent_is_busy = swarm_parent_runtime(
|
||
parent_agent_id,
|
||
active_session_id,
|
||
run_profile,
|
||
expected_parent_source,
|
||
&existing_runtimes,
|
||
)
|
||
.is_some_and(runtime_is_busy);
|
||
if matching_parent_is_busy {
|
||
writeln!(
|
||
output,
|
||
"[恢复扫描] 检测到未收束 Runtime;输入 /resume 继续观察,新消息会进入该 run 的 steer 队列。"
|
||
)
|
||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||
}
|
||
|
||
loop {
|
||
write!(output, "\n你> ").map_err(|error| format!("写入终端失败:{error}"))?;
|
||
output
|
||
.flush()
|
||
.map_err(|error| format!("刷新终端失败:{error}"))?;
|
||
let Some(line) = receive_swarm_chat_line(input)? else {
|
||
writeln!(output, "\n已退出 Agent Swarm Chat。")
|
||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||
return Ok(());
|
||
};
|
||
let Some(command) = parse_swarm_chat_input(&line) else {
|
||
continue;
|
||
};
|
||
match command {
|
||
SwarmChatInput::Help => print_swarm_chat_help(output)?,
|
||
SwarmChatInput::Agents => print_swarm_agents(root, output)?,
|
||
SwarmChatInput::Status => print_swarm_status(root, output)?,
|
||
SwarmChatInput::History => print_conversation_history(root, parent_agent_id, output)?,
|
||
SwarmChatInput::Compact => {
|
||
handle_swarm_context_compaction(root, parent_agent_id, output)?
|
||
}
|
||
SwarmChatInput::Goal(command) => {
|
||
let mut observer = SwarmRuntimeObserver::seed(root)?;
|
||
let Some(observation) =
|
||
handle_swarm_goal_command(root, parent_agent_id, command, output)?
|
||
else {
|
||
continue;
|
||
};
|
||
let conversation_baseline = new_swarm_turn_conversation_baseline(
|
||
observation.previous_message_count,
|
||
&observation.run_id,
|
||
);
|
||
let outcome = wait_for_swarm_turn(
|
||
root,
|
||
parent_agent_id,
|
||
&observation.session_id,
|
||
run_profile,
|
||
expected_parent_source,
|
||
conversation_baseline,
|
||
input,
|
||
output,
|
||
&mut observer,
|
||
SWARM_CHAT_POLL_INTERVAL,
|
||
SWARM_CHAT_SETTLE_WINDOW,
|
||
)?;
|
||
if outcome == SwarmTurnOutcome::Quit {
|
||
return print_swarm_chat_exit(output);
|
||
}
|
||
print_turn_outcome(outcome, output)?;
|
||
}
|
||
SwarmChatInput::InvalidGoal(error) => print_swarm_goal_error(output, &error)?,
|
||
SwarmChatInput::Resume => {
|
||
let before =
|
||
read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
|
||
let session_id = before
|
||
.session_id
|
||
.as_deref()
|
||
.ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
|
||
if handle_swarm_resume_turn(
|
||
root,
|
||
parent_agent_id,
|
||
session_id,
|
||
run_profile,
|
||
expected_parent_source,
|
||
before.messages.len(),
|
||
input,
|
||
output,
|
||
)? == SwarmChatFlow::Exit
|
||
{
|
||
return Ok(());
|
||
}
|
||
}
|
||
SwarmChatInput::Quit => {
|
||
writeln!(
|
||
output,
|
||
"已退出 Agent Swarm Chat;后台 Runner 和已投递任务保持运行。"
|
||
)
|
||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||
return Ok(());
|
||
}
|
||
SwarmChatInput::Message(message) => {
|
||
let before =
|
||
read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
|
||
let session_id = before
|
||
.session_id
|
||
.as_deref()
|
||
.ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
|
||
if handle_swarm_user_turn(
|
||
root,
|
||
parent_agent_id,
|
||
session_id,
|
||
run_profile,
|
||
new_run_launch,
|
||
&message,
|
||
input,
|
||
output,
|
||
)? == SwarmChatFlow::Exit
|
||
{
|
||
return Ok(());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(super) fn receive_swarm_chat_line(
|
||
input: &Receiver<SwarmInputEvent>,
|
||
) -> Result<Option<String>, String> {
|
||
match input.recv() {
|
||
Ok(SwarmInputEvent::Line(line)) => Ok(Some(line)),
|
||
Ok(SwarmInputEvent::Eof) | Err(_) => Ok(None),
|
||
Ok(SwarmInputEvent::Error(error)) => Err(format!("读取终端输入失败:{error}")),
|
||
}
|
||
}
|
||
|
||
pub(super) fn prompt_swarm_decision<W: Write>(
|
||
root: &Path,
|
||
parent_agent_id: &str,
|
||
input: &Receiver<SwarmInputEvent>,
|
||
output: &mut W,
|
||
prompt: &str,
|
||
) -> Result<SwarmPromptDecision, String> {
|
||
write!(output, "{prompt}").map_err(|error| format!("写入终端失败:{error}"))?;
|
||
output
|
||
.flush()
|
||
.map_err(|error| format!("刷新终端失败:{error}"))?;
|
||
loop {
|
||
let Some(line) = receive_swarm_chat_line(input)? else {
|
||
return Ok(SwarmPromptDecision::InputClosed);
|
||
};
|
||
match line.to_ascii_lowercase().as_str() {
|
||
"approve" | "yes" | "y" | "批准" => return Ok(SwarmPromptDecision::Approve),
|
||
"reject" | "no" | "n" | "拒绝" => return Ok(SwarmPromptDecision::Reject),
|
||
"/quit" | "/exit" => return Ok(SwarmPromptDecision::Quit),
|
||
_ => {
|
||
if let Some(command) = parse_swarm_chat_input(&line) {
|
||
match command {
|
||
SwarmChatInput::Goal(command) => {
|
||
let _ =
|
||
handle_swarm_goal_command(root, parent_agent_id, command, output)?;
|
||
return Ok(SwarmPromptDecision::Deferred);
|
||
}
|
||
SwarmChatInput::InvalidGoal(error) => {
|
||
print_swarm_goal_error(output, &error)?;
|
||
}
|
||
SwarmChatInput::Compact => {
|
||
handle_swarm_context_compaction(root, parent_agent_id, output)?;
|
||
return Ok(SwarmPromptDecision::Deferred);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
write!(output, "请输入 approve 或 reject:")
|
||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||
output
|
||
.flush()
|
||
.map_err(|error| format!("刷新终端失败:{error}"))?;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(super) fn print_swarm_chat_exit<W: Write>(output: &mut W) -> Result<(), String> {
|
||
writeln!(
|
||
output,
|
||
"已退出 Agent Swarm Chat;后台 Runner 和已投递任务保持运行。"
|
||
)
|
||
.map_err(|error| format!("写入终端失败:{error}"))
|
||
}
|
||
|
||
pub(super) fn parse_swarm_chat_input(input: &str) -> Option<SwarmChatInput> {
|
||
let input = input.trim();
|
||
if input.is_empty() {
|
||
return None;
|
||
}
|
||
if let Some(rest) = input.strip_prefix("/goal") {
|
||
if rest.is_empty() {
|
||
return Some(SwarmChatInput::Goal(SwarmGoalCommand::Status));
|
||
}
|
||
if !rest.chars().next().is_some_and(char::is_whitespace) {
|
||
return Some(SwarmChatInput::InvalidGoal(
|
||
"未知 /goal 命令;输入 /help 查看支持的 Goal 命令。".to_string(),
|
||
));
|
||
}
|
||
return Some(parse_swarm_goal_command(rest.trim()));
|
||
}
|
||
Some(match input {
|
||
"/help" => SwarmChatInput::Help,
|
||
"/agents" => SwarmChatInput::Agents,
|
||
"/status" => SwarmChatInput::Status,
|
||
"/history" => SwarmChatInput::History,
|
||
"/compact" => SwarmChatInput::Compact,
|
||
"/resume" => SwarmChatInput::Resume,
|
||
"/quit" | "/exit" => SwarmChatInput::Quit,
|
||
value => SwarmChatInput::Message(value.to_string()),
|
||
})
|
||
}
|
||
|
||
pub(super) fn parse_swarm_goal_command(input: &str) -> SwarmChatInput {
|
||
if input.is_empty() || input == "status" {
|
||
return SwarmChatInput::Goal(SwarmGoalCommand::Status);
|
||
}
|
||
if input == "pause" {
|
||
return SwarmChatInput::Goal(SwarmGoalCommand::Pause);
|
||
}
|
||
if input == "resume" {
|
||
return SwarmChatInput::Goal(SwarmGoalCommand::Resume);
|
||
}
|
||
if input == "clear" {
|
||
return SwarmChatInput::Goal(SwarmGoalCommand::Clear);
|
||
}
|
||
if let Some(outcome) = input.strip_prefix("edit") {
|
||
if outcome.is_empty() {
|
||
return SwarmChatInput::InvalidGoal("用法:/goal edit <目标>".to_string());
|
||
}
|
||
if outcome.chars().next().is_some_and(char::is_whitespace) {
|
||
let outcome = outcome.trim();
|
||
return if outcome.is_empty() {
|
||
SwarmChatInput::InvalidGoal("用法:/goal edit <目标>".to_string())
|
||
} else {
|
||
SwarmChatInput::Goal(SwarmGoalCommand::Edit(outcome.to_string()))
|
||
};
|
||
}
|
||
}
|
||
for command in ["status", "pause", "resume", "clear"] {
|
||
if input
|
||
.strip_prefix(command)
|
||
.is_some_and(|rest| rest.chars().next().is_some_and(char::is_whitespace))
|
||
{
|
||
return SwarmChatInput::InvalidGoal(format!("/goal {command} 不接受额外参数"));
|
||
}
|
||
}
|
||
SwarmChatInput::Goal(SwarmGoalCommand::Start(input.to_string()))
|
||
}
|
||
|
||
pub(super) fn print_swarm_chat_help<W: Write>(output: &mut W) -> Result<(), String> {
|
||
writeln!(output, "/agents 查看静态 Agent 与动态 child")
|
||
.and_then(|_| writeln!(output, "/status 查看全部 Runtime 状态"))
|
||
.and_then(|_| writeln!(output, "/history 查看父 Agent 当前 Session 历史"))
|
||
.and_then(|_| writeln!(output, "/compact 压缩父 Agent 当前空闲 Session 历史"))
|
||
.and_then(|_| writeln!(output, "/resume 继续观察当前 Session 的未收束 Runtime"))
|
||
.and_then(|_| writeln!(output, "/goal <目标> 启动当前 Session 的持久 Goal"))
|
||
.and_then(|_| writeln!(output, "/goal 查看当前 Goal"))
|
||
.and_then(|_| writeln!(output, "/goal status 查看当前 Goal"))
|
||
.and_then(|_| writeln!(output, "/goal edit <目标> 编辑当前 Goal"))
|
||
.and_then(|_| writeln!(output, "/goal pause 暂停当前 Goal"))
|
||
.and_then(|_| writeln!(output, "/goal resume 恢复当前 Goal"))
|
||
.and_then(|_| writeln!(output, "/goal clear 清理当前 Goal"))
|
||
.and_then(|_| writeln!(output, "/help 查看命令"))
|
||
.and_then(|_| writeln!(output, "/quit 退出终端观察客户端"))
|
||
.map_err(|error| format!("写入终端失败:{error}"))
|
||
}
|