package main import ( "context" "errors" "log" "github.com/kami/maven/internal/mcp" "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) case errors.Is(err, mcp.ErrNeedsArgs): // An MCP tool that wants named arguments a spoken verb cannot // supply. Guessing them would be a wrong act, so she says so // instead — the tool is still runnable from the authed surface, // where a human types them. return "этому инструменту нужны аргументы, которые я из голоса не соберу — я не буду угадывать." } log.Printf("voice: tool %s: %v", dec.Slots.Fn, err) if out != "" { return "не получилось выполнить команду: " + firstLine(out) } return "не получилось выполнить команду." } if out != "" { return "готово: " + firstLine(out) } return "готово." }