Merge ollie-tmp into master

This commit is contained in:
Levi Neely 2026-04-18 09:51:41 +02:00
commit a876190310
4 changed files with 170 additions and 21 deletions

View File

@ -29,6 +29,7 @@
// ctxsz (read) context size vs window
// sk/ (dir) skills
// t/ (dir) tool scripts
// tmp/ (dir) tool dumping ground (r/w, backed by OLLIE_TMP_PATH)
// tr/ (dir) transcripts (ro; write to s/{id}/chat to save)
package p9
@ -49,6 +50,7 @@ import (
"ollie/pkg/backend"
"ollie/pkg/config"
olog "ollie/pkg/log"
"ollie/pkg/paths"
"ollie/pkg/tools"
"ollie/pkg/tools/execute"
)
@ -147,6 +149,7 @@ type Server struct {
sessionStore Store
batchStore *BatchStore
transcriptStore Store
tmpStore Store
}
// New creates a new Server.
@ -155,10 +158,12 @@ func New() *Server {
os.MkdirAll(memDir, 0755) //nolint:errcheck
transcriptDir := defaultTranscriptDir()
os.MkdirAll(transcriptDir, 0755) //nolint:errcheck
tmpDir := defaultTmpDir()
os.MkdirAll(tmpDir, 0755) //nolint:errcheck
agentsDir := agent.DefaultAgentsDir()
s := &Server{
sessions: make(map[string]*session),
agentsDir: agentsDir,
sessions: make(map[string]*session),
agentsDir: agentsDir,
sessionsDir: agent.DefaultSessionsDir(),
agentStore: NewFlatDirStore(agentsDir, 0644),
promptStore: NewFlatDirStore(agent.DefaultPromptsDir(), 0444),
@ -167,6 +172,7 @@ func New() *Server {
toolStore: NewToolStore(),
skillStore: NewSkillStore(),
transcriptStore: NewFlatDirStore(transcriptDir, 0444),
tmpStore: NewFlatDirStore(tmpDir, 0600),
}
s.sessionStore = &SessionStore{srv: s}
s.batchStore = NewBatchStore(s)
@ -178,8 +184,7 @@ func defaultPlanDir() string {
if p := os.Getenv("OLLIE_PLAN_PATH"); p != "" {
return p
}
home, _ := os.UserHomeDir()
return home + "/.config/ollie/planning"
return paths.CfgDir() + "/planning"
}
// defaultTranscriptDir returns the transcript directory from OLLIE_TRANSCRIPT_PATH or the default.
@ -187,8 +192,7 @@ func defaultTranscriptDir() string {
if p := os.Getenv("OLLIE_TRANSCRIPT_PATH"); p != "" {
return p
}
home, _ := os.UserHomeDir()
return home + "/.config/ollie/transcript"
return paths.CfgDir() + "/transcript"
}
// defaultMemDir returns the memory directory from OLLIE_MEMORY_PATH or the default.
@ -196,8 +200,15 @@ func defaultMemDir() string {
if p := os.Getenv("OLLIE_MEMORY_PATH"); p != "" {
return p
}
home, _ := os.UserHomeDir()
return home + "/.config/ollie/memory"
return paths.CfgDir() + "/memory"
}
// defaultTmpDir returns the tmp directory from OLLIE_TMP_PATH or the default.
func defaultTmpDir() string {
if p := os.Getenv("OLLIE_TMP_PATH"); p != "" {
return p
}
return paths.DataDir() + "/tmp"
}
// sessionFileStore returns a SessionFileStore for the given session ID.
@ -356,6 +367,12 @@ func (s *Server) pathType(path string) string {
return "file"
}
}
case len(parts) == 1 && parts[0] == "tmp":
return "dir"
case len(parts) == 2 && parts[0] == "tmp":
if _, err := s.tmpStore.Stat(parts[1]); err == nil {
return "file"
}
case len(parts) == 1 && parts[0] == "backends":
return "file"
case len(parts) == 1 && parts[0] == "help":
@ -529,6 +546,10 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if err := s.toolStore.Create(fc.Name); err != nil {
return errFcall(fc, err.Error())
}
case "/tmp":
if err := s.tmpStore.Create(fc.Name); err != nil {
return errFcall(fc, err.Error())
}
case "/sk":
if err := s.skillStore.Create(fc.Name); err != nil {
return errFcall(fc, err.Error())
@ -585,8 +606,8 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
}
}
// s/new and s/idx are served from the session store.
if path == "/s/new" || path == "/s/idx" {
// s/new, s/idx, and s/ls are served from the session store.
if path == "/s/new" || path == "/s/idx" || path == "/s/ls" {
plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
content, err := s.sessionStore.Get(pathBase(path))
if err != nil {
@ -674,6 +695,16 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return s.readSlice(fc, content)
}
// Tmp files are served from the tmp store.
if strings.HasPrefix(path, "/tmp/") {
plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
content, err := s.tmpStore.Get(pathBase(path))
if err != nil {
return errFcall(fc, err.Error())
}
return s.readSlice(fc, content)
}
// Transcript files are served from the transcript store.
if strings.HasPrefix(path, "/tr/") {
plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
@ -727,8 +758,7 @@ func (s *Server) readSlice(fc *plan9.Fcall, content []byte) *plan9.Fcall {
}
func (s *Server) helpPath() string {
home, _ := os.UserHomeDir()
return home + "/.config/ollie/help.md"
return paths.CfgDir() + "/help.md"
}
@ -892,13 +922,6 @@ func (s *Server) renameSession(oldID, newID string) error {
c.mu.Unlock()
}
// Rename the session tmpdir so isread markers remain valid.
oldTemp := "/tmp/ollie/" + oldID
newTemp := "/tmp/ollie/" + newID
if _, err := os.Stat(oldTemp); err == nil {
os.Rename(oldTemp, newTemp) //nolint:errcheck
}
sess.appendChat([]byte(fmt.Sprintf("(session renamed: %s -> %s)\n", oldID, newID)))
plog.Info("renamed session %s -> %s", oldID, newID)
return nil
@ -971,6 +994,8 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
err = s.skillStore.Delete(pathBase(path))
case strings.HasPrefix(path, "/t/"):
err = s.toolStore.Delete(pathBase(path))
case strings.HasPrefix(path, "/tmp/"):
err = s.tmpStore.Delete(pathBase(path))
case strings.HasPrefix(path, "/tr/"):
err = s.transcriptStore.Delete(pathBase(path))
case strings.HasPrefix(path, "/b/") && path != "/b/new":
@ -1043,6 +1068,11 @@ func (s *Server) handleWrite(path, input string) error {
return s.toolStore.Put(pathBase(path), []byte(input))
}
// Tmp file writes go to the tmp store.
if strings.HasPrefix(path, "/tmp/") {
return s.tmpStore.Put(pathBase(path), []byte(input))
}
// Session file writes: /s/{sessid}/{file}
if strings.HasPrefix(path, "/s/") {
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
@ -1280,6 +1310,7 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555))
dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777))
dirs = append(dirs, makeDir("tmp", "/tmp", true, plan9.DMDIR|0755))
dirs = append(dirs, makeDir("tr", "/tr", true, plan9.DMDIR|0555))
} else if path == "/a" {
entries, _ := s.agentStore.List()
@ -1337,6 +1368,18 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
}
dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), false, mode))
}
} else if path == "/tmp" {
entries, _ := s.tmpStore.List()
for _, e := range entries {
if !e.IsDir() {
d := makeDir(e.Name(), "/tmp/"+e.Name(), false, 0600)
if info, err := e.Info(); err == nil {
d.Atime = uint32(info.ModTime().Unix())
d.Mtime = uint32(info.ModTime().Unix())
}
dirs = append(dirs, d)
}
}
} else if path == "/tr" {
entries, _ := s.transcriptStore.List()
for _, e := range entries {
@ -1440,7 +1483,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
qid.Type = QTDir
if path == "/t" {
mode = plan9.DMDIR | 0777
} else if path == "/a" || path == "/m" || path == "/pl" {
} else if path == "/a" || path == "/m" || path == "/pl" || path == "/tmp" {
mode = plan9.DMDIR | 0755
} else {
mode = plan9.DMDIR | 0555
@ -1456,6 +1499,8 @@ func (s *Server) makeStat(path string) plan9.Dir {
default:
if path == "/backends" || path == "/help" || path == "/s/idx" {
mode = 0444
} else if path == "/s/ls" {
mode = 0555
} else if path == "/s/new" {
mode = 0666
} else if strings.HasPrefix(path, "/a/") {
@ -1545,7 +1590,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
// that check stat before reading (cat, 9pfuse, etc.) see non-zero size.
if dir.Length == 0 && !isDir {
switch {
case path == "/s/new" || path == "/s/idx":
case path == "/s/new" || path == "/s/idx" || path == "/s/ls":
if content, err := s.sessionStore.Get(base); err == nil {
dir.Length = uint64(len(content))
}
@ -1571,6 +1616,12 @@ func (s *Server) makeStat(path string) plan9.Dir {
if content, err := s.toolStore.Get(base); err == nil {
dir.Length = uint64(len(content))
}
case strings.HasPrefix(path, "/tmp/"):
if info, err := s.tmpStore.Stat(base); err == nil {
dir.Length = uint64(info.Size())
dir.Atime = uint32(info.ModTime().Unix())
dir.Mtime = uint32(info.ModTime().Unix())
}
case strings.HasPrefix(path, "/tr/"):
if info, err := s.transcriptStore.Stat(base); err == nil {
dir.Length = uint64(info.Size())

View File

@ -3,9 +3,11 @@ package p9
import (
"fmt"
"os"
"strconv"
"strings"
"ollie/pkg/agent"
"ollie/pkg/backend"
)
// sessionFileList defines the fixed set of files in a session directory,
@ -30,6 +32,7 @@ var sessionFileList = []struct {
{"models", 0444},
{"mcp", 0444},
{"systemprompt", 0444},
{"params", 0666},
}
// SessionFileStore implements ReadWriteStore for the files within a single
@ -152,10 +155,96 @@ func (s *SessionFileStore) Put(name string, data []byte) error {
s.sess.mu.Lock()
s.sess.mutableVers["cwd"].update(s.sess.core.CWD())
s.sess.mu.Unlock()
case "params":
if s.sess.core.IsRunning() {
return fmt.Errorf("cannot change params while agent is running")
}
params, err := parseParams(input, s.sess.core.GenerationParams())
if err != nil {
return err
}
return s.sess.core.SetGenerationParams(params)
}
return nil
}
func formatParams(p backend.GenerationParams) string {
var sb strings.Builder
fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens)
if p.Temperature != nil {
fmt.Fprintf(&sb, "temperature=%g\n", *p.Temperature)
} else {
fmt.Fprintf(&sb, "temperature=\n")
}
if p.FrequencyPenalty != nil {
fmt.Fprintf(&sb, "frequencyPenalty=%g\n", *p.FrequencyPenalty)
} else {
fmt.Fprintf(&sb, "frequencyPenalty=\n")
}
if p.PresencePenalty != nil {
fmt.Fprintf(&sb, "presencePenalty=%g\n", *p.PresencePenalty)
} else {
fmt.Fprintf(&sb, "presencePenalty=\n")
}
return sb.String()
}
func parseParams(input string, current backend.GenerationParams) (backend.GenerationParams, error) {
p := current
for _, line := range strings.Split(input, "\n") {
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(k)
v = strings.TrimSpace(v)
switch k {
case "maxTokens":
if v == "" {
p.MaxTokens = 0
} else {
n, err := strconv.Atoi(v)
if err != nil {
return p, fmt.Errorf("invalid maxTokens: %s", v)
}
p.MaxTokens = n
}
case "temperature":
if v == "" {
p.Temperature = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid temperature: %s", v)
}
p.Temperature = &f
}
case "frequencyPenalty":
if v == "" {
p.FrequencyPenalty = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid frequencyPenalty: %s", v)
}
p.FrequencyPenalty = &f
}
case "presencePenalty":
if v == "" {
p.PresencePenalty = nil
} else {
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return p, fmt.Errorf("invalid presencePenalty: %s", v)
}
p.PresencePenalty = &f
}
}
}
return p, nil
}
// content returns the string content of a simple readable session file.
func (s *SessionFileStore) content(name string) string {
s.sess.mu.RLock()
@ -183,6 +272,8 @@ func (s *SessionFileStore) content(name string) string {
return s.sess.core.SystemPrompt()
case "offset":
return fmt.Sprintf("%d\n", s.sess.chatOffset)
case "params":
return formatParams(s.sess.core.GenerationParams())
}
return ""
}

View File

@ -17,6 +17,7 @@ func (s *SessionStore) List() ([]os.DirEntry, error) {
entries := []os.DirEntry{
syntheticEntry("new", 0666),
syntheticEntry("idx", 0444),
syntheticEntry("ls", 0555),
}
s.srv.mu.RLock()
for id := range s.srv.sessions {
@ -32,6 +33,8 @@ func (s *SessionStore) Stat(name string) (os.FileInfo, error) {
return &syntheticFileInfo{name: "new", mode: 0666}, nil
case "idx":
return &syntheticFileInfo{name: "idx", mode: 0444}, nil
case "ls":
return &syntheticFileInfo{name: "ls", mode: 0555}, nil
}
s.srv.mu.RLock()
_, ok := s.srv.sessions[name]
@ -48,6 +51,8 @@ func (s *SessionStore) Get(name string) ([]byte, error) {
return []byte("name=\ncwd=\nbackend=\nmodel=\nagent=\n"), nil
case "idx":
return s.index(), nil
case "ls":
return []byte("#!/usr/bin/env bash\ncat ${OLLIE:-$HOME/mnt/ollie}/s/idx\n"), nil
}
return nil, fmt.Errorf("%s: not a readable file", name)
}

View File

@ -148,6 +148,8 @@ func ensureEnv() {
"OLLIE_SKILLS_PATH": filepath.Join(home, ".config", "ollie", "skills"),
"OLLIE_PLAN_PATH": filepath.Join(home, ".config", "ollie", "planning"),
"OLLIE_MEMORY_PATH": filepath.Join(home, ".config", "ollie", "memory"),
"OLLIE_TMP_PATH": filepath.Join(home, ".local", "share", "ollie", "tmp"),
"OLLIE_TRANSCRIPT_PATH": filepath.Join(home, ".config", "ollie", "transcript"),
}
for k, v := range defaults {
if os.Getenv(k) == "" {