bots-garden/mini-mepublic Fork
⑂ 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.

Download ZIP
🛟 Updated. 99e21b0 · on main · k33g · 5d ago
pull_wiring.go · 141 lines · 4.3 KBGo Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"os"
	"time"

	"mm/internal/ask"
	"mm/internal/config"
	"mm/internal/engine"
	"mm/internal/spinner"
)

// autoPull is `autoPull: true` (ticket 61): before anything else starts, it
// downloads the models this mm will use and the server does not have yet —
// its own, then its mmConfigs agents' and its routing deciders' — with the
// progress on w (stderr: never the answer of -p, never the ACP stream).
//
// Only the agent's own model can stop mm: without it there is nothing to
// run. Another model that cannot be pulled is a warning — that agent or that
// decider fails later, where it is used. An asked agent (a child of an /ask)
// pulls nothing: its asker already did, and its start-up is bounded.
func autoPull(ctx context.Context, e *engine.Engine, w io.Writer, tty bool) error {
	if !config.Cfg.AutoPull || ask.Depth() > 0 {
		return nil
	}
	report := newPullReport(w, tty)
	pulled, err := e.EnsureModel(ctx, report.step)
	report.done(e.Backend.Model, pulled, err)
	switch {
	case errors.Is(err, engine.ErrCannotPull), errors.Is(err, engine.ErrServerDown):
		fmt.Fprintf(w, "[warning: autoPull: %s (%s): %v]\n", e.Backend.Model, e.Backend.Provider, err)
	case err != nil:
		return fmt.Errorf("autoPull: %s: %w", e.Backend.Model, err)
	}
	for _, path := range otherModelFiles() {
		model, pulled, err := engine.EnsureAgentModel(ctx, path, report.step)
		report.done(model, pulled, err)
		// Every failure is worth a line here: the model is NOT on its
		// server (a listed one returns no error), and for a provider that
		// cannot pull, the line says what to do instead.
		if err != nil {
			fmt.Fprintf(w, "[warning: autoPull: %s (%s): %v]\n", model, path, err)
		}
	}
	return nil
}

// otherModelFiles lists the config files of the other models this mm will
// use: its mmConfigs agents (in name order), then its routing deciders.
func otherModelFiles() []string {
	var paths []string
	for _, name := range config.AgentNames() {
		paths = append(paths, config.Cfg.MMConfigs[name].Path)
	}
	for _, decider := range []string{config.Cfg.SkillRouting.Decider, config.Cfg.ToolRouting.Decider} {
		if decider != "" {
			paths = append(paths, decider)
		}
	}
	return paths
}

// pullReport shows a download. On a terminal, one line rewritten in place;
// elsewhere (a log, an editor's stderr) a line per tenth, so a long download
// does not write a thousand lines.
type pullReport struct {
	w       io.Writer
	tty     bool
	started time.Time
	shown   int // the last tenth printed, off a terminal
	live    bool
}

func newPullReport(w io.Writer, tty bool) *pullReport {
	return &pullReport{w: w, tty: tty, shown: -1}
}

// step is engine.PullProgress's receiver.
func (r *pullReport) step(p engine.PullProgress) {
	if !r.live {
		r.live, r.started, r.shown = true, time.Now(), -1
	}
	line := "⬇ pulling " + p.Model
	tenth := 0
	if p.Total > 0 {
		line += fmt.Sprintf(" — %s / %s (%d %%)", size(p.Completed, p.Total), size(p.Total, p.Total), p.Completed*100/p.Total)
		tenth = int(p.Completed * 10 / p.Total)
	}
	if r.tty {
		fmt.Fprintf(r.w, "\r%s\033[K", line)
		return
	}
	if tenth > r.shown {
		r.shown = tenth
		fmt.Fprintln(r.w, line)
	}
}

// done closes a download's line: what was pulled and how long it took, or
// nothing when the model was already there.
func (r *pullReport) done(model string, pulled bool, err error) {
	if r.live && r.tty {
		fmt.Fprint(r.w, "\r\033[K")
	}
	if pulled && err == nil {
		fmt.Fprintf(r.w, "⬇ pulled %s in %s\n", model, elapsed(time.Since(r.started)))
	}
	r.live = false
}

// size prints n in the unit that suits the whole download: GB from 1 GB
// (8.1 GB, as the model cards say), MB below (145 MB rather than 0.1 GB).
func size(n, total int64) string {
	if total >= 1e9 {
		return fmt.Sprintf("%.1f GB", float64(n)/1e9)
	}
	return fmt.Sprintf("%d MB", n/1e6)
}

// elapsed gives tenths under ten seconds (a small model pulls in 0.8s, not
// "0s"), whole seconds beyond.
func elapsed(d time.Duration) string {
	if d < 10*time.Second {
		return fmt.Sprintf("%.1fs", d.Seconds())
	}
	return d.Round(time.Second).String()
}

// stderrIsTerminal reports whether the progress can be rewritten in place.
func stderrIsTerminal() bool {
	return spinner.Styled() && isCharDevice(os.Stderr)
}

func isCharDevice(f *os.File) bool {
	info, err := f.Stat()
	return err == nil && info.Mode()&os.ModeCharDevice != 0
}