Files
Assistant/cmd/continuity-runtime/attention.go
T
m b3491520bd
CI / test (push) Successful in 20s
CI / test (pull_request) Successful in 20s
DEV-000069 general agent delegation codex bridge foundation
2026-08-17 20:07:14 +03:00

186 lines
6.9 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"git.cin.su/m/Assistant/contracts/continuity"
"git.cin.su/m/Assistant/contracts/identity"
)
type attentionRepository interface {
AttentionByOwner(context.Context, identity.PrincipalID, int) ([]continuity.AttentionItem, error)
CreateAttentionItem(context.Context, continuity.AttentionItem) (continuity.AttentionItem, error)
}
func registerCreatorAttentionRoutes(mux *http.ServeMux, identityClient creatorIdentityClient, repository attentionRepository) {
mux.HandleFunc("GET /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) {
session, ok := creatorSessionForAttention(w, r, identityClient)
if !ok {
return
}
limit, err := parseListLimit(r.URL.Query().Get("limit"))
if err != nil {
http.Error(w, "invalid attention limit", http.StatusBadRequest)
return
}
items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, limit)
if err != nil {
http.Error(w, "attention unavailable", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(continuity.AttentionListResult{Items: items})
})
mux.HandleFunc("GET /v1/creator/today", func(w http.ResponseWriter, r *http.Request) {
session, ok := creatorSessionForAttention(w, r, identityClient)
if !ok {
return
}
items, err := repository.AttentionByOwner(r.Context(), session.PrincipalID, 50)
if err != nil {
http.Error(w, "today unavailable", http.StatusInternalServerError)
return
}
result := continuity.TodayResult{Items: make([]continuity.TodayItem, 0, len(items))}
for _, item := range items {
result.Items = append(result.Items, continuity.TodayItem{
Title: item.Summary,
WhenUnixMillis: item.DueAtUnixMillis,
AttentionLevel: item.Level,
AttentionState: item.State,
HumanState: humanAttentionState(item.State),
DeliveryState: humanDeliveryState(item),
SourceDescription: humanAttentionSource(item.SourceKind),
ResponseHint: humanResponseHint(item),
})
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
_ = json.NewEncoder(w).Encode(result)
})
mux.HandleFunc("POST /v1/creator/attention", func(w http.ResponseWriter, r *http.Request) {
session, ok := creatorSessionForAttention(w, r, identityClient)
if !ok {
return
}
var request continuity.CreateAttentionRequest
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 32<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&request); err != nil {
http.Error(w, "invalid attention request", http.StatusBadRequest)
return
}
if request.OwnerPrincipalID == "" {
request.OwnerPrincipalID = session.PrincipalID
}
if request.OwnerPrincipalID != session.PrincipalID || request.ScopeID == "" || request.SourceKind == "" || request.Level == "" || request.Summary == "" || request.CreatedAtUnixMillis < 1 || request.DueAtUnixMillis < 1 {
http.Error(w, "invalid attention request", http.StatusBadRequest)
return
}
if request.SourceKind == continuity.AttentionSourceAgentDelegation && request.SourceExternalRef == "" {
http.Error(w, "delegation attention requires source external ref", http.StatusBadRequest)
return
}
item := continuity.AttentionItem{
ID: continuity.AttentionItemID(request.SourceExternalRef),
OwnerPrincipalID: request.OwnerPrincipalID,
ScopeID: request.ScopeID,
SourceKind: request.SourceKind,
SourceProspectiveID: request.SourceProspectiveID,
SourceExternalRef: request.SourceExternalRef,
Level: request.Level,
State: continuity.AttentionPending,
Summary: request.Summary,
DueAtUnixMillis: request.DueAtUnixMillis,
CreatedAtUnixMillis: request.CreatedAtUnixMillis,
Revision: 1,
}
saved, err := repository.CreateAttentionItem(r.Context(), item)
if err != nil {
http.Error(w, "attention unavailable", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(saved)
})
}
func creatorSessionForAttention(w http.ResponseWriter, r *http.Request, identityClient creatorIdentityClient) (identity.CreatorSession, bool) {
session, err := identityClient.Session(r.Context(), r.Header.Get("Authorization"))
if errors.Is(err, errCreatorSessionDenied) {
http.Error(w, "creator session denied", http.StatusUnauthorized)
return identity.CreatorSession{}, false
}
if err != nil {
http.Error(w, "identity service unavailable", http.StatusServiceUnavailable)
return identity.CreatorSession{}, false
}
return session, true
}
func humanAttentionState(state continuity.AttentionState) string {
switch state {
case continuity.AttentionPending:
return "ожидает внимания"
case continuity.AttentionDelivering:
return "доставляется"
case continuity.AttentionDelivered:
return "доставлено"
case continuity.AttentionResolved:
return "закрыто"
case continuity.AttentionSuppressed:
return "скрыто"
case continuity.AttentionExpired:
return "истекло"
case continuity.AttentionFailed:
return "не доставлено"
default:
return "состояние уточняется"
}
}
func humanResponseHint(item continuity.AttentionItem) string {
if item.SourceKind == continuity.AttentionSourceAgentDelegation {
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
return "Можно ответить: «Продолжай» или «Нет, нужен другой вариант»."
}
if item.ResponseOutcome != "" {
return "Ответ записан: " + string(item.ResponseOutcome)
}
return ""
}
if item.State == continuity.AttentionDelivered || item.State == continuity.AttentionPending || item.State == continuity.AttentionFailed {
return "Можно ответить: «Готово», «Отложи на 10 минут» или «Отмени»."
}
if item.ResponseOutcome != "" {
return "Ответ записан: " + string(item.ResponseOutcome)
}
return ""
}
func humanDeliveryState(item continuity.AttentionItem) string {
if item.State == continuity.AttentionDelivered {
return "уведомление отправлено"
}
if item.State == continuity.AttentionDelivering {
return "уведомление отправляется"
}
return "уведомление ещё не отправлено"
}
func humanAttentionSource(kind continuity.AttentionSourceKind) string {
if kind == continuity.AttentionSourceProspectiveReminder {
return "напоминание"
}
if kind == continuity.AttentionSourceAgentDelegation {
return "задача coding-agent"
}
return "событие Домового"
}