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.

config.go · 281 lines · 11.2 KBGo Blame HistoryRaw
💾 Saved. d722711 k33g 4h ago1// Package config holds every setting of the agent in ONE place.
2//
3// The values below are the built-in defaults; a YAML file overrides the keys it
4// mentions and leaves the others alone. That way the agent still runs with no
5// config file at all, and a single `agent.yaml` is enough to change the model,
6// the system prompt or the sampling — no recompilation.
7package config
8
9import (
10 "errors"
11 "fmt"
12 "io/fs"
13 "os"
14 "time"
15
16 "gopkg.in/yaml.v3"
17)
18
19// Config: everything that can be tuned without touching the code.
20type Config struct {
21 // Provider: which LLM server is behind BaseURL — a key of the engine
22 // registry ("dmr", "llamacpp"). Defaults to "dmr" so that every agent.yaml
23 // written before this key existed keeps its meaning.
24 Provider string `yaml:"provider"`
25
26 // Model: identifier of the chat model, in the provider's own naming
27 // (DMR: "ai/qwen2.5-coder", "hf.co/…"; llama-server: the alias it serves).
28 Model string `yaml:"model"`
29
30 // BaseURL: the OpenAI-compatible endpoint, tried first. Empty = the
31 // provider's default, so a llama.cpp config does not inherit DMR's port.
32 BaseURL string `yaml:"baseUrl"`
33
34 // Fallback: used when BaseURL does not answer — on the host DMR sits on
35 // localhost, but from inside a container or a sandbox it is reachable as
36 // host.docker.internal. A pointer, because "absent" and "" differ: absent
37 // means the provider's default, "" means no fallback at all (the fake
38 // engine's probe.yaml relies on that to stay pinned to one URL).
39 Fallback *string `yaml:"fallback"`
40
41 // APIKeyEnv: the NAME of the environment variable holding the key — never
42 // the key itself, so agent.yaml can be committed. Empty = the provider's
43 // default variable; servers that ignore the key need none.
44 APIKeyEnv string `yaml:"apiKeyEnv"`
45
46 // ContextWindow: how many tokens the server actually serves, when the
47 // operator knows it (llama-server -c, `docker model configure`). 0 = ask the
48 // server, then unknown. Displayed at start-up; the context-compression work
49 // reads it.
50 ContextWindow int `yaml:"contextWindow"`
51
52 // MaxOutput: max number of characters a tool returns to the model
53 // (context safeguard: large outputs are truncated).
54 MaxOutput int `yaml:"maxOutput"`
55
56 // MaxTurns: max number of model ↔ tools round trips for one question.
57 MaxTurns int `yaml:"maxTurns"`
58
59 // PreviewLines: how many lines of a command's output are echoed to the
60 // terminal. The user asked to SEE something; running the command is not
61 // showing it. 0 disables the echo.
62 PreviewLines int `yaml:"previewLines"`
63
64 // DisplayCommands: recap, after the answer, the list of commands the agent
65 // actually ran. The count alone says HOW MUCH it worked; the list says
66 // WHAT it did — worth showing on a screen, noise in a log.
67 DisplayCommands bool `yaml:"displayCommands"`
68
69 // System: the system prompt — what the agent is, and what it may do.
70 System string `yaml:"system"`
71
72 // SkillsDir: directory of markdown procedures exposed by the `read_skill`
73 // tool. When it holds no *.md file, the tool is not declared at all.
74 SkillsDir string `yaml:"skillsDir"`
75
76 // EditTools: declare the built-in file tools (read_file, write_file,
77 // edit_file). Off, the agent has bash and read_skill only, exactly like
78 // part 09 — and edits files through the `edit` CLI if it is on the PATH.
79 // One binary, two set-ups: that is what lets the two be compared.
80 EditTools bool `yaml:"editTools"`
81
82 // Sampling: generation settings (OpenAI API keys; the plugin converts them.)
83 Sampling map[string]any `yaml:"sampling"`
84
85 // WatchdogTimeout: how long to wait for new tokens before assuming the connection has hung.
86 WatchdogTimeout time.Duration `yaml:"watchdogTimeout"`
87
88 // Context: when and how the conversation history is compressed
89 // (internal/compact). Off unless `context.enabled: true`. The window it
90 // measures against is the top-level ContextWindow above — or, when that is
91 // 0, what the provider's probe learned from the server.
92 Context ContextConfig `yaml:"context"`
93}
94
95// ContextConfig drives the compression of the history (see
96// 08-context-compression/CONTEXT_WINDOW.md; the code is the same as 08's).
97//
98// Off by default: with `enabled: false` this part behaves exactly as before.
99// Nothing in the agent shortens the history otherwise — measured with the fake
100// engine of part 03, it grew 2 → 5 → 7 → 9 messages over four requests, and on
101// a local model the window is fixed at load time.
102//
103// Unlike 08 there is no window here: `contextWindow` is ONE key, at the top
104// level, shared by the banner and the trigger — two keys for the same number
105// would drift apart.
106type ContextConfig struct {
107 // Enabled turns the automatic compression on. `/compact` works regardless.
108 Enabled bool `yaml:"enabled"`
109
110 // Threshold: share of the context window, in percent, beyond which the
111 // history is compressed before the next question is sent.
112 Threshold int `yaml:"threshold"`
113
114 // MaxMessages: fallback trigger on the message count, for when the window
115 // is unknown (no contextWindow, no /props) or the estimate is off. 0
116 // disables it.
117 MaxMessages int `yaml:"maxMessages"`
118
119 // KeepLastTurns: question turns kept raw at the end of the history.
120 KeepLastTurns int `yaml:"keepLastTurns"`
121
122 // SummaryMaxTokens: max_tokens of the summary request.
123 SummaryMaxTokens int `yaml:"summaryMaxTokens"`
124
125 // Prompt replaces the built-in summary prompt when not empty.
126 Prompt string `yaml:"prompt"`
127
128 // ShowStats: print the one-line 🗜️ report after each compression.
129 ShowStats bool `yaml:"showStats"`
130}
131
132// DefaultPath: the config file looked up when neither the command line nor
133// AGENT_CONFIG says otherwise.
134const DefaultPath = "agent.yaml"
135
136// Cfg: the live settings, pre-filled with the built-in defaults.
137//
138// Defaults suited to a CODING AGENT — low temperature for precise and
139// reproducible answers.
140var Cfg = Config{
141 Provider: "dmr",
142 Model: "huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M",
143 // BaseURL and Fallback are left empty on purpose: the provider fills them
144 // in (for "dmr": localhost:12434 and host.docker.internal:12434, exactly the
145 // values that used to be here).
146 MaxOutput: 16000,
147 MaxTurns: 10,
148 PreviewLines: 20,
149 // Off by default: the recap is a demo/debug aid, not something every run
150 // needs. `displayCommands: true` in the YAML turns it on.
151 DisplayCommands: false,
152 SkillsDir: "skills",
153 EditTools: true,
154 System: `You are a coding agent working in a terminal.
155You have a "bash" tool to run shell commands.
156Use it to explore files, run tests, inspect the repository, etc.
157Chain several commands if needed, then answer clearly in English.
158
159A request often mixes things you answer from yourself ("say hello") with things
160only a command can answer ("list the files"). Handle every part, in the order
161asked, and run a command for each part that needs one.
162Never state the contents of a file, the output of a command, or the state of the
163repository unless a command in THIS answer returned it. What you did not read,
164you do not know: run the command instead of recalling it.
165
166BACKGROUND JOBS
167Never let a command block the answer. Anything that serves, watches or runs
168long goes to the background, with BOTH streams redirected and its pid kept:
169
170 nohup <command> > /tmp/<job>.log 2>&1 & echo $! > /tmp/<job>.pid
171
172Redirecting only stdout still blocks until the process exits. Read the
173"bg-jobs" skill before you wait on, inspect or stop such a job — each has a
174rule you cannot guess. Stop every job you started before you finish, and say
175which ones you left running.`,
176 Sampling: map[string]any{
177 "temperature": 0.0,
178 "top_p": 0.9,
179 "max_tokens": 4096,
180 },
181 WatchdogTimeout: 20 * time.Second,
182 Context: ContextConfig{
183 Enabled: false,
184 // 75 % leaves a quarter of the window for the next question, the tool
185 // outputs of its turns and the answer: one `bash` output alone can be
186 // maxOutput characters, about 4-5k tokens.
187 Threshold: 75,
188 // One command costs 2 messages (call + response): 80 is roughly 30
189 // commands of history, the point where a 12B local model slows down.
190 MaxMessages: 80,
191 // The recent turns are where the model works; summarising them makes
192 // it re-run what it just did.
193 KeepLastTurns: 3,
194 // Seven sections of one line per item fit in far less; the cap stops
195 // a runaway model from filling the window it was asked to empty.
196 SummaryMaxTokens: 1200,
197 ShowStats: true,
198 },
199}
200
201// Load reads the YAML file on top of the defaults and returns the path actually
202// used ("" when no file was found).
203//
204// Three places are tried, first one wins: the command line (cliPath, what main
205// read from -config or from a lone argument), then AGENT_CONFIG, then
206// ./agent.yaml. The command line wins over the environment because it is the
207// more local of the two — closer to the run you are making right now.
208//
209// A path asked for EXPLICITLY (command line or AGENT_CONFIG) that does not
210// exist is an error: you named a file, it should be there. A missing
211// ./agent.yaml is not an error — the built-in defaults are enough to run.
212func Load(cliPath string) (string, error) {
213 path, explicit := cliPath, true
214 if path == "" {
215 path = os.Getenv("AGENT_CONFIG")
216 }
217 if path == "" {
218 path, explicit = DefaultPath, false
219 }
220
221 data, err := os.ReadFile(path)
222 if errors.Is(err, fs.ErrNotExist) && !explicit {
223 return "", nil // no file: the defaults stand
224 }
225 if err != nil {
226 return "", err
227 }
228
229 // Unmarshalling INTO Cfg: keys absent from the file keep their default.
230 if err := yaml.Unmarshal(data, &Cfg); err != nil {
231 return "", fmt.Errorf("%s: %w", path, err)
232 }
233 applyEnv()
234 if err := Cfg.validate(); err != nil {
235 return "", fmt.Errorf("%s: %w", path, err)
236 }
237 return path, nil
238}
239
240// applyEnv lets three variables override the file: AGENT_PROVIDER, AGENT_MODEL,
241// and — resolved by the provider, see engine.Resolve — AGENT_BASE_URL. Same
242// reasoning as for AGENT_CONFIG: switching engine for one run should not mean
243// editing a file that is shown on screen.
244func applyEnv() {
245 if v := os.Getenv("AGENT_PROVIDER"); v != "" {
246 Cfg.Provider = v
247 }
248 if v := os.Getenv("AGENT_MODEL"); v != "" {
249 Cfg.Model = v
250 }
251}
252
253// validate catches the settings that would break the agent at runtime.
254func (c Config) validate() error {
255 switch {
256 case c.Provider == "":
257 return errors.New("provider must not be empty")
258 case c.Model == "":
259 return errors.New("model must not be empty")
260 case c.ContextWindow < 0:
261 return errors.New("contextWindow must be >= 0")
262 case c.MaxOutput <= 0:
263 return errors.New("maxOutput must be > 0")
264 case c.MaxTurns <= 0:
265 return errors.New("maxTurns must be > 0")
266 case c.PreviewLines < 0:
267 return errors.New("previewLines must be >= 0")
268 case c.Context.Threshold < 1 || c.Context.Threshold > 100:
269 return errors.New("context.threshold must be between 1 and 100")
270 case c.Context.KeepLastTurns < 1:
271 return errors.New("context.keepLastTurns must be >= 1")
272 case c.Context.SummaryMaxTokens <= 0:
273 return errors.New("context.summaryMaxTokens must be > 0")
274 case c.Context.MaxMessages < 0:
275 return errors.New("context.maxMessages must be >= 0")
276 }
277 // `enabled: true` with no window and no maxMessages would never trigger —
278 // but the window may still come from the server's probe at start-up, so
279 // that case is a warning in main, not an error here.
280 return nil
281}