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