完善Agent多文件变更集能力

新增受确认的 project.patchset 事务式多文件修改与回滚机制

补充 checkpoint 内容差异和文件内容哈希约束

加强隔离 Agent 路径范围与仓库指纹复核

扩展共享命令契约、真实 LLM E2E 与 Runtime 文档
This commit is contained in:
AIGameCreator App
2026-07-13 01:08:36 +08:00
parent 49ea9eec67
commit 5c3b8ec267
15 changed files with 4193 additions and 100 deletions
File diff suppressed because it is too large Load Diff
+8
View File
@@ -1436,12 +1436,14 @@ dependencies = [
"serde_json",
"sha2",
"shared-contracts",
"similar",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tempfile",
"tokio",
"unicode-normalization",
"url",
"zip",
]
@@ -3799,6 +3801,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "similar"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "siphasher"
version = "1.0.3"
@@ -13,6 +13,7 @@ futures = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
similar = "2.7"
platform-llm = { path = "../../../server-rs/crates/platform-llm" }
platform-agent = { path = "../../../server-rs/crates/platform-agent" }
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
@@ -23,6 +24,7 @@ tauri-plugin-opener = "2.5.4"
tempfile = "3"
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "time"] }
url = "2"
unicode-normalization = "0.1"
zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(unix)'.dependencies]
File diff suppressed because one or more lines are too long
@@ -360,6 +360,31 @@ pub(crate) fn validate_isolated_agent_tool_scope_at(
"动态隔离子 Agent 默认拒绝无 writeScope 落点的工具:{tool}"
));
}
if tool == "project.patchset" {
let changes = input
.get("changes")
.and_then(Value::as_array)
.filter(|changes| !changes.is_empty())
.ok_or_else(|| "project.patchset 缺少非空 changes".to_string())?;
for change in changes {
let path = change
.get("path")
.and_then(Value::as_str)
.ok_or_else(|| "project.patchset change 缺少字符串 path".to_string())?;
let path = normalize_relative_path(path)?;
resolve_local_project_path(root, &path)?;
if !instance
.write_scopes
.iter()
.any(|scope| path_matches_write_scope(&path, scope))
{
return Err(format!(
"动态隔离子 Agent patchset 路径超出 writeScopes{path}"
));
}
}
return Ok(());
}
if !matches!(tool, "file.write" | "file.patch" | "file.delete") {
return Ok(());
}
@@ -1549,6 +1574,30 @@ mod tests {
&serde_json::json!({"path": "game/b/main.js"}),
)
.is_err());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"project.patchset",
&serde_json::json!({
"changes": [
{"operation": "create", "path": "game/a/main.js", "content": "ok"},
{"operation": "delete", "path": "game/a/old.js", "expectedSha256": "a".repeat(64)}
]
}),
)
.is_ok());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
"project.patchset",
&serde_json::json!({
"changes": [
{"operation": "create", "path": "game/a/main.js", "content": "ok"},
{"operation": "create", "path": "game/b/main.js", "content": "blocked"}
]
}),
)
.is_err());
assert!(validate_isolated_agent_tool_scope_at(
temp.path(),
instance_id,
@@ -50,6 +50,7 @@ mod config;
#[cfg(all(debug_assertions, not(test)))]
mod debug;
mod isolated_agent;
mod patchset;
mod preview;
mod project;
mod repository_context;
@@ -64,6 +65,7 @@ use command_exec::*;
use commands::*;
use config::*;
use isolated_agent::*;
use patchset::*;
use preview::*;
use project::*;
use repository_context::*;
@@ -796,14 +798,14 @@ struct LocalProjectExportPackagesResult {
packages: Vec<LocalProjectExportPackageSummary>,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectDiffEntry {
path: String,
status: String,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct LocalProjectDiffResult {
checkpoint_id: String,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4197,3 +4197,10 @@
- 决策:AppData LLM 配置使用全局 `llm.reasoningEffort` 和可选 `agentLlm.<agentId>.reasoningEffort`;per-Agent 配置有值时覆盖全局、缺省时继承全局。值只允许 `default / low / medium / high``default` 不向 Provider 发送推理档位;发布默认固定为 `high`planning、普通单 Agent 聊天和最终回复共用同一解析结果,不再硬编码 `low`
- 验证:真实 `gpt-5.5` 的最终安全收紧版 `llm-runtime` 套件已完成失败 `command.exec` -> 精确修复 -> 不同 argv 复验通过,并覆盖 Runner 强杀恢复且 run/session 身份稳定、95 条 task、161 条 event、137 条 Agent DB、13 条合法工具协议、6 套确认生命周期、3 个隔离实例和唯一 join;两次命令只审计 args 数量与 SHA-256,副作用重放、重复 action / message / receipt 和密钥 / 诱饵泄露均为 0,临时项目按 sentinel 自动清理。
- 详细白名单、参数拒绝规则与验收口径见 `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` 的“V1.2 对标 Codex CLI 增量”。
## 2026-07-12 AI 游戏创作 Agent Runtime V1.3 多文件变更集
- 决策:新增默认 `confirm``project.patchset`。一次 action 最多预检 12 个 create / update / deleteupdate / delete 必须绑定 `file.read` 返回的完整 SHA-256,create 必须绑定“目标不存在”。所有路径、大小、匹配数、NFKC + Unicode 小写后的可移植碰撞、符号 / 硬链接、Agent 控制面和敏感路径在源码写入前失败关闭。
- 决策:patchset 预检通过后在同一项目写锁内自动 checkpoint,prepared 审计成功后才推进一次 revision 并应用全部变更。锁内再次校验 policy、pending 身份、revision / verification gate 和 repository fingerprint;应用中途失败必须回滚。revision / gate、回滚或完成审计不完整进入 `needs-reconciliation``executing` 恢复不得自动重放。成功审计只保存路径、operation、前后摘要与字节数,不保存源码正文。
- 决策:`project.diff` 增加可选的有界内容 hunks;使用成熟文本 diff 库在项目锁内比较 checkpoint 与当前项目,生成后重算路径差异,不一致时拒绝混合快照;二进制、非 UTF-8、文件 / 总预算截断必须显式标记。最新内容 diff 在 128 KiB context bundle 中作为压缩保护项最多保留 24,256 字符,避免被普通 observation 的 1,600 字符上限截断。Agent 完成 patchset 后先用返回的 checkpointId 审查内容 diff,再执行可验证命令。
- 验证:本地 441 项 Tauri 测试已覆盖多文件成功、预检全失败、大小写重复、敏感 / 链接路径、SHA 漂移、prepared / completed 审计失败、应用中断回滚和一次 revision。真实 `gpt-5.5``llm-runtime` 套件已形成唯一 patchset,同时更新 / 创建文件并读取绑定 checkpointId 的 2 项未截断内容 diff,通过最终命令、项目验证和桌面 / 移动浏览器验证;Runner 强杀恢复后副作用重放、重复 action / message / receipt、半完成文件和密钥 / 诱饵泄露均为 0。
@@ -262,6 +262,59 @@ npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> -
- `default` 表示不向 Provider 发送推理档位;其余值映射到统一 LLM 请求。工具规划、普通单 Agent 聊天和最终回复必须使用同一解析后的 Agent 配置,Runtime 不再硬编码 `low`
- 发布默认值使用 `high`,旧配置缺少字段时由默认配置补齐;非 OpenAI Responses / Chat Provider 可以选择 `default`,避免发送不支持的参数。
## V1.3 多文件变更集与内容审查
### `project.patchset`
单 Agent 新增结构化 `project.patchset`,用于替代复杂代码任务中连续多次 `file.write / file.patch / file.delete` 形成的半完成中间态。该工具默认 `confirm`,一次最多 12 个互不重复的项目内文件变更,总输入正文不超过 256 KiB,单文件修改后仍受 2 MiB 文本上限约束:
```json
{
"changes": [
{
"operation": "create",
"path": "game/new-file.js",
"content": "..."
},
{
"operation": "update",
"path": "game/main.js",
"expectedSha256": "读取文件时返回的 64 位摘要",
"oldText": "唯一旧文本",
"newText": "新文本",
"expectedReplacements": 1
},
{
"operation": "delete",
"path": "game/obsolete.js",
"expectedSha256": "读取文件时返回的 64 位摘要"
}
]
}
```
- `create` 要求目标不存在;`update / delete` 要求目标为项目内普通文本文件且完整 SHA-256 与最近读取一致;`update` 还要求 `oldText` 精确匹配声明次数。`file.read` observation 必须返回当前内容 SHA-256,供模型形成乐观并发条件。
- 所有路径、文本大小、重复路径、NFKC + Unicode 小写后的可移植碰撞、符号链接 / reparse point、硬链接、Agent 控制面、checkpoint、敏感配置、密钥和数据库路径在任何源码写入前一次性校验。任一变更无效时整个 patchset 不写源码;自动 checkpoint 和内容 diff 同样拒绝多链接普通文件,不能借项目内 inode 别名读取或改写项目外文件。
- 预检通过后,Runtime 在同一项目写锁内自动创建 checkpoint,并先把 `actionId / actionFingerprint / checkpointId / 变更摘要` 写入 Agent DB prepared 审计;prepared 审计失败时不得推进 revision 或写源码。
- prepared 成功后只推进一次 project revision,再按计划应用全部变更。中途失败时用内存中的原始内容回滚已应用项;回滚完整则返回失败但保留已推进 revisionrevision / verification gate 持久化不完整、回滚不完整或完成审计不完整都进入 `needs-reconciliation`。Runner 在 `executing` 阶段退出仍不得自动重放,checkpoint 是人工核对和恢复依据。
- patchset 取得项目锁后必须再次校验 policy、pending action 身份、revision / verification gate 和 repository fingerprint;等待锁期间外部修改 `AGENTS.md` 或启动上下文时,旧动作返回 drift blocker,不进入预检、checkpoint 或 revision。
- 成功审计只保存路径、operation、前后 SHA-256、字节数和 checkpointId,不保存源码正文。一个 patchset 无论修改多少文件都只形成一个工具 action、一个 revision 和一个结构化审计记录。
### 有界内容 diff
现有 `project.diff` 增加 `includeContent``maxFiles``maxChars`
- `includeContent=false` 保持原有路径级摘要;`true` 时从指定 checkpoint 与当前项目生成统一 diff hunks,默认最多 20 个文件、24,000 字符、每个 hunk 保留 3 行上下文。
- 内容 diff 使用成熟文本 diff 库生成;二进制、非 UTF-8、单文件超预算或总预算耗尽时只返回路径、状态、SHA-256 和明确的 truncated / binary 标记,不把截断结果描述成完整 diff。
- 内容 diff 在项目锁内读取,并在生成 hunks 后重新计算路径级差异;前后快照不一致时整个结果失败,不得把并发混合状态标成 `truncated=false`
- 输出继续排除 Agent 控制面、checkpoint、敏感配置、密钥、数据库与构建产物,并在进入 LLM observation 前统一做路径和凭据清洗。
- 为让下一轮 planning 真正看到 hunks,最新内容 diff 不走普通 observation detail 的 1,600 字符压缩,而是在 128 KiB context bundle 内最多保留 24,256 字符并作为窗口压缩保护项;`maxChars` 的公开上限固定为 24,000。
- Runtime prompt 要求 `project.patchset` 成功后使用其 checkpointId 调用 `project.diff(includeContent=true)` 审查整体变更,再执行验证型命令。真实 Provider E2E 必须证明一次 patchset 同时更新和创建文件、内容 diff 同时看到两项变更、最终验证通过且没有重复 action 或半完成文件。
### 2026-07-13 真实验收结果
发布 AppData 中配置的真实 `gpt-5.5` 已通过 V1.3 `llm-runtime` 套件:同一 run 先用 `file.read` 取得完整 SHA-256,只执行 1 次 `project.patchset`,同时更新 `game/index.html` 和创建第二个文件;prepared / completed 审计各 1 条,patchset 只推进 1 次 revision,随后用返回的 checkpointId 读取 2 项未截断内容 hunks。Runner 强制终止后恢复原 run / session,最终项目验证、桌面 / 移动浏览器验证、3 个隔离实例和唯一 join 均通过;87 条 task、144 条 event、130 条 Agent DB 记录中副作用重放、重复 action / message / receipt、半完成文件、密钥和诱饵泄露均为 0,临时项目已按 sentinel 清理。
## 验收命令
- `npm run ai-game-creator-shell:typecheck`
@@ -20,13 +20,17 @@
同一文档的“V1.2 对标 Codex CLI 增量”继续作为受控命令与推理档位的事实源。`command.exec` 只接受 Runtime 白名单内的固定 `program` 和逐项 `args` argv,默认 `confirm`,可执行文件解析为项目外绝对路径且子进程只使用安全 PATH;不解析 shell 字符串,不提供管道、重定向、PTY 或后台进程。它的 action、stdout / stderr、退出码、超时与源码指纹结果统一进入现有 `action / observation`、project revision、verification gate 和 `needs-reconciliation` 链路;只有明确验证型命令且退出码、源码指纹、命令日志、manifest 与 Agent DB 审计全通过才签发 passed gateGit / rg / cargo metadata / 普通 npm run 只作诊断。首版只请求终止受控进程组,安全等级与 `project.verify` 相同,不宣称已具备完整 OS sandbox 或 detached-process 隔离。
同一文档的“V1.3 多文件变更集与内容审查”作为复杂代码修改的新事实源。`project.patchset` 在一个确认动作和一把项目锁内预检最多 12 个 create / update / delete,自动 checkpoint、只推进一次 revision,并以 SHA-256 乐观并发条件和回滚语义避免半完成修改;`project.diff(includeContent=true)` 返回有界统一 diff hunks。它不开放任意 `git apply` 文本,也不替代修改后的可执行验证。
2026-07-12 真实验收:发布 AppData 中的真实 `gpt-5.5` 已通过最终安全收紧后的 `llm-runtime` 套件,覆盖 Runner 强杀恢复且 run/session 身份稳定、仓库上下文、checkpoint/精确修改、失败命令诊断与修复复验、6 套确认生命周期、项目验证、桌面与移动非空画布证据、3 个隔离实例并行和唯一 all-join95 条 task、161 条 event、137 条 Agent DB、13 条合法工具协议、副作用判重、终态投影、assistant audit、消息、回执和密钥泄露均以结构化落盘事实验收。`full` 套件仍要求 External Editor API 配置,缺失时必须返回 `BLOCKED(editorApi)`,不得记为通过。
2026-07-13 V1.3 真实验收:同一真实 Provider 套件已改为先读取 SHA-256,再用唯一一次 `project.patchset` 同时更新和创建文件,并使用自动 checkpointId 读取 2 项内容 hunksprepared / completed 审计各 1 条、patchset revision 增量为 1Runner 强杀恢复、命令和项目验证、双视口浏览器验证、隔离 Agent join、重复副作用与密钥扫描继续全部通过。
以下能力清单保留 Runtime V1 的演进记录;其中“App 进程内 tokio task”“跨进程同项目写入不作为支持目标”和“恢复到当前 App 进程”的旧描述均已由 V1.1 替代。当前边界是 App / CLI 只落账并唤醒同一发布二进制的独立 Runnerappend-only JSONL 使用进程内锁加 OS 文件锁,恢复继续由 Runner 接管同一 run / session。
Agent Runtime 负责:
- 2026-07-12 安全边界补充:`project.verify` 的 script 最多 160 个字符,固定使用系统 script shell,并在解析和执行前拒绝项目级 `.npmrc` 改写 npm 语义。Runtime context bundle 绑定 `projectId / agentId / taskId / sessionId / runId / source / task`,结尾换行计入 64 KiB 上限;恢复时同时校验 `nextLoopIndex`、context window、当前窗口已完成轮数、观察指纹、计划、observation 数量,以及 `contextStalled` 只能位于非零上下文窗口边界。bundle 通过项目内无符号链接路径原子写入,并从同一文件句柄最多读取 64 KiB;项目路径和常见 `sk- / GitHub / npm / AWS / JWT` 凭据统一脱敏。工具 observation 进入 context checkpoint 后才删除 `observed-*` 动作账本,避免重启时旧 ledger 抢占有效 bundle。
- 2026-07-12 安全边界补充2026-07-13 调整`project.verify` 的 script 最多 160 个字符,固定使用系统 script shell,并在解析和执行前拒绝项目级 `.npmrc` 改写 npm 语义。Runtime context bundle 绑定 `projectId / agentId / taskId / sessionId / runId / source / task`,结尾换行计入 128 KiB 上限;恢复时同时校验 `nextLoopIndex`、context window、当前窗口已完成轮数、观察指纹、计划、observation 数量,以及 `contextStalled` 只能位于非零上下文窗口边界。bundle 通过项目内无符号链接路径原子写入,并从同一文件句柄最多读取 128 KiB;项目路径和常见 `sk- / GitHub / npm / AWS / JWT` 凭据统一脱敏。工具 observation 进入 context checkpoint 后才删除 `observed-*` 动作账本,避免重启时旧 ledger 抢占有效 bundle。
- 总任务拆分和任务图状态流转。
- 6 个专业组调度:策划组、美术组、程序组、数值组、音乐组、运营组。
@@ -73,7 +77,7 @@ Agent Runtime 负责:
- 2026-07-11 补充:开发验证可用 `npm run ai-game-creator-shell:agent-task -- [--init] <projectPath> <agentId> <task>` 无 UI 启动单 Agent 后台任务。CLI 只负责可选初始化、调用现有 Runtime、按 runId 轮询终态并打印 `status / phase / replyText / pendingActionId`,不复制 planning 或工具执行逻辑;默认 10 分钟轮询上限。`waiting-for-confirmation` 会以非零状态退出并要求转到开发窗口确认,CLI 不提供跳过项目权限的自动确认参数。该入口用于真实 provider 的可重复端到端验收,不进入普通用户界面。
- 2026-07-11 补充:后台工具规划与最终回复的 LLM 请求新增可恢复错误重试:`LlmError::EmptyResponse` 原样自动重试最多 3 次;`Timeout / Connectivity / Transport` 及上游 `408 / 429 / 5xx` 最多额外重试 2 次并按 `500ms / 1000ms` 退避。配置、请求、流能力、反序列化错误及其他 `4xx` 不重试。重试发生在工具计划被解析和执行前,或最终回复尚未落盘时,因此不会重复执行已经落盘的工具副作用;重试耗尽后仍写入原有 `error / turn.failed` 事件并把失败消息追加到当前 Agent 会话。
- 2026-07-11 调整,2026-07-12 更新:后台单 Agent planning loop 每 6 轮形成一个上下文压缩窗口,每轮最多 3 个工具动作;6 轮是窗口大小,不是单个 run 的固定上限。`loopIteration` 在同一 run 内连续递增,`maxLoopIterations` 指向当前窗口的结束轮次;待确认或重启恢复后按 context bundle 的 `nextLoopIndex` 在同一 run 继续。每个窗口结束时压缩已有 observation;窗口产生新的独立观察时继续下一窗口,最近 6 轮没有独立进展或相邻窗口指纹重复时才写入 `failed / budget-exhausted``loop-budget-exhausted`,不生成总结伪装完成。这只调整后台单 Agent Runtime;游戏草案 Generator/Evaluator 仍保持独立的 3 轮修复预算。旧摘要中“后台最多 3 轮”或“整个 run 最多 6 轮”的描述不再有效。
- 2026-07-12 补充并冻结:后台 Agent 每个 run 的可恢复 planning 上下文通过临时文件替换原子写入 `.agent/runtime/context-bundles/<agentId>/<runId>.json`,绑定 Agent、Task、Session、Run、任务正文和 revision / verification gate 关联,schema 固定升级为 `game-creator-runtime-context-bundle.v2`;保存 `nextLoopIndex`、当前窗口、计划、fallback response、压缩后的 observation、上一窗口指纹和 `contextStalled`。stale continuation 必须清空旧 actions 与 fallback response,保留 blocker、loop 位置和窗口进度;`contextStalled` 一旦在窗口边界成立,同 run 重规划和进程重启都不得清除。`runtime.verification` 的上下文指纹忽略动态 revision 数值前缀,仅保留稳定处置指引;成功 `project.verify / game.static_smoke` 的动态命令输出不进入窗口指纹。revision 数字或时间戳持续变化本身不算独立进展,重复 stale 最迟在相邻窗口指纹重复时以 `loop-budget-exhausted` 终止。单文件最多 64 KiB、最多 12 条 observation;写入前统一截断并过滤敏感内容和项目绝对路径,安全校验失败时拒绝落盘。读取时要求普通文件并校验 schema、Agent、Session、Run、任务正文、observation 数量和 revision / gate 关联,身份不一致时拒绝续跑。v1 context bundle 恢复必须失败关闭,不自动迁移,也不能把缺失 gate 当成 `requiresVerification=false`;revision 与验证资格仍以锁内重读的独立持久化文件为准,bundle 只保存恢复上下文。该文件属于 Runtime 私有控制面,不等同于根级 `.agent/context.bundle.json`,不得由通用文件工具暴露。
- 2026-07-12 补充并冻结2026-07-13 调整容量:后台 Agent 每个 run 的可恢复 planning 上下文通过临时文件替换原子写入 `.agent/runtime/context-bundles/<agentId>/<runId>.json`,绑定 Agent、Task、Session、Run、任务正文和 revision / verification gate 关联,schema 固定升级为 `game-creator-runtime-context-bundle.v2`;保存 `nextLoopIndex`、当前窗口、计划、fallback response、压缩后的 observation、上一窗口指纹和 `contextStalled`。stale continuation 必须清空旧 actions 与 fallback response,保留 blocker、loop 位置和窗口进度;`contextStalled` 一旦在窗口边界成立,同 run 重规划和进程重启都不得清除。`runtime.verification` 的上下文指纹忽略动态 revision 数值前缀,仅保留稳定处置指引;成功 `project.verify / game.static_smoke` 的动态命令输出不进入窗口指纹。revision 数字或时间戳持续变化本身不算独立进展,重复 stale 最迟在相邻窗口指纹重复时以 `loop-budget-exhausted` 终止。单文件最多 128 KiB、最多 12 条 observation普通 observation detail 仍压到 1,600 字符,最新内容 diff 可保留到 24,256 字符并在窗口压缩时优先保留。写入前统一截断并过滤敏感内容和项目绝对路径,安全校验失败时拒绝落盘。读取时要求普通文件并校验 schema、Agent、Session、Run、任务正文、observation 数量和 revision / gate 关联,身份不一致时拒绝续跑。v1 context bundle 恢复必须失败关闭,不自动迁移,也不能把缺失 gate 当成 `requiresVerification=false`;revision 与验证资格仍以锁内重读的独立持久化文件为准,bundle 只保存恢复上下文。该文件属于 Runtime 私有控制面,不等同于根级 `.agent/context.bundle.json`,不得由通用文件工具暴露。
- 2026-07-11 调整:后台任务的可执行正文上限统一为 4,000 字符。入队 JSONL、启动后的 `currentTask/currentGoal`、planning prompt、待确认动作 task context、确认续跑和重启恢复都保留同一份正文;对话仍保存用户原始消息。状态事件、列表卡片和 `agent.db` 摘要可继续使用较短安全预览,但不能再反向作为后续 LLM 执行输入。这样长任务末尾的验收标记和输出格式要求不会在队列边界被 180 字符截断。
- 2026-07-11 调整,2026-07-12 由 Runtime V1.2 更新:后台 planning 使用 4,000 输出 token,最终回复使用 2,400,并继续叠加最多 3 次 EmptyResponse 重试。推理档位不再硬编码为 `low`planning、普通单 Agent 聊天和最终回复统一使用解析后的 `llm.reasoningEffort``agentLlm.<agentId>.reasoningEffort` 有值时覆盖全局、缺省时继承全局;取值只允许 `default / low / medium / high`,发布默认 `high``default` 表示不向 Provider 发送推理档位。
- 2026-07-11 补充:后台单 Agent 的工具 planning 响应必须提供可反序列化为 `thinkingSummary / plan / actions / response` schema 的 JSON object。Runtime 从模型输出中解析首个完整对象,因此对象后的尾随说明可以忽略;只有普通文本、没有完整对象,或对象无法反序列化时都不构成有效工具计划。对于这两类无效输出,Runtime 最多追加 2 次自动格式修复请求,每次只把限长且经过统一敏感信息过滤的上一次输出作为修复上下文,并把修复尝试写入 `.agent/agent.db``agent.runtime.tool_plan.repair` 审计。修复预算耗尽后进入既有工具规划失败路径,不得把普通文本折算为空 actions + response,也不得因此进入 completed;最终回复阶段仍按其独立的普通文本契约处理。
@@ -19,7 +19,8 @@ describe('AI 游戏创作 App 共享契约', () => {
it('keeps command permissions explicit', () => {
const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(52);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(53);
expect(commandIds).toContain('project.patchset');
expect(commandIds).toContain('command.exec');
expect(commandIds.indexOf('command.exec')).toBe(
commandIds.indexOf('command.run_limited') + 1,
@@ -38,6 +39,11 @@ describe('AI 游戏创作 App 共享契约', () => {
(command) => command.id === 'command.exec',
)?.permission,
).toBe('confirm');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'project.patchset',
)?.permission,
).toBe('confirm');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'project.verify',
@@ -19,6 +19,7 @@ export const GAME_CREATION_APP_COMMANDS = [
{ id: 'project.index', permission: 'auto' },
{ id: 'project.checkpoint', permission: 'confirm' },
{ id: 'project.diff', permission: 'auto' },
{ id: 'project.patchset', permission: 'confirm' },
{ id: 'project.restore', permission: 'confirm' },
{ id: 'project.verify', permission: 'confirm' },
{ id: 'project.export_package', permission: 'confirm' },
@@ -21,13 +21,14 @@ pub struct GameCreationAppCommandDescriptor {
pub permission: GameCreationAppPermission,
}
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 52] = [
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 53] = [
command("help.show", GameCreationAppPermission::Auto),
command("project.create", GameCreationAppPermission::Confirm),
command("project.status", GameCreationAppPermission::Auto),
command("project.index", GameCreationAppPermission::Auto),
command("project.checkpoint", GameCreationAppPermission::Confirm),
command("project.diff", GameCreationAppPermission::Auto),
command("project.patchset", GameCreationAppPermission::Confirm),
command("project.restore", GameCreationAppPermission::Confirm),
command("project.verify", GameCreationAppPermission::Confirm),
command("project.export_package", GameCreationAppPermission::Confirm),
@@ -637,7 +638,7 @@ mod tests {
#[test]
fn command_contract_keeps_expected_permissions() {
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 52);
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 53);
let command_ids = GAME_CREATION_APP_COMMANDS
.iter()
@@ -671,6 +672,15 @@ mod tests {
.expect("command.exec should exist");
assert_eq!(command_exec.permission, GameCreationAppPermission::Confirm);
let project_patchset = GAME_CREATION_APP_COMMANDS
.iter()
.find(|command| command.id == "project.patchset")
.expect("project.patchset command should exist");
assert_eq!(
project_patchset.permission,
GameCreationAppPermission::Confirm
);
let project_verify = GAME_CREATION_APP_COMMANDS
.iter()
.find(|command| command.id == "project.verify")