okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffix(stream): restore unfinished responses on session attachmenta229862

main
13.1 KiB315 linesraw
1package daemon
2
3import (
4	"context"
5	"errors"
6	"fmt"
7	"io"
8	"net/http"
9	"slices"
10	"strings"
11
12	"albedo/cli/internal/daemon/protocol"
13)
14
15type DeletionResult struct {
16	OK         bool
17	Deleted    int
18	Remaining  int
19	State      string
20	DeletedIDs []string
21	Truncated  bool
22	Message    string
23}
24type PreviewItem struct {
25	Type    string
26	Preview string
27}
28type SessionPreview struct {
29	Items   []PreviewItem
30	Session *Session
31}
32type CreateSessionRequest struct{ Kind, Workspace, Provider, Model, Effort, Name, SourceSessionID, CheckpointID, ParentID, Address, Task string }
33type ForkRequest struct{ Checkpoint string }
34type Session struct {
35	ID               string    `json:"id"`
36	Title            string    `json:"name"`
37	LastAssistantAt  *int64    `json:"activity_at"` // Unix seconds, when observed.
38	TranscriptCount  int       `json:"-"`
39	Workspace        string    `json:"workspace"`
40	Location         *Location `json:"-"`
41	Cursor           *Cursor   `json:"-"`
42	Model            string    `json:"model"`
43	Effort           string    `json:"effort"`
44	Protocol         string    `json:"-"`
45	Provider         string    `json:"provider_profile"`
46	ETag             string    `json:"-"`
47	FamilyRevision   string    `json:"family_revision"`
48	ParentID         *string   `json:"parent_id"`
49	RootID           string    `json:"root_id"`
50	Depth            int       `json:"depth"`
51	Closed           bool      `json:"closed"`
52	Pinned, Archived bool
53	Opens            int
54	Status           AgentStatus
55	Glances          []PageGlance `json:"-"`
56	wire             protocol.Session
57}
58type Location struct {
59	Host  *string `json:"host"`
60	User  *string `json:"user"`
61	Path  string  `json:"path"`
62	Label *string `json:"label"`
63}
64
65func SplitLocation(workspace string) (host, path string) {
66	if workspace == "" || workspace[0] == '/' || workspace[0] == '~' {
67		return "", workspace
68	}
69	i := strings.Index(workspace, ":/")
70	if i <= 0 || strings.Contains(workspace[:i], "/") {
71		return "", workspace
72	}
73	return workspace[:i], workspace[i+1:]
74}
75
76func sessionValue(wire protocol.Session) Session {
77	location := &Location{Host: wire.Location.Host, User: wire.Location.User, Path: wire.Location.Path, Label: wire.Location.Label}
78	session := Session{ID: wire.ID, Title: wire.Name, Cursor: wire.Cursor, LastAssistantAt: timestampSeconds(value(wire.ActivityAt)), TranscriptCount: int(wire.Preview.TranscriptCount), Workspace: wire.Workspace, Location: location, Model: value(wire.Model), Effort: value(wire.Effort), Provider: value(wire.ProviderProfile), ETag: wire.ConfigurationResource.ETag, FamilyRevision: wire.FamilyRevision, ParentID: wire.ParentID, RootID: wire.RootID, Depth: int(wire.Depth), Closed: wire.Closed, Pinned: wire.Preferences.Pinned, Archived: wire.Preferences.Archived, Opens: int(wire.Preferences.Opens), Status: capturedStatus(wire), wire: wire}
79	for _, glance := range wire.Glances {
80		session.Glances = append(session.Glances, *glanceValue(glance))
81	}
82	slices.SortFunc(session.Glances, func(first, second PageGlance) int { return strings.Compare(first.Title, second.Title) })
83	return session
84}
85func decodeSession(data []byte) (Session, error) {
86	var wire protocol.Session
87	if err := decodeRequired(data, &wire); err != nil {
88		return Session{}, err
89	}
90	if err := validateSession(wire); err != nil {
91		return Session{}, err
92	}
93	return sessionValue(wire), nil
94}
95func validateSession(wire protocol.Session) error {
96	if wire.ID == "" || wire.RootID == "" || wire.Cursor == nil || !validGeneration(wire.Cursor.Generation) || wire.Cursor.Sequence < 0 || wire.ConfigurationResource.ETag == "" || wire.History.Items == nil || wire.ActiveOutput == nil {
97		return fieldError("session")
98	}
99	if err := validateActiveOutput(wire.ID, wire.ActiveOutput); err != nil {
100		return err
101	}
102	return validateSessionStatus(wire.Status)
103}
104func CreateSession(ctx context.Context, conn *Connection, request CreateSessionRequest) (Session, error) {
105	handle, err := NewCreation(request)
106	if err != nil {
107		return Session{}, err
108	}
109	return CreateSessionOperation(ctx, conn, handle)
110}
111func CreateSessionOperation(ctx context.Context, conn *Connection, handle *OperationHandle) (Session, error) {
112	return executeReceipt(ctx, conn, handle, []int{201}, func(body []byte, _ int) (Session, error) {
113		result, err := decodeSession(body)
114		if err == nil && result.ID != handle.ID() {
115			return result, fieldError("session ID")
116		}
117		return result, err
118	}, func(ctx context.Context) (Session, bool, error) {
119		result, err := ResolveCreation(ctx, conn, handle)
120		return result, err == nil, err
121	})
122}
123func GetSession(ctx context.Context, conn *Connection, id string) (Session, error) {
124	var result Session
125	err := executeRead(ctx, conn, operation{Name: "read session", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
126		return protocol.NewGetSessionRequest(base, id, &protocol.GetSessionParams{Tail: new(int64(0))})
127	}, Policy: readRecovery}, func(data []byte) error {
128		var err error
129		result, err = decodeSession(data)
130		if err == nil && result.ID != id {
131			return fieldError("session ID")
132		}
133		return err
134	})
135	return result, err
136}
137
138type SessionCondition struct{ ETag, FamilyRevision string }
139type SessionConfiguration struct {
140	Value protocol.SessionConfiguration
141	ETag  string
142}
143
144func GetSessionConfiguration(ctx context.Context, conn *Connection, id string) (SessionConfiguration, error) {
145	var result SessionConfiguration
146	err := executeRead(ctx, conn, operation{Name: "read session configuration", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
147		return protocol.NewGetSessionRequest(base, id, &protocol.GetSessionParams{View: new("configuration")})
148	}, Validator: &result.ETag, Policy: readRecovery}, func(data []byte) error {
149		if err := decodeRequired(data, &result.Value); err != nil {
150			return err
151		}
152		if result.ETag == "" || result.Value.ID != id {
153			return fieldError("configuration validator")
154		}
155		return nil
156	})
157	return result, err
158}
159func patchSession(ctx context.Context, conn *Connection, id, etag string, body any) (Session, error) {
160	headers, err := observedHeaders(etag)
161	if err != nil {
162		return Session{}, err
163	}
164	var result Session
165	err = executeMutation(ctx, conn, operation{Name: "edit session configuration", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
166		return protocol.NewPatchSessionRequestWithBody(base, id, &protocol.PatchSessionParams{View: "configuration", IfMatch: headers.Get("If-Match")}, "application/merge-patch+json", body)
167	}, Headers: headers, Body: body, Policy: authRecovery}, []int{200}, func(data []byte, _ int) error {
168		var change protocol.SessionChange
169		if err := decodeRequired(data, &change); err != nil {
170			return err
171		}
172		if change.Resource.ETag == "" || change.Session.ID != id || change.Resource.Value.ID != id || change.Resource.ETag != change.Session.ConfigurationResource.ETag {
173			return fieldError("session change")
174		}
175		if err := validateSession(change.Session); err != nil {
176			return err
177		}
178		result = sessionValue(change.Session)
179		return nil
180	})
181	return result, err
182}
183func RenameSession(ctx context.Context, conn *Connection, id, name, etag string) (Session, error) {
184	return patchSession(ctx, conn, id, etag, protocol.SessionPatch{Name: &name})
185}
186func ForkSession(ctx context.Context, conn *Connection, id string, body ForkRequest) (Session, error) {
187	return CreateSession(ctx, conn, CreateSessionRequest{Kind: "fork", SourceSessionID: id, CheckpointID: body.Checkpoint})
188}
189func DeleteSession(ctx context.Context, conn *Connection, id string, tree bool, condition SessionCondition) (DeletionResult, error) {
190	headers, err := observedHeaders(condition.ETag)
191	if err != nil {
192		return DeletionResult{}, err
193	}
194	params := protocol.DeleteSessionParams{View: "configuration", Scope: new("leaf"), IfMatch: condition.ETag}
195	if tree {
196		if condition.FamilyRevision == "" {
197			return DeletionResult{}, errors.New("subtree deletion requires the observed family revision")
198		}
199		params.Scope = new("subtree")
200		params.FamilyRevision = &condition.FamilyRevision
201	}
202	var result DeletionResult
203	err = executeMutation(ctx, conn, operation{Name: "delete session", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
204		return protocol.NewDeleteSessionRequest(base, id, &params)
205	}, Headers: headers, Policy: authRecovery}, []int{200}, func(data []byte, _ int) error {
206		var wire protocol.SessionDeletion
207		if err := decodeRequired(data, &wire); err != nil {
208			return err
209		}
210		if wire.DeletedCount < 0 || wire.RemainingCount < 0 || wire.State != "complete" && wire.State != "partial" || wire.State == "complete" && wire.RemainingCount != 0 {
211			return fieldError("deletion counts")
212		}
213		result = DeletionResult{OK: wire.State == "complete", Deleted: int(wire.DeletedCount), Remaining: int(wire.RemainingCount), State: wire.State, DeletedIDs: wire.DeletedIDs, Truncated: wire.Truncated, Message: fmt.Sprintf("Deleted %d sessions; %d remain.", wire.DeletedCount, wire.RemainingCount)}
214		for _, remaining := range wire.Remaining {
215			result.Message += " " + remaining.ID + ": " + remaining.Reason.Detail
216		}
217		if wire.Truncated {
218			result.Message += " Individual results are truncated; refresh the session list."
219		}
220		return nil
221	})
222	return result, err
223}
224func ListSessions(ctx context.Context, conn *Connection) ([]Session, error) {
225	return listSessions(ctx, conn, protocol.ListSessionsParams{Scope: new("roots")})
226}
227func listSessions(ctx context.Context, conn *Connection, query protocol.ListSessionsParams) ([]Session, error) {
228	result := []Session{}
229	query.Limit = new(int64(200))
230	err := walkPages(func(next *string) (protocol.SessionPage, *string, error) {
231		query.Next = next
232		var page protocol.SessionPage
233		pageErr := executeRead(ctx, conn, operation{Name: "list sessions", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
234			return protocol.NewListSessionsRequest(base, &query)
235		}, Policy: readRecovery}, func(data []byte) error { return decodeRequired(data, &page) })
236		return page, page.Next, pageErr
237	}, func(page protocol.SessionPage) error {
238		if page.Items == nil {
239			return fieldError("items")
240		}
241		for _, row := range page.Items {
242			if row.ID == "" || row.RootID == "" || row.Cursor != nil && (!validGeneration(row.Cursor.Generation) || row.Cursor.Sequence < 0) {
243				return fieldError("session ID")
244			}
245			// Summaries intentionally contain no configuration validator.
246			result = append(result, summaryValue(row))
247		}
248		return nil
249	}, "repeated session page")
250	if err != nil {
251		return nil, err
252	}
253	return result, nil
254}
255
256func GetSessionPreview(ctx context.Context, conn *Connection, id string, limit int) (SessionPreview, error) {
257	var wire protocol.Session
258	err := executeRead(ctx, conn, operation{Name: "read session preview", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
259		return protocol.NewGetSessionRequest(base, id, &protocol.GetSessionParams{Tail: new(int64(min(200, max(0, limit))))})
260	}, Policy: readRecovery}, func(data []byte) error { session, err := decodeSession(data); wire = session.wire; return err })
261	if err != nil {
262		return SessionPreview{}, err
263	}
264	session := sessionValue(wire)
265	items, err := previewItems(ctx, conn, id, wire.History.Items)
266	return SessionPreview{Items: items, Session: &session}, err
267}
268
269// PreviewHistory reads a session's newest entries from its stored history.
270// Unlike GetSessionPreview it never loads the session in the daemon.
271func PreviewHistory(ctx context.Context, conn *Connection, id string, limit int) (SessionPreview, error) {
272	wire, err := readHistory(ctx, conn, id, protocol.GetHistoryParams{Limit: new(int64(min(200, max(1, limit))))})
273	if err != nil {
274		return SessionPreview{}, err
275	}
276	items, err := previewItems(ctx, conn, id, wire.Items)
277	return SessionPreview{Items: items}, err
278}
279
280func previewItems(ctx context.Context, conn *Connection, id string, entries []protocol.HistoryEntry) ([]PreviewItem, error) {
281	// Long tool results arrive as references, read in full like the chat's.
282	entries, err := hydrateHistory(ctx, conn, id, entries)
283	if err != nil {
284		return nil, err
285	}
286	events, err := historyEvents(entries)
287	if err != nil {
288		return nil, err
289	}
290	items := []PreviewItem{}
291	for _, event := range events {
292		if event.Type == EventCommitted {
293			continue
294		}
295		text := event.Text
296		if event.Type == EventTool {
297			text = event.ToolName + ": " + event.ToolResult
298		}
299		items = append(items, PreviewItem{Type: string(event.Type), Preview: text})
300	}
301	return items, nil
302}
303
304func summaryValue(row protocol.SessionSummary) Session {
305	return sessionValue(protocol.Session{ID: row.ID, Name: row.Name, AutomaticName: row.AutomaticName, Workspace: row.Workspace, Location: row.Location, ParentID: row.ParentID, RootID: row.RootID, Address: row.Address, Depth: row.Depth, Closed: row.Closed, CreatedAt: row.CreatedAt, ActivityAt: row.ActivityAt, ProviderProfile: row.ProviderProfile, Model: row.Model, Effort: row.Effort, Status: row.Status, Preview: row.Preview, Preferences: row.Preferences, CurrentProgress: row.CurrentProgress, Activity: row.Activity, Cursor: row.Cursor})
306}
307func ListAllSessions(ctx context.Context, conn *Connection) ([]Session, error) {
308	return listSessions(ctx, conn, protocol.ListSessionsParams{Scope: new("all")})
309}
310
311func capturedStatus(w protocol.Session) AgentStatus {
312	status := statusValue(w.Status, w.Kernel)
313	status.InputOrder = w.InputOrder
314	return status
315}