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