1package modes
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8
9 "github.com/patriceckhart/zot/internal/core"
10 "github.com/patriceckhart/zot/internal/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 `zot --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.EvUserMessage:
45 m["content"] = ContentToJSON(e.Message.Content)
46 m["time"] = e.Message.Time
47 case core.EvAssistantMessage:
48 m["content"] = ContentToJSON(e.Message.Content)
49 m["time"] = e.Message.Time
50 case core.EvTextDelta:
51 m["delta"] = e.Delta
52 case core.EvToolCall:
53 m["id"] = e.ID
54 m["name"] = e.Name
55 var args any
56 _ = json.Unmarshal(e.Args, &args)
57 m["args"] = args
58 case core.EvToolProgress:
59 m["id"] = e.ID
60 m["text"] = e.Text
61 case core.EvToolResult:
62 m["id"] = e.ID
63 m["is_error"] = e.Result.IsError
64 m["content"] = ContentToJSON(e.Result.Content)
65 case core.EvUsage:
66 m["input"] = e.Usage.InputTokens
67 m["output"] = e.Usage.OutputTokens
68 m["cache_read"] = e.Usage.CacheReadTokens
69 m["cache_write"] = e.Usage.CacheWriteTokens
70 m["cost_usd"] = e.Usage.CostUSD
71 m["cumulative"] = map[string]any{
72 "input": e.Cumulative.InputTokens,
73 "output": e.Cumulative.OutputTokens,
74 "cache_read": e.Cumulative.CacheReadTokens,
75 "cache_write": e.Cumulative.CacheWriteTokens,
76 "cost_usd": e.Cumulative.CostUSD,
77 }
78 case core.EvTurnEnd:
79 m["stop"] = string(e.Stop)
80 if e.Err != nil {
81 m["error"] = e.Err.Error()
82 }
83 }
84 return m
85}
86
87// ContentToJSON serialises a transcript content slice into the same
88// shape used in EventToJSON. Exported alongside EventToJSON for the
89// RPC mode.
90func ContentToJSON(blocks []provider.Content) []map[string]any {
91 out := make([]map[string]any, 0, len(blocks))
92 for _, b := range blocks {
93 switch v := b.(type) {
94 case provider.TextBlock:
95 out = append(out, map[string]any{"type": "text", "text": v.Text})
96 case provider.ImageBlock:
97 out = append(out, map[string]any{"type": "image", "mime_type": v.MimeType, "bytes": len(v.Data)})
98 case provider.ToolCallBlock:
99 var args any
100 _ = json.Unmarshal(v.Arguments, &args)
101 out = append(out, map[string]any{"type": "tool_call", "id": v.ID, "name": v.Name, "args": args})
102 case provider.ToolResultBlock:
103 out = append(out, map[string]any{
104 "type": "tool_result",
105 "call_id": v.CallID,
106 "is_error": v.IsError,
107 "content": ContentToJSON(v.Content),
108 })
109 }
110 }
111 return out
112}
113
114func jsonString(s string) string {
115 b, _ := json.Marshal(s)
116 return string(b)
117}