补齐项目Skill渐进加载

发现并校验项目内 Codex 与 Agents Skill 元数据
按需加载命中正文并绑定仓库上下文指纹
补充确定性测试和真实 Provider 隔离验收
同步 Runtime 方案、实施计划与项目决策记录
This commit is contained in:
AIGameCreator App
2026-07-16 07:07:53 +08:00
parent daa5704759
commit 9d29631b01
8 changed files with 1628 additions and 16 deletions
File diff suppressed because it is too large Load Diff
+20
View File
@@ -1467,6 +1467,7 @@ dependencies = [
"rmcp",
"serde",
"serde_json",
"serde_yaml",
"sha2",
"shared-contracts",
"similar",
@@ -3844,6 +3845,19 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.14.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "serial2"
version = "0.2.37"
@@ -5058,6 +5072,12 @@ version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "url"
version = "2.5.8"
@@ -15,6 +15,7 @@ http = "1"
rmcp = { version = "2.2.0", default-features = false, features = ["client", "reqwest-native-tls", "transport-child-process", "transport-streamable-http-client-reqwest"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
sha2 = "0.10"
similar = "2.7"
platform-llm = { path = "../../../server-rs/crates/platform-llm" }
File diff suppressed because it is too large Load Diff
@@ -6794,7 +6794,7 @@ async fn background_agent_runtime_executes_native_function_tool_plan() {
assert!(first_request.contains("\"strict\":true"));
assert!(first_request.contains("\"stream\":false"));
assert!(first_request.contains("REPOSITORY STARTUP CONTEXT"));
assert!(first_request.contains("repository-startup-context-v2"));
assert!(first_request.contains("repository-startup-context-v3"));
assert!(first_request.contains("SCOPED REPOSITORY INSTRUCTIONS"));
assert!(first_request.contains("path=AGENTS.md scope=."));
assert!(first_request.contains("path=game/AGENTS.md scope=game"));
@@ -6839,6 +6839,142 @@ async fn background_agent_runtime_executes_native_function_tool_plan() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_loads_matching_project_skill_on_demand() {
const SKILL_DESCRIPTION: &str =
"Use for moonlight release capsule handoffs that require the repository workflow.";
const SKILL_BODY_MARKER: &str = "PROJECT_SKILL_BODY_LOADED_AFTER_FILE_READ";
const IRRELEVANT_BODY_MARKER: &str = "IRRELEVANT_SKILL_BODY_MUST_STAY_UNLOADED";
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光发布胶囊").expect("project init");
fs::create_dir_all(root.join(".codex/skills/release-capsule"))
.expect("create matching skill dir");
fs::create_dir_all(root.join(".codex/skills/unrelated-art"))
.expect("create unrelated skill dir");
fs::write(
root.join(".codex/skills/release-capsule/SKILL.md"),
format!(
"---\nname: release-capsule\ndescription: {SKILL_DESCRIPTION}\n---\n# Required workflow\n{SKILL_BODY_MARKER}\n"
),
)
.expect("write matching skill");
fs::write(
root.join(".codex/skills/unrelated-art/SKILL.md"),
format!(
"---\nname: unrelated-art\ndescription: Use only for unrelated art audits.\n---\n{IRRELEVANT_BODY_MARKER}\n"
),
)
.expect("write unrelated skill");
let (sender, receiver) = mpsc::channel();
let first_arguments = serde_json::json!({
"thinkingSummary": "任务匹配发布胶囊 Skill,先按 catalog 读取入口正文",
"planUpdate": null,
"plan": ["读取匹配 Skill", "依据工作流回复"],
"actions": [{
"tool": "file.read",
"reason": "加载命中的项目 Skill 正文",
"input": {
"path": ".codex/skills/release-capsule/SKILL.md",
"startLine": 1,
"maxLines": 120
}
}],
"response": ""
})
.to_string();
let final_arguments = final_tool_plan_response(
"已在读取匹配项目 Skill 后完成发布胶囊分析。PROJECT_SKILL_PROGRESSIVE_OK",
);
let base_url = spawn_mock_llm_raw_responses_with_capture(
vec![
native_agent_tool_plan_chat_response(
"call-project-skill-read",
AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME,
first_arguments,
),
native_agent_tool_plan_chat_response(
"call-project-skill-final",
AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME,
final_arguments,
),
],
Some(sender),
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"design-director": {{
"apiKey": "project-skill-key",
"baseUrl": {base_url:?},
"model": "project-skill-model",
"apiKind": "openai_chat",
"stream": false
}}
}}
}}"#
));
let run_id = "project-skill-progressive-load-run";
start_game_creator_agent_background_task_at(
&root,
"design-director",
"分析当前月光发布胶囊交付并遵循项目适用工作流",
run_id,
)
.expect("start project skill task");
let first_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("project skill first request");
assert!(first_request.contains("repository-startup-context-v3"));
assert!(first_request.contains("PROJECT SKILL CATALOG"));
assert!(first_request.contains("release-capsule"));
assert!(first_request.contains(SKILL_DESCRIPTION));
assert!(first_request.contains(".codex/skills/release-capsule/SKILL.md"));
assert!(first_request.contains("\\\"bodyLoaded\\\":false"));
assert!(!first_request.contains(SKILL_BODY_MARKER));
assert!(!first_request.contains(IRRELEVANT_BODY_MARKER));
let followup_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("project skill followup request");
assert!(followup_request.contains("file.read"));
assert!(followup_request.contains(SKILL_BODY_MARKER));
assert!(!followup_request.contains(IRRELEVANT_BODY_MARKER));
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.phase, "completed");
assert_eq!(
runtime.last_response.as_deref(),
Some("已在读取匹配项目 Skill 后完成发布胶囊分析。PROJECT_SKILL_PROGRESSIVE_OK")
);
assert_eq!(runtime.recent_tool_calls.len(), 1);
assert_eq!(runtime.recent_tool_calls[0].tool, "file.read");
assert!(runtime.recent_tool_calls[0]
.summary
.contains(".codex/skills/release-capsule/SKILL.md"));
let agent_db = read_agent_db_records_for_test(&root);
assert!(agent_db.iter().any(|record| {
record["recordType"] == "agent.runtime.tool_observation"
&& record["runId"] == run_id
&& record["tool"] == "file.read"
&& record["status"] == "ok"
}));
assert!(!agent_db.iter().any(|record| {
record["runId"] == run_id
&& record["tool"] == "file.read"
&& record
.to_string()
.contains(".codex/skills/unrelated-art/SKILL.md")
}));
fs::remove_dir_all(root).ok();
}
fn run_response_stream_distinct_final_reply_case(api_kind: &str, case_name: &str) {
let root = unique_project_path();
init_local_game_project_at(
@@ -11358,6 +11494,64 @@ fn repository_context_v1_pending_fingerprint_blocks_project_mutation() {
fs::remove_dir_all(root).ok();
}
#[test]
fn repository_context_v2_pending_fingerprint_blocks_project_mutation() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "旧 Skill catalog 快照迁移")
.expect("project init");
fs::create_dir_all(root.join(".codex/skills/release-capsule")).expect("create skill dir");
fs::write(
root.join(".codex/skills/release-capsule/SKILL.md"),
"---\nname: release-capsule\ndescription: Use for release capsule tasks.\n---\nSKILL_V3_BODY\n",
)
.expect("write skill");
let state = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"验证 v2 repository context 不会绕过 Skill 加载直接写入",
"repository-context-v2-pending-run",
"agent-background-task",
"准备旧 Skill catalog 快照动作",
vec!["重新确认项目 Skill".to_string()],
)
.expect("start runtime");
let mut pending = pending_tool_action_for_test(
&root,
&state,
AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("基于 v2 快照写文件".to_string()),
input: serde_json::json!({
"path": "game/legacy-skill-context-write.txt",
"content": "must not land\n"
}),
},
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
None,
);
let mut legacy_context = build_repository_startup_context_at(&root).expect("current context");
legacy_context.schema_version = "repository-startup-context-v2".to_string();
legacy_context.skills.clear();
legacy_context
.source_paths
.retain(|path| !path.ends_with("/SKILL.md"));
pending.planned_repository_context_fingerprint =
repository_startup_context_fingerprint(&legacy_context);
let observation = pending_repository_context_drift_observation(&root, &pending)
.expect("evaluate repository drift")
.expect("v2 fingerprint must drift");
assert_eq!(observation.status, "blocked");
assert!(observation.summary.contains("旧动作未执行"));
assert!(observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("repositoryContextDrift=true")));
assert!(!root.join("game/legacy-skill-context-write.txt").exists());
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn runtime_v11_closure_repository_context_drift_replans_before_auto_mutations() {
const DRIFT_COMMAND: &str = r#"node -e "require('fs').writeFileSync('AGENTS.md','drifted rules\\n');process.stdout.write('DRIFTED')""#;
@@ -16,6 +16,14 @@
---
## 2026-07-16 AI 游戏创作 Agent Runtime 使用项目 Skill 渐进加载
- 背景:单 Agent 已能按目录 scope 应用 `AGENTS.md`,但领域工作流如果全部预加载进每轮 prompt,会长期占用上下文并让无关说明干扰规划;只保存文件哈希又无法证明模型真正读取并遵循了匹配工作流。
- 决策:项目 Skill 只从 `.codex/skills/<name>/SKILL.md` 和兼容的 `.agents/skills/<name>/SKILL.md` 直接入口发现,同名时 `.codex` 优先。`repository-startup-context-v3` 首轮只向 Provider 提供清洗后的名称、描述、入口路径与正文哈希;Agent 判断任务命中后必须通过现有 `file.read` 渐进读取正文和必要引用。Skill 不新增工具或权限,不替用户确认,不放宽沙箱、隐私、验证、finalization、仓库 scope 或副作用重放门禁;适用路径的 `AGENTS.md` 始终优先。active Skill 内容或 metadata 变化必须推进 repository fingerprint,使旧 pending 动作先 blocked 后同 run 重规划。
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/repository_context.rs`、Agent planning prompt、pending repository-context drift 门禁、真实 Runtime E2E harness 和 AI 游戏创作 Runtime 文档。
- 验证方式:确定性测试覆盖发现根、优先级、YAML/路径/符号链接/预算、metadata 清洗、正文按需可见和 fingerprint 漂移;正式 `openai_chat / gpt-5.5``project-skill` suite 必须证明 hash-only fixture 先失败、匹配 Skill 在首个变更前读取、无关 Skill 不读取、唯一目标文件修改、Agent 与宿主验证通过,以及 Provider lifecycle、唯一回复、配置隔离和零泄漏全部闭合。
- 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md``docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`
## 2026-07-13 图片画布生成资源支持提交前统一命名
- 背景:图片画布的普通图片、规范、角色、图标图集、UI 设计、宣发素材、视频和音频默认使用“类型 + 数字”命名,用户只能在生成后单独重命名素材,画布图层、项目资源和素材库名称容易不一致。
@@ -975,6 +975,20 @@ V1.24 修正仓库启动上下文把 `AGENTS.md` 与 README/CONTEXT 一律描述
2026-07-16 正式 `openai_chat / gpt-5.5``scoped-agents` suite **PASS**。原始 fixture 先真实失败;最终脚本复跑中,同一 `code-prototype` Agent 以 2 个项目变更动作只修改两个目标文件,根规则、`game` 父规则和 `alpha / beta` 叶规则全部精确命中,兄弟规则串用为 0,Agent 的 `project.verify` 与宿主独立复验均通过。8 组 tool-plan Provider lifecycle 全部唯一 `started -> completed`,最终 assistant 和 completed audit 各 1,重复 message/receipt、遗留 finalization,以及最终回复/公共审计/报告中的内部规则正文、API Key、诱饵、项目/正式配置绝对路径泄漏均为 0;正式配置 CLI 调用为 0,源 Runner endpoint 和配置副本保持不变,隔离 Runner、AppData 与 disposable 项目已按 sentinel 清理。V1.24 真实行为门禁至此完成。
## V1.25 Codex 式项目 Skill 发现与渐进加载
V1.25 在 V1.24 仓库启动上下文上增加项目内 Skill catalog,但不把 Skill 正文预加载到每轮 prompt。目标是对齐 Codex 的 progressive disclosure:模型始终只看到用于触发判断的 `name / description / entryPath / contentSha256`,任务真实命中后再通过现有 `file.read` 获取 `SKILL.md` 正文,并只按正文导航读取必要 reference。Skill 是项目工作流知识,不是新工具、权限包或可执行插件。
- 发现根固定为项目内 `.codex/skills/<skill-name>/SKILL.md` 与兼容目录 `.agents/skills/<skill-name>/SKILL.md`,只接受这两个根下的直接子目录入口,不递归把 reference 中的其它 `SKILL.md` 当独立 Skill。本仓库既有规范以 `.codex/skills` 为准;同名且两处都合法时 `.codex` 胜出,删除高优先级入口后 `.agents` 才可接管。V1.25 不扫描 AppData、用户主目录、全局 Codex/Hermes 安装目录、Git submodule 外部路径或网络 marketplace。
- `skill-name` 必须与目录名和 YAML frontmatter `name` 完全一致,使用 1-64 个 ASCII 小写字母、数字或单连字符,首尾必须是字母或数字;frontmatter 必须位于文件开头并提供非空字符串 `name / description`。YAML 使用结构化 parser;未知字段不产生 Runtime 能力。描述清洗凭据和绝对路径、折叠为单行并限制 2048 bytes。
- 单个 `SKILL.md` 最大 128 KiBcatalog 最多 64 项,prompt 中 Skill metadata section 最大 4 KiB。超限、解析失败、符号链接、路径不规范或读取失败的入口不进入 catalog;预算或读取导致的省略必须使 repository context 标记 `truncated=true`,不能把部分 YAML 当有效 metadata。
- 仓库启动上下文升级为 `repository-startup-context-v3`,新增有界 `skills` 列表。fingerprint 覆盖 active Skill 的规范入口路径、来源根、清洗后 name/description、清洗后完整文件 SHA-256 和截断状态;Skill 正文、metadata、优先级或入口增删发生变化时,任何受 repository context gate 保护的旧 pending action 都必须先形成 drift blocker,再在同一 run 重规划。shadowed 的低优先级同名入口不影响 active 语义。
- Provider prompt 使用 `PROJECT SKILL CATALOG (UNTRUSTED DISCOVERY METADATA)` 边界,只列 metadata 和内容哈希,不包含 frontmatter 后正文。模型必须先判断任务是否匹配,只选择必要 Skill;在声称使用或依据 Skill 行动前,必须用 `file.read` 读取 catalog 给出的精确 `entryPath`,正文较长时按行继续读取足够上下文。Skill 指向的 `references / scripts / assets` 仍只是项目文件;只在任务需要时读取,脚本执行必须另走现有命令工具和权限确认,不能因 Skill 存在而自动执行。
- Skill metadata、正文和资源不能改变 Agent/Goal/Session/run 身份,不能授予工具、网络、MCP、文件或命令权限,不能替用户批准动作,也不能放宽沙箱、隐私、verification/finalization、仓库 scope 或副作用重放门禁。Skill 与 `AGENTS.md` 冲突时,适用路径的 scoped `AGENTS.md` 仍是更高的项目规范;Skill 只能在这些边界内补充领域工作流。
- 确定性验收必须覆盖两个发现根、同名优先级、非法名称/YAML/路径、符号链接、文件与数量预算、metadata 清洗、正文不进入首轮 prompt、Skill 内容变化推进 fingerprint、v2 pending 写动作被阻断,以及 Provider 捕获请求中首轮只有 metadata、成功 `file.read` 后下一轮才出现正文 marker。真实 Provider 使用现有 real-e2e harness 的 `project-skill` suite:任务不包含 Skill 名称、入口、正文 marker 或工具配方,hash-only 验收不能反推正文;最终必须证明首个项目变更前已真实读取匹配 Skill、未读取无关 Skill、只修改目标文件并完成真实验证,且 Provider lifecycle、唯一 assistant、配置隔离和零泄漏门禁全部通过。
2026-07-16 正式 `openai_chat / gpt-5.5``project-skill` suite **PASS**。一次性项目的 hash-only 原始验收先真实失败;Agent 在首个项目变更前精确读取匹配 `.codex/skills/release-capsule/SKILL.md` 1 次,无关 Skill 读取为 0,以 1 个项目变更动作只修改 `game/release-capsule.txt`,随后 Agent `project.verify` 与宿主独立复验均通过。最终脚本复跑记录 25 条 task、41 条 event、57 条 Agent DB、5 个成功工具动作和 2 个确认动作;4 组 tool-plan Provider lifecycle 全部唯一 `started -> completed`,最终 assistant 与 completed audit 各 1,重复 message/receipt、fallback replay 和遗留 finalization 均为 0。最终回复、公共审计、测试报告中的 Skill 正文、API Key、诱饵、项目与正式配置绝对路径泄漏均为 0;正式配置 CLI 调用为 0,源 Runner endpoint 和配置副本保持不变,隔离 Runner、AppData 与 disposable 项目已按 sentinel 清理。V1.25 真实行为门禁至此完成。
## 验收命令
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`
@@ -998,6 +1012,7 @@ V1.24 修正仓库启动上下文把 `AGENTS.md` 与 README/CONTEXT 一律描述
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite mcp-runtime`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite user-input-runtime`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite scoped-agents`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite project-skill`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite full`
- `npm run check:encoding`
- `git diff --check`
@@ -570,4 +570,6 @@ game-project/
- 2026-07-16 V1.23 已完成真实验收:正式 `openai_chat / gpt-5.5` 路由在 Project Supervisor 上产生 1 个含 2 选项的 Needs input,等待期 Runner pidfd 强杀恢复未增加 Provider 请求,回答后同 Session/run 完成唯一最终回复。问题/回答各一条,重复消息、公共正文、密钥、路径和报告泄漏均为 0,隔离现场已清理。
- 2026-07-16 起,同一 Runtime 文档的“V1.24 Codex 式 scoped `AGENTS.md` 仓库指令”作为项目规范加载事实源。仓库启动上下文升级为 v2,根与嵌套 `AGENTS.md` 携带规范 scope 并按根到叶适用,更深规则只覆盖自身目录树,兄弟 scope 不串用;README/CONTEXT 明确保持不可信参考数据。项目指令不能扩大工具、确认、沙箱、隐私或完成门禁,旧 v1 pending fingerprint 必须先形成 repository drift blocker 再重规划。
- 2026-07-16 V1.24 已完成真实验收:正式 `openai_chat / gpt-5.5``scoped-agents` suite 在无规则正文、期望内容和工具配方的任务下,让同一 Agent 只修改 `alpha / beta` 两个兄弟目录交付文件;根、父、各自叶规则全部精确命中且兄弟串用为 0,Agent `project.verify` 与宿主复验均通过。最终脚本复跑的 8 组 Provider lifecycle 唯一闭合,最终 assistant/completed 各 1,重复持久化,以及最终回复/公共审计/报告中的规则正文、API Key、诱饵、项目/配置路径泄漏均为 0,隔离现场完整清理;不再把 prompt 可见性代替模型遵循证据。
- 2026-07-16 起,同一 Runtime 文档的“V1.25 Codex 式项目 Skill 发现与渐进加载”作为项目工作流加载事实源。仓库启动上下文升级为 `repository-startup-context-v3`,只发现项目内 `.codex/skills/<name>/SKILL.md``.agents/skills/<name>/SKILL.md` 直接入口,同名时 `.codex` 优先;首轮 prompt 只注入清洗后的 `name / description / entryPath / contentSha256`,正文必须在任务命中后通过现有 `file.read` 按需读取。Skill 不能扩大工具、权限、确认、沙箱、隐私或完成门禁,与适用路径的 `AGENTS.md` 冲突时后者优先;active Skill 变化推进 repository fingerprint 并阻断旧 pending 动作后重规划。
- 2026-07-16 V1.25 已完成真实验收:正式 `openai_chat / gpt-5.5``project-skill` suite 先让 hash-only 原始验收真实失败;Agent 在首个变更前精确读取匹配 Skill 1 次、无关 Skill 0 次,以 1 个变更动作只修改目标文件,Agent `project.verify` 与宿主复验均通过。最终脚本复跑记录 25 条 task、41 条 event、57 条 Agent DB 和 5 个成功工具动作;4 组 tool-plan Provider lifecycle 唯一闭合,最终 assistant/completed 各 1Skill 正文、API Key、诱饵、项目/配置路径泄漏和重复持久化均为 0,隔离 Runner/AppData/项目完整清理。
- 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。