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
7.6 KiB238 linesraw
1package daemon
2
3import (
4	"encoding/json"
5	"errors"
6	"unicode/utf8"
7
8	"albedo/cli/internal/daemon/protocol"
9)
10
11// ToolActivity records display-only execution evidence, not model-facing tool output.
12type ToolActivity struct {
13	Kind   string `json:"kind"` // "read" | "search" | "list" | "run" | "web"
14	Target string `json:"target"`
15	// Detail is the markdown a web search answered with; empty otherwise.
16	Detail string `json:"detail,omitempty"`
17}
18
19type FileChange struct {
20	Path    string `json:"path"`
21	Kind    string `json:"kind"` // "diff" | "unavailable"
22	Diff    string `json:"diff,omitempty"`
23	Reason  string `json:"reason,omitempty"`
24	Added   int    `json:"added,omitempty"`
25	Removed int    `json:"removed,omitempty"`
26}
27
28type ToolTrace struct {
29	Activities []ToolActivity `json:"activities"`
30	Changes    []FileChange   `json:"changes"`
31	Truncated  bool           `json:"truncated,omitempty"`
32}
33
34// ParseToolTrace validates the display evidence encoded in a history or tool event.
35func ParseToolTrace(data json.RawMessage) *ToolTrace {
36	if len(data) == 0 {
37		return nil
38	}
39
40	var trace ToolTrace
41	if err := json.Unmarshal(data, &trace); err != nil {
42		return nil
43	}
44
45	if len(trace.Activities) > 64 || len(trace.Changes) > 16 {
46		return nil
47	}
48
49	for _, act := range trace.Activities {
50		switch act.Kind {
51		case "read", "search", "list", "run", "web":
52		default:
53			return nil
54		}
55		// Python bounds these strings by Unicode characters, not UTF-8 bytes.
56		if utf8.RuneCountInString(act.Target) > 1000 || utf8.RuneCountInString(act.Detail) > 16000 {
57			return nil
58		}
59	}
60
61	for _, ch := range trace.Changes {
62		if utf8.RuneCountInString(ch.Path) > 1000 {
63			return nil
64		}
65		switch ch.Kind {
66		case "unavailable":
67			if utf8.RuneCountInString(ch.Reason) > 1000 {
68				return nil
69			}
70		case "diff":
71			if utf8.RuneCountInString(ch.Diff) > 16000 || ch.Added < 0 || ch.Removed < 0 {
72				return nil
73			}
74		default:
75			return nil
76		}
77	}
78
79	return &trace
80}
81
82// ToolCodePreview contains the latest bounded window of decoded source code.
83type ToolCodePreview struct {
84	Text   string `json:"text"`
85	Offset int    `json:"offset"`
86}
87
88type ToolProgress struct {
89	Code       *ToolCodePreview `json:"code,omitempty"`
90	ToolCallID string           `json:"tool_call_id,omitempty"`
91	CallID     string           `json:"call_id"`
92	Name       string           `json:"name"`
93	Phase      string           `json:"phase"` // "generating" | "running"
94}
95
96const maxActiveToolProgress = 32
97
98type EventType string
99
100const (
101	EventReset          EventType = "reset"
102	EventRetry          EventType = "retry"
103	EventText           EventType = "text"
104	EventError          EventType = "error"
105	EventInterrupted    EventType = "interrupted"
106	EventMessage        EventType = "message"
107	EventUser           EventType = "user"
108	EventTurnMembership EventType = "turn_membership"
109	EventTurnCompleted  EventType = "turn_completed"
110	EventThinking       EventType = "thinking"
111	EventNote           EventType = "note"
112	EventCompacted      EventType = "compacted"
113	EventToolProgress   EventType = "tool_progress"
114	EventTool           EventType = "tool"
115	EventUsage          EventType = "usage"
116	EventInvalidate     EventType = "invalidate"
117	// EventCommitted says transcript rows up to Seq now cover what was shown.
118	EventCommitted EventType = "committed"
119)
120
121type Usage struct {
122	Model              string   `json:"model,omitempty"`
123	RecordedAt         *int64   `json:"recordedAt,omitempty"`
124	PromptTokens       *int     `json:"promptTokens,omitempty"`
125	CachedPromptTokens *int     `json:"cachedPromptTokens,omitempty"`
126	CacheWriteTokens   *int     `json:"cacheWriteTokens,omitempty"`
127	CompletionTokens   *int     `json:"completionTokens,omitempty"`
128	TotalTokens        *int     `json:"totalTokens,omitempty"`
129	ElapsedMs          *float64 `json:"elapsedMs,omitempty"`
130	TokensPerSecond    *float64 `json:"tokensPerSecond,omitempty"`
131	// CacheFade is how CachedPromptTokens fades once the session goes quiet,
132	// in time order.
133	CacheFade []CacheStep `json:"cacheFade,omitempty"`
134}
135
136// CacheStep says that from At (unix ms) on, a request would read Cached
137// tokens from the provider's cache; nil when that is no longer known.
138type CacheStep struct {
139	Cached *int  `json:"cached,omitempty"`
140	At     int64 `json:"at"`
141}
142
143type StreamEvent struct {
144	MessageID       string
145	ReplacesAllLive bool
146	ReplacesLiveIDs []string
147	EntryID         string
148	Position        int64
149	TurnType        string
150	Speaker         string
151	MailKind        string
152	SenderSessionID string
153	Invalidation    *ResourceInvalidation
154	Status          *AgentStatus
155	Snapshot        *Session
156	Receipt         *InputReceipt
157	OperationID     string
158	Images          []ImageMetadata
159	Usage           *Usage
160	ToolTrace       *ToolTrace
161	Timestamp       *int64
162	ToolArgs        map[string]any
163	Progress        *ToolProgress
164	TurnID          string
165	Type            EventType
166	Text            string
167	Source          string
168	Summary         string
169	Strategy        string
170	ToolName        string
171	ToolResult      string
172	ProgressCallID  string
173	SubmissionIDs   []string
174	Evicted         int
175	// ElapsedMs is how long a replayed thought took, when the daemon timed it.
176	ElapsedMs       int64
177	ElapsedObserved bool
178	// Seq is the newest transcript row an EventCommitted covers.
179	Seq int64
180	// Before and More describe the history a reset or page carried: Before is
181	// its first row (the cursor for the next older page), More whether one exists.
182	Before int64
183	More   bool
184	// Replayed marks an event from the transcript snapshot that follows a
185	// reset: history, which says nothing about what the session does now.
186	Replayed bool
187}
188
189type ResourceInvalidation struct {
190	Session, Settings, Catalog, Context, Extension bool
191}
192
193// decodeToolProgress validates the normalized progress object before delivery.
194// A null value clears the current live progress.
195func decodeToolProgress(raw json.RawMessage) (*ToolProgress, error) {
196	if string(raw) == "null" {
197		return nil, nil
198	}
199	if len(raw) > 8192 {
200		return nil, errors.New("tool progress exceeds 8 KiB")
201	}
202	var wire protocol.ToolProgress
203	if err := decodeRequired(raw, &wire); err != nil {
204		return nil, err
205	}
206	return validateToolProgress(wire)
207}
208
209func validateToolProgress(wire protocol.ToolProgress) (*ToolProgress, error) {
210	if wire.CallID == "" || wire.Name == "" || len(wire.Name) > 100 || (wire.Phase != "generating" && wire.Phase != "running") {
211		return nil, fieldError("tool progress")
212	}
213	preview := value(wire.Preview)
214	if preview.OffsetScalars < 0 || utf8.RuneCountInString(preview.Text) > 512 || len(preview.Text) > 2048 {
215		return nil, fieldError("tool progress preview")
216	}
217	return progressValue(wire), nil
218}
219func progressValue(wire protocol.ToolProgress) *ToolProgress {
220	progress := &ToolProgress{CallID: wire.CallID, ToolCallID: value(wire.ToolCallID), Name: wire.Name, Phase: wire.Phase}
221	preview := value(wire.Preview)
222	if preview.Text != "" || preview.OffsetScalars > 0 {
223		progress.Code = &ToolCodePreview{Text: preview.Text, Offset: int(preview.OffsetScalars)}
224	}
225	return progress
226}
227func usageValue(wire protocol.Usage) *Usage {
228	usage := &Usage{Model: value(wire.Model), PromptTokens: intPointer(wire.PromptTokens), CachedPromptTokens: intPointer(wire.CachedPromptTokens), CacheWriteTokens: intPointer(wire.CacheWriteTokens), CompletionTokens: intPointer(wire.CompletionTokens), TotalTokens: intPointer(wire.TotalTokens), ElapsedMs: wire.ElapsedMs, TokensPerSecond: wire.TokensPerSecond}
229	if wire.ObservedAt != nil {
230		usage.RecordedAt = timestampMilliseconds(*wire.ObservedAt)
231	}
232	for _, step := range wire.CacheFade {
233		if at := timestampMilliseconds(step.At); at != nil {
234			usage.CacheFade = append(usage.CacheFade, CacheStep{At: *at, Cached: intPointer(step.CachedTokens)})
235		}
236	}
237	return usage
238}