Answer a question again by forking at its answer's snapshot
This commit is contained in:
+78
-11
@@ -3,6 +3,7 @@ package agents
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
@@ -10,38 +11,104 @@ import (
|
||||
)
|
||||
|
||||
func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User) (api.Task, error) {
|
||||
t, _, err := m.fork(ctx, src, project, n, creator)
|
||||
return t, err
|
||||
}
|
||||
|
||||
func (m *Manager) AnswerAgain(ctx context.Context, creator api.User, create api.InboxResponseCreate) (api.Task, error) {
|
||||
item, err := m.store.GetInboxItem(ctx, create.ItemId)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
if item.State != api.Answered {
|
||||
return api.Task{}, fmt.Errorf("%w: question %s is %s, not answered", ErrConflict, item.Id, item.State)
|
||||
}
|
||||
n := 0
|
||||
for _, r := range item.Responses {
|
||||
if r.SnapshotN != nil {
|
||||
n = *r.SnapshotN
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return api.Task{}, fmt.Errorf("%w: question %s's answer reached the agent without a snapshot to fork from", ErrConflict, item.Id)
|
||||
}
|
||||
open := item
|
||||
open.State = api.Open
|
||||
_, err = CheckResponse(open, create)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
|
||||
src, err := m.store.GetTask(ctx, item.TaskId)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
project, err := m.store.GetProject(ctx, src.ProjectId)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
t, sessionID, err := m.fork(ctx, src, project, n, creator)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
}
|
||||
|
||||
id := store.NewID()
|
||||
err = m.store.CreateAnsweredItem(ctx, api.InboxItem{
|
||||
Id: id,
|
||||
ProjectId: t.ProjectId,
|
||||
TaskId: t.Id,
|
||||
AgentSessionId: sessionID,
|
||||
ThreadId: id,
|
||||
Payload: item.Payload,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}, newResponse(id, creator.Login, create))
|
||||
if err != nil {
|
||||
return api.Task{}, fmt.Errorf("task %s forked, but its answer was not recorded: %w", t.Id, err)
|
||||
}
|
||||
slog.Info("question answered again", "item", item.Id, "fork", t.Id, "snapshot", n, "user", creator.Login)
|
||||
|
||||
go func() {
|
||||
err := m.Deliver(context.Background(), sessionID)
|
||||
if err != nil {
|
||||
slog.Error("deliver inbox responses", "session", sessionID, "err", err)
|
||||
}
|
||||
}()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User) (api.Task, string, error) {
|
||||
if creator.GitName == "" || creator.GitEmail == "" {
|
||||
return api.Task{}, ErrNoGitIdentity
|
||||
return api.Task{}, "", ErrNoGitIdentity
|
||||
}
|
||||
if src.Status != api.TaskStatusActive {
|
||||
return api.Task{}, fmt.Errorf("%w: task %s is %s", ErrConflict, src.Id, src.Status)
|
||||
return api.Task{}, "", fmt.Errorf("%w: task %s is %s", ErrConflict, src.Id, src.Status)
|
||||
}
|
||||
|
||||
snap, err := m.store.GetSnapshot(ctx, src.Id, n)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
return api.Task{}, "", err
|
||||
}
|
||||
srcSess, err := m.store.GetSession(ctx, snap.AgentSessionId)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
return api.Task{}, "", err
|
||||
}
|
||||
if srcSess.Vendor == api.Antigravity {
|
||||
return api.Task{}, fmt.Errorf("%w: snapshot %d of task %s is from an Antigravity session, and agy can't fork a conversation", ErrConflict, n, src.Id)
|
||||
return api.Task{}, "", fmt.Errorf("%w: snapshot %d of task %s is from an Antigravity session, and agy can't fork a conversation", ErrConflict, n, src.Id)
|
||||
}
|
||||
events, err := m.store.TaskEvents(ctx, src.Id, 0, -1)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
return api.Task{}, "", err
|
||||
}
|
||||
at := forkPoint(events, snap.AgentSessionId, snap.EventSeq)
|
||||
if at == "" {
|
||||
return api.Task{}, fmt.Errorf("%w: snapshot %d of task %s has no recorded agent message at or before it to fork the conversation from", ErrConflict, n, src.Id)
|
||||
return api.Task{}, "", fmt.Errorf("%w: snapshot %d of task %s has no recorded agent message at or before it to fork the conversation from", ErrConflict, n, src.Id)
|
||||
}
|
||||
|
||||
var frozen *store.FrozenWorkflow
|
||||
if src.Workflow != nil {
|
||||
f, err := m.store.TaskWorkflow(ctx, src.Id)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
return api.Task{}, "", err
|
||||
}
|
||||
frozen = &f
|
||||
}
|
||||
@@ -58,7 +125,7 @@ func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n
|
||||
ParentTaskId: src.ParentTaskId,
|
||||
}, frozen, snap.CommitSha, creator.Login)
|
||||
if err != nil {
|
||||
return api.Task{}, err
|
||||
return api.Task{}, "", err
|
||||
}
|
||||
|
||||
id := newUUID()
|
||||
@@ -76,9 +143,9 @@ func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n
|
||||
ForkedAt: &at,
|
||||
}, t.ProjectId)
|
||||
if err != nil {
|
||||
return api.Task{}, fmt.Errorf("task %s created, but its agent session was not: %w", t.Id, err)
|
||||
return api.Task{}, "", fmt.Errorf("task %s created, but its agent session was not: %w", t.Id, err)
|
||||
}
|
||||
return t, nil
|
||||
return t, id, nil
|
||||
}
|
||||
|
||||
func forkPoint(events []api.Event, sessionID string, seq int64) string {
|
||||
|
||||
@@ -489,7 +489,7 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ev.Kind == "inbox.updated" {
|
||||
if ev.Kind == "inbox.updated" || item.State != api.Open {
|
||||
return m.answeredText(ctx, item), nil
|
||||
}
|
||||
return m.itemText(ctx, item), nil
|
||||
|
||||
+24
-21
@@ -38,28 +38,9 @@ func (m *Manager) Respond(ctx context.Context, user string, creates []api.InboxR
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attachments := []api.Attachment{}
|
||||
if create.Attachments != nil {
|
||||
attachments = *create.Attachments
|
||||
}
|
||||
images := []string{}
|
||||
if create.Images != nil {
|
||||
images = *create.Images
|
||||
}
|
||||
|
||||
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,
|
||||
Images: images,
|
||||
Attachments: attachments,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
},
|
||||
State: state,
|
||||
Response: newResponse(item.Id, user, create),
|
||||
})
|
||||
if !slices.Contains(sessions, item.AgentSessionId) {
|
||||
sessions = append(sessions, item.AgentSessionId)
|
||||
@@ -98,6 +79,28 @@ func (m *Manager) Respond(ctx context.Context, user string, creates []api.InboxR
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func newResponse(itemID, user string, create api.InboxResponseCreate) api.InboxResponse {
|
||||
attachments := []api.Attachment{}
|
||||
if create.Attachments != nil {
|
||||
attachments = *create.Attachments
|
||||
}
|
||||
images := []string{}
|
||||
if create.Images != nil {
|
||||
images = *create.Images
|
||||
}
|
||||
return api.InboxResponse{
|
||||
Id: store.NewID(),
|
||||
ItemId: itemID,
|
||||
User: user,
|
||||
Type: create.Type,
|
||||
Options: create.Options,
|
||||
Text: create.Text,
|
||||
Images: images,
|
||||
Attachments: attachments,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Dismiss(ctx context.Context, user, itemID string) (api.InboxItem, error) {
|
||||
item, err := m.store.GetInboxItem(ctx, itemID)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user