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.
38func eventToJSON(ev core.AgentEvent) map[string]any {
39 m := map[string]any{"type": ev.Type()}
40 switch e := ev.(type) {
41 case core.EvTurnStart:
42 m["step"] = e.Step
43 case core.EvUserMessage:
44 m["content"] = contentToJSON(e.Message.Content)
45 m["time"] = e.Message.Time
46 case core.EvAssistantMessage:
47 m["content"] = contentToJSON(e.Message.Content)
48 m["time"] = e.Message.Time
49 case core.EvTextDelta:
50 m["delta"] = e.Delta
51 case core.EvToolCall:
52 m["id"] = e.ID
53 m["name"] = e.Name
54 var args any
55 _ = json.Unmarshal(e.Args, &args)
56 m["args"] = args
57 case core.EvToolProgress:
58 m["id"] = e.ID
59 m["text"] = e.Text
60 case core.EvToolResult:
61 m["id"] = e.ID
62 m["is_error"] = e.Result.IsError
63 m["content"] = contentToJSON(e.Result.Content)
64 case core.EvUsage:
65 m["input"] = e.Usage.InputTokens
66 m["output"] = e.Usage.OutputTokens
67 m["cache_read"] = e.Usage.CacheReadTokens
68 m["cache_write"] = e.Usage.CacheWriteTokens
69 m["cost_usd"] = e.Usage.CostUSD
70 m["cumulative"] = map[string]any{
71 "input": e.Cumulative.InputTokens,
72 "output": e.Cumulative.OutputTokens,
73 "cache_read": e.Cumulative.CacheReadTokens,
74 "cache_write": e.Cumulative.CacheWriteTokens,
75 "cost_usd": e.Cumulative.CostUSD,
76 }
77 case core.EvTurnEnd:
78 m["stop"] = string(e.Stop)
79 if e.Err != nil {
80 m["error"] = e.Err.Error()
81 }
82 }
83 return m
84}
85
86func contentToJSON(blocks []provider.Content) []map[string]any {
87 out := make([]map[string]any, 0, len(blocks))
88 for _, b := range blocks {
89 switch v := b.(type) {
90 case provider.TextBlock:
91 out = append(out, map[string]any{"type": "text", "text": v.Text})
92 case provider.ImageBlock:
93 out = append(out, map[string]any{"type": "image", "mime_type": v.MimeType, "bytes": len(v.Data)})
94 case provider.ToolCallBlock:
95 var args any
96 _ = json.Unmarshal(v.Arguments, &args)
97 out = append(out, map[string]any{"type": "tool_call", "id": v.ID, "name": v.Name, "args": args})
98 case provider.ToolResultBlock:
99 out = append(out, map[string]any{
100 "type": "tool_result",
101 "call_id": v.CallID,
102 "is_error": v.IsError,
103 "content": contentToJSON(v.Content),
104 })
105 }
106 }
107 return out
108}
109
110func jsonString(s string) string {
111 b, _ := json.Marshal(s)
112 return string(b)
113}