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

@ -26,22 +26,12 @@ import (
// Re-export store types for use in server.go.
type (
StoreEntry = store.StoreEntry
Store = store.Store
Session = store.Session
SessionManager = store.SessionManager
StoreEntry = store.StoreEntry
Store = store.Store
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 (
QTDir = plan9.QTDIR
QTFile = plan9.QTFILE
@ -113,19 +103,19 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server {
agentStore: store.NewDirStore(agentsDirs, 0644),
promptStore: store.NewDirStore(agent.PromptsDirs(), 0444, store.WithReadOnly()),
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()),
pluginStore: store.NewDirStore([]string{execute.PluginsPath()}, 0555, store.WithReadOnly()),
skillStore: store.NewSkillStore(),
transcriptStore: store.NewDirStore([]string{transcriptDir}, 0444, store.WithReadOnly()),
tmpStore: store.NewDirStore([]string{tmpDir}, 0600),
rootStore: NewRootStore(),
rootStore: store.NewRootStore(),
groups: make(map[string]map[string]bool),
}
for _, o := range opts {
o(s)
}
s.sessionMgr = store.NewSessionManager(store.SessionManagerConfig{
s.sessionMgr = NewSessionManager(SessionManagerConfig{
AgentsDir: agentsDir,
SessionsDir: sessionsDir,
Log: s.log,
@ -474,7 +464,7 @@ func isSessionManagerFile(path string) bool {
if !ok || strings.Contains(name, "/") {
return false
}
_, ok = store.SessionManagerFileMode(name)
_, ok = SessionManagerFileMode(name)
return ok
}
@ -1087,7 +1077,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
if path == "/backends" || path == "/help" {
mode = 0444
} else if isSessionManagerFile(path) {
mode_, _ := store.SessionManagerFileMode(base)
mode_, _ := SessionManagerFileMode(base)
mode = plan9.Perm(mode_)
} else if strings.HasPrefix(path, "/a/") {
mode = 0666

View File

@ -1,4 +1,4 @@
package store
package main
import (
"context"
@ -16,6 +16,7 @@ import (
"ollie/pkg/paths"
"ollie/pkg/tools"
"ollie/pkg/tools/execute"
"olliesrv/store"
)
// Session holds all state for one agent session.
@ -112,7 +113,7 @@ type SessionManagerConfig struct {
OnRename func(oldID, newID string)
SaveTranscript func([]byte) error
// ToolStore provides access to the shared tool directory.
ToolStore Store
ToolStore store.Store
// 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.
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.
type SessionManager struct {
store *DirStore
store *store.DirStore
cfg SessionManagerConfig
mu sync.RWMutex
sessions map[string]*Session
@ -132,7 +133,7 @@ type SessionManager struct {
}
// 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.
func (s *SessionManager) nextUname() string {
@ -151,14 +152,14 @@ func NewSessionManager(cfg SessionManagerConfig) *SessionManager {
sessions: make(map[string]*Session),
}
ss.nextUID.Store(9999)
ss.store = NewDirStore(nil, 0,
WithStat(func(_ []string, name string) (os.FileInfo, error) { return ss.stat(name) }),
WithOpener(func(_ []string, name string) (StoreEntry, error) { return ss.openEntry(name) }),
WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }),
WithListDir(func(_ []string, name string) ([]os.DirEntry, error) { return ss.ListDir(name) }),
WithDeleter(func(_ []string, name string) error { return ss.del(name) }),
WithCreator(func(_ []string, name string, _ os.FileMode) error { return ss.create(name) }),
WithRenamer(func(_ []string, old, new string) error { return ss.renameSession(old, new) }),
ss.store = store.NewDirStore(nil, 0,
store.WithStat(func(_ []string, name string) (os.FileInfo, error) { return ss.stat(name) }),
store.WithOpener(func(_ []string, name string) (store.StoreEntry, error) { return ss.openEntry(name) }),
store.WithLister(func(_ []string) ([]os.DirEntry, error) { return ss.list() }),
store.WithListDir(func(_ []string, name string) ([]os.DirEntry, error) { return ss.ListDir(name) }),
store.WithDeleter(func(_ []string, name string) error { return ss.del(name) }),
store.WithCreator(func(_ []string, name string, _ os.FileMode) error { return ss.create(name) }),
store.WithRenamer(func(_ []string, old, new string) error { return ss.renameSession(old, new) }),
)
return ss
}
@ -173,11 +174,11 @@ func (s *SessionManager) AddSession(sess *Session) {
func (s *SessionManager) list() ([]os.DirEntry, error) {
entries := make([]os.DirEntry, 0, len(sessionStoreOrder))
for _, name := range sessionStoreOrder {
entries = append(entries, FileEntry(name, sessionStoreFiles[name]))
entries = append(entries, store.FileEntry(name, sessionStoreFiles[name]))
}
s.mu.RLock()
for id := range s.sessions {
entries = append(entries, DirEntry(id, 0555))
entries = append(entries, store.DirEntry(id, 0555))
}
s.mu.RUnlock()
return entries, nil
@ -201,7 +202,7 @@ func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) {
if err != nil {
return nil, err
}
entries = append(entries, DirEntry("t", 0500))
entries = append(entries, store.DirEntry("t", 0500))
return entries, nil
}
// {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) {
// Top-level fixed files (new, idx, sh, etc.)
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)
sessID := parts[0]
@ -235,12 +236,12 @@ func (s *SessionManager) stat(name string) (os.FileInfo, error) {
}
// Session directory: {id}
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
if parts[1] == "t" {
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}
if s.cfg.ToolStore != nil {
@ -256,7 +257,7 @@ func (s *SessionManager) stat(name string) (os.FileInfo, error) {
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) {
return nil, "", fmt.Errorf("blocking read not supported")
}
@ -264,8 +265,8 @@ func (s *SessionManager) openEntry(name string) (StoreEntry, error) {
// Top-level fixed files.
switch name {
case "new":
return &EntryConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "new", Mode_: 0666}, nil },
return &store.EntryConfig{
StatFn: func() (os.FileInfo, error) { return &store.SyntheticFileInfo{Name_: "new", Mode_: 0666}, nil },
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
},
@ -275,16 +276,16 @@ func (s *SessionManager) openEntry(name string) (StoreEntry, error) {
BlockingReadFn: notBlocking,
}, nil
case "idx":
return &EntryConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
return &store.EntryConfig{
StatFn: func() (os.FileInfo, error) { return &store.SyntheticFileInfo{Name_: "idx", Mode_: 0444}, nil },
ReadFn: func() ([]byte, error) { return s.index(), nil },
WriteFn: func([]byte) error { return fmt.Errorf("idx: read-only") },
BlockingReadFn: notBlocking,
}, nil
}
if _, ok := sessionStoreFiles[name]; ok {
return &EntryConfig{
StatFn: func() (os.FileInfo, error) { return &SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil },
return &store.EntryConfig{
StatFn: func() (os.FileInfo, error) { return &store.SyntheticFileInfo{Name_: name, Mode_: sessionStoreFiles[name]}, nil },
ReadFn: func() ([]byte, error) {
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")
if err != nil {
return nil, err
}
return &EntryConfig{
return &store.EntryConfig{
StatFn: base.Stat,
ReadFn: func() ([]byte, error) {
data, err := base.Read()
@ -433,14 +434,14 @@ func (s *SessionManager) SessionByUname(uname string) *Session {
}
// 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 {
return s.openStore(sess)
}
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()
return NewSessionFileStore(
sess,

View File

@ -1,4 +1,4 @@
package store
package main
import (
"context"
@ -11,6 +11,7 @@ import (
"ollie/pkg/agent"
"ollie/pkg/backend"
olog "ollie/pkg/log"
"olliesrv/store"
)
// 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.
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 {
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 {
specs[i] = h.fileSpec(f.Name, f.Mode)
specs[i].OneShot = f.OneShot
specs[i].Async = f.Async
}
return NewFileStore(specs)
return store.NewFileStore(specs)
}
// sessionHelper holds the dependencies needed to build session FileSpecs.
@ -64,8 +65,8 @@ type sessionHelper struct {
saveTranscript func([]byte) error
}
func (h *sessionHelper) fileSpec(name string, mode os.FileMode) FileSpec {
fs := FileSpec{Name: name, Mode: mode}
func (h *sessionHelper) fileSpec(name string, mode os.FileMode) store.FileSpec {
fs := store.FileSpec{Name: name, Mode: mode}
// Read
switch name {

View File

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

File diff suppressed because it is too large Load Diff

View File

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