AGC 官方 LLM Router 账号链路与流式联网输出 #242
+34
-11
@@ -288,7 +288,8 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
||||
.with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low)
|
||||
.with_function_tools(function_tools)
|
||||
.with_tool_choice(platform_llm::LlmToolChoice::Required);
|
||||
let request = apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false);
|
||||
let request =
|
||||
apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false);
|
||||
return Ok((
|
||||
llm,
|
||||
config_path,
|
||||
@@ -1330,7 +1331,8 @@ mod tests {
|
||||
let system_prompt = &request.messages[0].content;
|
||||
let user_prompt = &request.messages[1].content;
|
||||
assert!(system_prompt.contains("自主执行 Agent"));
|
||||
assert!(system_prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
|
||||
assert!(system_prompt
|
||||
.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
|
||||
assert!(user_prompt.contains("依赖只作为参考"));
|
||||
assert!(user_prompt.contains("不要等待或索要平台资产/验收回执"));
|
||||
assert!(!user_prompt.contains("固定 owner 收束协议"));
|
||||
@@ -1383,31 +1385,50 @@ mod tests {
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"code-prototype",
|
||||
);
|
||||
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
|
||||
for tool in [
|
||||
"project.verify",
|
||||
"command.run_limited",
|
||||
"preview.start",
|
||||
"preview.validate",
|
||||
] {
|
||||
assert!(!request_advertises_native_tool(&code, tool));
|
||||
}
|
||||
assert!(request_advertises_native_tool(&code, "file.write"));
|
||||
assert!(code.messages[0].content.contains("自主执行 Agent"));
|
||||
assert!(code.messages[1].content.contains("不要等待或索要平台资产/验收回执"));
|
||||
assert!(code.messages[1]
|
||||
.content
|
||||
.contains("不要等待或索要平台资产/验收回执"));
|
||||
|
||||
let readiness = build_autonomous_ready_child_request(
|
||||
"preview-readiness",
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
"preview-readiness",
|
||||
);
|
||||
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
|
||||
for tool in [
|
||||
"project.verify",
|
||||
"command.run_limited",
|
||||
"preview.start",
|
||||
"preview.validate",
|
||||
] {
|
||||
assert!(!request_advertises_native_tool(&readiness, tool));
|
||||
}
|
||||
assert!(request_advertises_native_tool(&readiness, "file.read"));
|
||||
assert!(readiness.messages[0].content.contains("自主执行 Agent"));
|
||||
assert!(readiness.messages[1].content.contains("不要等待或索要平台资产/验收回执"));
|
||||
assert!(readiness.messages[1]
|
||||
.content
|
||||
.contains("不要等待或索要平台资产/验收回执"));
|
||||
|
||||
let publish = build_autonomous_ready_child_request(
|
||||
"publish-package",
|
||||
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
|
||||
"publish-package",
|
||||
);
|
||||
for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] {
|
||||
for tool in [
|
||||
"project.verify",
|
||||
"command.run_limited",
|
||||
"preview.start",
|
||||
"preview.validate",
|
||||
] {
|
||||
assert!(!request_advertises_native_tool(&publish, tool));
|
||||
}
|
||||
assert!(request_advertises_native_tool(&publish, "file.write"));
|
||||
@@ -1482,9 +1503,8 @@ mod tests {
|
||||
assert!(prompts.contains("自主执行 Agent"));
|
||||
assert!(prompts.contains("依赖只作为参考"));
|
||||
assert!(!prompts.contains("非只读视觉规范生成任务"));
|
||||
assert!(!prompts.contains(
|
||||
crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER
|
||||
));
|
||||
assert!(!prompts
|
||||
.contains(crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER));
|
||||
assert!(!prompts.contains("会同时提交当前 run 的 mutation 与验证凭证"));
|
||||
assert!(!prompts.contains("无生图凭据只读协调任务"));
|
||||
}
|
||||
@@ -1527,7 +1547,10 @@ mod tests {
|
||||
// The autonomous execution marker lives in the system message; the
|
||||
// user message carries only the task-specific runtime context.
|
||||
let prompt = &request.messages[0].content;
|
||||
assert!(prompt.contains("自主执行 Agent"), "unexpected relaxed root prompt: {prompt}");
|
||||
assert!(
|
||||
prompt.contains("自主执行 Agent"),
|
||||
"unexpected relaxed root prompt: {prompt}"
|
||||
);
|
||||
assert!(prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件"));
|
||||
assert!(request.function_tools.len() > 1);
|
||||
for tool in [
|
||||
|
||||
@@ -817,12 +817,10 @@ fn autonomous_root_goal_contract_persisted_for_binding_at(
|
||||
root: &Path,
|
||||
binding: &AgentRuntimeRunProfileBinding,
|
||||
) -> Result<bool, String> {
|
||||
Ok(read_game_creator_agent_runtime_goal_contract_at(
|
||||
root,
|
||||
&binding.agent_id,
|
||||
&binding.run_id,
|
||||
)?
|
||||
.is_some())
|
||||
Ok(
|
||||
read_game_creator_agent_runtime_goal_contract_at(root, &binding.agent_id, &binding.run_id)?
|
||||
.is_some(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Return whether the trusted autonomous root has a valid, persisted Goal
|
||||
@@ -1099,11 +1097,7 @@ fn queue_or_recover_autonomous_manifest_ready_task_at(
|
||||
result.accepted_run_id = Some(record.run_id.clone());
|
||||
return Ok((result, (*record).clone(), true));
|
||||
}
|
||||
let run_id = format!(
|
||||
"autonomous-ready-{}-{}",
|
||||
task.id,
|
||||
unix_timestamp_nanos()
|
||||
);
|
||||
let run_id = format!("autonomous-ready-{}-{}", task.id, unix_timestamp_nanos());
|
||||
// Keep parent metadata as an optional correlation hint. Relaxed
|
||||
// orchestration never uses it as a readiness/identity gate, but it lets
|
||||
// the DAG state query distinguish this root's child from an older run.
|
||||
@@ -1222,16 +1216,13 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
|
||||
// status change makes subsequent passes ignore it.
|
||||
match existing {
|
||||
None => candidates.push((task.clone(), false)),
|
||||
Some(record)
|
||||
if game_creator_agent_runtime_terminal_status(&record).is_some() =>
|
||||
{
|
||||
Some(record) if game_creator_agent_runtime_terminal_status(&record).is_some() => {
|
||||
terminal_records.push(record);
|
||||
}
|
||||
Some(_) => {}
|
||||
}
|
||||
}
|
||||
for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks, &parent_binding.source)
|
||||
{
|
||||
for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks, &parent_binding.source) {
|
||||
if let Some(task) = manifest.tasks.iter().find(|task| task.id == task_id) {
|
||||
candidates.push((task.clone(), true));
|
||||
}
|
||||
@@ -1504,7 +1495,9 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
let task_id = [state.task_id.trim(), state.agent_id.trim()]
|
||||
.into_iter()
|
||||
.find(|candidate| !candidate.is_empty() && manifest.tasks.iter().any(|task| task.id == *candidate));
|
||||
.find(|candidate| {
|
||||
!candidate.is_empty() && manifest.tasks.iter().any(|task| task.id == *candidate)
|
||||
});
|
||||
let Some(task_id) = task_id else {
|
||||
// A relaxed child that is not a manifest task is an ordinary
|
||||
// Runtime run; it must not be guessed into the task graph.
|
||||
@@ -1571,9 +1564,7 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke
|
||||
{
|
||||
let status = match state.phase.as_str() {
|
||||
"completed" => GameCreationAppTaskStatus::Completed,
|
||||
"failed" | "cancelled" | "budget-exhausted" => {
|
||||
GameCreationAppTaskStatus::Failed
|
||||
}
|
||||
"failed" | "cancelled" | "budget-exhausted" => GameCreationAppTaskStatus::Failed,
|
||||
_ => return Ok(false),
|
||||
};
|
||||
let manifest = read_manifest_for_project(root)?;
|
||||
@@ -1775,7 +1766,8 @@ pub(super) fn autonomous_manifest_ready_task_requires_visual_asset(task_id: &str
|
||||
fn render_autonomous_manifest_ready_task_owner_prompt(task: &GameCreationAppTaskState) -> String {
|
||||
let base = render_manifest_ready_task_background_prompt(task);
|
||||
let paths = autonomous_manifest_owner_artifact_paths(&task.id).join(", ");
|
||||
let visual_usage_requirement = if task.id == "code-prototype" && editor_api_key_is_configured() {
|
||||
let visual_usage_requirement = if task.id == "code-prototype" && editor_api_key_is_configured()
|
||||
{
|
||||
"本轮必须实际接入已登记的平台美术切片:先用 asset.list 读取 assets/art-spritesheet-slices/manifest.json,再在 game/index.html 的可见 canvas 主循环中为 player、blocks-and-targets、obstacles-and-scene、feedback-effects 四个切片分别创建 Image 并用相对路径加载;在 requestAnimationFrame 绘制中对每个已加载切片调用 ctx.drawImage(image, dx, dy, dw, dh) 或九参数裁剪形式,目标区域必须可见且至少 32×32。只放置 <img>/<picture>、只展示整张 assets/art-spritesheet.png、只写路径或只在注释中引用都不满足完成合同。"
|
||||
} else {
|
||||
""
|
||||
@@ -1981,10 +1973,9 @@ mod tests {
|
||||
"art-director",
|
||||
&prompt
|
||||
));
|
||||
let art_asset_prompt =
|
||||
render_autonomous_manifest_ready_task_background_prompt(&seed_task(
|
||||
"art-asset-plan",
|
||||
));
|
||||
let art_asset_prompt = render_autonomous_manifest_ready_task_background_prompt(
|
||||
&seed_task("art-asset-plan"),
|
||||
);
|
||||
assert!(art_asset_prompt.contains("canvas.asset_generate"));
|
||||
assert!(art_asset_prompt.contains("asset.list"));
|
||||
assert!(art_asset_prompt.contains("file.write 写入 assets/manifest.art.json"));
|
||||
|
||||
@@ -16,8 +16,9 @@ struct UserSelectedPathGrant {
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
static GAME_CREATOR_USER_SELECTED_PATH_GRANTS:
|
||||
OnceLock<Mutex<HashMap<String, UserSelectedPathGrant>>> = OnceLock::new();
|
||||
static GAME_CREATOR_USER_SELECTED_PATH_GRANTS: OnceLock<
|
||||
Mutex<HashMap<String, UserSelectedPathGrant>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
#[cfg(windows)]
|
||||
fn user_selected_path_grants() -> &'static Mutex<HashMap<String, UserSelectedPathGrant>> {
|
||||
@@ -150,6 +151,19 @@ fn build_game_creator_platform_llm_config(
|
||||
if game_creator_official_llm_route_locked() {
|
||||
return build_game_creator_official_platform_llm_config(llm);
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
{
|
||||
return Err("AGC 正式运行只允许通过 API Server 使用官方 LLM Router".to_string());
|
||||
}
|
||||
#[cfg(test)]
|
||||
build_game_creator_test_platform_llm_config(llm, config_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn build_game_creator_test_platform_llm_config(
|
||||
llm: &GameCreatorLlmConfig,
|
||||
config_path: &str,
|
||||
) -> Result<LlmConfig, String> {
|
||||
let api_kind = validate_game_creator_llm_web_search_config(llm, config_path)?;
|
||||
let api_key =
|
||||
trim_config_string(&llm.api_key).ok_or_else(|| llm_api_key_config_error(config_path))?;
|
||||
@@ -3244,7 +3258,7 @@ pub(crate) fn configure_game_creator_runtime_config_dir(
|
||||
.map_err(std::io::Error::other)?;
|
||||
}
|
||||
// Both the normal config and the optional local override are persisted
|
||||
// inputs. A release build must scrub legacy provider credentials from
|
||||
// inputs. Every real AGC build scrubs legacy provider credentials from
|
||||
// either file before the next read can observe them again.
|
||||
for path in [
|
||||
config_path,
|
||||
@@ -3336,7 +3350,6 @@ pub(crate) fn load_game_creator_app_config() -> Result<GameCreatorAppConfig, Str
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
|
||||
pub(crate) fn scrub_locked_game_creator_config_file(config: &mut GameCreatorAppConfigFile) -> bool {
|
||||
let mut changed = config.agent_mode.as_deref()
|
||||
!= Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER)
|
||||
@@ -3431,8 +3444,19 @@ pub(crate) fn migrate_legacy_game_creator_agent_mode(path: &Path) -> Result<(),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns whether a real AGC build must use the authenticated API Server
|
||||
/// proxy instead of any persisted provider credentials.
|
||||
///
|
||||
/// Debug and release binaries intentionally share this decision. The only
|
||||
/// exception is the Rust unit-test build, where existing tests may install an
|
||||
/// explicit loopback provider fixture; that fixture is never compiled into a
|
||||
/// shipped binary and is not a runtime fallback.
|
||||
pub(crate) fn game_creator_official_llm_route_locked() -> bool {
|
||||
!cfg!(debug_assertions) && !cfg!(test) && editor_api_mode() == EditorApiMode::PlatformAccount
|
||||
game_creator_official_llm_route_locked_for_build(cfg!(test))
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_official_llm_route_locked_for_build(is_test_build: bool) -> bool {
|
||||
!is_test_build
|
||||
}
|
||||
|
||||
pub(crate) fn lock_game_creator_app_config_to_official_route(config: &mut GameCreatorAppConfig) {
|
||||
|
||||
@@ -99,7 +99,8 @@ fn config_file_overrides_defaults_without_env() {
|
||||
assert_eq!(config.editor_api.api_key, "editor-key");
|
||||
|
||||
fs::remove_dir_all(root).expect("cleanup test config dir");
|
||||
}#[test]
|
||||
}
|
||||
#[test]
|
||||
fn agent_mode_defaults_to_codex_app_server_and_preserves_explicit_modes() {
|
||||
let default_config = GameCreatorAppConfig::default();
|
||||
assert_eq!(
|
||||
@@ -276,7 +277,7 @@ fn unsupported_config_schema_version_fails_closed() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_release_config_scrub_removes_all_legacy_provider_credentials() {
|
||||
fn locked_config_scrub_removes_all_legacy_provider_credentials() {
|
||||
let mut config: GameCreatorAppConfigFile = serde_json::from_value(serde_json::json!({
|
||||
"agentMode": "provider",
|
||||
"llm": {
|
||||
@@ -323,6 +324,12 @@ fn locked_release_config_scrub_removes_all_legacy_provider_credentials() {
|
||||
assert!(!serialized.contains("legacy.example.test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn official_llm_route_is_locked_for_debug_and_release_platform_builds() {
|
||||
assert!(game_creator_official_llm_route_locked_for_build(false));
|
||||
assert!(!game_creator_official_llm_route_locked_for_build(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_app_server_requires_responses_route_and_disables_native_web_search() {
|
||||
let mut llm = GameCreatorLlmConfig::default();
|
||||
|
||||
+32
-8
@@ -745,7 +745,10 @@ async fn autonomous_game_build_non_read_only_code_first_round_repairs_response_i
|
||||
.expect("repair first-round response")
|
||||
.expect("first-round mutation plan");
|
||||
if !plan.response.is_empty() {
|
||||
assert!(plan.actions.is_empty(), "relaxed autonomous plan must choose response or actions");
|
||||
assert!(
|
||||
plan.actions.is_empty(),
|
||||
"relaxed autonomous plan must choose response or actions"
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
@@ -876,7 +879,10 @@ async fn autonomous_game_build_unverified_mutation_immediately_repairs_into_veri
|
||||
.expect("repair immediate unverified completion")
|
||||
.expect("verification-only plan");
|
||||
if !plan.response.is_empty() {
|
||||
assert!(plan.actions.is_empty(), "relaxed autonomous plan may complete directly after a mutation");
|
||||
assert!(
|
||||
plan.actions.is_empty(),
|
||||
"relaxed autonomous plan may complete directly after a mutation"
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
@@ -1026,7 +1032,10 @@ async fn autonomous_manifest_code_prototype_requires_its_own_static_smoke_after_
|
||||
.expect("repair project.verify-only delivery")
|
||||
.expect("static-smoke repair plan");
|
||||
if !repair_plan.response.is_empty() {
|
||||
assert!(repair_plan.actions.is_empty(), "relaxed autonomous plan may report a valid response directly");
|
||||
assert!(
|
||||
repair_plan.actions.is_empty(),
|
||||
"relaxed autonomous plan may report a valid response directly"
|
||||
);
|
||||
return;
|
||||
}
|
||||
assert_eq!(repair_plan.actions.len(), 1);
|
||||
@@ -2241,7 +2250,10 @@ async fn autonomous_game_build_repairs_new_revision_after_failed_playtest_with_v
|
||||
.expect("repair new revision revalidation stall")
|
||||
.expect("repaired supervisor verification plan");
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert!(matches!(plan.actions[0].tool.as_str(), "command.run_limited" | "project.index" | "project.verify"));
|
||||
assert!(matches!(
|
||||
plan.actions[0].tool.as_str(),
|
||||
"command.run_limited" | "project.index" | "project.verify"
|
||||
));
|
||||
if plan.actions[0].tool != "command.run_limited" {
|
||||
return;
|
||||
}
|
||||
@@ -2511,7 +2523,10 @@ async fn autonomous_game_build_claims_ready_delivery_before_fourth_playtest_dele
|
||||
.expect("repair full delivery convergence")
|
||||
.expect("ready delivery claim plan");
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert!(matches!(plan.actions[0].tool.as_str(), "agent.run_status" | "agent.delegate"));
|
||||
assert!(matches!(
|
||||
plan.actions[0].tool.as_str(),
|
||||
"agent.run_status" | "agent.delegate"
|
||||
));
|
||||
if plan.actions[0].tool != "agent.run_status" {
|
||||
return;
|
||||
}
|
||||
@@ -3020,7 +3035,10 @@ async fn autonomous_game_build_repairs_post_mutation_read_loop_into_verification
|
||||
.expect("repair post-mutation read loop")
|
||||
.expect("repaired verification plan");
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert!(matches!(plan.actions[0].tool.as_str(), "project.verify" | "project.index"));
|
||||
assert!(matches!(
|
||||
plan.actions[0].tool.as_str(),
|
||||
"project.verify" | "project.index"
|
||||
));
|
||||
if plan.actions[0].tool != "project.verify" {
|
||||
return;
|
||||
}
|
||||
@@ -3164,7 +3182,10 @@ async fn autonomous_game_build_repairs_pre_mutation_read_loop_into_action() {
|
||||
.expect("repair pre-mutation read loop")
|
||||
.expect("repaired autonomous action plan");
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert!(matches!(plan.actions[0].tool.as_str(), "file.write" | "project.index"));
|
||||
assert!(matches!(
|
||||
plan.actions[0].tool.as_str(),
|
||||
"file.write" | "project.index"
|
||||
));
|
||||
if plan.actions[0].tool != "file.write" {
|
||||
return;
|
||||
}
|
||||
@@ -3375,7 +3396,10 @@ async fn autonomous_game_build_repairs_truncated_scaffold_into_bounded_patch() {
|
||||
.expect("repair truncated autonomous scaffold")
|
||||
.expect("repaired autonomous patch plan");
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert!(matches!(plan.actions[0].tool.as_str(), "file.patch" | "file.write"));
|
||||
assert!(matches!(
|
||||
plan.actions[0].tool.as_str(),
|
||||
"file.patch" | "file.write"
|
||||
));
|
||||
if plan.actions[0].tool != "file.patch" {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user