v0.4.1 d107bf9

json.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close