ci / vet-and-test (pull_request) Successful in 42s
Why: The boundary linter allows agent-intent imports only in the adapter layer; route and runmgr consume aliases from intentgate instead of importing the ecosystem library directly. Evidence: lint-boundaries passes below; backend suite green. Impact: intentgate re-exports Command/ActionRef and the kind constants; route drops the agent-intent import.
115 lines
3.9 KiB
Go
115 lines
3.9 KiB
Go
// Package route 是 Backend 的唯一意图 Router(I1 WP3)。
|
||
// 它把识别器产出的命令列表统一裁决为路由决策,收编原散落在
|
||
// server(chat 内联分支)与 runmgr(HITL 自由输入转轨)的两套逻辑。
|
||
//
|
||
// 优先级明文(提案 §6 要求 I1 写明):
|
||
// - auto/handler:命令总线 > 工作流 > 普通回合(与现网行为一致)。
|
||
// - 显式 command/workflow/blueprint:按声明执行;声明目标在当前环境
|
||
// 不可解析(命令不存在、工作流未注册、小队未启用)时不静默错配——
|
||
// 回落普通回合并把原因写进 Reason。
|
||
// - PromptTrigger(技能触发)是提示词装配、不参与路由决胜;
|
||
// 意图产出 Start/Clarify 类命令时路由优先。
|
||
//
|
||
// 当前版本的已知边界(如实记录,不静默):
|
||
// - squad 决策需要 runmgr 的 per-run entry 覆盖才能真正执行(独立设计项);
|
||
// 本版本调用方按普通回合处理并保留 Decision 供审计。
|
||
// - SetSlot/Cancel/Handoff 命令由库层产出,但 Backend 尚无会话槽位状态机
|
||
// 与语义层,Router v1 不消费它们(expectation_hit 在 intentgate 已被折叠为
|
||
// fallback,Start 只来自 matched)。
|
||
package route
|
||
|
||
import (
|
||
"agent-system-backend/internal/intentgate"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// Kind 是路由决策类型。
|
||
type Kind string
|
||
|
||
const (
|
||
KindCommand Kind = "command"
|
||
KindWorkflow Kind = "workflow"
|
||
KindSquad Kind = "squad"
|
||
KindTurn Kind = "turn"
|
||
)
|
||
|
||
// Decision 是一次路由裁决。
|
||
type Decision struct {
|
||
Kind Kind
|
||
CommandID string
|
||
Workflow core.WorkflowDef
|
||
Blueprint string
|
||
Slots map[string]string
|
||
Missing []intentgate.SlotQuestion
|
||
Reason string // 回落 turn 的原因(显式声明不可解析时)
|
||
StartFound bool // 是否存在 Start 命令(runmgr 转轨判定用)
|
||
}
|
||
|
||
// Env 是路由裁决的环境解析器(由调用方注入;nil 视为不可解析)。
|
||
type Env struct {
|
||
HasCommand func(id string) bool
|
||
WorkflowFor func(intentID string) (core.WorkflowDef, bool)
|
||
SquadReady bool
|
||
}
|
||
|
||
// Decide 裁决一次识别结果。规则:
|
||
// 1. 非 matched(fallback/guard/ambiguous 经闸门折叠后)→ turn。
|
||
// 2. 找到首个 Start 命令;没有时按 TopID 走 auto(兼容兜底,与现网一致)。
|
||
// 3. 按 ActionRef.Kind 解析:command→命令总线;workflow→工作流;
|
||
// blueprint→squad 决策;handler/auto→命令总线 > 工作流 > turn。
|
||
func Decide(res intentgate.MatchResult, env Env) Decision {
|
||
d := Decision{Kind: KindTurn, Slots: res.Slots, Missing: res.Missing}
|
||
if res.Branch != "matched" {
|
||
return d
|
||
}
|
||
var start *intentgate.Command
|
||
for i := range res.Commands {
|
||
if res.Commands[i].Kind == intentgate.CommandStart {
|
||
start = &res.Commands[i]
|
||
break
|
||
}
|
||
}
|
||
action := intentgate.ActionRef{Kind: intentgate.ActionAuto, ID: res.ID}
|
||
if start != nil {
|
||
d.StartFound = true
|
||
action = start.Action
|
||
if len(start.Slots) > 0 {
|
||
d.Slots = start.Slots
|
||
}
|
||
}
|
||
switch action.Kind {
|
||
case intentgate.ActionCommand:
|
||
if env.HasCommand != nil && env.HasCommand(action.ID) {
|
||
d.Kind, d.CommandID = KindCommand, action.ID
|
||
return d
|
||
}
|
||
d.Reason = "声明的命令不存在:" + action.ID
|
||
case intentgate.ActionWorkflow:
|
||
if env.WorkflowFor != nil {
|
||
if def, ok := env.WorkflowFor(action.ID); ok {
|
||
d.Kind, d.Workflow = KindWorkflow, def
|
||
return d
|
||
}
|
||
}
|
||
d.Reason = "声明的工作流未注册:" + action.ID
|
||
case intentgate.ActionBlueprint:
|
||
if env.SquadReady {
|
||
d.Kind, d.Blueprint = KindSquad, action.ID
|
||
return d
|
||
}
|
||
d.Reason = "声明的小队未启用:" + action.ID
|
||
default: // auto / handler
|
||
if env.HasCommand != nil && env.HasCommand(action.ID) {
|
||
d.Kind, d.CommandID = KindCommand, action.ID
|
||
return d
|
||
}
|
||
if env.WorkflowFor != nil {
|
||
if def, ok := env.WorkflowFor(action.ID); ok {
|
||
d.Kind, d.Workflow = KindWorkflow, def
|
||
return d
|
||
}
|
||
}
|
||
}
|
||
return d
|
||
}
|