bots-garden/mini-mepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

ui.go · 120 lines · 4.5 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 4h ago1// Package ui decides where the agent's human-facing output goes, and — when a
2// front end wants events rather than text — carries those events as a Sink.
3//
4// Two front ends share the same engine and the same tools:
5//
6// - the terminal REPL (internal/agent): Sink stays nil, Out is os.Stdout,
7// and the tools print their own 🛠️/📄/📝 lines exactly as in part 10;
8// - the ACP façade (internal/acp): stdout belongs to JSON-RPC — the spec says
9// the agent "MUST NOT write anything to its stdout that is not a valid ACP
10// message" — so main.go moves Out to os.Stderr, and the façade installs a
11// Sink that turns every event into a session/update notification.
12//
13// The package holds no protocol knowledge: the event names below are bob's
14// (a tool starts, a tool ends, may I run this?), and internal/acp is the one
15// place that translates them into ACP messages.
16package ui
17
18import (
19 "context"
20 "fmt"
21 "io"
22 "os"
23 "path/filepath"
24 "sync"
25 "sync/atomic"
26)
27
28// Out is where human-facing text goes when no Sink is active: the banner, the
29// warnings, the watchdog message. os.Stdout for the terminal REPL; main.go
30// moves it to os.Stderr in ACP mode, so everything already written for the
31// terminal stays readable — in the editor's agent logs instead of the screen.
32var Out io.Writer = os.Stdout
33
34// ToolEvent describes one tool call, in bob's vocabulary.
35type ToolEvent struct {
36 ID string // "call_1" — unique within the process, see NextCallID
37 Tool string // the tool's name: "bash", "read_file", …
38 Title string // one line for the client's UI: "bash: ls -1"
39 Kind string // how a client should render it: "execute", "read", "edit"
40 Input map[string]any // the raw input, for the client to display
41 Path string // the file touched (absolute), "" when none
42}
43
44// ToolResult is what a tool call ended with.
45type ToolResult struct {
46 Output string // what goes back to the model (already truncated)
47 Failed bool // non-zero exit, refused edit, rejected permission…
48 Diff *Diff // the file change, when there is one a client can render
49}
50
51// Diff carries a file change for clients able to show a real diff (ACP has a
52// dedicated content type for it — that is how Zed renders edits).
53type Diff struct {
54 Path string // absolute
55 OldText string // ignored when Created
56 NewText string
57 Created bool // the file did not exist before
58}
59
60// Sink receives the events of a turn. nil (the default) means the terminal
61// front end: the tools print themselves and nobody asks for permission.
62type Sink interface {
63 // Text is a chunk of the model's streamed answer.
64 Text(text string)
65 // ToolStart announces a call before anything runs (ACP: status "pending").
66 ToolStart(ev ToolEvent)
67 // Allow asks whether the call may run. The terminal front end has no
68 // gatekeeper and would always say yes; a client shows the user a dialog.
69 // ctx is the turn's context: a cancelled turn abandons the question.
70 Allow(ctx context.Context, ev ToolEvent) bool
71 // ToolRunning reports that the call was allowed and is executing.
72 ToolRunning(id string)
73 // ToolEnd reports the outcome, output included.
74 ToolEnd(id string, res ToolResult)
75 // WorkDir is the session's working directory ("" = the process's own):
76 // where bash runs and what relative file paths resolve against.
77 WorkDir() string
78}
79
80// The active sink is set once per turn by the ACP façade and read by the tools
81// from Genkit's goroutines — hence the lock, not a plain variable.
82var (
83 mu sync.RWMutex
84 active Sink
85)
86
87// SetActive installs (or, with nil, removes) the sink for the current turn.
88func SetActive(s Sink) {
89 mu.Lock()
90 active = s
91 mu.Unlock()
92}
93
94// ActiveSink returns the sink of the current turn, nil for the terminal.
95func ActiveSink() Sink {
96 mu.RLock()
97 defer mu.RUnlock()
98 return active
99}
100
101// callID numbers the tool calls of the process. Never reset: a client
102// correlates tool_call and tool_call_update by this ID, and reusing one across
103// sessions would merge two calls in its UI.
104var callID atomic.Int64
105
106// NextCallID returns "call_1", "call_2", …
107func NextCallID() string {
108 return fmt.Sprintf("call_%d", callID.Add(1))
109}
110
111// Resolve makes a relative path absolute against the active session's working
112// directory. With no sink — the terminal REPL — the path is returned as-is:
113// part 10's behaviour, relative to the process's directory, unchanged.
114func Resolve(path string) string {
115 s := ActiveSink()
116 if s == nil || filepath.IsAbs(path) || s.WorkDir() == "" {
117 return path
118 }
119 return filepath.Join(s.WorkDir(), path)
120}