Merge remote-tracking branch 'web/master' into feat/five_min_design
冲突解法: - 实施计划.md:master 改 Provider 故障展示、本分支改跨轮阶段记录,各取各自较新的一条 - decision-log.md:两侧新增条目不重叠(master 2 条 / 本分支 7 条),全部保留 - agentPresentation.ts、agentRuntimeModel.test.ts:import 取并集 - project-development.suite.ts:保留 master 新增用例,用例标题用本分支 M0B-2 改后的语义 - SupervisorChatOnlyView.tsx:合成两侧。isAgentRuntimeTerminalState 本身已含 needs-reconciliation,master 单独那两行对其自身是冗余的;但本分支的 `|| descendantsStillActive` 会让 needs-reconciliation 的 run 因子 Agent 未收束 而重新判为运行中,正好绕过 master 这次要立的规矩。故保留本分支的 lineage 口径 (projectedRuntime)并补显式 needs-reconciliation 短路。 验证:前端 typecheck 干净;appSurface.test.ts 377 passed;agentRuntimeModel 27 passed; cargo check --all-targets 通过;static delegate 定向 9 passed;clarification 定向 4 passed。 response_stream / runtime_state 有失败,经对照确认不是回归:在纯 web/master 的独立 worktree 上跑同一批为 10 failed,本分支为 6 failed 且是其真子集,两次运行成员还不同。 失败模式统一为 Windows 下 .agent/agent.db 被占用(Os code 32),属本机既有基线。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,23 +69,8 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository lint gates
|
||||
run: npm run lint
|
||||
|
||||
- name: Build web applications
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content data
|
||||
run: npm run check:content
|
||||
|
||||
- name: Check committed whitespace
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
|
||||
test -n "${base_ref}"
|
||||
git cat-file -e "${base_ref}^{commit}"
|
||||
git diff --check "${base_ref}"...HEAD
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
npm run check:pre-push-master -- "$@"
|
||||
@@ -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;
|
||||
pub(in crate::agent) const GAME_CREATOR_CODEX_APP_SERVER_TERMINAL_UNKNOWN_PREFIX: &str =
|
||||
"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>;
|
||||
|
||||
@@ -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(
|
||||
inner: &Arc<CodexAppServerInner>,
|
||||
detail: impl Into<String>,
|
||||
@@ -1012,9 +1100,7 @@ impl CodexAppServerConnection {
|
||||
))
|
||||
}
|
||||
"failed" => {
|
||||
return Err(platform_llm::LlmError::InvalidRequest(
|
||||
"Codex app-server turn 执行失败".to_string(),
|
||||
))
|
||||
return Err(game_creator_codex_app_server_failed_turn_error(turn))
|
||||
}
|
||||
status => {
|
||||
return Err(platform_llm::LlmError::Deserialize(format!(
|
||||
@@ -1577,6 +1663,81 @@ mod tests {
|
||||
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]
|
||||
fn codex_app_server_rejects_non_responses_key_mapping() {
|
||||
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))
|
||||
}
|
||||
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::EmptyResponse => ("empty-response".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(
|
||||
root: &Path,
|
||||
plan: AgentRuntimeToolPlan,
|
||||
@@ -3799,7 +3818,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
{
|
||||
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")
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -4529,6 +4529,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]
|
||||
async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallback_once() {
|
||||
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 {
|
||||
"专业 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)) =
|
||||
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("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 {
|
||||
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
||||
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(),
|
||||
phase: phase.to_string(),
|
||||
summary: summary.to_string(),
|
||||
public_text: game_creator_agent_runtime_public_event_text(root, event_type, summary),
|
||||
public_text,
|
||||
detail: detail
|
||||
.filter(|_| {
|
||||
!(event_type == "observation"
|
||||
@@ -2624,10 +2639,9 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action(
|
||||
event_type,
|
||||
"error" | "turn.failed" | "turn.budget_exhausted"
|
||||
) {
|
||||
return game_creator_agent_runtime_public_failure_detail(
|
||||
&state.agent_id,
|
||||
value,
|
||||
);
|
||||
return failure_detail.clone().unwrap_or_else(|| {
|
||||
game_creator_agent_runtime_public_failure_detail(&state.agent_id, value)
|
||||
});
|
||||
}
|
||||
let max_chars = if event_type == "observation"
|
||||
&& 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())
|
||||
.expect("mock tool plan response");
|
||||
|
||||
let (mut final_stream, _) = listener.accept().expect("mock final reply accept");
|
||||
drop(read_mock_http_request(&mut final_stream));
|
||||
let invalid_body = "{invalid-json";
|
||||
let final_response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
invalid_body.len(),
|
||||
invalid_body
|
||||
);
|
||||
final_stream
|
||||
.write_all(final_response.as_bytes())
|
||||
.expect("mock invalid final reply response");
|
||||
// Autonomous runs enforce a 12-retry floor. Return the same malformed
|
||||
// response for the initial final-reply request and every retry so this
|
||||
// fixture tests deserialize exhaustion rather than an accidental
|
||||
// connection-refused fallback after the first malformed response.
|
||||
for _ in 0..=12 {
|
||||
let (mut final_stream, _) = listener.accept().expect("mock final reply accept");
|
||||
drop(read_mock_http_request(&mut final_stream));
|
||||
let invalid_body = "{invalid-json";
|
||||
let final_response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
invalid_body.len(),
|
||||
invalid_body
|
||||
);
|
||||
final_stream
|
||||
.write_all(final_response.as_bytes())
|
||||
.expect("mock invalid final reply response");
|
||||
}
|
||||
});
|
||||
base_url
|
||||
}
|
||||
|
||||
@@ -617,7 +617,8 @@ async fn response_stream_private_process_output_is_never_published_or_committed_
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn response_stream_final_failure_with_retry_disabled_commits_planning_fallback() {
|
||||
async fn response_stream_final_disconnect_with_retry_disabled_fails_without_committing_planning_fallback(
|
||||
) {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
@@ -686,32 +687,28 @@ async fn response_stream_final_failure_with_retry_disabled_commits_planning_fall
|
||||
);
|
||||
drop(finalization_lock);
|
||||
|
||||
let completed = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(completed.phase, "completed");
|
||||
assert_eq!(completed.last_response.as_deref(), Some(planning_fallback));
|
||||
let committed = wait_for_response_stream_status(
|
||||
&root,
|
||||
"design-director",
|
||||
run_id,
|
||||
"committed",
|
||||
ready.sequence.saturating_add(1),
|
||||
);
|
||||
assert_eq!(committed.accumulated_text, planning_fallback);
|
||||
let failed = wait_for_agent_runtime_phase(&root, "design-director", "failed");
|
||||
assert_eq!(failed.status, "failed");
|
||||
assert!(failed
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("kind=transport")));
|
||||
assert_eq!(failed.last_response, None);
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some("design-director"),
|
||||
Some(&started.state.session_id),
|
||||
)
|
||||
.expect("read committed fallback conversation");
|
||||
assert_eq!(
|
||||
conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.role == "assistant")
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![planning_fallback]
|
||||
);
|
||||
.expect("read failed final stream conversation");
|
||||
let assistant_messages = conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.role == "assistant")
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(assistant_messages.len(), 1);
|
||||
assert_ne!(assistant_messages[0], planning_fallback);
|
||||
assert!(assistant_messages[0].contains("失败"));
|
||||
|
||||
let requests = mock.stop_and_collect();
|
||||
assert_eq!(
|
||||
@@ -760,11 +757,21 @@ async fn response_stream_final_failure_with_retry_disabled_commits_planning_fall
|
||||
final_lifecycle[0]["requestSlot"],
|
||||
final_lifecycle[1]["requestSlot"]
|
||||
);
|
||||
assert_response_stream_completion_event_details(
|
||||
&root,
|
||||
"design-director",
|
||||
run_id,
|
||||
planning_fallback,
|
||||
let mut final_reply_failed_audit = false;
|
||||
for _ in 0..250 {
|
||||
final_reply_failed_audit = read_agent_db_records_for_test(&root).iter().any(|record| {
|
||||
record["recordType"] == "agent.runtime.background_task.failed"
|
||||
&& record["runId"] == run_id
|
||||
&& record["failureKind"] == "final-reply-failed"
|
||||
});
|
||||
if final_reply_failed_audit {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
assert!(
|
||||
final_reply_failed_audit,
|
||||
"final reply transport failure audit must eventually persist"
|
||||
);
|
||||
assert_response_stream_public_surfaces_exclude(&root, "design-director", &[planning_fallback]);
|
||||
|
||||
|
||||
@@ -147,12 +147,23 @@ fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() {
|
||||
"public-event-action-1",
|
||||
)
|
||||
.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(
|
||||
&game_creator_agent_runtime_event_path(&root, "code-prototype"),
|
||||
)
|
||||
.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()));
|
||||
let mut event_ids = events
|
||||
.iter()
|
||||
@@ -176,6 +187,16 @@ fn runtime_events_expose_stable_ids_and_backend_owned_public_text_only() {
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.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();
|
||||
}
|
||||
|
||||
@@ -846,6 +846,7 @@ export interface AgentStatusCard {
|
||||
runtimeWaitingOn: string | null;
|
||||
runtimeNextStep: string | null;
|
||||
runtimeTask: string | null;
|
||||
runtimeError?: string | null;
|
||||
runtimeRunId: string | null;
|
||||
runtimeLoopIteration: number | null;
|
||||
runtimeMaxLoopIterations: number | null;
|
||||
|
||||
@@ -709,8 +709,16 @@ export function agentRuntimeStartedRunId(
|
||||
|
||||
export function isAgentRuntimeTerminalState(runtime: AgentRuntimeState) {
|
||||
return (
|
||||
['completed', 'failed', 'cancelled'].includes(runtime.phase) ||
|
||||
['completed', 'failed', 'cancelled', 'idle'].includes(runtime.status)
|
||||
['completed', 'failed', 'cancelled', 'needs-reconciliation'].includes(
|
||||
runtime.phase,
|
||||
) ||
|
||||
[
|
||||
'completed',
|
||||
'failed',
|
||||
'cancelled',
|
||||
'idle',
|
||||
'needs-reconciliation',
|
||||
].includes(runtime.status)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -979,6 +987,12 @@ function agentRuntimeProviderRetryStatus(runtime: AgentRuntimeState) {
|
||||
|
||||
export function agentRuntimeConversationStatus(runtime: AgentRuntimeState) {
|
||||
if (isAgentRuntimeTerminalState(runtime)) {
|
||||
if (
|
||||
runtime.status === 'needs-reconciliation' ||
|
||||
runtime.phase === 'needs-reconciliation'
|
||||
) {
|
||||
return 'Agent 运行状态需要核对,正在同步记录';
|
||||
}
|
||||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||||
return 'Agent 运行失败,正在同步错误记录';
|
||||
}
|
||||
@@ -1023,6 +1037,14 @@ export function projectSupervisorChatRuntimeStatus(runtime: AgentRuntimeState) {
|
||||
if (runtime.status === 'idle' || runtime.phase === 'idle') {
|
||||
return '等待输入';
|
||||
}
|
||||
if (
|
||||
runtime.status === 'needs-reconciliation' ||
|
||||
runtime.phase === 'needs-reconciliation'
|
||||
) {
|
||||
return runtime.error
|
||||
? projectRuntimeVisibleError(runtime.error, '项目总控 Agent', true)
|
||||
: '项目总控 Agent 运行状态需要核对,请打开运行详情后重试';
|
||||
}
|
||||
if (isAgentRuntimeTerminalState(runtime)) {
|
||||
if (runtime.status === 'failed' || runtime.phase === 'failed') {
|
||||
return runtime.error
|
||||
@@ -1043,15 +1065,13 @@ export function formatAgentRuntimeEvent(event: AgentRuntimeEventRecord) {
|
||||
'turn.failed',
|
||||
'turn.budget_exhausted',
|
||||
].includes(event.eventType);
|
||||
const containsInternalFailureDiagnostics = Boolean(
|
||||
event.detail &&
|
||||
/(?:errorSha256|errorChars|fingerprint|chars|retryAttempt|retryState)=|<(?:absolute-path|redacted-url)>|\[redacted(?:[- ]secret| sensitive context)\]/i.test(
|
||||
event.detail,
|
||||
),
|
||||
);
|
||||
const visibleDetail =
|
||||
isFailureEvent && containsInternalFailureDiagnostics ? null : event.detail;
|
||||
const visibleDetail = isFailureEvent ? null : event.detail;
|
||||
const publicFailureText =
|
||||
isFailureEvent && typeof event.publicText === 'string'
|
||||
? event.publicText.trim()
|
||||
: '';
|
||||
const summary =
|
||||
publicFailureText ||
|
||||
event.summary ||
|
||||
visibleDetail ||
|
||||
(isFailureEvent ? 'Agent Runtime 本轮处理失败。' : event.runId);
|
||||
@@ -1371,11 +1391,16 @@ export function projectSupervisorRuntimeStatusLabel(
|
||||
runtime: AgentRuntimeState | null,
|
||||
runtimeError: string,
|
||||
) {
|
||||
if (
|
||||
runtime?.status === 'needs-reconciliation' ||
|
||||
runtime?.phase === 'needs-reconciliation'
|
||||
) {
|
||||
return '待核对';
|
||||
}
|
||||
if (
|
||||
runtimeError ||
|
||||
runtime?.status === 'failed' ||
|
||||
runtime?.phase === 'failed' ||
|
||||
runtime?.phase === 'needs-reconciliation'
|
||||
runtime?.phase === 'failed'
|
||||
) {
|
||||
return '失败';
|
||||
}
|
||||
@@ -1641,6 +1666,27 @@ export function projectRuntimeVisibleError(
|
||||
if (isMudPointInsufficientRuntimeError(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(
|
||||
/(?:^|[\s::])kind=upstream-(\d{3}) httpStatus=(\d{3}) fingerprint=[0-9a-f]{64} chars=\d+ retryAttempt=(\d+) maxRetries=(\d+) retryState=exhausted\s*$/,
|
||||
);
|
||||
@@ -1698,6 +1744,54 @@ export function projectRuntimeVisibleError(
|
||||
) {
|
||||
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 =
|
||||
normalized.includes('agentllm.') ||
|
||||
/(?:^|[\s::])kind=/.test(normalized) ||
|
||||
@@ -1725,7 +1819,8 @@ export function projectRuntimeVisibleError(
|
||||
|
||||
export function projectSupervisorVisibleConversationText(
|
||||
message: string,
|
||||
role: ChatMessage['role'] = 'assistant',
|
||||
role: ChatMessage['role'] | 'tool' = 'assistant',
|
||||
subject = '项目总控 Agent',
|
||||
) {
|
||||
const failurePrefix = '后台任务失败:';
|
||||
if (role !== 'assistant' || !message.startsWith(failurePrefix)) {
|
||||
@@ -1733,7 +1828,7 @@ export function projectSupervisorVisibleConversationText(
|
||||
}
|
||||
return projectRuntimeVisibleError(
|
||||
message.slice(failurePrefix.length),
|
||||
'项目总控 Agent',
|
||||
subject,
|
||||
true,
|
||||
);
|
||||
}
|
||||
@@ -1848,9 +1943,22 @@ export function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) {
|
||||
const goalSource = task.goalId
|
||||
? `Goal ${task.goalStatus ?? '-'} · revision ${task.goalRevision ?? 0}`
|
||||
: 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} · ${
|
||||
task.task || task.currentAction || task.runId
|
||||
}${goalSource ? ` · ${goalSource}` : ''}${
|
||||
delegationSource ? ` · ${delegationSource}` : ''
|
||||
}`;
|
||||
}${failureSummary ? ` · 失败原因:${failureSummary}` : ''}`;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ import {
|
||||
isAgentRuntimeTerminalState,
|
||||
isGameChatSupervisorRoot,
|
||||
projectCurrentGameChatRuntimeLineage,
|
||||
projectProfessionalAgentLabel,
|
||||
projectRuntimeVisibleCurrentWork,
|
||||
projectRuntimeVisibleError,
|
||||
taskRowsFromManifest,
|
||||
} from '../agent-runtime';
|
||||
import {
|
||||
@@ -157,6 +159,26 @@ export function taskRowsForAgentStatus(
|
||||
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(
|
||||
nextManifest: GameCreationAppManifest,
|
||||
trace: GameCreationAgentRunTrace | null,
|
||||
@@ -208,6 +230,7 @@ export function deriveAgentStatusCards(
|
||||
? (runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase))
|
||||
: null,
|
||||
runtimeTask: runtime?.currentTask ?? null,
|
||||
runtimeError: runtime ? agentRuntimeFailureDetail(runtime) : null,
|
||||
runtimeRunId: runtime?.runId ?? null,
|
||||
runtimeLoopIteration: runtime?.loopIteration ?? null,
|
||||
runtimeMaxLoopIterations: runtime?.maxLoopIterations ?? null,
|
||||
@@ -263,7 +286,12 @@ export function projectAgentRuntimeSummaries(
|
||||
) {
|
||||
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;
|
||||
}
|
||||
if (!isAgentRuntimeTerminalState(runtime)) {
|
||||
@@ -319,6 +347,9 @@ export function projectAgentRuntimeSummaries(
|
||||
runtime.phase === 'waiting-for-confirmation',
|
||||
);
|
||||
const failed = runtime.status === 'failed' || runtime.phase === 'failed';
|
||||
const needsReconciliation =
|
||||
runtime.status === 'needs-reconciliation' ||
|
||||
runtime.phase === 'needs-reconciliation';
|
||||
const completed =
|
||||
runtime.status === 'completed' || runtime.phase === 'completed';
|
||||
const cancelled =
|
||||
@@ -327,7 +358,7 @@ export function projectAgentRuntimeSummaries(
|
||||
const status: ProjectAgentRuntimeSummary['status'] =
|
||||
waitingForInput || waitingForConfirmation
|
||||
? 'waiting'
|
||||
: failed
|
||||
: failed || needsReconciliation
|
||||
? 'failed'
|
||||
: completed
|
||||
? 'completed'
|
||||
@@ -345,17 +376,28 @@ export function projectAgentRuntimeSummaries(
|
||||
? '待回答'
|
||||
: waitingForConfirmation
|
||||
? '待确认'
|
||||
: failed
|
||||
? '失败'
|
||||
: completed
|
||||
? '已完成'
|
||||
: cancelled
|
||||
? '已取消'
|
||||
: idle
|
||||
? '等待中'
|
||||
: runtime.phase === 'planning'
|
||||
? '分析中'
|
||||
: '工作中';
|
||||
: needsReconciliation
|
||||
? '待核对'
|
||||
: failed
|
||||
? '失败'
|
||||
: completed
|
||||
? '已完成'
|
||||
: cancelled
|
||||
? '已取消'
|
||||
: idle
|
||||
? '等待中'
|
||||
: runtime.phase === 'planning'
|
||||
? '分析中'
|
||||
: '工作中';
|
||||
const failureDetail = agentRuntimeFailureDetail(runtime);
|
||||
const failureSummary =
|
||||
(failed || needsReconciliation) && failureDetail
|
||||
? projectRuntimeVisibleError(
|
||||
failureDetail,
|
||||
projectProfessionalAgentLabel(runtime.agentId),
|
||||
true,
|
||||
)
|
||||
: null;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -363,6 +405,7 @@ export function projectAgentRuntimeSummaries(
|
||||
label,
|
||||
status,
|
||||
statusLabel,
|
||||
failureSummary,
|
||||
currentTask:
|
||||
(activePlanStep && agentRuntimePlanStepText(activePlanStep)) ||
|
||||
projectRuntimeVisibleCurrentWork(runtime),
|
||||
@@ -410,6 +453,7 @@ export function sameAgentStatusCard(
|
||||
left.runtimeWaitingOn === right.runtimeWaitingOn &&
|
||||
left.runtimeNextStep === right.runtimeNextStep &&
|
||||
left.runtimeTask === right.runtimeTask &&
|
||||
left.runtimeError === right.runtimeError &&
|
||||
left.runtimeRunId === right.runtimeRunId &&
|
||||
left.runtimeLoopIteration === right.runtimeLoopIteration &&
|
||||
left.runtimeMaxLoopIterations === right.runtimeMaxLoopIterations &&
|
||||
@@ -478,6 +522,7 @@ export function sameAgentRuntimeTasks(
|
||||
task.task === other.task &&
|
||||
task.currentAction === other.currentAction &&
|
||||
task.terminalDetail === other.terminalDetail &&
|
||||
task.error === other.error &&
|
||||
task.updatedAt === other.updatedAt
|
||||
);
|
||||
})
|
||||
@@ -924,8 +969,22 @@ export function formatAgentCardRuntimeStatus(agent: AgentStatusCard) {
|
||||
agent.runtimeMaxLoopIterations ?? 3
|
||||
}`
|
||||
: 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 [
|
||||
`Runtime:${agent.runtimeStatus} / ${agent.runtimePhase ?? '-'}`,
|
||||
failureSummary,
|
||||
loopProgress,
|
||||
agent.runtimeAction,
|
||||
agent.runtimeWaitingOn ? `等待 ${agent.runtimeWaitingOn}` : null,
|
||||
|
||||
+10
-2
@@ -16,7 +16,11 @@ import type {
|
||||
AgentStatusCard,
|
||||
LocalConversationMessageRecord,
|
||||
} from '../../app/types';
|
||||
import { AgentRuntimeStatusPanel } from '../agent-runtime';
|
||||
import {
|
||||
AgentRuntimeStatusPanel,
|
||||
projectProfessionalAgentLabel,
|
||||
projectSupervisorVisibleConversationText,
|
||||
} from '../agent-runtime';
|
||||
import { commandDraftFromSuggestedToolCall } from '../project-summary/agentPresentation';
|
||||
import {
|
||||
agentTaskGraphStateLabels,
|
||||
@@ -268,7 +272,11 @@ export function AgentConversationOverlay({
|
||||
key={`${message.updatedAt}-${index}`}
|
||||
className={`message message--${message.role}`}
|
||||
>
|
||||
{message.content}
|
||||
{projectSupervisorVisibleConversationText(
|
||||
message.content,
|
||||
message.role,
|
||||
projectProfessionalAgentLabel(selectedAgent.id),
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
|
||||
+16
-1
@@ -13,7 +13,10 @@ import type {
|
||||
PendingUiConfirmation,
|
||||
} from '../../app/types';
|
||||
import {
|
||||
projectProfessionalAgentLabel,
|
||||
projectRuntimeVisibleError,
|
||||
ProjectSupervisorRuntimePanel,
|
||||
projectSupervisorVisibleConversationText,
|
||||
projectWorkspaceStatusForDisplay,
|
||||
} from '../agent-runtime';
|
||||
import { formatAgentCardRuntimeStatus } from '../project-summary/agentPresentation';
|
||||
@@ -77,7 +80,10 @@ export function ProjectSupervisorView({
|
||||
key={message.messageId ?? `${message.role}-${index}`}
|
||||
className={`message message--${message.role}`}
|
||||
>
|
||||
{message.text}
|
||||
{projectSupervisorVisibleConversationText(
|
||||
message.text,
|
||||
message.role,
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
{transientReply ? (
|
||||
@@ -151,6 +157,15 @@ export function ProjectSupervisorView({
|
||||
taskStatusLabels[agent.status]}
|
||||
</span>
|
||||
{agent.runtimeTask ? <small>{agent.runtimeTask}</small> : null}
|
||||
{agent.runtimeError ? (
|
||||
<small>
|
||||
{projectRuntimeVisibleError(
|
||||
agent.runtimeError,
|
||||
projectProfessionalAgentLabel(agent.id),
|
||||
true,
|
||||
)}
|
||||
</small>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
+24
-7
@@ -303,12 +303,20 @@ export function formatGameChatStageRecord(
|
||||
) {
|
||||
const terminalStatus =
|
||||
runtime.status === 'failed' || runtime.phase === 'failed'
|
||||
? progress.interruptionText || '本轮失败'
|
||||
: runtime.status === 'cancelled' || runtime.phase === 'cancelled'
|
||||
? '本轮已取消'
|
||||
: runtime.status === 'completed' || runtime.phase === 'completed'
|
||||
? '本轮已完成'
|
||||
: projectSupervisorChatRuntimeStatus(runtime);
|
||||
? 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 === 'completed' || runtime.phase === 'completed'
|
||||
? '本轮已完成'
|
||||
: projectSupervisorChatRuntimeStatus(runtime);
|
||||
const lines = [
|
||||
GAME_CHAT_STAGE_RECORD_PREFIX,
|
||||
`${progress.title.replace('Supervisor 进度播报 · ', '')} · ${terminalStatus}`,
|
||||
@@ -990,7 +998,10 @@ export function SupervisorChatOnlyView({
|
||||
const running = Boolean(
|
||||
chatAgentBusy ||
|
||||
synchronizingAcceptedRun ||
|
||||
(projectedRuntime && (!runtimeTerminal || descendantsStillActive)),
|
||||
(projectedRuntime &&
|
||||
projectedRuntime.status !== 'needs-reconciliation' &&
|
||||
projectedRuntime.phase !== 'needs-reconciliation' &&
|
||||
(!runtimeTerminal || descendantsStillActive)),
|
||||
);
|
||||
const gameChatInterruptionText =
|
||||
gameChatMode && projectedRuntime
|
||||
@@ -1123,6 +1134,12 @@ export function SupervisorChatOnlyView({
|
||||
if (runtimeError) {
|
||||
return '运行异常';
|
||||
}
|
||||
if (
|
||||
runtime?.status === 'needs-reconciliation' ||
|
||||
runtime?.phase === 'needs-reconciliation'
|
||||
) {
|
||||
return '待人工核对';
|
||||
}
|
||||
if (synchronizingAcceptedRun) {
|
||||
return '正在启动';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -219,6 +219,7 @@ export type ProjectAgentRuntimeSummary = {
|
||||
label: string;
|
||||
status: 'pending' | 'running' | 'waiting' | 'completed' | 'failed';
|
||||
statusLabel: string;
|
||||
failureSummary?: string | null;
|
||||
currentTask: string;
|
||||
currentAction: string | null;
|
||||
waitingOn: string | null;
|
||||
@@ -375,6 +376,7 @@ function summarizeAgent(
|
||||
completed: '已完成',
|
||||
failed: '需处理',
|
||||
}[status],
|
||||
failureSummary: null,
|
||||
currentTask:
|
||||
activeTask?.title ??
|
||||
(completedCount > 0 ? '本轮任务已完成' : '等待项目总控分配任务'),
|
||||
@@ -3798,7 +3800,7 @@ export default function ProjectDevelopmentView({
|
||||
<article
|
||||
className={`game-agent-dock-item is-${agent.status}`}
|
||||
key={agent.group}
|
||||
aria-label={`${agent.label}:${agent.statusLabel}`}
|
||||
aria-label={`${agent.label}:${agent.failureSummary ?? agent.statusLabel}`}
|
||||
aria-describedby={`agent-detail-${agent.group}`}
|
||||
>
|
||||
<span className="game-agent-avatar">
|
||||
@@ -3815,7 +3817,7 @@ export default function ProjectDevelopmentView({
|
||||
</span>
|
||||
<span>
|
||||
<strong>{agent.label}</strong>
|
||||
<small>{agent.statusLabel}</small>
|
||||
<small>{agent.failureSummary ?? agent.statusLabel}</small>
|
||||
</span>
|
||||
<span
|
||||
className="game-agent-dock-detail"
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
AgentRuntimeResponseStream,
|
||||
AgentRuntimeResult,
|
||||
AgentRuntimeState,
|
||||
AgentRuntimeTaskRecord,
|
||||
ChatMessage,
|
||||
LocalConversationMessageRecord,
|
||||
} from '../src/app/types';
|
||||
@@ -17,7 +18,10 @@ import {
|
||||
projectGameChatPrimaryProgress,
|
||||
} from '../src/features/agent-runtime/gameChatRuntimeProjection';
|
||||
import {
|
||||
agentRuntimeConversationStatus,
|
||||
formatAgentRecentRuntimeTask,
|
||||
formatAgentRuntimeEvent,
|
||||
isAgentRuntimeTerminalState,
|
||||
mergeGameChatRuntimeResponseMessagesIntoHistory,
|
||||
mergeProjectSupervisorConversation,
|
||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||
@@ -30,7 +34,11 @@ import {
|
||||
projectWorkspaceStatusForDisplay,
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
} from '../src/features/agent-runtime/model';
|
||||
import { projectAgentRuntimeSummaries } from '../src/features/project-summary/agentPresentation';
|
||||
import {
|
||||
deriveAgentStatusCards,
|
||||
formatAgentCardRuntimeStatus,
|
||||
projectAgentRuntimeSummaries,
|
||||
} from '../src/features/project-summary/agentPresentation';
|
||||
|
||||
describe('普通用户工作区状态', () => {
|
||||
test('用项目名称替代 Unix 和 Windows 绝对路径', () => {
|
||||
@@ -46,7 +54,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', () => {
|
||||
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', () => {
|
||||
const projectRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
@@ -637,6 +743,54 @@ describe('Agent Runtime Provider 状态投影', () => {
|
||||
).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('隐藏旧失败对话与事件中的内部诊断标记', () => {
|
||||
const fingerprint = 'd'.repeat(64);
|
||||
const legacyConversation =
|
||||
@@ -649,6 +803,13 @@ describe('Agent Runtime Provider 状态投影', () => {
|
||||
expect(
|
||||
projectSupervisorVisibleConversationText(legacyConversation, 'user'),
|
||||
).toBe(legacyConversation);
|
||||
expect(
|
||||
projectSupervisorVisibleConversationText(
|
||||
legacyConversation,
|
||||
'assistant',
|
||||
'策划 Agent',
|
||||
),
|
||||
).toBe('策划 Agent 上游服务返回 HTTP 503;自动重试已耗尽(3/3)');
|
||||
|
||||
const formattedEvent = formatAgentRuntimeEvent({
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
@@ -661,17 +822,35 @@ describe('Agent Runtime Provider 状态投影', () => {
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
summary: 'Agent Runtime 本轮处理失败。',
|
||||
publicText: '项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||
detail:
|
||||
`<absolute-path> [redacted sensitive context] ` +
|
||||
`errorSha256=${fingerprint} · errorChars=99`,
|
||||
updatedAt: 1,
|
||||
});
|
||||
expect(formattedEvent).toBe(
|
||||
'turn.failed · failed / failed · Agent Runtime 本轮处理失败。',
|
||||
'turn.failed · failed / failed · 项目总控 Agent 模型上下文已超限,请缩小任务范围后重试',
|
||||
);
|
||||
expect(formattedEvent).not.toContain('errorSha256');
|
||||
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({
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'project-supervisor',
|
||||
|
||||
@@ -1909,9 +1909,10 @@ export function registerDeveloperAgentWindowTests() {
|
||||
expect(screen.getByText('最近事件')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'error · failed / failed · Agent Runtime 本轮处理失败。 · 最终回复调用失败',
|
||||
'error · failed / failed · Agent Runtime 本轮处理失败。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText(/最终回复调用失败/u)).toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'observation · running / action · file.read 返回项目笔记 · 已读取 game/notes.txt',
|
||||
@@ -5474,9 +5475,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
|
||||
@@ -5602,9 +5601,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5685,9 +5682,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5799,9 +5794,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
invoke.mockClear();
|
||||
|
||||
submitChat('/audit');
|
||||
@@ -5897,9 +5890,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
|
||||
@@ -5959,9 +5950,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
|
||||
@@ -6010,9 +5999,7 @@ export function registerDeveloperToolsTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
|
||||
submitChat('/audit');
|
||||
|
||||
|
||||
@@ -415,9 +415,7 @@ export function registerProjectConversationTests() {
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
@@ -1232,9 +1230,7 @@ export function registerProjectConversationTests() {
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已打开:authorized-game'),
|
||||
).not.toBeNull();
|
||||
expect(await screen.findByText('已打开:authorized-game')).not.toBeNull();
|
||||
|
||||
expect(await screen.findByText('正在拆解创作方向')).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -1330,8 +1326,24 @@ export function registerProjectConversationTests() {
|
||||
agentId: 'design-director',
|
||||
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: '刷新' }));
|
||||
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);
|
||||
fireEvent.click(screen.getByRole('button', { name: '填入同步命令' }));
|
||||
expect(screen.queryByLabelText('Agent 对话')).toBeNull();
|
||||
|
||||
@@ -4006,6 +4006,67 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
).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 () => {
|
||||
const projectPath = '/tmp/launcher-reconciliation-supervisor-game';
|
||||
const manifest = createGameCreationAppManifest(
|
||||
@@ -4100,7 +4161,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
|
||||
const supervisorSurface = await screen.findByLabelText('项目总控对话');
|
||||
expect(
|
||||
await within(supervisorSurface).findByText('项目总控 Agent · 失败'),
|
||||
await within(supervisorSurface).findByText('项目总控 Agent · 待核对'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(supervisorSurface).getByText('当前阶段:待核对'),
|
||||
@@ -4346,6 +4407,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: '运行时配置' }),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
expect(screen.getByLabelText('External Editor Base URL')).toHaveProperty(
|
||||
'value',
|
||||
'http://127.0.0.1:8082',
|
||||
@@ -6664,6 +6726,29 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
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 current single main stage in game-chat progress', () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'game-chat-progress-total',
|
||||
@@ -10112,7 +10197,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
expect(
|
||||
within(
|
||||
within(dock).getByRole('article', { name: /策划 Agent/ }),
|
||||
).getByText('失败'),
|
||||
).getByText('策划 Agent 服务连接失败,请稍后重试'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(
|
||||
@@ -10166,7 +10251,7 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
expect(
|
||||
within(
|
||||
within(dock).getByRole('article', { name: /策划 Agent/ }),
|
||||
).getByText('失败'),
|
||||
).getByText('策划 Agent 服务连接失败,请稍后重试'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
within(
|
||||
|
||||
@@ -412,16 +412,13 @@ export function registerRuntimeSettingsTests() {
|
||||
|
||||
const dialog = await screen.findByRole('dialog', { name: '运行时配置' });
|
||||
expect(await screen.findByDisplayValue('gpt-launcher')).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /高级参数/ }));
|
||||
expect(screen.getByLabelText('LLM 超时 ms')).toHaveProperty(
|
||||
'value',
|
||||
'1000',
|
||||
);
|
||||
expect(screen.getByLabelText('LLM 重试次数')).toHaveProperty('value', '0');
|
||||
expect(screen.getByLabelText('LLM 退避 ms')).toHaveProperty('value', '1');
|
||||
expect(screen.getByLabelText('LLM API 类型')).toHaveProperty(
|
||||
'value',
|
||||
'openai_responses',
|
||||
);
|
||||
expect(screen.getByLabelText('LLM 上下文窗口 tokens')).toHaveProperty(
|
||||
'value',
|
||||
'128000',
|
||||
@@ -434,19 +431,11 @@ export function registerRuntimeSettingsTests() {
|
||||
'value',
|
||||
'12000',
|
||||
);
|
||||
expect(screen.getByLabelText('External Editor Base URL')).toHaveProperty(
|
||||
fireEvent.click(screen.getByRole('button', { name: /常用设置/ }));
|
||||
expect(screen.getByLabelText('LLM API 类型')).toHaveProperty(
|
||||
'value',
|
||||
'http://127.0.0.1:8082',
|
||||
'openai_responses',
|
||||
);
|
||||
expect(screen.getByLabelText('External Editor API Key')).toHaveProperty(
|
||||
'type',
|
||||
'password',
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByLabelText('External Editor API Key')
|
||||
.getAttribute('autocomplete'),
|
||||
).toBe('off');
|
||||
fireEvent.change(screen.getByLabelText('LLM 模型'), {
|
||||
target: { value: 'gpt-launcher-updated' },
|
||||
});
|
||||
@@ -466,10 +455,12 @@ export function registerRuntimeSettingsTests() {
|
||||
});
|
||||
});
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'已保存:/home/test/AppData/game-creator.config.json',
|
||||
),
|
||||
).not.toBeNull();
|
||||
(
|
||||
await screen.findByText(
|
||||
'已保存:/home/test/AppData/game-creator.config.json',
|
||||
)
|
||||
).getAttribute('data-tone'),
|
||||
).toBe('success');
|
||||
fireEvent.mouseDown(dialog.parentElement as HTMLElement);
|
||||
expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull();
|
||||
});
|
||||
@@ -569,6 +560,7 @@ export function registerRuntimeSettingsTests() {
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /连接与工具/ }));
|
||||
const mcpRegion = await screen.findByRole('region', {
|
||||
name: 'MCP servers',
|
||||
});
|
||||
@@ -869,6 +861,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
expect(
|
||||
screen.getByLabelText('LLM API Key').getAttribute('autocomplete'),
|
||||
).toBe('off');
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
expect(screen.getByLabelText('External Editor API Key')).toHaveProperty(
|
||||
'type',
|
||||
'password',
|
||||
@@ -878,6 +871,21 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
.getByLabelText('External Editor API Key')
|
||||
.getAttribute('autocomplete'),
|
||||
).toBe('off');
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent 模型/ }));
|
||||
fireEvent.click(screen.getByRole('button', { name: /Planner planner/ }));
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /生成首版美术素材.*art-asset-plan/,
|
||||
}),
|
||||
);
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: /Generator generator/ }),
|
||||
);
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /项目总控 Agent project-supervisor/,
|
||||
}),
|
||||
);
|
||||
expect(screen.getByLabelText('Planner LLM API Key')).toHaveProperty(
|
||||
'type',
|
||||
'password',
|
||||
@@ -898,10 +906,6 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
'value',
|
||||
'claude-3-5-sonnet-latest',
|
||||
);
|
||||
expect(screen.getByLabelText('LLM 推理档')).toHaveProperty(
|
||||
'value',
|
||||
'medium',
|
||||
);
|
||||
expect(screen.getByLabelText('Planner LLM 推理档')).toHaveProperty(
|
||||
'value',
|
||||
'default',
|
||||
@@ -923,10 +927,6 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
expect(
|
||||
screen.getByLabelText('生成首版美术素材 (art/Asset) LLM 流式请求'),
|
||||
).toHaveProperty('value', 'true');
|
||||
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
||||
'checked',
|
||||
false,
|
||||
);
|
||||
expect(screen.getByLabelText('Planner LLM 联网检索')).toHaveProperty(
|
||||
'value',
|
||||
'false',
|
||||
@@ -942,6 +942,12 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
expect(supervisorWebSearch).toHaveProperty('value', 'true');
|
||||
fireEvent.change(supervisorWebSearch, { target: { value: 'false' } });
|
||||
expect(supervisorWebSearch).toHaveProperty('value', 'false');
|
||||
fireEvent.click(screen.getByRole('button', { name: /常用设置/ }));
|
||||
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
||||
'checked',
|
||||
false,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /高级参数/ }));
|
||||
expect(screen.getByLabelText('LLM 上下文窗口 tokens')).toHaveProperty(
|
||||
'value',
|
||||
'128000',
|
||||
@@ -954,11 +960,17 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
'value',
|
||||
'12000',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent 模型/ }));
|
||||
expect(screen.getByLabelText('Planner 上下文窗口 tokens')).toHaveProperty(
|
||||
'value',
|
||||
'',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /常用设置/ }));
|
||||
expect(screen.getByLabelText('LLM 推理档')).toHaveProperty(
|
||||
'value',
|
||||
'medium',
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('LLM API Key'), {
|
||||
target: { value: 'unit-new-secret-value' },
|
||||
});
|
||||
@@ -973,6 +985,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText('LLM 流式请求'));
|
||||
fireEvent.click(screen.getByLabelText('LLM 联网检索'));
|
||||
fireEvent.click(screen.getByRole('button', { name: /高级参数/ }));
|
||||
fireEvent.change(screen.getByLabelText('LLM 上下文窗口 tokens'), {
|
||||
target: { value: '160000' },
|
||||
});
|
||||
@@ -991,6 +1004,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
fireEvent.change(screen.getByLabelText('LLM 退避 ms'), {
|
||||
target: { value: '800' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent 模型/ }));
|
||||
fireEvent.change(screen.getByLabelText('Generator LLM API Key'), {
|
||||
target: { value: 'generator-new-secret' },
|
||||
});
|
||||
@@ -1018,6 +1032,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
target: { value: 'ark' },
|
||||
},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
fireEvent.change(screen.getByLabelText('External Editor Base URL'), {
|
||||
target: { value: 'http://127.0.0.1:8099' },
|
||||
});
|
||||
@@ -1092,10 +1107,12 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
within(runtimeConfigDialog).getByRole('button', { name: '读取' }),
|
||||
);
|
||||
expect(await screen.findByText(/已读取:/)).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /常用设置/ }));
|
||||
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
||||
'checked',
|
||||
true,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent 模型/ }));
|
||||
expect(screen.getByLabelText('项目总控 Agent LLM 联网检索')).toHaveProperty(
|
||||
'value',
|
||||
'false',
|
||||
@@ -1107,6 +1124,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
'generator-new-secret',
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /高级参数/ }));
|
||||
fireEvent.change(screen.getByLabelText('LLM 超时 ms'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
@@ -1137,6 +1155,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
);
|
||||
|
||||
expect(screen.getByText('已恢复默认配置,保存后生效')).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: /常用设置/ }));
|
||||
expect(screen.getByLabelText('Agent 模式')).toHaveProperty(
|
||||
'value',
|
||||
'codex_app_server',
|
||||
@@ -1158,10 +1177,12 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
'checked',
|
||||
false,
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Agent 模型/ }));
|
||||
expect(screen.getByLabelText('项目总控 Agent LLM 联网检索')).toHaveProperty(
|
||||
'value',
|
||||
'',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /高级参数/ }));
|
||||
expect(screen.getByLabelText('LLM 上下文窗口 tokens')).toHaveProperty(
|
||||
'value',
|
||||
'128000',
|
||||
@@ -1174,6 +1195,7 @@ export function registerPublishedRuntimeSettingsTests() {
|
||||
'value',
|
||||
'12000',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: /连接与工具/ }));
|
||||
expect(screen.getByLabelText('External Editor Base URL')).toHaveProperty(
|
||||
'value',
|
||||
'http://127.0.0.1:8082',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user