修复 game-chat 续跑目标漂移与产物覆盖
继承同一会话失败根任务的原始目标与完成基线 保护非占位游戏入口并拒绝重复整文件覆盖 纠正规范图运行时语义与状态序列推进规则 补充续跑继承、图集门禁和事故文档回归
This commit is contained in:
+15
-11
@@ -2,7 +2,7 @@ use super::*;
|
||||
|
||||
const AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL: &str = "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,本轮禁止直接调用 respond_to_user,也禁止在 legacy response 中填写最终回复;必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且完成门禁不再阻断后,才能给最终回复;不得反复提交 final response,也不得按项目正文硬编码某一种 blocker 的处理方式。";
|
||||
|
||||
const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。当前任务的第一目标是在一次 Provider planning 内产出首个完整可玩版本:如果最新 observation 尚未显示 game/index.html 已由本 run 写入,本响应必须直接调用一次 file.write,把完整、自包含、可运行的 game/index.html 一次写完;禁止先调用读取、搜索、任务查询、委派、只更新计划或提交半成品。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局;可以采用保守的原创玩法默认值。已登记的平台视觉规范图 ../assets/art-spec.png 是首版必需资源,必须在主要游戏画面中显著可见使用:至少把规范图实际绘制为主要背景,并从规范图中绘制玩家角色和目标实体。禁止仅放置隐藏 img、透明或屏外元素、微小水印、不可见预加载或只在源码中引用;也禁止用纯几何图形冒充平台图片使用。若规范图无法加载,游戏必须明确失败关闭,不能退回纯 Canvas 几何兜底。一次写入后不要继续扩写功能;Runtime 会在下一步自动执行静态自检并在通过后立即试玩。";
|
||||
const GAME_CHAT_CODE_PROTOTYPE_FAST_PATH_PROMPT: &str = "game-chat 首版使用五分钟快车道。必须先保护既有游戏语义和已完成产物:如果已有 observation 尚未包含 game/index.html 的当前正文与摘要,本响应第一步必须调用 file.read(path=game/index.html),不得猜测入口仍是初始化占位。只有文件缺失或正文与初始化页面一致、且当前 run 尚未写入项目时,才允许用一次 file.write 生成完整、自包含、可运行的 game/index.html;检测到非占位入口、已有可玩实现、当前 revision 已验证,或当前 run 已发生修改时,禁止整文件 file.write,必须保留原玩法,优先执行静态检查/试玩,确需修复时只能依据已读取的精确原文做最小 file.patch。不得把“继续”、continue 或其它纯续跑词当作游戏主题;缺少可继承的具体目标时必须失败关闭,不得另造收集能量等新玩法。HTML 必须满足下方固定试玩合同,包含真实 Canvas 游戏循环、键盘与触控输入、开始、主要操作、重开、胜负状态和移动端布局。../assets/art-spec.png 是视觉规范图,只能用于理解风格、构图与色板,禁止把整张规范图铺成游戏背景,禁止从中裁切或绘制玩家、目标和场景实体,也不能把它当作运行时必需图片;只有存在已登记的独立派生 spritesheet 时才可加载该派生素材,否则使用与规范一致的自包含原创 Canvas 视觉,并保证规范图加载失败不影响游戏启动。首次实现或最小修复后不要继续扩写功能;Runtime 会自动执行静态自检并在通过后立即试玩。";
|
||||
|
||||
fn game_chat_fast_path_prompt_for_root_source(
|
||||
agent_id: &str,
|
||||
@@ -507,7 +507,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_fast_path_prompt_forces_one_shot_playable_write_only_for_code_agent() {
|
||||
fn game_chat_fast_path_prompt_protects_existing_game_and_treats_art_spec_as_reference() {
|
||||
let prompt = game_chat_fast_path_prompt_for_root_source(
|
||||
"code-prototype",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
@@ -515,16 +515,20 @@ mod tests {
|
||||
.expect("game-chat code fast path prompt");
|
||||
|
||||
assert!(prompt.contains("五分钟快车道"));
|
||||
assert!(prompt.contains("一次 Provider planning"));
|
||||
assert!(prompt.contains("直接调用一次 file.write"));
|
||||
assert!(prompt.contains("禁止先调用读取、搜索、任务查询、委派"));
|
||||
assert!(prompt.contains("第一步必须调用 file.read(path=game/index.html)"));
|
||||
assert!(prompt.contains("正文与初始化页面一致"));
|
||||
assert!(prompt.contains("当前 run 尚未写入项目"));
|
||||
assert!(prompt.contains("检测到非占位入口"));
|
||||
assert!(prompt.contains("禁止整文件 file.write"));
|
||||
assert!(prompt.contains("最小 file.patch"));
|
||||
assert!(prompt.contains("不得把“继续”"));
|
||||
assert!(prompt.contains("../assets/art-spec.png"));
|
||||
assert!(prompt.contains("平台视觉规范图"));
|
||||
assert!(prompt.contains("主要背景"));
|
||||
assert!(prompt.contains("玩家角色和目标实体"));
|
||||
assert!(prompt.contains("禁止仅放置隐藏 img"));
|
||||
assert!(prompt.contains("不能退回纯 Canvas 几何兜底"));
|
||||
assert!(!prompt.contains("art-spritesheet.png"));
|
||||
assert!(prompt.contains("只能用于理解风格、构图与色板"));
|
||||
assert!(prompt.contains("禁止把整张规范图铺成游戏背景"));
|
||||
assert!(prompt.contains("禁止从中裁切或绘制玩家、目标和场景实体"));
|
||||
assert!(prompt.contains("自包含原创 Canvas 视觉"));
|
||||
assert!(prompt.contains("规范图加载失败不影响游戏启动"));
|
||||
assert!(prompt.contains("独立派生 spritesheet"));
|
||||
assert!(game_chat_fast_path_prompt_for_root_source(
|
||||
"quality-review",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
|
||||
@@ -432,6 +432,24 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_with_session_filter_at(
|
||||
}
|
||||
}
|
||||
}
|
||||
if !state.run_id.trim().is_empty() && !state.current_task.trim().is_empty() {
|
||||
match autonomous_effective_root_task_at(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
) {
|
||||
Ok(effective_task) => state.current_task = effective_task,
|
||||
Err(error) => {
|
||||
state.status = "failed".to_string();
|
||||
state.phase = "needs-reconciliation".to_string();
|
||||
state.current_action = "自主构建续跑语义需要人工核对".to_string();
|
||||
state.waiting_on = "开发者核对上一根 Run 与完成合同".to_string();
|
||||
state.next_step = "修复续跑身份后恢复当前 run".to_string();
|
||||
state.error = Some(sanitize_agent_runtime_text(&error, 500));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(error) = hydrate_game_creator_agent_goal_state_at(root, &mut state) {
|
||||
state.status = "failed".to_string();
|
||||
state.phase = "needs-reconciliation".to_string();
|
||||
|
||||
+349
-134
File diff suppressed because it is too large
Load Diff
@@ -1330,7 +1330,7 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt(
|
||||
let code_visual_asset_requirement = if task.id == "code-prototype"
|
||||
&& editor_api_key_is_configured()
|
||||
{
|
||||
" External Editor API 已配置时必须先调用 asset.list 核对 Canvas 登记与资源有效性:game-chat Run 使用 assets/art-spec.png(icon-spec、images/generations/spec),GUI/CLI Run 使用 assets/art-spritesheet.png;game/index.html 必须通过 HTML、CSS background 或 Canvas drawImage 可见使用对应资源,不得只用 emoji、色块、CSS 绘图或占位文本冒充。"
|
||||
" External Editor API 已配置时必须先调用 asset.list 核对 Canvas 登记与资源有效性:GUI/CLI Run 使用独立派生的 assets/art-spritesheet.png,并在 game/index.html 中通过 HTML、CSS background 或 Canvas drawImage 真实可见使用;game-chat Run 的 assets/art-spec.png 只是视觉规范参考,禁止把整张规范图铺成背景或从中裁切玩家、目标和场景实体,没有独立派生图集时应使用与规范一致的自包含原创 Canvas 视觉。"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
+197
-21
@@ -392,8 +392,12 @@ fn autonomous_code_prototype_art_asset_reference_gap_at(
|
||||
let Some(required_visual_task_id) = required_visual_task_id else {
|
||||
return Ok(None);
|
||||
};
|
||||
if required_visual_task_id == "art-director" {
|
||||
// game-chat still requires art-director to deliver a valid visual specification,
|
||||
// but the specification is a design reference rather than a runtime sprite/background.
|
||||
return Ok(None);
|
||||
}
|
||||
let (asset_path, asset_label) = match required_visual_task_id {
|
||||
"art-director" => ("assets/art-spec.png", "art-spec"),
|
||||
"art-asset-plan" => ("assets/art-spritesheet.png", "art-spritesheet"),
|
||||
_ => {
|
||||
return Err(format!(
|
||||
@@ -430,12 +434,7 @@ fn autonomous_code_prototype_art_asset_reference_gap_at(
|
||||
"game/index.html(missing-visible-{asset_label}-use:{asset_path})"
|
||||
)));
|
||||
};
|
||||
if !game_index_visibly_uses_visual_asset(
|
||||
&html,
|
||||
asset_path,
|
||||
asset_dimensions,
|
||||
required_visual_task_id == "art-director",
|
||||
) {
|
||||
if !game_index_visibly_uses_visual_asset(&html, asset_path, asset_dimensions, false) {
|
||||
return Ok(Some(format!(
|
||||
"game/index.html(missing-visible-{asset_label}-use:{asset_path})"
|
||||
)));
|
||||
@@ -1813,6 +1812,148 @@ pub(in crate::agent) fn read_autonomous_completion_contract(
|
||||
Ok(contract)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn is_pure_autonomous_continuation_intent(task: &str) -> bool {
|
||||
let normalized = task
|
||||
.trim()
|
||||
.trim_matches(|character: char| {
|
||||
matches!(
|
||||
character,
|
||||
'。' | '!' | '?' | ',' | '.' | '!' | '?' | ',' | '~' | '~'
|
||||
)
|
||||
})
|
||||
.chars()
|
||||
.filter(|character| !character.is_whitespace())
|
||||
.collect::<String>();
|
||||
matches!(
|
||||
normalized.as_str(),
|
||||
"继续"
|
||||
| "继续吧"
|
||||
| "请继续"
|
||||
| "继续做"
|
||||
| "继续做吧"
|
||||
| "请继续做"
|
||||
| "请继续做吧"
|
||||
| "继续完成"
|
||||
| "继续完成吧"
|
||||
| "请继续完成"
|
||||
| "请继续完成吧"
|
||||
| "接着做"
|
||||
| "接着做吧"
|
||||
| "请接着做"
|
||||
| "请接着做吧"
|
||||
| "接着完成"
|
||||
| "接着完成吧"
|
||||
| "请接着完成"
|
||||
| "请接着完成吧"
|
||||
| "继续之前的任务"
|
||||
| "继续完成之前的任务"
|
||||
| "接着完成之前的任务"
|
||||
)
|
||||
}
|
||||
|
||||
struct AutonomousContinuationInheritance {
|
||||
contract: AgentRuntimeAutonomousCompletionContract,
|
||||
task: String,
|
||||
}
|
||||
|
||||
fn failed_terminal_autonomous_root_contract_before_task_at(
|
||||
root: &Path,
|
||||
task: &AgentRuntimeTaskRecord,
|
||||
) -> Result<Option<AutonomousContinuationInheritance>, String> {
|
||||
if task.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
|| !is_pure_autonomous_continuation_intent(&task.task)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let records = read_all_game_creator_agent_runtime_tasks(
|
||||
&game_creator_agent_runtime_task_path(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
)?;
|
||||
let mut ordered_run_ids = Vec::new();
|
||||
let mut seen_run_ids = BTreeSet::new();
|
||||
for record in &records {
|
||||
if record.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& record.session_id == task.session_id
|
||||
&& record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& record.parent_agent_id.is_none()
|
||||
&& record.parent_run_id.is_none()
|
||||
&& agent_runtime_supervisor_source_is_trusted(&record.source)
|
||||
&& seen_run_ids.insert(record.run_id.clone())
|
||||
{
|
||||
ordered_run_ids.push(record.run_id.clone());
|
||||
}
|
||||
}
|
||||
let Some(current_index) = ordered_run_ids
|
||||
.iter()
|
||||
.position(|run_id| run_id == &task.run_id)
|
||||
else {
|
||||
return Err("自主构建续跑任务未出现在当前 Session 的根 Run journal 中".to_string());
|
||||
};
|
||||
let Some(previous_run_id) = current_index
|
||||
.checked_sub(1)
|
||||
.and_then(|index| ordered_run_ids.get(index))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let previous = latest_game_creator_agent_runtime_tasks(records)
|
||||
.into_iter()
|
||||
.find(|record| record.run_id == *previous_run_id)
|
||||
.ok_or_else(|| "自主构建续跑缺少上一根 Run 的最新 journal 投影".to_string())?;
|
||||
if !matches!(
|
||||
game_creator_agent_runtime_terminal_status(&previous),
|
||||
Some("failed" | "cancelled" | "budget-exhausted")
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(contract) = read_autonomous_completion_contract(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&previous.run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let previous_task_sha256 = format!("{:x}", Sha256::digest(previous.task.as_bytes()));
|
||||
if contract.task_sha256 != previous_task_sha256 {
|
||||
return Err("自主构建续跑的上一根 Run 任务语义与完成合同不一致".to_string());
|
||||
}
|
||||
Ok(Some(AutonomousContinuationInheritance {
|
||||
contract,
|
||||
task: previous.task,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn autonomous_effective_root_task_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
fallback_task: &str,
|
||||
) -> Result<String, String> {
|
||||
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
return Ok(fallback_task.to_string());
|
||||
}
|
||||
let Some(task) = read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)?
|
||||
else {
|
||||
return Ok(fallback_task.to_string());
|
||||
};
|
||||
let Some(current_contract) = read_autonomous_completion_contract(root, agent_id, run_id)?
|
||||
else {
|
||||
return Ok(fallback_task.to_string());
|
||||
};
|
||||
let journal_task_sha256 = format!("{:x}", Sha256::digest(task.task.as_bytes()));
|
||||
if current_contract.task_sha256 == journal_task_sha256 {
|
||||
return Ok(task.task);
|
||||
}
|
||||
let Some(inherited) = failed_terminal_autonomous_root_contract_before_task_at(root, &task)?
|
||||
else {
|
||||
return Err("自主构建根 Run 的任务语义与完成合同不一致".to_string());
|
||||
};
|
||||
if current_contract.task_sha256 != inherited.contract.task_sha256 {
|
||||
return Err("自主构建续跑的新旧完成合同任务语义不一致".to_string());
|
||||
}
|
||||
Ok(inherited.task)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn ensure_autonomous_completion_contract_for_task_at(
|
||||
root: &Path,
|
||||
task: &AgentRuntimeTaskRecord,
|
||||
@@ -1827,36 +1968,56 @@ pub(in crate::agent) fn ensure_autonomous_completion_contract_for_task_at(
|
||||
{
|
||||
return Err("自主构建完成合同只允许可信根 Supervisor Run".to_string());
|
||||
}
|
||||
let inherited = failed_terminal_autonomous_root_contract_before_task_at(root, task)?;
|
||||
let expected_task_sha256 = inherited
|
||||
.as_ref()
|
||||
.map(|inherited| inherited.contract.task_sha256.clone())
|
||||
.unwrap_or_else(|| format!("{:x}", Sha256::digest(task.task.as_bytes())));
|
||||
if let Some(existing) = read_autonomous_completion_contract(root, &task.agent_id, &task.run_id)?
|
||||
{
|
||||
if existing.run_profile_binding_fingerprint != task.run_profile_binding_fingerprint
|
||||
|| existing.task_sha256 != format!("{:x}", Sha256::digest(task.task.as_bytes()))
|
||||
|| existing.task_sha256 != expected_task_sha256
|
||||
{
|
||||
return Err("自主构建完成合同 CAS 冲突".to_string());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
reset_autonomous_manifest_seed_tasks_at(root, task)?;
|
||||
let revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let baseline_index_sha256 = read_autonomous_evidence_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"自主构建初始游戏入口",
|
||||
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
|
||||
)?
|
||||
.map(|(digest, _)| digest.sha256);
|
||||
let baseline_artifacts = autonomous_manifest_baseline_artifacts_at(root)?;
|
||||
let (baseline_revision, baseline_index_sha256, baseline_artifacts, playtest_scenario) =
|
||||
if let Some(inherited) = inherited.as_ref() {
|
||||
(
|
||||
inherited.contract.baseline_revision,
|
||||
inherited.contract.baseline_index_sha256.clone(),
|
||||
inherited.contract.baseline_artifacts.clone(),
|
||||
inherited.contract.playtest_scenario.clone(),
|
||||
)
|
||||
} else {
|
||||
reset_autonomous_manifest_seed_tasks_at(root, task)?;
|
||||
let revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let baseline_index_sha256 = read_autonomous_evidence_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"自主构建初始游戏入口",
|
||||
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
|
||||
)?
|
||||
.map(|(digest, _)| digest.sha256);
|
||||
(
|
||||
revision.revision,
|
||||
baseline_index_sha256,
|
||||
autonomous_manifest_baseline_artifacts_at(root)?,
|
||||
classify_autonomous_playtest_scenario(&task.task),
|
||||
)
|
||||
};
|
||||
let mut contract = AgentRuntimeAutonomousCompletionContract {
|
||||
schema_version: AGENT_RUNTIME_AUTONOMOUS_COMPLETION_CONTRACT_SCHEMA_VERSION.to_string(),
|
||||
project_id: game_creator_agent_runtime_context_project_id(root)?,
|
||||
agent_id: task.agent_id.clone(),
|
||||
run_id: task.run_id.clone(),
|
||||
run_profile_binding_fingerprint: task.run_profile_binding_fingerprint.clone(),
|
||||
task_sha256: format!("{:x}", Sha256::digest(task.task.as_bytes())),
|
||||
baseline_revision: revision.revision,
|
||||
task_sha256: expected_task_sha256,
|
||||
baseline_revision,
|
||||
baseline_index_sha256,
|
||||
baseline_artifacts,
|
||||
playtest_scenario: classify_autonomous_playtest_scenario(&task.task),
|
||||
playtest_scenario,
|
||||
contract_fingerprint: String::new(),
|
||||
created_at: unix_timestamp(),
|
||||
};
|
||||
@@ -1869,6 +2030,21 @@ pub(in crate::agent) fn ensure_autonomous_completion_contract_for_task_at(
|
||||
if persisted != contract {
|
||||
return Err("自主构建完成合同写入后回读不一致".to_string());
|
||||
}
|
||||
if let Some(inherited) = inherited {
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.autonomous_continuation.inherited",
|
||||
"agentId": task.agent_id,
|
||||
"sessionId": task.session_id,
|
||||
"runId": task.run_id,
|
||||
"previousRunId": inherited.contract.run_id,
|
||||
"taskSha256": contract.task_sha256,
|
||||
"baselineRevision": contract.baseline_revision,
|
||||
"contractFingerprint": contract.contract_fingerprint,
|
||||
}),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+240
-126
@@ -514,6 +514,235 @@ fn autonomous_completion_contract_freezes_baseline_and_scenario() {
|
||||
.expect("completion contract is idempotent");
|
||||
}
|
||||
|
||||
fn append_failed_autonomous_root_projection(
|
||||
root: &Path,
|
||||
record: &AgentRuntimeTaskRecord,
|
||||
phase: &str,
|
||||
) {
|
||||
append_game_creator_agent_runtime_task_record(
|
||||
root,
|
||||
&AgentRuntimeTaskRecord {
|
||||
status: "failed".to_string(),
|
||||
phase: phase.to_string(),
|
||||
current_action: "测试中的自主构建已失败".to_string(),
|
||||
terminal_detail: Some(format!("terminal phase={phase}")),
|
||||
error: Some(format!("terminal phase={phase}")),
|
||||
updated_at: unix_timestamp(),
|
||||
..record.clone()
|
||||
},
|
||||
)
|
||||
.expect("append failed autonomous root projection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_continuation_intent_is_exact_and_does_not_swallow_new_requirements() {
|
||||
for task in ["继续", " 继续完成。 ", "接着做", "请继续完成吧!"] {
|
||||
assert!(is_pure_autonomous_continuation_intent(task), "task={task}");
|
||||
}
|
||||
for task in [
|
||||
"继续,但改成一个收集能量的游戏",
|
||||
"继续做俄罗斯方块,不过换成三消玩法",
|
||||
"做一个全新的俄罗斯方块",
|
||||
] {
|
||||
assert!(!is_pure_autonomous_continuation_intent(task), "task={task}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress() {
|
||||
let original_task = "做一个水晶主题的俄罗斯方块,完成移动、旋转、消行和重开";
|
||||
let (_temporary, root, original_state, original_contract) =
|
||||
autonomous_fixture(original_task, "crystal-tetris-original-run");
|
||||
let original_record = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&original_state.run_id,
|
||||
)
|
||||
.expect("read original tetris root")
|
||||
.expect("original tetris root exists");
|
||||
append_failed_autonomous_root_projection(&root, &original_record, "budget-exhausted");
|
||||
|
||||
let continuation = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&original_state.session_id,
|
||||
"继续完成。",
|
||||
"crystal-tetris-continuation-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue tetris continuation");
|
||||
let continuation_contract = read_autonomous_completion_contract(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&continuation.run_id,
|
||||
)
|
||||
.expect("read continuation contract")
|
||||
.expect("continuation contract exists");
|
||||
|
||||
assert_eq!(
|
||||
continuation_contract.task_sha256, original_contract.task_sha256,
|
||||
"the continuation contract must retain the original game semantics"
|
||||
);
|
||||
assert_eq!(
|
||||
continuation_contract.baseline_revision,
|
||||
original_contract.baseline_revision
|
||||
);
|
||||
assert_eq!(
|
||||
continuation_contract.baseline_index_sha256,
|
||||
original_contract.baseline_index_sha256
|
||||
);
|
||||
assert_eq!(
|
||||
continuation_contract.baseline_artifacts,
|
||||
original_contract.baseline_artifacts
|
||||
);
|
||||
assert_eq!(
|
||||
continuation_contract.playtest_scenario,
|
||||
original_contract.playtest_scenario
|
||||
);
|
||||
assert_ne!(continuation_contract.run_id, original_contract.run_id);
|
||||
assert_ne!(
|
||||
continuation_contract.run_profile_binding_fingerprint,
|
||||
original_contract.run_profile_binding_fingerprint
|
||||
);
|
||||
assert_ne!(
|
||||
continuation_contract.contract_fingerprint,
|
||||
original_contract.contract_fingerprint
|
||||
);
|
||||
assert_eq!(
|
||||
autonomous_effective_root_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&continuation.run_id,
|
||||
&continuation.task,
|
||||
)
|
||||
.expect("resolve continued root semantics"),
|
||||
original_task
|
||||
);
|
||||
assert!(read_manifest_for_project(&root)
|
||||
.expect("read continued tetris manifest")
|
||||
.tasks
|
||||
.iter()
|
||||
.all(|task| task.status == GameCreationAppTaskStatus::Completed));
|
||||
|
||||
ensure_autonomous_completion_contract_for_task_at(&root, &continuation)
|
||||
.expect("continued completion contract is idempotent");
|
||||
assert_eq!(
|
||||
read_autonomous_completion_contract(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&continuation.run_id,
|
||||
)
|
||||
.expect("reread continued contract")
|
||||
.expect("continued contract remains present"),
|
||||
continuation_contract
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_detailed_new_request_after_failure_resets_manifest() {
|
||||
let (_temporary, root, original_state, original_contract) =
|
||||
autonomous_fixture("做一个水晶主题的俄罗斯方块", "new-request-original-run");
|
||||
let original_record = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&original_state.run_id,
|
||||
)
|
||||
.expect("read original root")
|
||||
.expect("original root exists");
|
||||
append_failed_autonomous_root_projection(&root, &original_record, "failed");
|
||||
|
||||
let replacement_task = "继续,但改成一个收集能量的全新游戏";
|
||||
let replacement = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&original_state.session_id,
|
||||
replacement_task,
|
||||
"new-request-replacement-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue detailed replacement request");
|
||||
let replacement_contract = read_autonomous_completion_contract(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&replacement.run_id,
|
||||
)
|
||||
.expect("read replacement contract")
|
||||
.expect("replacement contract exists");
|
||||
|
||||
assert_eq!(
|
||||
replacement_contract.task_sha256,
|
||||
format!("{:x}", Sha256::digest(replacement_task.as_bytes()))
|
||||
);
|
||||
assert_ne!(
|
||||
replacement_contract.task_sha256,
|
||||
original_contract.task_sha256
|
||||
);
|
||||
assert!(read_manifest_for_project(&root)
|
||||
.expect("read reset replacement manifest")
|
||||
.tasks
|
||||
.iter()
|
||||
.all(|task| task.status == GameCreationAppTaskStatus::Pending));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_pure_continue_does_not_inherit_across_sessions() {
|
||||
let (_temporary, root, original_state, original_contract) =
|
||||
autonomous_fixture("做一个水晶主题的俄罗斯方块", "cross-session-original-run");
|
||||
let original_record = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&original_state.run_id,
|
||||
)
|
||||
.expect("read cross-session original root")
|
||||
.expect("cross-session original root exists");
|
||||
append_failed_autonomous_root_projection(&root, &original_record, "failed");
|
||||
let other_session_id = "project-supervisor-cross-session";
|
||||
ensure_agent_conversation_session_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
other_session_id,
|
||||
"另一条 Supervisor 会话",
|
||||
)
|
||||
.expect("create another Supervisor session");
|
||||
|
||||
let continuation = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
other_session_id,
|
||||
"继续",
|
||||
"cross-session-continuation-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue cross-session continuation");
|
||||
let continuation_contract = read_autonomous_completion_contract(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&continuation.run_id,
|
||||
)
|
||||
.expect("read cross-session continuation contract")
|
||||
.expect("cross-session continuation contract exists");
|
||||
|
||||
assert_eq!(
|
||||
continuation_contract.task_sha256,
|
||||
format!("{:x}", Sha256::digest("继续".as_bytes()))
|
||||
);
|
||||
assert_ne!(
|
||||
continuation_contract.task_sha256,
|
||||
original_contract.task_sha256
|
||||
);
|
||||
assert!(read_manifest_for_project(&root)
|
||||
.expect("read cross-session reset manifest")
|
||||
.tasks
|
||||
.iter()
|
||||
.all(|task| task.status == GameCreationAppTaskStatus::Pending));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_completion_contract_recovery_rejects_v1_and_missing_baseline_artifacts() {
|
||||
let (_temporary, root, state, contract) = autonomous_fixture(
|
||||
@@ -1113,7 +1342,7 @@ fn game_chat_parent_completion_stops_after_preview_playtest_without_publish_task
|
||||
let revision = advance_game_index_revision(
|
||||
&root,
|
||||
&state,
|
||||
"<!doctype html><html><body><img id=\"art\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const image=document.getElementById('art');context.drawImage(image,0,0,canvas.width,canvas.height);context.drawImage(image,0,0,96,96);context.drawImage(image,120,80,112,112);</script></body></html>",
|
||||
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillStyle='#42d9d0';context.fillRect(0,0,96,96);</script></body></html>",
|
||||
);
|
||||
mark_verification_passed(&root, &state, "game.static_smoke");
|
||||
let result = browser_result_fixture(&root, &state, revision, contract.playtest_scenario);
|
||||
@@ -1172,7 +1401,7 @@ fn game_chat_schedule_ready_tool_cannot_bypass_the_single_round_publish_boundary
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_code_prototype_requires_registered_canvas_art_spec_visible_use() {
|
||||
fn game_chat_code_prototype_treats_art_spec_as_reference_not_runtime_asset() {
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"editorApi":{"apiKey":"game-chat-art-gate-key"}}"#.to_string(),
|
||||
);
|
||||
@@ -1189,122 +1418,12 @@ fn game_chat_code_prototype_requires_registered_canvas_art_spec_visible_use() {
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><canvas></canvas></body></html>",
|
||||
);
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
|
||||
.expect("missing art-spec reference must block code prototype");
|
||||
assert!(blocker
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("assets/art-spec.png")));
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img src=\"assets/art-spec.png\"><canvas></canvas></body></html>",
|
||||
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
|
||||
);
|
||||
assert!(
|
||||
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some(),
|
||||
"URL relative to game/index.html must resolve to the registered asset"
|
||||
autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none(),
|
||||
"game-chat code must not require the visual specification in the runtime DOM or Canvas"
|
||||
);
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><canvas></canvas><script>const unused = '../assets/art-spec.png';</script></body></html>",
|
||||
);
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
|
||||
.expect("an unused art-spec string must not satisfy visible-use validation");
|
||||
assert!(blocker
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("missing-visible-art-spec-use")));
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><canvas></canvas><script>const fake = '<img src=\"../assets/art-spec.png\">';</script></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><style>.hidden-canvas{display:none}</style><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas class=\"hidden-canvas\"></canvas><script>const art=document.getElementById('preload');context.drawImage(art,0,0,canvas.width,canvas.height);context.drawImage(art,0,0,96,96);context.drawImage(art,120,80,112,112);</script>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload');function never(){context.drawImage(art,0,0,canvas.width,canvas.height);context.drawImage(art,0,0,96,96);context.drawImage(art,120,80,112,112);}</script>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload');function draw(){context.drawImage(art,0,0,canvas.width,canvas.height);context.drawImage(art,0,0,96,96);context.drawImage(art,120,80,112,112);}requestAnimationFrame(draw);</script>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img hidden src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
for hidden_html in [
|
||||
"<!doctype html><img width=\"1\" height=\"1\" src=\"../assets/art-spec.png\"><canvas></canvas>",
|
||||
"<!doctype html><img style=\"opacity:0.05\" src=\"../assets/art-spec.png\"><canvas></canvas>",
|
||||
"<!doctype html><img style=\"position:absolute;left:-9999px\" src=\"../assets/art-spec.png\"><canvas></canvas>",
|
||||
] {
|
||||
advance_game_index_revision(&root, &code_state, hidden_html);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
}
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><head><style>.hero{width:96px;height:96px;background-image:url('../assets/art-spec.png')}</style></head><body><div class=\"hero\"></div><canvas></canvas></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><canvas></canvas><script>const art=new Image(); art\n .src =\n '../assets/art-spec.png'; context.drawImage(art,0,0);</script></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><canvas></canvas><script>const art=new Image(); art.src='../assets/art-spec.png'; context.drawImage(art,0,0,1,1);</script></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload'); context.drawImage(art,0,0,96,96);</script></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img id=\"preload\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const art=document.getElementById('preload'); context.drawImage(art,0,0,canvas.width,canvas.height); context.drawImage(art,0,0,96,96); context.drawImage(art,120,80,112,112);</script></body></html>",
|
||||
);
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1340,10 +1459,10 @@ fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_co
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_requires_canvas_art_spec_even_without_editor_configuration() {
|
||||
fn game_chat_code_prototype_does_not_require_art_spec_without_editor_configuration() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
|
||||
"创建一轮必须使用平台美术的小游戏",
|
||||
"创建一轮程序化水晶小游戏",
|
||||
"game-chat-unconfigured-art-gate-parent",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
);
|
||||
@@ -1352,19 +1471,14 @@ fn game_chat_requires_canvas_art_spec_even_without_editor_configuration() {
|
||||
let code_record =
|
||||
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
|
||||
let code_state = agent_runtime_state_from_task_record(&code_record);
|
||||
fs::remove_file(root.join("assets/art-spec.png")).expect("remove registered art-spec file");
|
||||
fs::remove_file(root.join("assets/art-spec.png")).expect("remove optional art-spec file");
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
|
||||
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
|
||||
);
|
||||
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
|
||||
.expect("game-chat must fail closed without a valid Canvas art spec");
|
||||
assert!(blocker
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("canvas-registration-invalid")));
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1443,7 +1557,7 @@ fn game_chat_later_code_child_rejects_pending_then_projects_verified_completion(
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
"<!doctype html><html><body><img id=\"art\" hidden src=\"../assets/art-spec.png\"><canvas></canvas><script>const image=document.getElementById('art');context.drawImage(image,0,0,canvas.width,canvas.height);context.drawImage(image,0,0,96,96);context.drawImage(image,120,80,112,112);requestAnimationFrame(()=>{});</script></body></html>",
|
||||
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillStyle='#42d9d0';context.fillRect(0,0,96,96);</script></body></html>",
|
||||
);
|
||||
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
|
||||
|
||||
@@ -5918,6 +5918,13 @@
|
||||
- Runtime 决策:game-chat 六任务 lane、平台美术、单轮收束和自动预览只由持久 `project-supervisor-game-chat` root source 启用。hydration 对 `Pending` 的容忍只适用于当前 source-aware lane 的三个零依赖首波任务,不再硬编码单个历史任务。页面进度、阶段记录和 final-reply 白名单统一使用六项任务与 `x/6`。
|
||||
- 显式协作合同:autonomous 的旧 `code-prototype + quality-review` 首批合同退出。显式 project collaboration policy 或持久 batch 恢复若进入首批 `agent.delegate` 路径,只允许且要求三个 Director 各一次;策划与程序 Director 是只读规划且 `expectedArtifacts=[]`,美术 Director 是非只读规范图任务且必须交付 `assets/art-spec.png`。任何非 repair 底层委派与 isolated child 都在首批失败关闭;默认 manifest DAG 仍是唯一自动首轮执行链,不额外复制三个 Director 委派。
|
||||
- 输出决策:保留未提交 `streaming / ready` 的当前 revision 门;已提交的专业 Agent final reply 继续使用既有 durable response-stream 身份,后续项目 revision 变化不再隐藏早期阶段回复。
|
||||
|
||||
## 2026-08-03 game-chat 失败续跑与首版产物保护
|
||||
|
||||
- 事故事实:`game01` 的首个根 run 已交付并验证水晶下落方块原型,但父 run 在上下文压缩后因 token 上限失败;用户随后输入“继续”时,客户端创建了 task 仅为“继续”的新 game-chat root,完成合同重置 seed manifest,五分钟 fallback 再把该短语当主题整文件覆盖 `game/index.html`。这不是模型随机换题,而是 root 目标未继承、每新 run 无条件 reset 和 fallback 无条件 `file.write` 叠加形成的确定性缺陷。
|
||||
- 续跑决策:同一 Supervisor Session 最近失败根 run 后的严格继续短语创建 successor root,并继承前序原始任务、baseline revision / artifact 身份;真正新需求、跨 Session、前序正常完成或含具体新约束的输入继续创建独立新任务并重置本轮 manifest。successor 不复用旧网络请求、pending、action、Provider lifecycle 或 sidecar,只继承业务目标和已提交项目事实。
|
||||
- 覆盖决策:game-chat fallback 只允许初始化缺失/占位入口的首次落盘。非占位 `game/index.html` 必须保留,先读取并局部修复或复用现有静态/试玩门;纯继续目标未恢复时失败关闭。完成门新增 baseline 玩法连续性和 action-driven state 检查,generic Canvas 非空、三个按钮存在或静态 smoke 通过都不能单独证明任务没有换题。
|
||||
- 美术决策:`art-spec.png` 回归为规范图和下游派生参考,不能铺作完整场景,也不能裁剪成玩家/目标。首版允许依据规范使用程序化 Canvas 视觉;需要位图时使用独立透明素材。删除快车道中“必须把规范图作为主要背景/实体”的 prompt、fallback 与测试口径,并以“规范图不出现在运行画面、sequence 不随 RAF 空转”作为回归边界。
|
||||
- 关联:`apps/ai-game-creator-shell/scripts/start-tauri-dev.mjs`、`start-dev-stack.mjs`、`src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`response_stream.rs`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-08-03 托管 MCP 未鉴权响应提供安全接入引导
|
||||
|
||||
@@ -4023,6 +4023,14 @@
|
||||
- 验证:`external_editor_api.rs` 的 openapi 断言只校验 schema 形状,不校验兼容性,通过不等于契约安全;判定 breaking 与否以「删字段、移出 required、收窄类型、改语义、新增必填」为准。
|
||||
- 关联:`docs/【后端架构】外部OpenAPI与APIKey接入方案-2026-06-19.md`、`docs/openapi/genarrative-external-v1.openapi.json`、`server-rs/crates/api-server/src/external_editor_api.rs`、`server-rs/crates/api-server/src/modules/external_api.rs`。
|
||||
|
||||
## “继续”不能成为新游戏主题或触发首版整文件覆盖(2026-08-03)
|
||||
|
||||
- 现象:原根 run 已经写出并验证目标玩法,但父 Runtime 因预算、上下文或 Provider 失败;用户在同一项目输入“继续”后,页面标题变成“继续”,玩法被默认收集/点击模板替换,美术规范总览图被直接铺进游戏画面。
|
||||
- 原因:终态失败 run 不能 steer,提交层因此创建 task 只有继续短语的新 root;每个新 autonomous 根合同又无条件 reset seed manifest;game-chat fallback 从当前 root task 取主题并直接 `file.write game/index.html`。同时快车道把 `icon-spec` 误当运行素材,要求整图背景和象限裁剪实体。通用 smoke/generic playtest 只验证结构与最小交互,无法发现玩法目标已经漂移。
|
||||
- 处理:严格继续意图必须在同一 Supervisor Session 内继承最近失败根 run 的原始目标和 baseline,但保持新的 run/Provider/sidecar 身份;真正新需求仍独立 reset。非占位入口禁止 fallback 整体覆盖,先保留并局部修复或验证。`art-spec.png` 只作规范参考,运行时位图必须来自独立派生素材;程序化首版不应为了“可见使用平台图片”把规范板塞进画面。
|
||||
- 验证:覆盖失败根任务“水晶俄罗斯方块”后输入“继续”、跨 Session、正常完成后新输入、带具体新需求、既有非占位入口、初始化占位、纯继续目标缺失、规范图不在运行 DOM/Canvas、真实动作前后 `sequence` 与 RAF 空转。浏览器验收必须同时比较 baseline 玩法关键文本/控件/状态和当前 revision,不能只看 Canvas 非空与三个固定按钮。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs`。
|
||||
|
||||
## Tauri beforeDevCommand 失败不等于已启动客户端会自动退出(2026-08-03)
|
||||
|
||||
- 现象:旧 worktree 的 AGC Vite 长期占用 `127.0.0.1:3080`,marker 仍指向旧 API;新 worktree 启动 game-chat 后,配套后端在新端口 ready,随后 `beforeDevCommand` 因代理 target 不匹配返回非零,终端已经回到提示符,但原生客户端和它启动的 Runner 仍存活。客户端 WebView 实际加载旧 Vite,因此当前 master 的界面优化看起来全部缺失。
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
- 对话输出:game-chat 的 Supervisor `ready` response stream 继续以稳定身份显示;`design-director / art-director / code-director / code-prototype / preview-readiness / preview-playtest` 的 `requestKind=final-reply` 且 `status=ready|committed` 的非空安全回复也分别以 Agent、Session、run、request slot 和 response revision 形成 durable message ID,并带 Agent 标签逐条追加到项目聊天;`art-asset-plan` 的回复不进入 game-chat 项目聊天。每条 Rust `eventId + publicText` 公开输出同样形成独立 durable 消息。所有这些消息通过 `append_local_conversation_message` 的顶层 `messageId` 幂等写入,事件、轮询、React StrictMode 和 hydration 重放不重复;tool-plan、半成品 stream、原始事件 detail、命令正文、绝对路径、Provider / Runner 元数据、哈希和凭据不得进入聊天。普通 `supervisor-chat` 保持原有 transient response 行为。
|
||||
- 单轮收束:game-chat source 只生成至 `preview-playtest` 的 manifest seed task,试玩完成后父 Run 直接进入完成门,不再调度 `publish-strategy` / `publish-package`;`agent.schedule_ready` 必须按当前 Supervisor Run 的持久 source/profile 选择同一 source-aware scheduler,不能绕过该边界。`task.list` 对同一 root source 必须从任务行、readyTaskIds 和统计中排除两个发布节点,`agent.delegate` 也必须按 root binding 拒绝直接委派这两个节点,不能让 Provider 用“读取完整 DAG 后手工委派”恢复已裁掉的发布阶段。完成门满足且 collaboration、Provider batch、进程会话、视觉资源等非验证屏障全部清零后,Runtime 必须用确定性回复直接收束结构化计划并结束父 Run,不再请求下一次 Provider 工具计划。普通 GUI / CLI 仍执行完整发布 DAG。
|
||||
- 轮次展示:`loopIteration` 只是同一父 Run 内的 Provider / 工具规划循环,用于委派、回执、返工和验收,不是用户发起的游戏生成轮次。game-chat 的进度卡、当前工作和“最新状态”事件统一显示“本轮”,整个页面不向用户显示“第 N 轮”;完整 GUI / CLI pre-publish 任务图仍可显示 `x/14`,但首版只按六项任务显示 `x/6`(详见 2026-08-03 小节),不得把两个发布节点计入任一分母。父 Run 终态后移除运行中进度卡,只保留终态阶段记录与预览。
|
||||
- 平台美术资源:live10 实测正式透明 `icon-spritesheet` 的生成与后处理超过 `300` 秒,因此 game-chat 五分钟首版不运行 `art-asset-plan` 图集链路。它必须配置可用的 External Editor API,由 `art-director` 通过一次平台 `images/generations` 生成并登记 `assets/art-spec.png`,再由 `code-prototype` 在 `game/index.html` 的用户可见画面中把该图片显著用于主要背景、玩家和目标;未配置 API,或缺少生成、登记、文件、引用、可见使用任一证据时均 fail-closed。普通 GUI / CLI autonomous 继续正式透明 `assets/art-spritesheet.png` 的完整 DAG,不采用该快车道。
|
||||
- 平台美术资源:live10 实测正式透明 `icon-spritesheet` 的生成与后处理超过 `300` 秒,因此 game-chat 五分钟首版不运行 `art-asset-plan` 图集链路。配置 External Editor API 时,`art-director` 仍通过一次平台 `images/generations` 生成并登记 `assets/art-spec.png`,但该文件只作为视觉规范与后续派生参考;`code-prototype` 不得把整张规范图铺作背景或裁成玩家、目标和场景实体,没有独立派生素材时使用与规范一致的自包含原创 Canvas 视觉。普通 GUI / CLI autonomous 继续正式透明 `assets/art-spritesheet.png` 的完整 DAG 和运行时可见使用硬门,不采用该快车道。
|
||||
- 验证:前端运行时模型定向测试、Rust completion/source/asset 合同测试、`cargo fmt --check`、`npm run check:encoding` 与 `git diff --check` 必须全部执行;Windows 文件锁竞态只可作为既有测试失败单独记录,不得将其改写为本次改动的通过证据。
|
||||
|
||||
## 2026-08-01 game-chat 首版六任务快车道与美术硬门
|
||||
@@ -63,9 +63,9 @@
|
||||
- 首版任务边界:game-chat 首版只展示 `design-director`、`art-director`、`code-director`、`code-prototype`、`preview-readiness`、`preview-playtest` 六项任务,进度统一显示为 `x/6`;首波仅并行激活三个 Director,`code-prototype` 必须等待三者完成,后续验证再串行推进。`art-asset-plan` 不进入进度、阶段记录或 final-reply 投影。不把完整 GUI / CLI 任务图的其它节点投影到该页面,也不显示内部 Provider / child loop 轮次。
|
||||
- 时间预算:从 game-chat 父 Run 接受用户请求开始,首版可玩版本使用 `240` 秒软预算;父 Run 与其全部 child Run、等待和回收阶段共享从 root `bound_at` 计算的 `300` 秒绝对硬上限,不能把硬上限拆成每个 Agent 独立计时。整个 Runtime pass 必须受同一 `timeout_at` 约束,覆盖 Provider、图片生成、文件写入、静态检查、浏览器试玩和 final-reply 的在途等待;超时先强制持久化 `failed`,再清理恢复 sidecar 与进程会话。达到软预算后只允许进入确定性的本地兜底、静态 smoke 和浏览器试玩;达到硬上限仍未通过完成门必须失败关闭。即使完成证据恰好在上限后到齐,单轮确定性收束也必须再次检查累计预算并拒绝写入 `single_round_converged`,不得把超时伪装成 completed。
|
||||
- Provider 次数:首波 `design-director / code-director` 的规划请求与 `art-director` 的确定性平台生图并行;各 Director 只处理本组规划,不得提前激活底层 Agent。三者收束后,`code-prototype` 最多执行一次 Provider 首版写入请求;后续不再请求第二次代码 tool-plan、自动传输重试或无限 repair。Provider 成功返回后由 Runtime 依次执行确定性的 `game.static_smoke` 与 `preview.validate`,以当前 revision 和真实浏览器证据决定是否可交付。
|
||||
- 可玩兜底:软预算或首版 Provider 无法及时完成时,可以生成完整、自包含、无远程运行依赖的中文 HTML 模板。模板必须从 `ready` 开始,包含真实 Canvas 绘制、`requestAnimationFrame`、键盘 / 触控主要操作、唯一可见且启用的 start / primary-action / restart 控件,状态 JSON 持续推进,并能在 primary-action 后保持 `playing`、在 restart 后稳定回到 `ready | playing`;不得在开始前固定进入 `lost`,也不得通过固定失败冒充试玩通过。模板只有在 `assets/art-spec.png` 已经完成有效登记并真实存在时才允许生成,且必须把该平台图片显著绘制为主要背景、玩家和目标;未配置 External Editor API、图片生成失败或缺少有效登记时直接失败关闭,不得生成纯几何首版。
|
||||
- 平台图片:live10 已证明透明 `icon-spritesheet` 后处理无法稳定收进 `300` 秒。game-chat 由 `art-director` 在首版预算内发起一次平台 `images/generations`,生成并登记 `assets/art-spec.png`;图片必须完整解码,`game/index.html` 的引用必须正确解析到该登记路径,且同一资源必须在非隐藏活动 Canvas 的可达执行路径中至少完成一次背景级和两次实体级 `drawImage`。`code-prototype` 必须在用户可见游戏画面中把它作为主要背景、玩家和目标真实加载和绘制。未配置 API,或生成、登记、文件存在、HTML 引用、可见使用任一缺失时,都必须在父 Run 的 `300` 秒累计硬上限内失败关闭,绝不写入 completed、`single_round_converged` 或其它成功结论。首版仍只在 `preview-playtest` 后单轮收束,不进入发布节点。普通 GUI / CLI 继续正式透明 `assets/art-spritesheet.png` 的完整 DAG。
|
||||
- 关联验收:快车道必须分别验证三 Director 首波并行、`code-prototype` 依赖三者、`x/6` 投影、六个专业 Agent 安全 final-reply 逐条入聊天且排除 `art-asset-plan`、240 / 300 秒累计预算、单次 `images/generations`、External Editor API 缺失时失败关闭、`assets/art-spec.png` 生成 / 登记 / 文件 / 引用 / 主要背景与玩家及目标可见使用硬门,以及当前 revision 的静态 smoke 与浏览器试玩;这些规则不改变普通 GUI / CLI 的完整任务图和透明图集硬门。
|
||||
- 可玩兜底:软预算或首版 Provider 无法及时完成时,可以生成完整、自包含、无远程运行依赖的中文 HTML 模板。模板必须从 `ready` 开始,包含真实 Canvas 绘制、`requestAnimationFrame`、键盘 / 触控主要操作、唯一可见且启用的 start / primary-action / restart 控件,状态 JSON 只随真实输入、状态迁移或模拟状态变化推进,并能在 primary-action 后保持 `playing`、在 restart 后稳定回到 `ready | playing`;不得在开始前固定进入 `lost`,不得通过固定失败冒充试玩通过,也不得由纯渲染帧空转 `sequence`。兜底只允许写入缺失或精确初始化占位的 `game/index.html`;存在非占位入口、当前 run 已修改入口或当前 revision 已有可信验证证据时,必须保留既有玩法并转入静态检查、试玩或基于已读取原文的最小修复。
|
||||
- 平台图片:live10 已证明透明 `icon-spritesheet` 后处理无法稳定收进 `300` 秒。game-chat 配置 External Editor API 时,由 `art-director` 在首版预算内发起一次平台 `images/generations`,生成并登记 `assets/art-spec.png`;该文件是规范图,只用于约束色板、材质、形状与后续派生,不是 `game/index.html` 的运行时背景、角色、目标或图集。完成门不得要求规范图出现在 DOM、CSS 或活动 Canvas,也不得因其没有被运行时加载而阻断程序化首版。首版仍只在 `preview-playtest` 后单轮收束,不进入发布节点;普通 GUI / CLI 继续正式透明 `assets/art-spritesheet.png` 的完整 DAG 与可见使用硬门。
|
||||
- 关联验收:快车道必须分别验证三 Director 首波并行、`code-prototype` 依赖三者、`x/6` 投影、六个专业 Agent 安全 final-reply 逐条入聊天且排除 `art-asset-plan`、240 / 300 秒累计预算、单次 `images/generations`、失败续跑目标继承、非占位入口禁止整文件覆盖、`art-spec.png` 仅作规范参考、程序化首版无需运行时加载规范图、action-driven `sequence`,以及当前 revision 的静态 smoke 与浏览器试玩;这些规则不改变普通 GUI / CLI 的完整任务图和透明图集硬门。
|
||||
|
||||
## 2026-08-03 game-chat 开发态同源与持久输出修复
|
||||
|
||||
@@ -822,6 +822,9 @@ game-project/
|
||||
- 2026-07-27 新起的“16 任务正式产物 + 两张真实画布图片 + current revision 静态 / 双视口浏览器 / PNG 证据 + 受限 repair 替换”独立外部 Provider 验收,使用 `npm run agc:test:chat -- --timeout-minutes 75`,约 `59m50s` 后以退出码 `0` 完整 **PASS**。同一轮真实生成并登记 `assets/ui-prototype.png`(`2829418` bytes)与 `assets/art-spritesheet.png`(`1361906` bytes),固定 `16` 个 manifest task 均为当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物、两张 PNG、当前 revision 的 `game.static_smoke`、desktop / mobile `lane-defense-v1` playtest、浏览器报告与截图全部通过。`turn.report=settled` 且唯一 assistant,busy / pending / running / confirmation / user-input / reconciliation 均为 `0`;隔离 Runner、一次性项目和隔离 AppData 已自动清理。此前失败轮继续独立保留,不与本轮拼接;未来合同变化仍须新起完整轮次复验。
|
||||
- 2026-07-27 补充 tool-plan 成功响应交接的内容边界:Provider 的自然语言计划叙述,以及结构化 arguments 中 `body / code / content / css / html / newText / oldText / patch / script / text` 等源码内容字段,只检查真实密钥 token 形状、凭据头标记和不安全控制字符;仅仅提及 `.env` 或 `game-creator.config` 不能阻断已经计费的安全响应。结构化输入中的敏感 JSON key、非内容字段中的配置痕迹或绝对路径、真实 token、容量、thinking、身份、顺序和账本完整性门禁仍失败关闭。成功 handoff 失败进入 reconciliation 时,Runtime 额外只持久化受控 `failureKind`、脱敏错误 SHA-256 和字符数,不保存 Provider 正文、function arguments、密钥或绝对路径。定向回归覆盖叙述/源码字段放行、`.env.local` 路径和真实 token 拒绝、全部 tool-plan handoff 回归及诊断零正文。
|
||||
- 自然语言 interaction 的 `resume` 只代表“继续当前未完成 Runtime”。宿主在进入 interaction 前已确认当前 Session 没有可 steer、pending 或 running 的 Runtime 时,模型返回的自然语言 `resume` 必须规范化为 `execute`,基于会话历史新建 run;显式 `/resume` 仍只执行恢复扫描且无任务时不新建,避免“那就继续修复”被反复吞成空恢复。
|
||||
- 2026-08-03 失败续跑收口:同一 `project-supervisor` Session 的最近可信根 run 已失败、取消或预算耗尽,且新输入只是严格受限的继续意图(例如“继续”“接着做”“继续完成”)时,宿主仍创建新的 root run 身份,但必须把上一根 run 的原始任务作为继承目标和完成合同基线;不得把继续短语本身当游戏主题,也不得按真正新需求重置 seed manifest。跨 Session、上一根 run 已正常完成、输入包含新的具体玩法要求或无法唯一识别前序根 run 时都不继承,继续按新任务执行。继承只复用目标与已有产物基线,不复用旧 Provider request、pending action 或副作用身份。
|
||||
- game-chat 快车道只能在 `game/index.html` 缺失或仍是初始化占位,且当前 child run 尚未写入正式入口时使用首次 fallback `file.write`。项目已存在非占位入口、已有可信 smoke/playtest 证据或 baseline 摘要与当前文件一致时,后续 run 必须先保留既有玩法并走读取、局部修复或直接验证;禁止为了满足“五分钟首版”重新生成整份默认小游戏。纯继续意图未能恢复唯一原始目标时失败关闭,不输出以“继续”为标题的兜底产物。
|
||||
- `assets/art-spec.png` 的唯一语义是视觉规范与派生参考,不是运行时背景、角色、目标或图集。game-chat 可根据其中的色板、材质和形状约束生成自包含程序化首版;若需要位图实体,必须使用独立派生的透明素材。完成门不得要求整张规范图可见铺满 Canvas、裁剪其象限充当实体或因规范图未在运行时加载而强制失败;隐藏引用、微小水印和诱饵路径仍不构成真实美术使用。`playable-web-game-state.v1.sequence` 只在真实输入、状态迁移或模拟状态变化时递增,不得由纯渲染帧推进。
|
||||
- tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。
|
||||
|
||||
## 2026-07-31 长耗时与恢复收口
|
||||
|
||||
Reference in New Issue
Block a user