Why: token budget let the current tool batch run, compaction could split a tool pair or race an append, writes were not crash-atomic, and several MCP and session edges only existed as comments. Evidence: GOWORK=off go test -count=1 -race ./runtime/ ./runtime/llm/ ./runtime/session/ ./runtime/tools/ ./runtime/mcp/ ./adapters/native/ ./adapters/intentgate/ passed, including the TestR2* cases for budget, replacement, cancel, compaction, SSE, UTF-8, schema, MCP retry, session terminate, poison reload, and registry reload. Impact: over-budget tool calls are skipped and paired; Stop.Replacement is appended to the session; the intent gate can follow Registry.Reload. Refs: N5, N6, N7, N8, N9, N11, N12, N15, N16, N17, N18, N19, N21, I9 Co-authored-by: Cursor <cursoragent@cursor.com>
82 lines
3.0 KiB
Go
82 lines
3.0 KiB
Go
// Package intentgate 提供可选的意图闸门装饰器:在把 Prompt 交给被装饰的
|
|
// 执行后端之前,用 agent-intent 做一次确定性识别;分支为守卫/命中/预期命中时
|
|
// 交由宿主 Handler 解释,Ambiguous/Fallback 或 Handler 放弃时原样透传。
|
|
//
|
|
// 本包是 Agent-Native 仓内唯一 import agent-intent 的地方。不使用本包时,
|
|
// 本仓与 agent-intent 无任何编译关系;未 Wrap 或快照零意图时,被装饰后端的
|
|
// 行为与原来完全一致。
|
|
package intentgate
|
|
|
|
import (
|
|
"context"
|
|
|
|
"agent-intent/intent"
|
|
"agent-native/exec"
|
|
)
|
|
|
|
// Handler 解释"识别到意图"的执行请求。返回 handled=false 表示宿主放弃处理,
|
|
// 装饰器回退为透传原后端。合成结果的语义(拒绝、引导、定向执行)由宿主定义,
|
|
// 本包不发明结果语义。
|
|
type Handler func(ctx context.Context, req exec.Request, res intent.Result) (result *exec.Result, err error, handled bool)
|
|
|
|
type gated struct {
|
|
next exec.Backend
|
|
rec intent.Recognizer
|
|
snap *intent.Snapshot
|
|
handler Handler
|
|
}
|
|
|
|
// Wrap 返回带意图闸门的后端。next/rec/snap/handler 任一为 nil 时直接返回 next
|
|
// (等价于未启用,行为不变)。
|
|
func Wrap(next exec.Backend, rec intent.Recognizer, snap *intent.Snapshot, h Handler) exec.Backend {
|
|
if next == nil || rec == nil || snap == nil || h == nil {
|
|
return next
|
|
}
|
|
return &gated{next: next, rec: rec, snap: snap, handler: h}
|
|
}
|
|
|
|
// WrapRegistry 持有 Registry,每次执行读取当前快照。Reload 之后不用重建装饰器。
|
|
func WrapRegistry(next exec.Backend, rec intent.Recognizer, reg *intent.Registry, h Handler) exec.Backend {
|
|
if next == nil || rec == nil || reg == nil || h == nil {
|
|
return next
|
|
}
|
|
return &gatedRegistry{next: next, rec: rec, reg: reg, handler: h}
|
|
}
|
|
|
|
type gatedRegistry struct {
|
|
next exec.Backend
|
|
rec intent.Recognizer
|
|
reg *intent.Registry
|
|
handler Handler
|
|
}
|
|
|
|
func (g *gatedRegistry) Name() string { return g.next.Name() }
|
|
|
|
func (g *gatedRegistry) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
|
|
return executeGated(ctx, g.next, g.rec, g.reg.Snapshot(), g.handler, req)
|
|
}
|
|
|
|
func (g *gated) Name() string { return g.next.Name() }
|
|
|
|
func (g *gated) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
|
|
return executeGated(ctx, g.next, g.rec, g.snap, g.handler, req)
|
|
}
|
|
|
|
func executeGated(ctx context.Context, next exec.Backend, rec intent.Recognizer, snap *intent.Snapshot, handler Handler, req exec.Request) (*exec.Result, error) {
|
|
res, err := rec.Recognize(ctx, req.Prompt, snap)
|
|
if err != nil {
|
|
// 识别系统错误 fail-closed:不静默放行可能应拦截的内容,也不吞错。
|
|
return nil, err
|
|
}
|
|
switch res.Branch {
|
|
case intent.BranchGuardHit, intent.BranchMatched, intent.BranchExpectationHit:
|
|
default:
|
|
return next.Execute(ctx, req) // 无裁决:透传原后端
|
|
}
|
|
out, herr, handled := handler(ctx, req, res)
|
|
if !handled {
|
|
return next.Execute(ctx, req)
|
|
}
|
|
return out, herr
|
|
}
|