Merge branch 'refactor/praxis-capability-registry' into integration/small-batch

This commit is contained in:
kami
2026-08-01 00:43:54 +04:00
+112 -56
View File
@@ -12,6 +12,68 @@ import (
"github.com/kami/maven/internal/router" "github.com/kami/maven/internal/router"
) )
// praxisCapability is one arm of the Praxis act dispatch. This is an interface
// rather than a map[string]func because each arm carries its own state: the
// verb aliases it answers to, the trace name it records, and its own reply
// formatting. The dispatch grows an arm per Praxis capability, so a new one is
// added to praxisCapabilities below and nothing else changes.
type praxisCapability interface {
// aliases are the verbs (router fn slots, EN and RU) this capability answers to.
aliases() []string
// handle runs the capability and returns the user-facing reply.
handle(ctx context.Context, h *reactiveHandler, px *praxisClient, dec router.Decision) string
}
// praxisCapabilities is the registry handlePraxisAct consults, in order.
var praxisCapabilities = []praxisCapability{
listAttentionCapability{},
praxisItemAction{
verbs: []string{"acknowledge_item", "принято", "понял", "поняла"},
ask: "какой пункт отметить принятым?",
op: "acknowledge",
failure: "не получилось отметить принятым.",
success: "принято.",
call: func(ctx context.Context, px *praxisClient, id string) error {
_, err := px.Acknowledge(ctx, id)
return err
},
},
praxisItemAction{
verbs: []string{"resolve_item", "сделано", "готово", "решено"},
ask: "какой пункт отметить сделанным?",
op: "resolve",
failure: "не получилось отметить сделанным.",
success: "отмечено как сделано.",
call: func(ctx context.Context, px *praxisClient, id string) error {
_, err := px.Resolve(ctx, id)
return err
},
},
praxisItemAction{
verbs: []string{"ignore_item", "игнорировать", "неважно"},
ask: "какой пункт игнорировать?",
op: "ignore",
failure: "не получилось проигнорировать.",
success: "проигнорировано.",
call: func(ctx context.Context, px *praxisClient, id string) error {
_, err := px.Ignore(ctx, id)
return err
},
},
praxisItemAction{
verbs: []string{"pin_item", "закрепить"},
ask: "какой пункт закрепить?",
op: "pin",
failure: "не получилось закрепить.",
success: "закреплено.",
call: func(ctx context.Context, px *praxisClient, id string) error {
_, err := px.Pin(ctx, id, true)
return err
},
},
listChangesCapability{},
}
// handlePraxisAct — dispatches ecosystem tool acts through the Praxis tools API. // handlePraxisAct — dispatches ecosystem tool acts through the Praxis tools API.
// Returns "" when the act is not a Praxis verb (the caller falls through to the // Returns "" when the act is not a Praxis verb (the caller falls through to the
// system command executor). Returns a reply string otherwise. // system command executor). Returns a reply string otherwise.
@@ -20,12 +82,51 @@ func (h *reactiveHandler) handlePraxisAct(ctx context.Context, dec router.Decisi
return "" return ""
} }
px := h.ecosystem.praxis px := h.ecosystem.praxis
fn := dec.Slots.Fn for _, capability := range praxisCapabilities {
for _, alias := range capability.aliases() {
if alias == dec.Slots.Fn {
return capability.handle(ctx, h, px, dec)
}
}
}
// Not a Praxis verb — let the caller fall through.
return ""
}
// Map verbs and Russian aliases to Praxis tool calls. // praxisItemAction is the shared shape of the item-lifecycle capabilities: take
// Each case: if the verb matches, call the tool and return a user-facing reply. // an item id from the value slot, call one Praxis endpoint, trace the result.
switch fn { type praxisItemAction struct {
case "list_attention", "attention", "внимание", "что требует внимания", "что нового": verbs []string
ask string // reply when no item id was given
op string // trace + log name of the operation
failure string // reply when the Praxis call errors
success string
call func(ctx context.Context, px *praxisClient, id string) error
}
func (a praxisItemAction) aliases() []string { return a.verbs }
func (a praxisItemAction) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, dec router.Decision) string {
id := dec.Slots.Value
if id == "" {
return a.ask
}
if err := a.call(ctx, px, id); err != nil {
log.Printf("ecosystem: praxis %s %s: %v", a.op, id, err)
return a.failure
}
h.recordPraxisTrace(ctx, a.op, map[string]any{"item_id": id})
return a.success
}
// listAttentionCapability reads the attention digest and surfaces every item it speaks.
type listAttentionCapability struct{}
func (listAttentionCapability) aliases() []string {
return []string{"list_attention", "attention", "внимание", "что требует внимания", "что нового"}
}
func (listAttentionCapability) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, _ router.Decision) string {
items, err := px.ListAttention(ctx, 20) items, err := px.ListAttention(ctx, 20)
if err != nil { if err != nil {
log.Printf("ecosystem: praxis attention: %v", err) log.Printf("ecosystem: praxis attention: %v", err)
@@ -61,56 +162,16 @@ func (h *reactiveHandler) handlePraxisAct(ctx context.Context, dec router.Decisi
} }
} }
return "требует внимания: " + strings.Join(parts, "; ") return "требует внимания: " + strings.Join(parts, "; ")
}
case "acknowledge_item", "принято", "понял", "поняла": // listChangesCapability reads the recent-changes feed.
id := dec.Slots.Value type listChangesCapability struct{}
if id == "" {
return "какой пункт отметить принятым?"
}
if _, err := px.Acknowledge(ctx, id); err != nil {
log.Printf("ecosystem: praxis acknowledge %s: %v", id, err)
return "не получилось отметить принятым."
}
h.recordPraxisTrace(ctx, "acknowledge", map[string]any{"item_id": id})
return "принято."
case "resolve_item", "сделано", "готово", "решено": func (listChangesCapability) aliases() []string {
id := dec.Slots.Value return []string{"list_changes", "changes", "изменения", "что изменилось"}
if id == "" {
return "какой пункт отметить сделанным?"
} }
if _, err := px.Resolve(ctx, id); err != nil {
log.Printf("ecosystem: praxis resolve %s: %v", id, err)
return "не получилось отметить сделанным."
}
h.recordPraxisTrace(ctx, "resolve", map[string]any{"item_id": id})
return "отмечено как сделано."
case "ignore_item", "игнорировать", "неважно": func (listChangesCapability) handle(ctx context.Context, h *reactiveHandler, px *praxisClient, _ router.Decision) string {
id := dec.Slots.Value
if id == "" {
return "какой пункт игнорировать?"
}
if _, err := px.Ignore(ctx, id); err != nil {
log.Printf("ecosystem: praxis ignore %s: %v", id, err)
return "не получилось проигнорировать."
}
h.recordPraxisTrace(ctx, "ignore", map[string]any{"item_id": id})
return "проигнорировано."
case "pin_item", "закрепить":
id := dec.Slots.Value
if id == "" {
return "какой пункт закрепить?"
}
if _, err := px.Pin(ctx, id, true); err != nil {
log.Printf("ecosystem: praxis pin %s: %v", id, err)
return "не получилось закрепить."
}
h.recordPraxisTrace(ctx, "pin", map[string]any{"item_id": id})
return "закреплено."
case "list_changes", "changes", "изменения", "что изменилось":
changes, err := px.ListChanges(ctx, 20) changes, err := px.ListChanges(ctx, 20)
if err != nil { if err != nil {
log.Printf("ecosystem: praxis changes: %v", err) log.Printf("ecosystem: praxis changes: %v", err)
@@ -127,11 +188,6 @@ func (h *reactiveHandler) handlePraxisAct(ctx context.Context, dec router.Decisi
parts = append(parts, fmt.Sprintf("%s (%s)", title, typ)) parts = append(parts, fmt.Sprintf("%s (%s)", title, typ))
} }
return "изменения: " + strings.Join(parts, "; ") return "изменения: " + strings.Join(parts, "; ")
default:
// Not a Praxis verb — let the caller fall through.
return ""
}
} }
// recordPraxisTrace — writes a fact recording a cross-service ecosystem call. // recordPraxisTrace — writes a fact recording a cross-service ecosystem call.