feat(status+webui): 运行态图形化 —— 排队/四级中断队列/中断栈/驻留子/通道拓扑

需求:首页不该只有文字,要能一眼看出内核在忙什么——排队消息数、各级中断
排队与中断栈、驻留子 agent 数量;这些要向**内部 SDK 暴露接口**,供 WebUI 等应用
展示;通道划分也要能画出来。

## 一、内核状态面(internal/sdk,内部 SDK,不受公开 SDK 冻结约束)

* SchedulerStatus 补:
  - interrupt_queues[5]:**四级中断队列各自的深度**(下标即级别 1..4,下标 0 恒 0,
    这样 level 能直接当数组下标用)。此前只有 pending_interrupts 总数,
    看不出"堵在 L1 还是 L4"——四级是抢占优先级,堵在哪级是完全不同的运行状态。
  - immediate:刚抢占成功、下一个安全点立即运行的那个中断(此前完全不可见)。
  - suspend_frames:中断栈的帧(栈底→栈顶,只给任务标识),depth 之外还能看出
    "谁被谁打断"。
  - interrupts_by_level / preempts_by_level:各级累计登记数与抢占成功数。
* KernelStatus 补 residents(驻留子运行时视图:状态/轮次/上下文已满/输入通道/允许输出)。
  刻意**不带**每个驻留子的 inputch 登记明细——状态面会被反复轮询,明细会让
  每次 /status 背上几十 KB;只给表大小,要明细走专门接口。
* ChannelInfo 补 direction(in/out/io)、description、tools、output_caps、caps_text。
  此前 collectKernelStatus 只透传 Name/Type,把描述/工具/能力**全丢了**,
  前端只能画出一排光秃秃的名字。

## 二、WebUI

* 新增只读 `/api/v1/runtime`:只回运行态三件事(scheduler/residents/channels),
  实测 **1.0KB**(/kernel 是 30KB 级)——所以能 3 秒轮询做"实时"感,
  而不必反复拉全量状态。
* 首页新增「运行态」面板(纯 CSS + 内联 SVG,前端仍无构建链):
  - 四个数字块:排队任务 / 待处理中断 / 中断栈(深度/上限) / 驻留子 Agent,带占比条;
  - 四级中断队列条形图:每级"深度 · 登记/抢占",四级语义**照抄内核**
    (L4 内核独占 / L3 交互 / L2 消息 / L1 后台),不自己起名字;
  - 中断栈层叠图(栈顶在上)+ ⚡立即运行项;
  - 通道拓扑:输入通道 → 内核 → 输出通道,双向通道两侧都出现,能力以胶囊标签显示。
* 3 秒轮询只在总览页可见时才发请求;切回总览时 renderAll 会立刻补一次。

## 验证

* 新增 TestSchedulerStatusExposesLevelsAndStack(四级队列/立即项/栈帧/各级计数映射,
  并断言"未使用的级别必须为 0"与"下标 0 恒 0")、TestChannelInfoCarriesTopology、
  TestRuntimeEndpoint(形状 + 不携带 tools/plugins + 无状态源时 503)。
* go build / vet / agent+core+sdk+plugin+webui 全量测试绿。
* 真实浏览器实测(CDP 驱动,注入运行态样本走真实渲染路径):
  数字块 [3, 5, 2/4, 2];四级条 L4=1/L3=2/L2=1/L1=1 与数据一致;
  栈帧按"栈底→栈顶"渲染且标出栈顶;通道左右分列、io 通道两侧都出现。
This commit is contained in:
HomeAgent Agent
2026-09-14 09:05:14 +08:00
parent 7dce320a31
commit 47808fced0
9 changed files with 685 additions and 6 deletions

View File

@ -47,6 +47,9 @@ type KernelStatus struct {
Tracker TrackerStatus `json:"tracker"`
// Residents 是驻留式子 agent 的运行时视图(数量 = len(Residents))。
Residents []ResidentStatus `json:"residents"`
// Scheduler 是输入调度器的运行时快照(可观测性,设计文档 §11 O1/O2)。
// M2 起输入不再直接排队在 channel 上,而是经 readyQueue/pendingInterrupts/
// suspendStack 三集合按优先级调度;这里把这些状态暴露出来。
@ -57,12 +60,30 @@ type KernelStatus struct {
type SchedulerStatus struct {
// Running 是当前执行的任务(空表示空闲)。
Running *SchedulerTask `json:"running,omitempty"`
// Immediate 是刚抢占成功、将在下一个安全点立即运行的中断(最多一个)。
Immediate *SchedulerTask `json:"immediate,omitempty"`
// ReadyQueueDepth / PendingInterrupts / SuspendStack 是三个集合的深度。
ReadyQueueDepth int `json:"ready_queue_depth"`
PendingInterrupts int `json:"pending_interrupts"`
SuspendStack int `json:"suspend_stack"`
MaxSuspendDepth int `json:"max_suspend_depth"`
// InterruptQueues 是**四条中断队列各自的深度**,下标即中断级别(1..4);
// 下标 0 恒为 0,这样 level 可以直接当数组下标用,省掉调用方 ±1 的翻译。
//
// 为什么单列:PendingInterrupts 只是总数,看不清"堵在哪一级"——
// 四级中断是抢占优先级,堵在 L1 还是 L4 是完全不同的运行状态。
InterruptQueues [5]int `json:"interrupt_queues"`
// SuspendFrames 是中断栈的帧,**栈底 → 栈顶**(只暴露任务标识,不含帧内容)。
// 深度见 SuspendStack;帧的顺序回答了"谁被谁打断"。
SuspendFrames []SchedulerFrame `json:"suspend_frames,omitempty"`
// InterruptsByLevel / PreemptsByLevel 是各级中断的累计计数(下标 1..4):
// 前者=被登记次数(含没抢成的),后者=判定可抢占并进入 immediate 的次数。
InterruptsByLevel [5]uint64 `json:"interrupts_by_level"`
PreemptsByLevel [5]uint64 `json:"preempts_by_level"`
Enqueued uint64 `json:"enqueued"`
Executed uint64 `json:"executed"`
Rejected uint64 `json:"rejected"`
@ -71,6 +92,26 @@ type SchedulerStatus struct {
Preempted uint64 `json:"preempted"`
}
// SchedulerFrame 是中断栈里的一帧(供图形化展示"压了几层现场")。
type SchedulerFrame struct {
Task SchedulerTask `json:"task"`
}
// ResidentStatus 是驻留式子 agent 的运行时视图。
//
// 为什么进状态面:驻留子是"常驻的独立 agent",它们的数量、轮次与上下文占用
// 是运行态里最需要一眼看到的东西(此前只在日志里,WebUI 只能显示文字)。
type ResidentStatus struct {
ID string `json:"id"`
State string `json:"state"`
Rounds int `json:"rounds"`
ContextFull bool `json:"context_full"`
InputChs []string `json:"input_channels,omitempty"`
AllowedOutputs []string `json:"allowed_outputs,omitempty"`
InputChTable int `json:"input_ch_table"`
CreatedAt string `json:"created_at,omitempty"`
}
// SchedulerTask 是任务的最小标识(不暴露帧内容)。
type SchedulerTask struct {
ID uint64 `json:"id"`
@ -97,9 +138,18 @@ type PluginInfo struct {
}
type ChannelInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Ready bool `json:"ready"`
Name string `json:"name"`
// Type 保持为 DeviceType 的数字字符串(历史字段,别改语义)。
Type string `json:"type"`
// Direction 是方向的可读名:in(只进)/ out(只出)/ io(双向)。
Direction string `json:"direction"`
Ready bool `json:"ready"`
// 以下三项供通道拓扑展示:此前 collectKernelStatus 只透传了 Name/Type,
// 把 Description/Tools/OutputCaps 全丢了,前端只能画出一排光秃秃的名字。
Description string `json:"description,omitempty"`
Tools []string `json:"tools,omitempty"`
OutputCaps int `json:"output_caps"`
CapsText string `json:"caps_text,omitempty"`
}
type MemoryStatus struct {