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
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:
@@ -11,6 +11,9 @@
|
|||||||
pub(crate) mod package_layout;
|
pub(crate) mod package_layout;
|
||||||
|
|
||||||
pub const VERSION: &str = package_layout::CODEX_VERSION;
|
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 CLI_VERSION: &str = package_layout::CODEX_CLI_VERSION;
|
||||||
pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA;
|
pub const SCHEMA: &str = package_layout::CODEX_MANIFEST_SCHEMA;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::BTreeSet;
|
|
||||||
use std::io::{Seek, SeekFrom};
|
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
||||||
|
|
||||||
mod art_manifest;
|
mod art_manifest;
|
||||||
mod claude_code_cli;
|
mod claude_code_cli;
|
||||||
|
|||||||
@@ -211,16 +211,6 @@ pub(crate) fn start_game_creator_manifest_invalidation_event_sink(
|
|||||||
Ok(GameCreatorManifestInvalidationEventSink { port, token })
|
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(
|
pub(crate) fn validate_game_creator_manifest_invalidation_event_sink(
|
||||||
port: u16,
|
port: u16,
|
||||||
token: &str,
|
token: &str,
|
||||||
@@ -266,10 +256,6 @@ pub(crate) struct GameCreatorManifestInvalidationEventSinkTestGuard {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
impl GameCreatorManifestInvalidationEventSinkTestGuard {
|
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> {
|
pub(crate) fn configured_sink(&self) -> Option<GameCreatorManifestInvalidationEventSink> {
|
||||||
lock_game_creator_manifest_invalidation_event_sinks()
|
lock_game_creator_manifest_invalidation_event_sinks()
|
||||||
.first()
|
.first()
|
||||||
|
|||||||
@@ -2,57 +2,24 @@ use super::*;
|
|||||||
|
|
||||||
mod canvas_generation;
|
mod canvas_generation;
|
||||||
mod draft_validation;
|
mod draft_validation;
|
||||||
mod draft_writer;
|
|
||||||
mod external_generation_state;
|
mod external_generation_state;
|
||||||
mod loop_orchestration;
|
mod loop_orchestration;
|
||||||
mod pass_artifacts;
|
mod pass_artifacts;
|
||||||
mod prompt_context;
|
mod prompt_context;
|
||||||
mod trace;
|
mod trace;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use canvas_generation::request_platform_art_asset_with_options_for_test;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use canvas_generation::submit_external_generation_request;
|
pub(crate) use canvas_generation::submit_external_generation_request;
|
||||||
pub(in crate::agent) use canvas_generation::{
|
pub(in crate::agent) use canvas_generation::{
|
||||||
admit_platform_art_generation_at, commit_prepared_platform_art_asset_at,
|
admit_platform_art_generation_at, generate_admitted_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, register_existing_platform_art_slices_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,
|
|
||||||
restore_platform_art_asset_bytes_at,
|
restore_platform_art_asset_bytes_at,
|
||||||
retained_platform_art_generation_runtime_spritesheet_identity_at,
|
retained_platform_art_generation_runtime_spritesheet_identity_at,
|
||||||
retained_platform_art_generation_runtime_state_matches_direct_stage_at,
|
retained_platform_art_generation_runtime_state_matches_direct_stage_at,
|
||||||
validate_platform_art_png_bytes_with_limits, AdmittedPlatformArtGeneration,
|
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)]
|
#[allow(unused_imports)]
|
||||||
pub(crate) use canvas_generation::{
|
pub(crate) use canvas_generation::{
|
||||||
build_platform_art_asset_prompt, editor_api_key_is_configured, generate_platform_art_asset_at,
|
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,
|
project_canvas_asset_media_types, role_has_canvas_assets, validate_platform_art_icon_prompt,
|
||||||
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
|
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
|
||||||
};
|
};
|
||||||
#[allow(unused_imports)]
|
pub(crate) use canvas_generation::{
|
||||||
pub(crate) use draft_validation::{
|
external_canvas_placeholder, external_editor_json_request, external_editor_response_data,
|
||||||
contains_any, evaluate_game_draft, extract_json_payload, fnv1a64,
|
external_generation_poll_after_ms, platform_art_generation_error_needs_reconciliation,
|
||||||
parse_llm_game_draft_response, render_evaluator_findings, render_planner_spec,
|
prepare_external_canvas_generation_context, resolve_canvas_resource_download_with_access,
|
||||||
strip_llm_thinking_blocks, validate_canvas_rendering_html, validate_game_html_smoke,
|
ExternalCanvasGenerationContext,
|
||||||
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 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)]
|
#[allow(unused_imports)]
|
||||||
pub(crate) use loop_orchestration::{
|
pub(crate) use loop_orchestration::{
|
||||||
emit_agent_progress, game_creator_agent_llm_error_is_mud_points_insufficient,
|
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,
|
game_creator_runtime_error_is_mud_points_insufficient, request_game_creator_llm_text,
|
||||||
AgentProgressEmitter, GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND,
|
AgentProgressEmitter, GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND,
|
||||||
};
|
};
|
||||||
#[allow(unused_imports)]
|
pub(crate) use pass_artifacts::agent_role_memory_relative_path_for_task;
|
||||||
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,
|
|
||||||
};
|
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub(crate) use prompt_context::{
|
pub(crate) use prompt_context::{
|
||||||
append_prompt_context, asset_source_kind_label, game_creator_generator_user_prompt,
|
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,
|
render_local_conversation_prompt_context_for_session, sanitize_error_context,
|
||||||
sanitize_prompt_context, truncate_prompt_context, truncate_prompt_context_preserving_tail,
|
sanitize_prompt_context, truncate_prompt_context, truncate_prompt_context_preserving_tail,
|
||||||
};
|
};
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub(crate) use trace::{
|
pub(crate) use trace::{
|
||||||
agent_run_lifecycle_status, agent_run_stop_reason, agent_trace_step, agent_trace_step_owned,
|
agent_trace_step_owned, append_preview_log, append_preview_start_trace_step,
|
||||||
append_agent_run_activity, append_agent_run_jsonl, append_agent_run_output,
|
append_preview_stop_trace_step, append_static_smoke_manual_trace_step,
|
||||||
append_agent_run_trace_step, append_local_artifact_write_step, append_preview_log,
|
record_replaced_preview_stop, with_task_context,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,82 +1,7 @@
|
|||||||
use super::*;
|
|
||||||
use oxc_allocator::Allocator as JavascriptAllocator;
|
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_parser::Parser as JavascriptParser;
|
||||||
use oxc_semantic::SemanticBuilder as JavascriptSemanticBuilder;
|
use oxc_semantic::SemanticBuilder as JavascriptSemanticBuilder;
|
||||||
use oxc_span::{GetSpan as JavascriptGetSpan, SourceType as JavascriptSourceType};
|
use oxc_span::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
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
|
pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||||
let mut hash = 0xcbf29ce484222325_u64;
|
let mut hash = 0xcbf29ce484222325_u64;
|
||||||
@@ -87,14 +12,6 @@ pub(crate) fn fnv1a64(bytes: &[u8]) -> u64 {
|
|||||||
hash
|
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 {
|
pub(crate) fn strip_llm_thinking_blocks(content: &str) -> String {
|
||||||
let mut output = String::new();
|
let mut output = String::new();
|
||||||
let mut rest = content;
|
let mut rest = content;
|
||||||
@@ -113,91 +30,6 @@ pub(crate) fn strip_llm_thinking_blocks(content: &str) -> String {
|
|||||||
output.trim().to_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> {
|
pub(crate) fn validate_playable_game_html(html: &str, label: &str) -> Result<(), String> {
|
||||||
let lower_html = html.to_ascii_lowercase();
|
let lower_html = html.to_ascii_lowercase();
|
||||||
if !contains_any(
|
if !contains_any(
|
||||||
@@ -381,28 +213,6 @@ pub(crate) fn contains_any(haystack: &str, needles: &[&str]) -> bool {
|
|||||||
needles.iter().any(|needle| haystack.contains(needle))
|
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 {
|
fn is_html_ascii_whitespace(byte: u8) -> bool {
|
||||||
matches!(byte, b'\t' | b'\n' | b'\x0c' | b'\r' | b' ')
|
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(())
|
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 {
|
fn html_script_type_is_executable(script_tag: &str) -> bool {
|
||||||
if let Some(script_type) = html_attribute_value(script_tag, "type") {
|
if let Some(script_type) = html_attribute_value(script_tag, "type") {
|
||||||
let script_type = script_type.trim();
|
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::*;
|
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(
|
pub(crate) fn agent_role_memory_relative_path(
|
||||||
group_definition: AgentGroupDefinition,
|
group_definition: AgentGroupDefinition,
|
||||||
role_definition: AgentRoleDefinition,
|
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}"))
|
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(
|
pub(crate) fn append_static_smoke_manual_trace_step(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
result: &LimitedLocalCommandResult,
|
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(
|
pub(crate) fn append_preview_start_trace_step(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
preview: &LocalPreviewResult,
|
preview: &LocalPreviewResult,
|
||||||
@@ -670,46 +555,6 @@ pub(crate) fn ready_task_ids_for_tasks(tasks: &[GameCreationAppTaskState]) -> Ve
|
|||||||
.collect()
|
.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 {
|
pub(crate) fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'static str {
|
||||||
match status {
|
match status {
|
||||||
"running" => "loop-running",
|
"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> {
|
pub(crate) fn count_agent_tool_calls(steps: &[GameCreationAgentRunStep]) -> Result<u16, String> {
|
||||||
let count = steps.iter().try_fold(0u16, |current, step| {
|
let count = steps.iter().try_fold(0u16, |current, step| {
|
||||||
let step_count = u16::try_from(step.tool_calls.len())
|
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
|
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 {
|
pub(crate) fn sanitize_agent_runtime_text(value: &str, max_chars: usize) -> String {
|
||||||
truncate_agent_runtime_text(sanitize_prompt_context(value).trim(), max_chars)
|
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
|
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 字母数字和 `-`、`_`、`.`,
|
/// 把一个身份值压成可安全拼进项目内路径的分量:只保留 ASCII 字母数字和 `-`、`_`、`.`,
|
||||||
/// 其余折叠成 `-`,超长截断;全空时退回 `fallback`。
|
/// 其余折叠成 `-`,超长截断;全空时退回 `fallback`。
|
||||||
pub(crate) fn agent_runtime_confirmation_path_component(value: &str, fallback: &str) -> String {
|
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::fs::{self, File};
|
||||||
use std::io::{Seek, SeekFrom, Write};
|
use std::io::{Seek, SeekFrom, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
#[cfg(unix)]
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
use std::time::Duration;
|
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()
|
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())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user