Files
Agent-Intent/intent/command.go
weichen 916ee0733f
ci / vet-and-test (pull_request) Successful in 17s
ci / docker-checkout-hostgateway (pull_request) Successful in 0s
Add the explicit spec.action declaration for action-key unification
Why: I1 WP2 - one explicit action field outranks the four legacy
action keys (Handler, metadata.blueprint, command-id, workflow
IntentID) so hosts migrate to a single derivation path.

Evidence: go vet clean; go test ./... all ok, including declared
action winning the Start command and kind/id validation rejections.

Impact: IntentSpec gains Action (command/workflow/blueprint/handler);
Candidate carries it through; actionOf prefers it, then blueprint,
then Handler, then auto. Legacy keys keep working.

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md I1 WP2
2026-10-07 07:31:05 +00:00

157 lines
5.5 KiB
Go

package intent
import (
"sort"
"strings"
)
// CommandKind 是一条路由指令的类型。
type CommandKind string
const (
// CommandStart 启动一个动作(流程/命令/蓝图/处理器),携带已抽取槽位。
CommandStart CommandKind = "start"
// CommandSetSlot 记录一个槽位值(多轮澄清/预期回答的落槽)。
CommandSetSlot CommandKind = "set_slot"
// CommandChangeSlot 改写一个已有槽位值。
CommandChangeSlot CommandKind = "change_slot"
// CommandCancel 取消某个进行中的动作(否定语义的命令层表达)。
CommandCancel CommandKind = "cancel"
// CommandClarify 向用户澄清:缺槽位或候选消歧。
CommandClarify CommandKind = "clarify"
// CommandChitchat 闲聊(不由规则漏斗产出;留给语义层与宿主映射)。
CommandChitchat CommandKind = "chitchat"
// CommandHandoff 转人工(不由规则漏斗产出;留给语义层与宿主映射)。
CommandHandoff CommandKind = "handoff"
)
// ActionRef 指向一个可执行动作。auto 表示由宿主在命令总线与工作流之间
// 裁决(与现网「意图 ID 同时是命令 ID 与 workflow IntentID」的现状兼容);
// handler 与 blueprint 来自 spec 字段的显式声明;command/workflow 预留给
// spec.action 显式声明(WP2)。
type ActionRef struct {
Kind string `json:"kind"`
ID string `json:"id"`
}
const (
ActionAuto = "auto"
ActionHandler = "handler"
ActionBlueprint = "blueprint"
ActionCommand = "command"
ActionWorkflow = "workflow"
)
// Command 是一条对宿主路由层的指令。一次识别可产出多条命令
// (如先 SetSlot 后 Start),宿主按序消费。
type Command struct {
Kind CommandKind `json:"kind"`
Action ActionRef `json:"action,omitempty"`
Slots map[string]string `json:"slots,omitempty"`
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
Missing []string `json:"missing,omitempty"`
Question string `json:"question,omitempty"`
Candidates []string `json:"candidates,omitempty"`
Scope string `json:"scope,omitempty"`
Target string `json:"target,omitempty"`
}
// actionOf 从候选推导动作引用:spec.action 显式声明优先,
// 其后是 blueprint 键、Handler,都没有时回落 auto(宿主裁决)。
func actionOf(c Candidate) ActionRef {
if c.Action != nil {
return *c.Action
}
if bp := strings.TrimSpace(c.Metadata["blueprint"]); bp != "" {
return ActionRef{Kind: ActionBlueprint, ID: bp}
}
if h := strings.TrimSpace(c.Handler); h != "" {
return ActionRef{Kind: ActionHandler, ID: h}
}
return ActionRef{Kind: ActionAuto, ID: c.ID}
}
// deriveCommands 把识别结果展开为命令列表(纯函数、确定性)。
// 规则:
// - guard_hit:不产出命令(守卫语义是「识别即拒绝」,由宿主处理;
// Cancel 命令留给声明 action 的意图与语义层,不在规则漏斗内代为表达)。
// - expectation_hit:每个已填槽位一条 SetSlot;槽位齐全且合法追加 Start,
// 否则追加 Clarify(缺槽/非法)。
// - matched:槽位齐全且合法 → 单条 Start;有缺失/非法 → Start(携带已抽槽,
// 宿主可带槽启动并追问,与现网行为一致)+ Clarify。
// - ambiguous:单条 Clarify(候选消歧,按现有候选序)。
// - fallback:不产出命令(交给宿主默认通道;Chitchat/Handoff 不由规则漏斗产出)。
func deriveCommands(res Result, snap *Snapshot) []Command {
if res.Top == nil {
if res.Branch == BranchAmbiguous && len(res.Candidates) > 1 {
ids := make([]string, 0, len(res.Candidates))
for _, c := range res.Candidates {
ids = append(ids, c.ID)
}
return []Command{{Kind: CommandClarify, Candidates: ids}}
}
return nil
}
top := res.Top
complete := len(top.MissingRequiredSlots) == 0 && len(top.InvalidSlots) == 0
switch res.Branch {
case BranchExpectationHit:
var cmds []Command
for _, k := range sortedKeys(top.Slots) {
cmds = append(cmds, Command{Kind: CommandSetSlot, Name: k, Value: top.Slots[k]})
}
if complete {
cmds = append(cmds, Command{Kind: CommandStart, Action: actionOf(*top), Slots: top.Slots})
} else {
cmds = append(cmds, clarifyOf(*top, snap))
}
return cmds
case BranchMatched:
start := Command{Kind: CommandStart, Action: actionOf(*top), Slots: top.Slots}
if complete {
return []Command{start}
}
return []Command{start, clarifyOf(*top, snap)}
}
return nil
}
// clarifyOf 组装澄清命令:Missing 为必填缺失与非法槽位的合并(确定性排序);
// Question 取第一个有 Question 配置的缺失/非法槽位的建议提问(查得到 spec 时)。
func clarifyOf(top Candidate, snap *Snapshot) Command {
invalid := sortedKeys(top.InvalidSlots)
missing := append(append([]string(nil), top.MissingRequiredSlots...), invalid...)
var question string
if snap != nil {
if spec, ok := snap.SpecByID(top.ID); ok {
asked := map[string]bool{}
for _, k := range missing {
if asked[k] {
continue
}
asked[k] = true
for _, s := range spec.Slots {
if s.Key == k && strings.TrimSpace(s.Question) != "" {
question = s.Question
break
}
}
if question != "" {
break
}
}
}
}
return Command{Kind: CommandClarify, Missing: missing, Question: question}
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}