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