repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

2.7 KB · 105 lines · Go Raw History
  1package main
  2
  3import (
  4	"strings"
  5	"sync"
  6)
  7
  8// The record of what a turn actually did, kept so the page can show it.
  9//
 10// All of this already happened and none of it was ever visible, so an answer
 11// written from the model's memory looked exactly like one read off a tool. A
 12// step says what went in and what came out, and the page draws them as a list
 13// under the answer.
 14//
 15// It is written next to the message, so it is bounded on purpose: a system
 16// prompt is several thousand characters and a page of them per turn would grow
 17// the database faster than the conversation does.
 18
 19// What one field of one step may store.
 20const stepMax = 1200
 21
 22// How many steps a turn may record. Six tool rounds with a gate and a nudge
 23// each is comfortably inside this, and a runaway turn cannot write a megabyte.
 24const stepsMax = 60
 25
 26type Step struct {
 27	// One of: prompt, memory, wikipedia, model, tool, gate, answer, title,
 28	// compact. The page styles by this and shows the label as written.
 29	Kind  string `json:"kind"`
 30	Label string `json:"label"`
 31	In    string `json:"in,omitempty"`
 32	Out   string `json:"out,omitempty"`
 33	MS    int64  `json:"ms,omitempty"`
 34	// Something failed or was rejected, which the page colours rather than
 35	// hides, since a refused tool is part of the work.
 36	Bad bool `json:"bad,omitempty"`
 37	// Free text for whatever the step counts, like "1,204 in / 86 out".
 38	Meta string `json:"meta,omitempty"`
 39}
 40
 41// Trace collects the steps of one turn and sends each one out as it happens,
 42// so the list fills in while the answer is still being written.
 43type Trace struct {
 44	mu    sync.Mutex
 45	steps []Step
 46	emit  func(Event)
 47}
 48
 49func NewTrace(emit func(Event)) *Trace {
 50	if emit == nil {
 51		emit = func(Event) {}
 52	}
 53	return &Trace{emit: emit}
 54}
 55
 56// Add records a step. It is safe from any goroutine, since the memory pass and
 57// the turn both write here.
 58func (t *Trace) Add(s Step) {
 59	if t == nil {
 60		return
 61	}
 62	s.In, s.Out = clip(s.In), clip(s.Out)
 63	t.mu.Lock()
 64	if len(t.steps) >= stepsMax {
 65		t.mu.Unlock()
 66		return
 67	}
 68	t.steps = append(t.steps, s)
 69	t.mu.Unlock()
 70	t.emit(Event{Kind: "step", Step: &s})
 71}
 72
 73func (t *Trace) Steps() []Step {
 74	if t == nil {
 75		return nil
 76	}
 77	t.mu.Lock()
 78	defer t.mu.Unlock()
 79	return append([]Step(nil), t.steps...)
 80}
 81
 82// clip keeps a field inside the budget and says how much it dropped, since a
 83// prompt cut off with no mark reads as a prompt that was that short.
 84func clip(s string) string {
 85	s = strings.TrimSpace(s)
 86	if len(s) <= stepMax {
 87		return s
 88	}
 89	return strings.TrimSpace(s[:stepMax]) + "\n… " + itoa(len(s)-stepMax) + " more characters"
 90}
 91
 92func itoa(n int) string {
 93	if n == 0 {
 94		return "0"
 95	}
 96	var b [20]byte
 97	i := len(b)
 98	for n > 0 {
 99		i--
100		b[i] = byte('0' + n%10)
101		n /= 10
102	}
103	return string(b[i:])
104}