1package modes
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8
9 "github.com/bnema/zut/packages/core"
10 "github.com/bnema/zut/packages/provider"
11)
12
13// RunJSON runs the agent to completion, writing one JSON object per
14// AgentEvent as newline-delimited JSON.
15func RunJSON(ctx context.Context, ag *core.Agent, prompt string, images []provider.ImageBlock, out io.Writer) error {
16 enc := json.NewEncoder(out)
17 write := func(v any) {
18 _ = enc.Encode(v)
19 }
20
21 var runErr error
22 sink := func(ev core.AgentEvent) {
23 write(EventToJSON(ev))
24 }
25
26 if err := ag.Prompt(ctx, prompt, images, sink); err != nil {
27 runErr = err
28 }
29
30 if runErr != nil {
31 fmt.Fprintln(out, `{"type":"error","message":`+jsonString(runErr.Error())+`}`)
32 }
33 return runErr
34}
35
36// EventToJSON converts an AgentEvent to a JSON-friendly map. The on-wire
37// schema is deliberately simple and flat. Exported so the RPC mode can
38// reuse the same serialisation as `zut --json`.
39func EventToJSON(ev core.AgentEvent) map[string]any {
40 m := map[string]any{"type": ev.Type()}
41 switch e := ev.(type) {
42 case core.EvTurnStart:
43 m["step"] = e.Step
44 case core.EvRequestStarted:
45 m["provider"] = e.Provider
46 m["model"] = e.Model
47 m["scope"] = string(e.Scope)
48 m["attempt"] = e.Attempt
49 m["max_attempts"] = e.MaxAttempts
50 case core.EvRetryScheduled:
51 m["scope"] = string(e.Scope)
52 m["attempt"] = e.Attempt
53 m["max_attempts"] = e.MaxAttempts
54 m["delay_ms"] = e.Delay.Milliseconds()
55 case core.EvUserMessage:
56 m["content"] = ContentToJSON(e.Message.Content)
57 m["time"] = e.Message.Time
58 case core.EvAssistantMessage:
59 m["content"] = ContentToJSON(e.Message.Content)
60 m["time"] = e.Message.Time
61 case core.EvTextDelta:
62 m["delta"] = e.Delta
63 case core.EvToolUseStart:
64 m["id"] = e.ID
65 m["name"] = e.Name
66 case core.EvToolUseArgs:
67 m["id"] = e.ID
68 m["delta"] = e.Delta
69 case core.EvToolUseEnd:
70 m["id"] = e.ID
71 case core.EvToolCall:
72 m["id"] = e.ID
73 m["name"] = e.Name
74 var args any
75 _ = json.Unmarshal(e.Args, &args)
76 m["args"] = args
77 case core.EvToolExecutionStarted:
78 m["id"] = e.ID
79 m["name"] = e.Name
80 case core.EvToolProgress:
81 m["id"] = e.ID
82 m["text"] = e.Text
83 case core.EvToolResult:
84 m["id"] = e.ID
85 m["is_error"] = e.Result.IsError
86 m["content"] = ContentToJSON(e.Result.Content)
87 case core.EvUsage:
88 m["input"] = e.Usage.InputTokens
89 m["output"] = e.Usage.OutputTokens
90 m["reasoning"] = nullableReasoningTokens(e.Usage)
91 m["cache_read"] = e.Usage.CacheReadTokens
92 m["cache_write"] = e.Usage.CacheWriteTokens
93 m["cost_usd"] = e.Usage.CostUSD
94 m["cumulative"] = map[string]any{
95 "input": e.Cumulative.InputTokens,
96 "output": e.Cumulative.OutputTokens,
97 "reasoning": nullableReasoningTokens(e.Cumulative),
98 "cache_read": e.Cumulative.CacheReadTokens,
99 "cache_write": e.Cumulative.CacheWriteTokens,
100 "cost_usd": e.Cumulative.CostUSD,
101 }
102 case core.EvTurnEnd:
103 m["stop"] = string(e.Stop)
104 if e.Err != nil {
105 m["error"] = e.Err.Error()
106 }
107 }
108 return m
109}
110
111// ContentToJSON serialises a transcript content slice into the same
112// shape used in EventToJSON. Exported alongside EventToJSON for the
113// RPC mode.
114func ContentToJSON(blocks []provider.Content) []map[string]any {
115 out := make([]map[string]any, 0, len(blocks))
116 for _, b := range blocks {
117 switch v := b.(type) {
118 case provider.TextBlock:
119 out = append(out, map[string]any{"type": "text", "text": v.Text})
120 case provider.ImageBlock:
121 out = append(out, map[string]any{"type": "image", "mime_type": v.MimeType, "bytes": len(v.Data)})
122 case provider.ToolCallBlock:
123 var args any
124 _ = json.Unmarshal(v.Arguments, &args)
125 out = append(out, map[string]any{"type": "tool_call", "id": v.ID, "name": v.Name, "args": args})
126 case provider.ToolResultBlock:
127 out = append(out, map[string]any{
128 "type": "tool_result",
129 "call_id": v.CallID,
130 "is_error": v.IsError,
131 "content": ContentToJSON(v.Content),
132 })
133 }
134 }
135 return out
136}
137
138func nullableReasoningTokens(usage provider.Usage) any {
139 if !usage.ReasoningTokensKnown {
140 return nil
141 }
142 return usage.ReasoningTokens
143}
144
145func jsonString(s string) string {
146 b, _ := json.Marshal(s)
147 return string(b)
148}