9p: files for per-session dynamic tool loading and inspection
This commit is contained in:
parent
f1f039da20
commit
96dda6e5f0
10
fs/fs.go
10
fs/fs.go
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -158,5 +158,3 @@ func TestFlatDirPutMkdirError(t *testing.T) {
|
|||
t.Error("Write should fail when dir doesn't exist")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
50
fs/perm.go
50
fs/perm.go
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
47
main.go
|
|
@ -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 },
|
||||
|
|
|
|||
160
main_test.go
160
main_test.go
|
|
@ -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 != "👍" {
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue