Files
HomeAgent/internal/sdk/status.go
JianFeeeee 943eef01cf feat(resident): 分诊助手定位 + 残余任务由父显式决定
用户澄清(重要定性):这不是「内核替父决定」,而是**及时反馈** ——
主 agent 忙时不该让用户干等十几分钟。子 agent 是**分诊助手**:
简单的直接处理并回复,需要主 agent 的立刻回「忙碌中,请稍候」、不勉强作答。

三处补齐:

1. 分诊助手的职责提示词(之前完全没给 ⇒ 子不知道自己为什么存在):
   两条路(直接办 / 报忙碌)、拿不准时报忙碌、必须 output_send 到原通道。
2. 驻留子继承父的 SystemPrompt(之前没传 ⇒ 子只用一句兜底文案,
   拿不到「异步通道必须显式 output_send,否则回复被静默丢弃」这条铁律。
   webui 这类同步通道能回是因为走 ResponseCh,掩盖了这个缺陷)。
3. 残余任务由父显式决定(用户要求):reclaim/destroy 时子手头未处理的消息
   不再由内核悄悄处置 —— 内核只负责列清楚,父用 residual=keep/drop 决定。
   之前 pendingEvents 只收带 ResponseCh 的,异步(qq)残余任务完全不在内,
   被销毁时静默消失、用户零反馈且日志无痕。

配套:
- scheduler.takeAllPendingEvents:取走全部未执行事件(不筛通道)
- ApplyResidual(keep|drop):keep 转回父队列(保留 ResponseCh),
  drop 逐条记日志 + 给同步调用方补终态(否则 cli/a2a 永久挂起)
- 状态面暴露 offload_owned,让父分清「我建的子」与「内核临时拉的助手」
- 工具 schema 加 residual 参数并说明 drop 的代价

这也是用户观察到的「机制很自然」的落点:分诊助手就在同一张登记表里,
父能 inspect/send/compress/reclaim/destroy,控制面 6 动作按 id 生效不区分来源。

测试 +7(残余 keep 转回且保留 ResponseCh / drop 通知同步调用方 /
空残余如实报告 / 分诊提示词 / 继承 SystemPrompt),
其中 drop 那条已实测「对着静默丢弃的旧实现会失败」。全套绿。
2026-09-19 17:43:16 +08:00

274 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package sdk
// StatusAPI provides a snapshot of the kernel runtime status.
type StatusAPI interface {
GetKernelStatus() *KernelStatus
}
// KernelStatus is the aggregated runtime snapshot of all kernel subsystems.
type KernelStatus struct {
Uptime string `json:"uptime"`
StartTime string `json:"start_time"`
// Build 是内核自身的版本与构建信息。
//
// 为何需要:此前 WebUI 展示的“版本”取自 `sdk.SDKVersion`,那是 **SDK 仓
// meta.Version 的硬编码值**;而 `-ldflags` 注入的是内核的
// `internal/meta.Version`——两条链完全不相交。于是:
// - 构建时注入的真实版本号、commit、构建时间全部丢失
// - 两仲版本号碰巧相等时看不出问题,一旦不等就报错版本;
// - 无法回答“现网跑的是哪个 commit 构出来的”。
Build BuildStatus `json:"build"`
AgentID string `json:"agent_id"`
Plugins []PluginInfo `json:"plugins"`
Tools []ToolDef `json:"tools"`
Channels []ChannelInfo `json:"channels"`
// InputChannels 是 inputch 的**登记与归属**视图(谁注册、划给了哪个 agent、
// 容量、默认回程输出)。
//
// 为何单列Channels 只描述设备侧能力,回答不了「这条输入归谁」。驻留子 agent
// 出现之后,归属才是运行态里最该看见的东西——只画设备能力,等于把
// 「输入路由发生在进内核之前」这条设计事实藏了起来。
InputChannels []InputChannelInfo `json:"input_channels"`
Memory MemoryStatus `json:"memory"`
Knowledge KnowledgeStatus `json:"knowledge"`
Documents DocumentStatus `json:"documents"`
TextMemory TextMemoryStatus `json:"text_memory"`
Social SocialStatus `json:"social"`
LLM LLMStatus `json:"llm"`
Context ContextStatus `json:"context"`
Runtime RuntimeStatus `json:"runtime"`
// ONNX 报告统一多模态向量空间ONNX 模型)是否**真的在用**。
//
// 为何单列:内核的向量能力是三层降级(统一多模态空间 → 词嵌入 → TF-IDF
// 只报「向量可用/不可用」分不清「ONNX 模型已加载」与「退回了纯文本路径」。
// 模型缺失 / 运行时缺失 / provider 打开失败时这里是 enabled=false + reason。
ONNX ONNXStatus `json:"onnx"`
Tracker TrackerStatus `json:"tracker"`
// Residents 是驻留式子 agent 的运行时视图(数量 = len(Residents))。
Residents []ResidentStatus `json:"residents"`
// Scheduler 是输入调度器的运行时快照(可观测性,设计文档 §11 O1/O2
// M2 起输入不再直接排队在 channel 上,而是经 readyQueue/pendingInterrupts/
// suspendStack 三集合按优先级调度;这里把这些状态暴露出来。
Scheduler SchedulerStatus `json:"scheduler"`
}
// SchedulerStatus 是调度器的原子快照 DTO。
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"`
Suspended uint64 `json:"suspended"`
Resumed uint64 `json:"resumed"`
// Preempted = Σ PreemptsByLevel[1..4],即「真正抢占成功」的次数。
// 它与 Suspended 不等价(受害者可能先自行结束),因此不是 Suspended 的别名。
Preempted uint64 `json:"preempted"`
// Backpressure 是就绪队列满、输入被挡回 channel 的次数(暂时不收,不是丢弃)。
Backpressure uint64 `json:"backpressure"`
}
// 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"`
// OffloadOwned 标记这是**内核为承接积压而拉起**的临时助手(见 core/offload.go
//
// 为什么要暴露给状态面/工具面:父的模型需要分清"我建的子"与
// "内核临时拉的分诊助手" —— 前者该按需回收,后者在空闲、无积压时
// 可以安全回收,而**正忙时不该回收**(会让用户的消息再次无声丢失)。
OffloadOwned bool `json:"offload_owned,omitempty"`
// 以下四项是该驻留子**自己的**调度器积压摘要,用于 per-agent 负载环形图。
//
// 根 agent 的积压看 KernelStatus.Scheduler每个驻留子是独立 agent、
// 各跑各的调度器,必须分别给,否则「哪个子忙」在界面无从判断。
ReadyQueueDepth int `json:"ready_queue_depth"`
PendingInterrupts int `json:"pending_interrupts"`
SuspendStack int `json:"suspend_stack"`
InterruptQueues [5]int `json:"interrupt_queues"`
}
// SchedulerTask 是任务的最小标识(不暴露帧内容)。
type SchedulerTask struct {
ID uint64 `json:"id"`
Level int `json:"level"`
Kind string `json:"kind"`
}
// ONNXStatus 是统一多模态向量空间ONNX 模型)的启用状态与身份。
type ONNXStatus struct {
// Enabled 是 provider 真正打开且元数据合法(不是「配置里写了 provider」
Enabled bool `json:"enabled"`
// Provider 是配置指定的 provider 名(如 chineseclip / qwen3vl / http
Provider string `json:"provider,omitempty"`
Dim int `json:"dim,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Modalities []string `json:"modalities,omitempty"`
// Reason 是未启用时的原因(未配置 / 打开失败的具体错误 / 其它)。
Reason string `json:"reason,omitempty"`
}
type PluginInfo struct {
Name string `json:"name"`
Loaded bool `json:"loaded"`
}
type ChannelInfo struct {
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"`
}
// InputChannelInfo 是一条 inputch 的登记与归属视图。
//
// 字段取自 agent/io 的 ChannelRegistry根 agent 与驻留子**共用同一份**登记表),
// 因此它天然覆盖驻留子的通道分配。
type InputChannelInfo struct {
Name string `json:"name"`
// Plugin 是注册它的插件名(归属可追溯)。
Plugin string `json:"plugin,omitempty"`
// Owner 是被划给的 agent id"" = 未分配,归根 agent / 内核默认。
Owner string `json:"owner,omitempty"`
// Capacity 是该 inputch 的队列容量0 = 用内核默认值。
Capacity int `json:"capacity,omitempty"`
// Output 是该 inputch 的默认回程输出通道;"" = 由来源决定。
Output string `json:"output,omitempty"`
}
type MemoryStatus struct {
Available bool `json:"available"`
EntityCount int `json:"entity_count"`
RelationCount int `json:"relation_count"`
EntityTypes int `json:"entity_types"`
}
type KnowledgeStatus struct {
Available bool `json:"available"`
ItemCount int `json:"item_count"`
Items []string `json:"items,omitempty"`
}
type DocumentStatus struct {
Available bool `json:"available"`
DocCount int `json:"doc_count"`
VectorCount int `json:"vector_count"`
}
type TextMemoryStatus struct {
Available bool `json:"available"`
FileCount int `json:"file_count"`
}
type SocialStatus struct {
Available bool `json:"available"`
PersonCount int `json:"person_count"`
}
type LLMStatus struct {
Available bool `json:"available"`
Provider string `json:"provider,omitempty"`
Sources int `json:"sources,omitempty"`
}
type ContextStatus struct {
EventCount int `json:"event_count,omitempty"`
}
type RuntimeStatus struct {
Goroutines int `json:"goroutines"`
MemoryMB int64 `json:"memory_mb"`
GoVersion string `json:"go_version"`
}
// BuildStatus 是内核二进制的构建身份,全部来自 `internal/meta`
// (由 -ldflags 在构建时注入,未注入时为源码默认值/unknown
type BuildStatus struct {
// Version 是内核语义版本(如 1.0.0)。
Version string `json:"version"`
// Commit 是构建时的 Git 短 hash未注入为 unknown。
Commit string `json:"commit"`
// BuildTime 是 UTC 构建时间;未注入为 unknown。
BuildTime string `json:"build_time"`
// SDKCompatible 是本内核可兼容的最高 SDK 版本。
// 插件用旧 SDK 编译时靠它判断能不能加载。
SDKCompatible string `json:"sdk_compatible"`
// KernelName 是内核名HomeAgent
KernelName string `json:"kernel_name"`
// SourceURL 是本次构建对应的源码地址。AGPL-3.0 §13 要求:向使用者提供网络
// 服务时,要给他们拿到 Corresponding Source 的机会——WebUI 状态页会把它渲染成
// 可见链接,所以**修改后对外部署的分支必须把这个值改指向自己的源码仓库**。
SourceURL string `json:"source_url,omitempty"`
// License 是开源许可标识SPDX如 AGPL-3.0-only
//
// 为何与 SourceURL 分开:只看一个源码链接,使用者没法从界面上看出这是什么
// 许可、网络服务场景下有哪些义务。许可名与许可全文地址是两个独立事实。
License string `json:"license,omitempty"`
// LicenseURL 是 License 对应的全文地址。
LicenseURL string `json:"license_url,omitempty"`
}
type TrackerStatus struct {
Available bool `json:"available"`
Dir string `json:"dir,omitempty"`
}