退役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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user