From 927616eb594d8c30d0def47087a37248a4334891 Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 17 Sep 2026 09:10:54 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=E4=B8=BA=E7=AD=96=E5=88=92Agent=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=AE=B5=E5=A2=9E=E5=8A=A0panic=E8=BE=B9=E7=95=8C=20f?= =?UTF-8?q?inish=5Fdesign=5Fcommand=20=E7=9A=84=20run=5Fdesign=5Floop=20?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=8C=85=E5=85=A5=20catch=5Funwind=EF=BC=8Cp?= =?UTF-8?q?anic=20=E8=BD=AC=E6=88=90=E6=99=AE=E9=80=9A=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E7=94=B1=E6=97=A2=E6=9C=89=E9=94=99=E8=AF=AF=E5=88=86=E6=94=AF?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=20=E6=96=B0=E5=A2=9E=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E5=85=AC=E5=BC=80=E6=96=87=E6=A1=88=EF=BC=9A=E7=AD=96=E5=88=92?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E5=8F=91=E7=94=9F=E5=86=85=E9=83=A8=E9=94=99?= =?UTF-8?q?=E8=AF=AF=EF=BC=8C=E6=9C=AC=E8=BD=AE=E5=B7=B2=E4=B8=AD=E6=96=AD?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E7=9B=B4=E6=8E=A5=E9=87=8D=E8=AF=95=EF=BC=9B?= =?UTF-8?q?=E8=8B=A5=E5=8F=8D=E5=A4=8D=E5=87=BA=E7=8E=B0=E8=AF=B7=E5=8F=8D?= =?UTF-8?q?=E9=A6=88=E3=80=82=20panic=20=E8=B4=9F=E8=BD=BD=E4=BB=85?= =?UTF-8?q?=E5=86=99=E5=85=A5=E7=A7=81=E6=9C=89=20design=5Fdebug=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E8=BF=9B=E5=85=A5=E7=94=A8=E6=88=B7=E5=8F=AF=E8=A7=81?= =?UTF-8?q?=E6=B6=88=E6=81=AF=20=E6=96=B0=E5=A2=9E=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=20design=5Ftool=5Fpanic=5Fbecomes=5Fvisible?= =?UTF-8?q?=5Fretryable=5Ferror=20=E8=A6=86=E7=9B=96=20panic=20=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E4=B8=8E=E9=87=8D=E8=AF=95=20=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E6=96=87=E6=A1=A3=E6=96=B0=E5=A2=9E=E7=AD=96?= =?UTF-8?q?=E5=88=92=20Agent=20=E8=BF=90=E8=A1=8C=20panic=20=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E5=B0=8F=E8=8A=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent/design_runtime.rs | 92 ++++++++++++++++++- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 4 + 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index 1a5a6e486..c81f84ec1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -1,5 +1,6 @@ use super::design_tools::*; use super::*; +use futures::FutureExt; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::fs::File; @@ -11,6 +12,9 @@ use uuid::Uuid; const DESIGN_ACTIVE_LOCK: &str = ".agent/design-agent/active.lock"; +const DESIGN_PANIC_PUBLIC_ERROR: &str = + "策划运行发生内部错误,本轮已中断,可直接重试;若反复出现请反馈。"; + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde( tag = "type", @@ -421,6 +425,10 @@ fn execute_design_tool( ) -> Result { let args: Value = serde_json::from_str(&call.arguments) .map_err(|error| format!("工具参数不是有效 JSON:{error}"))?; + #[cfg(test)] + if call.name == "design_test__panic" { + panic!("注入的策划工具 panic"); + } match call.name.as_str() { "get_workflow_status" => Ok(design_workflow_status(session)), "list_resources" => resources.list().map(Value::String), @@ -971,7 +979,18 @@ async fn finish_design_command( Some(design_view(&session, run)), )); if run { - if let Err(error) = run_design_loop(root, resources, &mut session, &mut emit).await { + // panic 边界:运行期 panic 转成普通失败,交给既有错误分支恢复(重读检查点、 + // 写 last_error、发最终 view)。否则 unwind 会杀死 command task,IPC 永不 + // 返回(前端停在工作态),会话停在无错误的 pending,用户只能看到无声的重试。 + let outcome = + std::panic::AssertUnwindSafe(run_design_loop(root, resources, &mut session, &mut emit)) + .catch_unwind() + .await; + let result = match outcome { + Ok(result) => result, + Err(payload) => Err(design_panic_error(root, payload)), + }; + if let Err(error) = result { // 从最后一个持久检查点恢复,防止写后未记结果被误认为已完成。 session = read_design_session(root)?.ok_or("策划会话丢失")?; session.last_error = Some(redact_agent_runtime_error(root, &error, 1800)); @@ -991,6 +1010,17 @@ async fn finish_design_command( Ok(view) } +// panic 负载可能包含路径或内容片段,公开文案固定;细节只进私有 design_debug。 +fn design_panic_error(root: &Path, payload: Box) -> String { + let detail = payload + .downcast_ref::<&str>() + .map(|value| (*value).to_string()) + .or_else(|| payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "未知 panic 负载".to_string()); + design_debug(root, "panic", json!({"error": detail})); + DESIGN_PANIC_PUBLIC_ERROR.to_string() +} + pub(crate) async fn continue_design_agent_at( root: &Path, resources: &DesignResources, @@ -1632,6 +1662,66 @@ mod tests { .clone() } + #[tokio::test(flavor = "current_thread")] + async fn design_tool_panic_becomes_visible_retryable_error() { + let (_temp, root, resources) = init_design_project(); + let panic_call = platform_llm::LlmToolCall { + id: "call-panic".into(), + name: "design_test__panic".into(), + arguments: "{}".into(), + }; + let _fake = fake_provider::install( + vec![ + Ok(fake_response("panic-turn", "", vec![panic_call])), + Ok(fake_response("recovery", "已恢复", Vec::new())), + ], + 0, + ); + let view = continue_design_agent_at( + &root, + &resources, + "turn-panic", + DesignInput::Message { + text: "需求".into(), + }, + |_| {}, + ) + .await + .expect("panic 必须转成可恢复视图而不是向上传播"); + assert!(!view.running); + assert!(view.can_retry); + assert_eq!( + view.session.last_error.as_deref(), + Some(DESIGN_PANIC_PUBLIC_ERROR) + ); + let session = read_design_session(&root) + .expect("read session") + .expect("session exists"); + assert!( + !session.history.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("function_call_output") + && item.get("call_id").and_then(Value::as_str) == Some("call-panic") + }), + "panic 不得写半个工具输出" + ); + + let view = + continue_design_agent_at(&root, &resources, "turn-retry", DesignInput::Retry, |_| {}) + .await + .expect("panic 后可重试"); + assert!(!view.running); + assert!(!view.can_retry); + assert!(view.session.last_error.is_none()); + let session = read_design_session(&root) + .expect("read session") + .expect("session exists"); + assert!(session.turn.as_ref().is_some_and(|turn| !turn.pending)); + assert!(session.history.iter().any(|item| { + item.get("type").and_then(Value::as_str) == Some("function_call_output") + && item.get("call_id").and_then(Value::as_str) == Some("call-panic") + })); + } + #[test] fn design_request_enables_reasoning_capture_only_for_design_runtime() { let session = new_design_session("project", "quality"); diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index dec89f82b..c505ceb32 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -4,6 +4,10 @@ `patch_file` 的所有 edits 均匹配同一份原文件,参数顺序不影响结果。完成唯一匹配与不重叠校验后,按原文起点升序拼接未修改片段与替换文本,最后一次性写入;任一校验失败时不写文件。回归用例覆盖乱序 edits、中文内容与替换长度增减,并核对完整落盘内容。此行为仅属于策划 Agent 文件工具。 +## 策划 Agent 运行 panic 边界 + +`finish_design_command` 的运行段包在 `catch_unwind` 边界内:任何运行期 panic 被转成一次普通失败,由既有错误分支从最后一个持久检查点恢复,向会话写入固定公开文案"策划运行发生内部错误,本轮已中断,可直接重试;若反复出现请反馈。",并以 `running=false、可重试、lastError 有值` 的视图正常返回。panic 负载只写入私有 design_debug,不进入用户可见消息;continue、recover_uncertain 与 decide 三个入口共享同一边界。边界不改变工具错误、Provider 瞬态重试和批次不确定恢复的既有语义。 + ## 资源画布交互与工作台状态同步 - 工作台向窗口标题栏发布正在运行的项目时,输入未变化不得形成重复发布与清理的渲染循环;打开项目动作始终使用当前工作台处理逻辑,退出工作台后清除其标题栏状态。 -- 2.52.0 From 73729394e22161e8658b4644ca8a54957b4853a6 Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 17 Sep 2026 09:27:48 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=E7=AD=96=E5=88=92panic=E6=97=B6=E6=8A=8A?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BD=8D=E7=BD=AE=E5=92=8C=E8=B4=9F=E8=BD=BD?= =?UTF-8?q?=E5=86=99=E5=85=A5design=5Fdebug=20=E8=BF=90=E8=A1=8C=E6=AE=B5?= =?UTF-8?q?=E7=BB=8Ftokio=20task-local=E6=90=BA=E5=B8=A6=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=A0=B9=EF=BC=8C=E5=A4=9A=E7=BA=BF=E7=A8=8Bruntime=E8=B7=A8wo?= =?UTF-8?q?rker=E8=BF=81=E7=A7=BB=E4=B9=9F=E8=83=BD=E5=BD=92=E5=9B=A0=20?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E8=BF=90=E8=A1=8C=E5=AE=89=E8=A3=85panic=20h?= =?UTF-8?q?ook=EF=BC=88take=5Fhook=E5=8C=85=E8=A3=85=E5=8E=9Fhook=EF=BC=8C?= =?UTF-8?q?update=5Fhook=E5=9C=A81.96=E4=BB=8Dunstable=EF=BC=89=20hook?= =?UTF-8?q?=E4=BB=85=E5=9C=A8=E7=AD=96=E5=88=92=E8=BF=90=E8=A1=8C=E6=AE=B5?= =?UTF-8?q?=E5=86=85=E7=94=9F=E6=95=88=EF=BC=8C=E5=85=B6=E5=AE=83=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=9B=B4=E6=8E=A5=E9=80=8F=E4=BC=A0=E5=8E=9Fhook=20de?= =?UTF-8?q?sign=5Fpanic=5Ferror=E5=8F=AA=E8=BF=94=E5=9B=9E=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E5=85=AC=E5=BC=80=E6=96=87=E6=A1=88=EF=BC=8Cdebug?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=94=B1hook=E7=BB=9F=E4=B8=80=E5=86=99?= =?UTF-8?q?=E5=85=A5=20=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=E6=96=AD?= =?UTF-8?q?=E8=A8=80panic=E8=AE=B0=E5=BD=95=E8=90=BD=E7=9B=98=E4=B8=94?= =?UTF-8?q?=E5=8C=85=E5=90=ABdesign=5Fruntime.rs=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=8E=E6=B3=A8=E5=85=A5payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent/design_runtime.rs | 85 ++++++++++++++++--- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index c81f84ec1..fbb42a22a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -15,6 +15,43 @@ const DESIGN_ACTIVE_LOCK: &str = ".agent/design-agent/active.lock"; const DESIGN_PANIC_PUBLIC_ERROR: &str = "策划运行发生内部错误,本轮已中断,可直接重试;若反复出现请反馈。"; +// 运行段经 task-local 携带项目根,panic hook 据此把位置和负载写进私有 design_debug。 +// task-local 而非 thread-local:多线程 runtime 下 future 会跨 worker 迁移。 +tokio::task_local! { + static DESIGN_PANIC_ROOT: Option; +} + +fn ensure_design_panic_hook() { + static ONCE: std::sync::Once = std::sync::Once::new(); + ONCE.call_once(|| { + let previous = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + if let Ok(Some(root)) = DESIGN_PANIC_ROOT.try_with(Clone::clone) { + let location = info + .location() + .map(|location| { + format!( + "{}:{}:{}", + location.file(), + location.line(), + location.column() + ) + }) + .unwrap_or_else(|| "未知位置".to_string()); + design_debug( + &root, + "panic", + json!({ + "location": location, + "error": info.payload_as_str().unwrap_or("未知 panic 负载"), + }), + ); + } + previous(info); + })); + }); +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde( tag = "type", @@ -982,13 +1019,15 @@ async fn finish_design_command( // panic 边界:运行期 panic 转成普通失败,交给既有错误分支恢复(重读检查点、 // 写 last_error、发最终 view)。否则 unwind 会杀死 command task,IPC 永不 // 返回(前端停在工作态),会话停在无错误的 pending,用户只能看到无声的重试。 - let outcome = - std::panic::AssertUnwindSafe(run_design_loop(root, resources, &mut session, &mut emit)) - .catch_unwind() - .await; + ensure_design_panic_hook(); + let run = DESIGN_PANIC_ROOT.scope( + Some(root.to_path_buf()), + run_design_loop(root, resources, &mut session, &mut emit), + ); + let outcome = std::panic::AssertUnwindSafe(run).catch_unwind().await; let result = match outcome { Ok(result) => result, - Err(payload) => Err(design_panic_error(root, payload)), + Err(payload) => Err(design_panic_error(payload)), }; if let Err(error) = result { // 从最后一个持久检查点恢复,防止写后未记结果被误认为已完成。 @@ -1010,14 +1049,9 @@ async fn finish_design_command( Ok(view) } -// panic 负载可能包含路径或内容片段,公开文案固定;细节只进私有 design_debug。 -fn design_panic_error(root: &Path, payload: Box) -> String { - let detail = payload - .downcast_ref::<&str>() - .map(|value| (*value).to_string()) - .or_else(|| payload.downcast_ref::().cloned()) - .unwrap_or_else(|| "未知 panic 负载".to_string()); - design_debug(root, "panic", json!({"error": detail})); +// panic 负载可能包含路径或内容片段,公开文案固定;位置和负载由 panic hook 写进私有 +// design_debug(task-local 提供项目根),不进入用户可见消息。 +fn design_panic_error(_payload: Box) -> String { DESIGN_PANIC_PUBLIC_ERROR.to_string() } @@ -1665,6 +1699,7 @@ mod tests { #[tokio::test(flavor = "current_thread")] async fn design_tool_panic_becomes_visible_retryable_error() { let (_temp, root, resources) = init_design_project(); + std::env::set_var("GENARRATIVE_AGC_DESIGN_DEBUG", "1"); let panic_call = platform_llm::LlmToolCall { id: "call-panic".into(), name: "design_test__panic".into(), @@ -1705,6 +1740,30 @@ mod tests { "panic 不得写半个工具输出" ); + // design_debug 经独立线程落盘,轮询等待 panic 记录出现。 + let debug_dir = root.join(".debug/design-agent"); + let mut panic_record = None; + for _ in 0..100 { + panic_record = fs::read_dir(&debug_dir).ok().and_then(|entries| { + entries.flatten().find_map(|entry| { + let path = entry.path(); + let name = path.file_name()?.to_string_lossy().into_owned(); + if name.ends_with("-panic.json") { + fs::read_to_string(path).ok() + } else { + None + } + }) + }); + if panic_record.is_some() { + break; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + let panic_record = panic_record.expect("panic hook 必须把位置和负载写入 design_debug"); + assert!(panic_record.contains("design_runtime.rs")); + assert!(panic_record.contains("注入的策划工具 panic")); + let view = continue_design_agent_at(&root, &resources, "turn-retry", DesignInput::Retry, |_| {}) .await diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index c505ceb32..0828b8fda 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -8,6 +8,8 @@ `finish_design_command` 的运行段包在 `catch_unwind` 边界内:任何运行期 panic 被转成一次普通失败,由既有错误分支从最后一个持久检查点恢复,向会话写入固定公开文案"策划运行发生内部错误,本轮已中断,可直接重试;若反复出现请反馈。",并以 `running=false、可重试、lastError 有值` 的视图正常返回。panic 负载只写入私有 design_debug,不进入用户可见消息;continue、recover_uncertain 与 decide 三个入口共享同一边界。边界不改变工具错误、Provider 瞬态重试和批次不确定恢复的既有语义。 +运行段通过 task-local 携带项目根;首次运行时安装的 panic hook 在 panic 瞬间把代码位置(file:line:column)和负载追加写入私有 design_debug,随后交给原 hook 维持既有 stderr 输出。hook 只在策划运行段内生效(其它任务无 task-local 上下文时直接透传),多线程 runtime 下 future 跨 worker 迁移也能正确归因。 + ## 资源画布交互与工作台状态同步 - 工作台向窗口标题栏发布正在运行的项目时,输入未变化不得形成重复发布与清理的渲染循环;打开项目动作始终使用当前工作台处理逻辑,退出工作台后清除其标题栏状态。 -- 2.52.0 From 958078def8dd426a1770ea32a3b9b615eca86437 Mon Sep 17 00:00:00 2001 From: Linghong Date: Thu, 17 Sep 2026 09:52:40 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AD=96=E5=88=92panic?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=9A=84env=E5=8F=98=E9=87=8F=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=20=E6=96=B0=E5=A2=9E=E9=9D=99=E6=80=81=E4=BA=92?= =?UTF-8?q?=E6=96=A5=E9=94=81=E4=B8=B2=E8=A1=8C=E5=8C=96=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E7=BA=A7debug=E5=BC=80=E5=85=B3=E4=BF=AE=E6=94=B9=20DesignDebu?= =?UTF-8?q?gEnvGuard=E5=9C=A8drop=E6=97=B6=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E5=80=BC=EF=BC=8C=E8=A6=86=E7=9B=96=E6=96=AD=E8=A8=80=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E4=B8=8Epanic=E8=B7=AF=E5=BE=84=20=E9=94=81=E4=B8=AD?= =?UTF-8?q?=E6=AF=92=E6=97=B6=E5=8F=96=E5=86=85=E9=83=A8=E5=80=BC=E7=BB=A7?= =?UTF-8?q?=E7=BB=AD=EF=BC=8C=E9=81=BF=E5=85=8D=E5=A4=B1=E8=B4=A5=E6=94=BE?= =?UTF-8?q?=E5=A4=A7=E5=88=B0=E5=90=8E=E7=BB=AD=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src-tauri/src/agent/design_runtime.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index fbb42a22a..9e907c2c0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -1696,10 +1696,40 @@ mod tests { .clone() } + // 进程级 env 在同一 binary 的并行用例间共享:持锁串行化修改,drop 时恢复原值, + // 避免 debug 开关泄漏给并发用例。锁中毒时取内部值继续,不让上游失败放大。 + static DESIGN_DEBUG_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + struct DesignDebugEnvGuard { + previous: Option, + _lock: std::sync::MutexGuard<'static, ()>, + } + + impl Drop for DesignDebugEnvGuard { + fn drop(&mut self) { + match &self.previous { + Some(value) => std::env::set_var("GENARRATIVE_AGC_DESIGN_DEBUG", value), + None => std::env::remove_var("GENARRATIVE_AGC_DESIGN_DEBUG"), + } + } + } + + fn enable_design_debug_for_test() -> DesignDebugEnvGuard { + let lock = DESIGN_DEBUG_ENV_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + let previous = std::env::var("GENARRATIVE_AGC_DESIGN_DEBUG").ok(); + std::env::set_var("GENARRATIVE_AGC_DESIGN_DEBUG", "1"); + DesignDebugEnvGuard { + previous, + _lock: lock, + } + } + #[tokio::test(flavor = "current_thread")] async fn design_tool_panic_becomes_visible_retryable_error() { let (_temp, root, resources) = init_design_project(); - std::env::set_var("GENARRATIVE_AGC_DESIGN_DEBUG", "1"); + let _debug_env = enable_design_debug_for_test(); let panic_call = platform_llm::LlmToolCall { id: "call-panic".into(), name: "design_test__panic".into(), -- 2.52.0