restructure: session logic out of store/, stores into store/

- session.go, sessionfile.go: moved to main package (lifecycle)
- store/tool.go, store/root.go: store constructors where they belong
- main_test.go: session tests in main package
- store/store_test.go: only store-level tests remain
- Removed: root.go, tools.go from main (moved to store/)
- Clean separation: store/ = abstraction, main = domain
This commit is contained in:
Levi Neely 2026-05-19 18:07:00 +02:00
parent 6b1ba9a998
commit 52ad150c22
8 changed files with 1277 additions and 1278 deletions

1219
main_test.go Normal file

File diff suppressed because it is too large Load Diff

BIN
olliesrv.test Executable file

Binary file not shown.

View File

@ -28,20 +28,10 @@ import (
type ( type (
StoreEntry = store.StoreEntry StoreEntry = store.StoreEntry
Store = store.Store Store = store.Store
Session = store.Session
SessionManager = store.SessionManager
syntheticFileInfo = store.SyntheticFileInfo syntheticFileInfo = store.SyntheticFileInfo
) )
func syntheticEntry(name string, mode os.FileMode) os.DirEntry {
return store.FileEntry(name, mode)
}
func syntheticDirEntry(name string, mode os.FileMode) os.DirEntry {
return store.DirEntry(name, mode)
}
const ( const (
QTDir = plan9.QTDIR QTDir = plan9.QTDIR
QTFile = plan9.QTFILE QTFile = plan9.QTFILE
@ -113,19 +103,19 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server {
agentStore: store.NewDirStore(agentsDirs, 0644), agentStore: store.NewDirStore(agentsDirs, 0644),
promptStore: store.NewDirStore(agent.PromptsDirs(), 0444, store.WithReadOnly()), promptStore: store.NewDirStore(agent.PromptsDirs(), 0444, store.WithReadOnly()),
memStore: store.NewDirStore([]string{memDir}, 0644), memStore: store.NewDirStore([]string{memDir}, 0644),
toolStore: store.NewDirStore([]string{execute.ToolsPath()}, 0755, store.WithIndex(toolIndex)), toolStore: store.NewDirStore([]string{execute.ToolsPath()}, 0755, store.WithIndex(store.ToolIndex)),
utilStore: store.NewDirStore([]string{paths.CfgDir() + "/scripts/u"}, 0555, store.WithReadOnly()), utilStore: store.NewDirStore([]string{paths.CfgDir() + "/scripts/u"}, 0555, store.WithReadOnly()),
pluginStore: store.NewDirStore([]string{execute.PluginsPath()}, 0555, store.WithReadOnly()), pluginStore: store.NewDirStore([]string{execute.PluginsPath()}, 0555, store.WithReadOnly()),
skillStore: store.NewSkillStore(), skillStore: store.NewSkillStore(),
transcriptStore: store.NewDirStore([]string{transcriptDir}, 0444, store.WithReadOnly()), transcriptStore: store.NewDirStore([]string{transcriptDir}, 0444, store.WithReadOnly()),
tmpStore: store.NewDirStore([]string{tmpDir}, 0600), tmpStore: store.NewDirStore([]string{tmpDir}, 0600),
rootStore: NewRootStore(), rootStore: store.NewRootStore(),
groups: make(map[string]map[string]bool), groups: make(map[string]map[string]bool),
} }
for _, o := range opts { for _, o := range opts {
o(s) o(s)
} }
s.sessionMgr = store.NewSessionManager(store.SessionManagerConfig{ s.sessionMgr = NewSessionManager(SessionManagerConfig{
AgentsDir: agentsDir, AgentsDir: agentsDir,
SessionsDir: sessionsDir, SessionsDir: sessionsDir,
Log: s.log, Log: s.log,
@ -474,7 +464,7 @@ func isSessionManagerFile(path string) bool {
if !ok || strings.Contains(name, "/") { if !ok || strings.Contains(name, "/") {
return false return false
} }
_, ok = store.SessionManagerFileMode(name) _, ok = SessionManagerFileMode(name)
return ok return ok
} }
@ -1087,7 +1077,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
if path == "/backends" || path == "/help" { if path == "/backends" || path == "/help" {
mode = 0444 mode = 0444
} else if isSessionManagerFile(path) { } else if isSessionManagerFile(path) {
mode_, _ := store.SessionManagerFileMode(base) mode_, _ := SessionManagerFileMode(base)
mode = plan9.Perm(mode_) mode = plan9.Perm(mode_)
} else if strings.HasPrefix(path, "/a/") { } else if strings.HasPrefix(path, "/a/") {
mode = 0666 mode = 0666

View File

@ -1,4 +1,4 @@
package store package main
import ( import (
"context" "context"
@ -16,6 +16,7 @@ import (
"ollie/pkg/paths" "ollie/pkg/paths"
"ollie/pkg/tools" "ollie/pkg/tools"
"ollie/pkg/tools/execute" "ollie/pkg/tools/execute"
"olliesrv/store"
) )
// Session holds all state for one agent session. // Session holds all state for one agent session.
@ -112,7 +113,7 @@ type SessionManagerConfig struct {
OnRename func(oldID, newID string) OnRename func(oldID, newID string)
SaveTranscript func([]byte) error SaveTranscript func([]byte) error
// ToolStore provides access to the shared tool directory. // ToolStore provides access to the shared tool directory.
ToolStore Store ToolStore store.Store
// NewCore, if non-nil, replaces the default backend.New + agent.NewAgentCore // NewCore, if non-nil, replaces the default backend.New + agent.NewAgentCore
// path. It receives the session ID, agent name, and cwd, and returns a Core. // path. It receives the session ID, agent name, and cwd, and returns a Core.
NewCore func(sessionID, agentName, cwd string) (agent.Core, error) NewCore func(sessionID, agentName, cwd string) (agent.Core, error)
@ -124,7 +125,7 @@ type SessionManagerConfig struct {
// SessionManager manages session lifecycle and exposes sessions as a DirStore. // SessionManager manages session lifecycle and exposes sessions as a DirStore.
type SessionManager struct { type SessionManager struct {
store *DirStore store *store.DirStore
cfg SessionManagerConfig cfg SessionManagerConfig
mu sync.RWMutex mu sync.RWMutex
sessions map[string]*Session sessions map[string]*Session
@ -132,7 +133,7 @@ type SessionManager struct {
} }
// Store returns the DirStore view of the session namespace. // Store returns the DirStore view of the session namespace.
func (s *SessionManager) Store() *DirStore { return s.store } func (s *SessionManager) Store() *store.DirStore { return s.store }
// nextUname generates the next uname atomically. // nextUname generates the next uname atomically.
func (s *SessionManager) nextUname() string { func (s *SessionManager) nextUname() string {
@ -151,14 +152,14 @@ func NewSessionManager(cfg SessionManagerConfig) *SessionManager {
sessions: make(map[string]*Session), sessions: make(map[string]*Session),
} }
ss.nextUID.Store(9999) ss.nextUID.Store(9999)
ss.store = NewDirStore(nil, 0, ss.store = store.NewDirStore(nil, 0,
WithStat(func(_ []string, name string) (os.FileInfo, error) { return ss.stat(name) }), store.WithStat(func(_ []string, name string) (os.FileInfo, error) { return ss.stat(name) }),
WithOpener(func(_ []string, name string) (StoreEntry, error) { return ss.openEntry(name) }), store.WithOpener(func(_ []string, name string) (store.StoreEntry, error) { return ss.openEntry(name) }),
WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }), store.WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }),
WithListDir(func(_ []string, name string) ([]os.DirEntry, error) { return ss.ListDir(name) }), store.WithListDir(func(_ []string, name string) ([]os.DirEntry, error) { return ss.ListDir(name) }),
WithDeleter(func(_ []string, name string) error { return ss.del(name) }), store.WithDeleter(func(_ []string, name string) error { return ss.del(name) }),
WithCreator(func(_ []string, name string, _ os.FileMode) error { return ss.create(name) }), store.WithCreator(func(_ []string, name string, _ os.FileMode) error { return ss.create(name) }),
WithRenamer(func(_ []string, old, new string) error { return ss.renameSession(old, new) }), store.WithRenamer(func(_ []string, old, new string) error { return ss.renameSession(old, new) }),
) )
return ss return ss
} }
@ -173,11 +174,11 @@ func (s *SessionManager) AddSession(sess *Session) {
func (s *SessionManager) list() ([]os.DirEntry, error) { func (s *SessionManager) list() ([]os.DirEntry, error) {
entries := make([]os.DirEntry, 0, len(sessionStoreOrder)) entries := make([]os.DirEntry, 0, len(sessionStoreOrder))
for _, name := range sessionStoreOrder { for _, name := range sessionStoreOrder {
entries = append(entries, FileEntry(name, sessionStoreFiles[name])) entries = append(entries, store.FileEntry(name, sessionStoreFiles[name]))
} }
s.mu.RLock() s.mu.RLock()
for id := range s.sessions { for id := range s.sessions {
entries = append(entries, DirEntry(id, 0555)) entries = append(entries, store.DirEntry(id, 0555))
} }
s.mu.RUnlock() s.mu.RUnlock()
return entries, nil return entries, nil
@ -201,7 +202,7 @@ func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
entries = append(entries, DirEntry("t", 0500)) entries = append(entries, store.DirEntry("t", 0500))
return entries, nil return entries, nil
} }
// {id}/t — list tools (filtered by allowTools) // {id}/t — list tools (filtered by allowTools)
@ -223,7 +224,7 @@ func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) {
func (s *SessionManager) stat(name string) (os.FileInfo, error) { func (s *SessionManager) stat(name string) (os.FileInfo, error) {
// Top-level fixed files (new, idx, sh, etc.) // Top-level fixed files (new, idx, sh, etc.)
if mode, ok := sessionStoreFiles[name]; ok { if mode, ok := sessionStoreFiles[name]; ok {
return &SyntheticFileInfo{Name_: name, Mode_: mode}, nil return &store.SyntheticFileInfo{Name_: name, Mode_: mode}, nil
} }
parts := strings.SplitN(name, "/", 3) parts := strings.SplitN(name, "/", 3)
sessID := parts[0] sessID := parts[0]
@ -235,12 +236,12 @@ func (s *SessionManager) stat(name string) (os.FileInfo, error) {
} }
// Session directory: {id} // Session directory: {id}
if len(parts) == 1 { if len(parts) == 1 {
return &SyntheticFileInfo{Name_: sessID, Mode_: 0555, IsDir_: true}, nil return &store.SyntheticFileInfo{Name_: sessID, Mode_: 0555, IsDir_: true}, nil
} }
// Tools directory: {id}/t // Tools directory: {id}/t
if parts[1] == "t" { if parts[1] == "t" {
if len(parts) == 2 { if len(parts) == 2 {
return &SyntheticFileInfo{Name_: "t", Mode_: 0500, IsDir_: true}, nil return &store.SyntheticFileInfo{Name_: "t", Mode_: 0500, IsDir_: true}, nil
} }
// Tool file: {id}/t/{rel} // Tool file: {id}/t/{rel}
if s.cfg.ToolStore != nil { if s.cfg.ToolStore != nil {
@ -256,7 +257,7 @@ func (s *SessionManager) stat(name string) (os.FileInfo, error) {
return sfs.Stat(parts[1]) return sfs.Stat(parts[1])
} }
func (s *SessionManager) openEntry(name string) (StoreEntry, error) { func (s *SessionManager) openEntry(name string) (store.StoreEntry, error) {
notBlocking := func(context.Context, string) ([]byte, string, error) { notBlocking := func(context.Context, string) ([]byte, string, error) {
return nil, "", fmt.Errorf("blocking read not supported") return nil, "", fmt.Errorf("blocking read not supported")
} }
@ -264,8 +265,8 @@ func (s *SessionManager) openEntry(name string) (StoreEntry, error) {
// Top-level fixed files. // Top-level fixed files.
switch name { switch name {
case "new": case "new":
return &EntryConfig{ return &store.EntryConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "new", Mode_: 0666}, nil }, StatFn: func() (os.FileInfo, error) { return &store.SyntheticFileInfo{Name_: "new", Mode_: 0666}, nil },
ReadFn: func() ([]byte, error) { ReadFn: func() ([]byte, error) {
return []byte("name=\ncwd=\nbackend=\nmodel=\nagent=\nmaxTokens=\nmaxCompletionTokens=\ntemperature=\ntopP=\ntopK=\nminP=\ntopA=\nfrequencyPenalty=\npresencePenalty=\nrepetitionPenalty=\nreasoning=\nreasoningEffort=\nincludeReasoning=\nresponseFormat=\nstop=\nverbosity=\n"), nil return []byte("name=\ncwd=\nbackend=\nmodel=\nagent=\nmaxTokens=\nmaxCompletionTokens=\ntemperature=\ntopP=\ntopK=\nminP=\ntopA=\nfrequencyPenalty=\npresencePenalty=\nrepetitionPenalty=\nreasoning=\nreasoningEffort=\nincludeReasoning=\nresponseFormat=\nstop=\nverbosity=\n"), nil
}, },
@ -275,16 +276,16 @@ func (s *SessionManager) openEntry(name string) (StoreEntry, error) {
BlockingReadFn: notBlocking, BlockingReadFn: notBlocking,
}, nil }, nil
case "idx": case "idx":
return &EntryConfig{ return &store.EntryConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil }, StatFn: func() (os.FileInfo, error) { return &store.SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
ReadFn: func() ([]byte, error) { return s.index(), nil }, ReadFn: func() ([]byte, error) { return s.index(), nil },
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") }, WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
BlockingReadFn: notBlocking, BlockingReadFn: notBlocking,
}, nil }, nil
} }
if _, ok := sessionStoreFiles[name]; ok { if _, ok := sessionStoreFiles[name]; ok {
return &EntryConfig{ return &store.EntryConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil }, StatFn: func() (os.FileInfo, error) { return &store.SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil },
ReadFn: func() ([]byte, error) { ReadFn: func() ([]byte, error) {
return s.cfg.ReadFile(paths.CfgDir() + "/scripts/s/" + name) return s.cfg.ReadFile(paths.CfgDir() + "/scripts/s/" + name)
}, },
@ -331,12 +332,12 @@ func (s *SessionManager) openEntry(name string) (StoreEntry, error) {
} }
func (s *SessionManager) openFilteredToolIdx(allowed map[string]bool) (StoreEntry, error) { func (s *SessionManager) openFilteredToolIdx(allowed map[string]bool) (store.StoreEntry, error) {
base, err := s.cfg.ToolStore.Open("idx") base, err := s.cfg.ToolStore.Open("idx")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &EntryConfig{ return &store.EntryConfig{
StatFn: base.Stat, StatFn: base.Stat,
ReadFn: func() ([]byte, error) { ReadFn: func() ([]byte, error) {
data, err := base.Read() data, err := base.Read()
@ -433,14 +434,14 @@ func (s *SessionManager) SessionByUname(uname string) *Session {
} }
// OpenStore returns a DirStore for the given session ID. // OpenStore returns a DirStore for the given session ID.
func (s *SessionManager) OpenStore(id string) (*DirStore, error) { func (s *SessionManager) OpenStore(id string) (*store.DirStore, error) {
if sess := s.Session(id); sess != nil { if sess := s.Session(id); sess != nil {
return s.openStore(sess) return s.openStore(sess)
} }
return nil, fmt.Errorf("session not found: %s", id) return nil, fmt.Errorf("session not found: %s", id)
} }
func (s *SessionManager) openStore(sess *Session) (*DirStore, error) { func (s *SessionManager) openStore(sess *Session) (*store.DirStore, error) {
id := sess.RunnableID() id := sess.RunnableID()
return NewSessionFileStore( return NewSessionFileStore(
sess, sess,

View File

@ -1,4 +1,4 @@
package store package main
import ( import (
"context" "context"
@ -11,6 +11,7 @@ import (
"ollie/pkg/agent" "ollie/pkg/agent"
"ollie/pkg/backend" "ollie/pkg/backend"
olog "ollie/pkg/log" olog "ollie/pkg/log"
"olliesrv/store"
) )
// SessionFileList defines the fixed set of files in a session directory. // SessionFileList defines the fixed set of files in a session directory.
@ -42,17 +43,17 @@ var SessionFileList = []struct {
} }
// SessionFileStore is a DirStore for a session directory. // SessionFileStore is a DirStore for a session directory.
type SessionFileStore = DirStore type SessionFileStore = store.DirStore
func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore { func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript} h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript}
specs := make([]FileSpec, len(SessionFileList)) specs := make([]store.FileSpec, len(SessionFileList))
for i, f := range SessionFileList { for i, f := range SessionFileList {
specs[i] = h.fileSpec(f.Name, f.Mode) specs[i] = h.fileSpec(f.Name, f.Mode)
specs[i].OneShot = f.OneShot specs[i].OneShot = f.OneShot
specs[i].Async = f.Async specs[i].Async = f.Async
} }
return NewFileStore(specs) return store.NewFileStore(specs)
} }
// sessionHelper holds the dependencies needed to build session FileSpecs. // sessionHelper holds the dependencies needed to build session FileSpecs.
@ -64,8 +65,8 @@ type sessionHelper struct {
saveTranscript func([]byte) error saveTranscript func([]byte) error
} }
func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec { func (h *sessionHelper) fileSpec(name string, mode os.FileMode) store.FileSpec {
fs := FileSpec{Name: name, Mode: mode} fs := store.FileSpec{Name: name, Mode: mode}
// Read // Read
switch name { switch name {

View File

@ -1,4 +1,4 @@
package main package store
import ( import (
"context" "context"
@ -8,11 +8,10 @@ import (
"ollie/pkg/backend" "ollie/pkg/backend"
"ollie/pkg/paths" "ollie/pkg/paths"
"olliesrv/store"
) )
// NewRootStore returns a read-only Store for synthetic root-level files. // NewRootStore returns a read-only Store for synthetic root-level files.
func NewRootStore() *store.DirStore { func NewRootStore() *DirStore {
helpPath := paths.CfgDir() + "/help.md" helpPath := paths.CfgDir() + "/help.md"
entries := map[string]func() ([]byte, error){ entries := map[string]func() ([]byte, error){
"backends": func() ([]byte, error) { "backends": func() ([]byte, error) {
@ -27,27 +26,27 @@ func NewRootStore() *store.DirStore {
} }
readOnly := func([]byte) error { return fmt.Errorf("read-only") } readOnly := func([]byte) error { return fmt.Errorf("read-only") }
return store.NewDirStore(nil, 0444, return NewDirStore(nil, 0444,
store.WithReadOnly(), WithReadOnly(),
store.WithResolver(func(_ []string, name string) (string, error) { WithResolver(func(_ []string, name string) (string, error) {
if _, ok := entries[name]; ok { if _, ok := entries[name]; ok {
return name, nil return name, nil
} }
return "", fmt.Errorf("%s: not found", name) return "", fmt.Errorf("%s: not found", name)
}), }),
store.WithLister(func(_ []string) ([]os.DirEntry, error) { WithLister(func(_ []string) ([]os.DirEntry, error) {
return []os.DirEntry{ return []os.DirEntry{
syntheticEntry("backends", 0444), FileEntry("backends", 0444),
syntheticEntry("help", 0444), FileEntry("help", 0444),
}, nil }, nil
}), }),
store.WithOpener(func(_ []string, name string) (StoreEntry, error) { WithOpener(func(_ []string, name string) (StoreEntry, error) {
readFn, ok := entries[name] readFn, ok := entries[name]
if !ok { if !ok {
return nil, fmt.Errorf("%s: not found", name) return nil, fmt.Errorf("%s: not found", name)
} }
return &store.EntryConfig{ return &EntryConfig{
StatFn: func() (os.FileInfo, error) { return &syntheticFileInfo{Name_: name, Mode_: 0444}, nil }, StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: name, Mode_: 0444}, nil },
ReadFn: readFn, ReadFn: readFn,
WriteFn: readOnly, WriteFn: readOnly,
BlockingReadFn: notBlocking, BlockingReadFn: notBlocking,

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
package main package store
import ( import (
"fmt" "fmt"
"strings" "strings"
) )
// toolIndex generates a tool index from a store's file listing. // ToolIndex generates a tool index from a store's file listing.
func toolIndex(s Store) ([]byte, error) { func ToolIndex(s Store) ([]byte, error) {
entries, err := s.List() entries, err := s.List()
if err != nil { if err != nil {
return nil, err return nil, err