Merge pull request '清理 Rust 编译 warning(含 AGC 壳)并加门禁(#649)' (#650) from chore/rust-compile-warnings into master
Project CI / AI game creator shell Rust crates (push) Successful in 7m33s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m42s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m18s
Project CI / Backend tests (push) Successful in 9m43s
Project CI / Frontend tests (push) Successful in 3m19s
Project CI / Repository checks (push) Successful in 5m54s
Project CI / Native shell tests (push) Successful in 7m47s
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Successful in 7m33s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m42s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 9m18s
Project CI / Backend tests (push) Successful in 9m43s
Project CI / Frontend tests (push) Successful in 3m19s
Project CI / Repository checks (push) Successful in 5m54s
Project CI / Native shell tests (push) Successful in 7m47s
Project CI / AI game creator shell web tests (push) Has been cancelled
Reviewed-on: http://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/650
This commit was merged in pull request #650.
This commit is contained in:
@@ -152,6 +152,9 @@ jobs:
|
||||
- name: Run AI game creator shell Rust shard 4/4
|
||||
run: npm run check:native-shells:agc-rust-shard-4
|
||||
|
||||
- name: Run AGC shell Rust warning check (Linux)
|
||||
run: npm run check:rust-warnings -- --profile=agc-linux
|
||||
|
||||
- name: Report isolated Rust compilation cache
|
||||
if: always()
|
||||
run: bash scripts/ci-rust-cache.sh report
|
||||
@@ -266,6 +269,9 @@ jobs:
|
||||
- name: Run AI game creator shell shared crate gates
|
||||
run: npm run check:native-shells:agc-rust-crates
|
||||
|
||||
- name: Run server-rs Rust warning check
|
||||
run: npm run check:rust-warnings -- --profile=server-rs
|
||||
|
||||
- name: Report isolated Rust compilation cache
|
||||
if: always()
|
||||
run: bash scripts/ci-rust-cache.sh report
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
# Git 在链接工作树里执行 Hook 时会注入 GIT_DIR 等仓库定位变量,优先级高于 cwd;
|
||||
# 钩子链(npm → check:repository-ci → 测试夹具)会继承它们并写到真实仓库,故在入口统一清除。
|
||||
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_PREFIX GIT_CONFIG_PARAMETERS GIT_CEILING_DIRECTORIES
|
||||
# Rust 告警检查对所有分支一视同仁,但按内容触发:脚本自己看本次推送的 refs 有没有碰 *.rs/Cargo.toml/Cargo.lock。
|
||||
# refs 落成临时文件传给脚本,这样最后一行原有的钩子命令保持逐字不变、且仍拿到完整的 stdin。
|
||||
push_refs="$(mktemp)"
|
||||
cat > "${push_refs}"
|
||||
trap 'rm -f "${push_refs}"' EXIT
|
||||
node scripts/check-rust-warnings.mjs --only-if-changed --refs-file="${push_refs}"
|
||||
exec < "${push_refs}"
|
||||
npm run check:pre-push-master -- "$@"
|
||||
|
||||
@@ -337,7 +337,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
)?;
|
||||
}
|
||||
let mut provider_fragment_sections = BTreeSet::new();
|
||||
for (name, _, section_id) in provider_fragment_entries(&manifest.provider_fragments) {
|
||||
for (name, section_id) in provider_fragment_entries(&manifest.provider_fragments) {
|
||||
validate_section_reference(section_id, §ions, name)?;
|
||||
if !provider_fragment_sections.insert(section_id) {
|
||||
return Err(format!("Provider fragment section 重复引用:{section_id}"));
|
||||
@@ -370,7 +370,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
used_sections.extend(
|
||||
provider_fragment_entries(&manifest.provider_fragments)
|
||||
.into_iter()
|
||||
.map(|(_, _, section_id)| section_id.clone()),
|
||||
.map(|(_, section_id)| section_id.clone()),
|
||||
);
|
||||
for section_id in sections.keys() {
|
||||
if !used_sections.contains(section_id) {
|
||||
@@ -379,7 +379,7 @@ pub fn compile_manifest(manifest_path: &Path) -> Result<CompiledPromptBundle, St
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut rust_source = render_rust(&manifest, §ions);
|
||||
let mut rust_source = render_rust(&manifest);
|
||||
rust_source.push_str(&render_text_macro(&texts));
|
||||
let specialist_nodes = manifest
|
||||
.agent_catalog
|
||||
@@ -443,7 +443,7 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
|
||||
register("roleOverlays", section);
|
||||
}
|
||||
}
|
||||
for (_, _, section) in provider_fragment_entries(&manifest.provider_fragments) {
|
||||
for (_, section) in provider_fragment_entries(&manifest.provider_fragments) {
|
||||
register("providerFragments", section);
|
||||
}
|
||||
|
||||
@@ -463,48 +463,40 @@ fn validate_section_ownership(manifest: &PromptBundleManifest) -> Result<(), Str
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn provider_fragment_entries(
|
||||
fragments: &ProviderFragments,
|
||||
) -> [(&'static str, &'static str, &String); 8] {
|
||||
/// 返回 `(manifest 路径键, section 键)`:常量名标签随退役 Runtime 的 `RUNTIME_PROMPT_*` 生成逻辑一并停发,
|
||||
/// 只剩校验与所有权归属两处用途,因此不再携带常量名字符串。
|
||||
fn provider_fragment_entries(fragments: &ProviderFragments) -> [(&'static str, &String); 8] {
|
||||
[
|
||||
(
|
||||
"providerFragments.isolatedToolContract",
|
||||
"RUNTIME_PROMPT_PROVIDER_ISOLATED_TOOL_CONTRACT_SECTION",
|
||||
&fragments.isolated_tool_contract,
|
||||
),
|
||||
(
|
||||
"providerFragments.autonomousRunProfile",
|
||||
"RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_RUN_PROFILE_SECTION",
|
||||
&fragments.autonomous_run_profile,
|
||||
),
|
||||
(
|
||||
"providerFragments.autonomousSupervisorManifest",
|
||||
"RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_SUPERVISOR_MANIFEST_SECTION",
|
||||
&fragments.autonomous_supervisor_manifest,
|
||||
),
|
||||
(
|
||||
"providerFragments.initialCollaborationRepair",
|
||||
"RUNTIME_PROMPT_PROVIDER_INITIAL_COLLABORATION_REPAIR_SECTION",
|
||||
&fragments.initial_collaboration_repair,
|
||||
),
|
||||
(
|
||||
"providerFragments.autonomousInitialCollaborationRepair",
|
||||
"RUNTIME_PROMPT_PROVIDER_AUTONOMOUS_INITIAL_COLLABORATION_REPAIR_SECTION",
|
||||
&fragments.autonomous_initial_collaboration_repair,
|
||||
),
|
||||
(
|
||||
"providerFragments.supervisorDeliveryConvergenceRepair",
|
||||
"RUNTIME_PROMPT_PROVIDER_SUPERVISOR_DELIVERY_CONVERGENCE_REPAIR_SECTION",
|
||||
&fragments.supervisor_delivery_convergence_repair,
|
||||
),
|
||||
(
|
||||
"providerFragments.manifestDagWaitRepair",
|
||||
"RUNTIME_PROMPT_PROVIDER_MANIFEST_DAG_WAIT_REPAIR_SECTION",
|
||||
&fragments.manifest_dag_wait_repair,
|
||||
),
|
||||
(
|
||||
"providerFragments.delegatedPlaytestRepair",
|
||||
"RUNTIME_PROMPT_PROVIDER_DELEGATED_PLAYTEST_REPAIR_SECTION",
|
||||
&fragments.delegated_playtest_repair,
|
||||
),
|
||||
]
|
||||
@@ -931,101 +923,16 @@ fn validate_nonempty(value: &str, field: &str) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_rust(manifest: &PromptBundleManifest, sections: &BTreeMap<String, String>) -> String {
|
||||
/// 只生成运行期仍被引用的内容:Agent 目录(supervisor/分组/角色)与 `prompt_text!` 目录由调用方
|
||||
/// 分别拼接。已退役 Runtime 的 section 视图(`runtime_prompt_bundle_section`、各
|
||||
/// `RUNTIME_PROMPT_*` 常量)不再产出——它们没有任何消费者,继续生成只会变成 dead_code。
|
||||
fn render_rust(manifest: &PromptBundleManifest) -> String {
|
||||
let mut output =
|
||||
String::from("// @generated by build.rs from prompts/runtime/manifest.json\n\n");
|
||||
output.push_str(&format!(
|
||||
"#[cfg(test)]\npub(crate) const RUNTIME_PROMPT_BUNDLE_ID: &str = {};\n",
|
||||
rust_literal(&manifest.id)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"#[cfg(test)]\npub(crate) const RUNTIME_PROMPT_BUNDLE_VERSION: &str = {};\n",
|
||||
rust_literal(&manifest.version)
|
||||
));
|
||||
output.push_str("pub(crate) fn runtime_prompt_bundle_section(id: &str) -> Option<&'static str> {\n match id {\n");
|
||||
for (id, content) in sections {
|
||||
output.push_str(&format!(
|
||||
" {} => Some({}),\n",
|
||||
rust_literal(id),
|
||||
rust_literal(content)
|
||||
));
|
||||
}
|
||||
output.push_str(" _ => None,\n }\n}\n");
|
||||
output.push_str(&render_string_slice_const(
|
||||
"RUNTIME_PROMPT_RUNTIME_COMPOSITION",
|
||||
&manifest.compositions.runtime,
|
||||
));
|
||||
output.push_str(&render_string_slice_const(
|
||||
"RUNTIME_PROMPT_SUPERVISOR_COMPOSITION",
|
||||
&manifest.compositions.supervisor,
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION: &[&str] = &[{}, {}];\n",
|
||||
rust_literal(&manifest.compositions.supervisor_chat.identity),
|
||||
rust_literal(&manifest.compositions.supervisor_chat.final_reply)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_PLATFORM_DEFAULT_SECTION: &str = {};\n",
|
||||
rust_literal(&manifest.variants.platform.default)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_PLATFORM_LINUX_SECTION: &str = {};\n",
|
||||
rust_literal(&manifest.variants.platform.linux)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_VISUAL_EDITOR_SECTION: &str = {};\n",
|
||||
rust_literal(&manifest.variants.visual_contract.editor_configured)
|
||||
));
|
||||
output.push_str(&format!(
|
||||
"pub(crate) const RUNTIME_PROMPT_VISUAL_NO_EDITOR_SECTION: &str = {};\n",
|
||||
rust_literal(&manifest.variants.visual_contract.editor_unavailable)
|
||||
));
|
||||
output.push_str("pub(crate) const RUNTIME_PROMPT_ROLE_OVERLAYS: &[(&str, &[&str])] = &[\n");
|
||||
for overlay in &manifest.role_overlays {
|
||||
output.push_str(&format!(
|
||||
" ({}, &{}),\n",
|
||||
rust_literal(&overlay.agent_id),
|
||||
render_string_slice(&overlay.sections)
|
||||
));
|
||||
}
|
||||
output.push_str("];\n\n");
|
||||
output.push_str(&render_provider_fragment_constants(
|
||||
&manifest.provider_fragments,
|
||||
));
|
||||
output.push_str(&render_agent_catalog(&manifest.agent_catalog));
|
||||
output
|
||||
}
|
||||
|
||||
fn render_provider_fragment_constants(fragments: &ProviderFragments) -> String {
|
||||
provider_fragment_entries(fragments)
|
||||
.into_iter()
|
||||
.map(|(_, name, section_id)| {
|
||||
format!(
|
||||
"pub(crate) const {name}: &str = {};\n",
|
||||
rust_literal(section_id)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_string_slice_const(name: &str, values: &[String]) -> String {
|
||||
format!(
|
||||
"pub(crate) const {name}: &[&str] = &{};\n",
|
||||
render_string_slice(values)
|
||||
)
|
||||
}
|
||||
|
||||
fn render_string_slice(values: &[String]) -> String {
|
||||
format!(
|
||||
"[{}]",
|
||||
values
|
||||
.iter()
|
||||
.map(|value| rust_literal(value))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
|
||||
fn render_agent_catalog(catalog: &AgentCatalog) -> String {
|
||||
let supervisor_role = &catalog.supervisor.roles[0];
|
||||
let mut output = format!(
|
||||
@@ -1072,11 +979,9 @@ fn render_role_array(name: &str, roles: &[AgentRole]) -> String {
|
||||
);
|
||||
for role in roles {
|
||||
output.push_str(&format!(
|
||||
" AgentRoleDefinition {{ id: {}, role: {}, task_id: {}, tool_id: {}, brief_path_name: {} }},\n",
|
||||
rust_literal(&role.id),
|
||||
" AgentRoleDefinition {{ role: {}, task_id: {}, brief_path_name: {} }},\n",
|
||||
rust_literal(&role.role),
|
||||
rust_literal(&role.task_id),
|
||||
rust_literal(&role.tool_id),
|
||||
rust_literal(&role.brief_path_name)
|
||||
));
|
||||
}
|
||||
@@ -1086,11 +991,9 @@ fn render_role_array(name: &str, roles: &[AgentRole]) -> String {
|
||||
|
||||
fn render_group_value(group: &AgentGroup, roles: &str) -> String {
|
||||
format!(
|
||||
"AgentGroupDefinition {{ id: {}, label: {}, role: {}, brief_path_name: {}, roles: {} }}",
|
||||
"AgentGroupDefinition {{ id: {}, label: {}, roles: {} }}",
|
||||
rust_literal(&group.id),
|
||||
rust_literal(&group.label),
|
||||
rust_literal(&group.role),
|
||||
rust_literal(&group.brief_path_name),
|
||||
roles
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
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;
|
||||
mod codex_app_server;
|
||||
mod codex_cli;
|
||||
mod codex_provider;
|
||||
// 既有调用点仍按旧名引用这两棵子树(`crate::agent::codex_cli::…` 等);保留同义别名,
|
||||
// 使收敛只改变挂载点,不改变对外路径。
|
||||
pub(crate) use codex_provider::{app_server as codex_app_server, cli as codex_cli};
|
||||
mod codex_provider_proxy;
|
||||
mod design_runtime;
|
||||
pub(crate) mod design_tools;
|
||||
@@ -34,20 +33,22 @@ mod direct_validation;
|
||||
pub(crate) use direct_validation::browser_tool_result;
|
||||
mod generation;
|
||||
pub(crate) mod json_sidecar;
|
||||
mod provider_request_snapshot;
|
||||
pub(crate) mod redaction;
|
||||
mod runtime_error;
|
||||
mod runtime_models;
|
||||
mod skill_pack;
|
||||
mod task_lock;
|
||||
mod thread_manager;
|
||||
pub mod tool;
|
||||
|
||||
pub(crate) use art_manifest::*;
|
||||
use claude_code_cli::*;
|
||||
use art_manifest::*;
|
||||
/// 首页直连回合只在生产入口接线,测试态不编译。
|
||||
#[cfg(not(test))]
|
||||
pub(crate) use claude_code_cli::direct_game_creator_claude_code_home_chat;
|
||||
pub(crate) use claude_code_cli::{
|
||||
cancel_direct_claude_code_turn_at, claude_code_failure_to_turn_error,
|
||||
direct_game_creator_claude_code_chat_at, direct_game_creator_claude_code_home_chat,
|
||||
game_creator_claude_code_cli_route_error, game_creator_claude_code_cli_version_identity,
|
||||
direct_game_creator_claude_code_chat_at, game_creator_claude_code_cli_route_error,
|
||||
game_creator_claude_code_cli_version_identity,
|
||||
};
|
||||
pub(crate) use codex_app_server::direct_game_creator_codex_chat_at;
|
||||
pub(crate) use codex_app_server::turn_error::*;
|
||||
@@ -76,12 +77,11 @@ pub(crate) use direct_tools_mcp::*;
|
||||
pub(crate) use direct_validation::DirectValidationConfig;
|
||||
pub(crate) use generation::*;
|
||||
pub(crate) use json_sidecar::*;
|
||||
pub(in crate::agent) use provider_request_snapshot::*;
|
||||
pub(crate) use redaction::*;
|
||||
pub(crate) use runtime_error::*;
|
||||
pub(in crate::agent) use runtime_models::*;
|
||||
pub(crate) use skill_pack::*;
|
||||
pub(crate) use task_lock::*;
|
||||
pub(crate) use thread_manager::dispatch::*;
|
||||
pub(crate) use thread_manager::queue::*;
|
||||
pub(crate) use thread_manager::wire::*;
|
||||
pub(crate) use thread_manager::*;
|
||||
|
||||
@@ -30,6 +30,7 @@ const CLAUDE_DIRECT_MAX_TURNS: u32 = 16;
|
||||
/// Codex 侧对同一套工具桥已设 `tool_timeout_sec=6600`(110 分钟),这里按同一口径对齐。
|
||||
const CLAUDE_CODE_MCP_TOOL_TIMEOUT_MS: u64 = 110 * 60 * 1_000;
|
||||
const CLAUDE_CODE_OUTPUT_MAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
#[cfg(not(test))]
|
||||
const CLAUDE_CODE_PROMPT_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
const CLAUDE_CODE_MODEL_MAX_CHARS: usize = 200;
|
||||
/// 超时收口时给 sidecar stderr 的排空窗口。
|
||||
@@ -976,6 +977,7 @@ fn configure_claude_code_process(command: &mut tokio::process::Command) {
|
||||
super::codex_cli::configure_game_creator_codex_cli_process(command);
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn render_claude_code_prompt(request: &LlmRunRequest) -> Result<String, String> {
|
||||
let messages = serde_json::to_string_pretty(&request.messages)
|
||||
.map_err(|error| format!("序列化 cc Agent 消息失败:{error}"))?;
|
||||
@@ -998,6 +1000,7 @@ fn render_claude_code_prompt(request: &LlmRunRequest) -> Result<String, String>
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn claude_code_schema(request: &LlmRunRequest) -> Option<serde_json::Value> {
|
||||
if request.function_tools.is_empty() {
|
||||
return None;
|
||||
@@ -1273,6 +1276,7 @@ fn parse_claude_code_result(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(in crate::agent) async fn request_game_creator_agent_claude_code_cli(
|
||||
request: LlmRunRequest,
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
@@ -1323,6 +1327,7 @@ pub(in crate::agent) async fn request_game_creator_agent_claude_code_cli(
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) async fn direct_game_creator_claude_code_home_chat(
|
||||
llm: &GameCreatorLlmConfig,
|
||||
system_prompt: String,
|
||||
|
||||
@@ -877,7 +877,7 @@ impl CodexAppServerWorkspaceMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn resolve_direct_codex_project_authority(
|
||||
pub(in crate::agent) fn resolve_direct_codex_project_authority(
|
||||
project_root: &std::path::Path,
|
||||
) -> Result<(std::path::PathBuf, std::path::PathBuf), String> {
|
||||
if !project_root.is_absolute() {
|
||||
|
||||
@@ -304,8 +304,11 @@ mod tests {
|
||||
use std::process::Stdio;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||
|
||||
// 夹具用 `--exact` 复跑本测试二进制,这个字符串必须与真实模块路径逐字一致;本模块随
|
||||
// codex provider 收敛挂到 `agent::codex_provider::app_server::` 下,故同步更新(否则子进程跑 0 个用例、
|
||||
// 父进程等 marker 超时 → `Elapsed(())`)。
|
||||
const FIXTURE: &str =
|
||||
"agent::codex_app_server::process_tree::tests::owned_tree_process_fixture";
|
||||
"agent::codex_provider::app_server::process_tree::tests::owned_tree_process_fixture";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Codex provider 相关逻辑的收敛父模块:凭据、模型目录、OAuth 交接,以及 app-server 与 CLI
|
||||
//! 两条请求路径。
|
||||
//!
|
||||
//! 这两棵子树里的实现目前**休眠**:AGC 交付口径恒走平台会话 / DirectProject 路径,原生
|
||||
//! app-server 与内置 CLI 的请求入口在 Windows 生产构建里没有调用方(只在用例里被引用)。
|
||||
//! 上游 master 仍在活跃编辑这些文件,因此本分支**不删除**它们,改为由本模块统一承载,并在此
|
||||
//! 显式允许 dead code。
|
||||
//!
|
||||
//! `#![allow(dead_code)]` 只作用于本模块及其子模块;其它 crate、其它目录与 CI 门禁不受影响。
|
||||
//! 代价是**本子树内后续新增的 dead code 不会再被任何机器检查发现**。这个例外的理由、失效边界
|
||||
//! 与退出条件写在 dev 运维文档「Rust 编译告警门禁」一节,改这里时请一并维护。
|
||||
//!
|
||||
//! 两个子模块用 `#[path]` 就地挂载,不搬动文件:这样与上游活跃编辑区的冲突面只剩本文件与
|
||||
//! `agent.rs` 的两行,子树内部的相对路径(`super::…`)由下面的转发与别名保持不变。
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Codex app-server:连接、进程池、模型目录、OAuth 交接与 Direct 回合编排。
|
||||
#[path = "../codex_app_server/mod.rs"]
|
||||
pub(crate) mod app_server;
|
||||
|
||||
/// 内置 Codex CLI:随包定位与校验、CLI 请求路径。
|
||||
#[path = "../codex_cli.rs"]
|
||||
pub(crate) mod cli;
|
||||
|
||||
// 子树内部既有 `super::codex_cli::…`(相对路径)引用;挂载后 `super` 变成本模块,这里保留同义
|
||||
// 别名,免去改动活跃编辑中的文件。
|
||||
pub(crate) use cli as codex_cli;
|
||||
@@ -6,11 +6,16 @@ use crate::{
|
||||
GameCreatorAgentProgressEvent, GameCreatorManifestInvalidatedEvent,
|
||||
GameCreatorManifestInvalidationEventSink, GameCreatorManifestInvalidationRelayEnvelope,
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
#[cfg(not(test))]
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
#[cfg(not(test))]
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
#[cfg(not(test))]
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::Emitter;
|
||||
@@ -226,16 +231,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,
|
||||
@@ -281,10 +276,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()
|
||||
|
||||
@@ -4772,7 +4772,6 @@ async fn run_direct_game_creator_turn_inner(
|
||||
}
|
||||
}
|
||||
};
|
||||
drop(observer);
|
||||
reply_result
|
||||
} else {
|
||||
let mut feedback_prompt = prompt.to_string();
|
||||
|
||||
@@ -3353,7 +3353,7 @@ async fn bridge_environment_check(
|
||||
root: &Path,
|
||||
arguments: &Value,
|
||||
) -> Result<Value, EnvironmentCheckError> {
|
||||
let arguments = ToolArgumentsRejection::check(arguments, &[])?;
|
||||
ToolArgumentsRejection::check(arguments, &[])?;
|
||||
enforce_project_permission_policy_rejection(root, "game.run_local")?;
|
||||
let result = crate::environment_check::check_environment(root).await;
|
||||
if result["status"] != "ready" {
|
||||
|
||||
@@ -1925,6 +1925,7 @@ async fn handle_external_mcp_http_request(
|
||||
Ok(Json(response).into_response())
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) async fn start_external_mcp_loopback(
|
||||
root: &Path,
|
||||
controlled_web_search: bool,
|
||||
@@ -2001,6 +2002,7 @@ async fn start_external_mcp_loopback_with_mode(
|
||||
Ok((url, token))
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn stop_external_mcp_loopback() {
|
||||
if let Some(registry) = EXTERNAL_MCP_SERVER.get() {
|
||||
match registry.lock() {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
use super::*;
|
||||
|
||||
mod canvas_generation;
|
||||
mod draft_validation;
|
||||
mod draft_writer;
|
||||
mod external_generation_state;
|
||||
mod loop_orchestration;
|
||||
mod pass_artifacts;
|
||||
mod game_html_validation;
|
||||
mod llm_request;
|
||||
mod prompt_context;
|
||||
mod trace;
|
||||
|
||||
/// 只服务生产入口(`commands::desktop`)的生成入口,测试态不编译。
|
||||
#[cfg(not(test))]
|
||||
pub(crate) use canvas_generation::generate_platform_art_asset_at;
|
||||
#[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,
|
||||
@@ -28,7 +26,16 @@ pub(crate) use canvas_generation::{
|
||||
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 canvas_generation::{
|
||||
generate_platform_art_asset_with_options_at, normalize_platform_art_asset_generation_kind,
|
||||
normalize_platform_art_reference_asset_ids, normalize_platform_art_target_category,
|
||||
platform_art_runtime_references_match_request_contract, validate_platform_art_icon_prompt,
|
||||
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use external_generation_state::platform_art_generation_legacy_external_configuration_fingerprint;
|
||||
#[cfg(test)]
|
||||
pub(in crate::agent) use external_generation_state::PLATFORM_ART_GENERATION_RUNTIME_SCHEMA_VERSION;
|
||||
pub(crate) use external_generation_state::{
|
||||
classify_platform_art_generation_service_identity,
|
||||
platform_art_generation_external_service_fingerprint,
|
||||
@@ -37,78 +44,19 @@ pub(crate) use external_generation_state::{
|
||||
};
|
||||
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,
|
||||
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 game_html_validation::{
|
||||
contains_any, fnv1a64, strip_llm_thinking_blocks, validate_game_html_smoke,
|
||||
};
|
||||
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,
|
||||
generate_platform_art_asset_with_options_at, maybe_generate_platform_art_asset_step,
|
||||
needs_platform_art_asset_generation, normalize_platform_art_asset_generation_kind,
|
||||
normalize_platform_art_reference_asset_ids, normalize_platform_art_target_category,
|
||||
platform_art_asset_art_spec, platform_art_asset_output_extension_matches,
|
||||
platform_art_runtime_references_match_request_contract, prepare_platform_art_asset_output_path,
|
||||
project_canvas_asset_media_types, role_has_canvas_assets, validate_platform_art_icon_prompt,
|
||||
PlatformArtAssetGenerationOptions, PLATFORM_ART_ASSET_GENERATION_KINDS,
|
||||
pub(crate) use llm_request::{
|
||||
game_creator_agent_llm_error_is_mud_points_insufficient,
|
||||
game_creator_agent_llm_error_public_summary, request_game_creator_llm_text,
|
||||
};
|
||||
#[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 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,
|
||||
game_creator_agent_llm_error_public_summary, game_creator_mud_points_insufficient_message,
|
||||
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,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use prompt_context::{
|
||||
append_prompt_context, asset_source_kind_label, game_creator_generator_user_prompt,
|
||||
game_creator_planner_system_prompt, game_creator_planner_user_prompt,
|
||||
game_creator_system_prompt, read_optional_text, redact_secret_tokens,
|
||||
render_local_asset_prompt_context, render_local_conversation_prompt_context,
|
||||
render_local_conversation_prompt_context_for_session, sanitize_error_context,
|
||||
sanitize_prompt_context, truncate_prompt_context, truncate_prompt_context_preserving_tail,
|
||||
redact_secret_tokens, sanitize_error_context, sanitize_prompt_context, truncate_prompt_context,
|
||||
};
|
||||
#[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,
|
||||
append_preview_log, append_preview_start_trace_step, append_preview_stop_trace_step,
|
||||
append_static_smoke_manual_trace_step, record_replaced_preview_stop,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::external_generation_state::{
|
||||
retain_platform_art_generation_runtime_accepted_result, PlatformArtGenerationRuntimeState,
|
||||
};
|
||||
use super::*;
|
||||
use crate::agent::redaction::redact_absolute_path_tokens;
|
||||
use crate::platform_session::{
|
||||
acquire_platform_session_identity_lease, validate_platform_session_identity,
|
||||
PlatformSessionIdentity,
|
||||
@@ -34,21 +35,6 @@ enum PlatformArtGenerationPurpose {
|
||||
GameArtPackage,
|
||||
}
|
||||
|
||||
/// 设计阶段的功能页面输出路径是否合法:必须落在 `assets/ui-pages/` 下、`.png` 结尾,
|
||||
/// 文件名只含安全字符且不超过 80 字符。
|
||||
pub(in crate::agent) fn design_foundation_ui_page_output_path_is_valid(path: &str) -> bool {
|
||||
let Some(page_id) = path
|
||||
.strip_prefix("assets/ui-pages/")
|
||||
.and_then(|value| value.strip_suffix(".png"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
!page_id.is_empty()
|
||||
&& page_id.len() <= 80
|
||||
&& page_id
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
|
||||
}
|
||||
const PLATFORM_ART_LOCAL_RECONCILIATION_PREFIX: &str = "platform-generation-local-reconciliation:";
|
||||
const PLATFORM_ART_SPRITESHEET_TOTAL_DOWNLOAD_BYTES: usize = 32 * 1024 * 1024;
|
||||
const PLATFORM_ART_SPRITESHEET_SINGLE_DOWNLOAD_BYTES: usize = 20 * 1024 * 1024;
|
||||
@@ -317,112 +303,6 @@ fn remove_external_canvas_binding_partial(
|
||||
sync_agent_runtime_sidecar_parent(&path, "External Editor project binding partial")
|
||||
}
|
||||
|
||||
pub(crate) fn project_canvas_asset_media_types(root: &Path) -> Vec<String> {
|
||||
read_manifest_for_project(root)
|
||||
.map(|manifest| {
|
||||
manifest
|
||||
.assets
|
||||
.iter()
|
||||
.filter(|asset| asset.source.kind == GameCreationAppAssetSourceKind::Canvas)
|
||||
.map(|asset| asset.media_type.clone())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_generate_platform_art_asset_step(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
briefs: &[AgentGroupBrief],
|
||||
pass: u8,
|
||||
progress: Option<&AgentProgressEmitter<'_>>,
|
||||
) -> Option<GameCreationAgentRunStep> {
|
||||
if !needs_platform_art_asset_generation(root, briefs) || !editor_api_key_is_configured() {
|
||||
return None;
|
||||
}
|
||||
emit_agent_progress(
|
||||
progress,
|
||||
"editor.image_generate",
|
||||
"美术组正在通过平台 External Editor API 生成首版素材",
|
||||
);
|
||||
let result = generate_platform_art_asset_at(root, prompt, briefs).await;
|
||||
let (status, output_paths, summary) = match result {
|
||||
Ok(generated) => (
|
||||
"completed",
|
||||
vec![generated.asset.local_path.clone()],
|
||||
format!(
|
||||
"已通过平台 External Editor API 生成首版美术素材:{}",
|
||||
generated.asset.local_path
|
||||
),
|
||||
),
|
||||
Err(error) => (
|
||||
"failed",
|
||||
Vec::new(),
|
||||
format!("平台 External Editor API 生成首版美术素材失败:{error}"),
|
||||
),
|
||||
};
|
||||
Some(with_task_context(
|
||||
agent_trace_step_owned(
|
||||
pass,
|
||||
"美术组 / PlatformImageGenerate",
|
||||
status,
|
||||
vec![
|
||||
".agent/spec.md".to_string(),
|
||||
".agent/manifest.json".to_string(),
|
||||
],
|
||||
output_paths,
|
||||
&summary,
|
||||
"agent.tool.platform.editor_image_generate",
|
||||
),
|
||||
"art",
|
||||
"Asset",
|
||||
Some("art-asset-plan"),
|
||||
"asset-generation",
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn needs_platform_art_asset_generation(root: &Path, briefs: &[AgentGroupBrief]) -> bool {
|
||||
let canvas_asset_media_types = project_canvas_asset_media_types(root);
|
||||
briefs.iter().any(|brief| {
|
||||
brief.role_briefs.iter().any(|role_brief| {
|
||||
role_brief.status == "completed"
|
||||
&& role_brief.group_definition.id == "art"
|
||||
&& role_brief.role_definition.id == "asset"
|
||||
&& !role_has_canvas_assets(role_brief, &canvas_asset_media_types)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn editor_api_key_is_configured() -> bool {
|
||||
if external_editor_api_credentials_override_is_active() {
|
||||
return true;
|
||||
}
|
||||
if platform_editor_uses_account_routes() {
|
||||
return platform_session_is_available();
|
||||
}
|
||||
load_game_creator_app_config()
|
||||
.ok()
|
||||
.and_then(|config| trim_config_string(&config.editor_api.api_key))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn role_has_canvas_assets(role_brief: &AgentRoleBrief, media_types: &[String]) -> bool {
|
||||
match (
|
||||
role_brief.group_definition.id,
|
||||
role_brief.role_definition.id,
|
||||
) {
|
||||
("art", "asset") => media_types.iter().any(|media_type| {
|
||||
let media_type = media_type.as_str();
|
||||
media_type.starts_with("image/")
|
||||
|| media_type == "application/vnd.genarrative.image-sequence"
|
||||
}),
|
||||
("audio", "sfx") => media_types
|
||||
.iter()
|
||||
.any(|media_type| media_type.starts_with("audio/")),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct PlatformArtAssetGenerationOptions {
|
||||
pub(crate) output_path: Option<String>,
|
||||
@@ -607,89 +487,6 @@ pub(crate) fn normalize_platform_art_reference_asset_ids(
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn recover_persisted_visual_generation_options(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
prompt: &str,
|
||||
requested: &PlatformArtAssetGenerationOptions,
|
||||
) -> Result<Option<PlatformArtAssetGenerationOptions>, String> {
|
||||
let context = platform_art_generation_runtime_context_from_pending(pending);
|
||||
let Some(state) = read_platform_art_generation_runtime_state(root, &context)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let (path, ratio, size, kind, label, generation_kind) = match context.agent_id.as_str() {
|
||||
"art-director" => (
|
||||
"assets/art-spec.png",
|
||||
"1:1",
|
||||
"1K",
|
||||
GameCreationAppAssetKind::IconSpec,
|
||||
"游戏统一视觉规范图",
|
||||
"spec",
|
||||
),
|
||||
"design-foundation" => (
|
||||
"assets/ui-prototype.png",
|
||||
"16:9",
|
||||
"2K",
|
||||
GameCreationAppAssetKind::UiDesign,
|
||||
"游戏横屏界面原型图",
|
||||
"ui-design",
|
||||
),
|
||||
"art-asset-plan" => (
|
||||
"assets/art-spritesheet.png",
|
||||
"1:1",
|
||||
"1K",
|
||||
GameCreationAppAssetKind::IconSpritesheet,
|
||||
"游戏首版核心美术素材",
|
||||
"icon-spritesheet",
|
||||
),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let mut options = requested.clone();
|
||||
let label = if context.agent_id == "design-foundation"
|
||||
&& options
|
||||
.output_path
|
||||
.as_deref()
|
||||
.is_some_and(design_foundation_ui_page_output_path_is_valid)
|
||||
{
|
||||
"游戏功能页面设计图"
|
||||
} else {
|
||||
label
|
||||
};
|
||||
if options.output_path.is_none() {
|
||||
options.output_path = Some(path.to_string());
|
||||
}
|
||||
if options.aspect_ratio.is_empty() {
|
||||
options.aspect_ratio = ratio.to_string();
|
||||
}
|
||||
if options.image_size.is_empty() {
|
||||
options.image_size = size.to_string();
|
||||
}
|
||||
if options.asset_kind == GameCreationAppAssetKind::Unknown {
|
||||
options.asset_kind = kind;
|
||||
}
|
||||
if options.asset_label.is_empty() {
|
||||
options.asset_label = label.to_string();
|
||||
}
|
||||
let snapshot = platform_art_generation_runtime_request_snapshot(&state)?;
|
||||
// 只恢复经过身份校验的已有请求;显式改参和新请求仍走当前合同。
|
||||
if snapshot.generation_kind == generation_kind
|
||||
&& snapshot.generation_prompt == build_platform_art_asset_prompt(prompt, &options)
|
||||
&& options.asset_kind == kind
|
||||
&& options.aspect_ratio == ratio
|
||||
&& options.image_size == size
|
||||
&& (options.output_path.as_deref() == Some(path)
|
||||
|| (context.agent_id == "design-foundation"
|
||||
&& options
|
||||
.output_path
|
||||
.as_deref()
|
||||
.is_some_and(design_foundation_ui_page_output_path_is_valid)))
|
||||
{
|
||||
Ok(Some(options))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct ExistingPlatformArtAssetFingerprint {
|
||||
local_path: String,
|
||||
@@ -757,14 +554,6 @@ fn prepare_platform_art_asset_output_path_for_mode(
|
||||
Ok(Some((normalized, absolute, existing)))
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_platform_art_asset_output_path(
|
||||
root: &Path,
|
||||
output_path: Option<&str>,
|
||||
) -> Result<Option<(String, PathBuf)>, String> {
|
||||
prepare_platform_art_asset_output_path_for_mode(root, output_path, false)
|
||||
.map(|prepared| prepared.map(|(local_path, absolute_path, _)| (local_path, absolute_path)))
|
||||
}
|
||||
|
||||
pub(crate) fn platform_art_asset_output_extension_matches(
|
||||
output_path: &str,
|
||||
generated_extension: &str,
|
||||
@@ -940,6 +729,7 @@ pub(crate) fn platform_art_generation_error_needs_reconciliation(error: &str) ->
|
||||
|| error.starts_with(PLATFORM_ART_LOCAL_RECONCILIATION_PREFIX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(in crate::agent) fn platform_art_generation_error_result_unknown(error: &str) -> bool {
|
||||
error.starts_with(EXTERNAL_GENERATION_RESULT_UNKNOWN_PREFIX)
|
||||
}
|
||||
@@ -1735,6 +1525,7 @@ pub(crate) fn external_canvas_placeholder(aspect_ratio: &str) -> serde_json::Val
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) async fn generate_platform_art_asset_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
@@ -3264,16 +3055,6 @@ async fn generate_platform_art_asset_with_runtime_options_and_retention_at(
|
||||
Ok(generated)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(in crate::agent) async fn request_platform_art_asset_with_options_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
briefs: &[AgentGroupBrief],
|
||||
options: &PlatformArtAssetGenerationOptions,
|
||||
) -> Result<PreparedPlatformArtAssetGeneration, String> {
|
||||
request_platform_art_asset_with_runtime_options_at(root, prompt, briefs, options, None).await
|
||||
}
|
||||
|
||||
pub(in crate::agent) async fn request_platform_art_asset_with_runtime_options_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
@@ -3969,17 +3750,6 @@ async fn request_platform_art_asset_for_purpose_at(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn request_platform_art_asset_with_options_for_test(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
options: &PlatformArtAssetGenerationOptions,
|
||||
) -> Result<(), String> {
|
||||
request_platform_art_asset_with_options_at(root, prompt, &[], options)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn commit_prepared_platform_art_asset_at(
|
||||
root: &Path,
|
||||
prepared: PreparedPlatformArtAssetGeneration,
|
||||
|
||||
@@ -1,155 +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 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(&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",
|
||||
"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());
|
||||
}
|
||||
}
|
||||
-130
@@ -192,14 +192,6 @@ pub(super) struct PlatformArtGenerationRuntimeRequestSnapshot {
|
||||
pub(super) reference_resource_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(in crate::agent) enum PlatformArtGenerationRuntimeRecovery {
|
||||
Missing,
|
||||
ResumePrepared,
|
||||
ResumeAccepted,
|
||||
ResumeLegacyCompleted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PlatformArtGenerationServiceIdentityMatch {
|
||||
Unbound,
|
||||
@@ -247,20 +239,6 @@ pub(in crate::agent) fn game_creator_agent_runtime_external_generation_exists(
|
||||
|| entry_exists_fail_closed(&agent_runtime_json_sidecar_backup_path(&path))
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn platform_art_generation_runtime_context_from_pending(
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> PlatformArtGenerationRuntimeContext {
|
||||
PlatformArtGenerationRuntimeContext {
|
||||
agent_id: pending.agent_id.clone(),
|
||||
task_id: pending.task_id.clone(),
|
||||
session_id: pending.session_id.clone(),
|
||||
run_id: pending.run_id.clone(),
|
||||
source: pending.source.clone(),
|
||||
action_id: pending.action_id.clone(),
|
||||
action_fingerprint: pending.action_fingerprint.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn request_body_json_and_sha256(
|
||||
request_body: &serde_json::Value,
|
||||
) -> Result<(String, String), String> {
|
||||
@@ -1055,31 +1033,6 @@ pub(super) fn platform_art_generation_runtime_status(
|
||||
&state.status
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn platform_art_generation_runtime_recovery_at(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<PlatformArtGenerationRuntimeRecovery, String> {
|
||||
if pending.action.tool != "canvas.asset_generate" {
|
||||
return Ok(PlatformArtGenerationRuntimeRecovery::Missing);
|
||||
}
|
||||
let context = platform_art_generation_runtime_context_from_pending(pending);
|
||||
let Some(state) = read_platform_art_generation_runtime_state(root, &context)? else {
|
||||
return Ok(PlatformArtGenerationRuntimeRecovery::Missing);
|
||||
};
|
||||
Ok(match state.status.as_str() {
|
||||
PLATFORM_ART_GENERATION_STATUS_PREPARED => {
|
||||
PlatformArtGenerationRuntimeRecovery::ResumePrepared
|
||||
}
|
||||
PLATFORM_ART_GENERATION_STATUS_ACCEPTED => {
|
||||
PlatformArtGenerationRuntimeRecovery::ResumeAccepted
|
||||
}
|
||||
PLATFORM_ART_GENERATION_STATUS_LEGACY_COMPLETED => {
|
||||
PlatformArtGenerationRuntimeRecovery::ResumeLegacyCompleted
|
||||
}
|
||||
_ => unreachable!("validated External Editor generation state status"),
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn remove_platform_art_generation_runtime_state_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -1240,93 +1193,10 @@ pub(super) fn platform_art_generation_runtime_action_fingerprint_for_test(
|
||||
&state.action_fingerprint
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn write_platform_art_generation_runtime_accepted_for_test(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<(), String> {
|
||||
let context = platform_art_generation_runtime_context_from_pending(pending);
|
||||
let (api_base_url, api_key, platform_session) = resolve_canvas_sync_api_credentials(None, None)
|
||||
.unwrap_or_else(|_| {
|
||||
(
|
||||
"http://127.0.0.1:1".to_string(),
|
||||
"durable-test-key".to_string(),
|
||||
None,
|
||||
)
|
||||
});
|
||||
let access =
|
||||
ExternalEditorBindingAccess::new(&api_base_url, &api_key, platform_session.as_ref())?;
|
||||
let (state, created) = prepare_platform_art_generation_runtime_state(
|
||||
root,
|
||||
&context,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"durable-test-canvas",
|
||||
"durable test generation",
|
||||
&serde_json::json!({
|
||||
"prompt": "durable test generation",
|
||||
"kind": "spec",
|
||||
"projectId": "test-canvas-project",
|
||||
"assetFolderId": "test-asset-folder",
|
||||
"referenceImageSrcs": []
|
||||
}),
|
||||
&access,
|
||||
)?;
|
||||
if !created {
|
||||
return Err("External Editor 测试账本已存在".to_string());
|
||||
}
|
||||
mark_platform_art_generation_runtime_accepted(root, state, "test-operation-id", 1_500)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn write_platform_art_generation_runtime_prepared_for_test(
|
||||
root: &Path,
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<(), String> {
|
||||
let context = platform_art_generation_runtime_context_from_pending(pending);
|
||||
let (api_base_url, api_key, platform_session) = resolve_canvas_sync_api_credentials(None, None)
|
||||
.unwrap_or_else(|_| {
|
||||
(
|
||||
"http://127.0.0.1:1".to_string(),
|
||||
"durable-test-key".to_string(),
|
||||
None,
|
||||
)
|
||||
});
|
||||
let access =
|
||||
ExternalEditorBindingAccess::new(&api_base_url, &api_key, platform_session.as_ref())?;
|
||||
let (_, created) = prepare_platform_art_generation_runtime_state(
|
||||
root,
|
||||
&context,
|
||||
"/api/external/v1/editor/images/generations",
|
||||
"durable-test-canvas",
|
||||
"durable test generation",
|
||||
&serde_json::json!({
|
||||
"prompt": "durable test generation",
|
||||
"kind": "spec",
|
||||
"projectId": "test-canvas-project",
|
||||
"assetFolderId": "test-asset-folder",
|
||||
"referenceImageSrcs": []
|
||||
}),
|
||||
&access,
|
||||
)?;
|
||||
if !created {
|
||||
return Err("External Editor 测试账本已存在".to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod external_generation_state_tests {
|
||||
use super::*;
|
||||
|
||||
fn developer_access<'a>(
|
||||
api_base_url: &'a str,
|
||||
api_key: &'a str,
|
||||
) -> ExternalEditorBindingAccess<'a> {
|
||||
ExternalEditorBindingAccess::for_developer(api_base_url, api_key)
|
||||
.expect("create developer binding access")
|
||||
}
|
||||
|
||||
fn direct_art_spec_runtime_context(project_id: &str) -> PlatformArtGenerationRuntimeContext {
|
||||
PlatformArtGenerationRuntimeContext {
|
||||
agent_id: "direct-codex-art".to_string(),
|
||||
|
||||
+5
-267
@@ -1,82 +1,11 @@
|
||||
use super::*;
|
||||
//! 游戏 HTML / JS 校验原语:静态冒烟自检与生成链共用。
|
||||
//!
|
||||
//! 草案(draft)与多 pass 校验链已随 Agent Runtime 退役删除;本模块只保留校验。
|
||||
|
||||
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 +16,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 +34,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 +217,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 +354,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();
|
||||
+6
-53
@@ -1,46 +1,12 @@
|
||||
//! LLM 文本请求与其额度 / 错误面:`request_game_creator_llm_text`,以及泥点(mud points)不足的
|
||||
//! 判定、消息构造与公开摘要。
|
||||
//!
|
||||
//! 多 pass 循环编排与进度发射面(`AgentProgressEmitter` / `emit_agent_progress`)已随 Agent
|
||||
//! Runtime 退役删除,本模块只剩请求与错误面。
|
||||
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) struct AgentProgressEmitter<'a> {
|
||||
app: &'a tauri::AppHandle,
|
||||
project_path: String,
|
||||
}
|
||||
|
||||
impl<'a> AgentProgressEmitter<'a> {
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn new(app: &'a tauri::AppHandle, project_path: &str) -> Self {
|
||||
Self {
|
||||
app,
|
||||
project_path: project_path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit(&self, stage: &str, message: &str) {
|
||||
if let Err(error) = self.app.emit(
|
||||
"game-creator-agent-progress",
|
||||
GameCreatorAgentProgressEvent {
|
||||
project_path: self.project_path.clone(),
|
||||
stage: stage.to_string(),
|
||||
message: message.to_string(),
|
||||
},
|
||||
) {
|
||||
app_log!(
|
||||
"agent.generation.progress.emit_failed stage={} detail={error}",
|
||||
stage
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn emit_agent_progress(
|
||||
progress: Option<&AgentProgressEmitter<'_>>,
|
||||
stage: &str,
|
||||
message: &str,
|
||||
) {
|
||||
if let Some(progress) = progress {
|
||||
progress.emit(stage, message);
|
||||
}
|
||||
}
|
||||
pub(crate) async fn request_game_creator_llm_text(
|
||||
client: &LlmClient,
|
||||
llm: &GameCreatorLlmConfig,
|
||||
@@ -53,15 +19,6 @@ pub(crate) async fn request_game_creator_llm_text(
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn build_game_creator_agent_runtime_llm_client(
|
||||
llm: &GameCreatorLlmConfig,
|
||||
config_path: &str,
|
||||
) -> Result<LlmClient, String> {
|
||||
let mut single_attempt = llm.clone();
|
||||
single_attempt.max_retries = 0;
|
||||
build_game_creator_llm_client_without_redirects_from_llm_config(&single_attempt, config_path)
|
||||
}
|
||||
|
||||
pub(crate) const GAME_CREATOR_MUD_POINTS_INSUFFICIENT_ERROR_KIND: &str =
|
||||
"kind=mud-points-insufficient";
|
||||
|
||||
@@ -70,10 +27,6 @@ pub(crate) fn game_creator_mud_points_insufficient_message(message: &str) -> boo
|
||||
message == "泥点余额不足" || message.starts_with("可消费泥点不足:")
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_runtime_error_is_mud_points_insufficient(error: &str) -> bool {
|
||||
error.contains("泥点余额不足") || error.contains("可消费泥点不足:")
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_agent_llm_error_is_mud_points_insufficient(
|
||||
error: &platform_llm::LlmError,
|
||||
) -> bool {
|
||||
@@ -1,499 +0,0 @@
|
||||
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,
|
||||
) -> String {
|
||||
format!(
|
||||
"memory/agents/{}/{}",
|
||||
group_definition.id, role_definition.brief_path_name
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_role_memory_relative_path_for_task(task_id: &str) -> Result<String, String> {
|
||||
if task_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
return Ok(GAME_CREATOR_PROJECT_SUPERVISOR_MEMORY_PATH.to_string());
|
||||
}
|
||||
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
for role in group.roles {
|
||||
if role.task_id == task_id {
|
||||
return Ok(agent_role_memory_relative_path(group, *role));
|
||||
}
|
||||
}
|
||||
}
|
||||
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\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
|
||||
}
|
||||
@@ -1,415 +1,3 @@
|
||||
use super::*;
|
||||
|
||||
/// 旧 Agent Runtime 写入的公开状态消息 id 前缀;历史工程里可能残留这类消息,
|
||||
/// 组装对话上下文时仍然跳过它们。
|
||||
const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX: &str = "runtime-public-status-";
|
||||
|
||||
pub(crate) fn game_creator_system_prompt() -> &'static str {
|
||||
prompt_text!("generation.generator_system")
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_planner_system_prompt() -> &'static str {
|
||||
prompt_text!("generation.planner_system")
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_planner_user_prompt(
|
||||
prompt: &str,
|
||||
short_memory: &str,
|
||||
long_memory: &str,
|
||||
project_blackboard: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
prompt_text!("generation.planner_user"),
|
||||
prompt.trim(),
|
||||
truncate_prompt_context(short_memory),
|
||||
truncate_prompt_context(long_memory),
|
||||
truncate_prompt_context(project_blackboard)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_generator_user_prompt(
|
||||
prompt: &str,
|
||||
short_memory: &str,
|
||||
long_memory: &str,
|
||||
project_blackboard: &str,
|
||||
spec_markdown: &str,
|
||||
findings_markdown: &str,
|
||||
group_briefs_markdown: &str,
|
||||
agenda_markdown: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
prompt_text!("generation.generator_user"),
|
||||
prompt.trim(),
|
||||
truncate_prompt_context(short_memory),
|
||||
truncate_prompt_context(long_memory),
|
||||
truncate_prompt_context(project_blackboard),
|
||||
truncate_prompt_context(spec_markdown),
|
||||
truncate_prompt_context(agenda_markdown),
|
||||
truncate_prompt_context(group_briefs_markdown),
|
||||
truncate_prompt_context(findings_markdown)
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn append_prompt_context(base: &str, extra: &str) -> String {
|
||||
match (base.trim().is_empty(), extra.trim().is_empty()) {
|
||||
(_, true) => base.to_string(),
|
||||
(true, false) => extra.trim().to_string(),
|
||||
(false, false) => format!("{}\n\n{}", base.trim_end(), extra.trim()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_local_asset_prompt_context(root: &Path) -> Result<String, String> {
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let registered_paths = manifest
|
||||
.assets
|
||||
.iter()
|
||||
.map(|asset| asset.local_path.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
|
||||
let mut output = String::new();
|
||||
if !manifest.assets.is_empty() {
|
||||
output.push_str(prompt_text!("generation.local_assets_heading"));
|
||||
for asset in manifest.assets.iter().take(24) {
|
||||
output.push_str("- ");
|
||||
output.push_str(&asset.id);
|
||||
output.push_str(": ");
|
||||
output.push_str(asset.kind.as_str());
|
||||
output.push_str(" / ");
|
||||
output.push_str(&asset.media_type);
|
||||
output.push_str(" / ");
|
||||
output.push_str(&asset.local_path);
|
||||
output.push_str(" / source=");
|
||||
output.push_str(asset_source_kind_label(&asset.source.kind));
|
||||
if let Some(canvas_project_id) = asset.source.canvas_project_id.as_deref() {
|
||||
output.push_str(" / canvasProjectId=");
|
||||
output.push_str(canvas_project_id);
|
||||
}
|
||||
if let Some(resource_id) = asset.source.resource_id.as_deref() {
|
||||
output.push_str(" / resourceId=");
|
||||
output.push_str(resource_id);
|
||||
}
|
||||
if let Some(asset_object_id) = asset.source.asset_object_id.as_deref() {
|
||||
output.push_str(" / assetObjectId=");
|
||||
output.push_str(asset_object_id);
|
||||
}
|
||||
if let Some(task_id) = asset.source.task_id.as_deref() {
|
||||
output.push_str(" / taskId=");
|
||||
output.push_str(task_id);
|
||||
}
|
||||
if let Some(model) = asset.source.model.as_deref() {
|
||||
output.push_str(" / model=");
|
||||
output.push_str(model);
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
if manifest.assets.len() > 24 {
|
||||
output.push_str(&format!(
|
||||
prompt_text!("generation.additional_assets"),
|
||||
manifest.assets.len() - 24
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// A project may contain useful media copied in by a user or another tool
|
||||
// before it has been registered in `.agent/manifest.json`. Surface a
|
||||
// small, metadata-only candidate list so the Agent can discover it, while
|
||||
// keeping the manifest as the sole source of formal asset identity and
|
||||
// provenance. Never expose control directories or sensitive config paths.
|
||||
let mut unregistered = list_local_project_files_at(root)?
|
||||
.files
|
||||
.into_iter()
|
||||
.filter(|file| file.kind == "file")
|
||||
.filter(|file| !registered_paths.contains(file.path.as_str()))
|
||||
.filter(|file| !prompt_context_hidden_project_path(&file.path))
|
||||
.filter(|file| reject_sensitive_project_file_read(&file.path).is_ok())
|
||||
.filter_map(|file| {
|
||||
let media_type = prompt_context_media_type(&file.path)?;
|
||||
Some((file.path, file.size, media_type))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
unregistered.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
|
||||
if !unregistered.is_empty() {
|
||||
if !output.is_empty() {
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(prompt_text!("generation.unregistered_media"));
|
||||
for (path, size, media_type) in unregistered.iter().take(48) {
|
||||
let asset_importable = !media_type.is_empty();
|
||||
output.push_str(&format!(
|
||||
"- {path} / {media_type} / {size} bytes / registered=false / assetImportable={asset_importable}\n"
|
||||
));
|
||||
}
|
||||
if unregistered.len() > 48 {
|
||||
output.push_str(&format!(
|
||||
prompt_text!("generation.additional_unregistered_media"),
|
||||
unregistered.len() - 48
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn prompt_context_hidden_project_path(path: &str) -> bool {
|
||||
// Keep prompt discovery aligned with the repository snapshot/read safety
|
||||
// boundary. This covers control trees as well as credentials, secret
|
||||
// directories and sensitive file suffixes; a media extension alone must
|
||||
// never make one of those paths visible to the model.
|
||||
should_skip_project_snapshot_path(path)
|
||||
|| path
|
||||
.split('/')
|
||||
.any(|component| component.eq_ignore_ascii_case(".codex"))
|
||||
}
|
||||
|
||||
fn prompt_context_media_type(path: &str) -> Option<&'static str> {
|
||||
let extension = Path::new(path)
|
||||
.extension()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
match extension.as_str() {
|
||||
"png" => Some("image/png"),
|
||||
"jpg" | "jpeg" => Some("image/jpeg"),
|
||||
"webp" => Some("image/webp"),
|
||||
"gif" => Some("image/gif"),
|
||||
"avif" => Some("image/avif"),
|
||||
"bmp" => Some("image/bmp"),
|
||||
"svg" => Some("image/svg+xml"),
|
||||
"ttf" => Some("font/ttf"),
|
||||
"otf" => Some("font/otf"),
|
||||
"woff" => Some("font/woff"),
|
||||
"woff2" => Some("font/woff2"),
|
||||
"mp3" => Some("audio/mpeg"),
|
||||
"wav" => Some("audio/wav"),
|
||||
"ogg" => Some("audio/ogg"),
|
||||
"flac" => Some("audio/flac"),
|
||||
"m4a" => Some("audio/mp4"),
|
||||
"aac" => Some("audio/aac"),
|
||||
"opus" => Some("audio/opus"),
|
||||
"mp4" => Some("video/mp4"),
|
||||
"webm" => Some("video/webm"),
|
||||
"mov" => Some("video/quicktime"),
|
||||
"json" => Some("application/json"),
|
||||
"yaml" | "yml" => Some("application/yaml"),
|
||||
"xml" => Some("application/xml"),
|
||||
"html" | "htm" => Some("text/html"),
|
||||
"md" | "markdown" | "mdx" | "txt" | "toml" | "csv" | "ini" | "conf" => Some("text/plain"),
|
||||
"css" | "scss" | "less" => Some("text/css"),
|
||||
"js" | "mjs" | "cjs" | "ts" | "tsx" | "gd" | "rs" | "py" | "go" | "java" | "kt" | "kts"
|
||||
| "c" | "cc" | "cpp" | "h" | "hpp" | "cs" | "swift" | "php" | "rb" | "lua" | "sh"
|
||||
| "bash" | "zsh" | "sql" | "graphql" | "gql" | "vue" | "svelte" => Some("text/plain"),
|
||||
/*
|
||||
* Cocos Creator 资源。这里只是给未登记文件清单标注媒体类型,取值必须与
|
||||
* `agent/direct_tool_bridge.rs::bridge_project_file_class` 和
|
||||
* `commands.rs::agent_local_project_file_type` 一致:少写一个扩展名,
|
||||
* Agent 的提示词就会把「其实可以登记」的资源说成只能发现。
|
||||
*/
|
||||
"glb" => Some("model/gltf-binary"),
|
||||
"gltf" => Some("model/gltf+json"),
|
||||
"fbx" | "dbbin" | "bin" | "skel" | "texture" | "cubemap" | "rt" | "psd" | "znt" => {
|
||||
Some("application/octet-stream")
|
||||
}
|
||||
"scene" | "fire" | "prefab" | "anim" | "animation" | "animgraph" | "animgraphvari"
|
||||
| "animask" | "mtl" | "material" | "pmtl" | "terrain" | "labelatlas" | "pac" | "mesh"
|
||||
| "skeleton" => Some("application/json"),
|
||||
"tmx" | "plist" => Some("application/xml"),
|
||||
"effect" | "chunk" | "fnt" | "atlas" => Some("text/plain"),
|
||||
"tga" => Some("image/x-tga"),
|
||||
"tif" | "tiff" => Some("image/tiff"),
|
||||
"hdr" => Some("image/vnd.radiance"),
|
||||
"exr" => Some("image/x-exr"),
|
||||
"pcm" => Some("audio/pcm"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::prompt_context_media_type;
|
||||
|
||||
#[test]
|
||||
fn prompt_context_media_type_covers_all_importable_resource_categories() {
|
||||
for (path, expected) in [
|
||||
("assets/icon.avif", "image/avif"),
|
||||
("assets/font.woff2", "font/woff2"),
|
||||
("assets/theme.opus", "audio/opus"),
|
||||
("assets/intro.mov", "video/quicktime"),
|
||||
("assets/data.json", "application/json"),
|
||||
("game/index.html", "text/html"),
|
||||
("game/main.rs", "text/plain"),
|
||||
// 引擎资源同样要在未登记清单里被标出可登记:漏一个扩展名,
|
||||
// Agent 就会把「其实可以登记」的 Cocos 资源说成只能发现。
|
||||
("assets/model/hero.glb", "model/gltf-binary"),
|
||||
("assets/model/hero.fbx", "application/octet-stream"),
|
||||
("assets/anim/walk.anim", "application/json"),
|
||||
("assets/scene/main.scene", "application/json"),
|
||||
("assets/shader/glow.effect", "text/plain"),
|
||||
("assets/atlas/hero.plist", "application/xml"),
|
||||
("assets/tex/grass.tga", "image/x-tga"),
|
||||
("assets/audio/voice.pcm", "audio/pcm"),
|
||||
] {
|
||||
assert_eq!(prompt_context_media_type(path), Some(expected), "{path}");
|
||||
}
|
||||
// `.bin` 现在是引擎 BufferAsset 的载体,不再是「未识别」;真正未知的扩展名才返回 None。
|
||||
assert_eq!(prompt_context_media_type("assets/unknown.dat"), None);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_local_conversation_prompt_context(
|
||||
root: &Path,
|
||||
agent_id: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
render_local_conversation_prompt_context_for_session(root, agent_id, None)
|
||||
}
|
||||
|
||||
pub(crate) fn render_local_conversation_prompt_context_for_session(
|
||||
root: &Path,
|
||||
agent_id: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
#[derive(Debug)]
|
||||
struct ConversationPromptEntry {
|
||||
updated_at: u64,
|
||||
agent_label: String,
|
||||
role: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
fn push_conversation_entries(
|
||||
entries: &mut Vec<ConversationPromptEntry>,
|
||||
conversation: LocalConversationResult,
|
||||
agent_label: &str,
|
||||
) {
|
||||
for message in conversation.messages {
|
||||
if message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id
|
||||
.trim()
|
||||
.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let content = sanitize_prompt_context(&message.content)
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
entries.push(ConversationPromptEntry {
|
||||
updated_at: message.updated_at,
|
||||
agent_label: agent_label.to_string(),
|
||||
role: message.role,
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validate_project_root(root)?;
|
||||
let mut entries = Vec::new();
|
||||
push_conversation_entries(
|
||||
&mut entries,
|
||||
read_local_conversation_for_session_at(root, None, None)?,
|
||||
"project",
|
||||
);
|
||||
|
||||
if let Some(agent_id) = agent_id.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
if agent_id == "*" {
|
||||
if session_id
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
{
|
||||
return Err("读取全部 Agent 对话时不能指定单个 sessionId".to_string());
|
||||
}
|
||||
let agents_dir = root.join(".agent/conversations/agents");
|
||||
match fs::read_dir(&agents_dir) {
|
||||
Ok(read_dir) => {
|
||||
let mut agent_ids = std::collections::BTreeSet::new();
|
||||
for entry in read_dir {
|
||||
let entry = entry.map_err(|error| {
|
||||
format!("读取 Agent 对话目录失败:{}: {error}", agents_dir.display())
|
||||
})?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|value| value.to_str()) == Some("jsonl") {
|
||||
if let Some(agent_id) =
|
||||
path.file_stem().and_then(|value| value.to_str())
|
||||
{
|
||||
agent_ids.insert(agent_id.to_string());
|
||||
}
|
||||
} else if path.is_dir() {
|
||||
if let Some(agent_id) =
|
||||
path.file_name().and_then(|value| value.to_str())
|
||||
{
|
||||
agent_ids.insert(agent_id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
for agent_id in agent_ids {
|
||||
push_conversation_entries(
|
||||
&mut entries,
|
||||
read_local_conversation_for_session_at(root, Some(&agent_id), None)?,
|
||||
&agent_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"读取 Agent 对话目录失败:{}: {error}",
|
||||
agents_dir.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
push_conversation_entries(
|
||||
&mut entries,
|
||||
read_local_conversation_for_session_at(root, Some(agent_id), session_id)?,
|
||||
agent_id,
|
||||
);
|
||||
}
|
||||
} else if session_id
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
{
|
||||
return Err("项目主对话不接受 sessionId".to_string());
|
||||
}
|
||||
|
||||
if entries.is_empty() {
|
||||
return Ok(String::new());
|
||||
}
|
||||
entries.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at
|
||||
.cmp(&left.updated_at)
|
||||
.then_with(|| left.agent_label.cmp(&right.agent_label))
|
||||
.then_with(|| left.role.cmp(&right.role))
|
||||
});
|
||||
entries.truncate(GAME_CREATOR_CONVERSATION_CONTEXT_MAX_MESSAGES);
|
||||
entries.sort_by(|left, right| {
|
||||
left.updated_at
|
||||
.cmp(&right.updated_at)
|
||||
.then_with(|| left.agent_label.cmp(&right.agent_label))
|
||||
.then_with(|| left.role.cmp(&right.role))
|
||||
});
|
||||
|
||||
let mut output = prompt_text!("generation.conversation_heading").to_string();
|
||||
for entry in entries {
|
||||
output.push_str(&format!(
|
||||
"- [{} / {}] {}\n",
|
||||
entry.agent_label, entry.role, entry.content
|
||||
));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
pub(crate) fn asset_source_kind_label(kind: &GameCreationAppAssetSourceKind) -> &'static str {
|
||||
match kind {
|
||||
GameCreationAppAssetSourceKind::Uploaded => "uploaded",
|
||||
GameCreationAppAssetSourceKind::Generated => "generated",
|
||||
GameCreationAppAssetSourceKind::Canvas => "canvas",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_prompt_context(value: &str) -> String {
|
||||
const MAX_CHARS: usize = 2400;
|
||||
let sanitized = sanitize_prompt_context(value);
|
||||
@@ -421,21 +9,6 @@ pub(crate) fn truncate_prompt_context(value: &str) -> String {
|
||||
output
|
||||
}
|
||||
|
||||
pub(crate) fn truncate_prompt_context_preserving_tail(value: &str) -> String {
|
||||
const MAX_CHARS: usize = 2400;
|
||||
let sanitized = sanitize_prompt_context(value);
|
||||
let trimmed = sanitized.trim();
|
||||
if trimmed.chars().count() <= MAX_CHARS {
|
||||
return trimmed.to_string();
|
||||
}
|
||||
let mut characters = trimmed.chars().rev().take(MAX_CHARS).collect::<Vec<_>>();
|
||||
characters.reverse();
|
||||
format!(
|
||||
"...<truncated>\n{}",
|
||||
characters.into_iter().collect::<String>()
|
||||
)
|
||||
}
|
||||
|
||||
fn contains_sensitive_assignment(line: &str) -> bool {
|
||||
let lower = line.to_ascii_lowercase();
|
||||
[
|
||||
@@ -1113,14 +686,6 @@ pub(crate) fn redact_secret_tokens(line: &str) -> String {
|
||||
output
|
||||
}
|
||||
|
||||
pub(crate) fn read_optional_text(path: &Path) -> Result<String, String> {
|
||||
match fs::read_to_string(path) {
|
||||
Ok(content) => Ok(content),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(String::new()),
|
||||
Err(error) => Err(format!("读取上下文失败:{}: {error}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_secret_token_end_with_minimum(
|
||||
content: &str,
|
||||
index: usize,
|
||||
|
||||
@@ -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,45 +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(),
|
||||
"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,
|
||||
@@ -669,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",
|
||||
@@ -725,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())
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//! Provider 请求身份快照:退役的 Agent Runtime 计划 / 观察 DTO 已删除,本模块只保留这一个结构。
|
||||
|
||||
/// 一次 Provider 请求的身份快照:退役的 Agent Runtime 计划/观察 DTO 已删除,
|
||||
/// 只保留仍被 Codex app-server 会话与 provider 请求路径使用的这个结构。
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct AgentRuntimeProviderRequestSnapshot {
|
||||
pub(in crate::agent) project_id: String,
|
||||
pub(in crate::agent) agent_id: String,
|
||||
pub(in crate::agent) task_id: String,
|
||||
pub(in crate::agent) session_id: String,
|
||||
pub(in crate::agent) run_id: String,
|
||||
pub(in crate::agent) source: String,
|
||||
pub(in crate::agent) goal_id: Option<String>,
|
||||
pub(in crate::agent) goal_revision: u64,
|
||||
pub(in crate::agent) goal_snapshot_fingerprint: String,
|
||||
pub(in crate::agent) applied_steer_cursor: u64,
|
||||
pub(in crate::agent) request_kind: String,
|
||||
pub(in crate::agent) request_slot: String,
|
||||
pub(in crate::agent) web_search_enabled: bool,
|
||||
pub(in crate::agent) allow_idle_context_compaction: bool,
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -194,3 +157,145 @@ pub(crate) fn game_creator_agent_runtime_context_project_id(root: &Path) -> Resu
|
||||
}
|
||||
Ok(redact_agent_runtime_project_paths(root, &project_id, 240))
|
||||
}
|
||||
|
||||
pub(crate) fn redact_absolute_path_tokens(value: &str) -> String {
|
||||
let bytes = value.as_bytes();
|
||||
let mut output = String::with_capacity(value.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
if starts_absolute_path(bytes, index) || starts_file_uri(bytes, index) {
|
||||
output.push_str("<absolute-path>");
|
||||
index = consume_path_token(bytes, index);
|
||||
continue;
|
||||
}
|
||||
let character = value[index..].chars().next().unwrap_or_default();
|
||||
if character.is_control() && !matches!(character, '\n' | '\r' | '\t') {
|
||||
output.push(' ');
|
||||
} else {
|
||||
output.push(character);
|
||||
}
|
||||
index += character.len_utf8();
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn starts_absolute_path(bytes: &[u8], index: usize) -> bool {
|
||||
let boundary = index == 0 || is_path_boundary(bytes[index - 1]);
|
||||
if !boundary {
|
||||
return false;
|
||||
}
|
||||
if bytes[index] == b'/' {
|
||||
return bytes.get(index + 1) != Some(&b'/') || starts_forward_slash_unc_path(bytes, index);
|
||||
}
|
||||
if bytes[index] == b'\\' && bytes.get(index + 1) == Some(&b'\\') {
|
||||
return true;
|
||||
}
|
||||
bytes[index].is_ascii_alphabetic()
|
||||
&& bytes.get(index + 1) == Some(&b':')
|
||||
&& bytes
|
||||
.get(index + 2)
|
||||
.is_some_and(|separator| matches!(separator, b'/' | b'\\'))
|
||||
}
|
||||
|
||||
fn starts_forward_slash_unc_path(bytes: &[u8], index: usize) -> bool {
|
||||
let server_start = index + 2;
|
||||
let token_end = consume_path_token(bytes, server_start);
|
||||
let Some(server_end) = bytes[server_start..token_end]
|
||||
.iter()
|
||||
.position(|byte| *byte == b'/')
|
||||
.map(|offset| server_start + offset)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let share_start = server_end + 1;
|
||||
server_end > server_start && share_start < token_end && bytes.get(share_start) != Some(&b'/')
|
||||
}
|
||||
|
||||
fn starts_file_uri(bytes: &[u8], index: usize) -> bool {
|
||||
const FILE_URI_PREFIX: &[u8] = b"file:";
|
||||
|
||||
let boundary = index == 0 || is_path_boundary(bytes[index - 1]);
|
||||
if !boundary {
|
||||
return false;
|
||||
}
|
||||
let Some(prefix_end) = index.checked_add(FILE_URI_PREFIX.len()) else {
|
||||
return false;
|
||||
};
|
||||
if !bytes
|
||||
.get(index..prefix_end)
|
||||
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(FILE_URI_PREFIX))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let token_end = consume_path_token(bytes, prefix_end);
|
||||
bytes.get(prefix_end) == Some(&b'/')
|
||||
&& token_end > prefix_end
|
||||
&& bytes.get(prefix_end..token_end) != Some(b"//")
|
||||
}
|
||||
|
||||
fn is_path_boundary(byte: u8) -> bool {
|
||||
byte.is_ascii_whitespace()
|
||||
|| matches!(
|
||||
byte,
|
||||
b'"' | b'\'' | b'`' | b'=' | b'(' | b'[' | b'{' | b'<' | b',' | b';'
|
||||
)
|
||||
}
|
||||
|
||||
fn consume_path_token(bytes: &[u8], mut index: usize) -> usize {
|
||||
while index < bytes.len()
|
||||
&& !bytes[index].is_ascii_whitespace()
|
||||
&& !matches!(
|
||||
bytes[index],
|
||||
b'"' | b'\'' | b'`' | b')' | b']' | b'}' | b'>' | b',' | b';'
|
||||
)
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::redact_absolute_path_tokens;
|
||||
|
||||
#[test]
|
||||
fn redacts_file_uri_tokens_without_redacting_relative_text() {
|
||||
assert_eq!(
|
||||
redact_absolute_path_tokens(
|
||||
"open file:///home/alice/project, FILE:///C:/Users/Alice/project; \
|
||||
file://server/share/project file:///home/alice/My%20Project \
|
||||
file:///%68ome/alice%2Fproject"
|
||||
),
|
||||
"open <absolute-path>, <absolute-path>; <absolute-path> <absolute-path> \
|
||||
<absolute-path>"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_absolute_path_tokens(
|
||||
"profile homeward docs/file.txt file:notes.txt file:// file:///"
|
||||
),
|
||||
"profile homeward docs/file.txt file:notes.txt file:// <absolute-path>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacts_cross_platform_absolute_paths_without_redacting_urls() {
|
||||
assert_eq!(
|
||||
redact_absolute_path_tokens(concat!(
|
||||
r#"open //server/share/private, \\server\share\private; "#,
|
||||
"file:/home/user/private file:/C:/Users/Alice/private ",
|
||||
r#"C:/Users/Alice/private C:\Users\Alice\private"#
|
||||
)),
|
||||
"open <absolute-path>, <absolute-path>; \
|
||||
<absolute-path> <absolute-path> \
|
||||
<absolute-path> <absolute-path>"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_absolute_path_tokens(
|
||||
"keep docs/private file:notes.txt // not-a-path /// docs \
|
||||
https://example.test/private http://localhost:3000/private"
|
||||
),
|
||||
"keep docs/private file:notes.txt // not-a-path /// docs \
|
||||
https://example.test/private http://localhost:3000/private"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimeToolAction {
|
||||
#[serde(default)]
|
||||
pub(crate) tool: String,
|
||||
#[serde(default)]
|
||||
pub(crate) reason: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimeToolObservation {
|
||||
pub(crate) tool: String,
|
||||
pub(crate) status: String,
|
||||
pub(crate) summary: String,
|
||||
pub(crate) detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimeProjectRevision {
|
||||
pub(crate) schema_version: String,
|
||||
pub(crate) project_id: String,
|
||||
pub(crate) revision: u64,
|
||||
pub(crate) updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct AgentRuntimeProviderRequestSnapshot {
|
||||
pub(in crate::agent) project_id: String,
|
||||
pub(in crate::agent) agent_id: String,
|
||||
pub(in crate::agent) task_id: String,
|
||||
pub(in crate::agent) session_id: String,
|
||||
pub(in crate::agent) run_id: String,
|
||||
pub(in crate::agent) source: String,
|
||||
pub(in crate::agent) goal_id: Option<String>,
|
||||
pub(in crate::agent) goal_revision: u64,
|
||||
pub(in crate::agent) goal_snapshot_fingerprint: String,
|
||||
pub(in crate::agent) applied_steer_cursor: u64,
|
||||
pub(in crate::agent) request_kind: String,
|
||||
pub(in crate::agent) request_slot: String,
|
||||
pub(in crate::agent) web_search_enabled: bool,
|
||||
pub(in crate::agent) allow_idle_context_compaction: bool,
|
||||
}
|
||||
|
||||
impl AgentRuntimeProviderRequestSnapshot {
|
||||
pub(in crate::agent) fn with_request_slot(&self, request_slot: impl Into<String>) -> Self {
|
||||
let mut snapshot = self.clone();
|
||||
snapshot.request_slot = request_slot.into();
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn with_web_search_enabled(&self, web_search_enabled: bool) -> Self {
|
||||
let mut snapshot = self.clone();
|
||||
snapshot.web_search_enabled = web_search_enabled;
|
||||
snapshot
|
||||
}
|
||||
|
||||
pub(super) fn with_idle_context_compaction(&self, allow: bool) -> Self {
|
||||
let mut snapshot = self.clone();
|
||||
snapshot.allow_idle_context_compaction = allow;
|
||||
snapshot
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimeVerificationGate {
|
||||
pub(crate) schema_version: String,
|
||||
pub(crate) project_id: String,
|
||||
pub(crate) agent_id: String,
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) requires_verification: bool,
|
||||
pub(crate) mutation_revision: Option<u64>,
|
||||
pub(crate) verified_revision: Option<u64>,
|
||||
pub(crate) last_mutation_tool: Option<String>,
|
||||
pub(crate) last_verification_tool: Option<String>,
|
||||
pub(crate) last_verification_status: Option<String>,
|
||||
/// `last_verification_*` is deliberately the most recent verification so
|
||||
/// preview failures can drive the repair loop. Keep the static smoke
|
||||
/// result separately: a successful `preview.validate` must not erase the
|
||||
/// proof that the exact same revision passed `game.static_smoke`.
|
||||
#[serde(default)]
|
||||
pub(crate) static_smoke_verified_revision: Option<u64>,
|
||||
/// Bind the durable static-smoke credential to the exact entry bytes.
|
||||
/// A project revision alone is insufficient because files can be changed
|
||||
/// outside the Runtime mutation path without advancing that revision.
|
||||
#[serde(default)]
|
||||
pub(crate) static_smoke_verified_game_index_sha256: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) failed_playtest_revision: Option<u64>,
|
||||
pub(crate) updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimePendingToolAction {
|
||||
pub(crate) schema_version: String,
|
||||
pub(crate) fingerprint_version: String,
|
||||
pub(crate) agent_id: String,
|
||||
pub(crate) task_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) source: String,
|
||||
#[serde(default = "default_agent_runtime_run_profile")]
|
||||
pub(crate) run_profile: String,
|
||||
#[serde(default)]
|
||||
pub(crate) run_profile_binding_fingerprint: String,
|
||||
pub(crate) task: String,
|
||||
#[serde(default)]
|
||||
pub(crate) goal_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) goal_revision: u64,
|
||||
#[serde(default)]
|
||||
pub(crate) goal_snapshot_fingerprint: String,
|
||||
pub(crate) loop_iteration: u32,
|
||||
pub(crate) action_index: u32,
|
||||
pub(crate) occurrence_nonce: u64,
|
||||
pub(crate) thinking_summary: String,
|
||||
pub(crate) plan: Vec<String>,
|
||||
pub(crate) fallback_response: String,
|
||||
pub(crate) observations: Vec<AgentRuntimeToolObservation>,
|
||||
pub(crate) project_revision_before: AgentRuntimeProjectRevision,
|
||||
pub(crate) verification_gate_before: AgentRuntimeVerificationGate,
|
||||
pub(crate) planned_repository_context_fingerprint: String,
|
||||
#[serde(default)]
|
||||
pub(crate) planned_steer_cursor: u64,
|
||||
pub(crate) action: AgentRuntimeToolAction,
|
||||
pub(crate) action_id: String,
|
||||
pub(crate) action_fingerprint: String,
|
||||
pub(crate) input_summary: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) execution_mode: String,
|
||||
pub(crate) status: String,
|
||||
pub(crate) observation: Option<AgentRuntimeToolObservation>,
|
||||
pub(crate) created_at: u64,
|
||||
pub(crate) updated_at: u64,
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user