b09967f9e6
Pure move: actionFact, actionReminder, actionAct and actionNote each get their own actions_<intent>.go. The two small ones (chat, system) and the actionHandlers table stay in actions.go, which is now just the dispatch layer and the notes about what does not belong in it. No behaviour change — only the file a handler is read in.
67 lines
2.3 KiB
Go
67 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
"github.com/kami/maven/internal/tool"
|
|
)
|
|
|
|
// actionAct handles router.IntentAct: match a verb to an enabled tool, offer
|
|
// it to the ecosystems first, and run it behind the confirm gate and the
|
|
// allowlist. proposeGap and the confirm gate itself live in confirm.go.
|
|
func (h *reactiveHandler) actionAct(ctx context.Context, dec router.Decision) string {
|
|
// tool executor: run the matched fn against the enabled allowlist.
|
|
// HasFn=false ⇒ try the matcher (for LLM-routed acts where the verb
|
|
// didn't go through the stage-0 act grammar).
|
|
if !dec.Slots.HasFn && dec.Slots.Text != "" && h.matcher != nil {
|
|
if fn, args, ok := h.matcher.Match(dec.Slots.Text); ok {
|
|
dec.Slots.Fn, dec.Slots.Args, dec.Slots.HasFn = fn, args, true
|
|
}
|
|
}
|
|
|
|
// Praxis ecosystem tools: intercept before the system command executor.
|
|
if h.ecosystem != nil && h.ecosystem.praxis != nil && dec.Slots.HasFn {
|
|
if reply := h.handlePraxisAct(ctx, dec); reply != "" {
|
|
return reply
|
|
}
|
|
}
|
|
|
|
// Hexis ecosystem action: if ecosystem is configured and we have a verb
|
|
// + entity text, try to resolve the entity and execute via Hexis.
|
|
if h.ecosystem != nil && h.ecosystem.hexis != nil && dec.Slots.Text != "" {
|
|
if reply := h.handleHexisAct(ctx, dec); reply != "" {
|
|
return reply
|
|
}
|
|
}
|
|
|
|
// HasFn still false ⇒ no allowlist match: scaffold a 'proposed' tool
|
|
// the user can enable on the authed surface ("earn the right to ask").
|
|
if !dec.Slots.HasFn {
|
|
return h.proposeGap(ctx, dec)
|
|
}
|
|
out, err := h.tools.Exec(ctx, dec.Slots.Fn, dec.Slots.Args, false)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, tool.ErrNeedsConfirm):
|
|
// destructive: park it and ask. The next utterance answers.
|
|
phrase := actPhrase(dec.Slots.Fn, dec.Slots.Args)
|
|
h.park(dec.Slots.Fn, dec.Slots.Args, phrase)
|
|
return "выполнить «" + phrase + "»? скажи «да» или «нет»."
|
|
case errors.Is(err, tool.ErrNotEnabled):
|
|
return h.proposeGap(ctx, dec)
|
|
}
|
|
log.Printf("voice: tool %s: %v", dec.Slots.Fn, err)
|
|
if out != "" {
|
|
return "не получилось выполнить команду: " + firstLine(out)
|
|
}
|
|
return "не получилось выполнить команду."
|
|
}
|
|
if out != "" {
|
|
return "готово: " + firstLine(out)
|
|
}
|
|
return "готово."
|
|
}
|