新增命令输出分页回查能力
新增 command.output_read 并按当前 Agent 的持久动作身份分页读取命令输出 为 command.exec 写入受限 transcript sidecar 并保持审计与回执零正文 完善隔离 Agent 模板提示、失败结果收束和重复 spawn 去重 将真实 Provider E2E 改为无固定配方的结果导向验收 同步共享命令与能力契约并补齐 Rust 和 TypeScript 测试 更新 Runtime 技术方案、实施计划和共享决策记录
This commit is contained in:
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(§ions.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(§ions.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 artifact:game/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 bundle;task/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` PASS:122 条 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 receipt;Runner 强杀后恢复原 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` 提供有界工作树状态和安全 hunks;V1.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` 提供有界工作树状态和安全 hunks;V1.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-join;95 条 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 / session,revision 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 / session,revision 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 只落账并唤醒同一发布二进制的独立 Runner,append-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")
|
||||
|
||||
Reference in New Issue
Block a user