mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
fix(scheduler)!: 撤掉“优先级=可配置策略表”的错误设计,回归内核内部属性
用户指正:**优先级是内核内部属性**,不是配置项,更不该由插件声明。 我此前把它建模成“策略表 + 字符串解析”,甚至准备接配置中心 (core.agent.priority.<channel>)——方向性错误,故整体撤销。 撤销: - 删除 ParseLevel(字符串解析只服务于“外部可配”这个错误前提) - 删除 AgentConfig.PriorityLookup / Agent.priorityLookup 及 taskLevel 中的查表分支; taskLevel 回归为纯内核内部规则(cli/webui/http→L3,system/_consolidation_→L1, 其余 L1),注释明确“不对外暴露、不做运维可调项” - 设计稿 §3.2 改写为“内核内部属性,不做成配置项”,并删除 §15 里 “ChannelDef.Priority / InjectOptions.Priority 进公开 SDK”这一方向(同属外化) - 未触碰配置中心(registry.go/main.go 的优先级配置一行未加) 同时落地 D6(与本撤销无关、此前遗漏的承诺): - AgentConfig.MaxToolTurns + runTaskSteps 在发起新一轮 LLM 前按 f.Turn 收尾; 0 = 不限;cmd/homed/main.go 从既有 core.agent.max_tool_turns 取值 - 新增 task_test.go 2 项:上限 3 时恰好跑 3 批工具/3 次 LLM 并收尾; 0 = 不限(跑完脚本) 验收:agent 全量 + -race;全仓 build/vet 通过
This commit is contained in:
@ -535,6 +535,7 @@ func main() {
|
|||||||
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
|
ArchiveInterval: cfgReg.GetDuration("core.agent.archive_interval", 60*time.Minute),
|
||||||
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
|
ReviewInterval: cfgReg.GetDuration("core.agent.review_interval", 120*time.Minute),
|
||||||
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
|
MergeInterval: cfgReg.GetDuration("core.agent.merge_interval", 120*time.Minute),
|
||||||
|
MaxToolTurns: cfgReg.GetInt("core.agent.max_tool_turns", 10),
|
||||||
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
ContextSavePath: filepath.Join(cfg.Daemon.DataDir, "memory", "context.json"),
|
||||||
EmbeddingModelPath: cfgReg.GetString("core.agent.embedding_model_path", ""),
|
EmbeddingModelPath: cfgReg.GetString("core.agent.embedding_model_path", ""),
|
||||||
Embedder: embedder,
|
Embedder: embedder,
|
||||||
|
|||||||
@ -95,15 +95,20 @@ Step(枚举,顺序执行,步与步之间是安全点):
|
|||||||
- **默认级 = `L1`**:未显式声明一律最低级("显式才是特权",避免新插件默认获得抢占权)。
|
- **默认级 = `L1`**:未显式声明一律最低级("显式才是特权",避免新插件默认获得抢占权)。
|
||||||
- **取值域仅这四档**,不引入任意整数,避免"9 级比 4 级大但没人知道怎么排"。
|
- **取值域仅这四档**,不引入任意整数,避免"9 级比 4 级大但没人知道怎么排"。
|
||||||
|
|
||||||
### 3.2 优先级从哪来(v1:内核策略表)
|
### 3.2 优先级从哪来(内核内部属性,**不做成配置项**)
|
||||||
|
|
||||||
v1 **不**在公开 SDK 上加字段,来源有三,按优先级取:
|
优先级是**内核内部属性**:内核预定义四级,并按**内核自己的规则**为任务与中断定级。
|
||||||
|
|
||||||
1. 内核策略表:`core.agent.priority.<channel>`(新增 ConfigDef,值 `L1..L4`);
|
- ❌ **不是运维可调项**。不引入 `core.agent.priority.<channel>` 这类配置键,
|
||||||
2. `ChannelDef` 的内核侧扩展(仅 `internal/sdk` 可用,内置插件可声明);
|
也不把 `PriorityLookup` 做成可注入的策略表——那等于把内核的调度内部属性
|
||||||
3. 未命中 → `L1`。
|
外化成配置,与“谁能打断谁”的内部语义相反。
|
||||||
|
- ❌ 也不暴露给插件声明(这个方向曾写入 v2 计划,已删除)。
|
||||||
|
- ✅ v1 的内部缺省规则(仅为实现缺省值,语义上是内核自己的事):
|
||||||
|
`cli`/`webui`/`http` → `L3`;`system` / `_consolidation_` → `L1`;其余 → `L1`(默认级)。
|
||||||
|
- 定级规则可随内核演进调整,但**始终不对外暴露**。
|
||||||
|
|
||||||
> v2 再把 `ChannelDef.Priority` / `InjectOptions.Priority` 加入公开 SDK(纯追加),见 §13。
|
> 具体“哪类工作算哪一级”的完整规则由内核定义;本稿只固定四级语义与定级位置
|
||||||
|
> (`(*Agent).taskLevel`),不承诺配置面。
|
||||||
|
|
||||||
### 3.3 抢占判据
|
### 3.3 抢占判据
|
||||||
|
|
||||||
@ -475,7 +480,9 @@ go test -race -count=1 ./internal/agent/... ./internal/plugin/... ./internal/sdk
|
|||||||
## 15. 开放问题(后续版本)
|
## 15. 开放问题(后续版本)
|
||||||
|
|
||||||
1. 异步 step + `tool.cancel`(真正让工具可抢占)。
|
1. 异步 step + `tool.cancel`(真正让工具可抢占)。
|
||||||
2. `ChannelDef.Priority` / `InjectOptions.Priority` 进入公开 SDK。
|
2. 帧落盘(跨进程/崩溃恢复)。
|
||||||
3. 帧落盘(跨进程/崩溃恢复)。
|
3. 多 agent 并行调度。
|
||||||
4. 多 agent 并行调度。
|
4. 与 `plan.md` §13.7 的 `RuntimeManager + 分组 worker` 合并(本设计是其前置)。
|
||||||
5. 与 `plan.md` §13.7 的 `RuntimeManager + 分组 worker` 合并(本设计是其前置)。
|
|
||||||
|
> 已删除:“`ChannelDef.Priority` / `InjectOptions.Priority` 进入公开 SDK”——
|
||||||
|
> 优先级是内核内部属性(§3.2),不应由插件声明。
|
||||||
|
|||||||
@ -114,8 +114,8 @@ type Agent struct {
|
|||||||
// M2 起取代 eventLoop 的隐式 channel 排队。
|
// M2 起取代 eventLoop 的隐式 channel 排队。
|
||||||
sched *scheduler
|
sched *scheduler
|
||||||
|
|
||||||
// 优先级策略表(可空);见 AgentConfig.PriorityLookup。
|
// 工具轮次硬上限(0 = 不限);见 AgentConfig.MaxToolTurns。
|
||||||
priorityLookup func(source, channel string) Level
|
maxToolTurns int
|
||||||
|
|
||||||
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
|
// 进行中的 LLM 请求取消函数,interceptLoop 可调用以在请求中打断
|
||||||
cancelLLM context.CancelFunc
|
cancelLLM context.CancelFunc
|
||||||
@ -224,9 +224,9 @@ type AgentConfig struct {
|
|||||||
|
|
||||||
InputProcessing types.InputProcessingConfig // 非文本输入处理配置
|
InputProcessing types.InputProcessingConfig // 非文本输入处理配置
|
||||||
|
|
||||||
// PriorityLookup 是内核的优先级策略表(设计文档 §3.2)。
|
// MaxToolTurns 是单个任务允许的工具轮次上限(0 = 不限)。
|
||||||
// 返回 L1..L4;返回 0 或越界值表示“无策略”,由 Agent 的通道名兜底决定。
|
// 设计文档 D6:主循环必须有硬上限,否则模型不停调用就永不完结。
|
||||||
PriorityLookup func(source, channel string) Level
|
MaxToolTurns int
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(cfg AgentConfig) *Agent {
|
func New(cfg AgentConfig) *Agent {
|
||||||
@ -310,7 +310,7 @@ func New(cfg AgentConfig) *Agent {
|
|||||||
selfInputCh: make(chan selfInputMsg, 64),
|
selfInputCh: make(chan selfInputMsg, 64),
|
||||||
childTasks: make(map[string]*childTaskState),
|
childTasks: make(map[string]*childTaskState),
|
||||||
sched: newScheduler(256),
|
sched: newScheduler(256),
|
||||||
priorityLookup: cfg.PriorityLookup,
|
maxToolTurns: cfg.MaxToolTurns,
|
||||||
pluginHealth: newPluginHealthTracker(),
|
pluginHealth: newPluginHealthTracker(),
|
||||||
thinkingEnabled: cfg.ThinkingEnabled,
|
thinkingEnabled: cfg.ThinkingEnabled,
|
||||||
inputCfg: cfg.InputProcessing,
|
inputCfg: cfg.InputProcessing,
|
||||||
|
|||||||
@ -66,6 +66,13 @@ func (l Level) String() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseLevel 已删除。
|
||||||
|
//
|
||||||
|
// 为何不保留:优先级是**内核内部属性**,不是配置项——内核预定义四级
|
||||||
|
// (L1 后台 / L2 消息 / L3 交互 / L4 紧急),由内核按内部规则为任务与中断定级。
|
||||||
|
// 曾一度做成 `core.agent.priority.<channel>` 这种“策略表 + 字符串解析”,
|
||||||
|
// 那等于把内核的内部属性外化成运维配置,与设计意图相反。
|
||||||
|
|
||||||
// TaskKind 区分任务来源。
|
// TaskKind 区分任务来源。
|
||||||
type TaskKind int
|
type TaskKind int
|
||||||
|
|
||||||
@ -471,17 +478,15 @@ func (s *scheduler) currentLevel() Level {
|
|||||||
return DefaultLevel
|
return DefaultLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// taskLevel 是内核的优先级策略:四级的来源(设计文档 §3.2)。
|
// taskLevel 是内核为任务定级的内部规则。
|
||||||
//
|
//
|
||||||
// 优先走注入的查找函数(配置表);未命中则用通道名兜底:
|
// ❗优先级是**内核内部属性**,不做成配置项:内核预定义四级,并按内部规则
|
||||||
// cli/webui/http 为人机交互(L3),system/_consolidation_ 为后台(L1),
|
// 为任务与中断定级。下方规则只是 v1 的内部缺省值——它决定“谁能让位于谁”,
|
||||||
// 其余一律默认级(L1)。显式才是特权:没有策略就不给抢占权。
|
// 属于内核自己的隐私,不对外暴露为运维可调项。
|
||||||
|
//
|
||||||
|
// 缺省:cli/webui/http 为人机交互(L3),system/_consolidation_ 为后台(L1),
|
||||||
|
// 其余一律默认级(L1)。
|
||||||
func (a *Agent) taskLevel(source, channel string) Level {
|
func (a *Agent) taskLevel(source, channel string) Level {
|
||||||
if a.priorityLookup != nil {
|
|
||||||
if l := a.priorityLookup(source, channel); l >= LevelBackground && l <= LevelCritical {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch channel {
|
switch channel {
|
||||||
case "cli", "webui", "http":
|
case "cli", "webui", "http":
|
||||||
return LevelInteractive
|
return LevelInteractive
|
||||||
|
|||||||
@ -150,6 +150,18 @@ func (a *Agent) runTaskSteps(f *TaskFrame) stepOutcome {
|
|||||||
if !a.inCriticalSection() && a.sched.preemptGrantedFor() && a.sched.canSuspend() {
|
if !a.inCriticalSection() && a.sched.preemptGrantedFor() && a.sched.canSuspend() {
|
||||||
return outcomeSuspended
|
return outcomeSuspended
|
||||||
}
|
}
|
||||||
|
// 工具轮次硬上限(设计文档 D6):在发起下一轮 LLM 前收尾。
|
||||||
|
// f.Turn 只在 stepTurnEnd 递增,所以它等于「已完成的工具批数」;
|
||||||
|
// 因此这里允许 maxToolTurns 批,而不会多跑第 maxToolTurns+1 轮。
|
||||||
|
if f.Step == StepLLM && a.maxToolTurns > 0 && f.Turn >= a.maxToolTurns {
|
||||||
|
log.Printf("[agent] 已达最大工具轮次 %d(turn=%d),强制收尾", a.maxToolTurns, f.Turn)
|
||||||
|
if f.Resp != nil && strings.TrimSpace(f.Resp.Content) != "" {
|
||||||
|
f.Response = f.Resp.Content
|
||||||
|
} else {
|
||||||
|
f.Response = fmt.Sprintf("[系统] 已达到最大工具轮次 %d,任务中止。", a.maxToolTurns)
|
||||||
|
}
|
||||||
|
return outcomeDone
|
||||||
|
}
|
||||||
switch a.step(f) {
|
switch a.step(f) {
|
||||||
case outcomeDone:
|
case outcomeDone:
|
||||||
return outcomeDone
|
return outcomeDone
|
||||||
|
|||||||
@ -165,3 +165,74 @@ func TestTaskFrame_UnknownStepFails(t *testing.T) {
|
|||||||
t.Fatal("未知 step 必须带错误信息")
|
t.Fatal("未知 step 必须带错误信息")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D6:工具轮次硬上限——模型不停调用工具时,必须在有限步内收尾。
|
||||||
|
//
|
||||||
|
// 这是审查里定位的 P0(core.agent.max_tool_turns 只定义、从没被读过),
|
||||||
|
// 也是调度器的前提:任务必须可终止。
|
||||||
|
func TestMaxToolTurns_CapsRunawayLoop(t *testing.T) {
|
||||||
|
sh := NewStageHost()
|
||||||
|
sh.RegisterTool("t_loop", sdk.ToolDef{Name: "t_loop", Plugin: "t"}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
|
return "again", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 脚本远长于上限:provider 每轮都给下一批工具调用,模拟“永不停止”。
|
||||||
|
script := make([]*agentAPI.CompletionResponse, 0, 20)
|
||||||
|
for i := 0; i < 20; i++ {
|
||||||
|
script = append(script, &agentAPI.CompletionResponse{
|
||||||
|
Content: "继续",
|
||||||
|
ToolCalls: []agentAPI.ToolCall{tc("c1", "t_loop")},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
sp := &scriptProvider{script: script}
|
||||||
|
a := New(AgentConfig{
|
||||||
|
ID: "cap",
|
||||||
|
Provider: sp,
|
||||||
|
ProviderManager: agentAPI.NewProviderManager(),
|
||||||
|
IO: agentIO.NewIOManager(),
|
||||||
|
StageHost: sh,
|
||||||
|
MaxToolTurns: 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, toolsUsed, toolResults, err := a.process("循环", a.stageCtxFromInput("循环", "", ""))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("process 返回错误: %v", err)
|
||||||
|
}
|
||||||
|
if len(toolsUsed) != 3 || len(toolResults) != 3 {
|
||||||
|
t.Fatalf("工具批=%d/%d,期望恰好 3(到上限即止,不多跑第 4 轮)", len(toolsUsed), len(toolResults))
|
||||||
|
}
|
||||||
|
if len(sp.reqs) != 3 {
|
||||||
|
t.Fatalf("LLM 调用=%d,期望 3(上限后不再发起新请求)", len(sp.reqs))
|
||||||
|
}
|
||||||
|
if resp != "继续" {
|
||||||
|
t.Fatalf("响应=%q,期望返回最近一次 LLM 文本", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上限为 0 表示不限(显式退出机制)。
|
||||||
|
func TestMaxToolTurns_ZeroMeansUnlimited(t *testing.T) {
|
||||||
|
sh := NewStageHost()
|
||||||
|
sh.RegisterTool("t_loop", sdk.ToolDef{Name: "t_loop", Plugin: "t"}, func(args map[string]interface{}) (interface{}, error) {
|
||||||
|
return "again", nil
|
||||||
|
})
|
||||||
|
sp := &scriptProvider{script: []*agentAPI.CompletionResponse{
|
||||||
|
{Content: "a", ToolCalls: []agentAPI.ToolCall{tc("c1", "t_loop")}},
|
||||||
|
{Content: "b", ToolCalls: []agentAPI.ToolCall{tc("c2", "t_loop")}},
|
||||||
|
{Content: "c"},
|
||||||
|
}}
|
||||||
|
a := New(AgentConfig{
|
||||||
|
ID: "nocap",
|
||||||
|
Provider: sp,
|
||||||
|
ProviderManager: agentAPI.NewProviderManager(),
|
||||||
|
IO: agentIO.NewIOManager(),
|
||||||
|
StageHost: sh,
|
||||||
|
MaxToolTurns: 0,
|
||||||
|
})
|
||||||
|
resp, toolsUsed, _, err := a.process("x", a.stageCtxFromInput("x", "", ""))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("process 返回错误: %v", err)
|
||||||
|
}
|
||||||
|
if len(toolsUsed) != 2 || resp != "c" {
|
||||||
|
t.Fatalf("不限时应跑完脚本:tools=%d resp=%q", len(toolsUsed), resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user