9p: files for per-session dynamic tool loading and inspection

This commit is contained in:
lkn 2026-07-28 14:12:41 +02:00
parent f1f039da20
commit 96dda6e5f0
11 changed files with 228 additions and 180 deletions

View File

@ -52,9 +52,9 @@ type FileConfig struct {
Async_ bool
}
func (e *FileConfig) Stat() (os.FileInfo, error) { return e.StatFn() }
func (e *FileConfig) Read() ([]byte, error) { return e.ReadFn() }
func (e *FileConfig) Write(data []byte) error { return e.WriteFn(data) }
func (e *FileConfig) Stat() (os.FileInfo, error) { return e.StatFn() }
func (e *FileConfig) Read() ([]byte, error) { return e.ReadFn() }
func (e *FileConfig) Write(data []byte) error { return e.WriteFn(data) }
func (e *FileConfig) BlockingRead(ctx context.Context, base string) ([]byte, string, error) {
return e.BlockingReadFn(ctx, base)
}
@ -84,8 +84,8 @@ type SyntheticEntry struct {
IsDir_ bool
}
func (e *SyntheticEntry) Name() string { return e.Name_ }
func (e *SyntheticEntry) IsDir() bool { return e.IsDir_ }
func (e *SyntheticEntry) Name() string { return e.Name_ }
func (e *SyntheticEntry) IsDir() bool { return e.IsDir_ }
func (e *SyntheticEntry) Type() os.FileMode {
if e.IsDir_ {
return os.ModeDir

View File

@ -158,5 +158,3 @@ func TestFlatDirPutMkdirError(t *testing.T) {
t.Error("Write should fail when dir doesn't exist")
}
}

View File

@ -12,8 +12,9 @@ import "os"
// Paths without trailing "/" are scoped entries within a parent.
//
// Template variables:
// {id} — session ID (dynamic, matches any session)
// {file} — file name within a session directory
//
// {id} — session ID (dynamic, matches any session)
// {file} — file name within a session directory
const (
PathRoot = "/"
PathSessions = "/s/"
@ -67,30 +68,33 @@ var Perms = map[string]Perm{
"cleanup": 0555,
},
},
PathSessionDir: {DirMode: 0755},
PathTools: {DirMode: 0755, FileMode: 0755},
PathSessionDir: {DirMode: 0755},
PathTools: {DirMode: 0755, FileMode: 0755},
PathSessionFile: {
Files: map[string]os.FileMode{
"elevate": 0666,
"plan": 0666,
"ctl": 0666,
"prompt": 0666,
"fifo.in": 0222,
"fifo.out": 0444,
"chat": 0444,
"offset": 0444,
"cfg": 0666,
"state": 0444,
"statewait": 0444,
"usage": 0444,
"cost": 0444,
"ctxsz": 0444,
"models": 0444,
"elevate": 0666,
"plan": 0666,
"ctl": 0666,
"prompt": 0666,
"fifo.in": 0222,
"fifo.out": 0444,
"chat": 0444,
"offset": 0444,
"cfg": 0666,
"state": 0444,
"statewait": 0444,
"usage": 0444,
"cost": 0444,
"ctxsz": 0444,
"models": 0444,
"systemprompt": 0444,
"env": 0444,
"tail": 0555,
"prompt.prev": 0444,
"context": 0444,
"env": 0444,
"tail": 0555,
"prompt.prev": 0444,
"context": 0444,
"tools.loaded": 0444,
"tools.rev": 0444,
"tools": 0666,
},
},
}

View File

@ -11,11 +11,11 @@ type FileSpec struct {
Name string
Mode os.FileMode
Read func() ([]byte, error)
Write func([]byte) error // nil = read-only
Write func([]byte) error // nil = read-only
Wait func(ctx context.Context, base string) (content []byte, nextBase string, err error) // nil = not waitable
Size func() int64 // optional; if nil, len(Read())
OneShot bool // true = yields data once per open
Async bool // true = writes dispatched asynchronously
Size func() int64 // optional; if nil, len(Read())
OneShot bool // true = yields data once per open
Async bool // true = writes dispatched asynchronously
}
// NewSynth creates a Tree backed by a table of FileSpecs (synthetic files).

View File

@ -365,4 +365,4 @@ func (d *Tree) MkdirAll(rel string) error {
return fmt.Errorf("no backing directory")
}
return os.MkdirAll(filepath.Join(d.Tree[0], rel), PermMkdir)
}
}

47
main.go
View File

@ -21,6 +21,7 @@ import (
olog "ollie/pkg/log"
"ollie/pkg/paths"
"ollie/pkg/tools/execute"
"ollie/pkg/tools/registry"
fs "olliesrv/fs"
"olliesrv/mount"
"olliesrv/server"
@ -159,10 +160,17 @@ func runServer(sockPath string) {
agentsDirs := agent.AgentsDirs()
sessionsDir := paths.DataDir() + "/sessions"
// Create the tool registry
toolRegistry, regErr := registry.NewRegistry()
if regErr != nil {
fmt.Fprintf(os.Stderr, "warning: tool registry: %v\n", regErr)
}
// D-Bus adapter (initialized after manager so callbacks can reference it).
var dbusAdapter *DBusAdapter
mgr := session.NewManager(session.ManagerConfig{
ToolRegistry: toolRegistry,
AgentsDir: agentsDirs[0],
SessionsDir: sessionsDir,
Log: sink.NewLogger("9p"),
@ -248,7 +256,7 @@ func runServer(sockPath string) {
srv = server.New(server.Config{
Sink: sink,
SessionMgr: mgr,
RootStore: NewRootStore(),
RootStore: NewRootStore(toolRegistry),
ElevateBroker: elevateBroker,
InvalidateModels: modelCache.Invalidate,
ToolPrompt: execute.ToolPrompt,
@ -329,7 +337,7 @@ func runServer(sockPath string) {
// ToolIndex generates a tool index from a tree's file listing.
// NewRootStore returns a read-only FileTree for synthetic root-level files.
func NewRootStore() *fs.Tree {
func NewRootStore(reg *registry.Registry) *fs.Tree {
helpPath := paths.CfgDir() + "/help.md"
readEntries := map[string]func() ([]byte, error){
"backends": func() ([]byte, error) {
@ -380,7 +388,7 @@ func NewRootStore() *fs.Tree {
if _, ok := readEntries[name]; ok {
return name, nil
}
if name == "ctl" || name == "complete" || name == "generate" || name == "route" || name == "tools" {
if name == "ctl" || name == "complete" || name == "generate" || name == "route" {
return name, nil
}
return "", fmt.Errorf("%s: not found", name)
@ -394,7 +402,6 @@ func NewRootStore() *fs.Tree {
fs.FileEntry("models", fs.Perms[fs.PathRoot].FileMode),
fs.FileEntry("help", fs.Perms[fs.PathRoot].FileMode),
fs.FileEntry("route", 0666),
fs.FileEntry("tools", 0666),
}, nil
}),
fs.WithOpener(func(_ []string, name string) (fs.File, error) {
@ -422,38 +429,6 @@ func NewRootStore() *fs.Tree {
BlockingReadFn: notBlocking,
}, nil
}
if name == "tools" {
var query string
return &fs.FileConfig{
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "tools", Mode_: 0666}, nil },
ReadFn: func() ([]byte, error) {
if query != "" {
result := execute.ToolPrompt(query)
query = "" // clear after read
if result == "" {
return []byte("no prompt found for: " + query + "\n"), nil
}
return []byte(result + "\n"), nil
}
// Surface listing
infos := execute.DiscoverTools()
var sb strings.Builder
for _, ti := range infos {
if ti.Description != "" {
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
} else {
sb.WriteString(ti.Name + "\n")
}
}
return []byte(sb.String()), nil
},
WriteFn: func(data []byte) error {
query = strings.TrimSpace(string(data))
return nil
},
BlockingReadFn: notBlocking,
}, nil
}
if name == "ctl" {
return &fs.FileConfig{
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "ctl", Mode_: 0666}, nil },

View File

@ -14,29 +14,29 @@ import (
"ollie/pkg/agent"
"ollie/pkg/backend"
olog "ollie/pkg/log"
"olliesrv/session"
"olliesrv/fs"
"olliesrv/session"
)
// --- stub agent.Core ---
type stubCore struct {
state string
running bool
backend_ string
model string
agentName string
cwd string
usage string
ctxsz string
models string
sysprompt string
reply string
params backend.GenerationParams
closed bool
waitCh chan string
submitted []string
queued []string
state string
running bool
backend_ string
model string
agentName string
cwd string
usage string
ctxsz string
models string
sysprompt string
reply string
params backend.GenerationParams
closed bool
waitCh chan string
submitted []string
queued []string
interrupted bool
setSessionIDErr error
reactResponseID string
@ -54,59 +54,65 @@ func (c *stubCore) Submit(_ context.Context, input string) {
close(c.submitCh)
}
}
func (c *stubCore) Interrupt(error) bool { c.interrupted = true; return c.running }
func (c *stubCore) Inject(string) {}
func (c *stubCore) Queue(s string) { c.queued = append(c.queued, s) }
func (c *stubCore) Interrupt(error) bool { c.interrupted = true; return c.running }
func (c *stubCore) Inject(string) {}
func (c *stubCore) Queue(s string) { c.queued = append(c.queued, s) }
func (c *stubCore) PopQueue() (string, bool) {
if len(c.queued) == 0 { return "", false }
s := c.queued[0]; c.queued = c.queued[1:]; return s, true
if len(c.queued) == 0 {
return "", false
}
s := c.queued[0]
c.queued = c.queued[1:]
return s, true
}
func (c *stubCore) IsRunning() bool { return c.running }
func (c *stubCore) State() string { return c.state }
func (c *stubCore) Reply() string { return c.reply }
func (c *stubCore) AgentName() string { return c.agentName }
func (c *stubCore) BackendName() string { return c.backend_ }
func (c *stubCore) ModelName() string { return c.model }
func (c *stubCore) CtxSz() string { return c.ctxsz }
func (c *stubCore) Usage() string { return c.usage }
func (c *stubCore) Cost() string { return "" }
func (c *stubCore) ListModels() string { return c.models }
func (c *stubCore) CWD() string { return c.cwd }
func (c *stubCore) SetCWD(dir string) error { c.cwd = dir; return nil }
func (c *stubCore) SetSessionID(string) error { return c.setSessionIDErr }
func (c *stubCore) Context() []backend.Message { return nil }
func (c *stubCore) SystemPrompt() string { return c.sysprompt }
func (c *stubCore) GenerationParams() backend.GenerationParams { return c.params }
func (c *stubCore) SetGenerationParams(p backend.GenerationParams) error { c.params = p; return nil }
func (c *stubCore) SetEnv(string, string) {}
func (c *stubCore) IsRunning() bool { return c.running }
func (c *stubCore) State() string { return c.state }
func (c *stubCore) Reply() string { return c.reply }
func (c *stubCore) AgentName() string { return c.agentName }
func (c *stubCore) BackendName() string { return c.backend_ }
func (c *stubCore) ModelName() string { return c.model }
func (c *stubCore) CtxSz() string { return c.ctxsz }
func (c *stubCore) Usage() string { return c.usage }
func (c *stubCore) Cost() string { return "" }
func (c *stubCore) ListModels() string { return c.models }
func (c *stubCore) CWD() string { return c.cwd }
func (c *stubCore) SetCWD(dir string) error { c.cwd = dir; return nil }
func (c *stubCore) SetSessionID(string) error { return c.setSessionIDErr }
func (c *stubCore) Context() []backend.Message { return nil }
func (c *stubCore) SystemPrompt() string { return c.sysprompt }
func (c *stubCore) GenerationParams() backend.GenerationParams { return c.params }
func (c *stubCore) SetGenerationParams(p backend.GenerationParams) error { c.params = p; return nil }
func (c *stubCore) SetEnv(string, string) {}
func (c *stubCore) WaitChange(ctx context.Context, _, _ string) (string, bool) {
if c.waitCh != nil {
select {
case v := <-c.waitCh: return v, true
case <-ctx.Done(): return "", false
case v := <-c.waitCh:
return v, true
case <-ctx.Done():
return "", false
}
}
<-ctx.Done()
return "", false
}
func (c *stubCore) Close() { c.closed = true }
func (c *stubCore) Detach() bool { return false }
func (c *stubCore) ListDetached() []agent.DetachedInfo { return nil }
func (c *stubCore) SignalDetached(int, int) error { return nil }
func (c *stubCore) GetDetachedOutput(int) (string, error) { return "", nil }
func (c *stubCore) DismissDetached(int) bool { return false }
func (c *stubCore) InjectSystemEvent(string) {}
func (c *stubCore) Reactions() map[string]string { return nil }
func (c *stubCore) React(string) {}
func (c *stubCore) Close() { c.closed = true }
func (c *stubCore) Detach() bool { return false }
func (c *stubCore) ListDetached() []agent.DetachedInfo { return nil }
func (c *stubCore) SignalDetached(int, int) error { return nil }
func (c *stubCore) GetDetachedOutput(int) (string, error) { return "", nil }
func (c *stubCore) DismissDetached(int) bool { return false }
func (c *stubCore) InjectSystemEvent(string) {}
func (c *stubCore) Reactions() map[string]string { return nil }
func (c *stubCore) React(string) {}
func (c *stubCore) ReactTo(responseID, emoji string) error {
c.reactResponseID = responseID
c.reactEmoji = emoji
return nil
}
func (c *stubCore) SaveSession(string) error { return nil }
func (c *stubCore) ToolCallCount() int64 { return 0 }
func (c *stubCore) CompactionModel() string { return "" }
func (c *stubCore) SetCompactionModel(string) {}
func (c *stubCore) SaveSession(string) error { return nil }
func (c *stubCore) ToolCallCount() int64 { return 0 }
func (c *stubCore) CompactionModel() string { return "" }
func (c *stubCore) SetCompactionModel(string) {}
func (c *stubCore) Bus() *pubsub.Bus {
if c.bus_ == nil {
c.bus_ = pubsub.NewBus()
@ -115,6 +121,7 @@ func (c *stubCore) Bus() *pubsub.Bus {
}
type publishCore struct{ *stubCore }
func (c *publishCore) Submit(_ context.Context, input string) {
c.submitted = append(c.submitted, input)
bus := c.Bus()
@ -126,6 +133,7 @@ func (c *publishCore) Submit(_ context.Context, input string) {
}
type blockingCore struct{ *stubCore }
func (c *blockingCore) Submit(ctx context.Context, input string) {
c.submitted = append(c.submitted, input)
<-ctx.Done()
@ -166,23 +174,31 @@ func newTestSessionManagerWithCore(t *testing.T) *session.Manager {
func testStoreRead(t *testing.T, s fs.FileTree, name string) []byte {
t.Helper()
e, err := s.Open(name)
if err != nil { t.Fatalf("Open(%s): %v", name, err) }
if err != nil {
t.Fatalf("Open(%s): %v", name, err)
}
data, err := e.Read()
if err != nil { t.Fatalf("Read(%s): %v", name, err) }
if err != nil {
t.Fatalf("Read(%s): %v", name, err)
}
return data
}
func testStoreWrite(t *testing.T, s fs.FileTree, name string, data []byte) {
t.Helper()
e, err := s.Open(name)
if err != nil { t.Fatalf("Open(%s): %v", name, err) }
if err := e.Write(data); err != nil { t.Fatalf("Write(%s): %v", name, err) }
if err != nil {
t.Fatalf("Open(%s): %v", name, err)
}
if err := e.Write(data); err != nil {
t.Fatalf("Write(%s): %v", name, err)
}
}
var _ = time.Second // suppress unused import
var _ = time.Second // suppress unused import
var _ = strings.Contains // suppress unused import
var _ = fmt.Sprintf // suppress unused import
var _ = os.Remove // suppress unused import
var _ = fmt.Sprintf // suppress unused import
var _ = os.Remove // suppress unused import
func newTestSessionFileStore(t *testing.T, sess *session.Session) (*fs.Tree, *stubCore) {
t.Helper()
@ -193,6 +209,7 @@ func newTestSessionFileStore(t *testing.T, sess *session.Session) (*fs.Tree, *st
func(id string) error { return nil },
func(data []byte) error { return nil },
func() {},
nil,
)
return sf, core
}
@ -200,8 +217,9 @@ func newTestSessionFileStore(t *testing.T, sess *session.Session) (*fs.Tree, *st
func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *fs.Tree {
t.Helper()
sink := testSink()
return session.NewSessionTree(sess, sink.NewLogger("test"), kill, rename, save, func() {})
return session.NewSessionTree(sess, sink.NewLogger("test"), kill, rename, save, func() {}, nil)
}
// ===== session.Session =====
func TestSessionAppendLog(t *testing.T) {
@ -418,7 +436,7 @@ func TestSessionFileStoreReadableContract(t *testing.T) {
defer sess.Cancel()
sink := testSink()
_ = session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
}
func TestSessionFileStoreList(t *testing.T) {
@ -426,7 +444,7 @@ func TestSessionFileStoreList(t *testing.T) {
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
entries, err := sf.List()
if err != nil {
@ -443,7 +461,7 @@ func TestSessionFileStoreStatChat(t *testing.T) {
sess.AppendLog([]byte("hello"))
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
fi, err := sf.Stat("chat")
if err != nil {
@ -460,7 +478,7 @@ func TestSessionFileStoreGetChat(t *testing.T) {
sess.AppendLog([]byte("hello"))
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
data := testStoreRead(t, sf, "chat")
if string(data) != "hello" {
@ -473,7 +491,7 @@ func TestSessionFileStoreGetContent(t *testing.T) {
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} {
if _, err := sf.Open(name); err != nil {
@ -487,7 +505,7 @@ func TestSessionFileStorePutCwd(t *testing.T) {
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
testStoreWrite(t, sf, "cfg", []byte("cwd=/new/path"))
core := sess.Core.(*stubCore)
@ -501,7 +519,7 @@ func TestSessionFileStorePutEmpty(t *testing.T) {
defer sess.Cancel()
sink := testSink()
sf := session.NewSessionTree(sess, sink.NewLogger("test"),
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {})
func() {}, func(string) error { return nil }, func([]byte) error { return nil }, func() {}, nil)
// Empty write is a no-op
e, err := sf.Open("cfg")
@ -949,7 +967,7 @@ func TestSessionReactFile(t *testing.T) {
defer cancel()
core := &stubCore{state: "idle"}
sess := session.NewSession("s1", core, ctx, cancel)
store := session.NewSessionTree(sess, testSink().NewLogger("test"), func() {}, func(string) error { return nil }, nil, nil)
store := session.NewSessionTree(sess, testSink().NewLogger("test"), func() {}, func(string) error { return nil }, nil, nil, nil)
testStoreWrite(t, store, "react", []byte("👍"))
if core.reactResponseID != "" || core.reactEmoji != "👍" {

View File

@ -59,9 +59,9 @@ func (t *elevateTree) Open(name string) (fs.File, error) {
return nil, fmt.Errorf("%s: not found", name)
}
func (t *elevateTree) Create(name string) error { return fmt.Errorf("cannot create in elevate") }
func (t *elevateTree) Delete(name string) error { return fmt.Errorf("cannot delete in elevate") }
func (t *elevateTree) Rename(_, _ string) error { return fmt.Errorf("cannot rename in elevate") }
func (t *elevateTree) Create(name string) error { return fmt.Errorf("cannot create in elevate") }
func (t *elevateTree) Delete(name string) error { return fmt.Errorf("cannot delete in elevate") }
func (t *elevateTree) Rename(_, _ string) error { return fmt.Errorf("cannot rename in elevate") }
// --- policy file ---

View File

@ -19,6 +19,7 @@ import (
"ollie/pkg/elevate"
olog "ollie/pkg/log"
"ollie/pkg/paths"
"ollie/pkg/tools/registry"
"olliesrv/fs"
"olliesrv/session"
@ -27,8 +28,8 @@ import (
// Re-export fs types for use in server.go.
type (
File = fs.File
FileTree = fs.FileTree
File = fs.File
FileTree = fs.FileTree
syntheticFileInfo = fs.SyntheticFileInfo
)
@ -74,6 +75,7 @@ type Server struct {
groups map[string]map[string]bool // group → set of members
invalidateModels func()
toolPrompt func(string) string
toolRegistry *registry.Registry
}
// Config holds the pre-built trees and manager for the server.
@ -84,6 +86,8 @@ type Config struct {
ElevateBroker *elevate.Broker
InvalidateModels func()
ToolPrompt func(string) string
// ToolRegistry is the shared tool registry for lazy tool promotion.
ToolRegistry *registry.Registry
}
// Option configures the 9P server.
@ -99,6 +103,7 @@ func New(cfg Config) *Server {
groups: make(map[string]map[string]bool),
invalidateModels: cfg.InvalidateModels,
toolPrompt: cfg.ToolPrompt,
toolRegistry: cfg.ToolRegistry,
}
if cfg.ElevateBroker != nil {
s.elevateTree = newElevateTree(cfg.ElevateBroker)
@ -312,12 +317,6 @@ func (s *Server) checkPerm(uname, path string, mode uint8) error {
return nil
}
// readFile opens an entry in a tree and reads it.
func readFile(s fs.FileTree, name string) ([]byte, error) {
e, err := s.Open(name)
@ -798,12 +797,6 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan
return s.readDNS(fc, &f.routeResult)
}
// /tools: return per-fid result from previous write (offset 0-based)
if path == "/tools" {
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
return s.readDNS(fc, &f.toolsResult)
}
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
entry, err := s.openEntry(path)
if err != nil {
@ -960,22 +953,6 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
}
// /tools: lookup tool prompt by name (per-fid state, like /net/dns)
if f.path == "/tools" {
cs.mu.Unlock()
name := strings.TrimSpace(string(fc.Data))
if s.toolPrompt == nil {
return errFcall(fc, "tool lookup not configured")
}
result := s.toolPrompt(name)
if result == "" {
f.toolsResult = []byte("no prompt found for: " + name + "\n")
} else {
f.toolsResult = []byte(result + "\n")
}
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
}
// Accumulate; the 9P client may split large writes across multiple Twrite messages.
end := int(fc.Offset) + len(fc.Data)
if end > len(f.writeBuf) {
@ -1097,7 +1074,6 @@ func (s *Server) clunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag}
}
func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
@ -1127,6 +1103,8 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
// as a goroutine because they block for the entire agent turn).
func (s *Server) handleWrite(path, input, uname string) error {
s.log.Debug("handleWrite path=%q input_len=%d uname=%q", path, len(input), uname)
// Handle /tools/load - promote a tool for the current session
st, name := s.route(path)
if st == nil {
return nil

View File

@ -21,6 +21,7 @@ import (
"ollie/pkg/remote"
"ollie/pkg/tools"
"ollie/pkg/tools/execute"
"ollie/pkg/tools/registry"
"olliesrv/fs"
"olliesrv/prompts"
)
@ -249,6 +250,8 @@ type ManagerConfig struct {
EnableDBus bool
// InvalidateModels clears the model cache, forcing a refresh.
InvalidateModels func()
// ToolRegistry is the shared tool registry for lazy tool promotion.
ToolRegistry *registry.Registry
// OnSessionCreated is called after a new session is added to the manager.
// Receives the session ID and the Session pointer.
OnSessionCreated func(id string, sess *Session)
@ -707,6 +710,7 @@ func (s *Manager) openStore(sess *Session) (*fs.Tree, error) {
func(newID string) error { return s.renameSession(sess.id, newID) },
nil, // no transcript saving
s.cfg.InvalidateModels,
s.cfg.ToolRegistry,
), nil
}
@ -901,7 +905,17 @@ func (s *Manager) restoreSession(ps *agent.PersistedSession) error {
}
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
rt := agent.BuildRuntime(cfg, newDisp(), cwd, env, sysPrompt, opModel, envBlock)
disp := newDisp()
// Register the registry-backed tool server for lazy tool promotion
if s.cfg.ToolRegistry != nil {
if execSrv, ok := disp.GetServer("execute"); ok {
if es, ok := execSrv.(*execute.Server); ok {
regSrv := registry.NewServer(s.cfg.ToolRegistry, sessID, es)
disp.AddServer("registry", regSrv)
}
}
}
rt := agent.BuildRuntime(cfg, disp, cwd, env, sysPrompt, opModel, envBlock)
restoredSession := agent.RestoreSession(ps)
@ -1218,7 +1232,17 @@ func (s *Manager) CreateSession(args []string) (string, error) {
}
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
rt := agent.BuildRuntime(cfg, newDisp(), cwd, env, sysPrompt, opModel, envBlock)
disp := newDisp()
// Register the registry-backed tool server for lazy tool promotion
if s.cfg.ToolRegistry != nil {
if execSrv, ok := disp.GetServer("execute"); ok {
if es, ok := execSrv.(*execute.Server); ok {
regSrv := registry.NewServer(s.cfg.ToolRegistry, sessID, es)
disp.AddServer("registry", regSrv)
}
}
}
rt := agent.BuildRuntime(cfg, disp, cwd, env, sysPrompt, opModel, envBlock)
// sessPtr is set after NewSession; the ReadPlanStep closure captures it.
core = agent.NewAgentCore(agent.AgentCoreConfig{

View File

@ -11,6 +11,7 @@ import (
"ollie/pkg/agent"
"ollie/pkg/backend"
olog "ollie/pkg/log"
"ollie/pkg/tools/registry"
"olliesrv/fs"
)
@ -42,6 +43,9 @@ var FileList = []struct {
{"context", false, false},
{"react", false, true},
{"reactions", false, false},
{"tools.loaded", false, false},
{"tools.rev", false, false},
{"tools", false, false},
}
// sessionFilePerms returns the permission registry for per-session files.
@ -49,8 +53,8 @@ func sessionFilePerms() map[string]os.FileMode {
return fs.Perms[fs.PathSessionFile].Files
}
func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error, invalidateModels func()) *fs.Tree {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript, invalidateModels: invalidateModels}
func NewSessionTree(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error, invalidateModels func(), toolRegistry *registry.Registry) *fs.Tree {
h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript, invalidateModels: invalidateModels, toolRegistry: toolRegistry}
perms := sessionFilePerms()
specs := make([]fs.FileSpec, len(FileList))
for i, f := range FileList {
@ -69,6 +73,7 @@ type sessionHelper struct {
rename func(newID string) error
saveTranscript func([]byte) error
invalidateModels func()
toolRegistry *registry.Registry
}
func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
@ -200,6 +205,17 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
}
return h.handleCfg(input)
}
case "tools":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
if input == "" {
return nil
}
if h.toolRegistry == nil {
return fmt.Errorf("registry not available")
}
return h.toolRegistry.Load(h.sess.id, input)
}
case "react":
fs.Write = func(data []byte) error {
input := strings.TrimSpace(string(data))
@ -276,6 +292,41 @@ func (h *sessionHelper) content(name string) string {
enc.Encode(m)
}
return sb.String()
case "tools.loaded":
if h.toolRegistry == nil {
return "registry not available\n"
}
loaded := h.toolRegistry.Loaded(h.sess.id)
if len(loaded) == 0 {
return "(no tools loaded)\n"
}
var sb strings.Builder
for _, ti := range loaded {
if ti.Description != "" {
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
} else {
sb.WriteString(ti.Name + "\n")
}
}
return sb.String()
case "tools.rev":
if h.toolRegistry == nil {
return "0\n"
}
return fmt.Sprintf("%d\n", h.toolRegistry.Revision(h.sess.id))
case "tools":
if h.toolRegistry == nil {
return "registry not available\n"
}
var sb strings.Builder
for _, ti := range h.toolRegistry.Summaries() {
if ti.Description != "" {
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
} else {
sb.WriteString(ti.Name + "\n")
}
}
return sb.String()
}
return ""
}