okami.mom/albedo

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

nekomimi.petfeat: multiple images, collapsed pastes, and an attachment strip in the prompt6e82811

main
10.3 KiB301 linesraw
1package daemon
2
3import (
4	"context"
5	"crypto/rand"
6	"encoding/json"
7	"errors"
8	"fmt"
9	"io"
10	"net/http"
11	"reflect"
12	"time"
13
14	"albedo/cli/internal/daemon/protocol"
15)
16
17// OperationHandle freezes the chosen resource ID and exact admission payload.
18// Retries always use this same input or session resource.
19type OperationHandle struct {
20	id        string
21	operation operation
22	kind      string
23	sessionID string
24}
25
26func (handle *OperationHandle) ID() string       { return handle.id }
27func (handle *OperationHandle) IsCreation() bool { return handle.kind == "creation" }
28
29func operationID() (string, error) {
30	var id [16]byte
31	if _, err := rand.Read(id[:]); err != nil {
32		return "", fmt.Errorf("create resource ID: %w", err)
33	}
34	stamp := uint64(time.Now().UnixMilli())
35	for i := 5; i >= 0; i-- {
36		id[i] = byte(stamp)
37		stamp >>= 8
38	}
39	id[6] = id[6]&0x0f | 0x70
40	id[8] = id[8]&0x3f | 0x80
41	return fmt.Sprintf("%x-%x-%x-%x-%x", id[:4], id[4:6], id[6:8], id[8:10], id[10:]), nil
42}
43
44func NewSubmission(session string, fields SubmissionRequest) (*OperationHandle, error) {
45	id := fields.SubmissionID
46	if id == "" {
47		var err error
48		id, err = operationID()
49		if err != nil {
50			return nil, err
51		}
52	}
53	kind := fields.Type
54	if kind == "" || kind == "user" {
55		kind = "message"
56	}
57	var body any
58	switch kind {
59	case "message":
60		if fields.Content == nil {
61			return nil, errors.New("message input requires text")
62		}
63		uploaded := make([]protocol.ImageUpload, len(fields.Images))
64		for i, image := range fields.Images {
65			uploaded[i] = protocol.ImageUpload{MimeType: string(image.MimeType), Data: image.Data}
66		}
67		body = protocol.MessageInputRequest{Kind: kind, Text: *fields.Content, Images: uploaded, Pastes: fields.Pastes, ClientID: optionalText(fields.ClientID)}
68	case "continue":
69		body = protocol.ContinueInputRequest{Kind: kind, ClientID: optionalText(fields.ClientID)}
70	case "skill":
71		body = protocol.SkillInputRequest{Kind: kind, CandidateID: fields.Name, CatalogRevision: fields.CatalogRevision, Arguments: fields.Arguments, ClientID: optionalText(fields.ClientID)}
72	case "command":
73		body = protocol.CommandInputRequest{Kind: kind, CommandID: fields.Name, Arguments: fields.CommandArguments, ClientID: optionalText(fields.ClientID)}
74	default:
75		return nil, fmt.Errorf("unknown input kind %q", kind)
76	}
77	payload, err := json.Marshal(body)
78	if err != nil {
79		return nil, err
80	}
81	handle := &OperationHandle{id: id, kind: kind, sessionID: session}
82	handle.operation = operation{Name: "admit input", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
83		return protocol.NewPutInputRequestWithBody(base, session, id, "application/json", body)
84	}, Body: json.RawMessage(payload), Policy: receiptRecovery, Handle: handle}
85	return handle, nil
86}
87
88func NewCreation(fields CreateSessionRequest) (*OperationHandle, error) {
89	kind := fields.Kind
90	if kind == "" {
91		kind = "new"
92	}
93	var body any
94	switch kind {
95	case "new":
96		body = protocol.NewSessionRequest{Kind: kind, Workspace: fields.Workspace, Name: optionalText(fields.Name), ProviderProfile: optionalText(fields.Provider), Model: optionalText(fields.Model), Effort: optionalText(fields.Effort)}
97	case "fork":
98		body = protocol.ForkSessionRequest{Kind: kind, SourceSessionID: fields.SourceSessionID, CheckpointID: fields.CheckpointID, Name: optionalText(fields.Name)}
99	case "child":
100		inputID, err := operationID()
101		if err != nil {
102			return nil, err
103		}
104		body = protocol.ChildSessionRequest{Kind: kind, ParentID: fields.ParentID, Address: fields.Address, Name: fields.Name, InitialInputID: inputID, Task: fields.Task, Model: optionalText(fields.Model), Effort: optionalText(fields.Effort)}
105	default:
106		return nil, fmt.Errorf("unknown creation kind %q", kind)
107	}
108	payload, err := json.Marshal(body)
109	if err != nil {
110		return nil, err
111	}
112	id, err := operationID()
113	if err != nil {
114		return nil, err
115	}
116	handle := &OperationHandle{id: id, kind: "creation", sessionID: id}
117	handle.operation = operation{Name: "create session", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
118		return protocol.NewCreateSessionRequestWithBody(base, id, &protocol.CreateSessionParams{IfNoneMatch: "*"}, "application/json", body)
119	}, Body: json.RawMessage(payload), Headers: http.Header{"If-None-Match": {"*"}}, Policy: receiptRecovery, Handle: handle}
120	return handle, nil
121}
122
123type InputReceipt protocol.Input
124
125func (receipt InputReceipt) Rejection() error {
126	if receipt.Admission != "rejected" {
127		return nil
128	}
129	err := &APIError{StatusCode: int(receipt.HTTPStatus)}
130	if receipt.Problem != nil {
131		err.Code, err.Message = receipt.Problem.Code, receipt.Problem.Detail
132		if err.Message == "" {
133			err.Message = receipt.Problem.Title
134		}
135	}
136	return err
137}
138
139func (receipt InputReceipt) Pending() bool { return value(receipt.Delivery) == "pending" }
140
141func (receipt InputReceipt) Settled() bool {
142	return value(receipt.Delivery) == "committed" || value(receipt.Delivery) == "cancelled"
143}
144
145func (receipt InputReceipt) InputOrder() int64 { return value(receipt.AcceptanceOrder) }
146
147func (receipt InputReceipt) BlockingDetail() string {
148	if receipt.BlockingReason != nil {
149		return receipt.BlockingReason.Detail
150	}
151	return ""
152}
153
154func GetInput(ctx context.Context, conn *Connection, session, id string) (InputReceipt, error) {
155	var input protocol.Input
156	err := executeRead(ctx, conn, operation{Name: "read input", BuildRequest: func(base string, body io.Reader) (*http.Request, error) {
157		return protocol.NewGetInputRequest(base, session, id)
158	}, Policy: readRecovery}, func(data []byte) error {
159		if err := decodeRequired(data, &input); err != nil {
160			return err
161		}
162		if input.ID != id || input.SessionID != session {
163			return fieldError("input identity")
164		}
165		return validInput(input)
166	})
167	if err != nil {
168		return InputReceipt{}, err
169	}
170	return InputReceipt(input), nil
171}
172func validInput(input protocol.Input) error {
173	if input.ID == "" || input.SessionID == "" {
174		return fieldError("input identity")
175	}
176	switch input.Kind {
177	case "message", "continue", "skill", "command":
178	default:
179		return fieldError("input kind")
180	}
181	if input.Admission == "accepted" {
182		if input.HTTPStatus != 202 || input.AcceptedAt == nil || input.AcceptanceOrder == nil || *input.AcceptanceOrder < 1 || input.Problem != nil {
183			return fieldError("input admission")
184		}
185		switch value(input.Delivery) {
186		case "pending", "committed", "cancelled":
187			return nil
188		}
189	} else if input.Admission == "rejected" && input.HTTPStatus >= 400 && input.HTTPStatus <= 599 && input.Problem != nil && input.Delivery == nil && input.AcceptedAt == nil && input.AcceptanceOrder == nil && input.Turn == nil {
190		return nil
191	}
192	return fieldError("input admission")
193}
194func ResolveOperation(ctx context.Context, conn *Connection, handle *OperationHandle) (InputReceipt, error) {
195	return GetInput(ctx, conn, handle.sessionID, handle.id)
196}
197
198// A found outcome is authoritative even when it rejects the input. A failed
199// lookup leaves admission uncertain and may permit replay of the frozen request.
200func executeReceipt[T any](ctx context.Context, conn *Connection, handle *OperationHandle, statuses []int, decode func([]byte, int) (T, error), resolve func(context.Context) (T, bool, error)) (T, error) {
201	var result T
202	if ctx == nil {
203		ctx = context.Background()
204	}
205	ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
206	defer cancel()
207	for attempt := range 2 {
208		err := executeMutation(ctx, conn, handle.operation, statuses, func(body []byte, status int) error {
209			var decodeErr error
210			result, decodeErr = decode(body, status)
211			return decodeErr
212		})
213		if err == nil {
214			return result, nil
215		}
216		var uncertain *UncertainOutcomeError
217		collision := false
218		if api, ok := errors.AsType[*APIError](err); ok {
219			collision = handle.kind == "creation" && api.StatusCode == 412
220		}
221		if !errors.As(err, &uncertain) && !collision {
222			return result, err
223		}
224		if ctx.Err() != nil {
225			return result, uncertainOperation(handle.operation, err)
226		}
227		resolved, found, lookupErr := resolve(ctx)
228		if found {
229			return resolved, lookupErr
230		}
231		if IsOperationExpired(lookupErr) {
232			return result, uncertainOperation(handle.operation, lookupErr)
233		}
234		if handle.kind == "creation" {
235			if api, ok := errors.AsType[*APIError](lookupErr); ok {
236				if api.Code == "creation_conflict" {
237					return result, lookupErr
238				}
239				if len(api.Decision) > 0 && api.StatusCode >= 400 && api.StatusCode < 500 && api.StatusCode != 404 {
240					return result, lookupErr
241				}
242			}
243		}
244		if attempt == 1 {
245			return result, uncertainOperation(handle.operation, err)
246		}
247	}
248	panic("unreachable admission retry budget")
249}
250func ResolveCreation(ctx context.Context, conn *Connection, handle *OperationHandle) (Session, error) {
251	session, err := GetSession(ctx, conn, handle.id)
252	if err != nil {
253		if api, ok := errors.AsType[*APIError](err); ok && len(api.Decision) > 0 {
254			var decision protocol.CreationDecision
255			if parseErr := decodeRequired(api.Decision, &decision); parseErr != nil {
256				return Session{}, &ProtocolError{Code: "invalid_response", Operation: "read creation decision", Cause: parseErr}
257			}
258			if decision.Kind != "creation" || decision.SessionID != handle.ID() {
259				return Session{}, fieldError("creation decision identity")
260			}
261			if decision.Creation != nil && !sameCreationIntent(handle.operation.Body.(json.RawMessage), decision.Creation.Submitted) {
262				return Session{}, &APIError{StatusCode: 409, Code: "creation_conflict", Message: "the session ID belongs to another creation intent"}
263			}
264		}
265		return Session{}, err
266	}
267	if session.wire.Creation == nil {
268		return Session{}, fieldError("creation")
269	}
270	if !sameCreationIntent(handle.operation.Body.(json.RawMessage), session.wire.Creation.Submitted) {
271		return Session{}, &APIError{StatusCode: 409, Code: "creation_conflict", Message: "the session ID belongs to another creation intent"}
272	}
273
274	return session, nil
275}
276func IsOperationExpired(err error) bool {
277	api, ok := errors.AsType[*APIError](err)
278	return ok && api.StatusCode == http.StatusGone
279}
280
281func sameCreationIntent(expected, stored json.RawMessage) bool {
282	var left, right map[string]any
283	if json.Unmarshal(expected, &left) != nil || json.Unmarshal(stored, &right) != nil {
284		return false
285	}
286	var optional []string
287	switch left["kind"] {
288	case "new":
289		optional = []string{"name", "provider_profile", "model", "effort"}
290	case "fork":
291		optional = []string{"name"}
292	case "child":
293		optional = []string{"model", "effort"}
294	}
295	for _, key := range optional {
296		if _, ok := left[key]; !ok {
297			left[key] = nil
298		}
299	}
300	return reflect.DeepEqual(left, right)
301}