Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f829cd252 | |||
| c146f7f99c | |||
| 818cfdba50 |
@@ -84,6 +84,7 @@ function resolveBackendTargetsFromState(
|
|||||||
requireAgcBackend = false,
|
requireAgcBackend = false,
|
||||||
expectedDatabase = backendDatabase,
|
expectedDatabase = backendDatabase,
|
||||||
expectedSpacetimeDataDir = backendSpacetimeDataDir,
|
expectedSpacetimeDataDir = backendSpacetimeDataDir,
|
||||||
|
expectedRepoRoot = repoRoot,
|
||||||
fallbackApiTarget = defaultApiTarget,
|
fallbackApiTarget = defaultApiTarget,
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
@@ -101,7 +102,25 @@ function resolveBackendTargetsFromState(
|
|||||||
const hasMatchingDataDir =
|
const hasMatchingDataDir =
|
||||||
Boolean(spacetimeDataDir) &&
|
Boolean(spacetimeDataDir) &&
|
||||||
spacetimeDataDir === resolve(expectedSpacetimeDataDir);
|
spacetimeDataDir === resolve(expectedSpacetimeDataDir);
|
||||||
const hasMatchingBackend = hasMatchingDatabase && hasMatchingDataDir;
|
const instanceId =
|
||||||
|
typeof state?.instanceId === 'string' ? state.instanceId.trim() : '';
|
||||||
|
const hasMatchingRepoRoot =
|
||||||
|
typeof state?.repoRoot === 'string' &&
|
||||||
|
resolve(state.repoRoot) === resolve(expectedRepoRoot);
|
||||||
|
const hasMatchingInstance =
|
||||||
|
Boolean(instanceId) &&
|
||||||
|
[apiServer, spacetime, bgfilterWorker]
|
||||||
|
.filter(Boolean)
|
||||||
|
.every(
|
||||||
|
(service) =>
|
||||||
|
service.repoRoot &&
|
||||||
|
resolve(service.repoRoot) === resolve(expectedRepoRoot) &&
|
||||||
|
service.instanceId === instanceId,
|
||||||
|
);
|
||||||
|
const hasMatchingBackend =
|
||||||
|
hasMatchingDatabase &&
|
||||||
|
hasMatchingDataDir &&
|
||||||
|
(!requireAgcBackend || (hasMatchingRepoRoot && hasMatchingInstance));
|
||||||
const canReuseState = !requireAgcBackend || hasMatchingBackend;
|
const canReuseState = !requireAgcBackend || hasMatchingBackend;
|
||||||
const apiUrl =
|
const apiUrl =
|
||||||
canReuseState && isActive(apiServer) && apiServer.url
|
canReuseState && isActive(apiServer) && apiServer.url
|
||||||
@@ -127,6 +146,8 @@ function resolveBackendTargetsFromState(
|
|||||||
spacetimeDataDir,
|
spacetimeDataDir,
|
||||||
hasMatchingDatabase,
|
hasMatchingDatabase,
|
||||||
hasMatchingDataDir,
|
hasMatchingDataDir,
|
||||||
|
hasMatchingRepoRoot,
|
||||||
|
hasMatchingInstance,
|
||||||
hasMatchingBackend,
|
hasMatchingBackend,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
处理代码任务时先用 project.search 定位,再用带行号的 file.read 获取足够上下文;单文件小改优先使用 file.patch;涉及多个文件时优先使用 project.patchset,它会自动创建 checkpoint,无需额外调用 project.checkpoint,并在成功后用返回的 checkpointId 调用 project.diff(includeContent=true) 审查整体变更;只有确认文件已废弃时才删除。
|
处理代码任务时先用 project.search 定位,再用带行号的 file.read 获取足够上下文;单文件小改优先使用 file.patch;涉及多个文件时优先使用 project.patchset,它会自动创建 checkpoint,无需额外调用 project.checkpoint,并在成功后用返回的 checkpointId 调用 project.diff(includeContent=true) 审查整体变更;只有确认文件已废弃时才删除。
|
||||||
|
|
||||||
每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec 或 command.start,都会产生新的项目 revision;最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。需要执行 package.json 中的验证脚本时,先读取 package.json,再把真实脚本名和读到的完整命令原样提交给 project.verify;script 可以是 check、typecheck、test、lint、build,或使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本,其中冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点;不得猜测或改写 expectedCommand。
|
每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec、command.start 或 project.bootstrap,都会产生新的项目 revision;DirectProject 的 npm/Phaser 工程先用 project.bootstrap {cwd:"game"} 执行受控无参数 npm install,再用 project.verify {cwd:"game",script:"build",expectedCommand:从 game/package.json 原样读取} 构建,并确认 game/dist/index.html 存在。最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。需要执行 package.json 中的验证脚本时,先读取 package.json,再把真实脚本名和读到的完整命令原样提交给 project.verify;script 可以是 check、typecheck、test、lint、build,或使用 check:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本,其中冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点;不得猜测或改写 expectedCommand。
|
||||||
|
|
||||||
每 6 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。
|
每 6 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。
|
||||||
|
|
||||||
|
|||||||
@@ -2829,8 +2829,6 @@ impl CodexAppServerConnection {
|
|||||||
callback(&platform_llm::LlmStreamDelta {
|
callback(&platform_llm::LlmStreamDelta {
|
||||||
accumulated_text: streamed_text.clone(),
|
accumulated_text: streamed_text.clone(),
|
||||||
delta_text: delta,
|
delta_text: delta,
|
||||||
accumulated_reasoning: String::new(),
|
|
||||||
reasoning_delta: String::new(),
|
|
||||||
finish_reason: None,
|
finish_reason: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3135,7 +3133,6 @@ fn parse_game_creator_codex_app_server_text(
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
},
|
},
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: Some(thread_id.to_string()),
|
response_id: Some(thread_id.to_string()),
|
||||||
usage: None,
|
usage: None,
|
||||||
|
|||||||
@@ -589,7 +589,6 @@ fn parse_game_creator_codex_cli_response(
|
|||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
},
|
},
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id,
|
response_id,
|
||||||
usage,
|
usage,
|
||||||
|
|||||||
@@ -104,17 +104,6 @@ fn design_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn design_reasoning_event(
|
|
||||||
root: &Path,
|
|
||||||
turn_id: &str,
|
|
||||||
id: Option<&str>,
|
|
||||||
reasoning: String,
|
|
||||||
) -> DesignEvent {
|
|
||||||
let mut event = design_event(root, turn_id, "reasoning", id, None, None);
|
|
||||||
event.reasoning_text = Some(reasoning);
|
|
||||||
event
|
|
||||||
}
|
|
||||||
|
|
||||||
fn design_project_id(root: &Path) -> Result<String, String> {
|
fn design_project_id(root: &Path) -> Result<String, String> {
|
||||||
validate_project_root(root)?;
|
validate_project_root(root)?;
|
||||||
Ok(read_existing_manifest_for_project(root)?.project_id)
|
Ok(read_existing_manifest_for_project(root)?.project_id)
|
||||||
@@ -473,7 +462,6 @@ fn build_design_request(
|
|||||||
.with_tool_choice(platform_llm::LlmToolChoice::Auto)
|
.with_tool_choice(platform_llm::LlmToolChoice::Auto)
|
||||||
.with_web_search(false);
|
.with_web_search(false);
|
||||||
apply_game_creator_llm_reasoning_effort(request, llm)
|
apply_game_creator_llm_reasoning_effort(request, llm)
|
||||||
.map(|request| request.with_reasoning_capture(true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调试队列只接收副本,写盘慢或失败时丢弃,不参与会话恢复。
|
// 调试队列只接收副本,写盘慢或失败时丢弃,不参与会话恢复。
|
||||||
@@ -560,12 +548,6 @@ async fn request_design_provider(
|
|||||||
Some(String::new()),
|
Some(String::new()),
|
||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
String::new(),
|
|
||||||
));
|
|
||||||
let result = if llm.stream {
|
let result = if llm.stream {
|
||||||
let mut stream_sequence = 0_u64;
|
let mut stream_sequence = 0_u64;
|
||||||
client
|
client
|
||||||
@@ -583,30 +565,18 @@ async fn request_design_provider(
|
|||||||
"model": llm.model,
|
"model": llm.model,
|
||||||
"deltaChars": delta.delta_text.chars().count(),
|
"deltaChars": delta.delta_text.chars().count(),
|
||||||
"accumulatedChars": delta.accumulated_text.chars().count(),
|
"accumulatedChars": delta.accumulated_text.chars().count(),
|
||||||
"reasoningDeltaChars": delta.reasoning_delta.chars().count(),
|
|
||||||
"reasoningAccumulatedChars": delta.accumulated_reasoning.chars().count(),
|
|
||||||
"deltaText": delta.delta_text,
|
"deltaText": delta.delta_text,
|
||||||
"finishReason": delta.finish_reason,
|
"finishReason": delta.finish_reason,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
if !delta.delta_text.is_empty() || delta.finish_reason.is_some() {
|
emit(design_event(
|
||||||
emit(design_event(
|
root,
|
||||||
root,
|
&turn_id,
|
||||||
&turn_id,
|
"text",
|
||||||
"text",
|
Some(&message_id),
|
||||||
Some(&message_id),
|
Some(delta.accumulated_text.clone()),
|
||||||
Some(delta.accumulated_text.clone()),
|
None,
|
||||||
None,
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
if !delta.reasoning_delta.is_empty() {
|
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
delta.accumulated_reasoning.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
@@ -614,14 +584,6 @@ async fn request_design_provider(
|
|||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if !response.reasoning.is_empty() {
|
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
response.reasoning.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
design_debug(
|
design_debug(
|
||||||
root,
|
root,
|
||||||
"response",
|
"response",
|
||||||
@@ -644,12 +606,6 @@ async fn request_design_provider(
|
|||||||
|| game_creator_agent_runtime_transient_provider_error_kind(&error, false)
|
|| game_creator_agent_runtime_transient_provider_error_kind(&error, false)
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
String::new(),
|
|
||||||
));
|
|
||||||
return Err(detail);
|
return Err(detail);
|
||||||
}
|
}
|
||||||
tokio::time::sleep(Duration::from_millis(
|
tokio::time::sleep(Duration::from_millis(
|
||||||
@@ -698,24 +654,8 @@ async fn request_scripted_design_provider(
|
|||||||
Some(String::new()),
|
Some(String::new()),
|
||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
String::new(),
|
|
||||||
));
|
|
||||||
match fake_provider::take() {
|
match fake_provider::take() {
|
||||||
Some(Ok(response)) => {
|
Some(Ok(response)) => return Ok(response),
|
||||||
if !response.reasoning.is_empty() {
|
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
response.reasoning.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
Some(Err(error)) => {
|
Some(Err(error)) => {
|
||||||
let detail = redact_agent_runtime_error(
|
let detail = redact_agent_runtime_error(
|
||||||
root,
|
root,
|
||||||
@@ -726,24 +666,10 @@ async fn request_scripted_design_provider(
|
|||||||
|| game_creator_agent_runtime_transient_provider_error_kind(&error, false)
|
|| game_creator_agent_runtime_transient_provider_error_kind(&error, false)
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
String::new(),
|
|
||||||
));
|
|
||||||
return Err(detail);
|
return Err(detail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => return Err("假 Provider 脚本耗尽".into()),
|
||||||
emit(design_reasoning_event(
|
|
||||||
root,
|
|
||||||
&turn_id,
|
|
||||||
Some(&message_id),
|
|
||||||
String::new(),
|
|
||||||
));
|
|
||||||
return Err("假 Provider 脚本耗尽".into());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unreachable!()
|
unreachable!()
|
||||||
@@ -1359,7 +1285,6 @@ mod tests {
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: "fake-design".into(),
|
model: "fake-design".into(),
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some(if calls.is_empty() {
|
finish_reason: Some(if calls.is_empty() {
|
||||||
"stop".into()
|
"stop".into()
|
||||||
} else {
|
} else {
|
||||||
@@ -1420,69 +1345,6 @@ mod tests {
|
|||||||
.clone()
|
.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn design_request_enables_reasoning_capture_only_for_design_runtime() {
|
|
||||||
let session = new_design_session("project", "quality");
|
|
||||||
let request = build_design_request(&session, &pack(), &GameCreatorLlmConfig::default())
|
|
||||||
.expect("design request");
|
|
||||||
assert!(request.capture_reasoning);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
|
||||||
async fn scripted_design_provider_emits_reasoning_without_persisting_it() {
|
|
||||||
let (_temp, root, _resources) = init_design_project();
|
|
||||||
let mut session = new_design_session("design-fake", "quality");
|
|
||||||
begin_design_turn(&mut session, "turn-reasoning");
|
|
||||||
let mut response = fake_response("reasoning", "正文", Vec::new());
|
|
||||||
response.reasoning = "先分析需求,再组织方案。".into();
|
|
||||||
let _fake = fake_provider::install(vec![Ok(response)], 0);
|
|
||||||
let mut events = Vec::new();
|
|
||||||
let response =
|
|
||||||
request_scripted_design_provider(&root, &mut session, &mut |event| events.push(event))
|
|
||||||
.await
|
|
||||||
.expect("scripted provider");
|
|
||||||
|
|
||||||
let reasoning_events = events
|
|
||||||
.iter()
|
|
||||||
.filter_map(|event| event.reasoning_text.as_deref())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(reasoning_events, vec!["", "先分析需求,再组织方案。"]);
|
|
||||||
assert_eq!(response.text, "正文");
|
|
||||||
assert!(session.history.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
|
||||||
async fn scripted_design_provider_retry_clears_previous_reasoning_attempt() {
|
|
||||||
let (_temp, root, _resources) = init_design_project();
|
|
||||||
let mut session = new_design_session("design-fake", "quality");
|
|
||||||
begin_design_turn(&mut session, "turn-reasoning-retry");
|
|
||||||
let mut response = fake_response("reasoning-retry", "重试后的正文", Vec::new());
|
|
||||||
response.reasoning = "重试后的推理".into();
|
|
||||||
let _fake = fake_provider::install(
|
|
||||||
vec![
|
|
||||||
Err(platform_llm::LlmError::Upstream {
|
|
||||||
status_code: 503,
|
|
||||||
message: "busy".into(),
|
|
||||||
}),
|
|
||||||
Ok(response),
|
|
||||||
],
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
let mut events = Vec::new();
|
|
||||||
let response =
|
|
||||||
request_scripted_design_provider(&root, &mut session, &mut |event| events.push(event))
|
|
||||||
.await
|
|
||||||
.expect("scripted retry provider");
|
|
||||||
|
|
||||||
let reasoning_events = events
|
|
||||||
.iter()
|
|
||||||
.filter_map(|event| event.reasoning_text.as_deref())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert_eq!(reasoning_events, vec!["", "", "重试后的推理"]);
|
|
||||||
assert_eq!(response.text, "重试后的正文");
|
|
||||||
assert!(session.history.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn fake_provider_walks_five_phases_and_enters_consultant() {
|
async fn fake_provider_walks_five_phases_and_enters_consultant() {
|
||||||
let (_temp, root, resources) = init_design_project();
|
let (_temp, root, resources) = init_design_project();
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ JSON schema:
|
|||||||
{ "group": "balance", "role": "Difficulty", "summary": "数值交接摘要", "outputs": ["game/balance.json"], "next": "交给程序组读取" },
|
{ "group": "balance", "role": "Difficulty", "summary": "数值交接摘要", "outputs": ["game/balance.json"], "next": "交给程序组读取" },
|
||||||
{ "group": "art", "role": "Asset", "summary": "美术交接摘要", "outputs": ["assets/manifest.art.json"], "next": "进入画板或本地资产登记" },
|
{ "group": "art", "role": "Asset", "summary": "美术交接摘要", "outputs": ["assets/manifest.art.json"], "next": "进入画板或本地资产登记" },
|
||||||
{ "group": "audio", "role": "SFX", "summary": "音乐音效交接摘要", "outputs": ["assets/manifest.audio.json"], "next": "进入画板音频链路" },
|
{ "group": "audio", "role": "SFX", "summary": "音乐音效交接摘要", "outputs": ["assets/manifest.audio.json"], "next": "进入画板音频链路" },
|
||||||
{ "group": "code", "role": "Code", "summary": "程序交接摘要", "outputs": ["game/index.html"], "next": "交给 Playtest" },
|
{ "group": "code", "role": "Code", "summary": "程序交接摘要", "outputs": ["game/index.html", "game/game.js", "game/package.json", "game/vite.config.js"], "next": "先 project.bootstrap,再 project.verify(build),交给 Playtest" },
|
||||||
{ "group": "publishing", "role": "Publish", "summary": "运营交接摘要", "outputs": ["exports/README.md"], "next": "等待预览验收" }
|
{ "group": "publishing", "role": "Publish", "summary": "运营交接摘要", "outputs": ["exports/README.md"], "next": "等待预览验收" }
|
||||||
],
|
],
|
||||||
"handoffSummary": "六组 agent 的交接摘要,每组一行",
|
"handoffSummary": "六组 agent 的交接摘要,每组一行",
|
||||||
@@ -24,6 +24,7 @@ JSON schema:
|
|||||||
}
|
}
|
||||||
|
|
||||||
gameHtml 规则:
|
gameHtml 规则:
|
||||||
|
- 本次若目标是 Phaser/npm,workspaceMode 必须为 DirectProject;先写入完整 game/ 工程文件,再由受控项目工具安装与构建。
|
||||||
- 此 JSON 协议仅用于已有的单文件 HTML 项目;npm / Phaser 项目必须使用 DirectProject,不能通过 gameHtml 交付 package.json 或模块源码。
|
- 此 JSON 协议仅用于已有的单文件 HTML 项目;npm / Phaser 项目必须使用 DirectProject,不能通过 gameHtml 交付 package.json 或模块源码。
|
||||||
- 必须是单文件 HTML,不能加载远程脚本、远程图片、远程 CSS 或 CDN。
|
- 必须是单文件 HTML,不能加载远程脚本、远程图片、远程 CSS 或 CDN。
|
||||||
- 必须包含 canvas、canvas getContext、实际绘制调用、键盘或鼠标输入、requestAnimationFrame 主循环、目标、失败或胜利状态、R 或按钮重开。
|
- 必须包含 canvas、canvas getContext、实际绘制调用、键盘或鼠标输入、requestAnimationFrame 主循环、目标、失败或胜利状态、R 或按钮重开。
|
||||||
|
|||||||
@@ -409,8 +409,6 @@ where
|
|||||||
(self.on_delta)(&platform_llm::LlmStreamDelta {
|
(self.on_delta)(&platform_llm::LlmStreamDelta {
|
||||||
accumulated_text,
|
accumulated_text,
|
||||||
delta_text,
|
delta_text,
|
||||||
accumulated_reasoning: String::new(),
|
|
||||||
reasoning_delta: String::new(),
|
|
||||||
finish_reason,
|
finish_reason,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -501,7 +499,6 @@ mod tests {
|
|||||||
provider: LlmProvider::OpenAiCompatible,
|
provider: LlmProvider::OpenAiCompatible,
|
||||||
model: "interaction-test".to_string(),
|
model: "interaction-test".to_string(),
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: Some("interaction-response".to_string()),
|
response_id: Some("interaction-response".to_string()),
|
||||||
usage: None,
|
usage: None,
|
||||||
|
|||||||
@@ -1192,6 +1192,7 @@ pub(in crate::agent) fn agent_runtime_public_action_input_summary(
|
|||||||
| "project.patchset"
|
| "project.patchset"
|
||||||
| "project.search"
|
| "project.search"
|
||||||
| "project.verify"
|
| "project.verify"
|
||||||
|
| "project.bootstrap"
|
||||||
| "file.list"
|
| "file.list"
|
||||||
| "file.read"
|
| "file.read"
|
||||||
| "file.write"
|
| "file.write"
|
||||||
@@ -1259,6 +1260,7 @@ pub(crate) fn agent_runtime_tool_action_fingerprint(
|
|||||||
| "command.stdin"
|
| "command.stdin"
|
||||||
| "command.terminate"
|
| "command.terminate"
|
||||||
| "project.verify"
|
| "project.verify"
|
||||||
|
| "project.bootstrap"
|
||||||
)
|
)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
let metadata = command_sandbox_platform_metadata();
|
let metadata = command_sandbox_platform_metadata();
|
||||||
@@ -1468,11 +1470,23 @@ pub(crate) fn agent_runtime_tool_action_input_summary(
|
|||||||
.and_then(|value| value.as_bool())
|
.and_then(|value| value.as_bool())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
),
|
),
|
||||||
|
"project.bootstrap" => {
|
||||||
|
format!(
|
||||||
|
"cwd={} · timeoutSeconds={}",
|
||||||
|
relative_path(&["cwd"]),
|
||||||
|
input
|
||||||
|
.get("timeoutSeconds")
|
||||||
|
.or_else(|| input.get("timeout_seconds"))
|
||||||
|
.and_then(|value| value.as_u64())
|
||||||
|
.unwrap_or(300)
|
||||||
|
)
|
||||||
|
}
|
||||||
"project.verify" => {
|
"project.verify" => {
|
||||||
let expected_command = text(&["expectedCommand", "expected_command"]);
|
let expected_command = text(&["expectedCommand", "expected_command"]);
|
||||||
let command_chars = expected_command.chars().count();
|
let command_chars = expected_command.chars().count();
|
||||||
format!(
|
format!(
|
||||||
"script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}",
|
"cwd={} · script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}",
|
||||||
|
relative_path(&["cwd"]),
|
||||||
text(&["script"]),
|
text(&["script"]),
|
||||||
Sha256::digest(expected_command.as_bytes()),
|
Sha256::digest(expected_command.as_bytes()),
|
||||||
command_chars,
|
command_chars,
|
||||||
|
|||||||
@@ -184,6 +184,17 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
"project.bootstrap" => {
|
||||||
|
observe_agent_runtime_project_bootstrap(
|
||||||
|
root,
|
||||||
|
agent_id,
|
||||||
|
run_id,
|
||||||
|
action_id,
|
||||||
|
&action_fingerprint,
|
||||||
|
&action.input,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
"project.checkpoint" => observe_agent_runtime_project_checkpoint(root),
|
"project.checkpoint" => observe_agent_runtime_project_checkpoint(root),
|
||||||
"project.restore" => {
|
"project.restore" => {
|
||||||
observe_agent_runtime_project_restore(root, agent_id, run_id, &action.input)
|
observe_agent_runtime_project_restore(root, agent_id, run_id, &action.input)
|
||||||
|
|||||||
@@ -8,10 +8,15 @@ pub(crate) struct AgentRuntimeAutonomousSourcePayloadStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::agent) fn agent_runtime_autonomous_project_verify_available(root: &Path) -> bool {
|
pub(in crate::agent) fn agent_runtime_autonomous_project_verify_available(root: &Path) -> bool {
|
||||||
let package_path = root.join("package.json");
|
[root.join("package.json"), root.join("game/package.json")]
|
||||||
fs::symlink_metadata(package_path)
|
.into_iter()
|
||||||
.map(|metadata| metadata.file_type().is_file() && !metadata.file_type().is_symlink())
|
.any(|package_path| {
|
||||||
.unwrap_or(false)
|
fs::symlink_metadata(package_path)
|
||||||
|
.map(|metadata| {
|
||||||
|
metadata.file_type().is_file() && !metadata.file_type().is_symlink()
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::agent) fn add_agent_runtime_autonomous_source_field(
|
pub(in crate::agent) fn add_agent_runtime_autonomous_source_field(
|
||||||
|
|||||||
+1
-1
@@ -345,7 +345,7 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
|||||||
let project_tools_contract = if runtime_owner_artifact_validation_available {
|
let project_tools_contract = if runtime_owner_artifact_validation_available {
|
||||||
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;当前固定 owner 的函数目录不广告 project.verify;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;当前固定 owner 的函数目录不广告 project.verify;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
||||||
} else {
|
} else {
|
||||||
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120},只执行项目根 package.json 中同名 npm 脚本,expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
"project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.bootstrap 使用 {\"cwd\":\"game\",\"timeoutSeconds\":300},只执行 game 目录无参数 npm install 并记录 package/lock 指纹;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从对应 cwd 的 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120,\"cwd\":\"game\"},只执行对应 cwd package.json 中同名 npm 脚本,build 必须确认 game/dist/index.html;expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string()
|
||||||
};
|
};
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
concat!(
|
concat!(
|
||||||
|
|||||||
-10
@@ -115,7 +115,6 @@ fn persist_tool_plan_handoff_repair_chain(
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: llm.model.clone(),
|
model: llm.model.clone(),
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -147,8 +146,6 @@ fn stream_delta(delta_text: &str, accumulated_text: &str) -> platform_llm::LlmSt
|
|||||||
platform_llm::LlmStreamDelta {
|
platform_llm::LlmStreamDelta {
|
||||||
accumulated_text: accumulated_text.to_string(),
|
accumulated_text: accumulated_text.to_string(),
|
||||||
delta_text: delta_text.to_string(),
|
delta_text: delta_text.to_string(),
|
||||||
accumulated_reasoning: String::new(),
|
|
||||||
reasoning_delta: String::new(),
|
|
||||||
finish_reason: None,
|
finish_reason: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1006,7 +1003,6 @@ async fn provider_handoff_identity_drift_closes_lifecycle_without_leaking_respon
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: old_llm.model.clone(),
|
model: old_llm.model.clone(),
|
||||||
text: private_response.to_string(),
|
text: private_response.to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -1120,7 +1116,6 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: old_llm.model.clone(),
|
model: old_llm.model.clone(),
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -1307,7 +1302,6 @@ async fn tool_plan_capacity_gate_runs_before_provider_lifecycle_and_network() {
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: llm.model.clone(),
|
model: llm.model.clone(),
|
||||||
text: format!("capacity response {loop_iteration}"),
|
text: format!("capacity response {loop_iteration}"),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -1442,7 +1436,6 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: llm.model.clone(),
|
model: llm.model.clone(),
|
||||||
text: text.to_string(),
|
text: text.to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -1559,7 +1552,6 @@ fn provider_recovery_cleanup_closes_tool_plan_lifecycle_before_removing_handoff(
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: llm.model.clone(),
|
model: llm.model.clone(),
|
||||||
text: "cleanup handoff".to_string(),
|
text: "cleanup handoff".to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -1631,7 +1623,6 @@ fn runtime_resume_scans_and_cleans_terminal_tool_plan_handoff() {
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: llm.model.clone(),
|
model: llm.model.clone(),
|
||||||
text: "terminal handoff".to_string(),
|
text: "terminal handoff".to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
@@ -1727,7 +1718,6 @@ async fn provider_handoff_retry_conflict_preserves_both_sidecars_for_reconciliat
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: llm.model.clone(),
|
model: llm.model.clone(),
|
||||||
text: "已成功但尚未消费的回复".to_string(),
|
text: "已成功但尚未消费的回复".to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: None,
|
response_id: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
|||||||
"project.index",
|
"project.index",
|
||||||
"project.search",
|
"project.search",
|
||||||
"project.verify",
|
"project.verify",
|
||||||
|
"project.bootstrap",
|
||||||
"project.checkpoint",
|
"project.checkpoint",
|
||||||
"project.restore",
|
"project.restore",
|
||||||
"project.diff",
|
"project.diff",
|
||||||
@@ -83,6 +84,7 @@ pub(crate) fn agent_runtime_acceptance_evidence_tools() -> BTreeSet<&'static str
|
|||||||
"project.index",
|
"project.index",
|
||||||
"project.search",
|
"project.search",
|
||||||
"project.verify",
|
"project.verify",
|
||||||
|
"project.bootstrap",
|
||||||
"project.diff",
|
"project.diff",
|
||||||
"git.inspect",
|
"git.inspect",
|
||||||
"project.patchset",
|
"project.patchset",
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS: &[&str] =
|
|||||||
"file.delete",
|
"file.delete",
|
||||||
"project.patchset",
|
"project.patchset",
|
||||||
"project.verify",
|
"project.verify",
|
||||||
|
"project.bootstrap",
|
||||||
"command.run_limited",
|
"command.run_limited",
|
||||||
"preview.validate",
|
"preview.validate",
|
||||||
"canvas.asset_generate",
|
"canvas.asset_generate",
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ pub(crate) fn agent_runtime_tool_requires_repository_context_fingerprint_gate(to
|
|||||||
| "project.index"
|
| "project.index"
|
||||||
| "project.search"
|
| "project.search"
|
||||||
| "project.verify"
|
| "project.verify"
|
||||||
|
| "project.bootstrap"
|
||||||
| "project.checkpoint"
|
| "project.checkpoint"
|
||||||
| "project.patchset"
|
| "project.patchset"
|
||||||
| "project.restore"
|
| "project.restore"
|
||||||
|
|||||||
@@ -798,7 +798,6 @@ mod provider_reconciliation_diagnostic_tests {
|
|||||||
let response = platform_llm::LlmRunResponse {
|
let response = platform_llm::LlmRunResponse {
|
||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: "test-model".to_string(),
|
model: "test-model".to_string(),
|
||||||
reasoning: String::new(),
|
|
||||||
text: "C:\\private\\response".to_string(),
|
text: "C:\\private\\response".to_string(),
|
||||||
finish_reason: Some("completed".to_string()),
|
finish_reason: Some("completed".to_string()),
|
||||||
response_id: Some("response-1".to_string()),
|
response_id: Some("response-1".to_string()),
|
||||||
|
|||||||
@@ -857,6 +857,12 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
|||||||
.and_then(serde_json::Value::as_str)
|
.and_then(serde_json::Value::as_str)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
let cwd = input
|
||||||
|
.get("cwd")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or(".")
|
||||||
|
.to_string();
|
||||||
let timeout_seconds =
|
let timeout_seconds =
|
||||||
agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
|
agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
|
||||||
.unwrap_or(AGENT_RUNTIME_PROJECT_VERIFY_DEFAULT_TIMEOUT_SECONDS);
|
.unwrap_or(AGENT_RUNTIME_PROJECT_VERIFY_DEFAULT_TIMEOUT_SECONDS);
|
||||||
@@ -896,6 +902,7 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
|||||||
script.as_str(),
|
script.as_str(),
|
||||||
expected_command.as_str(),
|
expected_command.as_str(),
|
||||||
timeout_seconds,
|
timeout_seconds,
|
||||||
|
cwd.as_str(),
|
||||||
|| {
|
|| {
|
||||||
verification_state = Some(begin_agent_runtime_project_verification_locked(
|
verification_state = Some(begin_agent_runtime_project_verification_locked(
|
||||||
root,
|
root,
|
||||||
@@ -931,6 +938,7 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
|||||||
"script": verification.script,
|
"script": verification.script,
|
||||||
"expectedCommand": audit_expected_command,
|
"expectedCommand": audit_expected_command,
|
||||||
"packageManager": verification.package_manager,
|
"packageManager": verification.package_manager,
|
||||||
|
"cwd": verification.cwd_relative,
|
||||||
"status": verification.status,
|
"status": verification.status,
|
||||||
"exitCode": verification.exit_code,
|
"exitCode": verification.exit_code,
|
||||||
"timedOut": verification.timed_out,
|
"timedOut": verification.timed_out,
|
||||||
@@ -1039,3 +1047,81 @@ pub(crate) async fn observe_agent_runtime_project_verify(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn observe_agent_runtime_project_bootstrap(
|
||||||
|
root: &Path,
|
||||||
|
agent_id: &str,
|
||||||
|
run_id: &str,
|
||||||
|
action_id: Option<&str>,
|
||||||
|
action_fingerprint: &str,
|
||||||
|
input: &serde_json::Value,
|
||||||
|
) -> AgentRuntimeToolObservation {
|
||||||
|
let cwd = input
|
||||||
|
.get("cwd")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let timeout = agent_runtime_tool_input_usize(input, &["timeoutSeconds", "timeout_seconds"])
|
||||||
|
.unwrap_or(300);
|
||||||
|
if cwd != "game" {
|
||||||
|
return AgentRuntimeToolObservation {
|
||||||
|
tool: "project.bootstrap".to_string(),
|
||||||
|
status: "failed".to_string(),
|
||||||
|
summary: "project.bootstrap 只允许 cwd=game".to_string(),
|
||||||
|
detail: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let _lock = match acquire_project_write_lock(root, "project.bootstrap") {
|
||||||
|
Ok(lock) => lock,
|
||||||
|
Err(error) => {
|
||||||
|
return AgentRuntimeToolObservation {
|
||||||
|
tool: "project.bootstrap".to_string(),
|
||||||
|
status: "failed".to_string(),
|
||||||
|
summary: "project.bootstrap 无法取得项目执行锁".to_string(),
|
||||||
|
detail: Some(redact_agent_runtime_project_paths(root, &error, 240)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = crate::project::run_project_bootstrap_at(root, timeout as u64).await;
|
||||||
|
match result {
|
||||||
|
Ok(value) if value.status == "completed" => {
|
||||||
|
if let Err(error) = append_agent_db_record(
|
||||||
|
root,
|
||||||
|
serde_json::json!({
|
||||||
|
"recordType":"agent.runtime.project.bootstrap", "agentId":agent_id, "runId":run_id,
|
||||||
|
"actionId":action_id, "actionFingerprint":action_fingerprint, "cwd":"game",
|
||||||
|
"packageSha256":value.package_sha256, "lockSha256":value.lock_sha256,
|
||||||
|
"status":value.status, "logPath":value.log_path, "output":value.output
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
return AgentRuntimeToolObservation {
|
||||||
|
tool: "project.bootstrap".to_string(),
|
||||||
|
status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(),
|
||||||
|
summary: "project.bootstrap 已安装,但审计记录无法落盘".to_string(),
|
||||||
|
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
AgentRuntimeToolObservation {
|
||||||
|
tool: "project.bootstrap".to_string(),
|
||||||
|
status: "ok".to_string(),
|
||||||
|
summary: "game 依赖安装已完成".to_string(),
|
||||||
|
detail: Some(format!(
|
||||||
|
"cwd=game · packageSha256={} · lockSha256={}",
|
||||||
|
value.package_sha256,
|
||||||
|
value.lock_sha256.as_deref().unwrap_or("none")
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(value) => AgentRuntimeToolObservation {
|
||||||
|
tool: "project.bootstrap".to_string(),
|
||||||
|
status: "failed".to_string(),
|
||||||
|
summary: "game 依赖安装失败".to_string(),
|
||||||
|
detail: Some(value.output),
|
||||||
|
},
|
||||||
|
Err(error) => AgentRuntimeToolObservation {
|
||||||
|
tool: "project.bootstrap".to_string(),
|
||||||
|
status: "failed".to_string(),
|
||||||
|
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||||
|
detail: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1009,7 +1009,8 @@ fn runtime_tool_description(tool: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
"project.index" => "刷新并读取有界仓库启动上下文。",
|
"project.index" => "刷新并读取有界仓库启动上下文。",
|
||||||
"project.search" => "在项目文本文件中做有界字面量搜索。",
|
"project.search" => "在项目文本文件中做有界字面量搜索。",
|
||||||
"project.verify" => "运行 package.json 中原样声明的验证脚本。",
|
"project.verify" => "在项目或指定相对 cwd 中运行 package.json 原样声明的验证脚本,并检查构建产物。",
|
||||||
|
"project.bootstrap" => "仅在项目 game 目录受控执行无参数 npm install,并记录依赖文件指纹。",
|
||||||
"project.checkpoint" => "创建项目本地 checkpoint。",
|
"project.checkpoint" => "创建项目本地 checkpoint。",
|
||||||
"project.restore" => "从 checkpoint 恢复当前项目。",
|
"project.restore" => "从 checkpoint 恢复当前项目。",
|
||||||
"project.diff" => "读取 checkpoint 与当前项目之间的有界差异。",
|
"project.diff" => "读取 checkpoint 与当前项目之间的有界差异。",
|
||||||
@@ -1154,10 +1155,18 @@ fn runtime_tool_input_schema(tool: &str) -> Value {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"project.verify" => json!({
|
"project.verify" => json!({
|
||||||
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds"], "additionalProperties": false,
|
"type": "object", "required": ["script", "expectedCommand", "timeoutSeconds", "cwd"], "additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"script": { "type": "string", "minLength": 1 },
|
"script": { "type": "string", "minLength": 1 },
|
||||||
"expectedCommand": { "type": "string", "minLength": 1 },
|
"expectedCommand": { "type": "string", "minLength": 1 },
|
||||||
|
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 },
|
||||||
|
"cwd": { "type": ["string", "null"], "pattern": "^[A-Za-z0-9._/-]+$" }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
"project.bootstrap" => json!({
|
||||||
|
"type": "object", "required": ["cwd", "timeoutSeconds"], "additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"cwd": { "type": "string", "const": "game" },
|
||||||
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 }
|
"timeoutSeconds": { "type": "integer", "minimum": 1, "maximum": 600 }
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -321,6 +321,44 @@ pub(crate) fn resolve_project_command_spec_at(
|
|||||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))
|
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the one privileged npm operation used to hydrate a DirectProject.
|
||||||
|
/// It intentionally bypasses the general command.exec npm allow-list: callers
|
||||||
|
/// must use the dedicated `project.bootstrap` action, which only accepts the
|
||||||
|
/// literal `npm install` in the project's `game` directory.
|
||||||
|
pub(crate) fn resolve_project_bootstrap_spec_at(
|
||||||
|
root: &Path,
|
||||||
|
timeout_seconds: u64,
|
||||||
|
) -> Result<ProjectCommandSpec, ProjectCommandError> {
|
||||||
|
validate_project_root(root)
|
||||||
|
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||||
|
let cwd_relative = "game".to_string();
|
||||||
|
let cwd = resolve_local_project_path(root, &cwd_relative)
|
||||||
|
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||||
|
validate_project_command_cwd_components(root, &cwd_relative)
|
||||||
|
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||||
|
if !(PROJECT_COMMAND_MIN_TIMEOUT_SECONDS..=PROJECT_COMMAND_MAX_TIMEOUT_SECONDS)
|
||||||
|
.contains(&timeout_seconds)
|
||||||
|
{
|
||||||
|
return Err(ProjectCommandError::new(
|
||||||
|
ProjectCommandErrorStage::Validation,
|
||||||
|
format!("project.bootstrap timeoutSeconds 必须在 {PROJECT_COMMAND_MIN_TIMEOUT_SECONDS}-{PROJECT_COMMAND_MAX_TIMEOUT_SECONDS} 之间"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let program = "npm".to_string();
|
||||||
|
let (executable, safe_path) = resolve_project_command_executable(root, &program)
|
||||||
|
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Validation, error))?;
|
||||||
|
Ok(ProjectCommandSpec {
|
||||||
|
program,
|
||||||
|
executable,
|
||||||
|
safe_path,
|
||||||
|
arguments: vec!["install".to_string()],
|
||||||
|
cwd_relative,
|
||||||
|
cwd,
|
||||||
|
timeout_seconds,
|
||||||
|
verification_eligible: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_project_command_spec_inner(
|
fn resolve_project_command_spec_inner(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
program: &str,
|
program: &str,
|
||||||
@@ -1202,6 +1240,19 @@ pub(crate) fn prepare_project_command_launch_spec(
|
|||||||
(OsString::from("NO_PROXY"), OsString::new()),
|
(OsString::from("NO_PROXY"), OsString::new()),
|
||||||
(OsString::from("PATH"), spec.safe_path.clone()),
|
(OsString::from("PATH"), spec.safe_path.clone()),
|
||||||
];
|
];
|
||||||
|
if spec
|
||||||
|
.arguments
|
||||||
|
.first()
|
||||||
|
.is_some_and(|argument| argument == "install")
|
||||||
|
{
|
||||||
|
for (name, value) in &mut environment {
|
||||||
|
match name.to_string_lossy().as_ref() {
|
||||||
|
"npm_config_offline" => *value = OsString::from("false"),
|
||||||
|
"HTTP_PROXY" | "HTTPS_PROXY" | "ALL_PROXY" => *value = OsString::new(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for name in ["SystemRoot", "PATHEXT", "RUSTUP_HOME"] {
|
for name in ["SystemRoot", "PATHEXT", "RUSTUP_HOME"] {
|
||||||
if let Some(value) = std::env::var_os(name) {
|
if let Some(value) = std::env::var_os(name) {
|
||||||
environment.push((OsString::from(name), value));
|
environment.push((OsString::from(name), value));
|
||||||
@@ -1998,6 +2049,16 @@ pub(crate) async fn run_project_command_with_output_at(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run_project_bootstrap_command_at(
|
||||||
|
root: &Path,
|
||||||
|
timeout_seconds: u64,
|
||||||
|
) -> Result<ProjectCommandResult, ProjectCommandError> {
|
||||||
|
let spec = resolve_project_bootstrap_spec_at(root, timeout_seconds)?;
|
||||||
|
let launch = prepare_project_command_launch_spec(root, &spec)?;
|
||||||
|
let staged = stage_project_command_launch_spec(&spec, launch)?;
|
||||||
|
run_prepared_project_command_with_output_at(root, &spec, staged, None, || Ok(())).await
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn run_prepared_project_command_with_output_at<F>(
|
pub(crate) async fn run_prepared_project_command_with_output_at<F>(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
spec: &ProjectCommandSpec,
|
spec: &ProjectCommandSpec,
|
||||||
|
|||||||
@@ -302,7 +302,19 @@ mod linux {
|
|||||||
cwd: &Path,
|
cwd: &Path,
|
||||||
environment: &[(OsString, OsString)],
|
environment: &[(OsString, OsString)],
|
||||||
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
|
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
|
||||||
let metadata = CommandSandboxMetadata::enforced_linux();
|
let mut metadata = CommandSandboxMetadata::enforced_linux();
|
||||||
|
let network_enabled = executable
|
||||||
|
.file_name()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.is_some_and(|name| {
|
||||||
|
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
|
||||||
|
})
|
||||||
|
&& arguments
|
||||||
|
.first()
|
||||||
|
.is_some_and(|argument| argument == "install");
|
||||||
|
if network_enabled {
|
||||||
|
metadata.network = "enabled";
|
||||||
|
}
|
||||||
let bwrap = find_trusted_bwrap().map_err(|error| {
|
let bwrap = find_trusted_bwrap().map_err(|error| {
|
||||||
CommandSandboxError::new(
|
CommandSandboxError::new(
|
||||||
format!("command sandbox unavailable: {error}"),
|
format!("command sandbox unavailable: {error}"),
|
||||||
@@ -363,7 +375,7 @@ mod linux {
|
|||||||
)?;
|
)?;
|
||||||
let fixed_system_read_only = collect_fixed_system_mounts();
|
let fixed_system_read_only = collect_fixed_system_mounts();
|
||||||
|
|
||||||
let launch = build_linux_bwrap_launch(LinuxSandboxPlan {
|
let mut launch = build_linux_bwrap_launch(LinuxSandboxPlan {
|
||||||
bwrap,
|
bwrap,
|
||||||
root,
|
root,
|
||||||
cwd,
|
cwd,
|
||||||
@@ -375,6 +387,7 @@ mod linux {
|
|||||||
external_read_only,
|
external_read_only,
|
||||||
fixed_system_read_only,
|
fixed_system_read_only,
|
||||||
});
|
});
|
||||||
|
launch.metadata = metadata.clone();
|
||||||
run_project_mount_preflight(&launch).map_err(|error| {
|
run_project_mount_preflight(&launch).map_err(|error| {
|
||||||
CommandSandboxError::new(
|
CommandSandboxError::new(
|
||||||
format!("command sandbox project mount preflight 失败:{error}"),
|
format!("command sandbox project mount preflight 失败:{error}"),
|
||||||
@@ -602,7 +615,19 @@ mod linux {
|
|||||||
|
|
||||||
fn build_linux_bwrap_launch(plan: LinuxSandboxPlan) -> CommandSandboxLaunch {
|
fn build_linux_bwrap_launch(plan: LinuxSandboxPlan) -> CommandSandboxLaunch {
|
||||||
let mut args = Vec::<OsString>::new();
|
let mut args = Vec::<OsString>::new();
|
||||||
push_namespace_arguments(&mut args);
|
push_namespace_arguments(
|
||||||
|
&mut args,
|
||||||
|
plan.executable
|
||||||
|
.file_name()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.is_some_and(|name| {
|
||||||
|
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
|
||||||
|
})
|
||||||
|
&& plan
|
||||||
|
.arguments
|
||||||
|
.first()
|
||||||
|
.is_some_and(|argument| argument == "install"),
|
||||||
|
);
|
||||||
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
||||||
for (target, destination) in &plan.merged_usr_links {
|
for (target, destination) in &plan.merged_usr_links {
|
||||||
push_option(
|
push_option(
|
||||||
@@ -673,7 +698,7 @@ mod linux {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_namespace_arguments(args: &mut Vec<OsString>) {
|
fn push_namespace_arguments(args: &mut Vec<OsString>, share_network: bool) {
|
||||||
for argument in [
|
for argument in [
|
||||||
"--die-with-parent",
|
"--die-with-parent",
|
||||||
"--unshare-all",
|
"--unshare-all",
|
||||||
@@ -686,6 +711,9 @@ mod linux {
|
|||||||
] {
|
] {
|
||||||
args.push(OsString::from(argument));
|
args.push(OsString::from(argument));
|
||||||
}
|
}
|
||||||
|
if share_network {
|
||||||
|
args.push(OsString::from("--share-net"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_option<'a, I>(args: &mut Vec<OsString>, option: &str, values: I)
|
fn push_option<'a, I>(args: &mut Vec<OsString>, option: &str, values: I)
|
||||||
@@ -829,7 +857,7 @@ mod linux {
|
|||||||
merged_usr_links: &[(OsString, PathBuf)],
|
merged_usr_links: &[(OsString, PathBuf)],
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut args = Vec::<OsString>::new();
|
let mut args = Vec::<OsString>::new();
|
||||||
push_namespace_arguments(&mut args);
|
push_namespace_arguments(&mut args, false);
|
||||||
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
||||||
for (target, destination) in merged_usr_links {
|
for (target, destination) in merged_usr_links {
|
||||||
push_option(
|
push_option(
|
||||||
|
|||||||
@@ -905,7 +905,6 @@ mod tests {
|
|||||||
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
provider: platform_llm::LlmProvider::OpenAiCompatible,
|
||||||
model: "context-compaction-test".to_string(),
|
model: "context-compaction-test".to_string(),
|
||||||
text: summary.into(),
|
text: summary.into(),
|
||||||
reasoning: String::new(),
|
|
||||||
finish_reason: Some("stop".to_string()),
|
finish_reason: Some("stop".to_string()),
|
||||||
response_id: Some("context-compaction-response".to_string()),
|
response_id: Some("context-compaction-response".to_string()),
|
||||||
usage: Some(platform_llm::LlmTokenUsage {
|
usage: Some(platform_llm::LlmTokenUsage {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ pub(crate) const ISOLATED_AGENT_PRIVATE_MEMORY_SCHEMA_VERSION: &str =
|
|||||||
|
|
||||||
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
|
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
|
||||||
"project.verify",
|
"project.verify",
|
||||||
|
"project.bootstrap",
|
||||||
"project.git_commit",
|
"project.git_commit",
|
||||||
"command.exec",
|
"command.exec",
|
||||||
"command.start",
|
"command.start",
|
||||||
@@ -59,6 +60,7 @@ pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[
|
|||||||
|
|
||||||
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS: &[&str] = &[
|
pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS: &[&str] = &[
|
||||||
"project.verify",
|
"project.verify",
|
||||||
|
"project.bootstrap",
|
||||||
"project.git_commit",
|
"project.git_commit",
|
||||||
"command.exec",
|
"command.exec",
|
||||||
"command.start",
|
"command.start",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::io::{Seek, SeekFrom};
|
|||||||
mod agent_db;
|
mod agent_db;
|
||||||
mod asset_export;
|
mod asset_export;
|
||||||
mod asset_rename;
|
mod asset_rename;
|
||||||
|
mod bootstrap;
|
||||||
mod checkpoint;
|
mod checkpoint;
|
||||||
mod conversation;
|
mod conversation;
|
||||||
mod export;
|
mod export;
|
||||||
@@ -23,6 +24,7 @@ mod write_lock;
|
|||||||
pub(crate) use agent_db::*;
|
pub(crate) use agent_db::*;
|
||||||
pub(crate) use asset_export::*;
|
pub(crate) use asset_export::*;
|
||||||
pub(crate) use asset_rename::*;
|
pub(crate) use asset_rename::*;
|
||||||
|
pub(crate) use bootstrap::*;
|
||||||
pub(crate) use checkpoint::*;
|
pub(crate) use checkpoint::*;
|
||||||
pub(crate) use conversation::*;
|
pub(crate) use conversation::*;
|
||||||
pub(crate) use export::*;
|
pub(crate) use export::*;
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
use super::*;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
const BOOTSTRAP_PACKAGE_MAX_BYTES: u64 = 512 * 1024;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct ProjectBootstrapResult {
|
||||||
|
pub(crate) status: String,
|
||||||
|
pub(crate) output: String,
|
||||||
|
pub(crate) package_sha256: String,
|
||||||
|
pub(crate) lock_sha256: Option<String>,
|
||||||
|
pub(crate) log_path: String,
|
||||||
|
pub(crate) updated_at: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_bootstrap_file(path: &Path, label: &str) -> Result<Vec<u8>, String> {
|
||||||
|
let metadata = fs::symlink_metadata(path)
|
||||||
|
.map_err(|error| format!("读取 {label} 失败:{}: {error}", path.display()))?;
|
||||||
|
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||||
|
return Err(format!("project.bootstrap 要求 {label} 是普通文件"));
|
||||||
|
}
|
||||||
|
if metadata.len() > BOOTSTRAP_PACKAGE_MAX_BYTES {
|
||||||
|
return Err(format!(
|
||||||
|
"project.bootstrap {label} 超过 {} 字节上限",
|
||||||
|
BOOTSTRAP_PACKAGE_MAX_BYTES
|
||||||
|
));
|
||||||
|
}
|
||||||
|
prepare_game_creator_private_path_for_read(path, false, label)?;
|
||||||
|
fs::read(path).map_err(|error| format!("读取 {label} 失败:{error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn run_project_bootstrap_at(
|
||||||
|
root: &Path,
|
||||||
|
timeout_seconds: u64,
|
||||||
|
) -> Result<ProjectBootstrapResult, String> {
|
||||||
|
validate_project_root(root)?;
|
||||||
|
let game = resolve_local_project_path(root, "game")?;
|
||||||
|
if !game.is_dir() {
|
||||||
|
return Err("project.bootstrap 只允许项目内 game 目录".to_string());
|
||||||
|
}
|
||||||
|
let package = read_bootstrap_file(&game.join("package.json"), "game/package.json")?;
|
||||||
|
let package_json: serde_json::Value = serde_json::from_slice(&package)
|
||||||
|
.map_err(|error| format!("解析 game/package.json 失败:{error}"))?;
|
||||||
|
if let Some(manager) = package_json
|
||||||
|
.get("packageManager")
|
||||||
|
.and_then(serde_json::Value::as_str)
|
||||||
|
{
|
||||||
|
if !manager.trim().starts_with("npm@") && manager.trim() != "npm" {
|
||||||
|
return Err("project.bootstrap 当前只支持 npm packageManager".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for lock_name in ["pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"] {
|
||||||
|
if game.join(lock_name).exists() {
|
||||||
|
return Err(format!("project.bootstrap 检测到非 npm 锁文件 {lock_name}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !package_json
|
||||||
|
.get("scripts")
|
||||||
|
.is_some_and(serde_json::Value::is_object)
|
||||||
|
{
|
||||||
|
return Err("project.bootstrap 要求 game/package.json 包含 scripts 对象".to_string());
|
||||||
|
}
|
||||||
|
let lock = match fs::symlink_metadata(game.join("package-lock.json")) {
|
||||||
|
Ok(_) => Some(read_bootstrap_file(
|
||||||
|
&game.join("package-lock.json"),
|
||||||
|
"game/package-lock.json",
|
||||||
|
)?),
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||||
|
Err(error) => return Err(format!("读取 game/package-lock.json 失败:{error}")),
|
||||||
|
};
|
||||||
|
let command = crate::command_exec::run_project_bootstrap_command_at(root, timeout_seconds)
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
let completed = command.exit_code == Some(0) && !command.timed_out;
|
||||||
|
let status = if completed { "completed" } else { "failed" };
|
||||||
|
let updated_at = unix_timestamp();
|
||||||
|
let log_path = resolve_local_project_path(root, ".agent/logs/command.log")?;
|
||||||
|
let package_sha256 = format!("{:x}", Sha256::digest(&package));
|
||||||
|
let lock_sha256 = lock
|
||||||
|
.as_ref()
|
||||||
|
.map(|bytes| format!("{:x}", Sha256::digest(bytes)));
|
||||||
|
let output = sanitize_project_verification_output(&command.output);
|
||||||
|
let line = format!(
|
||||||
|
"{updated_at} project.bootstrap status={} packageSha256={} lockSha256={} cwd=game\n{}\n",
|
||||||
|
status,
|
||||||
|
package_sha256,
|
||||||
|
lock_sha256.as_deref().unwrap_or("none"),
|
||||||
|
output
|
||||||
|
);
|
||||||
|
append_game_creator_private_file(&log_path, line.as_bytes(), "命令日志")?;
|
||||||
|
record_command_run(
|
||||||
|
root,
|
||||||
|
GameCreationAppCommandRunState {
|
||||||
|
command_id: "project.bootstrap".to_string(),
|
||||||
|
status: if completed {
|
||||||
|
GameCreationAppCommandRunStatus::Completed
|
||||||
|
} else {
|
||||||
|
GameCreationAppCommandRunStatus::Failed
|
||||||
|
},
|
||||||
|
output: output.clone(),
|
||||||
|
log_path: ".agent/logs/command.log".to_string(),
|
||||||
|
updated_at,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
Ok(ProjectBootstrapResult {
|
||||||
|
status: status.to_string(),
|
||||||
|
output,
|
||||||
|
package_sha256,
|
||||||
|
lock_sha256,
|
||||||
|
log_path: ".agent/logs/command.log".to_string(),
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user