1package sdk
2
3import (
4 "encoding/json"
5
6 "github.com/bnema/zut/packages/core"
7 "github.com/bnema/zut/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 `zut 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 // request_started / retry_scheduled
22 Provider string `json:"provider,omitempty"`
23 Model string `json:"model,omitempty"`
24 Scope string `json:"scope,omitempty"`
25 Attempt int `json:"attempt,omitempty"`
26 MaxAttempts int `json:"max_attempts,omitempty"`
27 // DelayMS is set for retry_scheduled, including a valid zero delay.
28 DelayMS *int64 `json:"delay_ms,omitempty"`
29
30 // text_delta
31 Delta string `json:"delta,omitempty"`
32
33 // tool_call
34 ID string `json:"id,omitempty"`
35 Name string `json:"name,omitempty"`
36 Args json.RawMessage `json:"args,omitempty"`
37
38 // tool_progress
39 Text string `json:"text,omitempty"`
40
41 // tool_result
42 IsError bool `json:"is_error,omitempty"`
43 Result []ContentBlock `json:"content,omitempty"`
44
45 // usage
46 Usage *Usage `json:"usage,omitempty"`
47 Cumulative *Usage `json:"cumulative,omitempty"`
48
49 // cache_diagnostics
50 CacheDiagnostics *CacheDiagnostics `json:"cache_diagnostics,omitempty"`
51
52 // user_message / assistant_message
53 Message *Message `json:"message,omitempty"`
54
55 // turn_end
56 Stop string `json:"stop,omitempty"`
57
58 // error
59 Error string `json:"error,omitempty"`
60}
61
62// Message is one transcript entry — a user prompt, assistant reply,
63// or tool result.
64type Message struct {
65 Role string `json:"role"`
66 Content []ContentBlock `json:"content"`
67 Time string `json:"time,omitempty"`
68}
69
70// ContentBlock is one piece of message content. Discriminate on Type:
71// - "text" → Text
72// - "image" → MimeType + Data (or Bytes for size-only)
73// - "tool_call" → ID + Name + Args
74// - "tool_result" → CallID + IsError + Content (recursive)
75type ContentBlock struct {
76 Type string `json:"type"`
77
78 Text string `json:"text,omitempty"`
79 MimeType string `json:"mime_type,omitempty"`
80 Data []byte `json:"data,omitempty"`
81 Bytes int `json:"bytes,omitempty"`
82 ID string `json:"id,omitempty"`
83 Name string `json:"name,omitempty"`
84 Args json.RawMessage `json:"args,omitempty"`
85 CallID string `json:"call_id,omitempty"`
86 IsError bool `json:"is_error,omitempty"`
87 Content []ContentBlock `json:"content,omitempty"`
88}
89
90// Image is a single user-attached image for Prompt.
91type Image struct {
92 MimeType string
93 Data []byte
94}
95
96// CacheDiagnostics reports sanitized provider cache-routing state.
97type CacheDiagnostics struct {
98 Eligible bool `json:"eligible"`
99 Mode string `json:"mode"`
100 Transport string `json:"transport"`
101 Continuation string `json:"continuation"`
102}
103
104// Usage is per-turn or cumulative token / cost counts.
105type Usage struct {
106 Input int `json:"input"`
107 Output int `json:"output"`
108 Reasoning *int `json:"reasoning"`
109 CacheRead int `json:"cache_read"`
110 CacheWrite int `json:"cache_write"`
111 CacheMeasuredPrompt int `json:"cache_measured_prompt,omitempty"`
112 CacheMeasuredRead int `json:"cache_measured_read,omitempty"`
113 CostUSD float64 `json:"cost_usd"`
114}
115
116// State is a snapshot of the runtime's current state.
117type State struct {
118 Provider string `json:"provider"`
119 Model string `json:"model"`
120 Reasoning string `json:"reasoning"`
121 CWD string `json:"cwd"`
122 Busy bool `json:"busy"`
123 MessageCount int `json:"message_count"`
124}
125
126// CompactResult describes the outcome of Compact.
127type CompactResult struct {
128 Summary string `json:"summary"`
129 Messages []Message `json:"messages"`
130}
131
132// ModelInfo describes one model known to the runtime.
133type ModelInfo struct {
134 ID string `json:"id"`
135 Provider string `json:"provider"`
136 ContextWindow int `json:"context_window"`
137 MaxOutput int `json:"max_output"`
138 Reasoning bool `json:"reasoning"`
139}
140
141// ---- internal converters ----
142
143func sdkReasoningTokens(usage provider.Usage) *int {
144 if !usage.ReasoningTokensKnown {
145 return nil
146 }
147 count := usage.ReasoningTokens
148 return &count
149}
150
151func toEvent(ev core.AgentEvent) Event {
152 out := Event{Type: ev.Type()}
153 switch e := ev.(type) {
154 case core.EvTurnStart:
155 out.Step = e.Step
156 case core.EvRequestStarted:
157 out.Provider = e.Provider
158 out.Model = e.Model
159 out.Scope = string(e.Scope)
160 out.Attempt = e.Attempt
161 out.MaxAttempts = e.MaxAttempts
162 case core.EvRetryScheduled:
163 out.Scope = string(e.Scope)
164 out.Attempt = e.Attempt
165 out.MaxAttempts = e.MaxAttempts
166 delayMS := e.Delay.Milliseconds()
167 out.DelayMS = &delayMS
168 case core.EvUserMessage:
169 out.Message = &Message{Role: string(e.Message.Role), Content: convertContent(e.Message.Content)}
170 case core.EvAssistantMessage:
171 out.Message = &Message{Role: string(e.Message.Role), Content: convertContent(e.Message.Content)}
172 case core.EvTextDelta:
173 out.Delta = e.Delta
174 case core.EvToolUseStart:
175 out.ID = e.ID
176 out.Name = e.Name
177 case core.EvToolUseArgs:
178 out.ID = e.ID
179 out.Delta = e.Delta
180 case core.EvToolUseEnd:
181 out.ID = e.ID
182 case core.EvToolCall:
183 out.ID = e.ID
184 out.Name = e.Name
185 out.Args = e.Args
186 case core.EvToolExecutionStarted:
187 out.ID = e.ID
188 out.Name = e.Name
189 case core.EvToolProgress:
190 out.ID = e.ID
191 out.Text = e.Text
192 case core.EvToolResult:
193 out.ID = e.ID
194 out.IsError = e.Result.IsError
195 out.Result = convertContent(e.Result.Content)
196 case core.EvCacheDiagnostics:
197 out.CacheDiagnostics = &CacheDiagnostics{
198 Eligible: e.Eligible,
199 Mode: e.Mode,
200 Transport: e.Transport,
201 Continuation: e.Continuation,
202 }
203 case core.EvUsage:
204 out.Usage = &Usage{
205 Input: e.Usage.InputTokens,
206 Output: e.Usage.OutputTokens,
207 Reasoning: sdkReasoningTokens(e.Usage),
208 CacheRead: e.Usage.CacheReadTokens,
209 CacheWrite: e.Usage.CacheWriteTokens,
210 CacheMeasuredPrompt: e.Usage.CacheMeasuredPromptTokens,
211 CacheMeasuredRead: e.Usage.CacheMeasuredReadTokens,
212 CostUSD: e.Usage.CostUSD,
213 }
214 out.Cumulative = &Usage{
215 Input: e.Cumulative.InputTokens,
216 Output: e.Cumulative.OutputTokens,
217 Reasoning: sdkReasoningTokens(e.Cumulative),
218 CacheRead: e.Cumulative.CacheReadTokens,
219 CacheWrite: e.Cumulative.CacheWriteTokens,
220 CacheMeasuredPrompt: e.Cumulative.CacheMeasuredPromptTokens,
221 CacheMeasuredRead: e.Cumulative.CacheMeasuredReadTokens,
222 CostUSD: e.Cumulative.CostUSD,
223 }
224 case core.EvTurnEnd:
225 out.Stop = string(e.Stop)
226 if e.Err != nil {
227 out.Error = e.Err.Error()
228 }
229 case core.EvError:
230 if e.Err != nil {
231 out.Error = e.Err.Error()
232 }
233 }
234 return out
235}
236
237func convertContent(blocks []provider.Content) []ContentBlock {
238 out := make([]ContentBlock, 0, len(blocks))
239 for _, b := range blocks {
240 switch v := b.(type) {
241 case provider.TextBlock:
242 out = append(out, ContentBlock{Type: "text", Text: v.Text})
243 case provider.ImageBlock:
244 out = append(out, ContentBlock{Type: "image", MimeType: v.MimeType, Bytes: len(v.Data)})
245 case provider.ToolCallBlock:
246 out = append(out, ContentBlock{Type: "tool_call", ID: v.ID, Name: v.Name, Args: v.Arguments})
247 case provider.ToolResultBlock:
248 out = append(out, ContentBlock{
249 Type: "tool_result",
250 CallID: v.CallID,
251 IsError: v.IsError,
252 Content: convertContent(v.Content),
253 })
254 }
255 }
256 return out
257}
258
259func rebuildContent(blocks []ContentBlock) []provider.Content {
260 out := make([]provider.Content, 0, len(blocks))
261 for _, b := range blocks {
262 switch b.Type {
263 case "text":
264 out = append(out, provider.TextBlock{Text: b.Text})
265 case "image":
266 out = append(out, provider.ImageBlock{MimeType: b.MimeType, Data: b.Data})
267 case "tool_call":
268 args := b.Args
269 if len(args) == 0 {
270 args = json.RawMessage("{}")
271 }
272 out = append(out, provider.ToolCallBlock{ID: b.ID, Name: b.Name, Arguments: args})
273 case "tool_result":
274 out = append(out, provider.ToolResultBlock{
275 CallID: b.CallID,
276 IsError: b.IsError,
277 Content: rebuildContent(b.Content),
278 })
279 }
280 }
281 return out
282}