chore(agc): 清掉 agent 生成链退役实现的 dead_code 与未用导入
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 4m1s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 2m15s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 4m45s
Project CI / Frontend tests (pull_request) Successful in 3m51s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m46s
Project CI / Repository checks (pull_request) Failing after 3m53s
Project CI / Backend tests (pull_request) Successful in 9m2s
Project CI / Native shell tests (pull_request) Successful in 11m6s

- 删除已退役的多 pass 生成链实现:generation/pass_artifacts.rs(保留仍被
  project/memory.rs 使用的 agent_role_memory_relative_path*)、
  generation/draft_writer.rs(整文件)、generation/draft_validation.rs 与
  generation/trace.rs 中已无任何 cfg 引用的实现与其专属测试。
- generation.rs 按新边界收敛 re-export;draft_validation.rs 不再需要 use super::*。
- redaction.rs / task_lock.rs 删除无调用方的 helper;task_lock 的 Mutex/OnceLock
  只在 unix 分支使用,按 cfg 收敛导入。
- direct_events.rs 删除只服务已删实现的测试 helper。
- agent.rs 删除未使用的 BTreeSet/Seek/Atomic* 导入(sha2 仍被子模块经 use super::*
  使用,保留)。
- build_support/codex_bundle.rs:CLI_VERSION 只在发行构建与单测使用,按 cfg 收敛。

无新增 allow,无假引用;AGC 壳 bin 口径(cocos/unity/godot-editor-execute)编译通过。
This commit is contained in:
2026-10-06 16:32:37 +08:00
parent 9f4c7d7639
commit 22540a874a
10 changed files with 38 additions and 1303 deletions
@@ -11,6 +11,9 @@
pub(crate) mod package_layout;
pub const VERSION: &str = package_layout::CODEX_VERSION;
/// 仅发行构建的协议门禁与共享单测需要它:开发构建按 profile 跳过版本门禁(见
/// `direct_execution::bind_codex_executor` 与 `execution::validate_approval_version`)。
#[cfg(any(test, not(debug_assertions)))]
pub const CLI_VERSION: &str = package_layout::CODEX_CLI_VERSION;
pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA;
@@ -1,8 +1,5 @@
use super::*;
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
use std::io::{Seek, SeekFrom};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
mod art_manifest;
mod claude_code_cli;
@@ -211,16 +211,6 @@ pub(crate) fn start_game_creator_manifest_invalidation_event_sink(
Ok(GameCreatorManifestInvalidationEventSink { port, token })
}
#[cfg(test)]
pub(crate) fn configure_game_creator_manifest_invalidation_event_sink(
port: u16,
token: &str,
) -> Result<(), String> {
let sink = validate_game_creator_manifest_invalidation_event_sink(port, token)?;
register_game_creator_manifest_invalidation_event_sink(sink);
Ok(())
}
pub(crate) fn validate_game_creator_manifest_invalidation_event_sink(
port: u16,
token: &str,
@@ -266,10 +256,6 @@ pub(crate) struct GameCreatorManifestInvalidationEventSinkTestGuard {
#[cfg(test)]
impl GameCreatorManifestInvalidationEventSinkTestGuard {
pub(crate) fn configure(&self, port: u16, token: &str) -> Result<(), String> {
configure_game_creator_manifest_invalidation_event_sink(port, token)
}
pub(crate) fn configured_sink(&self) -> Option<GameCreatorManifestInvalidationEventSink> {
lock_game_creator_manifest_invalidation_event_sinks()
.first()
@@ -2,57 +2,24 @@ use super::*;
mod canvas_generation;
mod draft_validation;
mod draft_writer;
mod external_generation_state;
mod loop_orchestration;
mod pass_artifacts;
mod prompt_context;
mod trace;
#[cfg(test)]
pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_test;
#[cfg(test)]
pub(crate) use canvas_generation::submit_external_generation_request;
pub(in crate::agent) use canvas_generation::{
admit_platform_art_generation_at, commit_prepared_platform_art_asset_at,
commit_prepared_platform_art_asset_strict_slices_at, generate_admitted_platform_art_asset_at,
generate_art_package_asset_with_runtime_options_at,
platform_art_generation_error_result_unknown, recover_persisted_visual_generation_options,
register_existing_platform_art_slices_at, request_platform_art_asset_with_runtime_options_at,
admit_platform_art_generation_at, generate_admitted_platform_art_asset_at,
generate_art_package_asset_with_runtime_options_at, register_existing_platform_art_slices_at,
restore_platform_art_asset_bytes_at,
retained_platform_art_generation_runtime_spritesheet_identity_at,
retained_platform_art_generation_runtime_state_matches_direct_stage_at,
validate_platform_art_png_bytes_with_limits, AdmittedPlatformArtGeneration,
};
pub(crate) use canvas_generation::{
external_canvas_placeholder, external_editor_json_request, external_editor_response_data,
external_generation_poll_after_ms, platform_art_generation_error_needs_reconciliation,
prepare_external_canvas_generation_context, resolve_canvas_resource_download_with_access,
ExternalCanvasGenerationContext,
};
pub(in crate::agent) use draft_validation::validate_closed_game_script_blocks;
pub(crate) use external_generation_state::{
classify_platform_art_generation_service_identity,
platform_art_generation_external_service_fingerprint,
platform_art_generation_external_service_origin, PlatformArtGenerationServiceIdentityMatch,
PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME,
};
pub(in crate::agent) use external_generation_state::{
game_creator_agent_runtime_external_generation_exists,
platform_art_generation_runtime_context_from_pending,
platform_art_generation_runtime_recovery_at, remove_platform_art_generation_runtime_state_at,
PlatformArtGenerationRuntimeContext, PlatformArtGenerationRuntimeRecovery,
PLATFORM_ART_GENERATION_RUNTIME_SCHEMA_VERSION,
};
#[cfg(test)]
pub(crate) use external_generation_state::{
platform_art_generation_legacy_external_configuration_fingerprint,
write_platform_art_generation_runtime_accepted_for_test,
write_platform_art_generation_runtime_prepared_for_test,
};
pub(in crate::agent) use loop_orchestration::build_game_creator_agent_runtime_llm_client;
pub(in crate::agent) use trace::game_creation_agent_group_id;
#[cfg(test)]
pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_test;
#[allow(unused_imports)]
pub(crate) use canvas_generation::{
build_platform_art_asset_prompt, editor_api_key_is_configured, generate_platform_art_asset_at,
@@ -64,15 +31,32 @@ pub(crate) use canvas_generation::{
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::{
contains_any, evaluate_game_draft, extract_json_payload, fnv1a64,
parse_llm_game_draft_response, render_evaluator_findings, render_planner_spec,
strip_llm_thinking_blocks, validate_canvas_rendering_html, validate_game_html_smoke,
validate_llm_agent_handoffs, validate_llm_game_draft, validate_non_placeholder_game_html,
validate_playable_game_html, validate_safe_game_html_runtime,
pub(crate) use canvas_generation::{
external_canvas_placeholder, external_editor_json_request, external_editor_response_data,
external_generation_poll_after_ms, platform_art_generation_error_needs_reconciliation,
prepare_external_canvas_generation_context, resolve_canvas_resource_download_with_access,
ExternalCanvasGenerationContext,
};
pub(crate) use draft_validation::{
contains_any, fnv1a64, strip_llm_thinking_blocks, validate_game_html_smoke,
};
pub(crate) use external_generation_state::{
classify_platform_art_generation_service_identity,
platform_art_generation_external_service_fingerprint,
platform_art_generation_external_service_origin, PlatformArtGenerationServiceIdentityMatch,
PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME,
};
pub(in crate::agent) use external_generation_state::{
game_creator_agent_runtime_external_generation_exists,
platform_art_generation_runtime_context_from_pending,
remove_platform_art_generation_runtime_state_at, PlatformArtGenerationRuntimeContext,
};
#[cfg(test)]
pub(crate) use external_generation_state::{
platform_art_generation_legacy_external_configuration_fingerprint,
write_platform_art_generation_runtime_accepted_for_test,
write_platform_art_generation_runtime_prepared_for_test,
};
pub(crate) use draft_writer::{ensure_legacy_json_generator_project, write_local_game_draft_at};
#[allow(unused_imports)]
pub(crate) use loop_orchestration::{
emit_agent_progress, game_creator_agent_llm_error_is_mud_points_insufficient,
@@ -80,14 +64,7 @@ pub(crate) use loop_orchestration::{
game_creator_runtime_error_is_mud_points_insufficient, request_game_creator_llm_text,
AgentProgressEmitter, GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND,
};
#[allow(unused_imports)]
pub(crate) use pass_artifacts::{
agent_role_memory_relative_path, agent_role_memory_relative_path_for_task,
append_agent_success_memories, append_group_brief_steps, read_previous_agent_role_brief,
render_agent_pass_agenda_markdown, render_agent_pass_task_graph_json,
render_carryover_role_brief, write_agent_group_brief, write_agent_pass_agenda,
write_agent_pass_artifacts, write_agent_pass_file, write_agent_role_brief,
};
pub(crate) use pass_artifacts::agent_role_memory_relative_path_for_task;
#[allow(unused_imports)]
pub(crate) use prompt_context::{
append_prompt_context, asset_source_kind_label, game_creator_generator_user_prompt,
@@ -97,18 +74,8 @@ pub(crate) use prompt_context::{
render_local_conversation_prompt_context_for_session, sanitize_error_context,
sanitize_prompt_context, truncate_prompt_context, truncate_prompt_context_preserving_tail,
};
#[allow(unused_imports)]
pub(crate) use trace::{
agent_run_lifecycle_status, agent_run_stop_reason, agent_trace_step, agent_trace_step_owned,
append_agent_run_activity, append_agent_run_jsonl, append_agent_run_output,
append_agent_run_trace_step, append_local_artifact_write_step, append_preview_log,
append_preview_start_trace_step, append_preview_stop_trace_step,
append_static_smoke_manual_trace_step, append_static_smoke_step,
build_agent_run_task_graph_trace, collect_agent_pass_plan_traces,
game_creation_agent_group_from_id, infer_agent_trace_phase, parse_agent_task_id_list,
read_agent_agenda_snapshot, ready_task_ids_for_tasks, record_replaced_preview_stop,
role_brief_completes_task, set_task_status_if_current, should_replace_task_status, status_rank,
task_has_status, task_status_from_agent_step, with_task_context,
write_agent_run_context_bundle, write_agent_run_trace, AgentAgendaSnapshot,
AgentPassTaskGraphSnapshot,
agent_trace_step_owned, append_preview_log, append_preview_start_trace_step,
append_preview_stop_trace_step, append_static_smoke_manual_trace_step,
record_replaced_preview_stop, with_task_context,
};
@@ -1,82 +1,7 @@
use super::*;
use oxc_allocator::Allocator as JavascriptAllocator;
use oxc_ast::ast::{
RegExpLiteral as JavascriptRegExpLiteral, StringLiteral as JavascriptStringLiteral,
TemplateElement as JavascriptTemplateElement,
};
use oxc_ast_visit::Visit as VisitJavascript;
use oxc_parser::Parser as JavascriptParser;
use oxc_semantic::SemanticBuilder as JavascriptSemanticBuilder;
use oxc_span::{GetSpan as JavascriptGetSpan, SourceType as JavascriptSourceType};
pub(crate) fn evaluate_game_draft(prompt: &str, draft: &LlmGameDraft) -> Vec<String> {
let mut issues = Vec::new();
if let Err(error) = validate_llm_game_draft(prompt, draft) {
issues.push(error);
}
let html = draft.game_html.to_ascii_lowercase();
if !["keydown", "keyup", "pointer", "mousedown", "touch", "click"]
.iter()
.any(|needle| html.contains(needle))
{
issues.push("gameHtml 缺少明确的键盘、鼠标或触摸输入监听".to_string());
}
issues
}
pub(crate) fn render_planner_spec(prompt: &str, spec: &str) -> String {
format!(
"# Planner Spec\n\n## 用户需求\n\n{}\n\n## 规格\n\n{}\n",
prompt.trim(),
spec.trim()
)
}
pub(crate) fn render_evaluator_findings(pass: u8, issues: &[impl AsRef<str>]) -> String {
let issue_values = issues
.iter()
.map(|issue| issue.as_ref().trim())
.filter(|issue| !issue.is_empty())
.collect::<Vec<_>>();
let actionable_issues = issue_values
.iter()
.filter(|issue| !issue.contains("暂无上一轮问题"))
.map(|issue| (*issue).to_string())
.collect::<Vec<_>>();
let mut output = format!(
"# Evaluator Findings\n\n- pass: {pass}\n- status: {}\n\n",
if issue_values.is_empty() {
"passed"
} else {
"needs-revision"
}
);
if issue_values.is_empty() {
output
.push_str("## 结果\n\n- 本地静态验收通过:HTML 自包含、包含 canvas、主循环和输入。\n");
} else {
output.push_str("## 问题\n\n");
for issue in &issue_values {
output.push_str("- ");
output.push_str(issue);
output.push('\n');
}
}
output.push_str("\n## Repair Routes\n\n```json\n");
if actionable_issues.is_empty() {
output.push_str("[]");
} else {
let routes = build_game_creation_seed_task_graph("AI 游戏创作")
.map(|graph| route_game_creation_repair_issues(&graph, &actionable_issues))
.unwrap_or_default();
match serde_json::to_string_pretty(&routes) {
Ok(payload) => output.push_str(&payload),
Err(_) => output.push_str("[]"),
}
}
output.push_str("\n```\n");
output
}
use oxc_span::SourceType as JavascriptSourceType;
pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf29ce484222325_u64;
@@ -87,14 +12,6 @@ pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
hash
}
pub(crate) fn parse_llm_game_draft_response(content: &str) -> Result<LlmGameDraft, String> {
let content = strip_llm_thinking_blocks(content);
let payload = extract_json_payload(content.as_str())
.ok_or_else(|| "LLM 返回不是 JSON 对象".to_string())?;
serde_json::from_str::<LlmGameDraft>(payload)
.map_err(|error| format!("解析 LLM 游戏草案失败:{error}"))
}
pub(crate) fn strip_llm_thinking_blocks(content: &str) -> String {
let mut output = String::new();
let mut rest = content;
@@ -113,91 +30,6 @@ pub(crate) fn strip_llm_thinking_blocks(content: &str) -> String {
output.trim().to_string()
}
pub(crate) fn extract_json_payload(content: &str) -> Option<&str> {
let trimmed = content.trim();
let without_fence = trimmed
.strip_prefix("```json")
.or_else(|| trimmed.strip_prefix("```"))
.and_then(|value| value.strip_suffix("```"))
.map(str::trim)
.unwrap_or(trimmed);
let start = without_fence.find('{')?;
let mut depth = 0usize;
let mut inside_string = false;
let mut escaped = false;
for (offset, character) in without_fence[start..].char_indices() {
if inside_string {
if escaped {
escaped = false;
} else if character == '\\' {
escaped = true;
} else if character == '"' {
inside_string = false;
}
continue;
}
match character {
'"' => inside_string = true,
'{' => depth += 1,
'}' => {
depth = depth.checked_sub(1)?;
if depth == 0 {
let end = start + offset + character.len_utf8();
return Some(&without_fence[start..end]);
}
}
_ => {}
}
}
None
}
pub(crate) fn validate_llm_game_draft(prompt: &str, draft: &LlmGameDraft) -> Result<(), String> {
if draft.title.trim().is_empty() {
return Err("LLM 草案缺少标题".to_string());
}
if draft.design_markdown.trim().is_empty() {
return Err("LLM 草案缺少设计说明".to_string());
}
if !draft.balance.is_object() {
return Err("LLM 草案 balance 必须是 JSON object".to_string());
}
if !draft.art_manifest.is_object() {
return Err("LLM 草案 artManifest 必须是 JSON object".to_string());
}
if !draft.audio_manifest.is_object() {
return Err("LLM 草案 audioManifest 必须是 JSON object".to_string());
}
if draft.publish_readme.trim().is_empty() {
return Err("LLM 草案缺少发布说明".to_string());
}
if draft.handoff_summary.trim().is_empty() {
return Err("LLM 草案缺少多智能体交接摘要".to_string());
}
validate_llm_agent_handoffs(draft)?;
let html = draft.game_html.trim();
let lower_html = html.to_ascii_lowercase();
if !lower_html.contains("<html") && !lower_html.contains("<!doctype html") {
return Err("LLM 草案 gameHtml 必须是完整 HTML".to_string());
}
if !html.contains("requestAnimationFrame") {
return Err("LLM 草案 gameHtml 必须包含游戏主循环".to_string());
}
if !lower_html.contains("<canvas") {
return Err("LLM 草案 gameHtml 必须包含可渲染画布".to_string());
}
validate_canvas_rendering_html(html, "LLM 草案 gameHtml")?;
validate_safe_game_html_runtime(html, "LLM 草案 gameHtml")?;
if (prompt.contains('<') || prompt.contains('>')) && html.contains(prompt) {
return Err("LLM 草案 gameHtml 包含未转义的用户输入".to_string());
}
validate_playable_game_html(html, "LLM 草案 gameHtml")?;
validate_non_placeholder_game_html(html, "LLM 草案 gameHtml")?;
Ok(())
}
pub(crate) fn validate_playable_game_html(html: &str, label: &str) -> Result<(), String> {
let lower_html = html.to_ascii_lowercase();
if !contains_any(
@@ -381,28 +213,6 @@ pub(crate) fn contains_any(haystack: &str, needles: &[&str]) -> bool {
needles.iter().any(|needle| haystack.contains(needle))
}
pub(crate) fn validate_llm_agent_handoffs(draft: &LlmGameDraft) -> Result<(), String> {
const REQUIRED_GROUPS: [&str; 6] = ["design", "balance", "art", "audio", "code", "publishing"];
for required_group in REQUIRED_GROUPS {
let handoff = draft
.handoffs
.iter()
.find(|handoff| handoff.group.trim() == required_group)
.ok_or_else(|| format!("LLM 草案 handoffs 缺少 {required_group} 专业组"))?;
if handoff.role.trim().is_empty()
|| handoff.summary.trim().is_empty()
|| handoff.next.trim().is_empty()
|| handoff
.outputs
.iter()
.all(|output| output.trim().is_empty())
{
return Err(format!("LLM 草案 handoffs.{required_group} 交接内容不完整"));
}
}
Ok(())
}
fn is_html_ascii_whitespace(byte: u8) -> bool {
matches!(byte, b'\t' | b'\n' | b'\x0c' | b'\r' | b' ')
}
@@ -540,82 +350,6 @@ fn validate_executable_inline_javascript_syntax(html: &str) -> Result<(), String
Ok(())
}
fn html_tag_like_start(content: &str, tag_start: usize) -> bool {
content
.as_bytes()
.get(tag_start + 1)
.is_some_and(|byte| byte.is_ascii_alphabetic() || matches!(byte, b'/' | b'!' | b'?'))
}
#[derive(Default)]
struct JavascriptLiteralSpanCollector {
strings: Vec<std::ops::Range<usize>>,
regular_expressions: Vec<std::ops::Range<usize>>,
template_elements: Vec<std::ops::Range<usize>>,
}
fn javascript_span_range(span: oxc_span::Span) -> std::ops::Range<usize> {
span.start as usize..span.end as usize
}
impl<'a> VisitJavascript<'a> for JavascriptLiteralSpanCollector {
fn visit_string_literal(&mut self, literal: &JavascriptStringLiteral<'a>) {
self.strings.push(javascript_span_range(literal.span));
}
fn visit_reg_exp_literal(&mut self, literal: &JavascriptRegExpLiteral<'a>) {
self.regular_expressions
.push(javascript_span_range(literal.span));
}
fn visit_template_element(&mut self, element: &JavascriptTemplateElement<'a>) {
self.template_elements
.push(javascript_span_range(element.span));
}
}
fn javascript_without_string_literals_or_comments(content: &str) -> String {
let allocator = JavascriptAllocator::default();
let parsed = JavascriptParser::new(
&allocator,
content,
JavascriptSourceType::default().with_unambiguous(true),
)
.parse();
if parsed.panicked || !parsed.diagnostics.is_empty() {
return " ".repeat(content.len());
}
let mut collector = JavascriptLiteralSpanCollector::default();
collector.visit_program(&parsed.program);
let mut output = content.as_bytes().to_vec();
for range in collector.template_elements {
output[range].fill(b' ');
}
for range in collector.strings {
if range.end.saturating_sub(range.start) > 2 {
output[range.start + 1..range.end - 1].fill(b' ');
}
}
for range in collector.regular_expressions {
let raw = &content[range.clone()];
if let Some(close) = raw[1..].rfind('/') {
let close = range.start + 1 + close;
output[range.start + 1..close].fill(b' ');
if close > range.start + 1 {
output[range.start + 1] = b'a';
}
}
}
for comment in &parsed.program.comments {
for byte in &mut output[javascript_span_range(comment.span())] {
if !matches!(*byte, b'\r' | b'\n') {
*byte = b' ';
}
}
}
String::from_utf8(output).expect("masking parsed JavaScript literals preserves UTF-8")
}
fn html_script_type_is_executable(script_tag: &str) -> bool {
if let Some(script_type) = html_attribute_value(script_tag, "type") {
let script_type = script_type.trim();
@@ -1,171 +0,0 @@
use super::*;
pub(crate) fn write_local_game_draft_at(
root: &Path,
prompt: &str,
draft: &LlmGameDraft,
) -> Result<GenerateLocalGameDraftResult, String> {
let prompt = prompt.trim();
if prompt.is_empty() {
return Err("创作想法不能为空".to_string());
}
ensure_legacy_json_generator_project(root)?;
validate_llm_game_draft(prompt, draft)?;
let checkpoint = create_local_project_checkpoint_at(root)?;
let timestamp = unix_timestamp();
let title = draft.title.trim();
let handoff_summary = draft.handoff_summary.trim();
let design_path = root.join("game/game_design.md");
let balance_path = root.join("game/balance.json");
let art_manifest_path = root.join("assets/manifest.art.json");
let audio_manifest_path = root.join("assets/manifest.audio.json");
let publish_readme_path = root.join("exports/README.md");
let agent_log_path = root.join(".agent/logs/agent.log");
let short_memory_path = root.join("memory/session.md");
let long_memory_path = root.join("memory/project.md");
let game_index_path = root.join("game/index.html");
append_markdown_entry(
&short_memory_path,
"# 短期记忆\n\n",
&format!("- {timestamp}: {prompt}\n"),
"写入短期记忆失败",
)?;
append_markdown_entry(
&long_memory_path,
"# 项目长期记忆\n\n## 当前约束\n\n- Web 小游戏原型\n- 本地 HTTP 预览\n\n## 创作目标记录\n\n",
&format!("- {timestamp}: {prompt}\n"),
"写入长期记忆失败",
)?;
fs::write(
&design_path,
format!(
"# 游戏设计草案\n\n## 原始想法\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n## LLM 生成草案\n\n{}\n",
draft.design_markdown.trim()
),
)
.map_err(|error| format!("写入游戏设计失败:{}: {error}", design_path.display()))?;
fs::write(
&balance_path,
serde_json::to_string_pretty(&draft.balance)
.map_err(|error| format!("生成数值配置失败:{error}"))?,
)
.map_err(|error| format!("写入数值配置失败:{}: {error}", balance_path.display()))?;
fs::write(
&art_manifest_path,
serde_json::to_string_pretty(&draft.art_manifest)
.map_err(|error| format!("生成美术清单失败:{error}"))?,
)
.map_err(|error| format!("写入美术清单失败:{}: {error}", art_manifest_path.display()))?;
fs::write(
&audio_manifest_path,
serde_json::to_string_pretty(&draft.audio_manifest)
.map_err(|error| format!("生成音乐音效清单失败:{error}"))?,
)
.map_err(|error| {
format!(
"写入音乐音效清单失败:{}: {error}",
audio_manifest_path.display()
)
})?;
fs::write(
&publish_readme_path,
format!(
"# 发布包装草案\n\n## 标题\n\n{title}\n\n## 简介\n\n{prompt}\n\n## Agent 协作交接\n\n{handoff_summary}\n\n{}\n",
draft.publish_readme.trim()
),
)
.map_err(|error| {
format!(
"写入发布包装草案失败:{}: {error}",
publish_readme_path.display()
)
})?;
fs::write(&game_index_path, draft.game_html.trim())
.map_err(|error| format!("写入游戏入口失败:{}: {error}", game_index_path.display()))?;
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&agent_log_path)
.and_then(|mut file| {
file.write_all(
format!("{timestamp} game.generate_draft llm\n{handoff_summary}\n").as_bytes(),
)
})
.map_err(|error| format!("写入 Agent 日志失败:{}: {error}", agent_log_path.display()))?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "game.generate_draft",
"goal": prompt,
"title": title,
"paths": [
"memory/session.md",
"memory/project.md",
"game/game_design.md",
"game/balance.json",
"assets/manifest.art.json",
"assets/manifest.audio.json",
"exports/README.md",
"game/index.html",
],
}),
)?;
let manifest = record_draft_task_progress(root, prompt, timestamp, &agent_log_path)?;
let diff = diff_local_project_checkpoint_at(root, &checkpoint.checkpoint_id)?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "project.diff",
"checkpointId": checkpoint.checkpoint_id,
"added": diff.added.len(),
"changed": diff.changed.len(),
"deleted": diff.deleted.len(),
}),
)?;
Ok(GenerateLocalGameDraftResult {
project_path: root.to_string_lossy().into_owned(),
game_index_path: game_index_path.to_string_lossy().into_owned(),
design_path: design_path.to_string_lossy().into_owned(),
short_memory_path: short_memory_path.to_string_lossy().into_owned(),
long_memory_path: long_memory_path.to_string_lossy().into_owned(),
manifest,
})
}
pub(crate) fn ensure_legacy_json_generator_project(root: &Path) -> Result<(), String> {
if root.join("game/package.json").exists()
|| root.join("package.json").exists()
|| !root.join("game/index.html").is_file()
|| !root.join(".agent/manifest.json").is_file()
{
return Err("JSON Generator 仅支持已有的单文件 HTML 项目;新建游戏与 npm / Phaser 4 项目请使用 DirectProject 完成依赖安装、构建和试玩".to_string());
}
Ok(())
}
#[cfg(test)]
mod legacy_generator_tests {
use super::*;
#[test]
fn legacy_generator_rejects_npm_before_writing() {
let root = tempfile::tempdir_in(std::env::temp_dir().canonicalize().unwrap()).unwrap();
fs::create_dir_all(root.path().join("game")).unwrap();
fs::create_dir_all(root.path().join(".agent")).unwrap();
fs::write(root.path().join("game/index.html"), "legacy source").unwrap();
fs::write(root.path().join(".agent/manifest.json"), "{}").unwrap();
assert!(ensure_legacy_json_generator_project(root.path()).is_ok());
fs::write(root.path().join("game/package.json"), "{}").unwrap();
assert!(ensure_legacy_json_generator_project(root.path())
.unwrap_err()
.contains("DirectProject"));
assert_eq!(
fs::read_to_string(root.path().join("game/index.html")).unwrap(),
"legacy source"
);
assert!(!root.path().join(".agent/spec.md").exists());
}
}
@@ -1,326 +1,5 @@
use super::*;
pub(crate) fn write_agent_pass_agenda(
root: &Path,
pass: u8,
findings_markdown: &str,
) -> Result<AgentPassAgenda, String> {
let graph = build_game_creation_seed_task_graph("AI 游戏创作")
.map_err(|error| format!("构建 Agent 编排任务图失败:{error}"))?;
let pass_plan = plan_game_creation_agent_pass(&graph, pass, findings_markdown)
.map_err(|error| format!("规划 Agent 编排任务图失败:{error}"))?;
let repair_routes = pass_plan
.repair_routes
.iter()
.map(|route| GameCreationAgentRepairRouteTrace {
issue: route.issue.clone(),
task_ids: route.task_ids.clone(),
reason: route.reason.clone(),
})
.collect::<Vec<_>>();
let relative_path = format!(".agent/passes/pass-{pass}/agenda.md");
let markdown = render_agent_pass_agenda_markdown(
pass,
&pass_plan.mode,
&pass_plan.active_task_ids,
&pass_plan.carried_task_ids,
&pass_plan.dependency_waves,
&pass_plan.repair_focus,
&repair_routes,
);
write_agent_pass_file(root, &relative_path, &markdown)?;
let task_graph_relative_path = format!(".agent/passes/pass-{pass}/task-graph.json");
write_agent_pass_file(
root,
&task_graph_relative_path,
&render_agent_pass_task_graph_json(
pass,
&pass_plan.mode,
&pass_plan.summary,
&pass_plan.active_task_ids,
&pass_plan.carried_task_ids,
&pass_plan.dependency_waves,
&pass_plan.repair_focus,
&repair_routes,
)?,
)?;
Ok(AgentPassAgenda {
relative_path,
task_graph_relative_path,
active_task_ids: pass_plan.active_task_ids,
carried_task_ids: pass_plan.carried_task_ids,
dependency_waves: pass_plan.dependency_waves,
repair_focus: pass_plan.repair_focus,
repair_routes,
summary: pass_plan.summary,
})
}
pub(crate) fn render_agent_pass_agenda_markdown(
pass: u8,
mode: &str,
active_task_ids: &[String],
carried_task_ids: &[String],
dependency_waves: &[Vec<String>],
issues: &[String],
repair_routes: &[GameCreationAgentRepairRouteTrace],
) -> String {
let mut output = format!(
"# Orchestrator Agenda\n\n- pass: {pass}\n- mode: {}\n- activeTasks: {}\n- carriedTasks: {}\n\n",
mode,
active_task_ids.join(", "),
if carried_task_ids.is_empty() {
"none".to_string()
} else {
carried_task_ids.join(", ")
}
);
output.push_str("## Repair Focus\n\n");
if issues.is_empty() {
output.push_str("- 首轮生成,所有组内角色参与。\n");
} else {
for issue in issues {
output.push_str("- ");
output.push_str(issue);
output.push('\n');
}
}
output.push_str("\n## Dependency Waves\n\n");
for (index, wave) in dependency_waves.iter().enumerate() {
output.push_str(&format!(
"- wave {}: {}\n",
index + 1,
if wave.is_empty() {
"none".to_string()
} else {
wave.join(", ")
}
));
}
output.push_str("\n## Repair Routes\n\n");
if repair_routes.is_empty() {
output.push_str("- none\n");
} else {
for route in repair_routes {
output.push_str(&format!(
"- issue: {}\n taskIds: {}\n reason: {}\n",
route.issue,
route.task_ids.join(", "),
route.reason
));
}
}
output.push_str(prompt_text!("goalContext.agenda_rules"));
output
}
pub(crate) fn render_agent_pass_task_graph_json(
pass: u8,
mode: &str,
summary: &str,
active_task_ids: &[String],
carried_task_ids: &[String],
dependency_waves: &[Vec<String>],
issues: &[String],
repair_routes: &[GameCreationAgentRepairRouteTrace],
) -> Result<String, String> {
serde_json::to_string_pretty(&serde_json::json!({
"schemaVersion": "game-creator-agent-pass-task-graph.v1",
"pass": pass,
"mode": mode,
"summary": summary,
"activeTaskIds": active_task_ids,
"carriedTaskIds": carried_task_ids,
"repairFocus": issues,
"repairRoutes": repair_routes,
"dependencyWaves": dependency_waves,
}))
.map(|payload| format!("{payload}\n"))
.map_err(|error| format!("生成 Agent pass task graph 失败:{error}"))
}
pub(crate) fn read_previous_agent_role_brief(
root: &Path,
pass: u8,
group_definition: AgentGroupDefinition,
role_definition: AgentRoleDefinition,
) -> Result<Option<(String, String)>, String> {
if pass <= 1 {
return Ok(None);
}
let relative_path = format!(
".agent/passes/pass-{}/groups/{}/{}",
pass - 1,
group_definition.id,
role_definition.brief_path_name
);
match fs::read_to_string(root.join(&relative_path)) {
Ok(content) => Ok(Some((relative_path, content))),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(format!(
"读取上一轮角色 brief 失败:{relative_path}: {error}"
)),
}
}
pub(crate) fn render_carryover_role_brief(
group_definition: AgentGroupDefinition,
role_definition: AgentRoleDefinition,
source_path: &str,
source_markdown: &str,
) -> String {
format!(
"本角色判断:沿用上一轮 {} / {} 输出。\n交付物:{}\n下游约束:本轮未命中该任务,Generator 只在必要时读取此约束。\n验收风险:如果 Evaluator 后续命中该组,下一轮必须重新激活。\n\n## 上一轮 brief\n\n{}",
group_definition.label,
role_definition.role,
source_path,
source_markdown.trim()
)
}
pub(crate) fn write_agent_pass_artifacts(
root: &Path,
pass: u8,
draft: &LlmGameDraft,
) -> Result<AgentPassArtifactPaths, String> {
let relative_dir = format!(".agent/passes/pass-{pass}");
let pass_dir = root.join(&relative_dir);
fs::create_dir_all(&pass_dir)
.map_err(|error| format!("创建 Agent pass 目录失败:{}: {error}", pass_dir.display()))?;
let paths = AgentPassArtifactPaths {
draft_json: format!("{relative_dir}/draft.json"),
design_markdown: format!("{relative_dir}/design.md"),
balance_json: format!("{relative_dir}/balance.json"),
art_manifest_json: format!("{relative_dir}/manifest.art.json"),
audio_manifest_json: format!("{relative_dir}/manifest.audio.json"),
publish_readme: format!("{relative_dir}/README.md"),
game_html: format!("{relative_dir}/game.html"),
handoff_markdown: format!("{relative_dir}/handoff.md"),
};
write_agent_pass_file(
root,
&paths.draft_json,
&format!(
"{}\n",
serde_json::to_string_pretty(draft)
.map_err(|error| format!("序列化 Agent pass draft 失败:{error}"))?
),
)?;
write_agent_pass_file(
root,
&paths.design_markdown,
&format!("# 策划组 / Gameplay\n\n{}\n", draft.design_markdown.trim()),
)?;
write_agent_pass_file(
root,
&paths.balance_json,
&format!(
"{}\n",
serde_json::to_string_pretty(&draft.balance)
.map_err(|error| format!("序列化 Agent pass 数值失败:{error}"))?
),
)?;
write_agent_pass_file(
root,
&paths.art_manifest_json,
&format!(
"{}\n",
serde_json::to_string_pretty(&draft.art_manifest)
.map_err(|error| format!("序列化 Agent pass 美术清单失败:{error}"))?
),
)?;
write_agent_pass_file(
root,
&paths.audio_manifest_json,
&format!(
"{}\n",
serde_json::to_string_pretty(&draft.audio_manifest)
.map_err(|error| format!("序列化 Agent pass 音乐清单失败:{error}"))?
),
)?;
write_agent_pass_file(
root,
&paths.publish_readme,
&format!("# 运营组 / Publish\n\n{}\n", draft.publish_readme.trim()),
)?;
write_agent_pass_file(root, &paths.game_html, &draft.game_html)?;
write_agent_pass_file(
root,
&paths.handoff_markdown,
&render_agent_pass_handoff(pass, draft),
)?;
Ok(paths)
}
pub(crate) fn write_agent_pass_file(
root: &Path,
relative_path: &str,
content: &str,
) -> Result<(), String> {
let path = root.join(relative_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent pass 文件目录失败:{}: {error}",
parent.display()
)
})?;
}
fs::write(&path, content)
.map_err(|error| format!("写入 Agent pass 文件失败:{}: {error}", path.display()))
}
pub(crate) fn write_agent_group_brief(
root: &Path,
pass: u8,
definition: AgentGroupDefinition,
markdown: &str,
) -> Result<String, String> {
let relative_path = format!(
".agent/passes/pass-{pass}/groups/{}",
definition.brief_path_name
);
write_agent_pass_file(
root,
&relative_path,
&format!(
"# {} / {}\n\n{}\n",
definition.label,
definition.role,
markdown.trim()
),
)?;
Ok(relative_path)
}
pub(crate) fn write_agent_role_brief(
root: &Path,
pass: u8,
group_definition: AgentGroupDefinition,
role_definition: AgentRoleDefinition,
markdown: &str,
) -> Result<String, String> {
let relative_path = format!(
".agent/passes/pass-{pass}/groups/{}/{}",
group_definition.id, role_definition.brief_path_name
);
write_agent_pass_file(
root,
&relative_path,
&format!(
"# {} / {}\n\n- task: {}\n\n{}\n",
group_definition.label,
role_definition.role,
role_definition.task_id,
markdown.trim()
),
)?;
Ok(relative_path)
}
pub(crate) fn agent_role_memory_relative_path(
group_definition: AgentGroupDefinition,
role_definition: AgentRoleDefinition,
@@ -344,156 +23,3 @@ pub(crate) fn agent_role_memory_relative_path_for_task(task_id: &str) -> Result<
}
Err(format!("未知 Agent 任务:{task_id}"))
}
pub(crate) fn append_agent_success_memories(
root: &Path,
pass: u8,
draft: &LlmGameDraft,
briefs: &[AgentGroupBrief],
) -> Result<(), String> {
let timestamp = unix_timestamp();
let title = draft.title.trim();
let mut blackboard_entry = format!(
"\n## pass {pass} - {title}\n\n- 时间:{timestamp}\n- 稳定原型:game/index.html\n- 设计:game/game_design.md\n- 数值:game/balance.json\n- 美术:assets/manifest.art.json\n- 音乐音效:assets/manifest.audio.json\n- 发布包装:exports/README.md\n\n## 角色共享摘要\n\n"
);
for brief in briefs {
for role_brief in &brief.role_briefs {
blackboard_entry.push_str(&format!(
"- {} / {}:{};status={};brief={}\n",
role_brief.group_definition.label,
role_brief.role_definition.role,
role_brief.summary,
role_brief.status,
role_brief.relative_path
));
let private_entry = format!(
"\n## pass {pass} - {title}\n\n- 时间:{timestamp}\n- task:{}\n- status:{}\n- brief:{}\n- 摘要:{}\n",
role_brief.role_definition.task_id,
role_brief.status,
role_brief.relative_path,
role_brief.summary
);
append_markdown_entry(
&root.join(&role_brief.memory_relative_path),
&format!(
"# Agent 私有记忆 - {} / {}\n\n",
role_brief.group_definition.label, role_brief.role_definition.role
),
&private_entry,
"写入 Agent 私有记忆失败",
)?;
}
}
append_markdown_entry(
&root.join(PROJECT_BLACKBOARD_MEMORY_PATH),
"# 项目黑板\n\n",
&blackboard_entry,
"写入项目黑板失败",
)
}
pub(crate) fn append_group_brief_steps(
pass: u8,
agenda_relative_path: &str,
briefs: &[AgentGroupBrief],
steps: &mut Vec<GameCreationAgentRunStep>,
) {
for brief in briefs {
for role_brief in &brief.role_briefs {
let input_paths = vec![
"memory/session.md".to_string(),
"memory/project.md".to_string(),
PROJECT_BLACKBOARD_MEMORY_PATH.to_string(),
".agent/conversations/project.jsonl".to_string(),
".agent/conversations/agents/".to_string(),
role_brief.memory_relative_path.clone(),
".agent/manifest.json".to_string(),
".agent/spec.md".to_string(),
".agent/findings.md".to_string(),
agenda_relative_path.to_string(),
];
let mut output_paths = vec![role_brief.relative_path.clone()];
if role_brief.status == "completed" || role_brief.status == "carried-over" {
output_paths.push(role_brief.memory_relative_path.clone());
output_paths.push(PROJECT_BLACKBOARD_MEMORY_PATH.to_string());
}
let step = with_task_context(
agent_trace_step_owned(
pass,
&format!(
"{} / {}",
role_brief.group_definition.label, role_brief.role_definition.role
),
&role_brief.status,
input_paths.clone(),
output_paths,
&role_brief.summary,
&role_brief.tool_id,
),
role_brief.group_definition.id,
role_brief.role_definition.role,
Some(role_brief.role_definition.task_id),
"role-brief",
);
steps.push(step);
}
let role_paths = brief
.role_briefs
.iter()
.map(|role_brief| role_brief.relative_path.clone())
.collect::<Vec<_>>();
steps.push(with_task_context(
agent_trace_step_owned(
pass,
&format!("{} / GroupCoordinator", brief.definition.label),
"completed",
role_paths,
vec![brief.relative_path.clone()],
"汇总组内角色 brief,交给 Generator",
&format!("agent.group.aggregate.{}", brief.definition.id),
),
brief.definition.id,
"GroupCoordinator",
None,
"group-aggregate",
));
}
}
pub(crate) fn handoff_group_label(group: &str) -> &'static str {
match group.trim() {
"design" => "策划组",
"balance" => "数值组",
"art" => "美术组",
"audio" => "音乐组",
"code" => "程序组",
"publishing" => "运营组",
_ => "专业组",
}
}
pub(crate) fn render_agent_pass_handoff(pass: u8, draft: &LlmGameDraft) -> String {
let mut output = format!("# Agent Handoff\n\n- pass: {pass}\n\n");
for handoff in &draft.handoffs {
output.push_str(&format!(
"## {} / {}\n\n{}\n\n- outputs: {}\n- next: {}\n\n",
handoff_group_label(&handoff.group),
handoff.role.trim(),
handoff.summary.trim(),
handoff
.outputs
.iter()
.map(|output| output.trim())
.filter(|output| !output.is_empty())
.collect::<Vec<_>>()
.join(", "),
handoff.next.trim()
));
}
output.push_str("## 总结\n\n");
output.push_str(draft.handoff_summary.trim());
output.push('\n');
output
}
@@ -110,81 +110,6 @@ pub(crate) fn game_creation_agent_group_from_id(
}
}
pub(in crate::agent) fn game_creation_agent_group_id(
group: &GameCreationAppAgentGroup,
) -> &'static str {
match group {
GameCreationAppAgentGroup::Design => "design",
GameCreationAppAgentGroup::Balance => "balance",
GameCreationAppAgentGroup::Art => "art",
GameCreationAppAgentGroup::Audio => "audio",
GameCreationAppAgentGroup::Code => "code",
GameCreationAppAgentGroup::Publishing => "publishing",
}
}
pub(crate) fn append_static_smoke_step(
root: &Path,
prompt: &str,
loop_result: &mut GameCreatorAgentLoopResult,
) -> Result<(), String> {
match run_limited_local_command_at(root, "game.static_smoke") {
Ok(smoke) => {
loop_result.steps.push(with_task_context(
agent_trace_step(
loop_result.passes,
"Playtest",
"completed",
&["game/index.html"],
&[".agent/logs/command.log", ".agent/manifest.json"],
&smoke.output,
"game.static_smoke",
),
"code",
"Preview",
Some("preview-readiness"),
"playtest",
));
write_agent_run_trace(
root,
&loop_result.run_id,
prompt,
"passed",
loop_result.passes,
&loop_result.steps,
None,
)
}
Err(error) => {
loop_result.steps.push(with_task_context(
agent_trace_step(
loop_result.passes,
"Playtest",
"failed",
&["game/index.html"],
&[".agent/logs/command.log"],
&error,
"game.static_smoke",
),
"code",
"Preview",
Some("preview-readiness"),
"playtest",
));
write_agent_run_trace(
root,
&loop_result.run_id,
prompt,
"failed",
loop_result.passes,
&loop_result.steps,
Some(&error),
)?;
Err(format!("生成后自检失败:{error}"))
}
}
}
pub(crate) fn append_static_smoke_manual_trace_step(
root: &Path,
result: &LimitedLocalCommandResult,
@@ -212,46 +137,6 @@ pub(crate) fn append_static_smoke_manual_trace_step(
)
}
pub(crate) fn append_local_artifact_write_step(
root: &Path,
prompt: &str,
loop_result: &mut GameCreatorAgentLoopResult,
) -> Result<(), String> {
loop_result.steps.push(agent_trace_step_owned(
loop_result.passes,
"ArtifactWriter",
"completed",
vec![
format!(".agent/passes/pass-{}/draft.json", loop_result.passes),
format!(".agent/passes/pass-{}/handoff.md", loop_result.passes),
],
vec![
"memory/session.md".to_string(),
"memory/project.md".to_string(),
PROJECT_BLACKBOARD_MEMORY_PATH.to_string(),
"memory/agents/".to_string(),
"game/game_design.md".to_string(),
"game/balance.json".to_string(),
"assets/manifest.art.json".to_string(),
"assets/manifest.audio.json".to_string(),
"exports/README.md".to_string(),
"game/index.html".to_string(),
".agent/manifest.json".to_string(),
],
"把通过 Evaluator 的草案写入本地项目产物",
"file.write.local_artifacts",
));
write_agent_run_trace(
root,
&loop_result.run_id,
prompt,
"artifacts-written",
loop_result.passes,
&loop_result.steps,
None,
)
}
pub(crate) fn append_preview_start_trace_step(
root: &Path,
preview: &LocalPreviewResult,
@@ -670,46 +555,6 @@ pub(crate) fn ready_task_ids_for_tasks(tasks: &[GameCreationAppTaskState]) -> Ve
.collect()
}
pub(crate) fn write_agent_run_trace(
root: &Path,
run_id: &str,
prompt: &str,
status: &str,
passes: u8,
steps: &[GameCreationAgentRunStep],
error: Option<&str>,
) -> Result<(), String> {
let tool_call_count = count_agent_tool_calls(steps)?;
let trace = GameCreationAgentRunTrace {
schema_version: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION.to_string(),
run_id: run_id.to_string(),
command_id: "game.generate_draft".to_string(),
status: status.to_string(),
lifecycle_status: Some(agent_run_lifecycle_status(status).to_string()),
passes,
max_passes: GAME_CREATOR_AGENT_LOOP_MAX_PASSES,
tool_call_count,
max_tool_calls: GAME_CREATOR_AGENT_TOOL_CALL_MAX,
stop_reason: agent_run_stop_reason(status, error).to_string(),
goal: prompt.trim().to_string(),
coordination: "filesystem".to_string(),
steps: steps.to_vec(),
artifacts: collect_agent_run_artifacts(root)?,
task_graph: build_agent_run_task_graph_trace(root, prompt, passes, steps)?,
pass_plans: collect_agent_pass_plan_traces(root, passes)?,
next_step: match status {
"passed" => "preview-playtest",
"artifacts-written" => "game.static_smoke",
"failed" => "inspect-error",
_ => "generator-revision",
}
.to_string(),
error: error.map(str::to_string),
updated_at: unix_timestamp(),
};
write_agent_run_trace_payload(root, &trace)
}
pub(crate) fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'static str {
match status {
"running" => "loop-running",
@@ -726,113 +571,6 @@ pub(crate) fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'stat
}
}
pub(crate) fn agent_run_lifecycle_status(status: &str) -> &'static str {
match status {
"running" | "needs-revision" | "artifacts-written" | "preview-running" => "running",
"waiting" | "preview-stopped" => "waiting",
"pending" => "pending",
"killed" => "killed",
"failed" => "failed",
"passed" => "done",
_ => "scheduled",
}
}
pub(crate) fn append_agent_run_activity(
root: &Path,
run_id: &str,
event: &str,
message: &str,
) -> Result<(), String> {
append_agent_run_jsonl(
root,
".agent/activity.jsonl",
&serde_json::json!({
"timestamp": unix_timestamp(),
"runId": run_id,
"event": event,
"message": message,
}),
)
}
pub(crate) fn append_agent_run_output(
root: &Path,
run_id: &str,
event: &str,
message: &str,
) -> Result<(), String> {
append_agent_run_jsonl(
root,
".agent/output.jsonl",
&serde_json::json!({
"timestamp": unix_timestamp(),
"runId": run_id,
"event": event,
"content": message,
}),
)
}
pub(crate) fn append_agent_run_jsonl(
root: &Path,
relative_path: &str,
value: &serde_json::Value,
) -> Result<(), String> {
let path = root.join(relative_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建 Agent 事件目录失败:{}: {error}", parent.display()))?;
}
let line =
serde_json::to_string(value).map_err(|error| format!("序列化 Agent 事件失败:{error}"))?;
append_jsonl_line(&path, &line, "Agent 事件")
}
pub(crate) fn write_agent_run_context_bundle(
root: &Path,
trace: &GameCreationAgentRunTrace,
) -> Result<(), String> {
let bundle_path = root.join(".agent/context.bundle.json");
if let Some(parent) = bundle_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent context bundle 目录失败:{}: {error}",
parent.display()
)
})?;
}
let manifest = read_manifest_for_project(root).ok();
let payload = serde_json::json!({
"schemaVersion": "game-creator-context-bundle.v1",
"runId": trace.run_id,
"commandId": trace.command_id,
"goal": trace.goal,
"lifecycleStatus": trace.lifecycle_status.as_deref().unwrap_or_else(|| agent_run_lifecycle_status(&trace.status)),
"status": trace.status,
"nextStep": trace.next_step,
"memory": {
"short": "memory/session.md",
"long": "memory/project.md",
"blackboard": PROJECT_BLACKBOARD_MEMORY_PATH,
"agents": "memory/agents/"
},
"manifest": manifest,
"trace": ".agent/run.latest.json",
"activity": ".agent/activity.jsonl",
"output": ".agent/output.jsonl",
"updatedAt": unix_timestamp()
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("生成 Agent context bundle 失败:{error}"))?;
fs::write(&bundle_path, content).map_err(|error| {
format!(
"写入 Agent context bundle 失败:{}: {error}",
bundle_path.display()
)
})
}
pub(crate) fn count_agent_tool_calls(steps: &[GameCreationAgentRunStep]) -> Result<u16, String> {
let count = steps.iter().try_fold(0u16, |current, step| {
let step_count = u16::try_from(step.tool_calls.len())
@@ -25,17 +25,6 @@ pub(crate) fn truncate_agent_runtime_text(value: &str, max_chars: usize) -> Stri
output
}
pub(crate) fn truncate_agent_runtime_text_preserving_tail(value: &str, max_chars: usize) -> String {
let value = value.trim();
let count = value.chars().count();
if count <= max_chars {
return value.to_string();
}
let mut characters = value.chars().rev().take(max_chars).collect::<Vec<_>>();
characters.reverse();
format!("…{}", characters.into_iter().collect::<String>())
}
pub(crate) fn sanitize_agent_runtime_text(value: &str, max_chars: usize) -> String {
truncate_agent_runtime_text(sanitize_prompt_context(value).trim(), max_chars)
}
@@ -133,32 +122,6 @@ pub(crate) fn redact_agent_runtime_project_paths_raw(root: &Path, value: &str) -
redacted
}
pub(crate) fn redact_agent_runtime_project_paths_preserving_tail(
root: &Path,
value: &str,
max_chars: usize,
) -> String {
let mut redacted = value.to_string();
let root_display = root.to_string_lossy();
if !root_display.is_empty() {
redacted = redacted.replace(root_display.as_ref(), "$PROJECT_ROOT");
#[cfg(windows)]
if let Some(non_verbatim_root) = root_display.strip_prefix(r"\\?\") {
redacted = redacted.replace(non_verbatim_root, "$PROJECT_ROOT");
}
}
if let Ok(canonical_root) = root.canonicalize() {
let canonical_display = canonical_root.to_string_lossy();
if !canonical_display.is_empty() && canonical_display != root_display {
redacted = redacted.replace(canonical_display.as_ref(), "$PROJECT_ROOT");
}
}
truncate_agent_runtime_text_preserving_tail(
sanitize_prompt_context(&redacted).trim(),
max_chars,
)
}
/// 把一个身份值压成可安全拼进项目内路径的分量:只保留 ASCII 字母数字和 `-`、`_`、`.`,
/// 其余折叠成 `-`,超长截断;全空时退回 `fallback`。
pub(crate) fn agent_runtime_confirmation_path_component(value: &str, fallback: &str) -> String {
@@ -10,6 +10,7 @@ use crate::project::{normalize_relative_path, unix_timestamp, validate_project_r
use std::fs::{self, File};
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
#[cfg(unix)]
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
@@ -333,12 +334,3 @@ pub(crate) fn try_open_game_creator_agent_runtime_task_lock_file(
root.join(relative_path).display()
))
}
#[cfg(test)]
pub(crate) fn game_creator_agent_runtime_task_lock_is_available(
root: &Path,
agent_id: &str,
) -> Result<bool, String> {
let relative_path = format!(".agent/runtime/locks/{agent_id}.lock");
Ok(try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?.is_some())
}