Why: Both intent hosts should fold compatibility-equivalent input (backend intentgate already does); the nested pin carries the I1 command model the gate depends on. Evidence: adapters/intent suite ok; all five squad modules green. Impact: NFKC on in NewFromOverlay; nested agent-intent at 2554b50.
138 lines
4.4 KiB
Go
138 lines
4.4 KiB
Go
// Package squadintent 把 agent-intent 的识别结果适配为 SquadEngine 的意图闸门。
|
||
//
|
||
// 本包是 Agent-Squad 仓内唯一 import agent-intent 的地方;squad/server/store 核心
|
||
// 不感知意图。未提供任何意图来源时 New 返回 nil 闸门(= 未启用),引擎行为与
|
||
// 未接入时完全一致;来源存在但配置非法时返回错误(调用方 fail-closed 拒绝启动)。
|
||
//
|
||
// 分支映射:
|
||
//
|
||
// GuardHit → Reject(引导文案取 Metadata["guidance"])
|
||
// Matched/ExpectationHit 且 Metadata["blueprint"] 非空 → 蓝图定向(等价显式 TaskKey)
|
||
// Matched 但无 blueprint 映射 / Ambiguous / Fallback → nil(不干预,走原路径)
|
||
// 识别系统错误 → error(引擎标记运行失败,fail-closed)
|
||
package squadintent
|
||
|
||
import (
|
||
"context"
|
||
"io/fs"
|
||
|
||
"agent-intent/intent"
|
||
"agent-squad/squad"
|
||
)
|
||
|
||
// Options 闸门构造选项。
|
||
type Options struct {
|
||
// IntentOptions 传给 agent-intent:GuardDomains/ProtectedDomains/上限等(Log 可选)。
|
||
IntentOptions intent.Options
|
||
// GuardSpecs 宿主内置守卫 spec,应位于 ProtectedDomains 声明的域内(overlay 不可覆盖)。
|
||
GuardSpecs []intent.IntentSpec
|
||
// FS 与 Roots 是 JSON overlay 来源;两者为空则只用 GuardSpecs。
|
||
FS fs.FS
|
||
Roots []string
|
||
}
|
||
|
||
// NewFromOverlay 从 JSON overlay 构造闸门,并把 guard 域标成受保护域。
|
||
// 命令行用它,这样命令本身不必 import agent-intent。
|
||
// NFKC 开启:全角/兼容等价输入与词条同形折叠(与 backend intentgate 一致)。
|
||
func NewFromOverlay(ctx context.Context, fsys fs.FS, roots ...string) (*Gate, error) {
|
||
return New(ctx, Options{
|
||
IntentOptions: intent.Options{
|
||
GuardDomains: []string{"guard"},
|
||
ProtectedDomains: []string{"guard"},
|
||
NFKC: true,
|
||
},
|
||
FS: fsys,
|
||
Roots: roots,
|
||
})
|
||
}
|
||
|
||
// New 构造闸门。没有任何意图来源时返回 (nil, nil)——引擎拿到 nil IntentGate,
|
||
// 行为与原实现一致(T0 行为解耦)。
|
||
func New(ctx context.Context, o Options) (*Gate, error) {
|
||
if len(o.GuardSpecs) == 0 && (o.FS == nil || len(o.Roots) == 0) {
|
||
return nil, nil
|
||
}
|
||
b := intent.NewRegistryBuilder(o.IntentOptions)
|
||
if len(o.GuardSpecs) > 0 {
|
||
if err := b.Register(o.GuardSpecs...); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
var (
|
||
reg *intent.Registry
|
||
err error
|
||
)
|
||
if o.FS != nil {
|
||
reg, err = b.BuildFS(ctx, o.FS, o.Roots...)
|
||
} else {
|
||
reg, err = b.Build()
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &Gate{rec: intent.NewRecognizer(o.IntentOptions, nil, nil), reg: reg}, nil
|
||
}
|
||
|
||
// Gate 实现 squad.IntentGate。快照从 Registry 现读,Reload 后下一次 Decide 即生效。
|
||
type Gate struct {
|
||
rec intent.Recognizer
|
||
reg *intent.Registry
|
||
}
|
||
|
||
// Snapshot 暴露当前识别快照(诊断/审计用)。
|
||
func (g *Gate) Snapshot() *intent.Snapshot {
|
||
if g == nil || g.reg == nil {
|
||
return nil
|
||
}
|
||
return g.reg.Snapshot()
|
||
}
|
||
|
||
// Reload 重读配置来源。失败时保留旧快照。
|
||
func (g *Gate) Reload(ctx context.Context) (*intent.Snapshot, error) {
|
||
if g == nil || g.reg == nil {
|
||
return nil, nil
|
||
}
|
||
return g.reg.Reload(ctx)
|
||
}
|
||
|
||
// Decide 实现 squad.IntentGate。同输入 + 同快照结果确定;无副作用。
|
||
func (g *Gate) Decide(ctx context.Context, goal squad.Goal, opts squad.RunOptions) (*squad.IntentDecision, error) {
|
||
if g == nil || g.Snapshot() == nil {
|
||
return nil, nil
|
||
}
|
||
if opts.TaskKey != "" {
|
||
return nil, nil // 显式 TaskKey 优先(引擎侧同样保证,双保险)
|
||
}
|
||
res, err := g.rec.Recognize(ctx, goal.Text, g.Snapshot())
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
switch res.Branch {
|
||
case intent.BranchGuardHit:
|
||
if res.Top == nil {
|
||
return nil, nil
|
||
}
|
||
return &squad.IntentDecision{
|
||
Reject: &squad.IntentRejection{
|
||
Reason: "命中守卫意图 " + res.Top.ID,
|
||
Guidance: res.Top.Metadata["guidance"],
|
||
},
|
||
IntentID: res.Top.ID,
|
||
Handler: res.Top.Handler,
|
||
Metadata: res.Top.Metadata,
|
||
}, nil
|
||
case intent.BranchMatched, intent.BranchExpectationHit:
|
||
if res.Top == nil || res.Top.Metadata["blueprint"] == "" {
|
||
return nil, nil // 命中但无蓝图映射:不干预
|
||
}
|
||
return &squad.IntentDecision{
|
||
BlueprintKey: res.Top.Metadata["blueprint"],
|
||
IntentID: res.Top.ID,
|
||
Handler: res.Top.Handler,
|
||
Metadata: res.Top.Metadata,
|
||
}, nil
|
||
default:
|
||
return nil, nil // Ambiguous / Fallback:交给原路径
|
||
}
|
||
}
|