完善自主构建运行时并提高 Runner 启动容错

完善 Agent Swarm 自主游戏构建、委派协作、验证门和试玩闭环

补齐浏览器验证、CLI 入口、项目状态与运行时测试覆盖

将外部 Agent Runner 冷启动超时提高到 30 秒并增加回归测试

同步真实 E2E 自测脚本、配置检查和项目决策记录
This commit is contained in:
AIGameCreator App
2026-07-21 12:53:27 +08:00
parent 1d4aad2600
commit 9bb048c11b
21 changed files with 12041 additions and 244 deletions
+1
View File
@@ -18,6 +18,7 @@
"agent-runtime:collaboration-policy-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-collaboration-policy-mixed-recovery",
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat",
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
"agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense",
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
"agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-final-reply-transient-retry",
"agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-tool-plan-handoff-runner-kill",
File diff suppressed because it is too large Load Diff
@@ -426,7 +426,13 @@ const eventCapability = JSON.parse(
);
const eventCapabilityWindows = new Set(eventCapability.windows ?? []);
const eventCapabilityPermissions = new Set(eventCapability.permissions ?? []);
for (const windowLabel of ['client', 'developer', 'main', 'launcher']) {
for (const windowLabel of [
'client',
'developer',
'main',
'launcher',
'supervisor-chat',
]) {
if (!eventCapabilityWindows.has(windowLabel)) {
throw new Error(
`AI game creator shell event capability missing window: ${windowLabel}`,
File diff suppressed because it is too large Load Diff
@@ -897,7 +897,8 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
"viewports": { "type": "array", "minItems": 1, "maxItems": 2, "items": { "type": "string", "enum": ["desktop", "mobile"] } },
"expectedText": string_array_schema(16),
"settleMs": { "type": "integer", "minimum": 0, "maximum": 10000 },
"failOnConsoleError": { "type": "boolean" }
"failOnConsoleError": { "type": "boolean" },
"playtestScenario": { "type": ["string", "null"], "enum": ["generic-v1", "lane-defense-v1", null] }
}
}),
"image.inspect" => json!({
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,7 @@ pub(crate) enum CliCommand {
project_path: PathBuf,
parent_agent_id: String,
initialize: bool,
run_profile: String,
},
AgentEnqueue {
project_path: PathBuf,
@@ -644,6 +645,7 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
}));
}
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);
@@ -651,10 +653,24 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
} 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(
"用法:--swarm-chat [--init] <本地项目绝对路径> [parentAgentId]".to_string(),
);
return Err(USAGE.to_string());
}
return Ok(Some(CliCommand::SwarmChat {
project_path: PathBuf::from(&rest[0]),
@@ -663,6 +679,11 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
.map(|value| value.trim().to_string())
.unwrap_or_else(|| GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
initialize,
run_profile: if autonomous_game_build {
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string()
} else {
AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
},
}));
}
if args.first().map(String::as_str) == Some("--agent-task") {
@@ -876,11 +897,12 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
project_path,
parent_agent_id,
initialize,
run_profile,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
initialize_cli_agent_project(&project_path, initialize)?;
run_game_creator_swarm_chat_at(&project_path, &parent_agent_id)
run_game_creator_swarm_chat_at(&project_path, &parent_agent_id, &run_profile)
}
CliCommand::AgentEnqueue {
project_path,
@@ -1704,6 +1726,7 @@ mod tests {
project_path,
parent_agent_id: "code-prototype".to_string(),
initialize: true,
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
}
);
assert!(command.requires_external_agent_runner());
@@ -1764,6 +1787,29 @@ mod tests {
project_path,
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
initialize: false,
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
}
);
}
#[test]
fn swarm_chat_autonomous_game_build_flag_selects_autonomous_profile() {
let project_path = std::env::current_dir().expect("current directory");
let command = parse_cli_command(&[
"--swarm-chat".to_string(),
project_path.display().to_string(),
"--autonomous-game-build".to_string(),
])
.expect("parse autonomous supervisor chat")
.expect("autonomous supervisor chat command");
assert_eq!(
command,
CliCommand::SwarmChat {
project_path,
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
initialize: false,
run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
}
);
}
@@ -1778,5 +1824,12 @@ mod tests {
"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());
}
}
@@ -22,6 +22,8 @@ const SUPERVISOR_COLLABORATION_POLICY_BINDING_LEGACY_CURRENT: &str =
const SUPERVISOR_COLLABORATION_MAX_STATIC_DELEGATES: usize = 3;
const SUPERVISOR_COLLABORATION_MAX_ISOLATED_CHILDREN: usize = 3;
const SUPERVISOR_COLLABORATION_MAX_ISOLATED_GROUPS_BEFORE_CLAIM: usize = 16;
const AUTONOMOUS_GAME_BUILD_REQUIRED_STATIC_AGENT_IDS: [&str; 2] =
["code-prototype", "quality-review"];
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
@@ -65,6 +67,70 @@ impl Default for SupervisorCollaborationPolicy {
}
}
fn autonomous_game_build_supervisor_collaboration_policy() -> SupervisorCollaborationPolicy {
SupervisorCollaborationPolicy {
required_initial_wave: SupervisorInitialCollaborationWave::Static,
min_static_delegates: AUTONOMOUS_GAME_BUILD_REQUIRED_STATIC_AGENT_IDS.len(),
required_static_agent_ids: AUTONOMOUS_GAME_BUILD_REQUIRED_STATIC_AGENT_IDS
.into_iter()
.map(str::to_string)
.collect(),
..SupervisorCollaborationPolicy::default()
}
}
#[derive(Clone, Debug)]
struct SupervisorCollaborationUnboundPolicy {
policy: SupervisorCollaborationPolicy,
source: &'static str,
project_policy_status: &'static str,
project_policy_present: bool,
}
fn read_supervisor_collaboration_unbound_policy_for_run_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<SupervisorCollaborationUnboundPolicy, String> {
let policy_path = root.join(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH);
match fs::symlink_metadata(&policy_path) {
Ok(_) => {
return Ok(SupervisorCollaborationUnboundPolicy {
policy: read_supervisor_collaboration_policy_at(root)?,
source: "project-policy-unbound",
project_policy_status: "current",
project_policy_present: true,
});
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"读取 Project Supervisor 协作策略文件状态失败:{error}"
));
}
}
let (run_profile, _) =
agent_runtime_run_profile_identity_at(root, parent_agent_id, parent_run_id, None, None)?;
if parent_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
return Ok(SupervisorCollaborationUnboundPolicy {
policy: normalize_supervisor_collaboration_policy(
autonomous_game_build_supervisor_collaboration_policy(),
)?,
source: "autonomous-run-default",
project_policy_status: "absent",
project_policy_present: false,
});
}
Ok(SupervisorCollaborationUnboundPolicy {
policy: SupervisorCollaborationPolicy::default(),
source: "project-policy-unbound",
project_policy_status: "current",
project_policy_present: false,
})
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct SupervisorCollaborationState {
pub(crate) initial_static_agent_ids: Vec<String>,
@@ -656,7 +722,12 @@ pub(crate) fn resolve_supervisor_collaboration_policy_for_run_at(
.to_string(),
);
}
let policy = read_supervisor_collaboration_policy_at(root)?;
let unbound_policy = read_supervisor_collaboration_unbound_policy_for_run_at(
root,
parent_agent_id,
parent_run_id,
)?;
let policy = unbound_policy.policy.clone();
let state = read_supervisor_collaboration_state_at(root, parent_agent_id, parent_run_id)?;
if state.has_collaboration() {
if !game_creator_agent_runtime_run_is_non_terminal_for_collaboration_migration_at(
@@ -693,8 +764,8 @@ pub(crate) fn resolve_supervisor_collaboration_policy_for_run_at(
policy,
snapshot_fingerprint: None,
binding_source: None,
source: "project-policy-unbound",
project_policy_status: "current",
source: unbound_policy.source,
project_policy_status: unbound_policy.project_policy_status,
current_project_policy_fingerprint: None,
})
}
@@ -703,16 +774,23 @@ fn supervisor_collaboration_policy_resolution_from_snapshot(
root: &Path,
snapshot: SupervisorCollaborationPolicySnapshot,
) -> Result<SupervisorCollaborationPolicyResolution, String> {
let current_project_policy = read_supervisor_collaboration_policy_at(root);
let (project_policy_status, current_project_policy_fingerprint) = match current_project_policy {
let current_policy = read_supervisor_collaboration_unbound_policy_for_run_at(
root,
&snapshot.parent_agent_id,
&snapshot.parent_run_id,
);
let (project_policy_status, current_project_policy_fingerprint) = match current_policy {
Ok(current) => {
let fingerprint = supervisor_collaboration_policy_fingerprint(&current)?;
let status = if current == snapshot.policy {
let fingerprint = current
.project_policy_present
.then(|| supervisor_collaboration_policy_fingerprint(&current.policy))
.transpose()?;
let status = if current.policy == snapshot.policy {
"matched"
} else {
"drifted"
};
(status, Some(fingerprint))
(status, fingerprint)
}
Err(_) => ("unreadable", None),
};
@@ -457,6 +457,33 @@ pub(crate) fn start_game_creator_agent_runtime_task(
)
}
#[tauri::command]
pub(crate) fn start_game_creator_supervisor_runtime_task(
project_path: String,
session_id: Option<String>,
task: String,
run_id: String,
run_profile: Option<String>,
) -> Result<AgentRuntimeResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
enforce_project_permission_policy(root, "conversation.write")?;
enforce_project_permission_policy(root, "agent.run_status")?;
let run_profile = run_profile
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(AGENT_RUNTIME_RUN_PROFILE_STANDARD);
start_game_creator_supervisor_background_task_for_session_at(
root,
session_id.as_deref(),
task.trim(),
run_id.trim(),
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
run_profile,
)
}
#[tauri::command]
pub(crate) async fn compact_game_creator_agent_runtime_context(
project_path: String,
@@ -190,6 +190,10 @@ struct AgentRuntimeState {
run_id: String,
#[serde(default)]
source: String,
#[serde(default = "default_agent_runtime_run_profile")]
run_profile: String,
#[serde(default)]
run_profile_binding_fingerprint: String,
#[serde(default)]
parent_agent_id: Option<String>,
#[serde(default)]
@@ -307,6 +311,10 @@ struct AgentRuntimeSteerRef {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeToolPolicySnapshot {
#[serde(default = "default_agent_runtime_run_profile")]
run_profile: String,
#[serde(default)]
run_profile_binding_fingerprint: String,
#[serde(default)]
allowed_tools: Vec<String>,
#[serde(default)]
@@ -322,6 +330,8 @@ struct AgentRuntimeToolPolicySnapshot {
impl Default for AgentRuntimeToolPolicySnapshot {
fn default() -> Self {
Self {
run_profile: default_agent_runtime_run_profile(),
run_profile_binding_fingerprint: String::new(),
allowed_tools: Vec::new(),
auto_tools: Vec::new(),
confirm_tools: Vec::new(),
@@ -497,6 +507,10 @@ struct AgentRuntimeTaskRecord {
run_id: String,
#[serde(default)]
source: String,
#[serde(default = "default_agent_runtime_run_profile")]
run_profile: String,
#[serde(default)]
run_profile_binding_fingerprint: String,
#[serde(default)]
parent_agent_id: Option<String>,
#[serde(default)]
@@ -1166,9 +1180,17 @@ const PROJECT_WRITE_LOCK_PATH: &str = ".agent/project.lock";
const LOCAL_CONVERSATION_SCHEMA_VERSION: &str = "game-creator-conversation.v1";
const AGENT_CONVERSATION_SESSION_SCHEMA_VERSION: &str = "game-creator-agent-sessions.v1";
const AGENT_RUNTIME_SCHEMA_VERSION: &str = "game-creator-agent-runtime.v1";
const AGENT_RUNTIME_RUN_PROFILE_STANDARD: &str = "standard";
const AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD: &str = "autonomous-game-build";
const AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS: usize = 8_000;
const AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS: usize = 10_000;
const AGENT_RUNTIME_RECENT_EVENT_LIMIT: usize = 20;
const AGENT_RUNTIME_RECENT_TASK_LIMIT: usize = 12;
const GAME_CREATOR_CONVERSATION_CONTEXT_MAX_MESSAGES: usize = 12;
fn default_agent_runtime_run_profile() -> String {
AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
}
const MAX_CANVAS_EXPORT_FILES: usize = 500;
const MAX_CANVAS_EXPORT_BYTES: u64 = 512 * 1024 * 1024;
const MAX_PROJECT_EXPORT_PACKAGE_FILES: usize = 1200;
@@ -1695,6 +1717,7 @@ fn main() {
chat_with_game_creator_role_agent,
chat_with_game_creator_role_agent_stream,
start_game_creator_agent_runtime_task,
start_game_creator_supervisor_runtime_task,
compact_game_creator_agent_runtime_context,
read_game_creator_agent_goal,
start_game_creator_agent_goal,
@@ -1906,6 +1906,8 @@ mod tests {
session_id: state.session_id.clone(),
run_id: state.run_id.clone(),
source: state.source.clone(),
run_profile: default_agent_runtime_run_profile(),
run_profile_binding_fingerprint: String::new(),
task: state.current_task.clone(),
goal_id: None,
goal_revision: 0,
@@ -1696,6 +1696,10 @@ pub(crate) fn append_agent_db_tool_plan_audit_idempotent(
"normalizedTextSha256",
"responseIdSha256",
"responseIdChars",
"autonomousSourcePayloadValidated",
"autonomousSourceMutationActionCount",
"autonomousSourceMaxFieldChars",
"autonomousSourceTotalChars",
];
const REPAIR_FIELDS: &[&str] = &[
"recordType",
@@ -1801,6 +1805,49 @@ pub(crate) fn append_agent_db_tool_plan_audit_idempotent(
return Err(format!("Agent DB tool-plan 幂等审计字段无效:{field}"));
}
}
let autonomous_source_payload_validated = record
.get("autonomousSourcePayloadValidated")
.and_then(serde_json::Value::as_bool)
.ok_or_else(|| {
"Agent DB tool-plan 幂等审计字段无效:autonomousSourcePayloadValidated"
.to_string()
})?;
let autonomous_source_values = [
"autonomousSourceMutationActionCount",
"autonomousSourceMaxFieldChars",
"autonomousSourceTotalChars",
]
.map(|field| record.get(field));
if autonomous_source_payload_validated {
let [Some(mutation_count), Some(max_field_chars), Some(total_chars)] =
autonomous_source_values
else {
return Err("Agent DB tool-plan 自主源码载荷审计缺少数值".to_string());
};
let Some(mutation_count) = mutation_count.as_u64() else {
return Err("Agent DB tool-plan 自主源码动作数量无效".to_string());
};
let Some(max_field_chars) = max_field_chars.as_u64() else {
return Err("Agent DB tool-plan 自主源码字段长度无效".to_string());
};
let Some(total_chars) = total_chars.as_u64() else {
return Err("Agent DB tool-plan 自主源码总长度无效".to_string());
};
if mutation_count > 1
|| max_field_chars
> AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS as u64
|| total_chars
> AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS as u64
|| total_chars < max_field_chars
{
return Err("Agent DB tool-plan 自主源码载荷审计越界".to_string());
}
} else if autonomous_source_values
.iter()
.any(|value| !matches!(value, Some(serde_json::Value::Null)))
{
return Err("非自主 tool-plan 不能携带源码载荷数值".to_string());
}
}
"agent.runtime.tool_plan.repair" => {
for field in ["protocolErrorSha256", "responsePreviewSha256"] {
@@ -5748,7 +5795,7 @@ pub(crate) fn run_limited_local_command_at(
}
validate_game_html_smoke(&html)?;
let output = format!("通过:{}{} 字节", game_index_path.display(), html.len());
let output = format!("通过:game/index.html{} 字节", html.len());
let log_path = root.join(".agent/logs/command.log");
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)
@@ -5769,7 +5816,7 @@ pub(crate) fn run_limited_local_command_at(
command_id: command_id.to_string(),
status: GameCreationAppCommandRunStatus::Completed,
output: output.clone(),
log_path: log_path.to_string_lossy().into_owned(),
log_path: ".agent/logs/command.log".to_string(),
updated_at,
},
)?;
@@ -5778,7 +5825,7 @@ pub(crate) fn run_limited_local_command_at(
command_id: command_id.to_string(),
status: "completed".to_string(),
output,
log_path: log_path.to_string_lossy().into_owned(),
log_path: ".agent/logs/command.log".to_string(),
updated_at,
})
}
@@ -8824,6 +8871,10 @@ mod agent_db_security_tests {
"normalizedTextSha256": null,
"responseIdSha256": null,
"responseIdChars": 0,
"autonomousSourcePayloadValidated": false,
"autonomousSourceMutationActionCount": null,
"autonomousSourceMaxFieldChars": null,
"autonomousSourceTotalChars": null,
})
}
@@ -9725,6 +9776,37 @@ mod agent_db_security_tests {
fs::remove_dir_all(root).ok();
}
#[test]
fn tool_plan_audit_accepts_current_autonomous_source_limits() {
let root = unique_agent_db_test_root("tool-plan-audit-autonomous-source-limits");
fs::create_dir_all(&root).expect("create autonomous source audit project");
let mut record = tool_plan_protocol_audit_record(
"code-prototype",
"autonomous-source-limit-run",
"loop-0-repair-0",
);
record["autonomousSourcePayloadValidated"] = serde_json::json!(true);
record["autonomousSourceMutationActionCount"] = serde_json::json!(1);
record["autonomousSourceMaxFieldChars"] =
serde_json::json!(AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS);
record["autonomousSourceTotalChars"] =
serde_json::json!(AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS);
assert!(
append_agent_db_tool_plan_audit_idempotent(&root, record.clone())
.expect("append current autonomous source limits")
);
record["requestSlot"] = serde_json::json!("loop-1-repair-0");
record["autonomousSourceMaxFieldChars"] =
serde_json::json!(AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS + 1);
let error = append_agent_db_tool_plan_audit_idempotent(&root, record)
.expect_err("reject autonomous source field above current limit");
assert_eq!(error, "Agent DB tool-plan 自主源码载荷审计越界");
fs::remove_dir_all(root).ok();
}
#[test]
fn action_group_append_repairs_torn_tail_and_scans_past_bounded_history() {
const RECORD_TYPE: &str = "agent.runtime.agent.isolated_join.claimed_by_parent";
@@ -33,7 +33,7 @@ const EXTERNAL_AGENT_RUNNER_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
const EXTERNAL_AGENT_RUNNER_IO_TIMEOUT: Duration = Duration::from_secs(10);
const EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT: Duration = Duration::from_secs(6 * 60);
const EXTERNAL_AGENT_RUNNER_MCP_STATUS_IO_TIMEOUT: Duration = Duration::from_secs(6 * 60);
const EXTERNAL_AGENT_RUNNER_START_TIMEOUT: Duration = Duration::from_secs(6);
const EXTERNAL_AGENT_RUNNER_START_TIMEOUT: Duration = Duration::from_secs(30);
const EXTERNAL_AGENT_RUNNER_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(2);
const EXTERNAL_AGENT_RUNNER_LOOP_INTERVAL: Duration = Duration::from_millis(25);
#[cfg(target_os = "linux")]
@@ -3957,6 +3957,11 @@ mod tests {
);
}
#[test]
fn runner_start_timeout_covers_cold_debug_binary_fingerprinting() {
assert!(EXTERNAL_AGENT_RUNNER_START_TIMEOUT >= Duration::from_secs(30));
}
#[test]
fn endpoint_reuse_requires_current_protocol_and_executable_identity() {
let current_fingerprint = "b".repeat(64);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -103,7 +103,24 @@ pub(crate) fn open_project_supervisor_chat_window(
#[cfg(debug_assertions)]
{
if let Some(existing) = app.get_webview_window("supervisor-chat") {
existing.close().map_err(|error| error.to_string())?;
let mut current_url = existing.url().map_err(|error| error.to_string())?;
let current_project_path = current_url
.query_pairs()
.find_map(|(key, value)| (key == "projectPath").then(|| value.into_owned()));
if current_project_path.as_deref() != Some(project_path) {
current_url.set_query(Some(&format!(
"supervisor-chat&projectPath={}",
percent_encode_query_value(project_path)
)));
current_url.set_fragment(None);
existing
.navigate(current_url)
.map_err(|error| error.to_string())?;
}
existing.show().map_err(|error| error.to_string())?;
existing.unminimize().map_err(|error| error.to_string())?;
existing.set_focus().map_err(|error| error.to_string())?;
return Ok(());
}
tauri::WebviewWindowBuilder::new(
&app,
+12 -2
View File
@@ -264,6 +264,8 @@ interface AgentRuntimeState {
sessionId: string;
runId: string;
source: string;
runProfile?: 'standard' | 'autonomous-game-build';
runProfileBindingFingerprint?: string;
parentAgentId?: string | null;
parentRunId?: string | null;
delegationId?: string | null;
@@ -336,6 +338,8 @@ interface AgentRuntimeContextCompactionResult {
}
interface AgentRuntimeToolPolicySnapshot {
runProfile?: 'standard' | 'autonomous-game-build';
runProfileBindingFingerprint?: string;
allowedTools: string[];
autoTools: string[];
confirmTools: string[];
@@ -421,6 +425,8 @@ interface AgentRuntimeEventRecord {
sessionId: string;
runId: string;
source: string;
runProfile?: 'standard' | 'autonomous-game-build';
runProfileBindingFingerprint?: string;
eventType: string;
status: string;
phase: string;
@@ -1562,12 +1568,14 @@ async function submitProjectSupervisorRuntimeTask({
sessionId,
prompt,
runtime,
runProfile,
}: {
invoke: TauriInvoke;
projectPath: string;
sessionId: string;
prompt: string;
runtime: AgentRuntimeState | null;
runProfile: 'standard' | 'autonomous-game-build';
}) {
const steerRuntime = matchingAgentRuntimeForSteer(
[runtime],
@@ -1589,13 +1597,13 @@ async function submitProjectSupervisorRuntimeTask({
return { mode: 'steer' as const, runtimeResult: steer.runtime };
}
const runtimeResult = await invoke<AgentRuntimeResult>(
'start_game_creator_agent_runtime_task',
'start_game_creator_supervisor_runtime_task',
{
projectPath,
agentId: PROJECT_SUPERVISOR_AGENT_ID,
sessionId,
task: prompt,
runId: createAgentChatRunId('project-supervisor-task'),
runProfile,
},
);
return { mode: 'start' as const, runtimeResult };
@@ -6833,6 +6841,7 @@ export function WorkspaceLauncher({
sessionId,
prompt: buildHomeConversationContent(mode, prompt, attachments),
runtime: null,
runProfile: 'autonomous-game-build',
});
setStatus('已创建项目并交给项目总控 Agent');
} catch (error) {
@@ -23215,6 +23224,7 @@ export function App({
sessionId,
prompt,
runtime: projectSupervisorRuntimeRef.current,
runProfile: supervisorChatOnly ? 'standard' : 'autonomous-game-build',
});
const runtimeResult = submission.runtimeResult;
setCommandLog((current) => [
@@ -253,6 +253,7 @@ function createProjectSupervisorRuntimeHarness({
initialRuntime,
initialResponseStream = null,
runtimeMapLoader,
expectedRunProfile = 'autonomous-game-build',
}: {
projectPath?: string;
sessionId?: string;
@@ -262,6 +263,7 @@ function createProjectSupervisorRuntimeHarness({
initialRuntime?: Record<string, unknown>;
initialResponseStream?: Record<string, unknown> | null;
runtimeMapLoader?: () => Promise<Array<Record<string, unknown>>>;
expectedRunProfile?: 'standard' | 'autonomous-game-build';
} = {}) {
const manifest = createGameCreationAppManifest(
'local-project-draft',
@@ -470,7 +472,10 @@ function createProjectSupervisorRuntimeHarness({
const states = runtimeMapLoader ? await runtimeMapLoader() : [];
return states.map((state) => runtimeResult(state));
}
if (command === 'start_game_creator_agent_runtime_task') {
if (command === 'start_game_creator_supervisor_runtime_task') {
if (args?.runProfile !== expectedRunProfile) {
throw new Error('unexpected Project Supervisor run profile');
}
const runId = String(args?.runId ?? '');
currentSupervisorMessages.push(
conversationRecord(
@@ -6859,15 +6864,15 @@ describe('AI 游戏创作 App 界面边界', () => {
title: '项目总控',
});
const startCalls = invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
expect(startCalls).toHaveLength(1);
expect(startCalls[0]?.[1]).toMatchObject({
projectPath: '/tmp/home-created-game',
agentId: 'project-supervisor',
sessionId: supervisorHarness.sessionId,
task: expect.stringContaining('初始意图:art / 做素材'),
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'autonomous-game-build',
});
expect(startCalls[0]?.[1]).toMatchObject({
task: expect.stringContaining(
@@ -6914,6 +6919,7 @@ describe('AI 游戏创作 App 界面边界', () => {
const historyMessage = '已持久化的项目总控历史';
const harness = createProjectSupervisorRuntimeHarness({
projectPath,
expectedRunProfile: 'standard',
supervisorMessages: [
{
schemaVersion: 'game-creator-conversation.v1',
@@ -6964,13 +6970,13 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'start_game_creator_agent_runtime_task',
'start_game_creator_supervisor_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
sessionId: harness.sessionId,
task: '继续完成可玩原型',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'standard',
},
);
});
@@ -7320,18 +7326,18 @@ describe('AI 游戏创作 App 界面边界', () => {
);
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_agent_runtime_task',
'start_game_creator_supervisor_runtime_task',
{
projectPath,
agentId: 'project-supervisor',
sessionId: supervisorHarness.sessionId,
task: '先完成正式客户端玩法拆解',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'autonomous-game-build',
},
);
});
const startCall = invoke.mock.calls.find(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const runId = String(startCall?.[1]?.runId ?? '');
await waitFor(() => {
@@ -7365,7 +7371,7 @@ describe('AI 游戏创作 App 界面边界', () => {
});
expect(
invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
expect(
@@ -27653,13 +27659,13 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(harness.invoke).toHaveBeenCalledWith(
'start_game_creator_agent_runtime_task',
'start_game_creator_supervisor_runtime_task',
{
projectPath: harness.projectPath,
agentId: 'project-supervisor',
sessionId: harness.sessionId,
task: '做一个反弹弹幕厨房游戏',
runId: expect.stringMatching(/^project-supervisor-task-/),
runProfile: 'autonomous-game-build',
},
);
});
@@ -27896,12 +27902,12 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const startCall = harness.invoke.mock.calls.find(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const newRunId = String(startCall?.[1]?.runId ?? '');
expect(newRunId).not.toBe('');
@@ -28033,12 +28039,12 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const startCall = harness.invoke.mock.calls.find(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const startedRunId = String(startCall?.[1]?.runId ?? '');
await waitFor(() => {
@@ -28065,7 +28071,7 @@ describe('AI 游戏创作 App 界面边界', () => {
});
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
@@ -28635,12 +28641,12 @@ describe('AI 游戏创作 App 界面边界', () => {
await waitFor(() => {
expect(
harness.invoke.mock.calls.filter(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
),
).toHaveLength(1);
});
const startCall = harness.invoke.mock.calls.find(
([command]) => command === 'start_game_creator_agent_runtime_task',
([command]) => command === 'start_game_creator_supervisor_runtime_task',
);
const runId = String(startCall?.[1]?.runId ?? '');
const respondingRuntime = harness.runtimeState({
@@ -5132,9 +5132,9 @@
## 2026-07-20 开发态 Project Supervisor 纯聊天独立窗口
- 背景:开发人员需要一个不依赖正式产品布局的最小 GUI,用于直接验证 `project-supervisor` 的持久多轮对话与 Runtime 行为。
- 入口决策:仅 debug 构建提供独立 Tauri 窗口,使用 `index.html?supervisor-chat&projectPath=...` 路由,并从现有 `index.html?agent-chat` 开发入口打开;`projectPath` 是 URL 编码后的项目绝对路径。
- 复用边界:窗口固定使用 `project-supervisor`,继续复用现有 active Session、External Runner、Agent Runtime、AppData 配置与持久 conversation;不创建新聊天后端、本地 HTTP 服务、数据库或平行配置体系。
- UI 边界:只显示持久消息区、输入框、必要的等待 / 错误状态和设置;不显示 Agent picker、Session / Goal / Runtime 面板或专业 Agent 协作栏。Session 控制面可隐藏,但对话仍按 `project-supervisor` active Session 持久化。
- 入口决策:当前仅 Tauri dev 提供独立窗口,使用 `index.html?supervisor-chat&projectPath=...` 路由,并从现有 `index.html?agent-chat` 开发入口打开;`projectPath` 是 URL 编码后的项目绝对路径。重复打开同一项目只恢复并聚焦原窗口,切换项目时在同一窗口导航,不销毁未发送输入。
- 复用边界:窗口固定使用 `project-supervisor`新 Run 固定选择 `standard` profile继续复用现有 active Session、External Runner、Agent Runtime、AppData 配置与持久 conversation;不创建新聊天后端、本地 HTTP 服务、数据库或平行配置体系,也不继承正式构建流程的自主交付 profile
- UI 边界:只显示持久消息区、输入框、必要的等待 / 错误状态、工具确认 / 用户追问卡片和设置;不显示 Agent picker、Session / Goal / 完整 Runtime 面板或专业 Agent 协作栏。Session 控制面可隐藏,但对话仍按 `project-supervisor` active Session 持久化`supervisor-chat` 纳入只读 Tauri event capability,保证同一 Runtime 的状态更新可实时到达窗口
- 产品边界:该窗口只是开发验证入口,不替换、不修改正式用户 `client` 窗口及其登录、首页和项目开发流程。
## 2026-07-18 AI 游戏创作项目工作台 Runtime 状态投影
@@ -16,9 +16,9 @@
## 开发态 Project Supervisor 纯聊天独立窗口
- 入口边界:仅 debug 构建提供独立 Tauri 窗口,路由为 `index.html?supervisor-chat&projectPath=...`,其中 `projectPath` 传入 URL 编码后的项目绝对路径。窗口从现有 `index.html?agent-chat` 开发入口打开,不增加正式用户入口。
- Runtime 边界:窗口固定对话 Agent 为 `project-supervisor`,复用现有 active Session、External Runner、Agent Runtime、AppData 配置和持久 conversation;不新建平行会话库、Runner 或配置存储。
- 界面边界:只显示持久消息区、输入框、必要的等待 / 错误状态和设置入口;不显示 Agent picker、Session 面板、Goal 面板、Runtime 面板或专业 Agent 协作栏。会话历史仍绑定 `project-supervisor` 的 active Session 持久化,不因隐藏 Session 控制面而变为临时聊天。
- 入口边界:当前仅 Tauri dev 提供独立窗口,路由为 `index.html?supervisor-chat&projectPath=...`,其中 `projectPath` 传入 URL 编码后的项目绝对路径。窗口从现有 `index.html?agent-chat` 开发入口打开,不增加正式用户入口;重复打开同一项目只恢复并聚焦原窗口,切换项目时在同一窗口导航
- Runtime 边界:窗口固定对话 Agent 为 `project-supervisor`新 Run 使用 `standard` profile复用现有 active Session、External Runner、Agent Runtime、AppData 配置和持久 conversation;不新建平行会话库、Runner 或配置存储,也不把该测试入口隐式切成自动修改项目的自主构建模式
- 界面边界:只显示持久消息区、输入框、必要的等待 / 错误状态、工具确认 / 用户追问卡片和设置入口;不显示 Agent picker、Session 面板、Goal 面板、完整 Runtime 面板或专业 Agent 协作栏。会话历史仍绑定 `project-supervisor` 的 active Session 持久化,不因隐藏 Session 控制面而变为临时聊天`supervisor-chat` 窗口必须具备只读 Tauri event listen / unlisten capability,以实时接收 Runtime 更新
- 产品边界:正式用户 `client` 窗口、登录后首页和项目开发流程保持不变,不暴露该开发验证面。
## Runtime 边界
+1
View File
@@ -147,6 +147,7 @@
"ai-game-creator-shell:agent-run:smoke": "npm --prefix apps/ai-game-creator-shell run agent-run:smoke",
"ai-game-creator-shell:agent-runtime:real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:real-e2e --",
"ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-autonomous-chat-real-e2e --",
"ai-game-creator-shell:agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e --",
"ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-transient-retry-real-e2e --",
"ai-game-creator-shell:agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-final-reply-transient-retry-real-e2e --",
"ai-game-creator-shell:agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-tool-plan-handoff-runner-kill-real-e2e --",