From f68862b981c932dff5e14ca2d88b643c9a3c39ed Mon Sep 17 00:00:00 2001 From: kris Date: Sat, 4 Apr 2026 07:35:32 +0800 Subject: [PATCH] feat: surface config drift across agent runs --- CHANGELOG.md | 16 ++++ collector-service/app/oneliner_features.py | 7 ++ tests/test_main_agent_governance.py | 5 ++ web/storyforge-web-v4/assets/app.js | 76 ++++++++++++++++++- .../tests/workbench-pages.test.mjs | 33 ++++++++ 5 files changed, 133 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bad9611..f03bc01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ ## 2026-04-04 +### 主 Agent 配置漂移提示与平台执行追溯 + +- 主 Agent 当前运行卡、执行结果卡现在不只展示 `配置 vN`,还会在发现本轮执行使用的是旧版主配置或旧版平台 Agent 配置时,直接标出 `主配置已更新 / 平台 Agent 已更新`。 +- 对于失败、阻塞、取消后的主 Agent 运行,如果当前配置已经变更,重试入口会明确显示成 `按当前配置重跑`,不再让用户自己盯着版本号判断要不要重开。 +- 平台 Agent 的 `recent_execution` 现在补上了更完整的追溯字段: + - `title / goal` + - `platform_scope` + - `delivery_mode` + - `active_executor_key` + - `source_action_key` +- 平台 Agent 总览卡和详情弹层已经开始直接使用这些 live 字段,最近执行不再只是“做过一次主 Agent 任务”的摘要,而是一条可判断范围和执行模式的业务记录。 +- 前端工作台回归新增了: + - 配置漂移提示与“按当前配置重跑”校验 + - 平台 Agent 最近执行 `title / platform_scope / delivery_mode` 展示校验 +- 后端治理回归也补上了 `recent_execution` 新字段断言,锁住这条主 Agent -> 平台 Agent 的执行追溯链。 + ### Playbook 与录制维护落点继续收口 - `创建 Agent / 编辑 Agent` 成功后,现在会直接回到 `Agent -> 当前 Agent / Agent 列表` 工作区,并把刚保存的 Agent 聚焦出来,不再只停在通用成功提示。 diff --git a/collector-service/app/oneliner_features.py b/collector-service/app/oneliner_features.py index dee2547..ac806b6 100644 --- a/collector-service/app/oneliner_features.py +++ b/collector-service/app/oneliner_features.py @@ -1488,6 +1488,7 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: return base_payload latest_result = _parse_json(run_row.get("result_json"), {}) latest_governance = _parse_json(run_row.get("governance_json"), {}) + latest_plan = _parse_json(run_row.get("plan_json"), {}) execution_card = (latest_result.get("execution_card") or {}) if isinstance(latest_result, dict) else {} result_sections = (latest_result.get("result_sections") or {}) if isinstance(latest_result, dict) else {} recommended_action = {} @@ -1505,6 +1506,12 @@ def register_oneliner_routes(app: Any, legacy: Any) -> None: ) return { **base_payload, + "title": str(run_row.get("title") or latest_plan.get("goal") or "").strip(), + "goal": str(latest_plan.get("goal") or run_row.get("title") or "").strip(), + "platform_scope": str(run_row.get("platform_scope") or "").strip(), + "delivery_mode": str(run_row.get("delivery_mode") or "").strip(), + "active_executor_key": str(run_row.get("active_executor_key") or "").strip(), + "source_action_key": str(run_row.get("source_action_key") or "").strip(), "oneliner_profile_version_id": str(oneliner_profile_version.get("version_id") or oneliner_profile_version.get("id") or "").strip(), "platform_agent_profile_version_id": str(platform_profile_version.get("version_id") or platform_profile_version.get("id") or "").strip(), "recommended_action": { diff --git a/tests/test_main_agent_governance.py b/tests/test_main_agent_governance.py index cdb1e98..7752f74 100644 --- a/tests/test_main_agent_governance.py +++ b/tests/test_main_agent_governance.py @@ -926,7 +926,12 @@ class MainAgentGovernanceTests(unittest.TestCase): self.assertIn("recent_execution", refreshed_douyin) self.assertEqual(refreshed_douyin["current_version"]["version_no"], rollback_profile_payload["current_version"]["version_no"]) self.assertEqual(refreshed_douyin["recent_execution"]["run_id"], run_payload["id"]) + self.assertEqual(refreshed_douyin["recent_execution"]["title"], "验证平台 Agent 执行回写") + self.assertEqual(refreshed_douyin["recent_execution"]["goal"], "验证平台 Agent 执行回写") self.assertEqual(refreshed_douyin["recent_execution"]["intent_key"], "governance_review") + self.assertEqual(refreshed_douyin["recent_execution"]["platform_scope"], "single_platform") + self.assertEqual(refreshed_douyin["recent_execution"]["delivery_mode"], "hybrid") + self.assertEqual(refreshed_douyin["recent_execution"]["source_action_key"], "platform-agent-handoff") self.assertGreaterEqual(refreshed_douyin["recent_execution"]["oneliner_profile_version_no"], 1) self.assertTrue(refreshed_douyin["recent_execution"]["oneliner_profile_version_id"]) self.assertTrue(refreshed_douyin["recent_execution"]["platform_agent_profile_version_id"]) diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 5c516ad..9a38f89 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -1136,6 +1136,14 @@ function renderOneLinerRunsHtml() { const canRetryCurrentRun = ["blocked", "failed", "cancelled"].includes(currentRun.run_status); const currentRunConfigVersion = currentRun.governance?.oneliner_profile_version || currentRun.governance?.oneliner_profile?.current_version || {}; const currentRunPlatformAgentProfile = currentRun.result?.execution_card?.platform_agent_profile || currentRun.governance?.platform_agent_profile || {}; + const latestOnelinerConfigVersion = appState.onelinerProfile?.current_version || {}; + const latestPlatformAgentProfile = safeArray(appState.platformAgents).find((item) => item.platform === currentRunPlatformAgentProfile.platform) || {}; + const currentRunOnelinerConfigStale = isConfigurationVersionStale(currentRunConfigVersion, latestOnelinerConfigVersion); + const currentRunPlatformAgentConfigStale = isConfigurationVersionStale( + currentRunPlatformAgentProfile, + latestPlatformAgentProfile.current_version || {} + ); + const retryRunLabel = currentRunOnelinerConfigStale || currentRunPlatformAgentConfigStale ? "按当前配置重跑" : "重新执行"; return `

近期运行概况

@@ -1192,7 +1200,7 @@ function renderOneLinerRunsHtml() { 确认执行 取消本轮 ` : ""} - ${canRetryCurrentRun ? `重新执行` : ""} + ${canRetryCurrentRun ? `${escapeHtml(retryRunLabel)}` : ""} ${hasResultPayload ? `查看结果` : ""} ${recommendedAction?.action ? `${escapeHtml(recommendedAction.label || "继续这个任务")}` : ""}
@@ -1233,6 +1241,7 @@ function renderOneLinerRunsHtml() {

${escapeHtml(currentRunConfigVersion.summary || currentRunConfigVersion.title || `OneLiner 配置 v${formatNumber(currentRunConfigVersion.version_no || 0)}`)}

配置 v${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))} + ${currentRunOnelinerConfigStale ? `主配置已更新` : ""} 查看配置历史
@@ -1246,6 +1255,7 @@ function renderOneLinerRunsHtml() { ${currentRunPlatformAgentProfile.name ? `${escapeHtml(currentRunPlatformAgentProfile.name)}` : ""} ${currentRunPlatformAgentProfile.assistant_name ? `${escapeHtml(currentRunPlatformAgentProfile.assistant_name)}` : ""} ${currentRunPlatformAgentProfile.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(currentRunPlatformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(currentRunPlatformAgentProfile.readiness_score || 0))}` : ""} + ${currentRunPlatformAgentConfigStale ? `平台 Agent 已更新` : ""} ` : ""} @@ -1267,7 +1277,7 @@ function renderOneLinerRunsHtml() { ` : ` ${escapeHtml(currentRun.status_summary || "主 Agent 正在推进中")} `} - ${canRetryCurrentRun ? `重新执行` : ""} + ${canRetryCurrentRun ? `${escapeHtml(retryRunLabel)}` : ""} ${hasResultPayload ? `查看结果` : ""} ${recommendedAction?.action ? `${escapeHtml(recommendedAction.label || "回到对应页面")}` : ""} @@ -1989,6 +1999,13 @@ function renderOneLinerExecutionPayloadHtml(payload) { const resultCards = safeArray(resultSections.cards).slice(0, 4); const configVersion = payload.execution_card?.oneliner_profile_version || payload.context?.oneliner_profile?.current_version || {}; const platformAgentProfile = payload.execution_card?.platform_agent_profile || {}; + const latestOnelinerConfigVersion = appState.onelinerProfile?.current_version || {}; + const latestPlatformAgentProfile = safeArray(appState.platformAgents).find((item) => item.platform === platformAgentProfile.platform) || {}; + const currentRunOnelinerConfigStale = isConfigurationVersionStale(configVersion, latestOnelinerConfigVersion); + const currentRunPlatformAgentConfigStale = isConfigurationVersionStale( + platformAgentProfile, + latestPlatformAgentProfile.current_version || {} + ); const landingAttrs = buildMainAgentLandingAttrs({ runId: landingRunId, screen: landingScreen, @@ -2003,6 +2020,8 @@ function renderOneLinerExecutionPayloadHtml(payload) { ${payload.platform ? `${escapeHtml(platformLabel(payload.platform))}` : ""} ${escapeHtml(payload.platform_scope === "all_platforms" ? "全平台" : "单平台")} ${configVersion.version_no ? `配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))}` : ""} + ${currentRunOnelinerConfigStale ? `主配置已更新` : ""} + ${currentRunPlatformAgentConfigStale ? `平台 Agent 已更新` : ""} 已收口 ${payload.recommended_action?.action ? `${escapeHtml(payload.recommended_action.label || "回到对应页面")}` : ""} @@ -2013,6 +2032,7 @@ function renderOneLinerExecutionPayloadHtml(payload) {

${escapeHtml(configVersion.summary || configVersion.title || `OneLiner 主配置版本 v${formatNumber(configVersion.version_no || 0)}`)}

配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))} + ${currentRunOnelinerConfigStale ? `主配置已更新` : ""} 查看配置历史
@@ -2026,6 +2046,7 @@ function renderOneLinerExecutionPayloadHtml(payload) { ${platformAgentProfile.name ? `${escapeHtml(platformAgentProfile.name)}` : ""} ${platformAgentProfile.assistant_name ? `${escapeHtml(platformAgentProfile.assistant_name)}` : ""} ${platformAgentProfile.version_no ? `${escapeHtml(platformLabel(platformAgentProfile.platform || payload.platform || ""))} Agent v${escapeHtml(formatNumber(platformAgentProfile.version_no || 0))}` : ""} + ${currentRunPlatformAgentConfigStale ? `平台 Agent 已更新` : ""} ${platformAgentProfile.platform && platformAgentProfile.version_no ? `看平台配置历史` : ""} ${platformAgentProfile.readiness_label ? `= 50 ? "blue" : "orange"}">${escapeHtml(platformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(platformAgentProfile.readiness_score || 0))}` : ""} @@ -4431,6 +4452,19 @@ function renderPlatformAgentPanel() {
${items.map((item) => ` + ${(() => { + const recentExecutionOnelinerConfigStale = isConfigurationVersionStale( + item.recent_execution || {}, + appState.onelinerProfile?.current_version || {} + ); + const recentExecutionPlatformConfigStale = isConfigurationVersionStale( + { + version_id: item.recent_execution?.platform_agent_profile_version_id, + version_no: item.recent_execution?.platform_agent_profile_version_no, + }, + item.current_version || {} + ); + return `
${escapeHtml(item.name || item.platform_label)}
${escapeHtml(item.mission || item.notes || "先绑定执行 Agent,再补任务目标和方法论。")}
@@ -4464,13 +4498,17 @@ function renderPlatformAgentPanel() { ${item.recent_execution?.run_id ? `

最近执行

-

${escapeHtml(item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}

+

${escapeHtml(item.recent_execution.title || item.recent_execution.goal || item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}

${escapeHtml(item.recent_execution.intent_label || "主 Agent 任务")} ${escapeHtml(item.recent_execution.run_status || "done")} + ${item.recent_execution.platform_scope ? `${escapeHtml(item.recent_execution.platform_scope === "all_platforms" ? "全平台" : "单平台")}` : ""} + ${item.recent_execution.delivery_mode ? `${escapeHtml(item.recent_execution.delivery_mode)}` : ""} ${item.recent_execution.workstream_label ? `${escapeHtml(item.recent_execution.workstream_label)}` : ""} ${item.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(item.recent_execution.oneliner_profile_version_no))}` : ""} + ${recentExecutionOnelinerConfigStale ? `主配置已更新` : ""} ${item.recent_execution.platform_agent_profile_version_no ? `${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent v${escapeHtml(formatNumber(item.recent_execution.platform_agent_profile_version_no))}` : ""} + ${recentExecutionPlatformConfigStale ? `${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent 已更新` : ""} ${item.recent_execution.source_screen ? `${escapeHtml(screenLabel(item.recent_execution.source_screen) || item.recent_execution.source_screen)}` : ""}
@@ -4488,6 +4526,8 @@ function renderPlatformAgentPanel() { 补技能
+ `; + })()} `).join("")}
@@ -7755,6 +7795,19 @@ function rememberAction(title, summary, tone = "blue", payload = null) { }; } +function getVersionIdentity(version = {}) { + return String(version?.version_id || version?.id || "").trim(); +} + +function isConfigurationVersionStale(runVersion, currentVersion) { + const runIdentity = getVersionIdentity(runVersion); + const currentIdentity = getVersionIdentity(currentVersion); + if (runIdentity && currentIdentity) return runIdentity !== currentIdentity; + const runNumber = Number(runVersion?.version_no || 0); + const currentNumber = Number(currentVersion?.version_no || 0); + return runNumber > 0 && currentNumber > 0 && runNumber !== currentNumber; +} + function extractGeneratedCopy(payload) { const raw = payload?.content || payload?.text || payload?.copy || payload?.result?.content || ""; return brief(raw, 2400); @@ -9534,6 +9587,17 @@ async function openPlatformAgentDetailAction(platform) { ]); const memories = safeArray(memoriesPayload?.items || memoriesPayload).slice(0, 6); const skills = safeArray(skillsPayload?.items || skillsPayload).slice(0, 6); + const recentExecutionOnelinerConfigStale = isConfigurationVersionStale( + profile?.recent_execution || {}, + appState.onelinerProfile?.current_version || {} + ); + const recentExecutionPlatformConfigStale = isConfigurationVersionStale( + { + version_id: profile?.recent_execution?.platform_agent_profile_version_id, + version_no: profile?.recent_execution?.platform_agent_profile_version_no, + }, + profile?.current_version || {} + ); const skillVersionEntries = await Promise.all( skills.map(async (item) => { const payload = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(normalizedPlatform)}/skills/${encodeURIComponent(item.id)}/versions?project_id=${encodeURIComponent(project.id)}`).catch(() => ({ items: [] })); @@ -9563,13 +9627,17 @@ async function openPlatformAgentDetailAction(platform) { ${profile.recent_execution?.run_id ? `

最近执行

-

${escapeHtml(profile.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}

+

${escapeHtml(profile.recent_execution.title || profile.recent_execution.goal || profile.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}

${escapeHtml(profile.recent_execution.intent_label || "主 Agent 任务")} ${escapeHtml(profile.recent_execution.run_status || "done")} + ${profile.recent_execution.platform_scope ? `${escapeHtml(profile.recent_execution.platform_scope === "all_platforms" ? "全平台" : "单平台")}` : ""} + ${profile.recent_execution.delivery_mode ? `${escapeHtml(profile.recent_execution.delivery_mode)}` : ""} ${profile.recent_execution.workstream_label ? `${escapeHtml(profile.recent_execution.workstream_label)}` : ""} ${profile.recent_execution.oneliner_profile_version_no ? `配置 v${escapeHtml(formatNumber(profile.recent_execution.oneliner_profile_version_no))}` : ""} + ${recentExecutionOnelinerConfigStale ? `主配置已更新` : ""} ${profile.recent_execution.platform_agent_profile_version_no ? `${escapeHtml(platformLabel(normalizedPlatform))} Agent v${escapeHtml(formatNumber(profile.recent_execution.platform_agent_profile_version_no))}` : ""} + ${recentExecutionPlatformConfigStale ? `${escapeHtml(platformLabel(normalizedPlatform))} Agent 已更新` : ""} ${profile.recent_execution.source_screen ? `${escapeHtml(screenLabel(profile.recent_execution.source_screen) || profile.recent_execution.source_screen)}` : ""}
diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index e2cdca9..148bafe 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -782,6 +782,28 @@ test("platform agent profiles expose history, rollback, and execution version co assert.match(actions, /openPlatformAgentProfileHistoryAction/); }); +test("platform agent recent execution highlights when newer configs exist", () => { + const detail = extractBetween(APP, "async function openPlatformAgentDetailAction(platform)", "function openPlatformSkillReviewAction(platform, skillId, accepted)"); + const panel = extractBetween(APP, "function renderPlatformAgentPanel()", "function renderAdminOpsPanel()"); + assert.match(detail, /recentExecutionOnelinerConfigStale/); + assert.match(detail, /recentExecutionPlatformConfigStale/); + assert.match(detail, /主配置已更新/); + assert.match(detail, /Agent 已更新/); + assert.match(panel, /recentExecutionOnelinerConfigStale/); + assert.match(panel, /recentExecutionPlatformConfigStale/); +}); + +test("platform agent recent execution surfaces title, platform scope, and delivery mode", () => { + const detail = extractBetween(APP, "async function openPlatformAgentDetailAction(platform)", "function openPlatformSkillReviewAction(platform, skillId, accepted)"); + const panel = extractBetween(APP, "function renderPlatformAgentPanel()", "function renderAdminOpsPanel()"); + assert.match(detail, /recent_execution\.title/); + assert.match(detail, /recent_execution\.platform_scope/); + assert.match(detail, /recent_execution\.delivery_mode/); + assert.match(panel, /recent_execution\.title/); + assert.match(panel, /recent_execution\.platform_scope/); + assert.match(panel, /recent_execution\.delivery_mode/); +}); + test("main agent route actions keep landing context and destination screens render a notice", () => { const execution = extractBetween(APP, "function renderOneLinerExecutionPayloadHtml(payload)", "function parseOneLinerActionPayloadValue(value)"); const actions = extractBetween(APP, "document.addEventListener(\"click\", async (event) => {", "document.addEventListener(\"submit\", async (event) => {"); @@ -979,6 +1001,17 @@ test("oneliner runtime exposes retry for retryable runs and wires the action han assert.match(actions, /await retryOneLinerRun\(action\.dataset\.runId \|\| "", "user requested retry"\)/); }); +test("oneliner runtime highlights stale configuration versions and suggests rerunning with current config", () => { + const runtime = extractBetween(APP, "function renderOneLinerRunsHtml()", "function renderOneLinerMessagesHtml()"); + assert.match(APP, /function getVersionIdentity\(version = \{\}\)/); + assert.match(APP, /function isConfigurationVersionStale\(runVersion, currentVersion\)/); + assert.match(runtime, /currentRunOnelinerConfigStale/); + assert.match(runtime, /currentRunPlatformAgentConfigStale/); + assert.match(runtime, /主配置已更新/); + assert.match(runtime, /平台 Agent 已更新/); + assert.match(runtime, /按当前配置重跑/); +}); + test("oneliner panel auto-polls active runs while the floating panel stays open", () => { const render = extractBetween(APP, "function renderOneLinerUi()", "function openOneLinerPanel()"); const open = extractBetween(APP, "function openOneLinerPanel()", "function closeOneLinerPanel()");