新增命令输出分页回查能力

新增 command.output_read 并按当前 Agent 的持久动作身份分页读取命令输出
为 command.exec 写入受限 transcript sidecar 并保持审计与回执零正文
完善隔离 Agent 模板提示、失败结果收束和重复 spawn 去重
将真实 Provider E2E 改为无固定配方的结果导向验收
同步共享命令与能力契约并补齐 Rust 和 TypeScript 测试
更新 Runtime 技术方案、实施计划和共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-14 00:22:02 +08:00
parent 74dd45ef13
commit 648170b224
13 changed files with 2698 additions and 123 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -110,6 +110,10 @@ pub(crate) struct ProjectCommandResult {
pub(crate) timed_out: bool,
pub(crate) duration_ms: u64,
pub(crate) output: String,
pub(crate) capture_truncated: bool,
pub(crate) output_ref: Option<String>,
pub(crate) output_sha256: String,
pub(crate) total_lines: usize,
pub(crate) source_fingerprint_before: String,
pub(crate) source_fingerprint_after: String,
pub(crate) source_changed: bool,
@@ -125,6 +129,7 @@ pub(crate) enum ProjectCommandErrorStage {
Spawn,
Execution,
PostExecutionFingerprint,
OutputSidecar,
AuditLog,
ManifestProjection,
}
@@ -156,6 +161,7 @@ impl ProjectCommandError {
self.stage,
ProjectCommandErrorStage::Execution
| ProjectCommandErrorStage::PostExecutionFingerprint
| ProjectCommandErrorStage::OutputSidecar
| ProjectCommandErrorStage::AuditLog
| ProjectCommandErrorStage::ManifestProjection
)
@@ -174,6 +180,7 @@ impl ProjectCommandErrorStage {
Self::Spawn => "spawn",
Self::Execution => "execution",
Self::PostExecutionFingerprint => "post-execution-fingerprint",
Self::OutputSidecar => "output-sidecar",
Self::AuditLog => "audit-log",
Self::ManifestProjection => "manifest-projection",
}
@@ -201,6 +208,13 @@ struct ProjectCommandProcessResult {
exit_code: Option<i32>,
timed_out: bool,
output: String,
capture_truncated: bool,
}
#[derive(Debug)]
struct BoundedCommandOutput {
text: String,
truncated: bool,
}
#[derive(Debug)]
@@ -233,20 +247,26 @@ impl BoundedCommandBytes {
}
}
fn finish(self) -> String {
fn finish(self) -> BoundedCommandOutput {
let tail = self.tail.into_iter().collect::<Vec<_>>();
if self.total <= self.max_bytes {
let mut bytes = self.head;
bytes.extend(tail);
return String::from_utf8_lossy(&bytes).into_owned();
return BoundedCommandOutput {
text: String::from_utf8_lossy(&bytes).into_owned(),
truncated: false,
};
}
let omitted = self.total.saturating_sub(self.head.len() + tail.len());
format!(
"{}\n...<{} output bytes omitted>...\n{}",
String::from_utf8_lossy(&self.head),
omitted,
String::from_utf8_lossy(&tail)
)
BoundedCommandOutput {
text: format!(
"{}\n...<{} output bytes omitted>...\n{}",
String::from_utf8_lossy(&self.head),
omitted,
String::from_utf8_lossy(&tail)
),
truncated: true,
}
}
}
@@ -1087,7 +1107,9 @@ async fn terminate_project_command_process_group(
))
}
async fn read_bounded_project_command_output<R>(mut reader: R) -> Result<String, String>
async fn read_bounded_project_command_output<R>(
mut reader: R,
) -> Result<BoundedCommandOutput, String>
where
R: tokio::io::AsyncRead + Unpin,
{
@@ -1107,9 +1129,9 @@ where
}
async fn collect_project_command_output_task(
mut task: tokio::task::JoinHandle<Result<String, String>>,
mut task: tokio::task::JoinHandle<Result<BoundedCommandOutput, String>>,
stream_name: &str,
) -> Result<String, String> {
) -> Result<BoundedCommandOutput, String> {
match tokio::time::timeout(Duration::from_secs(2), &mut task).await {
Ok(result) => {
result.map_err(|error| format!("收集 command.exec {stream_name} 失败:{error}"))?
@@ -1380,11 +1402,11 @@ async fn run_project_command_process(
let stderr = stderr
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Execution, error))?;
let mut sections = Vec::new();
if !stdout.trim().is_empty() {
sections.push(format!("stdout:\n{}", stdout.trim()));
if !stdout.text.trim().is_empty() {
sections.push(format!("stdout:\n{}", stdout.text.trim()));
}
if !stderr.trim().is_empty() {
sections.push(format!("stderr:\n{}", stderr.trim()));
if !stderr.text.trim().is_empty() {
sections.push(format!("stderr:\n{}", stderr.text.trim()));
}
if timed_out {
sections.push(format!(
@@ -1400,10 +1422,14 @@ async fn run_project_command_process(
if sections.is_empty() {
sections.push("command.exec 未产生输出".to_string());
}
let output = sanitize_project_verification_output(&sections.join("\n\n"));
let capture_truncated =
stdout.truncated || stderr.truncated || output.contains("...<output truncated:");
Ok(ProjectCommandProcessResult {
exit_code,
timed_out,
output: sanitize_project_verification_output(&sections.join("\n\n")),
output,
capture_truncated,
})
}
@@ -1431,6 +1457,17 @@ pub(crate) async fn run_project_command_at(
arguments: &[String],
cwd: &str,
timeout_seconds: u64,
) -> Result<ProjectCommandResult, ProjectCommandError> {
run_project_command_with_output_at(root, program, arguments, cwd, timeout_seconds, None).await
}
pub(crate) async fn run_project_command_with_output_at(
root: &Path,
program: &str,
arguments: &[String],
cwd: &str,
timeout_seconds: u64,
output_identity: Option<CommandOutputIdentity>,
) -> Result<ProjectCommandResult, ProjectCommandError> {
let spec = resolve_project_command_spec_at(root, program, arguments, cwd, timeout_seconds)?;
let source_fingerprint_before = project_command_source_fingerprint(root)
@@ -1449,6 +1486,43 @@ pub(crate) async fn run_project_command_at(
let status = if completed { "completed" } else { "failed" };
let command_id = project_command_id(&spec);
let updated_at = unix_timestamp();
let default_output_sha256 = format!("{:x}", Sha256::digest(process.output.as_bytes()));
let default_total_lines = command_output_line_count(&process.output);
let (output_ref, output_sha256, total_lines) = if let Some(identity) = output_identity {
let transcript = build_command_output_transcript(
identity,
&command_id,
&spec.program,
&spec.arguments,
&spec.cwd_relative,
process.exit_code,
process.timed_out,
duration_ms,
source_changed,
process.capture_truncated,
&process.output,
updated_at,
)
.map_err(|error| {
ProjectCommandError::new(
ProjectCommandErrorStage::OutputSidecar,
format!("command.exec 执行后构建输出 sidecar 失败,需要人工核对:{error}"),
)
})?;
write_command_output_transcript_at(root, &transcript).map_err(|error| {
ProjectCommandError::new(
ProjectCommandErrorStage::OutputSidecar,
format!("command.exec 执行后写入输出 sidecar 失败,需要人工核对:{error}"),
)
})?;
(
Some(transcript.output_ref),
transcript.output_sha256,
transcript.total_lines,
)
} else {
(None, default_output_sha256, default_total_lines)
};
let log_path = resolve_local_project_path(root, ".agent/logs/command.log")
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::AuditLog, error))?;
if let Some(parent) = log_path.parent() {
@@ -1518,6 +1592,10 @@ pub(crate) async fn run_project_command_at(
timed_out: process.timed_out,
duration_ms,
output: process.output,
capture_truncated: process.capture_truncated,
output_ref,
output_sha256,
total_lines,
source_fingerprint_before,
source_fingerprint_after,
source_changed,
@@ -2040,8 +2118,9 @@ mod tests {
let mut output = BoundedCommandBytes::new(30);
output.push(b"HEAD-0123456789-MIDDLE-abcdefghij-TAIL");
let output = output.finish();
assert!(output.contains("HEAD"));
assert!(output.contains("TAIL"));
assert!(output.contains("omitted"));
assert!(output.text.contains("HEAD"));
assert!(output.text.contains("TAIL"));
assert!(output.text.contains("omitted"));
assert!(output.truncated);
}
}
File diff suppressed because it is too large Load Diff
@@ -212,6 +212,27 @@ pub(crate) fn create_or_read_isolated_group_at(
let request = sanitize_spawn_request(root, request)?;
validate_game_creation_isolated_agent_spawn_request_at_depth(&request, parent_depth)
.map_err(|error| error.to_string())?;
let duplicate = list_json_records(
root,
ISOLATED_AGENT_GROUP_DIR,
"动态隔离 Agent group",
|record| validate_isolated_group_record(root, record),
)?
.into_iter()
.find(|group| {
group.parent_agent_id == parent_agent_id
&& group.parent_session_id == parent_session_id
&& group.parent_run_id == parent_run_id
&& group.parent_action_id != parent_action_id
&& group.depth == parent_depth + 1
&& group.request == request
});
if let Some(duplicate) = duplicate {
return Err(format!(
"相同动态隔离 Agent 请求已由 action {} 创建,不得在同一父 run 重复 spawn",
duplicate.parent_action_id
));
}
let derived = derive_game_creation_isolated_agent_group_at_depth(
parent_action_id,
&request,
@@ -536,6 +557,51 @@ pub(crate) fn build_isolated_child_result_at(
})
}
pub(crate) fn build_isolated_child_result_with_failure_fallback_at(
root: &Path,
instance_id: &str,
task: &IsolatedAgentTerminalTask,
expected_artifacts: &[String],
verification_gate: &IsolatedAgentVerificationGateSnapshot,
evidence: &[GameCreationIsolatedAgentEvidence],
) -> Result<IsolatedAgentBuildResult, String> {
match build_isolated_child_result_at(
root,
instance_id,
task,
expected_artifacts,
verification_gate,
evidence,
) {
Ok(result) => Ok(result),
Err(error)
if terminal_result_status(task)?
== GameCreationIsolatedAgentResultStatus::Completed =>
{
let failure = format!("动态隔离子 Agent 结果发布失败:{error}");
let failed_task = IsolatedAgentTerminalTask {
agent_id: task.agent_id.clone(),
session_id: task.session_id.clone(),
run_id: task.run_id.clone(),
delegation_id: task.delegation_id.clone(),
status: "failed".to_string(),
phase: "failed".to_string(),
terminal_detail: Some(failure.clone()),
error: Some(failure),
};
build_isolated_child_result_at(
root,
instance_id,
&failed_task,
expected_artifacts,
verification_gate,
evidence,
)
}
Err(error) => Err(error),
}
}
pub(crate) fn record_isolated_child_result_at(
root: &Path,
result: &GameCreationIsolatedAgentChildResult,
@@ -1657,6 +1723,73 @@ mod tests {
list_isolated_agent_instances_at(temp.path()).unwrap().len(),
1
);
let duplicate = create_or_read_isolated_group_at(
temp.path(),
"code-prototype",
"parent-run",
"parent-session",
"action-duplicate-request",
&request,
)
.unwrap_err();
assert!(duplicate.contains("不得在同一父 run 重复 spawn"));
assert_eq!(
list_isolated_agent_instances_at(temp.path()).unwrap().len(),
1
);
}
#[test]
fn completed_child_with_missing_expected_artifact_dispatches_failed_join_result() {
let temp = tempdir().unwrap();
let group = create_group(
temp.path(),
"action-missing-artifact",
&request(vec![("code-prototype", "game/missing/**")]),
);
let instance =
resolve_isolated_agent_instance_at(temp.path(), &group.instance_ids[0]).unwrap();
let task = IsolatedAgentTerminalTask {
agent_id: instance.instance_id.clone(),
session_id: instance.session_id.clone(),
run_id: instance.run_id.clone(),
delegation_id: instance.delegation_id.clone(),
status: "completed".to_string(),
phase: "completed".to_string(),
terminal_detail: Some("只读检查完成".to_string()),
error: None,
};
let gate = IsolatedAgentVerificationGateSnapshot {
agent_id: instance.instance_id.clone(),
run_id: instance.run_id.clone(),
requires_verification: false,
mutation_revision: None,
verified_revision: None,
last_verification_tool: None,
last_verification_status: None,
};
let built = build_isolated_child_result_with_failure_fallback_at(
temp.path(),
&instance.instance_id,
&task,
&instance.expected_artifacts,
&gate,
&[],
)
.unwrap();
assert_eq!(
built.result.status,
GameCreationIsolatedAgentResultStatus::Failed
);
assert!(built.result.artifacts.is_empty());
assert!(built
.result
.error
.as_deref()
.is_some_and(|error| error.contains("缺少 expected artifactgame/missing/**")));
assert!(built.join_dispatch.is_some());
}
#[test]
@@ -45,6 +45,7 @@ mod assets;
mod browser;
mod cli;
mod command_exec;
mod command_output;
mod commands;
mod config;
#[cfg(all(debug_assertions, not(test)))]
@@ -64,6 +65,7 @@ use assets::*;
use browser::*;
use cli::*;
use command_exec::*;
use command_output::*;
use commands::*;
use config::*;
use git_inspect::*;
File diff suppressed because it is too large Load Diff
@@ -4245,3 +4245,15 @@
- 决策:data URL、图片字节和视觉 Provider 原始 request 不进入 task、event、Agent DB、receipt 或 raw failure log。专用审计只保存相对路径、SHA-256、字节数、responseId 和结论字符数;terminal receipt 的 safeDetail 使用相同字段白名单,不保存结论正文。多模态 raw failure log 只保留请求元数据并省略 messages,上游错误若回显 data URL 也要清洗。
- 决策:`image.inspect` 进入 context milestone;视觉结论获得 8,000 字符上下文预算,但停滞指纹只使用图片 path / SHA 元数据,不能靠同一图片的措辞变化伪造无限进展。已有 terminal observation / receipt 的恢复只续 planning,不重复调用视觉 Provider。
- 验证:确定性 `image_inspect` 用例 `6/6` 通过;Tauri 全量 513 项中 510 通过、3 项真实浏览器 opt-in 用例按设计忽略。真实 `gpt-5.5` `llm-runtime` 形成 95 条 task、161 条 event、166 条 Agent DB、12 条合法工具协议、14 次成功工具执行和 24 条 receipt;真实视觉调用 1 次、输入图片 2 张、专用 audit / receipt 各 1 条、图片载荷泄漏 0。Runner 强杀恢复身份稳定,revision 3,重复 action / message / receipt、密钥和诱饵泄漏均为 0。
## 2026-07-13 AI 游戏创作 Agent Runtime V1.8 命令输出分页回查
- 决策:新增默认 `auto` 的只读工具 `command.output_read``command-output-read` capability;输入只接受 `actionId / startLine / maxLines`,不接受 agentId、runId、路径或 outputRef。项目或 per-Agent policy 可改为 `confirm / deny`,工具不推进 project revision、不改变 verification gate,也不认领 join。
- 决策:每个 durable `command.exec` 在命令日志、manifest 和 Agent DB 专用审计宣告成功前,先把已清洗且有界的 transcript 以 create-once sidecar 写入 `.agent/runtime/command-outputs/<identitySha256>.json`。sidecar 绑定 Agent、task、session、run、action、fingerprint 和命令终态;文件名由身份哈希生成,单文件最大 256 KiB,正文不扩大现有 stdout / stderr 捕获上限。
- 决策:读取时先按当前精确 Agent 和源 actionId 在 terminal receipt 中定位唯一源 run,再交叉复核 task ledger、`agent.runtime.command.exec` 审计、outputRef、SHA-256、行数、截断、退出码、超时、源码漂移与 sidecar 身份;同一 Agent 的历史 run 可读,跨 Agent、旧版无 sidecar、重复冲突、损坏、超限或链接文件全部失败关闭。
- 决策:transcript 正文只进入当前模型 observation 和受限 context bundletask/event、Agent DB 专用审计、terminal receipt、`agent.action_history` 与验收报告只保存结构化元数据。`command.exec``command.output_read` 的 event detail 均省略;context fingerprint 只使用源 action identity、输出 SHA 和页范围,使同页重复不伪造进展、不同页仍可继续。
- 决策:命令已启动后 sidecar、日志、manifest、Agent DB、verification gate 或 receipt 任一步失败,都保持 failed gate 并进入 `needs-reconciliation`;恢复不得重跑命令。`command.output_read` 自身沿用 durable pending,已有 terminal observation 时只续 planning并补齐 receipt,不生成第二份读取动作。
- 修正:隔离子 Agent 的有效策略和锁内 enforcement 统一以模板 Agent 作为 per-Agent policy subject;动态 `child-*` 实例不再出现策略快照显示 deny、真实执行却按实例 ID 放行的偏差。
- 修正:Runtime prompt 显式列出合法静态模板 taskId,并冻结 `expectedArtifacts` 为完成时必须存在的项目内相对文件/glob、只读任务填写现有被检查文件、`writeScopes` 使用互斥非私有目录 glob。相同父 Agent/run 下相同 spawn request 的新 actionId 在创建实例前拒绝,避免长等待或上下文压缩后重复启动整组 reviewer。
- 修正:completed child 若因 artifact/evidence 结果契约无法构造 completed result,降级落盘为结构化 failed child result 并继续推进 all-join,不能只记 `result_failed` 后永久悬挂父 run。真实 E2E 的副作用判重只统计实际发生的动作;失败与修复后使用相同 argv 的 `command.exec` 由一失败一成功专门契约验收,预检失败不算副作用。可重复只读动作不限定总次数,省略默认参数和显式默认值等价,无依赖的视觉与动作历史只要求都早于最终回复。
- 验证:Tauri 全量 523 项中 520 通过、3 项真实浏览器 opt-in 用例按设计忽略;共享 TS 与 Rust 契约各 7 项、shell typecheck 和 Windows GNU `cargo check` 通过。无固定配方的真实 `gpt-5.5` `llm-runtime` PASS122 条 task、210 条 event、213 条 Agent DB、13 条工具协议、15 次代表性成功工具执行、6 套确认、8 个实际副作用 action 和 32 条 receipt;两次 `command.output_read` 覆盖 248 行并命中短 observation 之外的根错误,唯一 patchset、Runner 强杀恢复、revision 3、3 个隔离实例 / 2 个模板、唯一 continuation delivery、项目验证和双视口视觉检查通过。副作用重放、重复 action/message/receipt、命令正文边界泄漏、图片载荷、密钥和诱饵泄漏均为 0。
@@ -405,9 +405,33 @@ Runner 强制终止后恢复原 run / session 且身份稳定,project revision
### 2026-07-13 真实验收结果
- 确定性 Rust 用例覆盖双图 Responses 请求、magic bytes、伪扩展名、单图超限、跨 Agent/run、符号链接、父目录符号链接、硬链接、auto / confirm / deny、revision 不推进,以及已有 terminal observation / receipt 恢复不重复调用 Provider`image_inspect` 定向用例 `6/6` 通过,Tauri 全量 513 项中 510 通过、3 项真实浏览器 opt-in 用例按设计忽略。
- 发布 AppData 中配置的真实 `gpt-5.5` 已通过 `llm-runtime`:模型实际调用 `image.inspect` 1 次并提交 desktop / mobile 两张截图,专用 audit 1 条、terminal receipt 1 条、responseId 存在,视觉结论在 `agent.action_history` 和最终回复前落盘
- 发布 AppData 中配置的真实 `gpt-5.5` 已通过 `llm-runtime`:模型实际调用 `image.inspect` 1 次并提交 desktop / mobile 两张截图,专用 audit 1 条、terminal receipt 1 条、responseId 存在,视觉结论在最终回复前落盘。`agent.action_history` 与视觉检查彼此独立,不要求固定先后顺序
- 本次形成 95 条 task、161 条 event、166 条 Agent DB、12 条合法工具协议、14 次成功工具执行和 24 条 terminal receiptRunner 强杀后恢复原 run / session 且身份稳定,project revision 为 3,3 个隔离实例、项目验证和浏览器验证通过。task / event / Agent DB / receipt 的图片载荷泄漏为 0,重复 action / message / receipt、密钥和诱饵泄漏均为 0。
## V1.8 命令完整输出分页回查
`command.exec` 会对 stdout / stderr 分别保留有界头尾,但普通 planning observation 只适合携带短摘要。长测试输出的根错误可能位于短摘要之外,因此新增只读模型工具 `command.output_read`,让当前 Agent 按行分页读取自己在当前或历史 run 中已完成命令的清洗后 transcript;不能借此读取任意日志、其他 Agent 的命令或宿主文件。
- 输入固定为 `{"actionId":"action-...","startLine":1,"maxLines":160}``actionId` 必填;`startLine` 为从 1 开始的行号,默认 1`maxLines` 默认 160、最大 240。返回 `lines / startLine / nextLine / totalLines / hasMore / captureTruncated / outputSha256 / exitCode / timedOut / sourceChanged``lines` 是带稳定行号的有界字符串,空 transcript 返回空字符串和 `hasMore=false`
- 每次 terminal `command.exec` 在命令审计完成前写入 `.agent/runtime/command-outputs/<identitySha256>.json`;文件名由 `agentId / taskId / sessionId / runId / actionId` 的稳定哈希生成,不直接使用模型字符串。sidecar 固定绑定这些身份以及 `actionFingerprint / commandId`,保存已经过凭据与绝对路径清洗的有界 transcript、SHA-256、总行数、stdout / stderr capture 是否截断及命令终态元数据。JSON 单文件最大 256 KiB;输出正文继续受现有 stdout / stderr 各 24 KiB 捕获上限约束,sidecar 不能扩大宿主读取面。
- sidecar 使用现有 Runtime 私有 JSON 原子写入与受限读取边界:父目录和目标拒绝符号链接、reparse point、硬链接、非普通文件、路径替换、身份漂移、损坏 JSON 和超限内容;`actionId` 必须先通过稳定格式校验,不能直接成为任意路径片段。通用文件工具、仓库索引、checkpoint、diff 和 startup context 继续排除整个 `.agent/runtime/**`
- `command.output_read` 只允许当前精确 `agentId` 读取属于同一 Agent 的 terminal `command.exec`。Runtime 先按 `agentId + actionId` 在 terminal receipt 中定位唯一源 run,再把 sidecar 与该 run 的 task ledger、terminal receipt / observation identity 交叉绑定,并复核 task、session、action fingerprint、tool 和终态;缺失、跨 Agent、重复冲突、未终态、旧格式或身份冲突全部失败关闭,不通过扫描目录猜测历史。
- 工具能力名为 `command-output-read`,模型工具与 command id 都固定为 `command.output_read`,权限默认 `auto`,可由项目或 per-Agent policy 改为 `confirm / deny`。它是 durable 只读 action,不推进 project revision、不改变 verification gate、不认领 join;适用仓库规范 fingerprint 仍要复核。confirm 指纹覆盖源 actionId 与分页参数。
- transcript 正文只进入本轮模型可见 observation 与受限 context bundle,不写入 task/event 投影、Agent DB 普通审计、terminal receipt、`agent.action_history`、command log 之外的新日志或最终验收报告。上述长期记录只保存源 actionId、output ref、SHA-256、总行数、页范围、截断和命令终态元数据;写入 observation 前再次执行凭据和绝对路径清洗,避免旧 sidecar 或未来清洗规则变化重新扩散敏感内容。
- `command.exec` 的 terminal receipt 和专用审计增加不含正文的 `outputRef / outputSha256 / totalLines / captureTruncated`。sidecar 必须在命令日志、manifest、Agent DB 专用审计和 terminal observation 宣告成功前持久化;命令一旦已经启动,sidecar、后续审计或 receipt 任一步失败,都先保持当前 revision 的 failed verification gate,再把原 action 置为 `needs-reconciliation`,不得重新执行命令。恢复只能按原 action identity 补齐可证明幂等的投影;无法证明 sidecar 完整时保持 reconciliation。
- `command.output_read` 已有 terminal observation / receipt 时,恢复只把已持久化的 context observation 交给下一轮 planning并补齐缺失 receipt,不重复读取生成另一份 observation,不执行源命令,也不调用 Provider 以外的额外副作用。分页读取允许不同 actionId 对同一源 command 读取不同页;相同 actionId 的输入或结果身份冲突继续失败关闭。
- Runtime prompt 明确要求:短命令摘要不足以定位失败时先调用 `command.output_read`,按 `nextLine` 继续分页,找到根错误后再修改;不得仅凭输出尾部猜测。真实 Provider E2E 必须把唯一根错误放在普通 900 字符 observation 之外,并且不提供文件名、脚本名、目标字符串或工具顺序,证明 Agent 自行执行命令、分页找到错误、修改、复验和唯一收束。
- 无配方真实运行还要求 Agent 能自行构造隔离评审:Runtime prompt 必须列出合法静态模板 taskId,并明确 `expectedArtifacts` 只能填写完成时必须存在的项目内相对文件或 glob,只读评审填写被检查的现有文件;`writeScopes` 必须是互不重叠的项目内非私有目录 glob。相同父 Agent/run 下完全相同的 spawn request 只能创建一组实例,新 actionId 重复请求必须在创建前拒绝。
- 隔离子 Agent 已进入终态但结果构造因 expected artifact、evidence 或其他结果契约失败时,必须落一条结构化 failed child result 并参与 all-join,不能只写失败审计后让父 run 永久等待。真实 Provider E2E 的验收器只把实际成功或确实启动过的动作计为副作用;预检失败不伪装成副作用重放。可重复只读审阅不限制固定次数,省略默认参数与显式默认值视为等价,独立证据只要求都早于最终回复,不强制无业务依赖的调用顺序。
确定性测试必须覆盖分页与 1-based 边界、Unicode 行、头中尾 marker、capture truncation、二次清洗、正文零进入 task/event/Agent DB/receipt、跨 Agent/run/actionId 拒绝、auto/confirm/deny、sidecar 符号链接/硬链接/损坏/超限、命令后 sidecar 写失败进入 `needs-reconciliation` 且执行计数仍为 1,以及强杀恢复不重复命令或 Provider。
### 2026-07-13 V1.8 真实验收结果
发布 AppData 中配置的真实 `gpt-5.5` 已通过无固定配方的 `llm-runtime` 套件。任务没有提供文件名、脚本名、目标字符串、marker、actionId 或工具顺序;模型自行运行失败命令,通过持久动作记录取得真实 actionId,用 2 次 `command.output_read` 覆盖 248 行 transcript 并命中普通短 observation 之外的唯一根错误,随后只执行 1 次 `project.patchset` 完成 2 项原子变更,再完成内容 diff、修改前后 Git 审阅、失败/成功各 1 次 `command.exec``project.verify`、双视口浏览器验证、双图视觉检查、3 个隔离 reviewer 和 patchset 历史回查。
最终形成 122 条 task、210 条 event、213 条 Agent DB、13 条合法工具协议、15 次代表性成功工具执行、6 套确认生命周期、8 个实际副作用 action、32 条 terminal receipt(主 run 23 条),project revision 为 3。命令 sidecar 和私有 context 各命中根错误,task/event/Agent DB/receipt/report 中根错误正文泄漏均为 0;副作用重放、重复 action/message/receipt、半完成文件、图片载荷、已加载密钥和项目诱饵泄漏均为 0。Runner 强杀后恢复原 run/session 且身份稳定,3 个隔离实例来自 2 个模板并形成唯一 continuation delivery,最终 completed 投影、assistant audit 和 assistant 消息均仅 1 条;保留现场核对后已按 disposable sentinel 清理。
## 验收命令
- `npm run ai-game-creator-shell:typecheck`
@@ -22,7 +22,7 @@
同一文档的“V1.3 多文件变更集与内容审查”作为复杂代码修改的新事实源。`project.patchset` 在一个确认动作和一把项目锁内预检最多 12 个 create / update / delete,自动 checkpoint、只推进一次 revision,并以 SHA-256 乐观并发条件和回滚语义避免半完成修改;`project.diff(includeContent=true)` 返回有界统一 diff hunks。它不开放任意 `git apply` 文本,也不替代修改后的可执行验证。
同一文档的 V1.4-V1.7 继续作为当前事实源:V1.4 用只读 `git.inspect` 提供有界工作树状态和安全 hunksV1.5 用跨 context window 的 milestones 保留已完成副作用与验证证据;V1.6 用 terminal receipt 和 `agent.action_history` 提供可恢复动作回查,并对未认领 all-join 的最终回复与动作历史设置双重完成门禁;V1.7 用 `image.inspect` 把 desktop / mobile 截图作为受控多模态输入交给当前 Agent 自己的 Provider,并严格禁止图片载荷持久化。历史能力清单与这些版本冲突时,以 Runtime V1.1 技术方案和当前代码为准。
同一文档的 V1.4-V1.8 继续作为当前事实源:V1.4 用只读 `git.inspect` 提供有界工作树状态和安全 hunksV1.5 用跨 context window 的 milestones 保留已完成副作用与验证证据;V1.6 用 terminal receipt 和 `agent.action_history` 提供可恢复动作回查,并对未认领 all-join 的最终回复与动作历史设置双重完成门禁;V1.7 用 `image.inspect` 把 desktop / mobile 截图作为受控多模态输入交给当前 Agent 自己的 Provider,并严格禁止图片载荷持久化V1.8 用 `command.output_read` 按 actionId 分页读取同一 Agent 当前或历史 run 的安全命令 transcript,正文只进入私有 context observation,不进入 task/event/Agent DB/receipt。历史能力清单与这些版本冲突时,以 Runtime V1.1 技术方案和当前代码为准。
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)`,不得记为通过。
@@ -30,7 +30,11 @@
2026-07-13 V1.6 最终真实验收:`llm-runtime` 形成 94 条 task、158 条 event、164 条 Agent DB、11 条合法工具协议、13 次成功工具执行和 24 条 terminal receipt;主 run receipt 为 18。`agent.action_history` 实际调用 1 次、返回 1 条、递归结果 0,且只在父 run 认领唯一 all-join 后执行;Runner 强杀恢复、revision 3、3 个隔离实例 / 2 个模板、双视口浏览器证据、重复项、身份冲突、半完成文件、密钥和诱饵泄漏均通过结构化检查。本次竞态走活跃父 run 直接认领路径,join continuation 数量为 0`parent-wake` 等待路径由确定性 Rust 测试覆盖。这些数字是单次观测结果,不是脚本固定阈值;`full` 套件仍需 External Editor API,缺失时保持 `BLOCKED(editorApi)`
2026-07-13 V1.7 最终真实验收:`llm-runtime` 形成 95 条 task、161 条 event、166 条 Agent DB、12 条合法工具协议、14 次成功工具执行和 24 条 terminal receipt。真实 Provider 在 `preview.validate` 后实际调用 `image.inspect` 1 次并读取 desktop / mobile 两张 PNG;专用 audit 与 receipt 各 1 条、responseId 存在,视觉 observation 在 `agent.action_history` 和最终回复前落盘。Runner 强杀恢复保持原 run / sessionrevision 3,3 个隔离实例、项目验证和浏览器验证通过;图片载荷、重复 action / message / receipt、密钥和诱饵泄漏均为 0。`full` 套件仍需 External Editor API,缺失时保持 `BLOCKED(editorApi)`
2026-07-13 V1.7 最终真实验收:`llm-runtime` 形成 95 条 task、161 条 event、166 条 Agent DB、12 条合法工具协议、14 次成功工具执行和 24 条 terminal receipt。真实 Provider 在 `preview.validate` 后实际调用 `image.inspect` 1 次并读取 desktop / mobile 两张 PNG;专用 audit 与 receipt 各 1 条、responseId 存在,视觉 observation 在最终回复前落盘。`agent.action_history` 与视觉检查彼此独立,不要求固定先后顺序。Runner 强杀恢复保持原 run / sessionrevision 3,3 个隔离实例、项目验证和浏览器验证通过;图片载荷、重复 action / message / receipt、密钥和诱饵泄漏均为 0。`full` 套件仍需 External Editor API,缺失时保持 `BLOCKED(editorApi)`
2026-07-13 V1.8 实现口径:`command.exec` 的清洗后有界输出在命令日志和 manifest 投影前写入 `.agent/runtime/command-outputs/<identitySha256>.json`,并由 terminal receipt 只记录 outputRef、SHA-256、行数、截断和终态元数据。`command.output_read` 默认 `auto`,输入只接受源 actionId 和分页参数;Runtime 从 terminal receipt 反查唯一源 run,交叉复核 task ledger、command audit 和 sidecar 身份。读取不推进 revision 或 verification gate,模板级 per-Agent policy 对动态 `child-*` 实例继续生效;同页恢复只续 planning,不重跑源命令。
2026-07-13 V1.8 最终真实验收:无固定配方的真实 `gpt-5.5` `llm-runtime` 已 PASS。模型自行取得失败命令 actionId,用 2 页覆盖 248 行输出并定位短 observation 之外的根错误,再以唯一 patchset 完成 2 项变更;122 条 task、210 条 event、213 条 Agent DB、13 条工具协议、15 次代表性成功工具执行、6 套确认和 32 条 receipt 中,副作用重放、重复 action/message/receipt、命令正文跨边界泄漏、图片载荷、密钥和诱饵泄漏均为 0。Runner 强杀恢复、revision 3、3 个隔离实例 / 2 个模板、唯一 continuation delivery、项目验证和双视口视觉证据全部通过。真实运行同时收紧了合法模板与 artifact/write scope 提示、失败 child result 的 all-join 终态降级、同父 run 相同 spawn request 去重,以及结果导向验收的等价默认输入和独立证据顺序。
以下能力清单保留 Runtime V1 的演进记录;其中“App 进程内 tokio task”“跨进程同项目写入不作为支持目标”和“恢复到当前 App 进程”的旧描述均已由 V1.1 替代。当前边界是 App / CLI 只落账并唤醒同一发布二进制的独立 Runnerappend-only JSONL 使用进程内锁加 OS 文件锁,恢复继续由 Runner 接管同一 run / session。
@@ -19,13 +19,17 @@ describe('AI 游戏创作 App 共享契约', () => {
it('keeps command permissions explicit', () => {
const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(55);
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(56);
expect(commandIds).toContain('project.git_inspect');
expect(commandIds).toContain('project.patchset');
expect(commandIds).toContain('command.exec');
expect(commandIds).toContain('command.output_read');
expect(commandIds.indexOf('command.exec')).toBe(
commandIds.indexOf('command.run_limited') + 1,
);
expect(commandIds.indexOf('command.output_read')).toBe(
commandIds.indexOf('command.exec') + 1,
);
expect(
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'help.show')
?.permission,
@@ -40,6 +44,11 @@ describe('AI 游戏创作 App 共享契约', () => {
(command) => command.id === 'command.exec',
)?.permission,
).toBe('confirm');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'command.output_read',
)?.permission,
).toBe('auto');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'project.git_inspect',
@@ -191,7 +200,7 @@ describe('AI 游戏创作 App 共享契约', () => {
(capability) => capability.id,
);
expect(GAME_CREATION_AGENT_CAPABILITIES).toHaveLength(33);
expect(GAME_CREATION_AGENT_CAPABILITIES).toHaveLength(34);
expect(capabilityIds).toEqual(
expect.arrayContaining([
'chat',
@@ -218,6 +227,7 @@ describe('AI 游戏创作 App 共享契约', () => {
'local-preview',
'developer-window',
'command-exec',
'command-output-read',
]),
);
expect(
@@ -230,6 +240,15 @@ describe('AI 游戏创作 App 共享契约', () => {
title:
'受控命令执行(固定 program + argv、非 shell、项目内 cwd、有界输出)',
});
expect(
GAME_CREATION_AGENT_CAPABILITIES.find(
(capability) => capability.id === 'command-output-read',
),
).toEqual({
id: 'command-output-read',
area: 'dev-runtime',
title: '命令输出分页回查',
});
expect(
GAME_CREATION_AGENT_CAPABILITIES.find(
(capability) => capability.id === 'visual-inspection',
@@ -58,6 +58,7 @@ export const GAME_CREATION_APP_COMMANDS = [
{ id: 'preview.status', permission: 'auto' },
{ id: 'command.run_limited', permission: 'confirm' },
{ id: 'command.exec', permission: 'confirm' },
{ id: 'command.output_read', permission: 'auto' },
{ id: 'canvas.project_open', permission: 'confirm' },
{ id: 'canvas.project_sync', permission: 'confirm' },
{ id: 'canvas.asset_import', permission: 'confirm' },
@@ -164,6 +165,11 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
title:
'受控命令执行(固定 program + argv、非 shell、项目内 cwd、有界输出)',
},
{
id: 'command-output-read',
area: 'dev-runtime',
title: '命令输出分页回查',
},
{ id: 'guardrails', area: 'dev-runtime', title: '权限 Gate' },
{ id: 'project-policy', area: 'dev-runtime', title: '项目级权限策略' },
{ id: 'trace-log', area: 'dev-runtime', title: '执行日志' },
@@ -21,7 +21,7 @@ pub struct GameCreationAppCommandDescriptor {
pub permission: GameCreationAppPermission,
}
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 55] = [
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 56] = [
command("help.show", GameCreationAppPermission::Auto),
command("project.create", GameCreationAppPermission::Confirm),
command("project.status", GameCreationAppPermission::Auto),
@@ -67,6 +67,7 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 55] = [
command("preview.status", GameCreationAppPermission::Auto),
command("command.run_limited", GameCreationAppPermission::Confirm),
command("command.exec", GameCreationAppPermission::Confirm),
command("command.output_read", GameCreationAppPermission::Auto),
command("canvas.project_open", GameCreationAppPermission::Confirm),
command("canvas.project_sync", GameCreationAppPermission::Confirm),
command("canvas.asset_import", GameCreationAppPermission::Confirm),
@@ -94,7 +95,7 @@ pub struct GameCreationAgentCapabilityDescriptor {
pub title: &'static str,
}
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 33] = [
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 34] = [
capability("chat", "user", "聊天入口"),
capability("file-upload", "user", "上传文件"),
capability("built-in-commands", "agent-runtime", "内置命令调用"),
@@ -145,6 +146,11 @@ pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescript
"dev-runtime",
"受控命令执行(固定 program + argv、非 shell、项目内 cwd、有界输出)",
),
capability(
"command-output-read",
"dev-runtime",
"命令输出分页回查",
),
capability("guardrails", "dev-runtime", "权限 Gate"),
capability("project-policy", "dev-runtime", "项目级权限策略"),
capability("trace-log", "dev-runtime", "执行日志"),
@@ -641,7 +647,7 @@ mod tests {
#[test]
fn command_contract_keeps_expected_permissions() {
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 55);
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 56);
let command_ids = GAME_CREATION_APP_COMMANDS
.iter()
@@ -655,7 +661,12 @@ mod tests {
.iter()
.position(|command_id| *command_id == "command.exec")
.expect("command.exec should exist");
let output_read_index = command_ids
.iter()
.position(|command_id| *command_id == "command.output_read")
.expect("command.output_read should exist");
assert_eq!(exec_index, limited_index + 1);
assert_eq!(output_read_index, exec_index + 1);
let help = GAME_CREATION_APP_COMMANDS
.iter()
@@ -675,6 +686,15 @@ mod tests {
.expect("command.exec should exist");
assert_eq!(command_exec.permission, GameCreationAppPermission::Confirm);
let command_output_read = GAME_CREATION_APP_COMMANDS
.iter()
.find(|command| command.id == "command.output_read")
.expect("command.output_read should exist");
assert_eq!(
command_output_read.permission,
GameCreationAppPermission::Auto
);
let project_patchset = GAME_CREATION_APP_COMMANDS
.iter()
.find(|command| command.id == "project.patchset")
@@ -905,7 +925,7 @@ mod tests {
#[test]
fn capabilities_cover_standard_agent_runtime_needs() {
assert_eq!(GAME_CREATION_AGENT_CAPABILITIES.len(), 33);
assert_eq!(GAME_CREATION_AGENT_CAPABILITIES.len(), 34);
let ids = GAME_CREATION_AGENT_CAPABILITIES
.iter()
@@ -933,6 +953,7 @@ mod tests {
"visual-inspection",
"developer-window",
"command-exec",
"command-output-read",
] {
assert!(ids.contains(&expected), "missing {expected}");
}
@@ -945,6 +966,12 @@ mod tests {
command_exec.title,
"受控命令执行(固定 program + argv、非 shell、项目内 cwd、有界输出)"
);
let command_output_read = GAME_CREATION_AGENT_CAPABILITIES
.iter()
.find(|capability| capability.id == "command-output-read")
.expect("command-output-read capability should exist");
assert_eq!(command_output_read.area, "dev-runtime");
assert_eq!(command_output_read.title, "命令输出分页回查");
let visual_inspection = GAME_CREATION_AGENT_CAPABILITIES
.iter()
.find(|capability| capability.id == "visual-inspection")