package agents import ( "context" "fmt" "log/slog" "slices" "strings" "time" "g.fc.run/misc/byre/internal/api" "g.fc.run/misc/byre/internal/store" ) func (m *Manager) Respond(ctx context.Context, user string, creates []api.InboxResponseCreate) ([]api.InboxItem, error) { if len(creates) == 0 { return nil, fmt.Errorf("%w: responses is empty", ErrInvalid) } resolutions := []store.Resolution{} sessions := []string{} seen := map[string]bool{} for _, create := range creates { if seen[create.ItemId] { return nil, fmt.Errorf("%w: item %s answered twice in one submission", ErrInvalid, create.ItemId) } seen[create.ItemId] = true item, err := m.store.GetInboxItem(ctx, create.ItemId) if err != nil { return nil, err } state, err := CheckResponse(item, create) if err != nil { return nil, err } attachments := []api.Attachment{} if create.Attachments != nil { attachments = *create.Attachments } resolutions = append(resolutions, store.Resolution{ State: state, Response: api.InboxResponse{ Id: store.NewID(), ItemId: item.Id, User: user, Type: create.Type, Options: create.Options, Text: create.Text, Attachments: attachments, CreatedAt: time.Now().UTC(), }, }) if !slices.Contains(sessions, item.AgentSessionId) { sessions = append(sessions, item.AgentSessionId) } } err := m.store.Respond(ctx, user, resolutions) if err != nil { slog.Error("record inbox responses", "user", user, "err", err) return nil, err } for _, res := range resolutions { slog.Info("inbox response recorded", "item", res.Response.ItemId, "response", res.Response.Id, "type", res.Response.Type, "user", user) } for _, id := range sessions { go func() { err := m.Deliver(context.Background(), id) if err != nil { slog.Error("deliver inbox responses", "session", id, "err", err) } }() } items := []api.InboxItem{} for _, res := range resolutions { item, err := m.store.GetInboxItem(ctx, res.Response.ItemId) if err != nil { return nil, err } items = append(items, item) } return items, nil } func CheckResponse(item api.InboxItem, c api.InboxResponseCreate) (api.InboxState, error) { if item.State != api.Open { return "", fmt.Errorf("%w: item %s is %s", ErrConflict, item.Id, item.State) } text := "" if c.Text != nil { text = strings.TrimSpace(*c.Text) } question := item.Kind == api.Question bad := func(format string, args ...any) (api.InboxState, error) { return "", fmt.Errorf("%w: item %s: "+format, append([]any{ErrInvalid, item.Id}, args...)...) } switch c.Type { case api.Option: if !question { return bad("only questions have options") } if c.Options == nil || len(*c.Options) == 0 { return bad("option needs the chosen options") } if item.Payload["multiple"] != true && len(*c.Options) != 1 { return bad("a pick-one question takes exactly one option") } seen := map[string]bool{} for _, o := range *c.Options { if !hasOption(item, o) { return bad("%q is not one of the question's options", o) } if seen[o] { return bad("%q chosen twice", o) } seen[o] = true } if text != "" && item.Payload["multiple"] != true { return bad("a pick-one option takes no text; use other") } return api.Answered, nil case api.Other: if text == "" { return bad("other needs text") } return api.Answered, nil case api.Dismiss: if question { return bad("questions need an option or other, not a dismissal") } return api.Dismissed, nil } return bad("unknown response type %q", c.Type) } func hasOption(item api.InboxItem, option string) bool { options, ok := item.Payload["options"].([]any) if !ok { return false } for _, o := range options { if o == option { return true } } return false }