okami.mom/albedo

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

nekomimi.petapi: zstd responses, streams and request bodies; images served as raw bytes; drop redundant re-encodes7fd8cf5

main
9.9 KiB269 linesraw
1package daemon
2
3import (
4	"bufio"
5	"context"
6	"encoding/json"
7	"errors"
8	"io"
9	"net/http"
10	"unicode/utf8"
11
12	"albedo/cli/internal/daemon/protocol"
13)
14
15type Activity = protocol.Activity
16type Cursor = protocol.Cursor
17
18func validGeneration(generation string) bool {
19	if len(generation) != 22 {
20		return false
21	}
22	for i := range len(generation) {
23		c := generation[i]
24		if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-') {
25			return false
26		}
27	}
28	return true
29}
30
31type AgentEvent struct {
32	MailID                                                                                                     string
33	Progress                                                                                                   *ToolProgress
34	Type, Session, Parent, Name, Model, From, To, FromName, Kind, Text, CallID, ProgressCallID, Output, Source string
35	Depth, Bytes                                                                                               int
36	Running                                                                                                    bool
37	Cursor                                                                                                     *Cursor
38	Status                                                                                                     *AgentStatus
39	CurrentProgress                                                                                            []ToolProgress
40	Activity                                                                                                   *Activity
41	SessionIDs                                                                                                 []string
42	ScopeDirty                                                                                                 bool
43}
44type AgentNode struct {
45	Parent, Address *string
46	Session         Session
47	Name            string
48	Depth           int
49	Running, Closed bool
50	Cursor          *Cursor
51	Activity        Activity
52	CurrentProgress []ToolProgress
53}
54type AgentsSnapshot struct {
55	Root, FamilyRevision string
56	Nodes                []AgentNode
57}
58
59func StreamAgents(ctx context.Context, conn *Connection, onBatch func([]AgentEvent) error) error {
60	first := true
61	err := scanEventStream(ctx, conn, operation{Name: "watch sessions", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
62		return protocol.NewListSessionsRequest(base, nil)
63	}, Policy: readRecovery}, streamLimits{requireSSE: true, lineBytes: 1048577}, func(scanner *bufio.Scanner) error {
64		return scanSSEFrames(ctx, scanner, func(payload []byte) error {
65			var w struct {
66				Events []json.RawMessage `json:"events"`
67			}
68			if err := decodeRequired(payload, &w, "events"); err != nil {
69				return streamFailure(StreamProtocol, err)
70			}
71			if w.Events == nil || len(w.Events) > 256 {
72				return streamFailure(StreamProtocol, fieldError("collection batch"))
73			}
74			events := []AgentEvent{}
75			types := []string{}
76			for _, raw := range w.Events {
77				event, err := decodeAgentEvent(raw)
78				if err != nil {
79					return streamFailure(StreamProtocol, err)
80				}
81				var kind struct {
82					Type string `json:"type"`
83				}
84				_ = json.Unmarshal(raw, &kind)
85				types = append(types, kind.Type)
86				if event != nil {
87					events = append(events, *event)
88				}
89			}
90			if first {
91				if len(types) != 2 || types[0] != "ready" || types[1] != "reset" {
92					return streamFailure(StreamProtocol, errors.New("collection stream requires ready and reset before snapshots"))
93				}
94				first = false
95			} else {
96				for _, kind := range types {
97					if kind == "ready" || kind == "reset" {
98						return streamFailure(StreamProtocol, fieldError("collection readiness"))
99					}
100					if kind == "overflow" || kind == "failure" {
101						if len(types) != 1 {
102							return streamFailure(StreamProtocol, fieldError("terminal collection batch"))
103						}
104					}
105				}
106			}
107			if err := onBatch(events); err != nil {
108				return streamFailure(StreamTerminal, err)
109			}
110			if len(types) == 1 && types[0] == "overflow" {
111				return streamFailure(StreamTransient, errors.New("collection stream overflow; capture a fresh snapshot"))
112			}
113			return nil
114		})
115	})
116	return classifyStreamFailure(err)
117}
118func decodeAgentEvent(raw json.RawMessage) (*AgentEvent, error) {
119	var envelope struct {
120		Type string          `json:"type"`
121		Data json.RawMessage `json:"data"`
122	}
123	if err := decodeRequired(raw, &envelope, "type", "data"); err != nil {
124		return nil, err
125	}
126	if envelope.Type == "" || envelope.Data == nil || string(envelope.Data) == "null" {
127		return nil, fieldError("collection event")
128	}
129	event := &AgentEvent{Type: envelope.Type}
130	switch envelope.Type {
131	case "ready", "overflow":
132	case "reset":
133		var d struct {
134			Reason string `json:"reason"`
135		}
136		if err := decodeRequired(envelope.Data, &d, "reason"); err != nil {
137			return nil, err
138		}
139		if d.Reason != "initial" {
140			return nil, fieldError("collection reset")
141		}
142	case "activity":
143		var d struct {
144			SessionID string                  `json:"session_id"`
145			Cursor    Cursor                  `json:"cursor"`
146			Status    protocol.SessionStatus  `json:"status"`
147			Progress  []protocol.ToolProgress `json:"current_progress"`
148			Activity  Activity                `json:"activity"`
149		}
150		if err := decodeRequired(envelope.Data, &d, "session_id", "cursor", "status", "current_progress", "activity"); err != nil {
151			return nil, err
152		}
153		if d.SessionID == "" || !validGeneration(d.Cursor.Generation) || d.Cursor.Sequence < 0 || len(d.Progress) > maxActiveToolProgress {
154			return nil, fieldError("collection activity")
155		}
156		if err := validateSessionStatus(d.Status); err != nil {
157			return nil, err
158		}
159		if err := validateActivity(d.Activity); err != nil {
160			return nil, err
161		}
162		event.Session = d.SessionID
163		event.Cursor = &d.Cursor
164		status := statusValue(d.Status, protocol.Kernel{})
165		event.Status = &status
166		event.Running = status.Running
167		event.Activity = &d.Activity
168		event.CurrentProgress = []ToolProgress{}
169		seen := map[string]bool{}
170		for _, p := range d.Progress {
171			if seen[p.CallID] {
172				return nil, fieldError("duplicate collection progress")
173			}
174			seen[p.CallID] = true
175			progress, err := validateToolProgress(p)
176			if err != nil || progress == nil {
177				return nil, fieldError("collection progress")
178			}
179			event.CurrentProgress = append(event.CurrentProgress, *progress)
180		}
181	case "mail":
182		var d protocol.MailMetadata
183		if err := decodeRequired(envelope.Data, &d); err != nil {
184			return nil, err
185		}
186		event.MailID = d.MailID
187		event.From, event.To, event.FromName, event.Kind, event.Bytes = value(d.SenderSessionID), d.ReceiverSessionID, value(d.SenderLabel), d.Kind, int(d.Bytes)
188	case "invalidate":
189		var d struct {
190			SessionIDs []string `json:"session_ids"`
191			ScopeDirty bool     `json:"scope_dirty"`
192		}
193		if err := decodeRequired(envelope.Data, &d, "urls", "session_ids", "scope_dirty"); err != nil {
194			return nil, err
195		}
196		event.SessionIDs, event.ScopeDirty = d.SessionIDs, d.ScopeDirty
197	case "failure":
198		var d protocol.SafeReason
199		if err := decodeRequired(envelope.Data, &d); err != nil {
200			return nil, err
201		}
202		return nil, streamFailure(StreamTerminal, &APIError{Code: d.Code, Message: d.Detail})
203	default:
204		return nil, nil
205	}
206	return event, nil
207}
208func GetAgents(ctx context.Context, conn *Connection, id string) (AgentsSnapshot, error) {
209	selected, err := GetSession(ctx, conn, id)
210	if err != nil {
211		return AgentsSnapshot{}, err
212	}
213	params := protocol.ListSessionsParams{Scope: new("all"), FamilyID: &selected.RootID, Limit: new(int64(200))}
214	result := AgentsSnapshot{Root: selected.RootID, Nodes: []AgentNode{}}
215	err = walkPages(func(next *string) (protocol.SessionPage, *string, error) {
216		params.Next = next
217		var page protocol.SessionPage
218		pageErr := executeRead(ctx, conn, operation{Name: "read session family", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
219			return protocol.NewListSessionsRequest(base, &params)
220		}, Policy: readRecovery}, func(data []byte) error { return decodeRequired(data, &page, "family") })
221		return page, page.Next, pageErr
222	}, func(page protocol.SessionPage) error {
223		if result.FamilyRevision != "" && page.Family.Revision != result.FamilyRevision {
224			return &APIError{StatusCode: 409, Code: "family_changed", Message: "family changed while paging; refresh it"}
225		}
226		result.FamilyRevision = page.Family.Revision
227		for _, row := range page.Items {
228			session := summaryValue(row)
229			session.FamilyRevision = result.FamilyRevision
230			node := AgentNode{Parent: row.ParentID, Address: row.Address, Session: session, Name: session.Title, Depth: session.Depth, Running: session.Status.Running, Closed: session.Closed, Cursor: row.Cursor, Activity: row.Activity, CurrentProgress: []ToolProgress{}}
231			for _, p := range row.CurrentProgress {
232				progress, progressErr := validateToolProgress(p)
233				if progressErr != nil || progress == nil {
234					return fieldError("family progress")
235				}
236				node.CurrentProgress = append(node.CurrentProgress, *progress)
237			}
238			result.Nodes = append(result.Nodes, node)
239		}
240		return nil
241	}, "family cursor")
242	if err != nil {
243		return result, err
244	}
245	return result, nil
246}
247
248func validateActivity(activity Activity) error {
249	if activity.CurrentRequest != nil && (activity.CurrentRequest.InputID == "" || len([]rune(activity.CurrentRequest.Text)) > 512) {
250		return fieldError("activity request")
251	}
252	if activity.LatestProgress != nil && len([]rune(*activity.LatestProgress)) > 512 {
253		return fieldError("activity progress")
254	}
255	if len(activity.Lines) > 12 || activity.Lines == nil || activity.OutputScalars < 0 || activity.OutputUTF8Bytes < 0 || timestampMilliseconds(activity.ObservedAt) == nil {
256		return fieldError("activity")
257	}
258	for _, line := range activity.Lines {
259		if len(line.Text) > 1024 || utf8.RuneCountInString(line.Text) > 256 {
260			return fieldError("activity line")
261		}
262		switch line.Kind {
263		case "assistant", "thinking", "tool", "input", "note", "error":
264		default:
265			return fieldError("activity kind")
266		}
267	}
268	return nil
269}