补齐Agent Runtime统一错误事件

失败和预算耗尽路径追加error语义事件并保留旧生命周期事件

Runtime状态面板支持展开全部最近事件并展示六类事件

补充前后端回归测试、可视验收截图和技术决策文档
This commit is contained in:
AIGameCreator App
2026-07-11 00:23:40 +08:00
parent b65541ce4c
commit 197e1e26c6
7 changed files with 140 additions and 4 deletions
@@ -6497,6 +6497,15 @@ pub(crate) fn fail_game_creator_agent_runtime_turn_at(
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
append_game_creator_agent_runtime_event(
root,
&state,
"error",
"failed",
"failed",
"Agent Runtime 本轮处理失败。",
state.error.as_deref(),
)?;
append_game_creator_agent_runtime_event(
root,
&state,
@@ -6530,6 +6539,15 @@ pub(crate) fn fail_game_creator_agent_runtime_budget_at(
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
append_game_creator_agent_runtime_event(
root,
&state,
"error",
"failed",
"budget-exhausted",
"Agent Runtime 达到 loop 预算但任务仍未收束。",
state.error.as_deref(),
)?;
append_game_creator_agent_runtime_event(
root,
&state,
@@ -2412,7 +2412,10 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
.collect::<Vec<_>>();
assert!(event_types.contains(&"thinking_summary"));
assert!(event_types.contains(&"plan"));
assert!(event_types.contains(&"action"));
assert!(event_types.contains(&"observation"));
assert!(event_types.contains(&"response"));
assert!(!event_types.contains(&"error"));
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recordType\":\"agent.runtime.tool_observation\""));
assert!(agent_db.contains("\"tool\":\"file.read\""));
@@ -2477,6 +2480,15 @@ async fn background_agent_runtime_marks_response_plan_step_failed_when_final_rep
.detail
.as_deref()
.is_some_and(|detail| detail.contains("后台 Agent 最终回复调用 LLM 失败")));
let failed_result =
read_game_creator_agent_runtime_at(&root, "design-director").expect("read failed events");
let event_types = failed_result
.recent_events
.iter()
.map(|event| event.event_type.as_str())
.collect::<Vec<_>>();
assert!(event_types.contains(&"error"));
assert!(event_types.contains(&"turn.failed"));
fs::remove_dir_all(root).ok();
}
@@ -2695,6 +2707,13 @@ async fn background_agent_runtime_marks_unconverged_loop_budget_exhausted() {
&& task.status == "failed"
&& task.phase == "budget-exhausted"
}));
let event_types = result
.recent_events
.iter()
.map(|event| event.event_type.as_str())
.collect::<Vec<_>>();
assert!(event_types.contains(&"error"));
assert!(event_types.contains(&"turn.budget_exhausted"));
let conversation = read_local_conversation_at(&root, Some("design-director"))
.expect("read budget conversation");
assert!(conversation.messages.iter().any(|message| {
+19 -1
View File
@@ -859,6 +859,7 @@ function AgentRuntimeStatusPanel({
onRejectRuntimeTask?: (runId: string, actionId: string) => void;
onRefreshRuntime?: () => void;
}) {
const [showAllRecentEvents, setShowAllRecentEvents] = useState(false);
if (!runtime && error) {
return (
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
@@ -879,7 +880,12 @@ function AgentRuntimeStatusPanel({
}
const planItems = runtime.plan.slice(0, 3);
const observations = runtime.observations.slice(-2);
const recentEvents = (runtime.recentEvents ?? []).slice(-4).reverse();
const allRecentEvents = runtime.recentEvents ?? [];
const hiddenRecentEventCount = Math.max(0, allRecentEvents.length - 4);
const recentEvents = (showAllRecentEvents
? allRecentEvents.slice()
: allRecentEvents.slice(-4)
).reverse();
const recentToolCalls = (runtime.recentToolCalls ?? []).slice(-3).reverse();
const recentTasks = (runtime.recentTasks ?? []).slice(-3).reverse();
const planSteps = (runtime.planSteps ?? []).slice(0, 5);
@@ -1041,6 +1047,18 @@ function AgentRuntimeStatusPanel({
{formatAgentRuntimeEvent(event)}
</small>
))}
{hiddenRecentEventCount > 0 ? (
<button
type="button"
className="agent-runtime-event-toggle"
aria-expanded={showAllRecentEvents}
onClick={() => setShowAllRecentEvents((expanded) => !expanded)}
>
{showAllRecentEvents
? '收起事件'
: `展开事件(另有 ${hiddenRecentEventCount} 条)`}
</button>
) : null}
</div>
) : null}
{recentToolCalls.length > 0 ? (
+12
View File
@@ -1413,6 +1413,18 @@ textarea {
gap: 3px;
}
.agent-runtime-status .agent-runtime-event-toggle {
justify-self: start;
min-height: 24px;
padding: 0;
border: 0;
background: transparent;
color: #44536a;
font-size: 12px;
text-decoration: underline;
text-underline-offset: 2px;
}
.launcher-project-development {
padding-top: 92px;
}
@@ -1842,6 +1842,20 @@ describe('AI 游戏创作 App 界面边界', () => {
detail: null,
updatedAt: 4001,
},
{
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'launcher-agent-task-test',
source: 'agent-background-task',
eventType: 'plan',
status: 'running',
phase: 'planning',
summary: '已生成行动计划',
detail: '读取项目笔记 / 回复开发者',
updatedAt: 4002,
},
{
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
@@ -1854,7 +1868,7 @@ describe('AI 游戏创作 App 界面边界', () => {
phase: 'action',
summary: '调用 file.read',
detail: 'game/notes.txt',
updatedAt: 4002,
updatedAt: 4003,
},
{
schemaVersion: 'game-creator-agent-runtime.v1',
@@ -1868,7 +1882,35 @@ describe('AI 游戏创作 App 界面边界', () => {
phase: 'action',
summary: 'file.read 返回项目笔记',
detail: '已读取 game/notes.txt',
updatedAt: 4003,
updatedAt: 4004,
},
{
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'launcher-agent-task-test',
source: 'agent-background-task',
eventType: 'response',
status: 'idle',
phase: 'completed',
summary: 'Agent 已生成最终回复。',
detail: '角色规范已整理。',
updatedAt: 4005,
},
{
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'launcher-agent-task-test',
source: 'agent-background-task',
eventType: 'error',
status: 'failed',
phase: 'failed',
summary: 'Agent Runtime 本轮处理失败。',
detail: '最终回复调用失败',
updatedAt: 4006,
},
];
const invoke = vi.fn(
@@ -2026,6 +2068,11 @@ describe('AI 游戏创作 App 界面边界', () => {
),
).not.toBeNull();
expect(screen.getByText('最近事件')).not.toBeNull();
expect(
screen.getByText(
'error · failed / failed · Agent Runtime 本轮处理失败。 · 最终回复调用失败',
),
).not.toBeNull();
expect(
screen.getByText(
'observation · running / action · file.read 返回项目笔记 · 已读取 game/notes.txt',
@@ -2034,9 +2081,29 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
screen.getByText('action · running / action · 调用 file.read · game/notes.txt'),
).not.toBeNull();
expect(
screen.getByText(
'response · idle / completed · Agent 已生成最终回复。 · 角色规范已整理。',
),
).not.toBeNull();
expect(
screen.queryByText('thinking_summary · running / planning · 需要先看项目笔记'),
).toBeNull();
expect(
screen.queryByText(
'plan · running / planning · 已生成行动计划 · 读取项目笔记 / 回复开发者',
),
).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '展开事件(另有 2 条)' }));
expect(
screen.getByText('thinking_summary · running / planning · 需要先看项目笔记'),
).not.toBeNull();
expect(
screen.getByText(
'plan · running / planning · 已生成行动计划 · 读取项目笔记 / 回复开发者',
),
).not.toBeNull();
expect(screen.getByRole('button', { name: '收起事件' })).not.toBeNull();
expect(screen.getByText('最近任务')).not.toBeNull();
expect(screen.getByText(/running \/ action · 后台整理角色规范/)).not.toBeNull();
expect(screen.getByText(/已启动后台任务:launcher-agent-task-/)).not.toBeNull();
@@ -4137,4 +4137,5 @@
- 决策:重启恢复继续遵守 `agent.resume` 默认确认策略。自动 command 只允许 auto;默认 confirm 由主工作区或独立开发 Agent 聊天窗口的 UI 明确确认后调用独立 command,确认绑定发起项目,切换项目取消旧确认且旧项目异步结果不得污染新项目状态;独立 command 只忽略 confirm、不允许绕过 deny,临时失败必须允许重试。
- 决策:后台 Agent loop 只有空 actions 才算收束;三轮预算耗尽仍有动作时写 `failed / budget-exhausted``loop-budget-exhausted`,不再生成总结后记成 completed。解析阶段保留 action 总数,超过单轮预算时写 `runtime.tool_budget` 并只执行前三个;Runtime 默认工具列表必须直接从可执行白名单派生。
- 决策:`agent.delegate` 子任务必须 durable 保存 `parentAgentId / parentRunId / delegationId`,其中 `delegationId` 从已持久化工具动作的 `actionId` 派生,不能使用执行时随机值;终态任务记录必须保存经过统一凭据清洗和安全截断的 `terminalDetail`,不能依赖可能被后续 run 覆盖的 Agent 全局 state。子任务进入 `completed / failed / cancelled / budget-exhausted` 任一终态后,Runtime 必须在 delegation 级 OS 文件锁内按固定 receipt runId 幂等生成且至多生成一次 `agent.delegate.result` 回执;不同委派并发写同一目标 Agent 时,runId 分配与 pending 追加还必须在目标 Agent 任务账本 OS 锁内原子完成。失败、排队或活跃取消、预算耗尽与成功同等需要回执,`needs-reconciliation` 只有最终取消后才回执。父 Agent 通过既有队列接收 `source=agent-delegate-receipt` 的续跑任务,回执 prompt 禁止重复同一委派,并携带完整的已清洗 `terminalDetail`,不能只保留 UI 摘要;排队期间不提前写入父会话,真正执行时才幂等落盘,用户消息或回执消息落盘失败时不得进入 LLM。回执任务必须保留父 run 关联,真正开始或恢复前再次核验父 run,关联缺失或父 run 不存在时失败关闭;父 run 已取消或普通失败时只保留 suppressed receipt 审计,不自动复活。父 Session 存在未结束委派时禁止切换或归档,极端竞态下回执回落到父 Agent 当前可写 Session。续跑继续遵守同 Agent FIFO、per-Agent OS 锁、权限确认、取消、恢复和 `needs-reconciliation` 屏障,不允许直接重入、插队或重复投递;恢复必须先恢复 pending action / reconciliation 屏障,再补齐“子终态已落盘、回执未入队”的崩溃窗口。
- 验证:Rust 覆盖首轮上下文不泄露、工具后 observation 可见、确认前后内容边界、前后台同 Agent 串行、前台结束后队列 drain、恢复确认 gate、预算耗尽失败、默认工具白名单一致性,以及 delegate 成功 / 失败 / 取消 / 预算耗尽终态回执、`delegationId` 幂等去重和父 Agent receipt 续跑仍受 FIFO / 锁 / 确认 / 恢复门禁;前端分别覆盖主工作区和独立开发 Agent 聊天窗口的默认恢复确认条与显式恢复 command
- 决策:Agent loop 的语义事件类型固定为 `thinking_summary / plan / action / observation / response / error`。普通失败和预算耗尽必须先追加统一 `error` 事件,同时保留 `turn.failed / turn.budget_exhausted` 生命周期事件供旧读取方兼容;状态、phase 和清洗后的错误详情必须在两类事件中一致。Runtime 状态面板默认展示最新 4 条事件,但在当前后端最近事件窗口大于 4 条时必须允许展开全部返回记录,不能让 `plan`、早期 observation 或 thinking summary 永久不可见
- 验证:Rust 覆盖首轮上下文不泄露、工具后 observation 可见、六类语义事件、确认前后内容边界、前后台同 Agent 串行、前台结束后队列 drain、恢复确认 gate、预算耗尽失败、默认工具白名单一致性,以及 delegate 成功 / 失败 / 取消 / 预算耗尽终态回执、`delegationId` 幂等去重和父 Agent receipt 续跑仍受 FIFO / 锁 / 确认 / 恢复门禁;前端覆盖统一事件展示、主工作区和独立开发 Agent 聊天窗口的默认恢复确认条与显式恢复 command。
@@ -51,6 +51,7 @@ Agent Runtime 负责:
- 2026-07-10 补充:Agent Runtime state 新增 `planSteps / activePlanStepIndex`,从 Agent 输出的 `plan` 派生结构化计划步骤,并在工具 action / observation / response / error 生命周期中更新 `pending / active / completed / failed` 和 detail;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示当前计划步骤与步骤进度,避免只能展示一串不可定位的 plan 文本。
- 2026-07-10 补充:`recentEvents` 接入前端归一态和 Runtime 状态面板,事件事实源仍是 `.agent/runtime/events/<agentId>.jsonl`;面板按时间展示最近 `thinking_summary / plan / action / observation / response / error` 事件,现在能同时看到 Agent 的计划、最近观察、最近事件、最近工具动作和任务队列。
- 2026-07-10 补充:后台 Runtime 每次追加 `.agent/runtime/events/<agentId>.jsonl` 后会通过 Tauri `game-creator-agent-runtime-update` 事件广播当前 `AgentRuntimeResult`,开发单 Agent 聊天页、项目内 Agent 对话弹窗和主窗口 Agent 状态卡用同一套前端归一化逻辑合并状态;该事件只做实时 UI 通知,`.agent/runtime/agents``events``tasks` 仍是重开项目后的事实源。
- 2026-07-10 补充:后台 Agent loop 的统一语义事件类型为 `thinking_summary / plan / action / observation / response / error`。普通失败和 loop 预算耗尽都会追加 `error` 事件,并继续保留 `turn.failed / turn.budget_exhausted` 生命周期事件兼容既有读取方;开发窗口、项目内 Agent 对话弹窗和主窗口状态卡通过现有最近事件列表直接展示统一错误事件及其安全详情。状态面板默认保持最新 4 条的紧凑视图,当前后端返回的最近事件超过 4 条时可展开查看全部返回记录,确保同一 run 的六类语义事件不会因 UI 硬截断而无法检查。
- 2026-07-10 补充:Agent Runtime state / result 新增 `taskQueue`,从 `.agent/runtime/tasks/<agentId>.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都读取该摘要,用于判断同一 Agent 是否仍有排队任务。该字段是运行观测摘要,不新增调度器、SQLite 或独立 worker。
- 2026-07-10 补充:Runtime 新增 `agent.schedule_ready` 调度入口。开发构建可在权限确认后扫描 manifest ready task,把依赖已完成且仍为 `pending` 的任务标成 `running`,并按 taskId 投递到对应 Agent 的既有后台队列;source 固定为 `agent-ready-task-scheduler`,审计记录写 `agent.runtime.ready_task.scheduled`。该入口只把 manifest ready task 接入现有 per-agent 队列、锁、JSONL、LLM loop、工具策略和事件流,不新增独立 worker,也不会在默认确认策略下静默启动。
- 2026-07-10 补充:主窗口 Agent 状态栏的“调度 Ready”只在开发模式显示。点击后复用项目策略确认弹窗,确认通过才调用 `schedule_game_creator_agent_ready_tasks`,并把返回的 Runtime 合并回 Agent 状态卡;普通用户窗口继续只展示状态和单 Agent 对话入口,不直接暴露 ready-task 调度按钮。