修复Agent失败原因丢失与成功误判
解析 Codex app-server 稳定失败分类并生成安全可行动摘要 统一 Runtime 事件、任务、阶段记录和各 Agent 状态面的失败展示 限制自主构建最终回复 fallback,避免鉴权与网络错误被提交为成功 补充失败分类、防泄漏、待核对和 fallback 回归测试 同步技术方案与共享决策记录
This commit is contained in:
@@ -20,6 +20,8 @@ const GAME_CREATOR_CODEX_APP_SERVER_THREAD_MAX: usize = 128;
|
|||||||
const GAME_CREATOR_CODEX_APP_SERVER_RPC_TIMEOUT_MS: u64 = 30_000;
|
const GAME_CREATOR_CODEX_APP_SERVER_RPC_TIMEOUT_MS: u64 = 30_000;
|
||||||
pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_TERMINAL_UNKNOWN_PREFIX: &str =
|
pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_TERMINAL_UNKNOWN_PREFIX: &str =
|
||||||
"codex-app-server-terminal-unknown:";
|
"codex-app-server-terminal-unknown:";
|
||||||
|
pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX: &str =
|
||||||
|
"codex-app-server-error:";
|
||||||
|
|
||||||
type RpcResult = Result<serde_json::Value, String>;
|
type RpcResult = Result<serde_json::Value, String>;
|
||||||
|
|
||||||
@@ -177,6 +179,92 @@ fn game_creator_codex_app_server_terminal_unknown(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_error_kind(kind: &str) -> platform_llm::LlmError {
|
||||||
|
platform_llm::LlmError::InvalidRequest(format!(
|
||||||
|
"{GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX}{kind}"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_error_http_status(
|
||||||
|
info: &serde_json::Value,
|
||||||
|
field: &str,
|
||||||
|
) -> Option<u16> {
|
||||||
|
info.get(field)?
|
||||||
|
.get("httpStatusCode")?
|
||||||
|
.as_u64()
|
||||||
|
.and_then(|status| u16::try_from(status).ok())
|
||||||
|
.filter(|status| (100..=599).contains(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_connection_error(
|
||||||
|
info: &serde_json::Value,
|
||||||
|
field: &str,
|
||||||
|
) -> platform_llm::LlmError {
|
||||||
|
match game_creator_codex_app_server_error_http_status(info, field) {
|
||||||
|
Some(401 | 403) => game_creator_codex_app_server_error_kind("unauthorized"),
|
||||||
|
Some(status_code) => platform_llm::LlmError::Upstream {
|
||||||
|
status_code,
|
||||||
|
message: "Codex app-server 连接上游失败".to_string(),
|
||||||
|
},
|
||||||
|
None => platform_llm::LlmError::Connectivity {
|
||||||
|
attempts: 1,
|
||||||
|
message: "Codex app-server 连接失败".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn game_creator_codex_app_server_failed_turn_error(
|
||||||
|
turn: &serde_json::Value,
|
||||||
|
) -> platform_llm::LlmError {
|
||||||
|
let Some(info) = turn
|
||||||
|
.get("error")
|
||||||
|
.and_then(|error| error.get("codexErrorInfo"))
|
||||||
|
.filter(|info| !info.is_null())
|
||||||
|
else {
|
||||||
|
return game_creator_codex_app_server_error_kind("other");
|
||||||
|
};
|
||||||
|
if let Some(kind) = info.as_str() {
|
||||||
|
return match kind {
|
||||||
|
"contextWindowExceeded" => {
|
||||||
|
game_creator_codex_app_server_error_kind("context-window-exceeded")
|
||||||
|
}
|
||||||
|
"sessionBudgetExceeded" => {
|
||||||
|
game_creator_codex_app_server_error_kind("session-budget-exceeded")
|
||||||
|
}
|
||||||
|
"usageLimitExceeded" => {
|
||||||
|
game_creator_codex_app_server_error_kind("usage-limit-exceeded")
|
||||||
|
}
|
||||||
|
"serverOverloaded" | "internalServerError" => platform_llm::LlmError::Upstream {
|
||||||
|
status_code: 503,
|
||||||
|
message: "Codex app-server 上游服务暂时不可用".to_string(),
|
||||||
|
},
|
||||||
|
"cyberPolicy" => game_creator_codex_app_server_error_kind("cyber-policy"),
|
||||||
|
"unauthorized" => game_creator_codex_app_server_error_kind("unauthorized"),
|
||||||
|
"badRequest" => game_creator_codex_app_server_error_kind("bad-request"),
|
||||||
|
"threadRollbackFailed" => {
|
||||||
|
game_creator_codex_app_server_error_kind("thread-rollback-failed")
|
||||||
|
}
|
||||||
|
"sandboxError" => game_creator_codex_app_server_error_kind("sandbox-error"),
|
||||||
|
"other" => game_creator_codex_app_server_error_kind("other"),
|
||||||
|
_ => game_creator_codex_app_server_error_kind("other"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for field in [
|
||||||
|
"httpConnectionFailed",
|
||||||
|
"responseStreamConnectionFailed",
|
||||||
|
"responseStreamDisconnected",
|
||||||
|
"responseTooManyFailedAttempts",
|
||||||
|
] {
|
||||||
|
if info.get(field).is_some() {
|
||||||
|
return game_creator_codex_app_server_connection_error(info, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.get("activeTurnNotSteerable").is_some() {
|
||||||
|
return game_creator_codex_app_server_error_kind("active-turn-not-steerable");
|
||||||
|
}
|
||||||
|
game_creator_codex_app_server_error_kind("other")
|
||||||
|
}
|
||||||
|
|
||||||
async fn isolate_game_creator_codex_app_server_terminal_unknown(
|
async fn isolate_game_creator_codex_app_server_terminal_unknown(
|
||||||
inner: &Arc<CodexAppServerInner>,
|
inner: &Arc<CodexAppServerInner>,
|
||||||
detail: impl Into<String>,
|
detail: impl Into<String>,
|
||||||
@@ -1012,9 +1100,7 @@ impl CodexAppServerConnection {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
"failed" => {
|
"failed" => {
|
||||||
return Err(platform_llm::LlmError::InvalidRequest(
|
return Err(game_creator_codex_app_server_failed_turn_error(turn))
|
||||||
"Codex app-server turn 执行失败".to_string(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
status => {
|
status => {
|
||||||
return Err(platform_llm::LlmError::Deserialize(format!(
|
return Err(platform_llm::LlmError::Deserialize(format!(
|
||||||
@@ -1577,6 +1663,81 @@ mod tests {
|
|||||||
assert!(response.text.is_empty());
|
assert!(response.text.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_app_server_failed_turn_uses_structured_error_info_without_raw_details() {
|
||||||
|
let secret = ["sk", "turn-secret"].join("-");
|
||||||
|
let failed_turn = serde_json::json!({
|
||||||
|
"status": "failed",
|
||||||
|
"error": {
|
||||||
|
"message": format!("private message {secret}"),
|
||||||
|
"additionalDetails": "https://provider.example/private C:\\Users\\victim\\project",
|
||||||
|
"codexErrorInfo": "contextWindowExceeded"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let error = game_creator_codex_app_server_failed_turn_error(&failed_turn);
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
platform_llm::LlmError::InvalidRequest(
|
||||||
|
"codex-app-server-error:context-window-exceeded".to_string()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let visible = error.to_string();
|
||||||
|
assert!(!visible.contains(&secret));
|
||||||
|
assert!(!visible.contains("provider.example"));
|
||||||
|
assert!(!visible.contains("victim"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_app_server_failed_turn_maps_stable_categories_and_http_status() {
|
||||||
|
for (info, expected) in [
|
||||||
|
(
|
||||||
|
serde_json::json!("usageLimitExceeded"),
|
||||||
|
platform_llm::LlmError::InvalidRequest(
|
||||||
|
"codex-app-server-error:usage-limit-exceeded".to_string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
serde_json::json!("unauthorized"),
|
||||||
|
platform_llm::LlmError::InvalidRequest(
|
||||||
|
"codex-app-server-error:unauthorized".to_string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
serde_json::json!({"httpConnectionFailed":{"httpStatusCode":429}}),
|
||||||
|
platform_llm::LlmError::Upstream {
|
||||||
|
status_code: 429,
|
||||||
|
message: "Codex app-server 连接上游失败".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
serde_json::json!({"responseStreamDisconnected":{"httpStatusCode":null}}),
|
||||||
|
platform_llm::LlmError::Connectivity {
|
||||||
|
attempts: 1,
|
||||||
|
message: "Codex app-server 连接失败".to_string(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let turn = serde_json::json!({
|
||||||
|
"status": "failed",
|
||||||
|
"error": {
|
||||||
|
"message": "private upstream body",
|
||||||
|
"additionalDetails": "private diagnostics",
|
||||||
|
"codexErrorInfo": info
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
game_creator_codex_app_server_failed_turn_error(&turn),
|
||||||
|
expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
game_creator_codex_app_server_failed_turn_error(
|
||||||
|
&serde_json::json!({"status":"failed","error":null})
|
||||||
|
),
|
||||||
|
platform_llm::LlmError::InvalidRequest("codex-app-server-error:other".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn codex_app_server_rejects_non_responses_key_mapping() {
|
fn codex_app_server_rejects_non_responses_key_mapping() {
|
||||||
let mut llm = test_llm();
|
let mut llm = test_llm();
|
||||||
|
|||||||
@@ -547,7 +547,13 @@ pub(crate) fn game_creator_agent_llm_error_public_summary(
|
|||||||
(format!("upstream-{status_code}"), Some(*status_code))
|
(format!("upstream-{status_code}"), Some(*status_code))
|
||||||
}
|
}
|
||||||
platform_llm::LlmError::InvalidConfig(_) => ("invalid-config".to_string(), None),
|
platform_llm::LlmError::InvalidConfig(_) => ("invalid-config".to_string(), None),
|
||||||
platform_llm::LlmError::InvalidRequest(_) => ("invalid-request".to_string(), None),
|
platform_llm::LlmError::InvalidRequest(message) => (
|
||||||
|
message
|
||||||
|
.strip_prefix(GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX)
|
||||||
|
.map(|kind| format!("codex-app-server-{kind}"))
|
||||||
|
.unwrap_or_else(|| "invalid-request".to_string()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
platform_llm::LlmError::StreamUnavailable => ("stream-unavailable".to_string(), None),
|
platform_llm::LlmError::StreamUnavailable => ("stream-unavailable".to_string(), None),
|
||||||
platform_llm::LlmError::EmptyResponse => ("empty-response".to_string(), None),
|
platform_llm::LlmError::EmptyResponse => ("empty-response".to_string(), None),
|
||||||
platform_llm::LlmError::Deserialize(_) => ("deserialize".to_string(), None),
|
platform_llm::LlmError::Deserialize(_) => ("deserialize".to_string(), None),
|
||||||
|
|||||||
@@ -60,6 +60,25 @@ pub(super) fn game_creator_agent_background_final_reply_fallback(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn game_creator_agent_final_reply_error_allows_fallback(error: &str) -> bool {
|
||||||
|
const KIND_PREFIX: &str = "kind=";
|
||||||
|
let Some(start) = error.rfind(KIND_PREFIX) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if error[..start]
|
||||||
|
.chars()
|
||||||
|
.next_back()
|
||||||
|
.is_some_and(|boundary| !boundary.is_whitespace() && boundary != ':' && boundary != ':')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let kind = error[start + KIND_PREFIX.len()..]
|
||||||
|
.chars()
|
||||||
|
.take_while(|character| character.is_ascii_lowercase() || *character == '-')
|
||||||
|
.collect::<String>();
|
||||||
|
matches!(kind.as_str(), "empty-response" | "deserialize")
|
||||||
|
}
|
||||||
|
|
||||||
fn requested_game_chat_fast_path_plan_at(
|
fn requested_game_chat_fast_path_plan_at(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
plan: AgentRuntimeToolPlan,
|
plan: AgentRuntimeToolPlan,
|
||||||
@@ -3799,7 +3818,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
|||||||
{
|
{
|
||||||
return AgentBackgroundTaskOutcome::NeedsReconciliation;
|
return AgentBackgroundTaskOutcome::NeedsReconciliation;
|
||||||
}
|
}
|
||||||
Err(_) if final_reply_fallback.is_some() => {
|
Err(error)
|
||||||
|
if final_reply_fallback.is_some()
|
||||||
|
&& game_creator_agent_final_reply_error_allows_fallback(&error) =>
|
||||||
|
{
|
||||||
final_reply_fallback.expect("checked final reply fallback")
|
final_reply_fallback.expect("checked final reply fallback")
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@@ -3294,6 +3294,36 @@ fn plan_response_precedes_autonomous_supervisor_deterministic_fallback() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn autonomous_final_reply_fallback_only_accepts_safe_response_shape_failures() {
|
||||||
|
let fingerprint = "a".repeat(64);
|
||||||
|
for allowed in ["empty-response", "deserialize"] {
|
||||||
|
assert!(game_creator_agent_final_reply_error_allows_fallback(
|
||||||
|
&format!("后台 Agent 最终回复调用 LLM 失败:kind={allowed} fingerprint={fingerprint} chars=12")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for rejected in [
|
||||||
|
"codex-app-server-unauthorized",
|
||||||
|
"codex-app-server-usage-limit-exceeded",
|
||||||
|
"codex-app-server-context-window-exceeded",
|
||||||
|
"codex-app-server-cyber-policy",
|
||||||
|
"codex-app-server-sandbox-error",
|
||||||
|
"invalid-config",
|
||||||
|
"transport",
|
||||||
|
"upstream-503",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!game_creator_agent_final_reply_error_allows_fallback(&format!(
|
||||||
|
"后台 Agent 最终回复调用 LLM 失败:kind={rejected} fingerprint={fingerprint} chars=12"
|
||||||
|
)),
|
||||||
|
"final reply fallback must reject {rejected}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(!game_creator_agent_final_reply_error_allows_fallback(
|
||||||
|
"上游自由文本kind=deserialize fingerprint=private"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallback_once() {
|
async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallback_once() {
|
||||||
const RUN_ID: &str = "autonomous-final-reply-fallback-run";
|
const RUN_ID: &str = "autonomous-final-reply-fallback-run";
|
||||||
|
|||||||
@@ -204,6 +204,40 @@ pub(in crate::agent) fn game_creator_agent_runtime_failure_conversation_message(
|
|||||||
} else {
|
} else {
|
||||||
"专业 Agent"
|
"专业 Agent"
|
||||||
};
|
};
|
||||||
|
let public_kind_prefix = "kind=codex-app-server-";
|
||||||
|
let codex_error_kind = error
|
||||||
|
.rfind(public_kind_prefix)
|
||||||
|
.map(|start| &error[start + public_kind_prefix.len()..])
|
||||||
|
.or_else(|| {
|
||||||
|
error
|
||||||
|
.rfind(GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX)
|
||||||
|
.map(|start| {
|
||||||
|
&error[start + GAME_CREATOR_CODEX_APP_SERVER_ERROR_KIND_PREFIX.len()..]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|kind| {
|
||||||
|
kind.chars()
|
||||||
|
.take_while(|character| character.is_ascii_lowercase() || *character == '-')
|
||||||
|
.collect::<String>()
|
||||||
|
})
|
||||||
|
.filter(|kind| !kind.is_empty());
|
||||||
|
if let Some(kind) = codex_error_kind {
|
||||||
|
let detail = match kind
|
||||||
|
.trim_matches(|character: char| !character.is_ascii_lowercase() && character != '-')
|
||||||
|
{
|
||||||
|
"context-window-exceeded" => "模型上下文已超限,请缩小任务范围后重试",
|
||||||
|
"session-budget-exceeded" => "本次会话预算已耗尽,请缩小任务范围或新建任务",
|
||||||
|
"usage-limit-exceeded" => "Codex 用量已达上限,请检查账户额度后重试",
|
||||||
|
"unauthorized" => "Codex 鉴权失败,请重新登录或检查 API Key",
|
||||||
|
"bad-request" => "Codex 请求无效,请检查模型与运行时配置",
|
||||||
|
"cyber-policy" => "Codex 安全策略拒绝了本次请求,请调整任务内容",
|
||||||
|
"sandbox-error" => "Codex 隔离环境启动失败,请重试或检查本机环境",
|
||||||
|
"thread-rollback-failed" => "Codex 会话恢复失败,请新建任务后重试",
|
||||||
|
"active-turn-not-steerable" => "当前 Codex 任务无法追加指令,请等待结束后重试",
|
||||||
|
_ => "Codex 执行失败,请查看运行详情后重试",
|
||||||
|
};
|
||||||
|
return format!("{subject} {detail}");
|
||||||
|
}
|
||||||
if let Some((http_status, retry_attempt, max_retries)) =
|
if let Some((http_status, retry_attempt, max_retries)) =
|
||||||
game_creator_agent_runtime_exhausted_upstream_retry_fields(error)
|
game_creator_agent_runtime_exhausted_upstream_retry_fields(error)
|
||||||
{
|
{
|
||||||
@@ -1956,4 +1990,43 @@ mod tests {
|
|||||||
assert!(!unrelated_visible.contains("absolute-path"));
|
assert!(!unrelated_visible.contains("absolute-path"));
|
||||||
assert!(!unrelated_visible.contains("redacted-secret"));
|
assert!(!unrelated_visible.contains("redacted-secret"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codex_app_server_failure_kind_has_actionable_safe_public_summary() {
|
||||||
|
let private_error = format!(
|
||||||
|
"agentLlm.code-prototype 调用 LLM 失败:kind=codex-app-server-context-window-exceeded fingerprint={} chars=999",
|
||||||
|
"a".repeat(64)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game_creator_agent_runtime_failure_conversation_message(
|
||||||
|
"code-prototype",
|
||||||
|
&private_error,
|
||||||
|
),
|
||||||
|
"专业 Agent 模型上下文已超限,请缩小任务范围后重试"
|
||||||
|
);
|
||||||
|
let unauthorized = format!(
|
||||||
|
"kind=codex-app-server-unauthorized fingerprint={} chars=32",
|
||||||
|
"b".repeat(64)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game_creator_agent_runtime_failure_conversation_message(
|
||||||
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||||
|
&unauthorized,
|
||||||
|
),
|
||||||
|
"项目总控 Agent Codex 鉴权失败,请重新登录或检查 API Key"
|
||||||
|
);
|
||||||
|
for visible in [
|
||||||
|
game_creator_agent_runtime_failure_conversation_message(
|
||||||
|
"code-prototype",
|
||||||
|
&private_error,
|
||||||
|
),
|
||||||
|
game_creator_agent_runtime_failure_conversation_message(
|
||||||
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||||
|
&unauthorized,
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert!(!visible.contains("fingerprint"));
|
||||||
|
assert!(!visible.contains("chars="));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2599,6 +2599,21 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
let failure_detail = matches!(
|
||||||
|
event_type,
|
||||||
|
"error" | "turn.failed" | "turn.budget_exhausted"
|
||||||
|
)
|
||||||
|
.then(|| {
|
||||||
|
detail.map(|value| game_creator_agent_runtime_public_failure_detail(&state.agent_id, value))
|
||||||
|
})
|
||||||
|
.flatten();
|
||||||
|
let public_text = if matches!(event_type, "turn.failed" | "turn.budget_exhausted") {
|
||||||
|
failure_detail
|
||||||
|
.clone()
|
||||||
|
.or_else(|| game_creator_agent_runtime_public_event_text(root, event_type, summary))
|
||||||
|
} else {
|
||||||
|
game_creator_agent_runtime_public_event_text(root, event_type, summary)
|
||||||
|
};
|
||||||
let event = AgentRuntimeEvent {
|
let event = AgentRuntimeEvent {
|
||||||
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
||||||
agent_id: state.agent_id.clone(),
|
agent_id: state.agent_id.clone(),
|
||||||
@@ -2612,7 +2627,7 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action(
|
|||||||
status: status.to_string(),
|
status: status.to_string(),
|
||||||
phase: phase.to_string(),
|
phase: phase.to_string(),
|
||||||
summary: summary.to_string(),
|
summary: summary.to_string(),
|
||||||
public_text: game_creator_agent_runtime_public_event_text(root, event_type, summary),
|
public_text,
|
||||||
detail: detail
|
detail: detail
|
||||||
.filter(|_| {
|
.filter(|_| {
|
||||||
!(event_type == "observation"
|
!(event_type == "observation"
|
||||||
@@ -2624,10 +2639,9 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action(
|
|||||||
event_type,
|
event_type,
|
||||||
"error" | "turn.failed" | "turn.budget_exhausted"
|
"error" | "turn.failed" | "turn.budget_exhausted"
|
||||||
) {
|
) {
|
||||||
return game_creator_agent_runtime_public_failure_detail(
|
return failure_detail.clone().unwrap_or_else(|| {
|
||||||
&state.agent_id,
|
game_creator_agent_runtime_public_failure_detail(&state.agent_id, value)
|
||||||
value,
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let max_chars = if event_type == "observation"
|
let max_chars = if event_type == "observation"
|
||||||
&& summary.starts_with("agent.action_history:")
|
&& summary.starts_with("agent.action_history:")
|
||||||
|
|||||||
@@ -1628,17 +1628,23 @@ pub(crate) fn spawn_mock_llm_tool_plan_then_invalid_final_reply(
|
|||||||
.write_all(planning_response.as_bytes())
|
.write_all(planning_response.as_bytes())
|
||||||
.expect("mock tool plan response");
|
.expect("mock tool plan response");
|
||||||
|
|
||||||
let (mut final_stream, _) = listener.accept().expect("mock final reply accept");
|
// Autonomous runs enforce a 12-retry floor. Return the same malformed
|
||||||
drop(read_mock_http_request(&mut final_stream));
|
// response for the initial final-reply request and every retry so this
|
||||||
let invalid_body = "{invalid-json";
|
// fixture tests deserialize exhaustion rather than an accidental
|
||||||
let final_response = format!(
|
// connection-refused fallback after the first malformed response.
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
for _ in 0..=12 {
|
||||||
invalid_body.len(),
|
let (mut final_stream, _) = listener.accept().expect("mock final reply accept");
|
||||||
invalid_body
|
drop(read_mock_http_request(&mut final_stream));
|
||||||
);
|
let invalid_body = "{invalid-json";
|
||||||
final_stream
|
let final_response = format!(
|
||||||
.write_all(final_response.as_bytes())
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
.expect("mock invalid final reply response");
|
invalid_body.len(),
|
||||||
|
invalid_body
|
||||||
|
);
|
||||||
|
final_stream
|
||||||
|
.write_all(final_response.as_bytes())
|
||||||
|
.expect("mock invalid final reply response");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
base_url
|
base_url
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,12 +147,23 @@ fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() {
|
|||||||
"public-event-action-1",
|
"public-event-action-1",
|
||||||
)
|
)
|
||||||
.expect("repeat public action idempotently");
|
.expect("repeat public action idempotently");
|
||||||
|
append_game_creator_agent_runtime_action_event(
|
||||||
|
&root,
|
||||||
|
&state,
|
||||||
|
"turn.failed",
|
||||||
|
"failed",
|
||||||
|
"failed",
|
||||||
|
"Agent Runtime 本轮处理失败。",
|
||||||
|
Some("kind=codex-app-server-context-window-exceeded fingerprint=private"),
|
||||||
|
"public-event-failure-1",
|
||||||
|
)
|
||||||
|
.expect("append classified public failure");
|
||||||
|
|
||||||
let events = read_recent_game_creator_agent_runtime_events(
|
let events = read_recent_game_creator_agent_runtime_events(
|
||||||
&game_creator_agent_runtime_event_path(&root, "code-prototype"),
|
&game_creator_agent_runtime_event_path(&root, "code-prototype"),
|
||||||
)
|
)
|
||||||
.expect("read public runtime events");
|
.expect("read public runtime events");
|
||||||
assert_eq!(events.len(), 4);
|
assert_eq!(events.len(), 5);
|
||||||
assert!(events.iter().all(|event| !event.event_id.trim().is_empty()));
|
assert!(events.iter().all(|event| !event.event_id.trim().is_empty()));
|
||||||
let mut event_ids = events
|
let mut event_ids = events
|
||||||
.iter()
|
.iter()
|
||||||
@@ -176,6 +187,16 @@ fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.contains("raw tool input must stay private"));
|
.contains("raw tool input must stay private"));
|
||||||
|
assert_eq!(
|
||||||
|
events[4].public_text.as_deref(),
|
||||||
|
Some("专业 Agent 模型上下文已超限,请缩小任务范围后重试")
|
||||||
|
);
|
||||||
|
assert_eq!(events[4].detail, events[4].public_text);
|
||||||
|
assert!(!events[4]
|
||||||
|
.public_text
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("fingerprint"));
|
||||||
|
|
||||||
fs::remove_dir_all(root).ok();
|
fs::remove_dir_all(root).ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -846,6 +846,7 @@ export interface AgentStatusCard {
|
|||||||
runtimeWaitingOn: string | null;
|
runtimeWaitingOn: string | null;
|
||||||
runtimeNextStep: string | null;
|
runtimeNextStep: string | null;
|
||||||
runtimeTask: string | null;
|
runtimeTask: string | null;
|
||||||
|
runtimeError?: string | null;
|
||||||
runtimeRunId: string | null;
|
runtimeRunId: string | null;
|
||||||
runtimeLoopIteration: number | null;
|
runtimeLoopIteration: number | null;
|
||||||
runtimeMaxLoopIterations: number | null;
|
runtimeMaxLoopIterations: number | null;
|
||||||
|
|||||||
@@ -703,8 +703,16 @@ export function agentRuntimeStartedRunId(
|
|||||||
|
|
||||||
export function isAgentRuntimeTerminalState(runtime: AgentRuntimeState) {
|
export function isAgentRuntimeTerminalState(runtime: AgentRuntimeState) {
|
||||||
return (
|
return (
|
||||||
['completed', 'failed', 'cancelled'].includes(runtime.phase) ||
|
['completed', 'failed', 'cancelled', 'needs-reconciliation'].includes(
|
||||||
['completed', 'failed', 'cancelled', 'idle'].includes(runtime.status)
|
runtime.phase,
|
||||||
|
) ||
|
||||||
|
[
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'cancelled',
|
||||||
|
'idle',
|
||||||
|
'needs-reconciliation',
|
||||||
|
].includes(runtime.status)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,6 +981,12 @@ function agentRuntimeProviderRetryStatus(runtime: AgentRuntimeState) {
|
|||||||
|
|
||||||
export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) {
|
export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) {
|
||||||
if (isAgentRuntimeTerminalState(runtime)) {
|
if (isAgentRuntimeTerminalState(runtime)) {
|
||||||
|
if (
|
||||||
|
runtime.status === 'needs-reconciliation' ||
|
||||||
|
runtime.phase === 'needs-reconciliation'
|
||||||
|
) {
|
||||||
|
return 'Agent 运行状态需要核对,正在同步记录';
|
||||||
|
}
|
||||||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||||||
return 'Agent 运行失败,正在同步错误记录';
|
return 'Agent 运行失败,正在同步错误记录';
|
||||||
}
|
}
|
||||||
@@ -1017,6 +1031,14 @@ export function projectSupervisorChatRuntimeStatus(runtime: AgentRuntimeState) {
|
|||||||
if (runtime.status === 'idle' || runtime.phase === 'idle') {
|
if (runtime.status === 'idle' || runtime.phase === 'idle') {
|
||||||
return '等待输入';
|
return '等待输入';
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
runtime.status === 'needs-reconciliation' ||
|
||||||
|
runtime.phase === 'needs-reconciliation'
|
||||||
|
) {
|
||||||
|
return runtime.error
|
||||||
|
? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true)
|
||||||
|
: '项目总控 Agent 运行状态需要核对,请打开运行详情后重试';
|
||||||
|
}
|
||||||
if (isAgentRuntimeTerminalState(runtime)) {
|
if (isAgentRuntimeTerminalState(runtime)) {
|
||||||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||||||
return runtime.error
|
return runtime.error
|
||||||
@@ -1037,15 +1059,13 @@ export function formatAgentRuntimeEvent(event: AgentRuntimeEventRecord) {
|
|||||||
'turn.failed',
|
'turn.failed',
|
||||||
'turn.budget_exhausted',
|
'turn.budget_exhausted',
|
||||||
].includes(event.eventType);
|
].includes(event.eventType);
|
||||||
const containsInternalFailureDiagnostics = Boolean(
|
const visibleDetail = isFailureEvent ? null : event.detail;
|
||||||
event.detail &&
|
const publicFailureText =
|
||||||
/(?:errorSha256|errorChars|fingerprint|chars|retryAttempt|retryState)=|<(?:absolute-path|redacted-url)>|\[redacted(?:[- ]secret| sensitive context)\]/i.test(
|
isFailureEvent && typeof event.publicText === 'string'
|
||||||
event.detail,
|
? event.publicText.trim()
|
||||||
),
|
: '';
|
||||||
);
|
|
||||||
const visibleDetail =
|
|
||||||
isFailureEvent && containsInternalFailureDiagnostics ? null : event.detail;
|
|
||||||
const summary =
|
const summary =
|
||||||
|
publicFailureText ||
|
||||||
event.summary ||
|
event.summary ||
|
||||||
visibleDetail ||
|
visibleDetail ||
|
||||||
(isFailureEvent ? 'Agent Runtime 本轮处理失败。' : event.runId);
|
(isFailureEvent ? 'Agent Runtime 本轮处理失败。' : event.runId);
|
||||||
@@ -1365,11 +1385,16 @@ export function projectSupervisorRuntimeStatusLabel(
|
|||||||
runtime: AgentRuntimeState | null,
|
runtime: AgentRuntimeState | null,
|
||||||
runtimeError: string,
|
runtimeError: string,
|
||||||
) {
|
) {
|
||||||
|
if (
|
||||||
|
runtime?.status === 'needs-reconciliation' ||
|
||||||
|
runtime?.phase === 'needs-reconciliation'
|
||||||
|
) {
|
||||||
|
return '待核对';
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
runtimeError ||
|
runtimeError ||
|
||||||
runtime?.status === 'failed' ||
|
runtime?.status === 'failed' ||
|
||||||
runtime?.phase === 'failed' ||
|
runtime?.phase === 'failed'
|
||||||
runtime?.phase === 'needs-reconciliation'
|
|
||||||
) {
|
) {
|
||||||
return '失败';
|
return '失败';
|
||||||
}
|
}
|
||||||
@@ -1627,6 +1652,27 @@ export function projectRuntimeVisibleError(
|
|||||||
if (isMudPointInsufficientRuntimeError(message)) {
|
if (isMudPointInsufficientRuntimeError(message)) {
|
||||||
return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE;
|
return MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE;
|
||||||
}
|
}
|
||||||
|
const codexAppServerKind = visibleMessage.match(
|
||||||
|
/(?:^|[\s::])kind=codex-app-server-([a-z-]+)(?=\s|$)/,
|
||||||
|
)?.[1];
|
||||||
|
if (codexAppServerKind) {
|
||||||
|
const detail = {
|
||||||
|
'context-window-exceeded': '模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
'session-budget-exceeded': '本次会话预算已耗尽,请缩小任务范围或新建任务',
|
||||||
|
'usage-limit-exceeded': 'Codex 用量已达上限,请检查账户额度后重试',
|
||||||
|
unauthorized: 'Codex 鉴权失败,请重新登录或检查 API Key',
|
||||||
|
'bad-request': 'Codex 请求无效,请检查模型与运行时配置',
|
||||||
|
'cyber-policy': 'Codex 安全策略拒绝了本次请求,请调整任务内容',
|
||||||
|
'sandbox-error': 'Codex 隔离环境启动失败,请重试或检查本机环境',
|
||||||
|
'thread-rollback-failed': 'Codex 会话恢复失败,请新建任务后重试',
|
||||||
|
'active-turn-not-steerable':
|
||||||
|
'当前 Codex 任务无法追加指令,请等待结束后重试',
|
||||||
|
other: 'Codex 执行失败,请查看运行详情后重试',
|
||||||
|
}[codexAppServerKind];
|
||||||
|
if (detail) {
|
||||||
|
return `${subject} ${detail}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
const exhaustedUpstreamRetry = visibleMessage.match(
|
const exhaustedUpstreamRetry = visibleMessage.match(
|
||||||
/(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
|
/(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
|
||||||
);
|
);
|
||||||
@@ -1684,6 +1730,54 @@ export function projectRuntimeVisibleError(
|
|||||||
) {
|
) {
|
||||||
return `${subject} 服务繁忙,请稍后重试`;
|
return `${subject} 服务繁忙,请稍后重试`;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
normalized.includes('needs-reconciliation') ||
|
||||||
|
normalized.includes('result-unknown') ||
|
||||||
|
normalized.includes('终态未知') ||
|
||||||
|
normalized.includes('需要人工核对')
|
||||||
|
) {
|
||||||
|
return `${subject} 运行状态需要核对,请打开运行详情后重试`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized.includes('budget-exhausted') ||
|
||||||
|
normalized.includes('预算耗尽') ||
|
||||||
|
normalized.includes('预算已耗尽')
|
||||||
|
) {
|
||||||
|
return `${subject} 本轮预算已耗尽,请缩小任务范围后重试`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized.includes('missing expected artifact') ||
|
||||||
|
normalized.includes('expected artifact') ||
|
||||||
|
normalized.includes('缺少预期产物') ||
|
||||||
|
normalized.includes('缺少 expected artifact')
|
||||||
|
) {
|
||||||
|
return `${subject} 未生成要求的产物,请查看任务要求后重试`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized.includes('verification') ||
|
||||||
|
normalized.includes('project.verify') ||
|
||||||
|
normalized.includes('preview.validate') ||
|
||||||
|
normalized.includes('验证未通过') ||
|
||||||
|
normalized.includes('验证失败')
|
||||||
|
) {
|
||||||
|
return `${subject} 项目验证未通过,请查看运行详情并修复后重试`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized.includes('policy') ||
|
||||||
|
normalized.includes('permission') ||
|
||||||
|
normalized.includes('拒绝') ||
|
||||||
|
normalized.includes('禁止') ||
|
||||||
|
normalized.includes('不允许')
|
||||||
|
) {
|
||||||
|
return `${subject} 被项目权限或安全策略阻止,请检查审批配置`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
normalized.includes('落盘失败') ||
|
||||||
|
normalized.includes('持久化失败') ||
|
||||||
|
normalized.includes('写入失败')
|
||||||
|
) {
|
||||||
|
return `${subject} 保存运行记录失败,请检查项目目录后重试`;
|
||||||
|
}
|
||||||
const containsInternalDiagnostics =
|
const containsInternalDiagnostics =
|
||||||
normalized.includes('agentllm.') ||
|
normalized.includes('agentllm.') ||
|
||||||
/(?:^|[\s::])kind=/.test(normalized) ||
|
/(?:^|[\s::])kind=/.test(normalized) ||
|
||||||
@@ -1711,7 +1805,8 @@ export function projectRuntimeVisibleError(
|
|||||||
|
|
||||||
export function projectSupervisorVisibleConversationText(
|
export function projectSupervisorVisibleConversationText(
|
||||||
message: string,
|
message: string,
|
||||||
role: ChatMessage['role'] = 'assistant',
|
role: ChatMessage['role'] | 'tool' = 'assistant',
|
||||||
|
subject = '项目总控 Agent',
|
||||||
) {
|
) {
|
||||||
const failurePrefix = '后台任务失败:';
|
const failurePrefix = '后台任务失败:';
|
||||||
if (role !== 'assistant' || !message.startsWith(failurePrefix)) {
|
if (role !== 'assistant' || !message.startsWith(failurePrefix)) {
|
||||||
@@ -1719,7 +1814,7 @@ export function projectSupervisorVisibleConversationText(
|
|||||||
}
|
}
|
||||||
return projectRuntimeVisibleError(
|
return projectRuntimeVisibleError(
|
||||||
message.slice(failurePrefix.length),
|
message.slice(failurePrefix.length),
|
||||||
'项目总控 Agent',
|
subject,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1834,9 +1929,22 @@ export function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) {
|
|||||||
const goalSource = task.goalId
|
const goalSource = task.goalId
|
||||||
? `Goal ${task.goalStatus ?? '-'} · revision ${task.goalRevision ?? 0}`
|
? `Goal ${task.goalStatus ?? '-'} · revision ${task.goalRevision ?? 0}`
|
||||||
: null;
|
: null;
|
||||||
|
const failed =
|
||||||
|
task.status === 'failed' ||
|
||||||
|
['failed', 'budget-exhausted', 'needs-reconciliation'].includes(task.phase);
|
||||||
|
const failureDetail = failed
|
||||||
|
? task.terminalDetail?.trim() || task.error?.trim() || null
|
||||||
|
: null;
|
||||||
|
const failureSummary = failureDetail
|
||||||
|
? projectRuntimeVisibleError(
|
||||||
|
failureDetail,
|
||||||
|
projectProfessionalAgentLabel(task.agentId),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
return `${task.status} / ${task.phase} · ${
|
return `${task.status} / ${task.phase} · ${
|
||||||
task.task || task.currentAction || task.runId
|
task.task || task.currentAction || task.runId
|
||||||
}${goalSource ? ` · ${goalSource}` : ''}${
|
}${goalSource ? ` · ${goalSource}` : ''}${
|
||||||
delegationSource ? ` · ${delegationSource}` : ''
|
delegationSource ? ` · ${delegationSource}` : ''
|
||||||
}`;
|
}${failureSummary ? ` · 失败原因:${failureSummary}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import {
|
|||||||
formatAgentRuntimeTaskQueue,
|
formatAgentRuntimeTaskQueue,
|
||||||
isAgentRuntimeTerminalState,
|
isAgentRuntimeTerminalState,
|
||||||
projectRuntimeVisibleCurrentWork,
|
projectRuntimeVisibleCurrentWork,
|
||||||
|
projectRuntimeVisibleError,
|
||||||
|
projectProfessionalAgentLabel,
|
||||||
taskRowsFromManifest,
|
taskRowsFromManifest,
|
||||||
} from '../agent-runtime';
|
} from '../agent-runtime';
|
||||||
import {
|
import {
|
||||||
@@ -153,6 +155,26 @@ export function taskRowsForAgentStatus(
|
|||||||
return mergedTasks;
|
return mergedTasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function agentRuntimeFailureDetail(runtime: AgentRuntimeState) {
|
||||||
|
const runtimeError = runtime.error?.trim();
|
||||||
|
if (runtimeError) {
|
||||||
|
return runtimeError;
|
||||||
|
}
|
||||||
|
const terminalTask = [...(runtime.recentTasks ?? [])]
|
||||||
|
.reverse()
|
||||||
|
.find(
|
||||||
|
(task) =>
|
||||||
|
task.runId === runtime.runId &&
|
||||||
|
(task.status === 'failed' ||
|
||||||
|
['failed', 'budget-exhausted', 'needs-reconciliation'].includes(
|
||||||
|
task.phase,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
terminalTask?.terminalDetail?.trim() || terminalTask?.error?.trim() || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function deriveAgentStatusCards(
|
export function deriveAgentStatusCards(
|
||||||
nextManifest: GameCreationAppManifest,
|
nextManifest: GameCreationAppManifest,
|
||||||
trace: GameCreationAgentRunTrace | null,
|
trace: GameCreationAgentRunTrace | null,
|
||||||
@@ -204,6 +226,7 @@ export function deriveAgentStatusCards(
|
|||||||
? (runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase))
|
? (runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase))
|
||||||
: null,
|
: null,
|
||||||
runtimeTask: runtime?.currentTask ?? null,
|
runtimeTask: runtime?.currentTask ?? null,
|
||||||
|
runtimeError: runtime ? agentRuntimeFailureDetail(runtime) : null,
|
||||||
runtimeRunId: runtime?.runId ?? null,
|
runtimeRunId: runtime?.runId ?? null,
|
||||||
runtimeLoopIteration: runtime?.loopIteration ?? null,
|
runtimeLoopIteration: runtime?.loopIteration ?? null,
|
||||||
runtimeMaxLoopIterations: runtime?.maxLoopIterations ?? null,
|
runtimeMaxLoopIterations: runtime?.maxLoopIterations ?? null,
|
||||||
@@ -247,7 +270,12 @@ export function projectAgentRuntimeSummaries(
|
|||||||
) {
|
) {
|
||||||
return 4;
|
return 4;
|
||||||
}
|
}
|
||||||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
if (
|
||||||
|
runtime.status === 'failed' ||
|
||||||
|
runtime.phase === 'failed' ||
|
||||||
|
runtime.status === 'needs-reconciliation' ||
|
||||||
|
runtime.phase === 'needs-reconciliation'
|
||||||
|
) {
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
if (!isAgentRuntimeTerminalState(runtime)) {
|
if (!isAgentRuntimeTerminalState(runtime)) {
|
||||||
@@ -296,6 +324,9 @@ export function projectAgentRuntimeSummaries(
|
|||||||
runtime.phase === 'waiting-for-confirmation',
|
runtime.phase === 'waiting-for-confirmation',
|
||||||
);
|
);
|
||||||
const failed = runtime.status === 'failed' || runtime.phase === 'failed';
|
const failed = runtime.status === 'failed' || runtime.phase === 'failed';
|
||||||
|
const needsReconciliation =
|
||||||
|
runtime.status === 'needs-reconciliation' ||
|
||||||
|
runtime.phase === 'needs-reconciliation';
|
||||||
const completed =
|
const completed =
|
||||||
runtime.status === 'completed' || runtime.phase === 'completed';
|
runtime.status === 'completed' || runtime.phase === 'completed';
|
||||||
const cancelled =
|
const cancelled =
|
||||||
@@ -304,7 +335,7 @@ export function projectAgentRuntimeSummaries(
|
|||||||
const status: ProjectAgentRuntimeSummary['status'] =
|
const status: ProjectAgentRuntimeSummary['status'] =
|
||||||
waitingForInput || waitingForConfirmation
|
waitingForInput || waitingForConfirmation
|
||||||
? 'waiting'
|
? 'waiting'
|
||||||
: failed
|
: failed || needsReconciliation
|
||||||
? 'failed'
|
? 'failed'
|
||||||
: completed
|
: completed
|
||||||
? 'completed'
|
? 'completed'
|
||||||
@@ -322,17 +353,28 @@ export function projectAgentRuntimeSummaries(
|
|||||||
? '待回答'
|
? '待回答'
|
||||||
: waitingForConfirmation
|
: waitingForConfirmation
|
||||||
? '待确认'
|
? '待确认'
|
||||||
: failed
|
: needsReconciliation
|
||||||
? '失败'
|
? '待核对'
|
||||||
: completed
|
: failed
|
||||||
? '已完成'
|
? '失败'
|
||||||
: cancelled
|
: completed
|
||||||
? '已取消'
|
? '已完成'
|
||||||
: idle
|
: cancelled
|
||||||
? '等待中'
|
? '已取消'
|
||||||
: runtime.phase === 'planning'
|
: idle
|
||||||
? '分析中'
|
? '等待中'
|
||||||
: '工作中';
|
: runtime.phase === 'planning'
|
||||||
|
? '分析中'
|
||||||
|
: '工作中';
|
||||||
|
const failureDetail = agentRuntimeFailureDetail(runtime);
|
||||||
|
const failureSummary =
|
||||||
|
(failed || needsReconciliation) && failureDetail
|
||||||
|
? projectRuntimeVisibleError(
|
||||||
|
failureDetail,
|
||||||
|
projectProfessionalAgentLabel(runtime.agentId),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -340,6 +382,7 @@ export function projectAgentRuntimeSummaries(
|
|||||||
label,
|
label,
|
||||||
status,
|
status,
|
||||||
statusLabel,
|
statusLabel,
|
||||||
|
failureSummary,
|
||||||
currentTask:
|
currentTask:
|
||||||
(activePlanStep && agentRuntimePlanStepText(activePlanStep)) ||
|
(activePlanStep && agentRuntimePlanStepText(activePlanStep)) ||
|
||||||
projectRuntimeVisibleCurrentWork(runtime),
|
projectRuntimeVisibleCurrentWork(runtime),
|
||||||
@@ -387,6 +430,7 @@ export function sameAgentStatusCard(
|
|||||||
left.runtimeWaitingOn === right.runtimeWaitingOn &&
|
left.runtimeWaitingOn === right.runtimeWaitingOn &&
|
||||||
left.runtimeNextStep === right.runtimeNextStep &&
|
left.runtimeNextStep === right.runtimeNextStep &&
|
||||||
left.runtimeTask === right.runtimeTask &&
|
left.runtimeTask === right.runtimeTask &&
|
||||||
|
left.runtimeError === right.runtimeError &&
|
||||||
left.runtimeRunId === right.runtimeRunId &&
|
left.runtimeRunId === right.runtimeRunId &&
|
||||||
left.runtimeLoopIteration === right.runtimeLoopIteration &&
|
left.runtimeLoopIteration === right.runtimeLoopIteration &&
|
||||||
left.runtimeMaxLoopIterations === right.runtimeMaxLoopIterations &&
|
left.runtimeMaxLoopIterations === right.runtimeMaxLoopIterations &&
|
||||||
@@ -455,6 +499,7 @@ export function sameAgentRuntimeTasks(
|
|||||||
task.task === other.task &&
|
task.task === other.task &&
|
||||||
task.currentAction === other.currentAction &&
|
task.currentAction === other.currentAction &&
|
||||||
task.terminalDetail === other.terminalDetail &&
|
task.terminalDetail === other.terminalDetail &&
|
||||||
|
task.error === other.error &&
|
||||||
task.updatedAt === other.updatedAt
|
task.updatedAt === other.updatedAt
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -901,8 +946,22 @@ export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) {
|
|||||||
agent.runtimeMaxLoopIterations ?? 3
|
agent.runtimeMaxLoopIterations ?? 3
|
||||||
}`
|
}`
|
||||||
: null;
|
: null;
|
||||||
|
const needsAttention =
|
||||||
|
agent.runtimeStatus === 'failed' ||
|
||||||
|
agent.runtimePhase === 'failed' ||
|
||||||
|
agent.runtimeStatus === 'needs-reconciliation' ||
|
||||||
|
agent.runtimePhase === 'needs-reconciliation';
|
||||||
|
const failureSummary =
|
||||||
|
needsAttention && agent.runtimeError
|
||||||
|
? projectRuntimeVisibleError(
|
||||||
|
agent.runtimeError,
|
||||||
|
projectProfessionalAgentLabel(agent.id),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
return [
|
return [
|
||||||
`Runtime:${agent.runtimeStatus} / ${agent.runtimePhase ?? '-'}`,
|
`Runtime:${agent.runtimeStatus} / ${agent.runtimePhase ?? '-'}`,
|
||||||
|
failureSummary,
|
||||||
loopProgress,
|
loopProgress,
|
||||||
agent.runtimeAction,
|
agent.runtimeAction,
|
||||||
agent.runtimeWaitingOn ? `等待 ${agent.runtimeWaitingOn}` : null,
|
agent.runtimeWaitingOn ? `等待 ${agent.runtimeWaitingOn}` : null,
|
||||||
|
|||||||
+10
-2
@@ -16,7 +16,11 @@ import type {
|
|||||||
AgentStatusCard,
|
AgentStatusCard,
|
||||||
LocalConversationMessageRecord,
|
LocalConversationMessageRecord,
|
||||||
} from '../../app/types';
|
} from '../../app/types';
|
||||||
import { AgentRuntimeStatusPanel } from '../agent-runtime';
|
import {
|
||||||
|
AgentRuntimeStatusPanel,
|
||||||
|
projectProfessionalAgentLabel,
|
||||||
|
projectSupervisorVisibleConversationText,
|
||||||
|
} from '../agent-runtime';
|
||||||
import { commandDraftFromSuggestedToolCall } from '../project-summary/agentPresentation';
|
import { commandDraftFromSuggestedToolCall } from '../project-summary/agentPresentation';
|
||||||
import {
|
import {
|
||||||
agentTaskGraphStateLabels,
|
agentTaskGraphStateLabels,
|
||||||
@@ -268,7 +272,11 @@ export function AgentConversationOverlay({
|
|||||||
key={`${message.updatedAt}-${index}`}
|
key={`${message.updatedAt}-${index}`}
|
||||||
className={`message message--${message.role}`}
|
className={`message message--${message.role}`}
|
||||||
>
|
>
|
||||||
{message.content}
|
{projectSupervisorVisibleConversationText(
|
||||||
|
message.content,
|
||||||
|
message.role,
|
||||||
|
projectProfessionalAgentLabel(selectedAgent.id),
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+16
-1
@@ -14,6 +14,9 @@ import type {
|
|||||||
} from '../../app/types';
|
} from '../../app/types';
|
||||||
import {
|
import {
|
||||||
ProjectSupervisorRuntimePanel,
|
ProjectSupervisorRuntimePanel,
|
||||||
|
projectProfessionalAgentLabel,
|
||||||
|
projectRuntimeVisibleError,
|
||||||
|
projectSupervisorVisibleConversationText,
|
||||||
projectWorkspaceStatusForDisplay,
|
projectWorkspaceStatusForDisplay,
|
||||||
} from '../agent-runtime';
|
} from '../agent-runtime';
|
||||||
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
|
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
|
||||||
@@ -77,7 +80,10 @@ export function ProjectSupervisorView({
|
|||||||
key={message.messageId ?? `${message.role}-${index}`}
|
key={message.messageId ?? `${message.role}-${index}`}
|
||||||
className={`message message--${message.role}`}
|
className={`message message--${message.role}`}
|
||||||
>
|
>
|
||||||
{message.text}
|
{projectSupervisorVisibleConversationText(
|
||||||
|
message.text,
|
||||||
|
message.role,
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
{transientReply ? (
|
{transientReply ? (
|
||||||
@@ -151,6 +157,15 @@ export function ProjectSupervisorView({
|
|||||||
taskStatusLabels[agent.status]}
|
taskStatusLabels[agent.status]}
|
||||||
</span>
|
</span>
|
||||||
{agent.runtimeTask ? <small>{agent.runtimeTask}</small> : null}
|
{agent.runtimeTask ? <small>{agent.runtimeTask}</small> : null}
|
||||||
|
{agent.runtimeError ? (
|
||||||
|
<small>
|
||||||
|
{projectRuntimeVisibleError(
|
||||||
|
agent.runtimeError,
|
||||||
|
projectProfessionalAgentLabel(agent.id),
|
||||||
|
true,
|
||||||
|
)}
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+27
-2
@@ -303,7 +303,23 @@ export function formatGameChatStageRecord(
|
|||||||
) {
|
) {
|
||||||
const terminalStatus =
|
const terminalStatus =
|
||||||
runtime.status === 'failed' || runtime.phase === 'failed'
|
runtime.status === 'failed' || runtime.phase === 'failed'
|
||||||
? progress.interruptionText || '本轮失败'
|
? progress.interruptionText ||
|
||||||
|
(runtime.error
|
||||||
|
? projectRuntimeVisibleError(
|
||||||
|
runtime.error,
|
||||||
|
'项目总控 Agent',
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: '本轮失败')
|
||||||
|
: runtime.status === 'needs-reconciliation' ||
|
||||||
|
runtime.phase === 'needs-reconciliation'
|
||||||
|
? runtime.error
|
||||||
|
? projectRuntimeVisibleError(
|
||||||
|
runtime.error,
|
||||||
|
'项目总控 Agent',
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
: '项目总控 Agent 运行状态需要核对,请打开运行详情后重试'
|
||||||
: runtime.status === 'cancelled' || runtime.phase === 'cancelled'
|
: runtime.status === 'cancelled' || runtime.phase === 'cancelled'
|
||||||
? '本轮已取消'
|
? '本轮已取消'
|
||||||
: runtime.status === 'completed' || runtime.phase === 'completed'
|
: runtime.status === 'completed' || runtime.phase === 'completed'
|
||||||
@@ -971,7 +987,10 @@ export function SupervisorChatOnlyView({
|
|||||||
const running = Boolean(
|
const running = Boolean(
|
||||||
chatAgentBusy ||
|
chatAgentBusy ||
|
||||||
synchronizingAcceptedRun ||
|
synchronizingAcceptedRun ||
|
||||||
(runtime && !isAgentRuntimeTerminalState(runtime)),
|
(runtime &&
|
||||||
|
runtime.status !== 'needs-reconciliation' &&
|
||||||
|
runtime.phase !== 'needs-reconciliation' &&
|
||||||
|
!isAgentRuntimeTerminalState(runtime)),
|
||||||
);
|
);
|
||||||
const gameChatInterruptionText =
|
const gameChatInterruptionText =
|
||||||
gameChatMode && runtime
|
gameChatMode && runtime
|
||||||
@@ -1099,6 +1118,12 @@ export function SupervisorChatOnlyView({
|
|||||||
if (runtimeError) {
|
if (runtimeError) {
|
||||||
return '运行异常';
|
return '运行异常';
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
runtime?.status === 'needs-reconciliation' ||
|
||||||
|
runtime?.phase === 'needs-reconciliation'
|
||||||
|
) {
|
||||||
|
return '待人工核对';
|
||||||
|
}
|
||||||
if (synchronizingAcceptedRun) {
|
if (synchronizingAcceptedRun) {
|
||||||
return '正在启动';
|
return '正在启动';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export type ProjectAgentRuntimeSummary = {
|
|||||||
label: string;
|
label: string;
|
||||||
status: 'pending' | 'running' | 'waiting' | 'completed' | 'failed';
|
status: 'pending' | 'running' | 'waiting' | 'completed' | 'failed';
|
||||||
statusLabel: string;
|
statusLabel: string;
|
||||||
|
failureSummary?: string | null;
|
||||||
currentTask: string;
|
currentTask: string;
|
||||||
currentAction: string | null;
|
currentAction: string | null;
|
||||||
waitingOn: string | null;
|
waitingOn: string | null;
|
||||||
@@ -375,6 +376,7 @@ function summarizeAgent(
|
|||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
failed: '需处理',
|
failed: '需处理',
|
||||||
}[status],
|
}[status],
|
||||||
|
failureSummary: null,
|
||||||
currentTask:
|
currentTask:
|
||||||
activeTask?.title ??
|
activeTask?.title ??
|
||||||
(completedCount > 0 ? '本轮任务已完成' : '等待项目总控分配任务'),
|
(completedCount > 0 ? '本轮任务已完成' : '等待项目总控分配任务'),
|
||||||
@@ -3794,7 +3796,7 @@ export default function ProjectDevelopmentView({
|
|||||||
<article
|
<article
|
||||||
className={`game-agent-dock-item is-${agent.status}`}
|
className={`game-agent-dock-item is-${agent.status}`}
|
||||||
key={agent.group}
|
key={agent.group}
|
||||||
aria-label={`${agent.label}:${agent.statusLabel}`}
|
aria-label={`${agent.label}:${agent.failureSummary ?? agent.statusLabel}`}
|
||||||
aria-describedby={`agent-detail-${agent.group}`}
|
aria-describedby={`agent-detail-${agent.group}`}
|
||||||
>
|
>
|
||||||
<span className="game-agent-avatar">
|
<span className="game-agent-avatar">
|
||||||
@@ -3811,7 +3813,7 @@ export default function ProjectDevelopmentView({
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{agent.label}</strong>
|
<strong>{agent.label}</strong>
|
||||||
<small>{agent.statusLabel}</small>
|
<small>{agent.failureSummary ?? agent.statusLabel}</small>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="game-agent-dock-detail"
|
className="game-agent-dock-detail"
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import type {
|
|||||||
AgentRuntimeResponseStream,
|
AgentRuntimeResponseStream,
|
||||||
AgentRuntimeResult,
|
AgentRuntimeResult,
|
||||||
AgentRuntimeState,
|
AgentRuntimeState,
|
||||||
|
AgentRuntimeTaskRecord,
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
LocalConversationMessageRecord,
|
LocalConversationMessageRecord,
|
||||||
} from '../src/app/types';
|
} from '../src/app/types';
|
||||||
import {
|
import {
|
||||||
formatAgentRuntimeEvent,
|
formatAgentRuntimeEvent,
|
||||||
|
formatAgentRecentRuntimeTask,
|
||||||
|
isAgentRuntimeTerminalState,
|
||||||
|
agentRuntimeConversationStatus,
|
||||||
mergeGameChatRuntimeResponseMessagesIntoHistory,
|
mergeGameChatRuntimeResponseMessagesIntoHistory,
|
||||||
mergeProjectSupervisorConversation,
|
mergeProjectSupervisorConversation,
|
||||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||||
@@ -20,6 +24,11 @@ import {
|
|||||||
projectWorkspaceStatusForDisplay,
|
projectWorkspaceStatusForDisplay,
|
||||||
submitProjectSupervisorRuntimeTask,
|
submitProjectSupervisorRuntimeTask,
|
||||||
} from '../src/features/agent-runtime/model';
|
} from '../src/features/agent-runtime/model';
|
||||||
|
import {
|
||||||
|
deriveAgentStatusCards,
|
||||||
|
formatAgentCardRuntimeStatus,
|
||||||
|
} from '../src/features/project-summary/agentPresentation';
|
||||||
|
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||||
|
|
||||||
describe('普通用户工作区状态', () => {
|
describe('普通用户工作区状态', () => {
|
||||||
test('用项目名称替代 Unix 和 Windows 绝对路径', () => {
|
test('用项目名称替代 Unix 和 Windows 绝对路径', () => {
|
||||||
@@ -35,7 +44,105 @@ describe('普通用户工作区状态', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Agent 最近任务失败摘要', () => {
|
||||||
|
test('展示安全可行动原因且不透传私有诊断', () => {
|
||||||
|
const task: AgentRuntimeTaskRecord = {
|
||||||
|
schemaVersion: 'game-creator-agent-runtime-task.v1',
|
||||||
|
agentId: 'code-prototype',
|
||||||
|
taskId: 'code-prototype',
|
||||||
|
sessionId: 'session-code',
|
||||||
|
runId: 'run-code-failed',
|
||||||
|
source: 'agent-delegate',
|
||||||
|
task: '实现首个可玩版本',
|
||||||
|
status: 'failed',
|
||||||
|
phase: 'failed',
|
||||||
|
currentAction: '等待开发者处理失败',
|
||||||
|
terminalDetail:
|
||||||
|
'kind=codex-app-server-context-window-exceeded fingerprint=' +
|
||||||
|
'a'.repeat(64),
|
||||||
|
error: 'private provider body https://provider.example/api?key=secret',
|
||||||
|
updatedAt: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const visible = formatAgentRecentRuntimeTask(task);
|
||||||
|
expect(visible).toContain(
|
||||||
|
'失败原因:程序原型 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
);
|
||||||
|
expect(visible).not.toContain('fingerprint');
|
||||||
|
expect(visible).not.toContain('provider.example');
|
||||||
|
expect(visible).not.toContain('secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('状态卡在 Runtime error 为空时使用当前 Run 的终态任务原因', () => {
|
||||||
|
const terminalDetail =
|
||||||
|
'kind=codex-app-server-context-window-exceeded fingerprint=' +
|
||||||
|
'b'.repeat(64);
|
||||||
|
const manifest = createGameCreationAppManifest(
|
||||||
|
'local-project-draft',
|
||||||
|
'failure-card-game',
|
||||||
|
);
|
||||||
|
const cards = deriveAgentStatusCards(manifest, null, {
|
||||||
|
'code-prototype': {
|
||||||
|
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||||
|
agentId: 'code-prototype',
|
||||||
|
taskId: 'code-prototype',
|
||||||
|
sessionId: 'session-code',
|
||||||
|
runId: 'run-code-failed',
|
||||||
|
source: 'agent-delegate',
|
||||||
|
status: 'failed',
|
||||||
|
phase: 'failed',
|
||||||
|
currentTask: '实现首个可玩版本',
|
||||||
|
plan: [],
|
||||||
|
observations: [],
|
||||||
|
allowedTools: [],
|
||||||
|
error: null,
|
||||||
|
updatedAt: 2,
|
||||||
|
recentTasks: [
|
||||||
|
{
|
||||||
|
schemaVersion: 'game-creator-agent-runtime-task.v1',
|
||||||
|
agentId: 'code-prototype',
|
||||||
|
taskId: 'code-prototype',
|
||||||
|
sessionId: 'session-code',
|
||||||
|
runId: 'run-code-failed',
|
||||||
|
source: 'agent-delegate',
|
||||||
|
task: '实现首个可玩版本',
|
||||||
|
status: 'failed',
|
||||||
|
phase: 'failed',
|
||||||
|
currentAction: '等待开发者处理失败',
|
||||||
|
terminalDetail,
|
||||||
|
error: null,
|
||||||
|
updatedAt: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const card = cards.find((candidate) => candidate.id === 'code-prototype');
|
||||||
|
expect(card?.runtimeError).toBe(terminalDetail);
|
||||||
|
expect(formatAgentCardRuntimeStatus(card!)).toContain(
|
||||||
|
'程序原型 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
);
|
||||||
|
expect(formatAgentCardRuntimeStatus(card!)).not.toContain('fingerprint');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Runtime-owned public statuses', () => {
|
describe('Runtime-owned public statuses', () => {
|
||||||
|
test('treats needs-reconciliation as a terminal state that requires manual resolution', () => {
|
||||||
|
const runtime = {
|
||||||
|
...providerRetryRuntime(),
|
||||||
|
status: 'needs-reconciliation',
|
||||||
|
phase: 'needs-reconciliation',
|
||||||
|
error: 'result-unknown,需要人工核对',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(isAgentRuntimeTerminalState(runtime)).toBe(true);
|
||||||
|
expect(agentRuntimeConversationStatus(runtime)).toBe(
|
||||||
|
'Agent 运行状态需要核对,正在同步记录',
|
||||||
|
);
|
||||||
|
expect(projectSupervisorChatRuntimeStatus(runtime)).toBe(
|
||||||
|
'项目总控 Agent 运行状态需要核对,请打开运行详情后重试',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('keeps backend status messages visible without treating them as client-authored conversation', () => {
|
test('keeps backend status messages visible without treating them as client-authored conversation', () => {
|
||||||
const projectRecords: LocalConversationMessageRecord[] = [
|
const projectRecords: LocalConversationMessageRecord[] = [
|
||||||
{
|
{
|
||||||
@@ -626,6 +733,54 @@ describe('Agent Runtime Provider 状态投影', () => {
|
|||||||
).toBe('项目总控 Agent 执行失败,请稍后重试');
|
).toBe('项目总控 Agent 执行失败,请稍后重试');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('展示 Codex app-server 稳定失败分类且隐藏内部诊断', () => {
|
||||||
|
const fingerprint = 'e'.repeat(64);
|
||||||
|
const contextError =
|
||||||
|
`agentLlm.code-prototype 调用 LLM 失败:` +
|
||||||
|
`kind=codex-app-server-context-window-exceeded ` +
|
||||||
|
`fingerprint=${fingerprint} chars=2048`;
|
||||||
|
const visible = projectRuntimeVisibleError(
|
||||||
|
contextError,
|
||||||
|
'程序原型 Agent',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(visible).toBe(
|
||||||
|
'程序原型 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
);
|
||||||
|
expect(visible).not.toContain('fingerprint');
|
||||||
|
expect(visible).not.toContain(fingerprint);
|
||||||
|
expect(
|
||||||
|
projectRuntimeVisibleError(
|
||||||
|
`kind=codex-app-server-unauthorized fingerprint=${fingerprint} chars=1`,
|
||||||
|
'项目总控 Agent',
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toBe('项目总控 Agent Codex 鉴权失败,请重新登录或检查 API Key');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('把常见 Runtime 失败映射为可行动原因', () => {
|
||||||
|
expect(
|
||||||
|
projectRuntimeVisibleError(
|
||||||
|
'动态隔离子 Agent 缺少 expected artifact:game/index.html',
|
||||||
|
'程序 Agent',
|
||||||
|
),
|
||||||
|
).toBe('程序 Agent 未生成要求的产物,请查看任务要求后重试');
|
||||||
|
expect(
|
||||||
|
projectRuntimeVisibleError('project.verify 验证失败', '程序 Agent'),
|
||||||
|
).toBe('程序 Agent 项目验证未通过,请查看运行详情并修复后重试');
|
||||||
|
expect(
|
||||||
|
projectRuntimeVisibleError('Agent loop 预算耗尽', '程序 Agent'),
|
||||||
|
).toBe('程序 Agent 本轮预算已耗尽,请缩小任务范围后重试');
|
||||||
|
expect(
|
||||||
|
projectSupervisorChatRuntimeStatus({
|
||||||
|
...providerRetryRuntime(),
|
||||||
|
status: 'needs-reconciliation',
|
||||||
|
phase: 'needs-reconciliation',
|
||||||
|
error: 'result-unknown,需要人工核对',
|
||||||
|
}),
|
||||||
|
).toBe('项目总控 Agent 运行状态需要核对,请打开运行详情后重试');
|
||||||
|
});
|
||||||
|
|
||||||
test('隐藏旧失败对话与事件中的内部诊断标记', () => {
|
test('隐藏旧失败对话与事件中的内部诊断标记', () => {
|
||||||
const fingerprint = 'd'.repeat(64);
|
const fingerprint = 'd'.repeat(64);
|
||||||
const legacyConversation =
|
const legacyConversation =
|
||||||
@@ -638,6 +793,13 @@ describe('Agent Runtime Provider 状态投影', () => {
|
|||||||
expect(
|
expect(
|
||||||
projectSupervisorVisibleConversationText(legacyConversation, 'user'),
|
projectSupervisorVisibleConversationText(legacyConversation, 'user'),
|
||||||
).toBe(legacyConversation);
|
).toBe(legacyConversation);
|
||||||
|
expect(
|
||||||
|
projectSupervisorVisibleConversationText(
|
||||||
|
legacyConversation,
|
||||||
|
'assistant',
|
||||||
|
'策划 Agent',
|
||||||
|
),
|
||||||
|
).toBe('策划 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3)');
|
||||||
|
|
||||||
const formattedEvent = formatAgentRuntimeEvent({
|
const formattedEvent = formatAgentRuntimeEvent({
|
||||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||||
@@ -650,17 +812,35 @@ describe('Agent Runtime Provider 状态投影', () => {
|
|||||||
status: 'failed',
|
status: 'failed',
|
||||||
phase: 'failed',
|
phase: 'failed',
|
||||||
summary: 'Agent Runtime 本轮处理失败。',
|
summary: 'Agent Runtime 本轮处理失败。',
|
||||||
|
publicText: '项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
detail:
|
detail:
|
||||||
`<absolute-path> [redacted sensitive context] ` +
|
`<absolute-path> [redacted sensitive context] ` +
|
||||||
`errorSha256=${fingerprint} · errorChars=99`,
|
`errorSha256=${fingerprint} · errorChars=99`,
|
||||||
updatedAt: 1,
|
updatedAt: 1,
|
||||||
});
|
});
|
||||||
expect(formattedEvent).toBe(
|
expect(formattedEvent).toBe(
|
||||||
'turn.failed · failed / failed · Agent Runtime 本轮处理失败。',
|
'turn.failed · failed / failed · 项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
);
|
);
|
||||||
expect(formattedEvent).not.toContain('errorSha256');
|
expect(formattedEvent).not.toContain('errorSha256');
|
||||||
expect(formattedEvent).not.toContain(fingerprint);
|
expect(formattedEvent).not.toContain(fingerprint);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
formatAgentRuntimeEvent({
|
||||||
|
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||||
|
agentId: 'project-supervisor',
|
||||||
|
taskId: 'legacy-private-detail',
|
||||||
|
sessionId: 'legacy-private-detail-session',
|
||||||
|
runId: 'legacy-private-detail-run',
|
||||||
|
source: 'project-supervisor-chat',
|
||||||
|
eventType: 'error',
|
||||||
|
status: 'failed',
|
||||||
|
phase: 'failed',
|
||||||
|
summary: 'Agent Runtime 本轮处理失败。',
|
||||||
|
detail: 'private provider body without stable diagnostics marker',
|
||||||
|
updatedAt: 1,
|
||||||
|
}),
|
||||||
|
).toBe('error · failed / failed · Agent Runtime 本轮处理失败。');
|
||||||
|
|
||||||
const emptySummaryEvent = formatAgentRuntimeEvent({
|
const emptySummaryEvent = formatAgentRuntimeEvent({
|
||||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||||
agentId: 'project-supervisor',
|
agentId: 'project-supervisor',
|
||||||
|
|||||||
@@ -1909,9 +1909,10 @@ export function registerDeveloperAgentWindowTests() {
|
|||||||
expect(screen.getByText('最近事件')).not.toBeNull();
|
expect(screen.getByText('最近事件')).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
'error · failed / failed · Agent Runtime 本轮处理失败。 · 最终回复调用失败',
|
'error · failed / failed · Agent Runtime 本轮处理失败。',
|
||||||
),
|
),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
|
expect(screen.queryByText(/最终回复调用失败/u)).toBeNull();
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getByText(
|
||||||
'observation · running / action · file.read 返回项目笔记 · 已读取 game/notes.txt',
|
'observation · running / action · file.read 返回项目笔记 · 已读取 game/notes.txt',
|
||||||
@@ -5474,9 +5475,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
|
|
||||||
@@ -5602,9 +5601,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
invoke.mockClear();
|
invoke.mockClear();
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
@@ -5685,9 +5682,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
invoke.mockClear();
|
invoke.mockClear();
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
@@ -5799,9 +5794,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
invoke.mockClear();
|
invoke.mockClear();
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
@@ -5897,9 +5890,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
|
|
||||||
@@ -5959,9 +5950,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
|
|
||||||
@@ -6010,9 +5999,7 @@ export function registerDeveloperToolsTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
submitChat('/audit');
|
submitChat('/audit');
|
||||||
|
|
||||||
|
|||||||
@@ -415,9 +415,7 @@ export function registerProjectConversationTests() {
|
|||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
|
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
|
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
|
||||||
projectPath: '/tmp/authorized-game',
|
projectPath: '/tmp/authorized-game',
|
||||||
@@ -1232,9 +1230,7 @@ export function registerProjectConversationTests() {
|
|||||||
|
|
||||||
submitChat('/project /tmp/authorized-game');
|
submitChat('/project /tmp/authorized-game');
|
||||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||||
expect(
|
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||||
await screen.findByText('已打开:authorized-game'),
|
|
||||||
).not.toBeNull();
|
|
||||||
|
|
||||||
expect(await screen.findByText('正在拆解创作方向')).not.toBeNull();
|
expect(await screen.findByText('正在拆解创作方向')).not.toBeNull();
|
||||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||||
@@ -1330,8 +1326,24 @@ export function registerProjectConversationTests() {
|
|||||||
agentId: 'design-director',
|
agentId: 'design-director',
|
||||||
updatedAt: 2,
|
updatedAt: 2,
|
||||||
});
|
});
|
||||||
|
agentMessages.push({
|
||||||
|
schemaVersion: 'game-creator-conversation.v1',
|
||||||
|
role: 'assistant',
|
||||||
|
content:
|
||||||
|
'后台任务失败:kind=codex-app-server-context-window-exceeded ' +
|
||||||
|
`fingerprint=${'c'.repeat(64)} chars=2048`,
|
||||||
|
agentId: 'design-director',
|
||||||
|
updatedAt: 3,
|
||||||
|
});
|
||||||
fireEvent.click(within(agentDialog).getByRole('button', { name: '刷新' }));
|
fireEvent.click(within(agentDialog).getByRole('button', { name: '刷新' }));
|
||||||
expect(await screen.findByText('刷新后外部记录')).not.toBeNull();
|
expect(await screen.findByText('刷新后外部记录')).not.toBeNull();
|
||||||
|
expect(
|
||||||
|
await screen.findByText(
|
||||||
|
'策划 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
),
|
||||||
|
).not.toBeNull();
|
||||||
|
expect(agentDialog.textContent).not.toContain('fingerprint');
|
||||||
|
expect(agentDialog.textContent).not.toContain('chars=');
|
||||||
expect(agentConversationReadCount).toBeGreaterThanOrEqual(2);
|
expect(agentConversationReadCount).toBeGreaterThanOrEqual(2);
|
||||||
fireEvent.click(screen.getByRole('button', { name: '填入同步命令' }));
|
fireEvent.click(screen.getByRole('button', { name: '填入同步命令' }));
|
||||||
expect(screen.queryByLabelText('Agent 对话')).toBeNull();
|
expect(screen.queryByLabelText('Agent 对话')).toBeNull();
|
||||||
|
|||||||
@@ -3965,6 +3965,67 @@ export function registerProjectSupervisorSurfaceTests() {
|
|||||||
).toHaveProperty('disabled', false);
|
).toHaveProperty('disabled', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sanitizes a persisted legacy Supervisor failure before rendering it', async () => {
|
||||||
|
const projectPath = '/tmp/launcher-legacy-failure-supervisor-game';
|
||||||
|
const legacyFailure =
|
||||||
|
'后台任务失败:kind=codex-app-server-context-window-exceeded ' +
|
||||||
|
`fingerprint=${'a'.repeat(64)} chars=2048`;
|
||||||
|
const supervisorHarness = createProjectSupervisorRuntimeHarness({
|
||||||
|
projectPath,
|
||||||
|
supervisorMessages: [
|
||||||
|
{
|
||||||
|
schemaVersion: 'game-creator-conversation.v1',
|
||||||
|
role: 'assistant',
|
||||||
|
content: legacyFailure,
|
||||||
|
agentId: 'project-supervisor',
|
||||||
|
messageId: 'legacy-failure-message',
|
||||||
|
updatedAt: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const invoke = vi.fn(
|
||||||
|
async (command: string, args?: Record<string, unknown>) => {
|
||||||
|
if (command === 'inspect_local_project_directory') {
|
||||||
|
return {
|
||||||
|
projectPath,
|
||||||
|
exists: true,
|
||||||
|
isDirectory: true,
|
||||||
|
isGameCreatorProject: true,
|
||||||
|
projectName: 'launcher-legacy-failure-supervisor-game',
|
||||||
|
recentRunStatus: null,
|
||||||
|
recentRunStopReason: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (command === 'get_local_game_manifest') {
|
||||||
|
return createGameCreationAppManifest(
|
||||||
|
'local-project-draft',
|
||||||
|
'launcher-legacy-failure-supervisor-game',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return supervisorHarness.invoke(command, args);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
window.__TAURI__ = {
|
||||||
|
core: { invoke },
|
||||||
|
event: { listen: supervisorHarness.listen },
|
||||||
|
};
|
||||||
|
renderLauncherProjectsAt('/?launcher');
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByLabelText('项目目录'), {
|
||||||
|
target: { value: projectPath },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '打开' }));
|
||||||
|
|
||||||
|
const messageList = await screen.findByLabelText('项目总控消息');
|
||||||
|
expect(
|
||||||
|
await within(messageList).findByText(
|
||||||
|
'项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
),
|
||||||
|
).not.toBeNull();
|
||||||
|
expect(messageList.textContent).not.toContain('fingerprint');
|
||||||
|
expect(messageList.textContent).not.toContain('chars=');
|
||||||
|
});
|
||||||
|
|
||||||
it('hydrates a persisted needs-reconciliation Supervisor runtime without an active Session index', async () => {
|
it('hydrates a persisted needs-reconciliation Supervisor runtime without an active Session index', async () => {
|
||||||
const projectPath = '/tmp/launcher-reconciliation-supervisor-game';
|
const projectPath = '/tmp/launcher-reconciliation-supervisor-game';
|
||||||
const manifest = createGameCreationAppManifest(
|
const manifest = createGameCreationAppManifest(
|
||||||
@@ -4059,7 +4120,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
|||||||
|
|
||||||
const supervisorSurface = await screen.findByLabelText('项目总控对话');
|
const supervisorSurface = await screen.findByLabelText('项目总控对话');
|
||||||
expect(
|
expect(
|
||||||
await within(supervisorSurface).findByText('项目总控 Agent · 失败'),
|
await within(supervisorSurface).findByText('项目总控 Agent · 待核对'),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
within(supervisorSurface).getByText('当前阶段:待核对'),
|
within(supervisorSurface).getByText('当前阶段:待核对'),
|
||||||
@@ -6335,6 +6396,29 @@ export function registerProjectSupervisorSurfaceTests() {
|
|||||||
expect(stageRecord).not.toContain('private-operation-id');
|
expect(stageRecord).not.toContain('private-operation-id');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('archives a safe actionable Runtime failure instead of a generic failed stage', () => {
|
||||||
|
const runtime = gameChatRuntimeState({
|
||||||
|
runId: 'game-chat-context-failure-run',
|
||||||
|
status: 'failed',
|
||||||
|
phase: 'failed',
|
||||||
|
error:
|
||||||
|
'kind=codex-app-server-context-window-exceeded fingerprint=' +
|
||||||
|
'a'.repeat(64),
|
||||||
|
updatedAt: 9200,
|
||||||
|
});
|
||||||
|
const progress = buildGameChatProgressEvidence(runtime, {}, null);
|
||||||
|
if (!progress) {
|
||||||
|
throw new Error('missing context failure progress fixture');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stageRecord = formatGameChatStageRecord(runtime, progress, []);
|
||||||
|
expect(stageRecord).toContain(
|
||||||
|
'项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||||
|
);
|
||||||
|
expect(stageRecord).not.toContain('fingerprint');
|
||||||
|
expect(stageRecord).not.toContain('本轮失败');
|
||||||
|
});
|
||||||
|
|
||||||
it('counts only the seven first-playable tasks in game-chat progress', () => {
|
it('counts only the seven first-playable tasks in game-chat progress', () => {
|
||||||
const manifest = createGameCreationAppManifest(
|
const manifest = createGameCreationAppManifest(
|
||||||
'game-chat-progress-total',
|
'game-chat-progress-total',
|
||||||
@@ -9579,7 +9663,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
|||||||
expect(
|
expect(
|
||||||
within(
|
within(
|
||||||
within(dock).getByRole('article', { name: /策划 Agent/ }),
|
within(dock).getByRole('article', { name: /策划 Agent/ }),
|
||||||
).getByText('失败'),
|
).getByText('策划 Agent 服务连接失败,请稍后重试'),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
within(
|
within(
|
||||||
@@ -9633,7 +9717,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
|||||||
expect(
|
expect(
|
||||||
within(
|
within(
|
||||||
within(dock).getByRole('article', { name: /策划 Agent/ }),
|
within(dock).getByRole('article', { name: /策划 Agent/ }),
|
||||||
).getByText('失败'),
|
).getByText('策划 Agent 服务连接失败,请稍后重试'),
|
||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
expect(
|
expect(
|
||||||
within(
|
within(
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# 决策记录
|
# 决策记录
|
||||||
|
|
||||||
|
## 2026-08-12 Agent 失败原因使用稳定分类贯穿 Runtime 与正式展示面
|
||||||
|
|
||||||
|
- 背景:Codex app-server 的 failed turn 已携带 `turn.error.codexErrorInfo`,但适配器曾丢弃该字段并写固定失败句;Runtime、事件 `publicText`、最近任务、game-chat 阶段记录和多个 Agent 卡片又各自用固定文案覆盖已有安全原因。自主构建 final-reply 已形成确定性完成文案时还对任意错误 fallback 成成功,导致鉴权、额度、上下文、策略、sandbox、配置或网络失败可能被伪装为完成。
|
||||||
|
- 决策:app-server 只消费协议稳定错误分类和 HTTP 状态,不公开 `message / additionalDetails`;Runtime 持久私有诊断继续脱敏,正式 conversation、失败事件 `publicText`、阶段记录、最近任务和所有 Agent 卡片统一从封闭分类派生可行动中文摘要。失败事件只允许后端 `publicText` 进入正式活动详情,缺失时使用固定安全 summary,私有 `detail` 不得展示;旧 Supervisor 与专业 Agent 失败 conversation 也必须经过同一安全映射。`needs-reconciliation` 是停止自动推进并等待人工处置的终态,显示为“待核对”,不得归为普通运行中或普通完成;未知错误保留固定安全兜底。自主构建确定性 final-reply fallback 只允许稳定 `empty-response / deserialize` 回复形状错误,任何鉴权、额度、上下文、策略、sandbox、配置、网络或上游错误都保持失败。
|
||||||
|
- 安全边界:不得把 Provider 正文、URL/query、API Key、Token、Cookie、本地绝对路径、fingerprint、字符数或 `[redacted ...]` 占位符放入正式 UI、conversation、事件 `publicText` 或阶段记录。前端只消费后端稳定分类或已通过严格门禁的公共摘要,不从自由文本猜测敏感上游错误。
|
||||||
|
- 未完成恢复项:isolated join 唤醒、isolated child result 发布、manifest terminal projection 和 terminal-unknown reconciliation 在持久化自身失败时仍需要独立 durable marker 与重启扫描协议;这些跨崩溃窗口必须单独设计和验证,不能用 best-effort 事件或日志冒充已恢复。
|
||||||
|
- 验证方式:覆盖 Codex 分类与敏感诱饵、失败事件公共摘要、最近任务与各正式卡片、game-chat 阶段记录、待核对状态、final-reply fallback 白名单及 malformed 响应完整重试;运行 Rust 定向测试、前端模型/AppSurface 定向测试、Shell typecheck、编码和 diff 门禁。
|
||||||
|
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||||
|
|
||||||
|
|
||||||
## 2026-08-10 资源管理评审阻塞项按第二轮正式合同修复
|
## 2026-08-10 资源管理评审阻塞项按第二轮正式合同修复
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ Supervisor 认领该回执后,由父 run 自己为每个原 delivery 逐一创
|
|||||||
- Supervisor 持久决策与单主条件美术:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 child;Supervisor Provider 只通过 auto-safe 的 `agent.route_manifest` 提交 `game-chat-workflow-decision.v2`:`intentSummary` 是 Supervisor 自行理解并持久化的用户意图,`strategy=audit-existing-first` 只是固定安全执行策略,两者不得混用。此动作不能审计、生成、委派或替代后续判断,也不能把整体视觉重做解释成整套美术的强制重生成;成功后 Runtime 只启动唯一 `code-prototype` 主 Agent。升级恢复时严格校验 v1 sidecar 的旧 fingerprint,并从完成合同绑定的有效任务恢复 `intentSummary`;旧 `code-director` coverage/route 只作为迁移输入,不作为当前完成证据,必须由同一根 Run 的 `code-prototype` 重新 `asset.list` 后原位替换为单主合同。确定性 `code-prototype` Run 仅兼容已知 canonical task 文本版本,其余 task/binding/root 身份继续失败关闭;升级前已运行的 fixed-graph 美术 child 不再具备任何 mutation 或生图权限。主 Agent 必须以当前正式资产、Canvas 登记、私有图集合同、四张语义切片和 art manifest 判断真实缺口;完整覆盖时直接接入,不得生成或扣费。只有可证实缺失 `art-spec` 或核心 spritesheet 时,主 Agent 才可对相应 `art-director` 或 `art-asset-plan` 建立一条 durable 委派;每次最多一个活跃美术 child,child 仅可写 `assets/**`,不得修改 `game/**` 或接入/验收游戏。若两个槽位都缺失,必须先完成 `art-director`,由同一主 Run 认领其 `EvidenceReady` delivery 后,才能委派依赖规范图的 `art-asset-plan`;失败或未就绪 delivery 不得消耗不可重试的图集委派槽位。主 Agent 认领必要回执后继续同一 Run 完成素材接入、原玩法语义校验、`game.static_smoke` 与桌面/移动 `preview.validate`。绝对硬截止对嵌套美术 child 继续核验 `root -> code-prototype -> agent-delegate` 完整身份并保留未知外部生成的 reconciliation 证据。Runtime 只负责校验根/父子身份、当前 revision、路径、Canvas 登记、缺口/路由 fingerprint、写入范围及完成证据;纯“继续”仍走既有正式 continuation 合同,普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。
|
- Supervisor 持久决策与单主条件美术:game-chat 的关键词、用户是否报告“美术未接入”、占位状态和当前资产探测只形成 `advisoryOnly=true` 的补充上下文,不得直接重置 Graph、预完成美术节点、选择复用/生成分支或继承历史试玩类型。当前根 Run 没有持久化 Supervisor 决策时,scheduler 不启动任何 child;Supervisor Provider 只通过 auto-safe 的 `agent.route_manifest` 提交 `game-chat-workflow-decision.v2`:`intentSummary` 是 Supervisor 自行理解并持久化的用户意图,`strategy=audit-existing-first` 只是固定安全执行策略,两者不得混用。此动作不能审计、生成、委派或替代后续判断,也不能把整体视觉重做解释成整套美术的强制重生成;成功后 Runtime 只启动唯一 `code-prototype` 主 Agent。升级恢复时严格校验 v1 sidecar 的旧 fingerprint,并从完成合同绑定的有效任务恢复 `intentSummary`;旧 `code-director` coverage/route 只作为迁移输入,不作为当前完成证据,必须由同一根 Run 的 `code-prototype` 重新 `asset.list` 后原位替换为单主合同。确定性 `code-prototype` Run 仅兼容已知 canonical task 文本版本,其余 task/binding/root 身份继续失败关闭;升级前已运行的 fixed-graph 美术 child 不再具备任何 mutation 或生图权限。主 Agent 必须以当前正式资产、Canvas 登记、私有图集合同、四张语义切片和 art manifest 判断真实缺口;完整覆盖时直接接入,不得生成或扣费。只有可证实缺失 `art-spec` 或核心 spritesheet 时,主 Agent 才可对相应 `art-director` 或 `art-asset-plan` 建立一条 durable 委派;每次最多一个活跃美术 child,child 仅可写 `assets/**`,不得修改 `game/**` 或接入/验收游戏。若两个槽位都缺失,必须先完成 `art-director`,由同一主 Run 认领其 `EvidenceReady` delivery 后,才能委派依赖规范图的 `art-asset-plan`;失败或未就绪 delivery 不得消耗不可重试的图集委派槽位。主 Agent 认领必要回执后继续同一 Run 完成素材接入、原玩法语义校验、`game.static_smoke` 与桌面/移动 `preview.validate`。绝对硬截止对嵌套美术 child 继续核验 `root -> code-prototype -> agent-delegate` 完整身份并保留未知外部生成的 reconciliation 证据。Runtime 只负责校验根/父子身份、当前 revision、路径、Canvas 登记、缺口/路由 fingerprint、写入范围及完成证据;纯“继续”仍走既有正式 continuation 合同,普通美术措辞不得借用更老项目的具体试玩场景。不得以增加 loop 预算、伪造 revision、机械改写 manifest 或重放历史图片 action 代替 Supervisor 决策和程序侧审计。
|
||||||
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
- ready-task 对账取消续跑:未知工具结果仍停在 `needs-reconciliation` 且禁止自动重放;人工核对后显式取消原 child,保留 cancel tombstone,旧 child 和旧父 Run 按真实终态收口。若随后创建同 Session、同 Supervisor source、同有效任务语义的 continuation,新完成合同只对同时具有历史 `failed / needs-reconciliation`、最终 `cancelled` 和 durable tombstone 的 ready-task,把当前 manifest 对应 failed 节点恢复为 pending,并由 scheduler 创建全新 child Run。manifest 的读取、failed 筛选、每任务一次的 child journal 索引、证据重验和写回必须位于同一项目写锁域;较新的无 child 根 Run 只有在 durable journal 精确表明为旧 failed Graph 在进入调度前即失败时才能跨过,scheduler 自身失败必须阻断借用更老 tombstone。普通失败、无 tombstone、不同 source/Session/任务语义或证据冲突均保持失败关闭;不得复活旧 pending action、补造 observation 或把取消任务标成 completed。
|
||||||
- 完成门静态分析预算:Canvas 视觉门必须先做只会提前拒绝的词法预检。经典或模块脚本同时不含大小写精确的 `import` 与 `export` 字节序列时,不运行模块依赖语义分析;纯 `export ... from` / `export * from` 仍须进入正式模块图分析。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN`、`\\uNNNN`、`\\u{...}`、简单转义和续行;解码后仍无候选才不运行完整 Canvas alias / 函数可达性分析,解码不确定则保守进入 Oxc。存在任一候选时仍执行原 parser、semantic binding、解码后的 computed 属性/StringLiteral 路径、可达 `drawImage`、可见 Canvas、路径大小写和动态 namespace 写入门禁;HTML 中存在某个绑定元素不得使所有无关 JavaScript 单元进入重分析,禁止把词法命中当作通过条件。
|
- 完成门静态分析预算:Canvas 视觉门必须先做只会提前拒绝的词法预检。经典或模块脚本同时不含大小写精确的 `import` 与 `export` 字节序列时,不运行模块依赖语义分析;纯 `export ... from` / `export * from` 仍须进入正式模块图分析。当前脚本不含目标文件名或任一已绑定 DOM 图片元素 ID 时,先低成本解码 `\\xNN`、`\\uNNNN`、`\\u{...}`、简单转义和续行;解码后仍无候选才不运行完整 Canvas alias / 函数可达性分析,解码不确定则保守进入 Oxc。存在任一候选时仍执行原 parser、semantic binding、解码后的 computed 属性/StringLiteral 路径、可达 `drawImage`、可见 Canvas、路径大小写和动态 namespace 写入门禁;HTML 中存在某个绑定元素不得使所有无关 JavaScript 单元进入重分析,禁止把词法命中当作通过条件。
|
||||||
- Provider 故障展示:Provider retry 的“是否可重试”继续使用 `upstream-5xx` 等稳定类别判断,但 durable retry record 保留安全的精确 `upstream-<HTTP status>` 身份。等待态必须从真实 record 显示 HTTP 状态、`nextAttempt/maxRetries` 与当前持久退避剩余秒数,例如“Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 8 秒后重试”;不得以动画或前端自增计时伪造 attempt。重试耗尽的 Runtime 私有错误只保存 `kind/httpStatus/fingerprint/chars/retryAttempt/maxRetries/retryState`,前端和持久 conversation 仅在字段顺序、范围、状态一致且无尾随正文时派生“上游服务返回 HTTP 503;自动重试已耗尽(3/3)”;其它错误使用固定安全摘要。Provider 响应正文、URL/query、凭据、本地绝对路径、fingerprint、字符数和 `[redacted ...]` 占位符均不得进入用户可见消息。
|
- Provider 故障展示:Provider retry 的“是否可重试”继续使用 `upstream-5xx` 等稳定类别判断,但 durable retry record 保留安全的精确 `upstream-<HTTP status>` 身份。等待态必须从真实 record 显示 HTTP 状态、`nextAttempt/maxRetries` 与当前持久退避剩余秒数,例如“Provider 上游返回 HTTP 503,准备自动重试 1/3;预计 8 秒后重试”;不得以动画或前端自增计时伪造 attempt。重试耗尽的 Runtime 私有错误只保存 `kind/httpStatus/fingerprint/chars/retryAttempt/maxRetries/retryState`,前端和持久 conversation 仅在字段顺序、范围、状态一致且无尾随正文时派生“上游服务返回 HTTP 503;自动重试已耗尽(3/3)”。`codex_app_server` 收到 failed turn 时必须读取协议 `turn.error.codexErrorInfo`,按上下文超限、会话预算、用量、鉴权、请求、策略、sandbox、会话恢复和连接 / HTTP 状态生成封闭稳定分类;不得丢弃该字段后统一写“turn 执行失败”,也不得把 `message / additionalDetails` 原文公开。自主构建已有本地确定性完成文案时,也只允许 `empty-response / deserialize` 这类回复形状错误使用 fallback;鉴权、额度、上下文、策略、sandbox、配置、网络和上游错误必须保持失败,禁止用完成文案掩盖。正式面、阶段记录、Runtime 活动详情和持久 conversation 从稳定分类派生同一份可行动中文摘要;失败事件活动详情只消费后端 `publicText`,缺失时退回固定安全 summary,禁止公开私有 `detail`。旧 Supervisor 与专业 Agent 失败 conversation 必须在展示时经过相同安全映射。`needs-reconciliation` 是停止自动推进、轮询和活跃计数并等待人工处置的终态,明确显示为待核对,不能显示为普通运行中或已完成;未知分类仍使用固定安全兜底。Provider 响应正文、URL/query、凭据、本地绝对路径、fingerprint、字符数和 `[redacted ...]` 占位符均不得进入用户可见消息。
|
||||||
- 跨轮阶段记录:game-chat 父 run 进入真实 completed / failed / cancelled 终态后,客户端等待唯一 `code-prototype` 主 Run 及其所有必要美术委派都已形成真实终态,再把本轮、主 Agent 进度、是否复用/补齐素材、最新试玩 / 静态检查、最近返工决定和已登记成果图片路径整理成一条 `【Supervisor 阶段记录】` 项目 assistant 消息。父 run 先终态而 child 或 manifest 仍在 hydration 时不得以陈旧快照提前归档,要暂存终态 Runtime 并在状态刷新后重试。页面初始 hydration 若直接读到缺少阶段记录的真实终态 run,也必须补写,但 `idle` 不是可归档终态。每个“项目 + 父 run”最多追加一次,进入现有 `conversation.write` 权限与项目 conversation 持久化链路,下一轮及重载后继续保留。阶段记录不是 Supervisor Runtime 正式回复,不写入 Agent Session、不增加 final assistant 数量,也不逐条复制原始事件或内部正文。
|
- 跨轮阶段记录:game-chat 父 run 进入真实 completed / failed / cancelled 终态后,客户端等待唯一 `code-prototype` 主 Run 及其所有必要美术委派都已形成真实终态,再把本轮、主 Agent 进度、是否复用/补齐素材、最新试玩 / 静态检查、最近返工决定和已登记成果图片路径整理成一条 `【Supervisor 阶段记录】` 项目 assistant 消息。父 run 先终态而 child 或 manifest 仍在 hydration 时不得以陈旧快照提前归档,要暂存终态 Runtime 并在状态刷新后重试。页面初始 hydration 若直接读到缺少阶段记录的真实终态 run,也必须补写,但 `idle` 不是可归档终态。每个“项目 + 父 run”最多追加一次,进入现有 `conversation.write` 权限与项目 conversation 持久化链路,下一轮及重载后继续保留。阶段记录不是 Supervisor Runtime 正式回复,不写入 Agent Session、不增加 final assistant 数量,也不逐条复制原始事件或内部正文。
|
||||||
- 图片成果:当前 manifest 新增或恢复已登记的 PNG / JPEG / WebP 资源时,聊天消息流同步显示 Runtime-owned “Supervisor 成果图片”卡,最多展示最新 4 张并随 manifest 原位更新。图片必须通过现有 `read_local_project_image_preview` 读取,只允许当前授权项目中 `assets/` 下的已登记资源,继续执行 `file.read` auto 权限、真实格式、大小、尺寸、普通文件、祖先目录和项目根边界校验;前端只接受返回路径、媒体类型和 `data:` 前缀与请求完全一致的结果。缩略图点击后使用独立模态查看器,支持按钮与滚轮缩放、指针拖拽、双击 / 按钮复位、Esc / 按钮 / 遮罩关闭,移动端占满视口;不得在聊天卡下方追加展开区。图片卡不写入 conversation,不解析 assistant 文本中的任意 Markdown / 绝对路径,也不开放 `.agent` 验收截图读取。
|
- 图片成果:当前 manifest 新增或恢复已登记的 PNG / JPEG / WebP 资源时,聊天消息流同步显示 Runtime-owned “Supervisor 成果图片”卡,最多展示最新 4 张并随 manifest 原位更新。图片必须通过现有 `read_local_project_image_preview` 读取,只允许当前授权项目中 `assets/` 下的已登记资源,继续执行 `file.read` auto 权限、真实格式、大小、尺寸、普通文件、祖先目录和项目根边界校验;前端只接受返回路径、媒体类型和 `data:` 前缀与请求完全一致的结果。缩略图点击后使用独立模态查看器,支持按钮与滚轮缩放、指针拖拽、双击 / 按钮复位、Esc / 按钮 / 遮罩关闭,移动端占满视口;不得在聊天卡下方追加展开区。图片卡不写入 conversation,不解析 assistant 文本中的任意 Markdown / 绝对路径,也不开放 `.agent` 验收截图读取。
|
||||||
- Run 接管:External Runner 模式下首次提交可能返回“旧 canonical state + 新 `acceptedRunId`”;页面必须以 `acceptedRunId` 作为本轮权威身份,在 state 尚未切换时显示“已投递,正在同步 Agent Runner”,并允许该 run 的 Tauri event 或轮询结果接管。不得把旧 idle state 当作本轮结果、过滤新 run 事件,自动预览授权也必须绑定 `acceptedRunId`。
|
- Run 接管:External Runner 模式下首次提交可能返回“旧 canonical state + 新 `acceptedRunId`”;页面必须以 `acceptedRunId` 作为本轮权威身份,在 state 尚未切换时显示“已投递,正在同步 Agent Runner”,并允许该 run 的 Tauri event 或轮询结果接管。不得把旧 idle state 当作本轮结果、过滤新 run 事件,自动预览授权也必须绑定 `acceptedRunId`。
|
||||||
@@ -197,7 +197,7 @@ V1.47 在只读工具边界和 batch v3/v2/v1 恢复终审修复后的最新独
|
|||||||
|
|
||||||
V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。
|
V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。
|
||||||
|
|
||||||
2026-07-18 起,正式项目工作台的总控与策划 / 美术 / 程序 Agent 状态统一投影当前 Supervisor 父 run 的真实 Runtime;专业 Agent 只有在 `parentRunId` 精确匹配该父 run 时才可进入当前项目状态列表。普通项目页在 Tauri event 之外必须保留只读轮询,兜底独立 Runner 无法可靠投递 App event 的情况;短暂读取失败时保留最后一份可信快照,不得清空或倒退界面状态。正式面只展示真实运行阶段、计划完成数 / 总数、最近更新时间、失败、待确认与待回答等紧凑状态;专业 Agent 的确认或拒绝必须同时绑定真实 `agentId + runId + actionId`。`manifest.tasks` 只能在没有匹配 Runtime 时作为回退,不得覆盖真实 Runtime;正式面不展示内部 `currentAction`、`observation`、工具计划正文、Provider 错误原文、fingerprint 或字符计数,transport / timeout / 鉴权 / 限流等失败只映射为可理解的安全文案,也不得根据 manifest 或动画伪造生产中、进度百分比或完成状态。当前父 run 或专业状态集合变化时 Runtime 状态区回到顶部,总控摘要在内部滚动期间保持可见。
|
2026-07-18 起,正式项目工作台的总控与策划 / 美术 / 程序 Agent 状态统一投影当前 Supervisor 父 run 的真实 Runtime;专业 Agent 只有在 `parentRunId` 精确匹配该父 run 时才可进入当前项目状态列表。普通项目页在 Tauri event 之外必须保留只读轮询,兜底独立 Runner 无法可靠投递 App event 的情况;短暂读取失败时保留最后一份可信快照,不得清空或倒退界面状态。正式面只展示真实运行阶段、计划完成数 / 总数、最近更新时间、失败、待确认与待回答等紧凑状态;专业 Agent 的确认或拒绝必须同时绑定真实 `agentId + runId + actionId`。`manifest.tasks` 只能在没有匹配 Runtime 时作为回退,不得覆盖真实 Runtime;正式面不展示内部 `currentAction`、`observation`、工具计划正文、Provider 错误原文、fingerprint 或字符计数。transport / timeout / 鉴权 / 限流、Codex 稳定错误分类,以及验证、预期产物、权限策略、预算、恢复对账和持久化等常见 Runtime 失败必须映射为可理解、可行动的安全文案;底部子 Agent 状态卡在失败时直接展示同一安全摘要,不能只写“失败”或“子 Agent 任务失败”。不得根据 manifest 或动画伪造生产中、进度百分比或完成状态。当前父 run 或专业状态集合变化时 Runtime 状态区回到顶部,总控摘要在内部滚动期间保持可见。
|
||||||
|
|
||||||
2026-07-19 起,当前父 run 下的专业 Agent 进入 `failed` 后,正式工作台必须提供“在当前项目重试”恢复入口,不得要求用户新建项目。重试必须精确核对原 `agentId + runId + parentRunId`,复用原 task、active Session 和父 run 归属,同时生成新的专业 Agent runId;新 run 继承已持久化的上下文和父子绑定,不覆写旧失败 run 的审计事实,也不得把 UI 重试解释为底层 transport 根因已修复。`agent.resume` 默认 `confirm` 不变:自动 retry command 继续执行 auto gate;正式失败卡按钮自身是本次明确确认,使用 deny-only 的 confirmed retry command。按钮必须原卡即时显示“正在提交重试”、受理或安全错误;若 Supervisor 已为同一 delegation 准备合同 repair,则该按钮优先确认既有 repair,避免重复派发。
|
2026-07-19 起,当前父 run 下的专业 Agent 进入 `failed` 后,正式工作台必须提供“在当前项目重试”恢复入口,不得要求用户新建项目。重试必须精确核对原 `agentId + runId + parentRunId`,复用原 task、active Session 和父 run 归属,同时生成新的专业 Agent runId;新 run 继承已持久化的上下文和父子绑定,不覆写旧失败 run 的审计事实,也不得把 UI 重试解释为底层 transport 根因已修复。`agent.resume` 默认 `confirm` 不变:自动 retry command 继续执行 auto gate;正式失败卡按钮自身是本次明确确认,使用 deny-only 的 confirmed retry command。按钮必须原卡即时显示“正在提交重试”、受理或安全错误;若 Supervisor 已为同一 delegation 准备合同 repair,则该按钮优先确认既有 repair,避免重复派发。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user