feat: surface config drift across agent runs
Some checks failed
StoryForge CI / Baseline checks (push) Has been cancelled
StoryForge CI / Backend tests (push) Has been cancelled
StoryForge CI / Web tests (push) Has been cancelled

This commit is contained in:
kris
2026-04-04 07:35:32 +08:00
parent 96446a25df
commit f68862b981
5 changed files with 133 additions and 4 deletions

View File

@@ -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 聚焦出来,不再只停在通用成功提示。

View File

@@ -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": {

View File

@@ -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"])

View File

@@ -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 `
<div class="task-item compact" style="margin-bottom:10px;">
<h4>近期运行概况</h4>
@@ -1192,7 +1200,7 @@ function renderOneLinerRunsHtml() {
<span class="tag clickable-tag" data-action="confirm-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">确认执行</span>
<span class="tag clickable-tag" data-action="cancel-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">取消本轮</span>
` : ""}
${canRetryCurrentRun ? `<span class="tag clickable-tag" data-action="retry-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">重新执行</span>` : ""}
${canRetryCurrentRun ? `<span class="tag clickable-tag" data-action="retry-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">${escapeHtml(retryRunLabel)}</span>` : ""}
${hasResultPayload ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(currentRun.id)}">查看结果</span>` : ""}
${recommendedAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(recommendedAction.action)}" ${resultLandingAttrs}>${escapeHtml(recommendedAction.label || "继续这个任务")}</span>` : ""}
</div>
@@ -1233,6 +1241,7 @@ function renderOneLinerRunsHtml() {
<p>${escapeHtml(currentRunConfigVersion.summary || currentRunConfigVersion.title || `OneLiner 配置 v${formatNumber(currentRunConfigVersion.version_no || 0)}`)}</p>
<div class="task-meta">
<span class="tag blue">配置 v${escapeHtml(formatNumber(currentRunConfigVersion.version_no || 0))}</span>
${currentRunOnelinerConfigStale ? `<span class="tag orange">主配置已更新</span>` : ""}
<span class="tag clickable-tag" data-action="open-oneliner-profile-history">查看配置历史</span>
</div>
</div>
@@ -1246,6 +1255,7 @@ function renderOneLinerRunsHtml() {
${currentRunPlatformAgentProfile.name ? `<span class="tag">${escapeHtml(currentRunPlatformAgentProfile.name)}</span>` : ""}
${currentRunPlatformAgentProfile.assistant_name ? `<span class="tag green">${escapeHtml(currentRunPlatformAgentProfile.assistant_name)}</span>` : ""}
${currentRunPlatformAgentProfile.readiness_label ? `<span class="tag ${currentRunPlatformAgentProfile.readiness_score >= 75 ? "green" : currentRunPlatformAgentProfile.readiness_score >= 50 ? "blue" : "orange"}">${escapeHtml(currentRunPlatformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(currentRunPlatformAgentProfile.readiness_score || 0))}</span>` : ""}
${currentRunPlatformAgentConfigStale ? `<span class="tag orange">平台 Agent 已更新</span>` : ""}
</div>
</div>
` : ""}
@@ -1267,7 +1277,7 @@ function renderOneLinerRunsHtml() {
` : `
<span class="tag ${statusTone}">${escapeHtml(currentRun.status_summary || "主 Agent 正在推进中")}</span>
`}
${canRetryCurrentRun ? `<span class="tag clickable-tag" data-action="retry-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">重新执行</span>` : ""}
${canRetryCurrentRun ? `<span class="tag clickable-tag" data-action="retry-oneliner-run" data-run-id="${escapeHtml(currentRun.id)}">${escapeHtml(retryRunLabel)}</span>` : ""}
${hasResultPayload ? `<span class="tag clickable-tag" data-action="open-oneliner-run-result" data-run-id="${escapeHtml(currentRun.id)}">查看结果</span>` : ""}
${recommendedAction?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(recommendedAction.action)}" ${resultLandingAttrs}>${escapeHtml(recommendedAction.label || "回到对应页面")}</span>` : ""}
</div>
@@ -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 ? `<span class="tag blue">${escapeHtml(platformLabel(payload.platform))}</span>` : ""}
<span class="tag">${escapeHtml(payload.platform_scope === "all_platforms" ? "全平台" : "单平台")}</span>
${configVersion.version_no ? `<span class="tag">配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))}</span>` : ""}
${currentRunOnelinerConfigStale ? `<span class="tag orange">主配置已更新</span>` : ""}
${currentRunPlatformAgentConfigStale ? `<span class="tag orange">平台 Agent 已更新</span>` : ""}
<span class="tag green">已收口</span>
${payload.recommended_action?.action ? `<span class="tag clickable-tag" data-action="${escapeHtml(payload.recommended_action.action)}" data-main-agent-run-id="${escapeHtml(landingRunId)}" data-main-agent-screen="${escapeHtml(landingScreen)}" data-main-agent-title="${escapeHtml(landingTitle)}" data-main-agent-summary="${escapeHtml(landingSummary)}" ${landingAttrs}>${escapeHtml(payload.recommended_action.label || "回到对应页面")}</span>` : ""}
</div>
@@ -2013,6 +2032,7 @@ function renderOneLinerExecutionPayloadHtml(payload) {
<p>${escapeHtml(configVersion.summary || configVersion.title || `OneLiner 主配置版本 v${formatNumber(configVersion.version_no || 0)}`)}</p>
<div class="task-meta">
<span class="tag blue">配置 v${escapeHtml(formatNumber(configVersion.version_no || 0))}</span>
${currentRunOnelinerConfigStale ? `<span class="tag orange">主配置已更新</span>` : ""}
<span class="tag clickable-tag" data-action="open-oneliner-profile-history">查看配置历史</span>
</div>
</div>
@@ -2026,6 +2046,7 @@ function renderOneLinerExecutionPayloadHtml(payload) {
${platformAgentProfile.name ? `<span class="tag">${escapeHtml(platformAgentProfile.name)}</span>` : ""}
${platformAgentProfile.assistant_name ? `<span class="tag green">${escapeHtml(platformAgentProfile.assistant_name)}</span>` : ""}
${platformAgentProfile.version_no ? `<span class="tag">${escapeHtml(platformLabel(platformAgentProfile.platform || payload.platform || ""))} Agent v${escapeHtml(formatNumber(platformAgentProfile.version_no || 0))}</span>` : ""}
${currentRunPlatformAgentConfigStale ? `<span class="tag orange">平台 Agent 已更新</span>` : ""}
${platformAgentProfile.platform && platformAgentProfile.version_no ? `<span class="tag clickable-tag" data-action="open-platform-agent-profile-history" data-platform="${escapeHtml(platformAgentProfile.platform)}">看平台配置历史</span>` : ""}
${platformAgentProfile.readiness_label ? `<span class="tag ${platformAgentProfile.readiness_score >= 75 ? "green" : platformAgentProfile.readiness_score >= 50 ? "blue" : "orange"}">${escapeHtml(platformAgentProfile.readiness_label)} ${escapeHtml(formatNumber(platformAgentProfile.readiness_score || 0))}</span>` : ""}
</div>
@@ -4431,6 +4452,19 @@ function renderPlatformAgentPanel() {
</div>
<div class="three-col">
${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 `
<div class="entity-card pad">
<div class="cell-title">${escapeHtml(item.name || item.platform_label)}</div>
<div class="cell-desc">${escapeHtml(item.mission || item.notes || "先绑定执行 Agent再补任务目标和方法论。")}</div>
@@ -4464,13 +4498,17 @@ function renderPlatformAgentPanel() {
${item.recent_execution?.run_id ? `
<div class="task-item compact" style="margin-top:10px;">
<h4>最近执行</h4>
<p>${escapeHtml(item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}</p>
<p>${escapeHtml(item.recent_execution.title || item.recent_execution.goal || item.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(item.recent_execution.intent_label || "主 Agent 任务")}</span>
<span class="tag">${escapeHtml(item.recent_execution.run_status || "done")}</span>
${item.recent_execution.platform_scope ? `<span class="tag">${escapeHtml(item.recent_execution.platform_scope === "all_platforms" ? "全平台" : "单平台")}</span>` : ""}
${item.recent_execution.delivery_mode ? `<span class="tag">${escapeHtml(item.recent_execution.delivery_mode)}</span>` : ""}
${item.recent_execution.workstream_label ? `<span class="tag green">${escapeHtml(item.recent_execution.workstream_label)}</span>` : ""}
${item.recent_execution.oneliner_profile_version_no ? `<span class="tag">配置 v${escapeHtml(formatNumber(item.recent_execution.oneliner_profile_version_no))}</span>` : ""}
${recentExecutionOnelinerConfigStale ? `<span class="tag orange">主配置已更新</span>` : ""}
${item.recent_execution.platform_agent_profile_version_no ? `<span class="tag">${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent v${escapeHtml(formatNumber(item.recent_execution.platform_agent_profile_version_no))}</span>` : ""}
${recentExecutionPlatformConfigStale ? `<span class="tag orange">${escapeHtml(item.platform_label || platformLabel(item.platform))} Agent 已更新</span>` : ""}
${item.recent_execution.source_screen ? `<span class="tag">${escapeHtml(screenLabel(item.recent_execution.source_screen) || item.recent_execution.source_screen)}</span>` : ""}
</div>
<div class="task-meta" style="margin-top:8px;">
@@ -4488,6 +4526,8 @@ function renderPlatformAgentPanel() {
<span class="tag clickable-tag" data-action="open-platform-agent-skill" data-platform="${escapeHtml(item.platform)}">补技能</span>
</div>
</div>
`;
})()}
`).join("")}
</div>
</div>
@@ -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 ? `
<div class="task-item compact" style="margin-top:12px;">
<h4>最近执行</h4>
<p>${escapeHtml(profile.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}</p>
<p>${escapeHtml(profile.recent_execution.title || profile.recent_execution.goal || profile.recent_execution.summary || "最近一次主 Agent 执行已回写到当前平台 Agent。")}</p>
<div class="task-meta">
<span class="tag blue">${escapeHtml(profile.recent_execution.intent_label || "主 Agent 任务")}</span>
<span class="tag">${escapeHtml(profile.recent_execution.run_status || "done")}</span>
${profile.recent_execution.platform_scope ? `<span class="tag">${escapeHtml(profile.recent_execution.platform_scope === "all_platforms" ? "全平台" : "单平台")}</span>` : ""}
${profile.recent_execution.delivery_mode ? `<span class="tag">${escapeHtml(profile.recent_execution.delivery_mode)}</span>` : ""}
${profile.recent_execution.workstream_label ? `<span class="tag green">${escapeHtml(profile.recent_execution.workstream_label)}</span>` : ""}
${profile.recent_execution.oneliner_profile_version_no ? `<span class="tag">配置 v${escapeHtml(formatNumber(profile.recent_execution.oneliner_profile_version_no))}</span>` : ""}
${recentExecutionOnelinerConfigStale ? `<span class="tag orange">主配置已更新</span>` : ""}
${profile.recent_execution.platform_agent_profile_version_no ? `<span class="tag">${escapeHtml(platformLabel(normalizedPlatform))} Agent v${escapeHtml(formatNumber(profile.recent_execution.platform_agent_profile_version_no))}</span>` : ""}
${recentExecutionPlatformConfigStale ? `<span class="tag orange">${escapeHtml(platformLabel(normalizedPlatform))} Agent 已更新</span>` : ""}
${profile.recent_execution.source_screen ? `<span class="tag">${escapeHtml(screenLabel(profile.recent_execution.source_screen) || profile.recent_execution.source_screen)}</span>` : ""}
</div>
<div class="task-meta" style="margin-top:8px;">

View File

@@ -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()");