// 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:交给原路径 } }