diff --git a/dashboard/server.py b/dashboard/server.py
index 351222c..66f1582 100644
--- a/dashboard/server.py
+++ b/dashboard/server.py
@@ -2548,9 +2548,94 @@ def _run_turn_panels_html(run_id: str, candidates: list, terminal_state) -> str:
)
+def _agent_events_html(run_id):
+ """链路透视 · 事件账本:探索轨迹(读了什么)与模型回合(example_agent_event)。"""
+
+ if not table_exists("example_agent_event"):
+ return empty_gap("事件账本 · 待建表", "事件账本表 example_agent_event 尚未建立。")
+ cols, rows = q(
+ "SELECT seq,event_type,tool_name,status,actual_model_id,input_tokens,output_tokens,cost_usd,details "
+ "FROM example_agent_event WHERE run_id=%s ORDER BY seq",
+ (run_id,),
+ )
+ events = _row_dicts(cols, rows)
+ if not events:
+ return empty_gap("事件账本 · 该运行暂无记录", "框架派发运行后,探索轨迹与模型回合在此逐条可见。")
+
+ def _details_text(value):
+ if value is None:
+ return "—"
+ text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
+ return esc(text[:300] + ("…" if len(text) > 300 else ""))
+
+ tool_events = [e for e in events if str(e["event_type"]).startswith("tool.")]
+ model_events = [e for e in events if str(e["event_type"]).startswith("model.")]
+ tool_rows = "".join(
+ f"
| {esc(e['seq'])} | {esc(e['event_type'])} | "
+ f"{esc(e['tool_name'])} | {chip(e['status'])} | "
+ f"{_details_text(e['details'])} |
"
+ for e in tool_events
+ )
+ model_rows = "".join(
+ f"| {esc(e['seq'])} | {esc(e['event_type'])} | "
+ f"{esc(e['actual_model_id'])} | {chip(e['status'])} | "
+ f"{fmt_num(e['input_tokens'])} | {fmt_num(e['output_tokens'])} | "
+ f"{fmt_num(e['cost_usd'])} |
"
+ for e in model_events
+ )
+ tool_table = (
+ "探索轨迹(工具事件)
"
+ "| 序 | 事件类型 | 工具 | 状态 | 参数/明细 |
"
+ f"{tool_rows}
" if tool_events
+ else "探索轨迹(工具事件)
本运行未发生工具探索。
"
+ )
+ model_table = (
+ "模型回合
"
+ "| 序 | 事件类型 | 实际模型编码 | 状态 | "
+ "输入令牌 | 输出令牌 | 成本美元 |
"
+ f"{model_rows}
" if model_events
+ else "模型回合
本运行无模型回合事件。
"
+ )
+ return f"{tool_table}{model_table}
"
+
+
+def _dispatch_chain_html(run_id, trigger_detail):
+ """链路透视 · 派发链:经 trigger_detail 关联的生产/评测父运行与派发子运行。"""
+
+ parent_ref = None
+ if isinstance(trigger_detail, dict):
+ parent_ref = trigger_detail.get("productionRunId") or trigger_detail.get("evaluationRunId")
+ _, child_rows = q(
+ "SELECT run_id, terminal_state, trigger_detail->>'stage' AS stage "
+ "FROM example_run WHERE deleted=false AND "
+ "(trigger_detail->>'productionRunId'=%s OR trigger_detail->>'evaluationRunId'=%s) "
+ "ORDER BY started_at",
+ (run_id, run_id),
+ )
+ if not parent_ref and not child_rows:
+ return ""
+ parts = ["派发链(父子运行)
"]
+ if parent_ref:
+ parts.append(
+ f"
父运行:{esc(str(parent_ref))}
"
+ )
+ if child_rows:
+ parts.append(
+ "
| 派发子运行 | 终态 | 阶段 |
"
+ + "".join(
+ f"{esc(c[0])} | "
+ f"{chip(c[1])} | {esc(c[2] or '—')} |
" for c in child_rows
+ )
+ + "
"
+ )
+ parts.append("
")
+ return "".join(parts)
+
+
def view_run_detail(run_id):
_, rr = q(
"""SELECT r.run_id,r.work_id,r.target_chapter,r.trigger_source,r.started_at,r.finished_at,r.terminal_state,
+ r.trigger_detail,
w.title AS work_title,c.id AS chapter_id,c.order_no AS chapter_order,c.title AS chapter_title
FROM example_run r
LEFT JOIN muse_content_work w ON w.id=r.work_id
@@ -2560,7 +2645,7 @@ def view_run_detail(run_id):
)
if not rr:
return layout("运行", "运行不存在
", "/runs")
- rid, wid, chap, trig, started, finished, state, work_title, chapter_id, chapter_order, chapter_title = rr[0]
+ rid, wid, chap, trig, started, finished, state, trigger_detail, work_title, chapter_id, chapter_order, chapter_title = rr[0]
rc_cols, rc_rows = q(
"""SELECT id,sample_id,revision,stage_kind,adapter_role,attempt,loop_seq,candidate_version,is_error,
stop_reason,terminal_reason,safe_summary,raw_content_id,requested_model_id,actual_model_id,
@@ -2758,9 +2843,12 @@ def view_run_detail(run_id):
error_items.append("运行终态为失败,但回执和质量表没有提供更细错误记录。
")
error_html = ("错误与诊断
" + "".join(error_items) + "
") if error_items else ""
turn_html = _run_turn_panels_html(rid, candidates, state)
+ events_html = _agent_events_html(rid)
+ chain_html = _dispatch_chain_html(rid, trigger_detail)
body = (f""
f"运行详情
{info}{error_html}{turn_html}"
f"回执链
{rc_html}质量评判
{q_html}模型调用
{call_html}"
+ f"链路透视
{chain_html}{events_html}"
f"候选与决策
{cand}{dec}证据与上下文
{raw_html}{raw_detail_html}{freeze_html}")
return layout("运行详情", body, "/runs")
diff --git a/docs/plans/2026-08-22-阶段G-第一部分-链路透视接线-事件账本与派发链.md b/docs/plans/2026-08-22-阶段G-第一部分-链路透视接线-事件账本与派发链.md
new file mode 100644
index 0000000..6b01138
--- /dev/null
+++ b/docs/plans/2026-08-22-阶段G-第一部分-链路透视接线-事件账本与派发链.md
@@ -0,0 +1,37 @@
+# 阶段 G 第一部分:链路透视接线——事件账本与派发链上看板
+
+日期:2026-08-23
+状态:已完成(看板测试绿 + 真实运行页走查通过)
+上游事实:两阶段写手、抽取、盲评派发运行已在 `example_agent_event` 与 `example_run` 留下真实数据;§11 矩阵中"探索轨迹与事件链""依赖清单"此前为接线缺口。
+
+## 1. 意图
+
+把框架派发链的运行时证据接上看板:运行详情页新增"链路透视"区块,回放一次运行的探索轨迹(读了什么)、模型回合(成本)与派发链父子关系。只读库,不拼命令,不写库。
+
+## 2. 改动台账
+
+| 文件 | 动作 | 原因 |
+|---|---|---|
+| `dashboard/server.py` | 修改 | `view_run_detail` 主查询补 `trigger_detail`;新增 `_agent_events_html`(探索轨迹 + 模型回合,读 `example_agent_event`)与 `_dispatch_chain_html`(经 `trigger_detail.productionRunId/evaluationRunId` 关联父子运行);页面加"链路透视"区块 |
+
+## 3. 验证
+
+- `dashboard/test_server_display.py` 全绿(展示层中文语义与编码合同)。
+- 真实页走查(只读看板):
+ - `…-writer-v9`:探索轨迹含 `read_chapter_text`×8、`read_fine_outline`×4 等,模型回合带令牌/成本,"派发链"回链父运行。
+ - 生产父运行 `…-42beb650`:列出子运行 explore-v9 / writer-v9。
+ - 盲评运行 `…-30f33c50c7`:列出 judge-v1 / judge-v2。
+
+## 4. §11 对照(本部分消解的缺口)
+
+| 内容 | 之前 | 现在 |
+|---|---|---|
+| 探索轨迹与事件链(example_agent_event) | 看板未接(缺口) | 运行详情页"链路透视"可见 |
+| 依赖清单(本次运行实际读了什么) | 看板未接(缺口) | 以工具事件(工具名+参数明细)呈现 |
+| 派发链父子关系 | 无视图 | "派发链"区块双向可导航 |
+
+## 5. 未决(阶段 G 后续)
+
+- 任务包(spec)与生产 artifacts 读侧迁到运行目录/库投影的看板呈现(§11"生产 artifacts"行的读侧迁移)。
+- 待审视图增强:候选详情页直达其生产运行与检查报告(现经 /runs 手工跳转)。
+- 五视图整体走查与 §11 全矩阵验收。