9p: add agents dir, backends, help, plan rm; refactor readSlice
This commit is contained in:
parent
fc5443e314
commit
0a4ca8a869
48
README.md
48
README.md
|
|
@ -8,25 +8,31 @@ The goal is integration, not self-sufficiency. Rather than providing orchestrati
|
|||
|
||||
```
|
||||
ollie/
|
||||
a/ dir: agent configs (r/w, backed by ~/.config/ollie/agents/)
|
||||
<name>.json r/w: agent config; supports mv, cp, rm
|
||||
backends read: list of ollie-provided backends
|
||||
help read: help file (backed by ~/.config/ollie/help.md)
|
||||
p/ dir: prompt templates (r/w, backed by ~/.config/ollie/prompts/)
|
||||
SYSTEM_PROMPT.md r/w: the system prompt template
|
||||
pl/ dir: plans
|
||||
s/ dir: one entry per active session, sorted by creation time
|
||||
new r/w: read: KV template; write: create session
|
||||
<session-id>/ rm -r to kill session; mv to rename
|
||||
prompt write: submit a prompt to the agent
|
||||
enqueue write: queue a prompt for later execution
|
||||
dequeue read: pop the next queued prompt
|
||||
agent r/w: active agent name
|
||||
backend r/w: active backend name
|
||||
chat read: cumulative conversation history
|
||||
ctl write: stop | <command>
|
||||
ctxsz read: estimated context size vs context window
|
||||
dequeue read: pop the next queued prompt
|
||||
enqueue write: queue a prompt for later execution
|
||||
mcp read: MCP server list
|
||||
model r/w: active model name
|
||||
models read: available models from the backend
|
||||
prompt write: submit a prompt to the agent
|
||||
reply read: assistant text from the most recent turn only
|
||||
state read: current agent state (idle, thinking, calling: <tool>)
|
||||
ctl write: stop | <command>
|
||||
backend r/w: active backend name
|
||||
agent r/w: active agent name
|
||||
model r/w: active model name
|
||||
workdir r/w: working directory for tool execution and system prompt
|
||||
usage read: token counts (input, output, requests; [estimated] if not reported by backend)
|
||||
ctxsz read: estimated context size vs context window
|
||||
workdir r/w: working directory for tool execution and system prompt
|
||||
sk/ dir: skills (r/w, from OLLIE_SKILLS_PATH or ~/.config/ollie/skills/)
|
||||
<name>.md r/w: skill SKILL.md content
|
||||
t/ dir: tool scripts (r/w, backed by ~/.config/ollie/tools/)
|
||||
|
|
@ -129,6 +135,30 @@ mv ~/mnt/ollie/s/<session-id> ~/mnt/ollie/s/my-friendly-name
|
|||
|
||||
Rename is rejected if the agent is running or the target name already exists. All open file handles into the session are updated automatically.
|
||||
|
||||
## Agents
|
||||
|
||||
Agent configs live in `a/` and are backed by `~/.config/ollie/agents/`. They're plain JSON files.
|
||||
|
||||
```sh
|
||||
ls ~/mnt/ollie/a/ # list agents
|
||||
cat ~/mnt/ollie/a/default.json # read an agent config
|
||||
cp ~/mnt/ollie/a/default.json ~/mnt/ollie/a/yolo.json # copy an agent
|
||||
mv ~/mnt/ollie/a/old.json ~/mnt/ollie/a/new.json # rename
|
||||
rm ~/mnt/ollie/a/scratch.json # delete
|
||||
```
|
||||
|
||||
## Backends
|
||||
|
||||
```sh
|
||||
cat ~/mnt/ollie/backends # list available backends
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
```sh
|
||||
cat ~/mnt/ollie/help # show help (from ~/.config/ollie/help.md)
|
||||
```
|
||||
|
||||
## Example shell session
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
// Filesystem layout:
|
||||
//
|
||||
// ollie/
|
||||
// a/ (dir) agent configs (r/w, backed by ~/.config/ollie/agents/)
|
||||
// backends (read) list of ollie-provided backends
|
||||
// help (read) help file (backed by ~/.config/ollie/help.md)
|
||||
// p/ (dir) prompt templates
|
||||
// pl/ (dir) plans
|
||||
// s/ (dir) session directory
|
||||
|
|
@ -13,8 +16,16 @@
|
|||
// enqueue (write) queue a prompt for later execution
|
||||
// dequeue (read) pop the next queued prompt
|
||||
// chat (read) cumulative chat history
|
||||
// backend (r/w) read/write the active backend name
|
||||
// agent (r/w) read/write the active agent name
|
||||
// reply (read) assistant text from the most recent turn
|
||||
// state (read) current agent state
|
||||
// backend (r/w) active backend name
|
||||
// agent (r/w) active agent name
|
||||
// model (r/w) active model name
|
||||
// models (read) available models from the backend
|
||||
// mcp (read) MCP server list
|
||||
// workdir (r/w) working directory
|
||||
// usage (read) token counts
|
||||
// ctxsz (read) context size vs window
|
||||
// sk/ (dir) skills
|
||||
// t/ (dir) tool scripts
|
||||
package p9
|
||||
|
|
@ -210,6 +221,8 @@ func (s *Server) pathType(path string) string {
|
|||
return "dir"
|
||||
case len(parts) == 2 && parts[0] == "s" && parts[1] == "new":
|
||||
return "file"
|
||||
case len(parts) == 1 && parts[0] == "a":
|
||||
return "dir"
|
||||
case len(parts) == 1 && parts[0] == "p":
|
||||
return "dir"
|
||||
case len(parts) == 1 && parts[0] == "pl":
|
||||
|
|
@ -218,6 +231,14 @@ func (s *Server) pathType(path string) string {
|
|||
return "dir"
|
||||
case len(parts) == 1 && parts[0] == "t":
|
||||
return "dir"
|
||||
case len(parts) == 1 && parts[0] == "backends":
|
||||
return "file"
|
||||
case len(parts) == 1 && parts[0] == "help":
|
||||
return "file"
|
||||
case len(parts) == 2 && parts[0] == "a":
|
||||
if _, err := os.Stat(s.agentsDir + "/" + parts[1]); err == nil {
|
||||
return "file"
|
||||
}
|
||||
case len(parts) == 2 && parts[0] == "p":
|
||||
if _, err := os.Stat(s.promptsDir + "/" + parts[1]); err == nil {
|
||||
return "file"
|
||||
|
|
@ -365,7 +386,7 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
if !ok {
|
||||
return errFcall(fc, "bad fid")
|
||||
}
|
||||
if f.path != "/p" && f.path != "/pl" && f.path != "/sk" && f.path != "/t" {
|
||||
if f.path != "/a" && f.path != "/p" && f.path != "/pl" && f.path != "/sk" && f.path != "/t" {
|
||||
return errFcall(fc, "create not supported")
|
||||
}
|
||||
if fc.Perm&plan9.DMDIR != 0 {
|
||||
|
|
@ -377,6 +398,11 @@ func (s *Server) create(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
// Actually create the file on disk so that even a no-write create
|
||||
// (e.g. touch) produces a real file.
|
||||
switch f.path {
|
||||
case "/a":
|
||||
os.MkdirAll(s.agentsDir, 0755) //nolint:errcheck
|
||||
if err := os.WriteFile(s.agentsDir+"/"+fc.Name, nil, 0644); err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
case "/p":
|
||||
os.MkdirAll(s.promptsDir, 0755) //nolint:errcheck
|
||||
if err := os.WriteFile(s.promptsDir+"/"+fc.Name, nil, 0644); err != nil {
|
||||
|
|
@ -440,16 +466,31 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
// s/new returns the session creation template.
|
||||
if path == "/s/new" {
|
||||
content := []byte("workdir=\nbackend=\nmodel=\nagent=\n")
|
||||
var data []byte
|
||||
off := int(fc.Offset)
|
||||
if off < len(content) {
|
||||
end := off + int(fc.Count)
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
data = content[off:end]
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// backends is a static list of ollie-provided backends.
|
||||
if path == "/backends" {
|
||||
content := []byte(strings.Join(backend.Backends(), "\n") + "\n")
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// help is served from ~/.config/ollie/help.md.
|
||||
if path == "/help" {
|
||||
content, err := os.ReadFile(s.helpPath())
|
||||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// Agent config files are served from disk.
|
||||
if strings.HasPrefix(path, "/a/") {
|
||||
content, err := os.ReadFile(s.agentsDir + "/" + pathBase(path))
|
||||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// Prompt files are served from disk.
|
||||
|
|
@ -458,16 +499,7 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
var data []byte
|
||||
off := int(fc.Offset)
|
||||
if off < len(content) {
|
||||
end := off + int(fc.Count)
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
data = content[off:end]
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// Plan files are served from disk.
|
||||
|
|
@ -476,16 +508,7 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
var data []byte
|
||||
off := int(fc.Offset)
|
||||
if off < len(content) {
|
||||
end := off + int(fc.Count)
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
data = content[off:end]
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// Skill files are served via pkg/skills (reads from disk each time).
|
||||
|
|
@ -495,16 +518,7 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
var data []byte
|
||||
off := int(fc.Offset)
|
||||
if off < len(content) {
|
||||
end := off + int(fc.Count)
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
data = content[off:end]
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
// Tool files are served from the tools path.
|
||||
|
|
@ -513,16 +527,7 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
var data []byte
|
||||
off := int(fc.Offset)
|
||||
if off < len(content) {
|
||||
end := off + int(fc.Count)
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
data = content[off:end]
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
return s.readSlice(fc, content)
|
||||
}
|
||||
|
||||
content := s.readFile(path)
|
||||
|
|
@ -625,6 +630,25 @@ func (s *Server) readReply(fc *plan9.Fcall, path string) *plan9.Fcall {
|
|||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
}
|
||||
|
||||
// readSlice serves a byte slice at the requested offset/count.
|
||||
func (s *Server) readSlice(fc *plan9.Fcall, content []byte) *plan9.Fcall {
|
||||
var data []byte
|
||||
off := int(fc.Offset)
|
||||
if off < len(content) {
|
||||
end := off + int(fc.Count)
|
||||
if end > len(content) {
|
||||
end = len(content)
|
||||
}
|
||||
data = content[off:end]
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
}
|
||||
|
||||
func (s *Server) helpPath() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return home + "/.config/ollie/help.md"
|
||||
}
|
||||
|
||||
// readFile returns the text content for a readable session file.
|
||||
func (s *Server) readFile(path string) string {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||
|
|
@ -720,6 +744,17 @@ func (s *Server) wstat(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(f.path, "/a/"):
|
||||
oldPath := s.agentsDir + "/" + oldName
|
||||
newPath := s.agentsDir + "/" + newDir.Name
|
||||
if err := os.Rename(oldPath, newPath); err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
cs.mu.Lock()
|
||||
f.path = "/a/" + newDir.Name
|
||||
f.qid.Path = qidPath(f.path)
|
||||
cs.mu.Unlock()
|
||||
|
||||
case strings.HasPrefix(f.path, "/pl/"):
|
||||
oldPath := s.planDir + "/" + oldName
|
||||
newPath := s.planDir + "/" + newDir.Name
|
||||
|
|
@ -845,8 +880,12 @@ func (s *Server) remove(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
path := f.path
|
||||
var err error
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/a/"):
|
||||
err = os.Remove(s.agentsDir + "/" + pathBase(path))
|
||||
case strings.HasPrefix(path, "/p/"):
|
||||
err = os.Remove(s.promptsDir + "/" + pathBase(path))
|
||||
case strings.HasPrefix(path, "/pl/"):
|
||||
err = os.Remove(s.planDir + "/" + pathBase(path))
|
||||
case strings.HasPrefix(path, "/sk/"):
|
||||
name := strings.TrimSuffix(pathBase(path), ".md")
|
||||
for _, m := range skills.List() {
|
||||
|
|
@ -885,6 +924,11 @@ func (s *Server) handleWrite(path, input string) error {
|
|||
return os.WriteFile(s.promptsDir+"/"+pathBase(path), []byte(input), 0644)
|
||||
}
|
||||
|
||||
// Agent config writes go directly to disk.
|
||||
if strings.HasPrefix(path, "/a/") {
|
||||
return os.WriteFile(s.agentsDir+"/"+pathBase(path), []byte(input), 0644)
|
||||
}
|
||||
|
||||
// Plan file writes go directly to disk.
|
||||
if strings.HasPrefix(path, "/pl/") {
|
||||
os.MkdirAll(s.planDir, 0755) //nolint:errcheck
|
||||
|
|
@ -1183,11 +1227,21 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
|||
}
|
||||
|
||||
if path == "/" {
|
||||
dirs = append(dirs, makeDir("a", "/a", true, plan9.DMDIR|0755))
|
||||
dirs = append(dirs, makeDir("backends", "/backends", false, 0444))
|
||||
dirs = append(dirs, makeDir("help", "/help", false, 0444))
|
||||
dirs = append(dirs, makeDir("p", "/p", true, plan9.DMDIR|0755))
|
||||
dirs = append(dirs, makeDir("pl", "/pl", true, plan9.DMDIR|0755))
|
||||
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
|
||||
dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555))
|
||||
dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777))
|
||||
} else if path == "/a" {
|
||||
entries, _ := os.ReadDir(s.agentsDir)
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
dirs = append(dirs, makeDir(e.Name(), "/a/"+e.Name(), false, 0666))
|
||||
}
|
||||
}
|
||||
} else if path == "/p" {
|
||||
entries, _ := os.ReadDir(s.promptsDir)
|
||||
for _, e := range entries {
|
||||
|
|
@ -1304,7 +1358,7 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
qid.Type = QTDir
|
||||
if path == "/t" {
|
||||
mode = plan9.DMDIR | 0777
|
||||
} else if path == "/p" || path == "/pl" {
|
||||
} else if path == "/a" || path == "/p" || path == "/pl" {
|
||||
mode = plan9.DMDIR | 0755
|
||||
} else {
|
||||
mode = plan9.DMDIR | 0555
|
||||
|
|
@ -1318,7 +1372,11 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
case "backend", "agent", "model", "workdir":
|
||||
mode = 0666
|
||||
default:
|
||||
if path == "/s/new" {
|
||||
if path == "/backends" || path == "/help" {
|
||||
mode = 0444
|
||||
} else if path == "/s/new" {
|
||||
mode = 0666
|
||||
} else if strings.HasPrefix(path, "/a/") {
|
||||
mode = 0666
|
||||
} else if strings.HasPrefix(path, "/p/") {
|
||||
mode = 0666
|
||||
|
|
|
|||
Reference in New Issue