From b4a997d5d1f2103e2b011714e9cc390ddbfdf9fd Mon Sep 17 00:00:00 2001 From: EMRG Evolution Date: Wed, 19 Aug 2026 07:15:18 +0800 Subject: [PATCH] emrg: fix vibe-check done-frame content loss + evidence-driven prompt template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (rant 2026-08-19T07:10:40): the normal-completion done frame broadcast "content": "" — the agent's full final reply was persisted to session but dropped from the done frame, so the scheduler's completion_summary was empty and the memoryless vibe-check LLM judged meaningful=false with an empty done, producing machine fallback strings in the GUI. - daemon.py: done frame now carries full_content or "" (normal completion) - _task_vibe_check: rewritten evidence-driven, rendered from new prompts/vibe_check.j2 (same live-reload jinja2 mechanism as system.j2) - scheduler: removed completion-first-line summary fallback; status() empty summary -> None (no impact machine fallback) - GUI dialogs.js: empty done shows "-" (no impact fallback) - tests updated to assert the no-fallback contract --- emrg/gui/renderer/js/dialogs.js | 4 ++-- emrg/gui/test/renderer.smoke.test.js | 4 ++-- emrg/server/daemon.py | 35 ++++++++++++---------------- emrg/server/prompts/vibe_check.j2 | 27 +++++++++++++++++++++ emrg/server/scheduler.py | 18 +++++--------- tests/test_scheduler.py | 20 ++++++++-------- 6 files changed, 62 insertions(+), 46 deletions(-) create mode 100644 emrg/server/prompts/vibe_check.j2 diff --git a/emrg/gui/renderer/js/dialogs.js b/emrg/gui/renderer/js/dialogs.js index 448c7797..4cecffab 100644 --- a/emrg/gui/renderer/js/dialogs.js +++ b/emrg/gui/renderer/js/dialogs.js @@ -701,7 +701,8 @@ const Dialogs = (() => { // rant 2026-08-18T21:32:32:任务卡点击 → 手风琴展开最近运行子表 // (时间 / 干了什么 / 降频)。数据来自 daemon handler.status().recent_runs - // (最多 5 条);旧数据无 summary → fallback impact 拼接;无记录 → 占位文案。 + // (最多 5 条)。Rant 2026-08-19T07:06:45(宿主定稿):无 summary 不再 + // fallback impact 机器串——空则显示 "-";无记录 → 占位文案。 function buildTaskRunDetail(t) { const wrap = el("div", { class: "task-run-detail hidden" }); const runs = Array.isArray(t.recent_runs) ? t.recent_runs : []; @@ -718,7 +719,6 @@ const Dialogs = (() => { const row = el("div", { class: "task-run-row" }); row.appendChild(el("span", { class: "task-run-time" }, formatRelativeTime(r.timestamp))); let done = (typeof r.summary === "string" && r.summary) ? r.summary : ""; - if (!done && Array.isArray(r.impact) && r.impact.length) done = r.impact.join(", "); row.appendChild(el("span", { class: "task-run-done" }, done || "-")); const flagCell = el("span", { class: "task-run-flag" }); if (r.recommend_slowdown) { diff --git a/emrg/gui/test/renderer.smoke.test.js b/emrg/gui/test/renderer.smoke.test.js index fa87bad1..e5891446 100644 --- a/emrg/gui/test/renderer.smoke.test.js +++ b/emrg/gui/test/renderer.smoke.test.js @@ -2688,9 +2688,9 @@ test("rant 21:32:32:任务卡点击展开最近运行子表(时间/干了什 assert.ok(warnCount >= 1, "recommend_slowdown=true 应显示建议降频徽章"); const idleCount = vm.runInContext(`document.getElementById("task-list").children[0].querySelectorAll(".task-run-badge-idle").length`, ctx); assert.ok(idleCount >= 1, "meaningful=false 应显示空转徽章"); - // 旧数据无 summary → fallback impact 拼接(第二行 run) + // 无 summary → 显示 "-"(rant 2026-08-19T07:06:45 宿主定稿:不再 fallback impact 机器串) const doneTexts = vm.runInContext(`Array.from(document.getElementById("task-list").children[0].querySelectorAll(".task-run-done")).map((n) => n.textContent)`, ctx); - assert.ok(doneTexts[1].includes("cycle-ts-complete"), `无 summary 应 fallback impact,实际: ${doneTexts[1]}`); + assert.strictEqual(doneTexts[1], "-", `无 summary 应显示 "-",实际: ${doneTexts[1]}`); // 再次点击 → 折叠 await vm.runInContext(`document.getElementById("task-list").children[0].click()`, ctx); await tick(); diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index c938e89b..67881bc3 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -1160,32 +1160,27 @@ async def _task_vibe_check(self, task_name: str, prompt: str, completion_summary what meaningful work was done this cycle, for humans to read in the GUI task recent-runs table. Old models / old parsing omit it → "". + Rant 2026-08-19T07:10:40 (root cause): the done frame used to carry an + empty ``content``, so ``completion_summary`` here was empty and the + memoryless LLM could not judge what happened. The done frame now + carries the agent's full final reply (daemon.py done broadcast), so + this prompt is evidence-driven: judge from the real final reply, not + from an empty shell. System + user messages live in + ``prompts/vibe_check.j2`` (same live-reload mechanism as system.j2). + Raises on any failure (caller sends ``ok: false``); the scheduler conservatively leaves its empty-cycle counter unchanged then. """ - system = ( - "你是 EMRG 定时任务调度助手。刚完成一次定时任务「" + (task_name or "") + "」," - "任务要求与最终回复摘要如下。\n" - "请用 JSON 严格回答(不要任何其他文字),格式:\n" - '{"meaningful": true|false, "recommend_slowdown": true|false, ' - '"reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"}\n' - "- meaningful:这轮是否对项目产生了有意义的价值(产出/提交/分析/决策/" - "维护动作都算;纯空转/无可做=NTE 算 false)\n" - "- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token" - "(true=建议降低检查频率)\n" - "- reason:简短中文原因(给降频判断用)\n" - "- done:这次干了哪些有意义有价值的事(自然语言列举,如:修了 XX bug / " - "加了 XX 功能 / 分析了 XX;没有则空字符串)" - ) - user = ( - "任务名称:" + (task_name or "") + "\n" - "任务要求:" + (prompt or "")[:2000] + "\n" - "任务最终回复摘要:" + (completion_summary or "")[:3000] + template = _get_jinja_env().get_template("vibe_check.j2") + system = template.render( + task_name=task_name or "", + prompt=(prompt or "")[:2000], + completion_summary=(completion_summary or "")[:3000], ) msg = await self.llm.chat( [ {"role": "system", "content": system}, - {"role": "user", "content": user}, + {"role": "user", "content": "请基于以上任务信息,严格按 system 中要求的 JSON 格式回答。"}, ], tools=[], ) @@ -2411,7 +2406,7 @@ async def _run_tool_loop( await self._broadcast(session.session_id, { "request_id": req.id, - "content": "", + "content": full_content or "", "done": True, "delta": False, "session_id": session.session_id, diff --git a/emrg/server/prompts/vibe_check.j2 b/emrg/server/prompts/vibe_check.j2 new file mode 100644 index 00000000..7ea5b40b --- /dev/null +++ b/emrg/server/prompts/vibe_check.j2 @@ -0,0 +1,27 @@ +{# Vibe check prompt (rant 2026-08-19T07:10:40) — evidence-driven, one-shot. + Rendered by daemon._task_vibe_check via _get_jinja_env() (FileSystemLoader, + same live-reload mechanism as system.j2: editing this template takes effect + without a daemon restart). Context: task_name, prompt, completion_summary. #} +你是 EMRG 定时任务调度助手。刚完成一次定时任务「{{ task_name }}」,以下是该任务的 +实际信息(任务要求 + Agent 最终回复摘要)。请基于这些**真实输入**做总结与判断: +只有信息显示确实没做任何事(无回复、无动作)才算空转;不要因为"没有 commit/PR"就 +武断判为无产出——工具执行、分析、排查、写 memory、决策等具体动作都是实际工作。 + +请用 JSON 严格回答(不要任何其他文字),格式: +{"meaningful": true|false, "recommend_slowdown": true|false, "reason": "一句话原因", "done": "这次干了哪些有意义有价值的事"} + +字段要求: +- meaningful:这轮是否对项目产生了有意义的价值。只要任务最终回复/工作内容显示执行了 + 具体动作(工具调用、分析、排查、写 memory、决策、产出等)即为 true;只有确实 + 空转(无回复、无任何动作)才为 false。 +- recommend_slowdown:若本任务长期无有意义产出,是否建议降频省 token + (true=建议降低检查频率)。 +- reason:简短中文原因(给降频判断用),引用实际信息说明依据。 +- done:从任务最终回复/工作内容中**提炼**这次具体做了什么(工具动作、产出、分析, + 自然语言列举,如:执行了 N 次工具调用,git fetch 检查了 X、分析了 Y、写了 + memory Z)。**只要回复/工作内容有实质内容就必须写具体动作**;回复确实为空、 + 纯空转才写空字符串 ""。 + +任务名称:{{ task_name }} +任务要求:{{ prompt }} +任务最终回复摘要:{{ completion_summary }} diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index ee97a269..5ff26584 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -779,13 +779,9 @@ def status(self) -> dict: if self.evolutions: last = self.evolutions[-1] last_run_at = last.timestamp - # brief summary: agent's natural-language "done" summary preferred - # (rant 2026-08-18T21:32:32), fallback to machine impact tags. - if last.summary: - last_cycle_summary = last.summary - else: - parts = [str(i) for i in last.impact if i] - last_cycle_summary = ", ".join(parts[:3]) if parts else None + # rant 2026-08-19T07:06:45 (host-finalized): NO machine impact + # fallback — empty summary shows as None (GUI renders "-"). + last_cycle_summary = last.summary if last.summary else None # rant 2026-08-18T21:32:32: last 5 run records for the GUI accordion # subtable — {timestamp, summary, impact, meaningful, # recommend_slowdown, tool_count}; all in-memory, no extra I/O. @@ -1120,8 +1116,9 @@ async def _run_evolution_cycle(self) -> None: # rant 2026-08-18T21:32:32: persist the agent's own summary of what # meaningful work was done (vibe check "done" field) + the vibe flags, # so the GUI task recent-runs table shows real value, not a machine - # string. Fallbacks: vibe unavailable → None flags + first line of the - # completion summary as a rough summary (never crash). + # string. Rant 2026-08-19T07:06:45 (host-finalized): NO fallback to the + # completion first line — summary uses only the vibe check "done" + # field; empty stays empty (GUI shows "-"), never a machine fallback. summary = "" meaningful = None recommend = False @@ -1129,9 +1126,6 @@ async def _run_evolution_cycle(self) -> None: summary = str(vibe_result.get("done") or "")[:500] meaningful = vibe_result.get("meaningful") recommend = bool(vibe_result.get("recommend_slowdown")) - if not summary and completion_content: - first = completion_content.strip().splitlines()[0] if completion_content.strip() else "" - summary = first[:500] log = EvolutionLog( timestamp=cycle_ts, diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index a54045c3..cdf0931c 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -518,7 +518,8 @@ def test_evolution_handler_status_last_run_fields(): handler._empty_cycles = 3 st = handler.status() assert st["last_run_at"] == "2026-08-18T10:00:00" - assert st["last_cycle_summary"] == "tools-executed=24, cycle-complete" + # rant 2026-08-19T07:06:45: empty summary → None (no machine impact fallback) + assert st["last_cycle_summary"] is None assert st["saturation"]["empty_cycles"] == 3 assert len(st["recent_runs"]) == 1 r0 = st["recent_runs"][0] @@ -1673,10 +1674,10 @@ def test_evolution_cycle_complete_agent_says_meaningful_resets_streak(tmp_path): assert log.tool_count == 0 -def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path): - """Vibe check ok but missing 'done' → log.summary falls back to the first - line of the completion content; vibe unavailable → empty summary (never - crash). Rant 2026-08-18T21:32:32.""" +def test_evolution_cycle_log_summary_no_completion_fallback(tmp_path): + """Rant 2026-08-19T07:06:45 (host-finalized): the summary uses ONLY the + vibe check "done" field — NO fallback to the completion first line. Empty + stays empty (GUI renders "-"), never a machine/rough fallback.""" handler, captured = _make_cycle_handler(tmp_path, frames=[ {"request_id": "r1", "content": "Reviewed PR and posted LGTM", "done": True, "delta": False, "session_id": "s"}, @@ -1686,19 +1687,18 @@ def test_evolution_cycle_log_summary_falls_back_to_completion(tmp_path): ]) asyncio.run(handler._run_evolution_cycle()) log = captured["log"] - assert log.summary == "Reviewed PR and posted LGTM", \ - "missing done → completion first line fallback" + assert log.summary == "", \ + "missing done → summary stays empty (no completion fallback)" assert log.meaningful is True - # vibe check entirely unavailable → summary falls back to the first line - # of the completion content (per rant design), flags None/False + # vibe check entirely unavailable → summary stays empty, flags None/False handler2, captured2 = _make_cycle_handler(tmp_path, frames=[ {"request_id": "r1", "content": "Done", "done": True, "delta": False, "session_id": "s"}, ]) asyncio.run(handler2._run_evolution_cycle()) log2 = captured2["log"] - assert log2.summary == "Done", "vibe unavailable → completion first line fallback" + assert log2.summary == "", "vibe unavailable → summary stays empty (no fallback)" assert log2.meaningful is None assert log2.recommend_slowdown is False assert log2.tool_count == 0