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.EvToolUseStart:
53 m["id"] = e.ID
54 m["name"] = e.Name
55 case core.EvToolUseArgs:
56 m["id"] = e.ID
57 m["delta"] = e.Delta
58 case core.EvToolUseEnd:
59 m["id"] = e.ID
60 case core.EvToolCall:
61 m["id"] = e.ID
62 m["name"] = e.Name
63 var args any
64 _ = json.Unmarshal(e.Args, &args)
65 m["args"] = args
66 case core.EvToolProgress:
67 m["id"] = e.ID
68 m["text"] = e.Text
69 case core.EvToolResult:
70 m["id"] = e.ID
71 m["is_error"] = e.Result.IsError
72 m["content"] = ContentToJSON(e.Result.Content)
73 case core.EvUsage:
74 m["input"] = e.Usage.InputTokens
75 m["output"] = e.Usage.OutputTokens
76 m["cache_read"] = e.Usage.CacheReadTokens
77 m["cache_write"] = e.Usage.CacheWriteTokens
78 m["cost_usd"] = e.Usage.CostUSD
79 m["cumulative"] = map[string]any{
80 "input": e.Cumulative.InputTokens,
81 "output": e.Cumulative.OutputTokens,
82 "cache_read": e.Cumulative.CacheReadTokens,
83 "cache_write": e.Cumulative.CacheWriteTokens,
84 "cost_usd": e.Cumulative.CostUSD,
85 }
86 case core.EvTurnEnd:
87 m["stop"] = string(e.Stop)
88 if e.Err != nil {
89 m["error"] = e.Err.Error()
90 }
91 }
92 return m
93}
94
95// ContentToJSON serialises a transcript content slice into the same
96// shape used in EventToJSON. Exported alongside EventToJSON for the
97// RPC mode.
98func ContentToJSON(blocks []provider.Content) []map[string]any {
99 out := make([]map[string]any, 0, len(blocks))
100 for _, b := range blocks {
101 switch v := b.(type) {
102 case provider.TextBlock:
103 out = append(out, map[string]any{"type": "text", "text": v.Text})
104 case provider.ImageBlock:
105 out = append(out, map[string]any{"type": "image", "mime_type": v.MimeType, "bytes": len(v.Data)})
106 case provider.ToolCallBlock:
107 var args any
108 _ = json.Unmarshal(v.Arguments, &args)
109 out = append(out, map[string]any{"type": "tool_call", "id": v.ID, "name": v.Name, "args": args})
110 case provider.ToolResultBlock:
111 out = append(out, map[string]any{
112 "type": "tool_result",
113 "call_id": v.CallID,
114 "is_error": v.IsError,
115 "content": ContentToJSON(v.Content),
116 })
117 }
118 }
119 return out
120}
121
122func jsonString(s string) string {
123 b, _ := json.Marshal(s)
124 return string(b)
125}