退役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 用例,移除斜杠命令断言
This commit is contained in:
2026-09-23 11:12:39 +08:00
parent 530538842e
commit 6106b4e67c
53 changed files with 40 additions and 17025 deletions
@@ -71,9 +71,8 @@ pub(crate) use canvas_generation::{
normalize_platform_art_reference_asset_ids, normalize_platform_art_target_category,
platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path,
project_canvas_asset_media_types, role_has_canvas_assets, suggested_canvas_tool_call,
validate_platform_art_icon_prompt, PlatformArtAssetGenerationOptions,
PLATFORM_ART_ASSET_GENERATION_KINDS,
project_canvas_asset_media_types, role_has_canvas_assets, validate_platform_art_icon_prompt,
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
};
#[allow(unused_imports)]
pub(crate) use draft_validation::{
@@ -315,33 +315,6 @@ pub(crate) fn project_canvas_asset_media_types(root: &Path) -> Vec<String> {
.unwrap_or_default()
}
pub(crate) fn suggested_canvas_tool_call(
role_brief: &AgentRoleBrief,
input_paths: &[String],
canvas_asset_media_types: &[String],
) -> Option<GameCreationAgentToolCallTrace> {
if role_brief.status != "completed"
|| role_has_canvas_assets(role_brief, canvas_asset_media_types)
{
return None;
}
let tool_id = match (
role_brief.group_definition.id,
role_brief.role_definition.id,
) {
("art", "asset") | ("audio", "sfx") => "agent.tool.suggest.canvas.project_sync",
_ => return None,
};
Some(GameCreationAgentToolCallTrace {
tool_id: tool_id.to_string(),
status: "suggested".to_string(),
input_paths: input_paths.to_vec(),
output_paths: Vec::new(),
summary: "项目还没有对应类型的画板回流素材;建议用户确认 /sync-canvas-project <画板项目ID> 后同步画板资源到本地 assets/。"
.to_string(),
})
}
pub(crate) async fn maybe_generate_platform_art_asset_step(
root: &Path,
prompt: &str,
@@ -204,7 +204,7 @@ pub(crate) async fn run_game_creator_agent_loop_at(
let platform_art_step =
maybe_generate_platform_art_asset_step(root, prompt, &group_briefs, pass, progress)
.await;
append_group_brief_steps(root, pass, &agenda.relative_path, &group_briefs, &mut steps);
append_group_brief_steps(pass, &agenda.relative_path, &group_briefs, &mut steps);
if let Some(step) = platform_art_step {
steps.push(step);
}
@@ -395,13 +395,11 @@ pub(crate) fn append_agent_success_memories(
}
pub(crate) fn append_group_brief_steps(
root: &Path,
pass: u8,
agenda_relative_path: &str,
briefs: &[AgentGroupBrief],
steps: &mut Vec<GameCreationAgentRunStep>,
) {
let canvas_asset_media_types = project_canvas_asset_media_types(root);
for brief in briefs {
for role_brief in &brief.role_briefs {
let input_paths = vec![
@@ -421,7 +419,7 @@ pub(crate) fn append_group_brief_steps(
output_paths.push(role_brief.memory_relative_path.clone());
output_paths.push(PROJECT_BLACKBOARD_MEMORY_PATH.to_string());
}
let mut step = with_task_context(
let step = with_task_context(
agent_trace_step_owned(
pass,
&format!(
@@ -439,11 +437,6 @@ pub(crate) fn append_group_brief_steps(
Some(role_brief.role_definition.task_id),
"role-brief",
);
if let Some(tool_call) =
suggested_canvas_tool_call(role_brief, &input_paths, &canvas_asset_media_types)
{
step.tool_calls.push(tool_call);
}
steps.push(step);
}
let role_paths = brief
File diff suppressed because it is too large Load Diff
@@ -85,7 +85,7 @@ pub(crate) use run_configuration::{
pub(crate) use steering::{
acquire_game_creator_agent_runtime_steer_project_write_lock_with_wait,
append_game_creator_agent_runtime_steer_decision_failure_reply_at,
consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_accepts_steer,
consume_game_creator_agent_runtime_steers,
game_creator_agent_runtime_provider_request_count_for_roots,
game_creator_agent_runtime_steer_ledger_path,
interrupt_game_creator_agent_runtime_provider_for_decided_steer_at,
+1 -166
View File
@@ -24,13 +24,6 @@ pub(crate) enum CliCommand {
task: String,
initialize: bool,
},
SwarmChat {
project_path: PathBuf,
parent_agent_id: String,
initialize: bool,
run_profile: String,
supervisor_source: &'static str,
},
AgentEnqueue {
project_path: PathBuf,
agent_id: String,
@@ -136,7 +129,6 @@ impl CliCommand {
matches!(
self,
Self::AgentTask { .. }
| Self::SwarmChat { .. }
| Self::AgentEnqueue { .. }
| Self::AgentContextCompact { .. }
| Self::AgentConfirm { .. }
@@ -175,11 +167,6 @@ impl CliCommand {
initialize,
..
}
| Self::SwarmChat {
project_path,
initialize,
..
}
| Self::AgentEnqueue {
project_path,
initialize,
@@ -725,49 +712,6 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
prompt: prompt.trim().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()
},
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}));
}
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") {
@@ -1057,7 +1001,7 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
} 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())
Err("单 Agent 任务正在等待用户回答,请在开发窗口继续".to_string())
} else {
Err(format!(
"单 Agent 任务未完成:{} / {}",
@@ -1065,23 +1009,6 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
))
}
}
CliCommand::SwarmChat {
project_path,
parent_agent_id,
initialize,
run_profile,
supervisor_source,
} => {
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,
supervisor_source,
)
}
CliCommand::AgentEnqueue {
project_path,
agent_id,
@@ -1954,34 +1881,6 @@ mod tests {
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(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}
);
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()])
@@ -2035,68 +1934,4 @@ mod tests {
])
.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(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}
);
}
#[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(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}
);
}
#[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());
}
}
@@ -1345,6 +1345,9 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
}
#[tauri::command]
#[allow(dead_code)]
// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。
// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。
pub(crate) fn start_game_creator_agent_runtime_task(
project_path: String,
agent_id: String,
@@ -1547,6 +1550,9 @@ pub(crate) fn clear_game_creator_agent_goal(
}
#[tauri::command]
#[allow(dead_code)]
// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。
// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。
pub(crate) async fn steer_game_creator_agent_runtime_task(
project_path: String,
agent_id: String,
@@ -5301,16 +5307,6 @@ pub(crate) fn open_canvas_project(
Ok(OpenCanvasProjectResult { url })
}
#[tauri::command]
pub(crate) fn get_game_creation_agent_capabilities() -> Vec<GameCreationAgentCapabilityDescriptor> {
GAME_CREATION_AGENT_CAPABILITIES.to_vec()
}
#[tauri::command]
pub(crate) fn get_limited_local_commands() -> Vec<GameCreationAppLimitedRunCommandDescriptor> {
GAME_CREATION_APP_LIMITED_RUN_COMMANDS.to_vec()
}
#[tauri::command]
pub(crate) fn run_limited_local_command(
project_path: String,
@@ -36,20 +36,18 @@ use shared_contracts::game_creation_app::{
game_creation_app_asset_effective_category, new_game_creation_app_manifest,
new_game_creation_app_seed_tasks, normalize_game_creation_app_asset_tags,
validate_game_iteration_versions, GameCreationAgentArtifactTrace,
GameCreationAgentCapabilityDescriptor, GameCreationAgentPassPlanTrace,
GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
GameCreationAgentPassPlanTrace, GameCreationAgentRepairRouteTrace, GameCreationAgentRunStep,
GameCreationAgentRunTaskGraphTrace, GameCreationAgentRunTrace, GameCreationAgentToolCallTrace,
GameCreationAppAgentGroup, GameCreationAppAssetKind, GameCreationAppAssetManifestEntry,
GameCreationAppAssetSource, GameCreationAppAssetSourceKind, GameCreationAppCommandRunState,
GameCreationAppCommandRunStatus, GameCreationAppLimitedRunCommandDescriptor,
GameCreationAppManifest, GameCreationAppPermission, GameCreationAppPreviewState,
GameCreationAppPreviewStatus, GameCreationAppTaskState, GameCreationAppTaskStatus,
GameIterationVersion, GameIterationVersionCreatedReason, GameIterationVersionResourceBinding,
ProjectResourceCanvasLayout, ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition,
GameCreationAppCommandRunStatus, GameCreationAppManifest, GameCreationAppPermission,
GameCreationAppPreviewState, GameCreationAppPreviewStatus, GameCreationAppTaskState,
GameCreationAppTaskStatus, GameIterationVersion, GameIterationVersionCreatedReason,
GameIterationVersionResourceBinding, ProjectResourceCanvasLayout,
ProjectResourceCanvasLayoutMode, ProjectResourceCanvasPosition,
UpdateProjectResourceCanvasLayoutResult, UpdateProjectResourceCanvasLayoutStatus,
GAME_CREATION_AGENT_CAPABILITIES, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
GAME_CREATION_AGENT_TOOL_CALL_MAX, GAME_CREATION_APP_COMMANDS,
GAME_CREATION_APP_LIMITED_RUN_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION,
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GAME_CREATION_AGENT_TOOL_CALL_MAX,
GAME_CREATION_APP_COMMANDS, GAME_CREATION_RESOURCE_LAYOUT_SCHEMA_VERSION,
};
// `Emitter` 同时被 `use super::*` 的子模块依赖(通知、Agent 事件等都从 crate 根取该 trait),
// 不要因为根模块自身不再直接 `.emit(..)` 就删掉它。
@@ -151,7 +149,6 @@ mod repository_context;
mod resource_inspect;
mod resource_preview_scheduler;
mod runner;
mod swarm_cli;
mod template_library;
mod tool_plan_handoff;
mod user_input;
@@ -193,7 +190,6 @@ use repository_context::*;
use resource_inspect::*;
use resource_preview_scheduler::*;
use runner::*;
use swarm_cli::*;
use template_library::*;
use user_input::*;
use windows::*;
@@ -2682,8 +2678,6 @@ fn main() {
start_local_project_asset_generation,
list_local_project_asset_generations,
open_canvas_project,
get_game_creation_agent_capabilities,
get_limited_local_commands,
run_limited_local_command,
append_local_permission_log,
list_local_project_files,
@@ -1,72 +0,0 @@
use super::*;
use std::collections::{BTreeMap, BTreeSet};
use std::io::{BufRead, Write};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::time::{Duration, Instant};
mod commands;
mod conversation;
mod goal_commands;
mod input;
mod observer;
mod report;
mod terminal_classification;
mod turn_dispatch;
mod turn_wait;
use commands::*;
use conversation::*;
use goal_commands::*;
use input::*;
use observer::*;
use report::*;
use terminal_classification::*;
use turn_dispatch::*;
use turn_wait::*;
#[cfg(test)]
mod tests;
pub(crate) fn run_game_creator_swarm_chat_at(
root: &Path,
parent_agent_id: &str,
run_profile: &str,
supervisor_source: &'static str,
) -> Result<(), String> {
let (input_tx, input_rx) = mpsc::channel();
std::thread::spawn(move || {
let stdin = std::io::stdin();
let mut input = stdin.lock();
loop {
let mut line = String::new();
match input.read_line(&mut line) {
Ok(0) => {
let _ = input_tx.send(SwarmInputEvent::Eof);
break;
}
Ok(_) => {
if input_tx
.send(SwarmInputEvent::Line(line.trim().to_string()))
.is_err()
{
break;
}
}
Err(error) => {
let _ = input_tx.send(SwarmInputEvent::Error(error.to_string()));
break;
}
}
}
});
let stdout = std::io::stdout();
let mut output = stdout.lock();
run_game_creator_swarm_chat_with_input(
root,
parent_agent_id,
run_profile,
supervisor_source,
&input_rx,
&mut output,
)
}
@@ -1,79 +0,0 @@
use super::*;
pub(super) fn print_swarm_agents<W: Write>(root: &Path, output: &mut W) -> Result<(), String> {
writeln!(output, "静态 Agent").map_err(|error| format!("写入终端失败:{error}"))?;
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
for role in group.roles {
writeln!(
output,
"- {} / {}: {} ({})",
group.label, role.role, role.task_id, role.id
)
.map_err(|error| format!("写入终端失败:{error}"))?;
}
}
let dynamic = read_game_creator_agent_runtimes_at(root)?
.into_iter()
.filter(|runtime| runtime.state.agent_id.starts_with("child-"))
.collect::<Vec<_>>();
if !dynamic.is_empty() {
writeln!(output, "动态隔离 Agent").map_err(|error| format!("写入终端失败:{error}"))?;
for runtime in dynamic {
writeln!(
output,
"- {} <- {} run={} delegation={} status={}/{}",
runtime.state.agent_id,
runtime
.state
.parent_agent_id
.as_deref()
.unwrap_or("unknown"),
runtime.state.run_id,
runtime.state.delegation_id.as_deref().unwrap_or("unknown"),
runtime.state.status,
runtime.state.phase
)
.map_err(|error| format!("写入终端失败:{error}"))?;
}
}
Ok(())
}
pub(super) fn print_swarm_status<W: Write>(root: &Path, output: &mut W) -> Result<(), String> {
let runtimes = read_game_creator_agent_runtimes_at(root)?;
for runtime in runtimes.iter().filter(|runtime| {
!runtime.state.run_id.is_empty()
|| runtime.task_queue.pending > 0
|| runtime.task_queue.running > 0
|| runtime.task_queue.waiting_for_confirmation > 0
|| runtime.task_queue.waiting_for_user_input > 0
}) {
print_runtime_state(&runtime.state, &runtime.task_queue, output)?;
print_runtime_response_stream_status(runtime.response_stream.as_ref(), output)?;
}
if runtimes
.iter()
.all(|runtime| runtime.state.run_id.is_empty())
{
writeln!(output, "当前没有 Agent Runtime 记录。")
.map_err(|error| format!("写入终端失败:{error}"))?;
}
Ok(())
}
pub(super) fn print_runtime_response_stream_status<W: Write>(
stream: Option<&AgentRuntimeResponseStream>,
output: &mut W,
) -> Result<(), String> {
let Some(stream) = stream else {
return Ok(());
};
writeln!(
output,
"[回复流] status={} sequence={} chars={}",
stream.status,
stream.sequence,
stream.accumulated_text.chars().count()
)
.map_err(|error| format!("写入终端失败:{error}"))
}
@@ -1,337 +0,0 @@
use super::*;
pub(super) const SWARM_CHAT_HISTORY_LIMIT: usize = 50;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(super) struct SwarmTurnConversationMetrics {
pub(super) new_assistant_message_count: usize,
pub(super) final_reply_chars: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct SwarmRecoveredAssistant {
pub(super) run_id: String,
pub(super) finalization_id: String,
pub(super) message_id: String,
pub(super) content: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct SwarmTurnConversationBaseline {
pub(super) previous_message_count: usize,
pub(super) parent_run_id: String,
pub(super) recovered_assistant: Option<SwarmRecoveredAssistant>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct SwarmTurnConversationSnapshot {
pub(super) metrics: SwarmTurnConversationMetrics,
pub(super) final_reply: Option<String>,
pub(super) recovered_before_observation: bool,
}
pub(super) fn handle_swarm_context_compaction<W: Write>(
root: &Path,
parent_agent_id: &str,
output: &mut W,
) -> Result<(), String> {
let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
match compact_external_agent_runner_context(
root,
parent_agent_id,
conversation.session_id.as_deref(),
) {
Ok(result) => writeln!(
output,
"[上下文压缩] revision={} reused={} estimated={}->{} covered={}/{}/{}",
result.revision,
result.reused,
result.estimated_tokens_before,
result.estimated_tokens_after,
result.covered_agent_messages,
result.covered_project_messages,
result.covered_observations,
),
Err(error) => writeln!(output, "[上下文压缩失败] {error}"),
}
.map_err(|error| format!("写入终端失败:{error}"))
}
pub(super) fn print_conversation_history<W: Write>(
root: &Path,
parent_agent_id: &str,
output: &mut W,
) -> Result<(), String> {
let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
writeln!(
output,
"[历史] session={} messages={}",
conversation.session_id.as_deref().unwrap_or("unknown"),
conversation.messages.len()
)
.map_err(|error| format!("写入终端失败:{error}"))?;
let start = conversation
.messages
.len()
.saturating_sub(SWARM_CHAT_HISTORY_LIMIT);
for message in &conversation.messages[start..] {
let label = if message.role == "assistant" {
"Agent"
} else if message.role == "user" {
""
} else {
message.role.as_str()
};
writeln!(output, "{label}> {}", message.content)
.map_err(|error| format!("写入终端失败:{error}"))?;
}
Ok(())
}
pub(super) fn new_swarm_turn_conversation_baseline(
previous_message_count: usize,
parent_run_id: impl Into<String>,
) -> SwarmTurnConversationBaseline {
SwarmTurnConversationBaseline {
previous_message_count,
parent_run_id: parent_run_id.into(),
recovered_assistant: None,
}
}
pub(super) fn capture_recovered_swarm_assistant_at(
root: &Path,
parent_agent_id: &str,
session_id: &str,
baseline: &mut SwarmTurnConversationBaseline,
) -> Result<(), String> {
if baseline.parent_run_id.trim().is_empty() || baseline.recovered_assistant.is_some() {
return Ok(());
}
let conversation =
read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?;
if baseline.previous_message_count > conversation.messages.len() {
return Err("Swarm turn 对话 baseline 超出当前 Session 消息数".to_string());
}
if baseline.previous_message_count < conversation.messages.len() {
return Ok(());
}
let Some(journal) = read_game_creator_agent_runtime_finalization_journal(
root,
parent_agent_id,
&baseline.parent_run_id,
)?
else {
return Ok(());
};
if journal.agent_id != parent_agent_id
|| journal.session_id != session_id
|| journal.run_id != baseline.parent_run_id
{
return Err("Swarm 恢复 finalization 与目标 parent Session/run 不匹配".to_string());
}
if !game_creator_agent_runtime_finalization_assistant_exists(root, &journal)? {
return Ok(());
}
baseline.recovered_assistant = Some(SwarmRecoveredAssistant {
run_id: journal.run_id,
finalization_id: journal.finalization_id,
message_id: journal.message_id,
content: journal.response,
});
Ok(())
}
pub(super) fn read_turn_conversation_snapshot(
root: &Path,
parent_agent_id: &str,
session_id: &str,
baseline: &SwarmTurnConversationBaseline,
) -> Result<SwarmTurnConversationSnapshot, String> {
let conversation =
read_local_conversation_for_session_at(root, Some(parent_agent_id), Some(session_id))?;
if baseline.previous_message_count > conversation.messages.len() {
return Err("Swarm turn 对话 baseline 超出当前 Session 消息数".to_string());
}
let target_message_id = game_creator_agent_runtime_finalization_message_id(
parent_agent_id,
session_id,
&baseline.parent_run_id,
);
if let Some(journal) = read_game_creator_agent_runtime_finalization_journal(
root,
parent_agent_id,
&baseline.parent_run_id,
)? {
if journal.agent_id != parent_agent_id
|| journal.session_id != session_id
|| journal.run_id != baseline.parent_run_id
|| journal.message_id != target_message_id
{
return Err("Swarm finalization 与目标 parent Session/run 不匹配".to_string());
}
}
let (mut metrics, final_reply) = summarize_scoped_new_assistant_messages(
conversation
.messages
.iter()
.skip(baseline.previous_message_count)
.map(|message| {
(
message.role.as_str(),
message.content.as_str(),
message.message_id.as_deref(),
)
}),
Some(&target_message_id),
);
let mut final_reply = final_reply.map(str::to_string);
let recovered_before_observation = baseline.recovered_assistant.is_some();
if let Some(recovered) = baseline.recovered_assistant.as_ref() {
if recovered.run_id != baseline.parent_run_id
|| recovered.finalization_id.trim().is_empty()
|| recovered.message_id.trim().is_empty()
{
return Err("Swarm 恢复 assistant 身份不完整".to_string());
}
if metrics.new_assistant_message_count != 0 {
return Err("Swarm 恢复 assistant 与 baseline 后的新回复重叠".to_string());
}
metrics.new_assistant_message_count = metrics.new_assistant_message_count.saturating_add(1);
if final_reply.is_none() {
metrics.final_reply_chars = recovered.content.chars().count();
final_reply = Some(recovered.content.clone());
}
}
Ok(SwarmTurnConversationSnapshot {
metrics,
final_reply,
recovered_before_observation,
})
}
pub(super) fn read_turn_conversation_metrics(
root: &Path,
parent_agent_id: &str,
session_id: &str,
baseline: &SwarmTurnConversationBaseline,
) -> Result<SwarmTurnConversationMetrics, String> {
read_turn_conversation_snapshot(root, parent_agent_id, session_id, baseline)
.map(|snapshot| snapshot.metrics)
}
pub(super) fn summarize_new_assistant_messages<'a>(
messages: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> (SwarmTurnConversationMetrics, Option<&'a str>) {
summarize_scoped_new_assistant_messages(
messages
.into_iter()
.map(|(role, content)| (role, content, None)),
None,
)
}
pub(super) fn summarize_scoped_new_assistant_messages<'a>(
messages: impl IntoIterator<Item = (&'a str, &'a str, Option<&'a str>)>,
target_message_id: Option<&str>,
) -> (SwarmTurnConversationMetrics, Option<&'a str>) {
let mut scoped_count = 0;
let mut scoped_reply = None;
let mut legacy_count = 0;
let mut legacy_reply = None;
let mut identified_assistant_exists = false;
for (role, content, message_id) in messages {
if role != "assistant" {
continue;
}
match message_id {
Some(message_id) if target_message_id == Some(message_id) => {
identified_assistant_exists = true;
scoped_count += 1;
scoped_reply = Some(content);
}
None => {
legacy_count += 1;
legacy_reply = Some(content);
}
Some(_) => identified_assistant_exists = true,
}
}
let (new_assistant_message_count, final_reply) =
if target_message_id.is_some() && (scoped_count > 0 || identified_assistant_exists) {
(scoped_count, scoped_reply)
} else {
(legacy_count, legacy_reply)
};
(
SwarmTurnConversationMetrics {
new_assistant_message_count,
final_reply_chars: final_reply.map_or(0, |reply| reply.chars().count()),
},
final_reply,
)
}
pub(super) fn print_new_parent_reply<W: Write>(
root: &Path,
parent_agent_id: &str,
session_id: &str,
baseline: &SwarmTurnConversationBaseline,
output: &mut W,
observer: &mut SwarmRuntimeObserver,
) -> Result<SwarmTurnConversationMetrics, String> {
let snapshot = read_turn_conversation_snapshot(root, parent_agent_id, session_id, baseline)?;
observer.close_response_line(output)?;
if snapshot.recovered_before_observation {
writeln!(output, "[本轮结束] 父 Agent 回复已在恢复前持久化。")
.map_err(|error| format!("写入终端失败:{error}"))?;
} else {
print_settled_parent_reply_for_run(
parent_agent_id,
session_id,
Some(&baseline.parent_run_id),
snapshot.final_reply.as_deref(),
observer,
output,
)?;
}
Ok(snapshot.metrics)
}
pub(super) fn print_settled_parent_reply<W: Write>(
parent_agent_id: &str,
session_id: &str,
reply: Option<&str>,
observer: &SwarmRuntimeObserver,
output: &mut W,
) -> Result<(), String> {
print_settled_parent_reply_for_run(parent_agent_id, session_id, None, reply, observer, output)
}
pub(super) fn print_settled_parent_reply_for_run<W: Write>(
parent_agent_id: &str,
session_id: &str,
target_run_id: Option<&str>,
reply: Option<&str>,
observer: &SwarmRuntimeObserver,
output: &mut W,
) -> Result<(), String> {
let Some(reply) = reply else {
return writeln!(output, "[本轮结束] 父 Agent 未产生新的最终回复。")
.map_err(|error| format!("写入终端失败:{error}"));
};
let stream_belongs_to_target = target_run_id.is_none_or(|target_run_id| {
observer
.response_streams
.get(parent_agent_id)
.is_some_and(|cursor| cursor.identity.run_id == target_run_id)
});
if stream_belongs_to_target
&& observer.parent_reply_was_fully_streamed(parent_agent_id, session_id, reply)
{
writeln!(output, "[本轮结束] 父 Agent 回复已完整流式输出。")
.map_err(|error| format!("写入终端失败:{error}"))
} else {
writeln!(output, "\nAgent> {reply}").map_err(|error| format!("写入终端失败:{error}"))
}
}
@@ -1,189 +0,0 @@
use super::*;
#[derive(Debug, Eq, PartialEq)]
pub(super) enum SwarmGoalCommand {
Status,
Start(String),
Edit(String),
Pause,
Resume,
Clear,
}
#[derive(Debug, Eq, PartialEq)]
pub(super) struct SwarmGoalObservation {
pub(super) session_id: String,
pub(super) run_id: String,
pub(super) previous_message_count: usize,
}
pub(super) fn handle_swarm_goal_command<W: Write>(
root: &Path,
parent_agent_id: &str,
command: SwarmGoalCommand,
output: &mut W,
) -> Result<Option<SwarmGoalObservation>, String> {
match execute_swarm_goal_command(root, parent_agent_id, command, output) {
Ok(observation) => Ok(observation),
Err(error) => {
print_swarm_goal_error(output, &error)?;
Ok(None)
}
}
}
pub(super) fn execute_swarm_goal_command<W: Write>(
root: &Path,
parent_agent_id: &str,
command: SwarmGoalCommand,
output: &mut W,
) -> Result<Option<SwarmGoalObservation>, String> {
let conversation = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
let session_id = conversation
.session_id
.clone()
.ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
let previous_message_count = conversation.messages.len();
let current_goal = read_game_creator_agent_goal_at(root, parent_agent_id, &session_id)?;
let project_path = root.display().to_string();
match command {
SwarmGoalCommand::Status => {
print_swarm_goal_status(&session_id, current_goal.as_ref(), output)?;
Ok(None)
}
SwarmGoalCommand::Start(outcome) => {
let requested_run_id = format!("swarm-goal-{parent_agent_id}-{}", unix_millis());
let result = start_game_creator_agent_goal(
project_path,
parent_agent_id.to_string(),
Some(session_id.clone()),
outcome.clone(),
Vec::new(),
vec![outcome],
requested_run_id,
)?;
print_swarm_goal_mutation("已启动", &result, output)?;
Ok(Some(SwarmGoalObservation {
session_id,
run_id: result.goal.run_id.clone(),
previous_message_count,
}))
}
SwarmGoalCommand::Edit(outcome) => {
let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
let result = edit_game_creator_agent_goal(
project_path,
parent_agent_id.to_string(),
session_id.clone(),
goal.goal_id,
goal.revision,
outcome.clone(),
Vec::new(),
vec![outcome],
)?;
print_swarm_goal_mutation("已编辑", &result, output)?;
Ok(
(result.goal.status == AGENT_GOAL_STATUS_ACTIVE).then_some(SwarmGoalObservation {
session_id,
run_id: result.goal.run_id.clone(),
previous_message_count,
}),
)
}
SwarmGoalCommand::Pause => {
let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
let result = pause_game_creator_agent_goal(
project_path,
parent_agent_id.to_string(),
session_id,
goal.goal_id,
goal.revision,
)?;
print_swarm_goal_mutation("已暂停", &result, output)?;
Ok(None)
}
SwarmGoalCommand::Resume => {
let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
let result = resume_game_creator_agent_goal(
project_path,
parent_agent_id.to_string(),
session_id.clone(),
goal.goal_id,
goal.revision,
)?;
print_swarm_goal_mutation("已恢复", &result, output)?;
Ok(Some(SwarmGoalObservation {
session_id,
run_id: result.goal.run_id.clone(),
previous_message_count,
}))
}
SwarmGoalCommand::Clear => {
let goal = current_goal.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
let result = clear_game_creator_agent_goal(
project_path,
parent_agent_id.to_string(),
session_id,
goal.goal_id,
goal.revision,
)?;
print_swarm_goal_mutation("已清理", &result, output)?;
Ok(None)
}
}
}
pub(super) fn print_swarm_goal_status<W: Write>(
session_id: &str,
goal: Option<&AgentGoalRecord>,
output: &mut W,
) -> Result<(), String> {
let Some(goal) = goal else {
return writeln!(output, "[Goal] session={session_id} 当前尚未设置持久目标。")
.map_err(|error| format!("写入终端失败:{error}"));
};
writeln!(
output,
"[Goal] session={} goal={} run={} revision={} status={}",
goal.session_id, goal.goal_id, goal.run_id, goal.revision, goal.status
)
.and_then(|_| writeln!(output, "[Goal 目标] {}", goal.outcome))
.map_err(|error| format!("写入终端失败:{error}"))?;
for constraint in &goal.constraints {
writeln!(output, "[Goal 约束] {constraint}")
.map_err(|error| format!("写入终端失败:{error}"))?;
}
for verification in &goal.verification {
writeln!(output, "[Goal 完成标准] {verification}")
.map_err(|error| format!("写入终端失败:{error}"))?;
}
if let Some(error) = goal.error.as_deref() {
writeln!(output, "[Goal 错误] {error}")
.map_err(|write_error| format!("写入终端失败:{write_error}"))?;
}
Ok(())
}
pub(super) fn print_swarm_goal_mutation<W: Write>(
action: &str,
result: &AgentGoalMutationResult,
output: &mut W,
) -> Result<(), String> {
writeln!(
output,
"[Goal {action}] goal={} run={} revision={} status={} providerInterrupted={}",
result.goal.goal_id,
result.goal.run_id,
result.goal.revision,
result.goal.status,
result.provider_interrupted
)
.map_err(|error| format!("写入终端失败:{error}"))?;
print_swarm_goal_status(&result.goal.session_id, Some(&result.goal), output)
}
pub(super) fn print_swarm_goal_error<W: Write>(output: &mut W, error: &str) -> Result<(), String> {
writeln!(output, "[Goal 失败] {error}")
.map_err(|write_error| format!("写入终端失败:{write_error}"))
}
@@ -1,390 +0,0 @@
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}"))
}
File diff suppressed because it is too large Load Diff
@@ -1,285 +0,0 @@
use super::*;
pub(super) const SWARM_TURN_REPORT_PREFIX: &str = "[turn.report] ";
pub(super) const SWARM_TURN_REPORT_SCHEMA_VERSION: &str = "game-creator-swarm-turn-report.v1";
pub(super) const SWARM_TURN_FAILED_ERROR: &str = "swarm-turn-failed";
pub(super) const SWARM_TURN_INCOMPLETE_ERROR: &str = "swarm-turn-incomplete";
pub(super) const SWARM_TURN_RECONCILIATION_ERROR: &str = "swarm-turn-needs-reconciliation";
#[derive(Debug, Eq, PartialEq)]
pub(super) enum SwarmTurnOutcome {
Settled(SwarmTurnReport),
Failed {
agent_ids: Vec<String>,
report: SwarmTurnReport,
},
Incomplete {
reasons: Vec<String>,
report: SwarmTurnReport,
},
NeedsReconciliation {
agent_ids: Vec<String>,
report: SwarmTurnReport,
},
Quit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) enum SwarmTurnReportOutcome {
Settled,
Failed,
Incomplete,
NeedsReconciliation,
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct SwarmTurnReport {
pub(super) schema_version: &'static str,
pub(super) outcome: SwarmTurnReportOutcome,
pub(super) parent_agent_id: String,
pub(super) session_id: String,
pub(super) parent_run_id: Option<String>,
pub(super) runtime_count: usize,
pub(super) busy_runtime_count: usize,
pub(super) pending_task_count: u64,
pub(super) running_task_count: u64,
pub(super) waiting_for_confirmation_count: u64,
pub(super) waiting_for_user_input_count: u64,
pub(super) new_assistant_message_count: usize,
pub(super) final_reply_chars: usize,
pub(super) reconciliation_agent_count: usize,
}
#[derive(Default)]
struct SwarmTurnRuntimeMetrics {
runtime_count: usize,
busy_runtime_count: usize,
pending_task_count: u64,
running_task_count: u64,
waiting_for_confirmation_count: u64,
waiting_for_user_input_count: u64,
}
struct SwarmTurnRuntimeSnapshot {
agent_id: String,
run_id: String,
status: String,
phase: String,
updated_at: u64,
}
fn runtime_state_belongs_to_turn(
state: &AgentRuntimeState,
parent_agent_id: &str,
session_id: &str,
parent_run_id: &str,
) -> bool {
(state.agent_id == parent_agent_id
&& state.session_id == session_id
&& state.run_id == parent_run_id)
|| (state.parent_agent_id.as_deref() == Some(parent_agent_id)
&& state.parent_run_id.as_deref() == Some(parent_run_id))
}
fn runtime_task_belongs_to_turn(
task: &AgentRuntimeTaskRecord,
parent_agent_id: &str,
session_id: &str,
parent_run_id: &str,
) -> bool {
(task.agent_id == parent_agent_id
&& task.session_id == session_id
&& task.run_id == parent_run_id)
|| (task.parent_agent_id.as_deref() == Some(parent_agent_id)
&& task.parent_run_id.as_deref() == Some(parent_run_id))
}
fn upsert_turn_runtime_snapshot(
snapshots: &mut Vec<SwarmTurnRuntimeSnapshot>,
candidate: SwarmTurnRuntimeSnapshot,
) {
if candidate.run_id.trim().is_empty() {
return;
}
if let Some(existing) = snapshots.iter_mut().find(|snapshot| {
snapshot.agent_id == candidate.agent_id && snapshot.run_id == candidate.run_id
}) {
if candidate.updated_at >= existing.updated_at {
*existing = candidate;
}
} else {
snapshots.push(candidate);
}
}
fn scoped_swarm_turn_runtime_metrics(
parent_agent_id: &str,
session_id: &str,
parent_run_id: Option<&str>,
runtimes: &[AgentRuntimeResult],
) -> Result<SwarmTurnRuntimeMetrics, String> {
let Some(parent_run_id) = parent_run_id else {
return Ok(SwarmTurnRuntimeMetrics::default());
};
let mut snapshots = Vec::new();
for runtime in runtimes {
let journal_tasks = if runtime.task_path.trim().is_empty() {
None
} else {
Some(read_all_game_creator_agent_runtime_tasks(Path::new(
&runtime.task_path,
))?)
};
let tasks = journal_tasks.as_deref().unwrap_or(&runtime.recent_tasks);
for task in tasks {
if runtime_task_belongs_to_turn(task, parent_agent_id, session_id, parent_run_id) {
upsert_turn_runtime_snapshot(
&mut snapshots,
SwarmTurnRuntimeSnapshot {
agent_id: task.agent_id.clone(),
run_id: task.run_id.clone(),
status: task.status.clone(),
phase: task.phase.clone(),
updated_at: task.updated_at,
},
);
}
}
if runtime_state_belongs_to_turn(&runtime.state, parent_agent_id, session_id, parent_run_id)
{
upsert_turn_runtime_snapshot(
&mut snapshots,
SwarmTurnRuntimeSnapshot {
agent_id: runtime.state.agent_id.clone(),
run_id: runtime.state.run_id.clone(),
status: runtime.state.status.clone(),
phase: runtime.state.phase.clone(),
updated_at: runtime.state.updated_at,
},
);
}
}
let mut metrics = SwarmTurnRuntimeMetrics {
runtime_count: snapshots.len(),
..SwarmTurnRuntimeMetrics::default()
};
for snapshot in snapshots {
if matches!(
snapshot.status.as_str(),
"pending"
| "running"
| "waiting-for-confirmation"
| "waiting-for-user-input"
| "cancelling"
) || snapshot.phase == "needs-reconciliation"
{
metrics.busy_runtime_count += 1;
}
match snapshot.status.as_str() {
"pending" => metrics.pending_task_count += 1,
"running" => metrics.running_task_count += 1,
"waiting-for-confirmation" => metrics.waiting_for_confirmation_count += 1,
"waiting-for-user-input" => metrics.waiting_for_user_input_count += 1,
_ => {}
}
}
Ok(metrics)
}
pub(super) fn build_swarm_turn_report(
outcome: SwarmTurnReportOutcome,
parent_agent_id: &str,
session_id: &str,
expected_parent_run_id: Option<&str>,
runtimes: &[AgentRuntimeResult],
conversation_metrics: SwarmTurnConversationMetrics,
reconciliation_agent_count: usize,
) -> Result<SwarmTurnReport, String> {
let parent_run_id = expected_parent_run_id
.map(str::trim)
.filter(|run_id| !run_id.is_empty())
.map(str::to_string)
.or_else(|| {
runtimes
.iter()
.find(|runtime| {
runtime.state.agent_id == parent_agent_id
&& runtime.state.session_id == session_id
})
.map(|runtime| runtime.state.run_id.trim())
.filter(|run_id| !run_id.is_empty())
.map(str::to_string)
});
let runtime_metrics = scoped_swarm_turn_runtime_metrics(
parent_agent_id,
session_id,
parent_run_id.as_deref(),
runtimes,
)?;
Ok(SwarmTurnReport {
schema_version: SWARM_TURN_REPORT_SCHEMA_VERSION,
outcome,
parent_agent_id: parent_agent_id.to_string(),
session_id: session_id.to_string(),
parent_run_id,
runtime_count: runtime_metrics.runtime_count,
busy_runtime_count: runtime_metrics.busy_runtime_count,
pending_task_count: runtime_metrics.pending_task_count,
running_task_count: runtime_metrics.running_task_count,
waiting_for_confirmation_count: runtime_metrics.waiting_for_confirmation_count,
waiting_for_user_input_count: runtime_metrics.waiting_for_user_input_count,
new_assistant_message_count: conversation_metrics.new_assistant_message_count,
final_reply_chars: conversation_metrics.final_reply_chars,
reconciliation_agent_count,
})
}
pub(super) fn print_turn_outcome<W: Write>(
outcome: SwarmTurnOutcome,
output: &mut W,
) -> Result<(), String> {
match outcome {
SwarmTurnOutcome::Settled(report) => print_swarm_turn_report(&report, output),
SwarmTurnOutcome::Failed { agent_ids, report } => {
writeln!(
output,
"[已失败] 以下 Runtime 到达失败终态:{}",
agent_ids.join(", ")
)
.map_err(|error| format!("写入终端失败:{error}"))?;
print_swarm_turn_report(&report, output)
}
SwarmTurnOutcome::Incomplete { reasons, report } => {
writeln!(
output,
"[未完成] 当前 turn 未满足可信终态:{}",
reasons.join(", ")
)
.map_err(|error| format!("写入终端失败:{error}"))?;
print_swarm_turn_report(&report, output)
}
SwarmTurnOutcome::NeedsReconciliation { agent_ids, report } => {
writeln!(
output,
"[已阻断] 以下 Agent 需要人工 reconciliation{}",
agent_ids.join(", ")
)
.map_err(|error| format!("写入终端失败:{error}"))?;
print_swarm_turn_report(&report, output)
}
SwarmTurnOutcome::Quit => Ok(()),
}
}
pub(super) fn print_swarm_turn_report<W: Write>(
report: &SwarmTurnReport,
output: &mut W,
) -> Result<(), String> {
let json = serde_json::to_string(report)
.map_err(|error| format!("序列化 turn report 失败:{error}"))?;
writeln!(output, "{SWARM_TURN_REPORT_PREFIX}{json}")
.map_err(|error| format!("写入终端失败:{error}"))
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,427 +0,0 @@
use super::*;
pub(super) const SWARM_CHAT_POLL_INTERVAL: Duration = Duration::from_millis(250);
pub(super) const SWARM_CHAT_SETTLE_WINDOW: Duration = Duration::from_millis(1_500);
#[derive(Debug, Eq, PartialEq)]
pub(super) struct SwarmTurnObservation {
pub(super) outcome: SwarmTurnOutcome,
pub(super) input_closed: bool,
}
pub(super) fn wait_for_swarm_turn<W: Write>(
root: &Path,
parent_agent_id: &str,
session_id: &str,
run_profile: &str,
expected_parent_source: Option<&str>,
conversation_baseline: SwarmTurnConversationBaseline,
input: &Receiver<SwarmInputEvent>,
output: &mut W,
observer: &mut SwarmRuntimeObserver,
poll_interval: Duration,
settle_window: Duration,
) -> Result<SwarmTurnOutcome, String> {
let mut stable_since: Option<Instant> = None;
let mut recovery_scan_required = true;
let mut last_runner_check = Instant::now();
let mut input_closed = false;
loop {
let runtimes = read_game_creator_agent_runtimes_at(root)?;
let turn_runtimes = swarm_current_runtimes_for_run(
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
&conversation_baseline.parent_run_id,
&runtimes,
);
let changed = observer.print_changes(&turn_runtimes, output)?;
if changed {
stable_since = None;
}
let mut reconciliation = swarm_reconciliation_agents(&turn_runtimes);
if !reconciliation.is_empty() {
observer.close_response_line(output)?;
return build_reconciliation_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
reconciliation,
);
}
if !input_closed {
match observer.resolve_confirmations(
root,
parent_agent_id,
&turn_runtimes,
input,
output,
)? {
SwarmConfirmationResolution::Handled => {
stable_since = None;
recovery_scan_required = true;
continue;
}
SwarmConfirmationResolution::InputClosed => {
mark_swarm_turn_input_closed(&mut input_closed, observer, output)?;
stable_since = None;
continue;
}
SwarmConfirmationResolution::Quit => return Ok(SwarmTurnOutcome::Quit),
SwarmConfirmationResolution::None => {}
}
match observer.resolve_user_input_requests(
root,
parent_agent_id,
&turn_runtimes,
input,
output,
)? {
SwarmConfirmationResolution::Handled => {
stable_since = None;
recovery_scan_required = true;
continue;
}
SwarmConfirmationResolution::InputClosed => {
mark_swarm_turn_input_closed(&mut input_closed, observer, output)?;
stable_since = None;
continue;
}
SwarmConfirmationResolution::Quit => return Ok(SwarmTurnOutcome::Quit),
SwarmConfirmationResolution::None => {}
}
}
let pending_interactions =
swarm_unhandled_interaction_reasons(parent_agent_id, &turn_runtimes, input_closed);
if !pending_interactions.is_empty() {
observer.close_response_line(output)?;
return build_incomplete_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
pending_interactions,
);
}
let failure_scan = scan_swarm_terminal_failures_at(
root,
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
&conversation_baseline.parent_run_id,
&runtimes,
);
if !failure_scan.reconciliation_agents.is_empty() {
observer.close_response_line(output)?;
return build_reconciliation_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
failure_scan.reconciliation_agents,
);
}
if !failure_scan.failed_agents.is_empty() {
observer.close_response_line(output)?;
return build_failed_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
failure_scan.failed_agents,
);
}
if !failure_scan.incomplete_reasons.is_empty() {
observer.close_response_line(output)?;
return build_incomplete_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
failure_scan.incomplete_reasons,
);
}
if last_runner_check.elapsed() >= Duration::from_secs(2) {
let runner = read_external_agent_runner_status();
last_runner_check = Instant::now();
if swarm_turn_is_busy(
root,
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
&conversation_baseline.parent_run_id,
&runtimes,
)? && (!runner.enabled || !runner.running)
{
reconciliation.push("external-runner".to_string());
observer.close_response_line(output)?;
return build_reconciliation_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
reconciliation,
);
}
}
if swarm_turn_is_busy(
root,
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
&conversation_baseline.parent_run_id,
&runtimes,
)? {
stable_since = None;
recovery_scan_required = true;
} else {
let since = stable_since.get_or_insert_with(Instant::now);
if since.elapsed() >= settle_window {
if recovery_scan_required {
observer.close_response_line(output)?;
writeln!(
output,
"[收束] Runtime 已空闲,检查待发布的 receipt / join。"
)
.map_err(|error| format!("写入终端失败:{error}"))?;
output
.flush()
.map_err(|error| format!("刷新终端失败:{error}"))?;
resume_game_creator_agent_background_tasks_at(root)?;
recovery_scan_required = false;
stable_since = Some(Instant::now());
continue;
}
let conversation_metrics = read_turn_conversation_metrics(
root,
parent_agent_id,
session_id,
&conversation_baseline,
)?;
let parent_runtime_is_current = swarm_parent_runtime_for_run(
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
&conversation_baseline.parent_run_id,
&runtimes,
)
.is_some();
let parent_runtime = swarm_parent_runtime_snapshot_for_run(
root,
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
&conversation_baseline.parent_run_id,
&runtimes,
)?;
let completion_blockers = if parent_runtime_is_current {
parent_runtime
.as_ref()
.map(|parent| swarm_parent_completion_contract_blockers_at(root, parent))
.unwrap_or_else(|| vec!["parent-runtime-missing".to_string()])
} else if parent_runtime
.as_ref()
.is_some_and(parent_runtime_completed)
{
Vec::new()
} else {
vec!["parent-runtime-missing".to_string()]
};
match classify_swarm_turn_terminal(
parent_runtime.as_ref(),
conversation_metrics,
0,
0,
completion_blockers.len(),
) {
SwarmTurnTerminalClassification::Settled => {
let printed_metrics = print_new_parent_reply(
root,
parent_agent_id,
session_id,
&conversation_baseline,
output,
observer,
)?;
if printed_metrics != conversation_metrics {
return build_incomplete_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
vec!["conversation-changed-before-settle".to_string()],
);
}
let report = build_swarm_turn_report(
SwarmTurnReportOutcome::Settled,
parent_agent_id,
session_id,
Some(&conversation_baseline.parent_run_id),
&runtimes,
conversation_metrics,
0,
)?;
return Ok(SwarmTurnOutcome::Settled(report));
}
SwarmTurnTerminalClassification::Failed => {
return build_failed_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
vec![format!(
"{}:{}",
parent_agent_id,
parent_runtime
.as_ref()
.map(|runtime| runtime.state.phase.as_str())
.unwrap_or("missing")
)],
);
}
SwarmTurnTerminalClassification::Incomplete => {
let mut reasons = completion_blockers;
append_swarm_terminal_snapshot_reasons(
&mut reasons,
parent_runtime.as_ref(),
conversation_metrics,
);
return build_incomplete_turn_outcome(
root,
parent_agent_id,
session_id,
&conversation_baseline,
&runtimes,
reasons,
);
}
}
}
}
if input_closed {
std::thread::sleep(poll_interval);
continue;
}
match input.recv_timeout(poll_interval) {
Ok(SwarmInputEvent::Line(line)) => {
let Some(command) = parse_swarm_chat_input(&line) else {
continue;
};
observer.close_response_line(output)?;
match command {
SwarmChatInput::Quit => return Ok(SwarmTurnOutcome::Quit),
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 _ = handle_swarm_goal_command(root, parent_agent_id, command, output)?;
stable_since = None;
recovery_scan_required = true;
}
SwarmChatInput::InvalidGoal(error) => print_swarm_goal_error(output, &error)?,
SwarmChatInput::Resume => {
writeln!(output, "[恢复] 当前已经在观察这个 Runtime。")
.map_err(|error| format!("写入终端失败:{error}"))?;
}
SwarmChatInput::Message(message) => {
if let Some(parent) = swarm_parent_steer_target(
parent_agent_id,
session_id,
run_profile,
expected_parent_source,
Some(&conversation_baseline.parent_run_id),
&runtimes,
) {
let steer_id = format!("swarm-steer-{}", unix_millis());
let result = tauri::async_runtime::block_on(
steer_game_creator_agent_runtime_task(
root.display().to_string(),
parent_agent_id.to_string(),
parent.state.session_id.clone(),
parent.state.run_id.clone(),
steer_id.clone(),
message,
Some(run_profile.to_string()),
None,
),
)?;
writeln!(
output,
"[已追加] run={} steer={} providerInterrupted={}",
parent.state.run_id, steer_id, result.provider_interrupted
)
.map_err(|error| format!("写入终端失败:{error}"))?;
} else {
writeln!(output, "[暂未发送] 子 Agent 尚未收束,请稍后重发。")
.map_err(|error| format!("写入终端失败:{error}"))?;
}
stable_since = None;
recovery_scan_required = true;
}
}
}
Ok(SwarmInputEvent::Eof) | Err(RecvTimeoutError::Disconnected) => {
mark_swarm_turn_input_closed(&mut input_closed, observer, output)?;
}
Ok(SwarmInputEvent::Error(error)) => {
observer.close_response_line(output)?;
return Err(format!("读取终端输入失败:{error}"));
}
Err(RecvTimeoutError::Timeout) => {}
}
}
}
pub(super) fn runtimes_are_busy(runtimes: &[AgentRuntimeResult]) -> bool {
runtimes.iter().any(runtime_is_busy)
}
pub(super) fn runtime_is_busy(runtime: &AgentRuntimeResult) -> bool {
matches!(
runtime.state.status.as_str(),
"pending"
| "running"
| "waiting-for-confirmation"
| "waiting-for-user-input"
| "cancelling"
) || runtime.state.phase == "needs-reconciliation"
|| runtime.task_queue.pending > 0
|| runtime.task_queue.running > 0
|| runtime.task_queue.waiting_for_confirmation > 0
|| runtime.task_queue.waiting_for_user_input > 0
}
pub(super) fn mark_swarm_turn_input_closed<W: Write>(
input_closed: &mut bool,
observer: &mut SwarmRuntimeObserver,
output: &mut W,
) -> Result<(), String> {
if *input_closed {
return Ok(());
}
*input_closed = true;
observer.close_response_line(output)?;
writeln!(output, "[输入已关闭] 当前 turn 继续运行,等待可信终态。")
.map_err(|error| format!("写入终端失败:{error}"))
}
@@ -1929,58 +1929,6 @@ async fn generate_local_game_draft_fails_after_max_passes_without_final_artifact
fs::remove_dir_all(root).ok();
}
#[test]
fn canvas_sync_suggestion_is_media_type_aware() {
let art_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS
.iter()
.find(|definition| definition.id == "art")
.copied()
.expect("art group");
let audio_group = GAME_CREATOR_AGENT_GROUP_DEFINITIONS
.iter()
.find(|definition| definition.id == "audio")
.copied()
.expect("audio group");
let art_role = ART_AGENT_ROLES
.iter()
.find(|role| role.id == "asset")
.copied()
.expect("art asset role");
let audio_role = AUDIO_AGENT_ROLES
.iter()
.find(|role| role.id == "sfx")
.copied()
.expect("audio sfx role");
let art_brief = AgentRoleBrief {
group_definition: art_group,
role_definition: art_role,
markdown: String::new(),
relative_path: ".agent/passes/pass-1/groups/art/asset.md".to_string(),
memory_relative_path: agent_role_memory_relative_path(art_group, art_role),
status: "completed".to_string(),
tool_id: art_role.tool_id.to_string(),
summary: String::new(),
};
let audio_brief = AgentRoleBrief {
group_definition: audio_group,
role_definition: audio_role,
markdown: String::new(),
relative_path: ".agent/passes/pass-1/groups/audio/sfx.md".to_string(),
memory_relative_path: agent_role_memory_relative_path(audio_group, audio_role),
status: "completed".to_string(),
tool_id: audio_role.tool_id.to_string(),
summary: String::new(),
};
let input_paths = vec![".agent/manifest.json".to_string()];
let image_canvas_assets = vec!["image/png".to_string()];
let audio_canvas_assets = vec!["audio/wav".to_string()];
assert!(suggested_canvas_tool_call(&art_brief, &input_paths, &image_canvas_assets).is_none());
assert!(suggested_canvas_tool_call(&audio_brief, &input_paths, &image_canvas_assets).is_some());
assert!(suggested_canvas_tool_call(&audio_brief, &input_paths, &audio_canvas_assets).is_none());
assert!(suggested_canvas_tool_call(&art_brief, &input_paths, &audio_canvas_assets).is_some());
}
#[test]
fn init_local_game_project_creates_manifest_and_dirs() {
let root = unique_project_path();
@@ -6461,31 +6461,7 @@ async fn agent_loop_writes_spec_findings_and_retries_generator() {
.iter()
.any(|step| step["agent"] == "数值组 / Difficulty"));
assert!(steps.iter().any(|step| step["agent"] == "美术组 / Asset"));
assert!(steps.iter().any(|step| {
step["agent"] == "美术组 / Asset"
&& step["toolCalls"]
.as_array()
.unwrap()
.iter()
.any(|tool_call| {
tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync"
&& tool_call["status"] == "suggested"
})
}));
assert!(steps.iter().any(|step| step["agent"] == "音乐组 / SFX"));
assert!(steps.iter().any(|step| {
step["agent"] == "音乐组 / SFX"
&& step["toolCalls"]
.as_array()
.unwrap()
.iter()
.any(|tool_call| {
tool_call["toolId"] == "agent.tool.suggest.canvas.project_sync"
&& tool_call["summary"]
.as_str()
.is_some_and(|summary| summary.contains("/sync-canvas-project"))
})
}));
assert!(steps.iter().any(|step| step["agent"] == "程序组 / Code"));
assert!(steps.iter().any(|step| step["agent"] == "运营组 / Publish"));
assert!(steps
+8 -69
View File
@@ -40,7 +40,6 @@ import type {
LocalGameProjectRevisionStatus,
LocalPreviewResult,
LocalPreviewStatus,
LocalProjectFileResult,
LocalProjectKind,
PendingUiConfirmation,
ProjectPermissionPolicyView,
@@ -67,19 +66,13 @@ import {
writeRecentWorkspace,
} from './features/app-shell/model';
import { WorkspaceLauncherShell } from './features/app-shell/WorkspaceLauncher';
import {
projectAgentRuntimeSummaries,
summarizeAgentRunCompletionForChat,
} from './features/project-summary/agentPresentation';
import { projectAgentRuntimeSummaries } from './features/project-summary/agentPresentation';
import {
isAbsoluteProjectPath,
projectPathHasControlCharacter,
} from './features/project-summary/projectSummary';
import { parseAgentRunTrace } from './features/project-workspace/agentRunTrace';
import { importDesignFiles } from './features/project-workspace/importDesignFiles';
import { parseRememberInput } from './features/project-workspace/memoryCommands';
import {
isAgentTraceFilePath,
needsInitializedChatProject,
resolveChatProjectPath,
} from './features/project-workspace/projectCommandPolicy';
@@ -146,17 +139,9 @@ function isPersistableDirectCodexConversationMessage(message: ChatMessage) {
*/
export { AuthenticatedClient } from './app/AuthenticatedClient';
export {
deriveAgentStatusCards,
summarizeAgentAudit,
summarizeAgentRunTrace,
} from './features/project-summary/agentPresentation';
export { deriveAgentStatusCards } from './features/project-summary/agentPresentation';
export { isAbsoluteProjectPath } from './features/project-summary/projectSummary';
export {
needsInitializedChatProject,
parseRememberInput,
resolveChatProjectPath,
};
export { needsInitializedChatProject, resolveChatProjectPath };
export function WorkspaceLauncher(props: WorkspaceLauncherProps) {
return <WorkspaceLauncherShell {...props} ProjectChat={App} />;
@@ -857,7 +842,7 @@ export function App({
localProjectPathRef.current === initialProjectPath &&
!planningStartMode
) {
void refreshAgentRunTrace(initialProjectPath);
void refreshAgentRuntimes(initialProjectPath);
}
});
// Initial project opening is guarded by initialProjectOpenedRef.
@@ -1512,7 +1497,7 @@ export function App({
}
void loadProjectConversation(openedProject.projectPath);
if (!directProjectMode) {
void refreshAgentRunTrace(openedProject.projectPath);
void refreshAgentRuntimes(openedProject.projectPath);
}
} catch (error) {
if (projectScopeVersionRef.current !== projectScopeVersion) {
@@ -1730,9 +1715,8 @@ export function App({
*
* `activate_local_game_preview` 是 Rust 侧的「这条预览还活着、且属于这个项目」闸门:
* 它只核对内存 registry 里的状态并回传可用的 loopback 地址,前端据此进客户端运行视图。
* 同一个动作过去由工作台壳的 `/preview open` 聊天命令承担,那条命令链随 Supervisor
* 前端链路一起退役,行为改由「运行」入口承接,结果经 DirectProject 聊天的 `announce`
* 交给聊天自己的消息流。返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。
* 行为由「运行」入口承接,结果经 DirectProject 聊天的 `announce` 交给聊天自己的消息流。
* 返回 `null` 表示没有可复用的活体预览,调用方照旧重启预览。
*/
async function activateRunningPreview(
invoke: TauriInvoke,
@@ -1795,7 +1779,7 @@ export function App({
updateClientPreview(previewResult);
void refreshManifest(nextProjectPath);
if (!directProjectMode) {
void refreshAgentRunTrace(nextProjectPath);
void refreshAgentRuntimes(nextProjectPath);
}
if (announceToChat) {
announceProjectChatMessage(
@@ -1810,36 +1794,6 @@ export function App({
}
}
async function loadAgentRunTraceFile(
relativePath: string,
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
const invoke = resolveTauriInvoke();
if (!invoke) {
return null;
}
if (!nextProjectPath) {
return null;
}
try {
const result = await invoke<LocalProjectFileResult>(
'read_local_project_file',
{
projectPath: nextProjectPath,
relativePath,
commandId: isAgentTraceFilePath(relativePath)
? 'agent.trace_read'
: 'file.read',
},
);
const trace = parseAgentRunTrace(result.content);
return summarizeAgentRunCompletionForChat(trace);
} catch {
return null;
}
}
function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) {
if (!runtime) {
return;
@@ -1955,21 +1909,6 @@ export function App({
}
}
async function refreshAgentRunTrace(
nextProjectPath = resolveChatProjectPath(localProject) ?? '',
) {
if (!nextProjectPath) {
await refreshAgentRuntimes(nextProjectPath);
return null;
}
const summary = await loadAgentRunTraceFile(
'.agent/run.latest.json',
nextProjectPath,
);
await refreshAgentRuntimes(nextProjectPath);
return summary;
}
const professionalResultCandidates = taskRowsFromManifest(manifest).map(
(task) => ({
agentId: agentConversationId(task),
File diff suppressed because it is too large Load Diff
@@ -1,311 +0,0 @@
import {
type GameCreationAgentRunTrace,
type GameCreationAppManifest,
type GameCreationAppTaskState,
selectGameCreationAppReadyTasks,
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
import {
type AgentRunHistoryItem,
type AgentStatusCard,
} from '../../app/types';
import { taskRowsFromManifest } from '../agent-runtime';
import {
formatAgentRunStatus,
formatTraceRepairRoutes,
isAgentReviewStep,
isAgentRunTracePassed,
} from './agentTrace';
import { isSafeProjectRelativePath } from './projectPath';
import { previewStatusLabels } from './projectSummaryConstants';
export function summarizeAgentRunBudget(
trace: GameCreationAgentRunTrace | null,
) {
if (!trace) {
return {
text: '运行预算:\n- 最近 Run:暂无\n- 建议:/next',
draftCommand: '/next',
draftCommandLabel: '查看下一步',
};
}
const remainingPasses = Math.max(trace.maxPasses - trace.passes, 0);
const remainingToolCalls = Math.max(
trace.maxToolCalls - trace.toolCallCount,
0,
);
const blocked =
trace.lifecycleStatus === 'killed' ||
trace.status === 'failed' ||
trace.status === 'needs-revision' ||
trace.stopReason === 'max-passes-exhausted' ||
remainingPasses === 0 ||
remainingToolCalls === 0;
const draftCommand = blocked
? '/review'
: isAgentRunTracePassed(trace)
? '/publish'
: '/trace';
return {
text: [
'运行预算:',
`- Run${trace.runId} · ${formatAgentRunStatus(trace)}`,
`- 轮次:已用 ${trace.passes}/${trace.maxPasses} · 剩余 ${remainingPasses}`,
`- 工具调用:已用 ${trace.toolCallCount}/${trace.maxToolCalls} · 剩余 ${remainingToolCalls}`,
`- 下一步:${trace.nextStep}`,
`- 建议:${draftCommand}`,
].join('\n'),
draftCommand,
draftCommandLabel:
draftCommand === '/review'
? '查看评审'
: draftCommand === '/publish'
? '查看发布准备'
: '查看 trace',
};
}
export function summarizeAgentReviewState(
trace: GameCreationAgentRunTrace | null,
) {
if (!trace) {
return {
text: '评审状态:暂无最近 trace',
draftCommand: '/next',
draftCommandLabel: '查看下一步',
};
}
const reviewSteps = trace.steps.filter(isAgentReviewStep);
const visibleReviewSteps = reviewSteps.slice(-3);
const reviewStepLines = visibleReviewSteps.map((step) => {
const outputPaths = step.outputPaths.filter(isSafeProjectRelativePath);
return [
`- ${step.agent} #${step.pass} · ${step.status} · ${step.summary}`,
outputPaths.length > 0 ? `输出 ${outputPaths.join(', ')}` : null,
]
.filter(Boolean)
.join(' · ');
});
if (reviewSteps.length > visibleReviewSteps.length) {
reviewStepLines.push(
`- 还有 ${reviewSteps.length - visibleReviewSteps.length} 个较早评审步骤`,
);
}
const repair = formatTraceRepairRoutes(
trace.taskGraph.repairRoutes,
trace.taskGraph.tasks,
);
const needsResume =
trace.lifecycleStatus === 'killed' ||
trace.status === 'failed' ||
trace.status === 'needs-revision' ||
trace.stopReason === 'max-passes-exhausted';
const evaluatorState = isAgentRunTracePassed(trace)
? '通过'
: needsResume
? '需返工'
: '未通过';
return {
text: [
'评审状态:',
`- Run${trace.runId} · ${formatAgentRunStatus(trace)}`,
`- Evaluator${evaluatorState}`,
`- 返工焦点:${
trace.taskGraph.repairFocus.length > 0
? trace.taskGraph.repairFocus.join('')
: '暂无'
}`,
`- 返工路线:${repair || '暂无'}`,
`- 下一步:${trace.nextStep}`,
'- 评审记录:/read .agent/findings.md',
reviewStepLines.length > 0
? `- 最近评审步骤:\n${reviewStepLines.join('\n')}`
: '- 最近评审步骤:暂无',
].join('\n'),
draftCommand: needsResume ? '/agent-resume ' : '/read .agent/findings.md',
draftCommandLabel: needsResume ? '继续修复' : '读取评审记录',
};
}
export function summarizeProjectContextSources(
nextManifest: GameCreationAppManifest,
trace: GameCreationAgentRunTrace | null,
) {
const tasks = taskRowsFromManifest(nextManifest);
const llmInputPaths = trace
? Array.from(
new Set(
trace.steps
.filter((step) =>
step.toolCalls.some((toolCall) =>
toolCall.toolId.startsWith('llm.'),
),
)
.flatMap((step) => step.inputPaths)
.filter(isSafeProjectRelativePath),
),
).slice(0, 8)
: [];
const inputPathLines = llmInputPaths.map(
(path) => `- ${path}/read ${path}`,
);
const draftCommand =
llmInputPaths.length > 0
? `/read ${llmInputPaths[0]}`
: '/memory blackboard';
return {
text: [
'上下文来源:',
'- 项目对话:/history',
'- 短期记忆:/memory short',
'- 长期记忆:/memory long',
'- 项目黑板:/memory blackboard',
`- Agent 对话:${tasks.length} 个 · /agent-conversations`,
`- Agent 私有记忆:${tasks.length} 个 · /agent-memories`,
'- 项目 manifest/read .agent/manifest.json',
trace ? `- 最近 Run${trace.runId} · /trace` : '- 最近 Run:暂无',
inputPathLines.length > 0
? `最近 LLM 输入:\n${inputPathLines.join('\n')}`
: '最近 LLM 输入:暂无',
].join('\n'),
draftCommand,
draftCommandLabel: llmInputPaths.length > 0 ? '读取首个上下文' : '查看黑板',
};
}
export function summarizeProjectTimeline(
nextManifest: GameCreationAppManifest,
trace: GameCreationAgentRunTrace | null,
) {
const commandRuns = (nextManifest.commandRuns ?? []).slice(-5);
const commandLines = commandRuns.map((commandRun) => {
const logSuffix = isSafeProjectRelativePath(commandRun.logPath)
? ` · 日志 /read ${commandRun.logPath}`
: '';
return `- 命令 ${commandRun.commandId} · ${
commandRun.status === 'completed' ? '完成' : '失败'
}${logSuffix}`;
});
const visibleSteps = trace?.steps.slice(-6) ?? [];
const stepLines = visibleSteps.map((step) => {
const outputPaths = step.outputPaths
.filter(isSafeProjectRelativePath)
.slice(0, 3);
return [
`- ${step.agent} #${step.pass} / ${step.phase} · ${step.status} · ${step.summary}`,
outputPaths.length > 0 ? `输出 ${outputPaths.join(', ')}` : null,
]
.filter(Boolean)
.join(' · ');
});
const latestSafeLogPath = [...commandRuns]
.reverse()
.find((commandRun) =>
isSafeProjectRelativePath(commandRun.logPath),
)?.logPath;
const draftCommand = latestSafeLogPath
? `/read ${latestSafeLogPath}`
: trace
? '/trace'
: '/history';
return {
text: [
'项目时间线:',
`- Run${trace ? `${trace.runId} · ${formatAgentRunStatus(trace)}` : '暂无最近 run'}`,
commandLines.length > 0
? `- 最近命令:\n${commandLines.join('\n')}`
: '- 最近命令:暂无',
stepLines.length > 0
? `- 最近步骤:\n${stepLines.join('\n')}`
: '- 最近步骤:暂无',
].join('\n'),
draftCommand,
draftCommandLabel: latestSafeLogPath
? '读取最近日志'
: trace
? '查看 trace'
: '查看历史',
};
}
export function summarizeProjectHandoff(
nextManifest: GameCreationAppManifest,
nextProjectPath: string,
trace: GameCreationAgentRunTrace | null,
history: AgentRunHistoryItem[],
agents: AgentStatusCard[],
) {
const tasks = taskRowsFromManifest(nextManifest);
const completedCount = tasks.filter(
(task) => task.status === 'completed',
).length;
const manifestTasksById = new Map(tasks.map((task) => [task.id, task]));
const traceTasksById = new Map(
trace?.taskGraph.tasks.map((task) => [task.id, task]) ?? [],
);
const traceReadyTasks =
trace?.taskGraph.readyTaskIds
.map(
(taskId) => traceTasksById.get(taskId) ?? manifestTasksById.get(taskId),
)
.filter((task): task is GameCreationAppTaskState => Boolean(task)) ?? [];
const readyTasks =
traceReadyTasks.length > 0
? traceReadyTasks
: selectGameCreationAppReadyTasks({ tasks });
const failedTasks = tasks.filter((task) => task.status === 'failed');
const sourceCounts = nextManifest.assets.reduce(
(counts, asset) => {
counts[asset.source.kind] += 1;
return counts;
},
{ uploaded: 0, generated: 0, canvas: 0 },
);
const preview = nextManifest.preview;
const previewSummary =
preview?.status === 'running' && preview.url
? `运行中 ${preview.url}`
: preview
? previewStatusLabels[preview.status]
: '未启动';
const commandRuns = nextManifest.commandRuns ?? [];
const latestCommandRun = commandRuns[commandRuns.length - 1];
const evidenceAgentCount = agents.filter(
(agent) => agent.hasRecentEvidence,
).length;
const activeAgentCount = agents.filter(
(agent) => agent.taskGraphState === 'active',
).length;
const runSummary = trace
? `${trace.runId} · ${formatAgentRunStatus(trace)} · next ${trace.nextStep}`
: history[0]
? `无 latest,最近历史 ${history[0].trace.runId} · ${formatAgentRunStatus(history[0].trace)}`
: '暂无 run';
const readySummary =
readyTasks.length > 0
? readyTasks
.slice(0, 3)
.map((task) => `${task.group}/${task.role} ${task.title}`)
.join('')
: '暂无';
const failedSummary =
failedTasks.length > 0
? failedTasks
.slice(0, 3)
.map((task) => `${task.group}/${task.role} ${task.title}`)
.join('')
: '暂无';
return `项目交接:\n- 项目:${nextManifest.name}\n- 目录:${nextProjectPath}\n- Run${runSummary}\n- 任务:完成 ${completedCount}/${tasks.length} · ready ${readyTasks.length} · 失败 ${failedTasks.length}\n- Ready${readySummary}\n- 失败项:${failedSummary}\n- 资产:${nextManifest.assets.length} 个 · 上传 ${sourceCounts.uploaded} / 生成 ${sourceCounts.generated} / 画板 ${sourceCounts.canvas}\n- 预览:${previewSummary}\n- Agent${evidenceAgentCount}/${agents.length} 有运行证据 · active ${activeAgentCount}\n- 历史:已加载 ${history.length} 个 run\n- 最近命令:${
latestCommandRun
? `${latestCommandRun.commandId} · ${
latestCommandRun.status === 'completed' ? '完成' : '失败'
}`
: '暂无'
}`;
}

Some files were not shown because too many files have changed in this diff Show More