让 AGC 错误反馈给 LLM 后继续修复

DirectProject 对可修复的工具、构建、验证和试玩错误最多反馈三次

补充 AGC 工作流与试玩 Skill 的错误反馈合同

保留鉴权、身份、传输和操作状态不确定等安全终止边界
This commit is contained in:
Git Hooks Test
2026-09-16 02:23:52 +08:00
parent d650bf9b74
commit bca198a17e
5 changed files with 195 additions and 27 deletions
@@ -13,6 +13,7 @@ Use `agc_browser_playtest` from the `agc_tools` MCP server. Do not replace it wi
2. Inspect both desktop and mobile results, including page readiness, visible text, screenshots, console errors, exceptions, failed requests, Canvas probes, blocked actions, and interaction evidence.
3. Compare screenshots with the user's request. Check that the active game fills its intended area, HUD elements do not cover gameplay, controls are visible, and requested platform art appears in the core experience.
4. If evidence exposes a defect, edit the actual game files and call the tool again when that is useful. The client enforces its own execution and resource bounds; do not invent a fixed repair loop in the response.
Feed the structured diagnostics, console errors, failed requests, and exception text back to the same LLM repair turn before reporting the playtest as failed. Treat the evidence as debugging input and rerun the affected stage after a real code or project change.
5. Treat browser infrastructure failure, an unloaded page, an unhandled exception, or missing evidence as a failed validation. Do not claim success from a partial result.
6. Use game-specific reasoning for quality. Do not require a fixed board, fixed text, fixed number of slices, or a legacy harness scenario; the tool result is evidence for Codex to interpret.
@@ -27,6 +27,8 @@ For a small edit to an existing game where the brief and suitable assets are unc
When a stage tool, command, or verification fails, retry at most three times before treating that stage as failed. Keep the retries serial and scoped to the same stage and the same input: a retry must not open a parallel path, skip ahead to a later stage, or substitute a placeholder for the missing output.
Every repairable failure must be fed back to the current LLM as the next debugging context before the stage is considered failed. Preserve the redacted tool or command error, the stage, the attempted input, and the evidence already collected; ask the LLM to inspect the current project, make the smallest real repair, and rerun the failed stage. A client-side `isError` tool result or a failed verification is feedback for the LLM, not by itself a terminal user-facing result. Do not silently swallow the error, replace it with a placeholder, or stop after the first failed attempt. Authentication, permission, billing, project identity, corrupted history, transport loss, cancellation, and uncertain paid-operation state remain terminal safety boundaries.
Only after the third attempt also fails, stop and tell the user the failure reason — which stage failed, which tool or command reported the error, what the error says, and what is still missing. A stage whose three attempts never succeeded is not complete, and its missing output cannot be reported as delivered.
Read the referenced specialist Skills for their detailed contracts: `agc-project-structure`, `taonier-art-assets`, `agc-web-game-development`, `agc-client-projection`, and `agc-browser-playtest`.
@@ -1,6 +1,6 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.15",
"version": "2026-08-26.16",
"skills": [
{
"name": "agc-game-production-workflow",
@@ -22,7 +22,7 @@
"agents/openai.yaml",
"references/workflow-contract.md"
],
"sha256": "d9d8e7e0a6bc512e0b463e0e4bd77edee1cc57f4a6965c9553e0920e38985d5c"
"sha256": "f25e5bd27e8fc82c61b08dc66366b5b253ee8d16d7fa72dbf2c94d2462f4e7fc"
},
{
"name": "agc-project-structure",
@@ -98,7 +98,7 @@
"agents/openai.yaml",
"references/browser-evidence-contract.md"
],
"sha256": "4437cd8a927a1c79a5faf4bcd40e9946676c08a3b460ab171298cabf899f49ad"
"sha256": "92ecce42d6589e034d32b75bcd155c1fee34a8c7b843eea5780c0577300ed521"
},
{
"name": "agc-client-projection",
@@ -1941,6 +1941,84 @@ fn direct_codex_failure_is_retryable(error: &str) -> bool {
.any(|marker| error.contains(marker))
}
/// DirectProject 的工具 / 构建 / 试玩失败应作为下一轮 LLM 的调试上下文继续处理,
/// 而不是在 app-server 把本轮标成 failed 后立即把错误交给用户。基础设施、身份和
/// 历史一致性错误没有安全的自动修复路径,必须保持终止语义。
const DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS: usize = 3;
fn direct_codex_error_should_feedback(error: &str) -> bool {
let normalized = error.to_ascii_lowercase();
let terminal_markers = [
"authentication-required",
"401",
"403",
"泥点余额不足",
"insufficient_mud_points",
"身份不唯一",
"身份不匹配",
"合同发生变化",
"历史记录类型无效",
"历史记录缺少 payload",
"历史注入载荷超过单行上限",
"工具参数",
"transport closed",
"连接已关闭",
"连接上游失败",
"硬上限",
"超时",
"取消",
"凭据",
"credential",
"context-window-exceeded",
"request-too-large",
"session-budget-exceeded",
"usage-limit-exceeded",
"stream-required",
"cyber-policy",
"sandbox-error",
"thread-rollback-failed",
"bad-request",
];
if terminal_markers.iter().any(|marker| {
if marker.chars().any(|character| character.is_uppercase()) {
error.contains(marker)
} else {
normalized.contains(marker)
}
}) {
return false;
}
let repairable_markers = [
"工具",
"tool",
"构建",
"build",
"编译",
"验证",
"verify",
"试玩",
"playtest",
"console",
"exception",
"未通过",
"失败",
"error",
];
repairable_markers.iter().any(|marker| {
if marker.chars().any(|character| character.is_uppercase()) {
error.contains(marker)
} else {
normalized.contains(marker)
}
})
}
fn direct_codex_error_feedback_prompt(error: &str, attempt: usize) -> String {
format!(
"上一轮 AGC 工具、构建或试玩执行失败。不要直接结束本轮,请把下面的错误当作新的调试信息:读取当前项目和相关输出,定位原因,修改实际项目文件后重新执行必要的失败步骤;只有确认属于鉴权、余额、项目身份、历史损坏、传输断开或操作状态不确定时才停止。不要伪造成功,也不要只复述错误。\n\n错误信息(客户端已脱敏):\n{error}\n\n这是第 {attempt}/{DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS} 次错误反馈。",
)
}
/// DirectProject 历史文件里与“行形状”有关的失败:同一份文件每次读都会得到同一结果,
/// 重试不会改变结论。IO 类失败(打开/读取目录)不在其中,那些仍按可重试处理。
const DIRECT_PROJECT_HISTORY_SHAPE_FAILURE_MARKERS: &[&str] = &[
@@ -4151,7 +4229,7 @@ fn build_direct_codex_system_prompt_with_search(
DIRECT_AGC_ENGINEERING_GUIDANCE.to_string(),
DIRECT_COCOS_BUILTIN_PLUGIN_GUIDANCE.to_string(),
DIRECT_COCOS_CAPABILITY_GUIDE.to_string(),
"工程执行要求:优先复用现有结构;按需读取真实文件,不依赖客户端预注入源码快照;修改后运行与改动相关的本地验证。不要创建 Supervisor、专业 Agent 或平行项目。".to_string(),
"工程执行要求:优先复用现有结构;按需读取真实文件,不依赖客户端预注入源码快照;修改后运行与改动相关的本地验证。工具返回 isError、构建失败、验证失败或试玩异常时,把错误当作调试上下文,读取当前项目、修复真实文件并重跑失败步骤,不要直接结束或伪造成功;鉴权、权限、余额、身份、历史、传输断开和操作状态不确定等安全错误才停止。不要创建 Supervisor、专业 Agent 或平行项目。".to_string(),
format!("提示词与技能:{skill_index}"),
];
if controlled_web_search {
@@ -4359,6 +4437,7 @@ async fn run_direct_game_creator_turn_inner(
let reply = if let Some(emitter) = turn_emitter {
let client_turn_id = emitter.turn_id().to_string();
let emitter = emitter.clone();
let observer_emitter = emitter.clone();
let mut observer = move |observation: DirectCodexTurnObservation| {
let status = direct_codex_observation_status(&observation, stream_enabled);
match observation {
@@ -4368,7 +4447,7 @@ async fn run_direct_game_creator_turn_inner(
if visible_text.is_none() {
return;
}
emitter.emit(status, None, visible_text);
observer_emitter.emit(status, None, visible_text);
}
DirectCodexTurnObservation::IntermediateText(intermediate_text) => {
let visible_text = if stream_enabled
@@ -4379,35 +4458,90 @@ async fn run_direct_game_creator_turn_inner(
None
};
if let Some(visible_text) = visible_text {
emitter.emit(status, None, Some(visible_text));
observer_emitter.emit(status, None, Some(visible_text));
}
}
DirectCodexTurnObservation::Activity(activity) => {
emitter.emit(status, Some(activity), None);
observer_emitter.emit(status, Some(activity), None);
}
}
};
direct_game_creator_codex_chat_at_with_optional_observer(
root,
system_prompt,
prompt.to_string(),
Some(&client_turn_id),
Some(&mut observer),
audit,
direct_user_item.clone(),
)
.await
let mut feedback_prompt = prompt.to_string();
let mut audit = audit;
let mut response = None;
for attempt in 1..=DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS {
match direct_game_creator_codex_chat_at_with_optional_observer(
root,
system_prompt.clone(),
feedback_prompt.clone(),
Some(&client_turn_id),
Some(&mut observer),
audit.as_deref_mut(),
direct_user_item.clone(),
)
.await
{
Ok(value) => {
response = Some(value);
break;
}
Err(error)
if attempt < DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS
&& direct_codex_error_should_feedback(&error) =>
{
let detail = redact_agent_runtime_error(root, &error, 1800);
emitter.emit(
"running",
Some("error-feedback"),
Some(format!("检测到执行错误,正在反馈给陶泥儿继续修复({attempt}/{DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS}")),
);
feedback_prompt = direct_codex_error_feedback_prompt(&detail, attempt + 1);
}
Err(error) => {
return Err(DirectCodexTurnFailure::new(
DirectCodexFailureStage::CodeGeneration,
error,
));
}
}
}
response.ok_or_else(|| "陶泥儿错误反馈回合未返回结果".to_string())
} else {
direct_game_creator_codex_chat_at_with_optional_observer(
root,
system_prompt,
prompt.to_string(),
None,
None,
audit,
direct_user_item.clone(),
)
.await
let mut feedback_prompt = prompt.to_string();
let mut audit = audit;
let mut response = None;
for attempt in 1..=DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS {
match direct_game_creator_codex_chat_at_with_optional_observer(
root,
system_prompt.clone(),
feedback_prompt.clone(),
None,
None,
audit.as_deref_mut(),
direct_user_item.clone(),
)
.await
{
Ok(value) => {
response = Some(value);
break;
}
Err(error)
if attempt < DIRECT_CODEX_ERROR_FEEDBACK_MAX_ATTEMPTS
&& direct_codex_error_should_feedback(&error) =>
{
let detail = redact_agent_runtime_error(root, &error, 1800);
feedback_prompt = direct_codex_error_feedback_prompt(&detail, attempt + 1);
}
Err(error) => {
return Err(DirectCodexTurnFailure::new(
DirectCodexFailureStage::CodeGeneration,
error,
));
}
}
}
response.ok_or_else(|| "陶泥儿错误反馈回合未返回结果".to_string())
}
.map_err(|error| DirectCodexTurnFailure::new(DirectCodexFailureStage::CodeGeneration, error))?;
let visible_reply = project_direct_codex_visible_text(&reply).ok_or_else(|| {
@@ -4737,6 +4871,33 @@ fn persist_direct_codex_assistant_reply_at(
mod tests {
use super::*;
#[test]
fn direct_tool_and_playtest_errors_are_feedbackable_but_transport_and_identity_errors_stop() {
assert!(direct_codex_error_should_feedback(
"agc_browser_playtest 失败:页面抛出异常"
));
assert!(direct_codex_error_should_feedback("npm run build 编译失败"));
assert!(!direct_codex_error_should_feedback(
"authentication-required: HTTP 401"
));
assert!(!direct_codex_error_should_feedback(
"Codex app-server 连接已关闭"
));
assert!(!direct_codex_error_should_feedback("项目身份不匹配"));
assert!(!direct_codex_error_should_feedback(
"工具参数 attempt 必须是 1 到 3 的整数"
));
}
#[test]
fn direct_error_feedback_prompt_requires_real_repair_and_is_bounded() {
let prompt = direct_codex_error_feedback_prompt("npm run build 失败:入口不存在", 2);
assert!(prompt.contains("读取当前项目和相关输出"));
assert!(prompt.contains("不要伪造成功"));
assert!(prompt.contains("第 2/3 次错误反馈"));
assert!(prompt.contains("入口不存在"));
}
fn direct_test_llm() -> GameCreatorLlmConfig {
GameCreatorLlmConfig {
api_key: "fixture-secret".to_string(),
@@ -4,6 +4,10 @@
`chat_with_game_creator_direct_codex` 必须携带 `projectPath``prompt`、稳定的 `clientTurnId` 和完整 `userItem``creationType``attachments` 按实际输入传递。Rust 通过 `projectPath` 解析项目身份,不接收额外 `projectId`。界面测试必须核对 `userItem` 的消息身份、角色、正文及附件内容,拒绝回合用例仍验证实际返回的错误原因。重开项目的历史恢复测试使用 `read_direct_project_history_slice` 的 canonical raw items 与 `hasMore`,首屏 `limit: 20`。资源图和生成任务的读取继续遵守原有工作台恢复逻辑,不因聊天断言失败延迟、关闭或改变它们。
## 2026-09-16 DirectProject 可修复错误回传 LLM
DirectProject 的 AGC 工具、构建、验证和浏览器试玩错误,若不属于鉴权、权限、余额、项目身份、历史损坏、传输断开、取消或付费操作状态不确定等安全终止边界,必须作为脱敏错误上下文回传同一 LLM 会话,由 LLM 读取当前项目、修改真实文件并重跑失败阶段。客户端最多连续反馈三次;每次保留 stage、工具 / 命令、错误正文和已有证据,不得静默吞错、伪造成功或用占位产物跳过阶段。达到三次仍失败后,才向用户投影终态错误和诊断引用。
## 2026-09-15 DirectProject 长回合平台会话保活
DirectProject 的生图、素材处理、构建和试玩可能跨越短生命周期 access token 的有效期。普通 `/api/*` 请求和 Codex app-server 已有 401 刷新路径,但 AGC 工具由 Rust 工具桥直接使用客户端当前会话,工具内部的 401 不会自动触发前端刷新。客户端在 DirectProject 回合处于 busy 状态时每 5 分钟调用现有 `requestPlatformSessionRefresh()`;刷新仍复用单飞请求、generation 校验和 native session 安装,不改变凭据来源,也不把 401 降级为成功。刷新失败保持静默,由原始 AGC 工具错误按现有鉴权失败合同返回,避免后台保活覆盖真实错误。