AGC 官方 LLM Router 账号链路与流式联网输出 #242

Merged
kdletters merged 59 commits from feat/agc-llm-router-official-chain into master 2026-09-06 16:49:13 +08:00
5 changed files with 120 additions and 51 deletions
Showing only changes of commit e9591c0543 - Show all commits
@@ -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();
@@ -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;
}