|
|
|
@@ -9,6 +9,7 @@ import (
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
hexisclient "github.com/kami/hexis/pkg/client"
|
|
|
|
|
"github.com/kami/maven/internal/phraser"
|
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
|
|
|
"github.com/kami/maven/internal/store"
|
|
|
|
|
)
|
|
|
|
@@ -144,10 +145,10 @@ func (listAttentionCapability) handle(ctx context.Context, h *reactiveHandler, p
|
|
|
|
|
log.Printf("ecosystem: praxis attention: %v", err)
|
|
|
|
|
h.recordEcosystemTrace(ctx, "praxis", "list_attention", traceStatusForError(err),
|
|
|
|
|
started, traceErrorFields(err))
|
|
|
|
|
return "не могу сейчас узнать, что требует внимания."
|
|
|
|
|
return phraser.A(phraser.AttentionFail, nil)
|
|
|
|
|
}
|
|
|
|
|
if len(items) == 0 {
|
|
|
|
|
return "ничего не требует внимания."
|
|
|
|
|
return phraser.A(phraser.AttentionNone, nil)
|
|
|
|
|
}
|
|
|
|
|
h.recordPraxisTrace(ctx, "list_attention", started, map[string]any{"count": len(items)})
|
|
|
|
|
var parts []string
|
|
|
|
@@ -175,7 +176,7 @@ func (listAttentionCapability) handle(ctx context.Context, h *reactiveHandler, p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "требует внимания: " + strings.Join(parts, "; ")
|
|
|
|
|
return phraser.A(phraser.AttentionList, map[string]string{"items": strings.Join(parts, "; ")})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// listChangesCapability reads the recent-changes feed.
|
|
|
|
@@ -192,10 +193,10 @@ func (listChangesCapability) handle(ctx context.Context, h *reactiveHandler, px
|
|
|
|
|
log.Printf("ecosystem: praxis changes: %v", err)
|
|
|
|
|
h.recordEcosystemTrace(ctx, "praxis", "list_changes", traceStatusForError(err),
|
|
|
|
|
started, traceErrorFields(err))
|
|
|
|
|
return "не могу сейчас узнать об изменениях."
|
|
|
|
|
return phraser.A(phraser.ChangesFail, nil)
|
|
|
|
|
}
|
|
|
|
|
if len(changes) == 0 {
|
|
|
|
|
return "нет изменений."
|
|
|
|
|
return phraser.A(phraser.ChangesNone, nil)
|
|
|
|
|
}
|
|
|
|
|
h.recordPraxisTrace(ctx, "list_changes", started, map[string]any{"count": len(changes)})
|
|
|
|
|
var parts []string
|
|
|
|
@@ -204,7 +205,7 @@ func (listChangesCapability) handle(ctx context.Context, h *reactiveHandler, px
|
|
|
|
|
typ, _ := c["change_type"].(string)
|
|
|
|
|
parts = append(parts, fmt.Sprintf("%s (%s)", title, typ))
|
|
|
|
|
}
|
|
|
|
|
return "изменения: " + strings.Join(parts, "; ")
|
|
|
|
|
return phraser.A(phraser.ChangesList, map[string]string{"items": strings.Join(parts, "; ")})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// entityAttentionCapability answers "what's going on with X" by resolving X to
|
|
|
|
@@ -230,12 +231,12 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|
|
|
|
subject = dec.Slots.Text
|
|
|
|
|
}
|
|
|
|
|
if subject == "" {
|
|
|
|
|
return "про что именно спросить?"
|
|
|
|
|
return phraser.A(phraser.EcoAboutWhat, nil)
|
|
|
|
|
}
|
|
|
|
|
if h.ecosystem == nil || h.ecosystem.nexus == nil {
|
|
|
|
|
// Without Nexus there is no canonical ref to scope by. Say so rather
|
|
|
|
|
// than quietly answering about something else.
|
|
|
|
|
return "не могу связать это с сущностью — Nexus не настроен."
|
|
|
|
|
return phraser.A(phraser.EcoNoNexus, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
started := h.now()
|
|
|
|
@@ -248,15 +249,15 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|
|
|
|
h.recordEcosystemTrace(ctx, "nexus", "resolve", traceStatusForError(err), started,
|
|
|
|
|
mergeFields(traceErrorFields(err), map[string]any{"subject": redactSubject(subject)}))
|
|
|
|
|
if unauthorizedEcosystemError(err) {
|
|
|
|
|
return "экосистема отклоняет доступ, проверь токен."
|
|
|
|
|
return phraser.A(phraser.EcoDenied, nil)
|
|
|
|
|
}
|
|
|
|
|
return "экосистема недоступна, попробуй ещё раз."
|
|
|
|
|
return phraser.A(phraser.EcoDown, nil)
|
|
|
|
|
}
|
|
|
|
|
if len(ambiguous) > 0 {
|
|
|
|
|
return "уточни, что именно: " + strings.Join(ambiguous, ", ") + "?"
|
|
|
|
|
return phraser.A(phraser.EcoAmbiguous, map[string]string{"items": strings.Join(ambiguous, ", ")})
|
|
|
|
|
}
|
|
|
|
|
if entityID == "" {
|
|
|
|
|
return "не знаю такой сущности."
|
|
|
|
|
return phraser.A(phraser.EcoUnknownEntity, nil)
|
|
|
|
|
}
|
|
|
|
|
if displayName == "" {
|
|
|
|
|
displayName = subject
|
|
|
|
@@ -268,7 +269,7 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|
|
|
|
log.Printf("ecosystem: praxis attention for %s: %v", entityID, err)
|
|
|
|
|
h.recordEcosystemTrace(ctx, "praxis", "entity_attention", traceStatusForError(err),
|
|
|
|
|
queried, mergeFields(traceErrorFields(err), map[string]any{"entity_id": entityID}))
|
|
|
|
|
return "не могу сейчас узнать, что требует внимания по «" + displayName + "»."
|
|
|
|
|
return phraser.A(phraser.AttentionFailEntity, map[string]string{"name": displayName})
|
|
|
|
|
}
|
|
|
|
|
items, scoped := scopedToEntity(items, entityID)
|
|
|
|
|
if !scoped {
|
|
|
|
@@ -279,7 +280,7 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|
|
|
|
log.Printf("ecosystem: praxis returned unscoped items for %s, refusing to answer", entityID)
|
|
|
|
|
h.recordEcosystemTrace(ctx, "praxis", "entity_attention", traceFailed, queried,
|
|
|
|
|
map[string]any{"entity_id": entityID, "class": "unscoped_response"})
|
|
|
|
|
return "не могу сейчас узнать, что требует внимания по «" + displayName + "»."
|
|
|
|
|
return phraser.A(phraser.AttentionFailEntity, map[string]string{"name": displayName})
|
|
|
|
|
}
|
|
|
|
|
h.recordPraxisTrace(ctx, "entity_attention", queried, map[string]any{
|
|
|
|
|
"entity_id": entityID, "count": len(items),
|
|
|
|
@@ -303,9 +304,9 @@ func (entityAttentionCapability) handle(ctx context.Context, h *reactiveHandler,
|
|
|
|
|
parts = append(parts, known)
|
|
|
|
|
}
|
|
|
|
|
if len(parts) == 0 {
|
|
|
|
|
return "по «" + displayName + "» ничего нет."
|
|
|
|
|
return phraser.A(phraser.AttentionNoneEntity, map[string]string{"name": displayName})
|
|
|
|
|
}
|
|
|
|
|
return "по «" + displayName + "»: " + strings.Join(parts, "; ")
|
|
|
|
|
return phraser.A(phraser.AttentionListEntity, map[string]string{"name": displayName, "items": strings.Join(parts, "; ")})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scopedToEntity drops items that carry an entity_id other than the one asked
|
|
|
|
@@ -370,7 +371,7 @@ func (h *reactiveHandler) localFactsForEntity(ctx context.Context, entityID stri
|
|
|
|
|
if len(parts) == 0 {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
out := "я помню: " + strings.Join(parts, ", ")
|
|
|
|
|
out := phraser.A(phraser.EcoRecall, map[string]string{"items": strings.Join(parts, ", ")})
|
|
|
|
|
if more {
|
|
|
|
|
out += ", и это не всё"
|
|
|
|
|
}
|
|
|
|
@@ -520,18 +521,18 @@ func (h *reactiveHandler) handleHexisAct(ctx context.Context, dec router.Decisio
|
|
|
|
|
h.recordEcosystemTrace(ctx, "nexus", "resolve", traceStatusForError(err), started,
|
|
|
|
|
mergeFields(traceErrorFields(err), map[string]any{"subject": redactSubject(dec.Slots.Text)}))
|
|
|
|
|
if unauthorizedEcosystemError(err) {
|
|
|
|
|
return "экосистема отклоняет доступ, проверь токен."
|
|
|
|
|
return phraser.A(phraser.EcoDenied, nil)
|
|
|
|
|
}
|
|
|
|
|
// A genuine Nexus dependency failure, not "no such entity" — stop here
|
|
|
|
|
// and report degradation rather than silently falling through to the
|
|
|
|
|
// local command executor (ECOSYSTEM-SPEC.md: services degrade
|
|
|
|
|
// independently, never a silent all-clear).
|
|
|
|
|
return "экосистема недоступна, попробуй ещё раз."
|
|
|
|
|
return phraser.A(phraser.EcoDown, nil)
|
|
|
|
|
}
|
|
|
|
|
if len(ambiguous) > 0 {
|
|
|
|
|
h.recordEcosystemTrace(ctx, "nexus", "resolve", traceAmbig, started,
|
|
|
|
|
map[string]any{"candidates": len(ambiguous)})
|
|
|
|
|
return "уточни, что именно: " + strings.Join(ambiguous, ", ") + "?"
|
|
|
|
|
return phraser.A(phraser.EcoAmbiguous, map[string]string{"items": strings.Join(ambiguous, ", ")})
|
|
|
|
|
}
|
|
|
|
|
if entityID == "" {
|
|
|
|
|
h.recordEcosystemTrace(ctx, "nexus", "resolve", traceNotFound, started,
|
|
|
|
@@ -550,9 +551,9 @@ func (h *reactiveHandler) handleHexisAct(ctx context.Context, dec router.Decisio
|
|
|
|
|
h.recordEcosystemTrace(ctx, "hexis", "capabilities", traceStatusForError(err), discovered,
|
|
|
|
|
mergeFields(traceErrorFields(err), map[string]any{"entity_id": entityID}))
|
|
|
|
|
if unauthorizedEcosystemError(err) {
|
|
|
|
|
return "экосистема отклоняет доступ, проверь токен."
|
|
|
|
|
return phraser.A(phraser.EcoDenied, nil)
|
|
|
|
|
}
|
|
|
|
|
return "экосистема недоступна, попробуй ещё раз."
|
|
|
|
|
return phraser.A(phraser.EcoDown, nil)
|
|
|
|
|
}
|
|
|
|
|
h.recordEcosystemTrace(ctx, "hexis", "capabilities", traceOK, discovered,
|
|
|
|
|
map[string]any{"entity_id": entityID, "count": len(caps)})
|
|
|
|
@@ -584,7 +585,7 @@ func (h *reactiveHandler) handleHexisAct(ctx context.Context, dec router.Decisio
|
|
|
|
|
for _, m := range matches {
|
|
|
|
|
names = append(names, m.Name)
|
|
|
|
|
}
|
|
|
|
|
return "какую команду для " + displayName + ": " + strings.Join(names, ", ") + "?"
|
|
|
|
|
return phraser.A(phraser.ActWhich, map[string]string{"name": displayName, "items": strings.Join(names, ", ")})
|
|
|
|
|
}
|
|
|
|
|
matched := matches[0]
|
|
|
|
|
|
|
|
|
@@ -602,7 +603,7 @@ func (h *reactiveHandler) handleHexisAct(ctx context.Context, dec router.Decisio
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
h.recordEcosystemTrace(ctx, "hexis", "confirmation", tracePending, started,
|
|
|
|
|
map[string]any{"entity_id": entityID, "capability": matched.Name})
|
|
|
|
|
return "выполнить «" + matched.Name + "» для " + displayName + "? скажи «да» или «нет»."
|
|
|
|
|
return phraser.A(phraser.ActConfirmEntity, map[string]string{"name": matched.Name, "entity": displayName})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return h.execHexis(ctx, matched.ID, matched.Name, entityID, displayName)
|
|
|
|
@@ -622,7 +623,7 @@ func (h *reactiveHandler) execHexis(ctx context.Context, capID, capName, entityI
|
|
|
|
|
mergeFields(traceErrorFields(err), map[string]any{
|
|
|
|
|
"entity_id": entityID, "capability": capName, "causation_id": causationID,
|
|
|
|
|
}))
|
|
|
|
|
return "не получилось выполнить команду для " + displayName + "."
|
|
|
|
|
return phraser.A(phraser.ActFailEntity, map[string]string{"name": displayName})
|
|
|
|
|
}
|
|
|
|
|
// One record per hop: the second write this used to make said the same
|
|
|
|
|
// thing under a different key, in a different shape.
|
|
|
|
@@ -630,5 +631,5 @@ func (h *reactiveHandler) execHexis(ctx context.Context, capID, capName, entityI
|
|
|
|
|
"entity_id": entityID, "entity_name": displayName,
|
|
|
|
|
"capability": capName, "causation_id": causationID,
|
|
|
|
|
})
|
|
|
|
|
return "команда выполнена для " + displayName + "."
|
|
|
|
|
return phraser.A(phraser.ActDoneEntity, map[string]string{"name": displayName})
|
|
|
|
|
}
|
|
|
|
|