1package sdk
2
3import (
4 "encoding/json"
5
6 "github.com/patriceckhart/zot/packages/core"
7 "github.com/patriceckhart/zot/packages/provider"
8)
9
10// Event is the unit emitted by Runtime.Prompt's channel. Type
11// identifies the event kind; remaining fields are populated based on
12// kind. The wire schema is the same as the JSON-RPC stream notifications
13// emitted by `zot rpc`, so consumers can share parsing code.
14type Event struct {
15 Type string `json:"type"`
16
17 // Common fields.
18 Step int `json:"step,omitempty"`
19 Time string `json:"time,omitempty"`
20
21 // text_delta
22 Delta string `json:"delta,omitempty"`
23
24 // tool_call
25 ID string `json:"id,omitempty"`
26 Name string `json:"name,omitempty"`
27 Args json.RawMessage `json:"args,omitempty"`
28
29 // tool_progress
30 Text string `json:"text,omitempty"`
31
32 // tool_result
33 IsError bool `json:"is_error,omitempty"`
34 Result []ContentBlock `json:"content,omitempty"`
35
36 // usage
37 Usage *Usage `json:"usage,omitempty"`
38 Cumulative *Usage `json:"cumulative,omitempty"`
39
40 // user_message / assistant_message
41 Message *Message `json:"message,omitempty"`
42
43 // turn_end
44 Stop string `json:"stop,omitempty"`
45
46 // error
47 Error string `json:"error,omitempty"`
48}
49
50// Message is one transcript entry — a user prompt, assistant reply,
51// or tool result.
52type Message struct {
53 Role string `json:"role"`
54 Content []ContentBlock `json:"content"`
55 Time string `json:"time,omitempty"`
56}
57
58// ContentBlock is one piece of message content. Discriminate on Type:
59// - "text" → Text
60// - "image" → MimeType + Data (or Bytes for size-only)
61// - "tool_call" → ID + Name + Args
62// - "tool_result" → CallID + IsError + Content (recursive)
63type ContentBlock struct {
64 Type string `json:"type"`
65
66 Text string `json:"text,omitempty"`
67 MimeType string `json:"mime_type,omitempty"`
68 Data []byte `json:"data,omitempty"`
69 Bytes int `json:"bytes,omitempty"`
70 ID string `json:"id,omitempty"`
71 Name string `json:"name,omitempty"`
72 Args json.RawMessage `json:"args,omitempty"`
73 CallID string `json:"call_id,omitempty"`
74 IsError bool `json:"is_error,omitempty"`
75 Content []ContentBlock `json:"content,omitempty"`
76}
77
78// Image is a single user-attached image for Prompt.
79type Image struct {
80 MimeType string
81 Data []byte
82}
83
84// Usage is per-turn or cumulative token / cost counts.
85type Usage struct {
86 Input int `json:"input"`
87 Output int `json:"output"`
88 Reasoning *int `json:"reasoning"`
89 CacheRead int `json:"cache_read"`
90 CacheWrite int `json:"cache_write"`
91 CostUSD float64 `json:"cost_usd"`
92}
93
94// State is a snapshot of the runtime's current state.
95type State struct {
96 Provider string `json:"provider"`
97 Model string `json:"model"`
98 Reasoning string `json:"reasoning"`
99 CWD string `json:"cwd"`
100 Busy bool `json:"busy"`
101 MessageCount int `json:"message_count"`
102}
103
104// CompactResult describes the outcome of Compact.
105type CompactResult struct {
106 Summary string `json:"summary"`
107 Messages []Message `json:"messages"`
108}
109
110// ModelInfo describes one model known to the runtime.
111type ModelInfo struct {
112 ID string `json:"id"`
113 Provider string `json:"provider"`
114 ContextWindow int `json:"context_window"`
115 MaxOutput int `json:"max_output"`
116 Reasoning bool `json:"reasoning"`
117}
118
119// ---- internal converters ----
120
121func sdkReasoningTokens(usage provider.Usage) *int {
122 if !usage.ReasoningTokensKnown {
123 return nil
124 }
125 count := usage.ReasoningTokens
126 return &count
127}
128
129func toEvent(ev core.AgentEvent) Event {
130 out := Event{Type: ev.Type()}
131 switch e := ev.(type) {
132 case core.EvTurnStart:
133 out.Step = e.Step
134 case core.EvUserMessage:
135 out.Message = &Message{Role: string(e.Message.Role), Content: convertContent(e.Message.Content)}
136 case core.EvAssistantMessage:
137 out.Message = &Message{Role: string(e.Message.Role), Content: convertContent(e.Message.Content)}
138 case core.EvTextDelta:
139 out.Delta = e.Delta
140 case core.EvToolCall:
141 out.ID = e.ID
142 out.Name = e.Name
143 out.Args = e.Args
144 case core.EvToolProgress:
145 out.ID = e.ID
146 out.Text = e.Text
147 case core.EvToolResult:
148 out.ID = e.ID
149 out.IsError = e.Result.IsError
150 out.Result = convertContent(e.Result.Content)
151 case core.EvUsage:
152 out.Usage = &Usage{
153 Input: e.Usage.InputTokens,
154 Output: e.Usage.OutputTokens,
155 Reasoning: sdkReasoningTokens(e.Usage),
156 CacheRead: e.Usage.CacheReadTokens,
157 CacheWrite: e.Usage.CacheWriteTokens,
158 CostUSD: e.Usage.CostUSD,
159 }
160 out.Cumulative = &Usage{
161 Input: e.Cumulative.InputTokens,
162 Output: e.Cumulative.OutputTokens,
163 Reasoning: sdkReasoningTokens(e.Cumulative),
164 CacheRead: e.Cumulative.CacheReadTokens,
165 CacheWrite: e.Cumulative.CacheWriteTokens,
166 CostUSD: e.Cumulative.CostUSD,
167 }
168 case core.EvTurnEnd:
169 out.Stop = string(e.Stop)
170 if e.Err != nil {
171 out.Error = e.Err.Error()
172 }
173 case core.EvError:
174 if e.Err != nil {
175 out.Error = e.Err.Error()
176 }
177 }
178 return out
179}
180
181func convertContent(blocks []provider.Content) []ContentBlock {
182 out := make([]ContentBlock, 0, len(blocks))
183 for _, b := range blocks {
184 switch v := b.(type) {
185 case provider.TextBlock:
186 out = append(out, ContentBlock{Type: "text", Text: v.Text})
187 case provider.ImageBlock:
188 out = append(out, ContentBlock{Type: "image", MimeType: v.MimeType, Bytes: len(v.Data)})
189 case provider.ToolCallBlock:
190 out = append(out, ContentBlock{Type: "tool_call", ID: v.ID, Name: v.Name, Args: v.Arguments})
191 case provider.ToolResultBlock:
192 out = append(out, ContentBlock{
193 Type: "tool_result",
194 CallID: v.CallID,
195 IsError: v.IsError,
196 Content: convertContent(v.Content),
197 })
198 }
199 }
200 return out
201}
202
203func rebuildContent(blocks []ContentBlock) []provider.Content {
204 out := make([]provider.Content, 0, len(blocks))
205 for _, b := range blocks {
206 switch b.Type {
207 case "text":
208 out = append(out, provider.TextBlock{Text: b.Text})
209 case "image":
210 out = append(out, provider.ImageBlock{MimeType: b.MimeType, Data: b.Data})
211 case "tool_call":
212 args := b.Args
213 if len(args) == 0 {
214 args = json.RawMessage("{}")
215 }
216 out = append(out, provider.ToolCallBlock{ID: b.ID, Name: b.Name, Arguments: args})
217 case "tool_result":
218 out = append(out, provider.ToolResultBlock{
219 CallID: b.CallID,
220 IsError: b.IsError,
221 Content: rebuildContent(b.Content),
222 })
223 }
224 }
225 return out
226}