9p: add transcript store (tr/) and save via chat write or ctl save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
261fa547fb
commit
b4974cc6c2
|
|
@ -17,7 +17,7 @@
|
||||||
// prompt (write) submit a prompt to the agent
|
// prompt (write) submit a prompt to the agent
|
||||||
// enqueue (write) queue a prompt for later execution
|
// enqueue (write) queue a prompt for later execution
|
||||||
// dequeue (read) pop the next queued prompt
|
// dequeue (read) pop the next queued prompt
|
||||||
// chat (read) cumulative chat history
|
// chat (r/w) cumulative chat history; write to save a transcript to tr/
|
||||||
// state (read) current agent state
|
// state (read) current agent state
|
||||||
// backend (r/w) active backend name
|
// backend (r/w) active backend name
|
||||||
// agent (r/w) active agent name
|
// agent (r/w) active agent name
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
// ctxsz (read) context size vs window
|
// ctxsz (read) context size vs window
|
||||||
// sk/ (dir) skills
|
// sk/ (dir) skills
|
||||||
// t/ (dir) tool scripts
|
// t/ (dir) tool scripts
|
||||||
|
// tr/ (dir) transcripts (ro; write to s/{id}/chat to save)
|
||||||
package p9
|
package p9
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -41,6 +42,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"9fans.net/go/plan9"
|
"9fans.net/go/plan9"
|
||||||
"ollie/pkg/agent"
|
"ollie/pkg/agent"
|
||||||
|
|
@ -144,12 +146,15 @@ type Server struct {
|
||||||
skillStore Store
|
skillStore Store
|
||||||
sessionStore Store
|
sessionStore Store
|
||||||
batchStore *BatchStore
|
batchStore *BatchStore
|
||||||
|
transcriptStore Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new Server.
|
// New creates a new Server.
|
||||||
func New() *Server {
|
func New() *Server {
|
||||||
memDir := defaultMemDir()
|
memDir := defaultMemDir()
|
||||||
os.MkdirAll(memDir, 0755) //nolint:errcheck
|
os.MkdirAll(memDir, 0755) //nolint:errcheck
|
||||||
|
transcriptDir := defaultTranscriptDir()
|
||||||
|
os.MkdirAll(transcriptDir, 0755) //nolint:errcheck
|
||||||
agentsDir := agent.DefaultAgentsDir()
|
agentsDir := agent.DefaultAgentsDir()
|
||||||
s := &Server{
|
s := &Server{
|
||||||
sessions: make(map[string]*session),
|
sessions: make(map[string]*session),
|
||||||
|
|
@ -161,6 +166,7 @@ func New() *Server {
|
||||||
memStore: NewFlatDirStore(memDir, 0644),
|
memStore: NewFlatDirStore(memDir, 0644),
|
||||||
toolStore: NewToolStore(),
|
toolStore: NewToolStore(),
|
||||||
skillStore: NewSkillStore(),
|
skillStore: NewSkillStore(),
|
||||||
|
transcriptStore: NewFlatDirStore(transcriptDir, 0444),
|
||||||
}
|
}
|
||||||
s.sessionStore = &SessionStore{srv: s}
|
s.sessionStore = &SessionStore{srv: s}
|
||||||
s.batchStore = NewBatchStore(s)
|
s.batchStore = NewBatchStore(s)
|
||||||
|
|
@ -176,6 +182,15 @@ func defaultPlanDir() string {
|
||||||
return home + "/.config/ollie/planning"
|
return home + "/.config/ollie/planning"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// defaultTranscriptDir returns the transcript directory from OLLIE_TRANSCRIPT_PATH or the default.
|
||||||
|
func defaultTranscriptDir() string {
|
||||||
|
if p := os.Getenv("OLLIE_TRANSCRIPT_PATH"); p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
return home + "/.config/ollie/transcript"
|
||||||
|
}
|
||||||
|
|
||||||
// defaultMemDir returns the memory directory from OLLIE_MEMORY_PATH or the default.
|
// defaultMemDir returns the memory directory from OLLIE_MEMORY_PATH or the default.
|
||||||
func defaultMemDir() string {
|
func defaultMemDir() string {
|
||||||
if p := os.Getenv("OLLIE_MEMORY_PATH"); p != "" {
|
if p := os.Getenv("OLLIE_MEMORY_PATH"); p != "" {
|
||||||
|
|
@ -197,6 +212,10 @@ func (s *Server) sessionFileStore(sessID string) (*SessionFileStore, bool) {
|
||||||
sess,
|
sess,
|
||||||
func() { s.killSession(sessID) },
|
func() { s.killSession(sessID) },
|
||||||
func(newID string) error { return s.renameSession(sessID, newID) },
|
func(newID string) error { return s.renameSession(sessID, newID) },
|
||||||
|
func(data []byte) error {
|
||||||
|
name := time.Now().Format("20060102T150405") + "-chat.md"
|
||||||
|
return s.transcriptStore.Put(name, data)
|
||||||
|
},
|
||||||
), true
|
), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,6 +333,12 @@ func (s *Server) pathType(path string) string {
|
||||||
return "dir"
|
return "dir"
|
||||||
case len(parts) == 1 && parts[0] == "t":
|
case len(parts) == 1 && parts[0] == "t":
|
||||||
return "dir"
|
return "dir"
|
||||||
|
case len(parts) == 1 && parts[0] == "tr":
|
||||||
|
return "dir"
|
||||||
|
case len(parts) == 2 && parts[0] == "tr":
|
||||||
|
if _, err := s.transcriptStore.Stat(parts[1]); err == nil {
|
||||||
|
return "file"
|
||||||
|
}
|
||||||
case len(parts) == 1 && parts[0] == "b":
|
case len(parts) == 1 && parts[0] == "b":
|
||||||
return "dir"
|
return "dir"
|
||||||
case len(parts) == 2 && parts[0] == "b":
|
case len(parts) == 2 && parts[0] == "b":
|
||||||
|
|
@ -649,6 +674,16 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
||||||
return s.readSlice(fc, content)
|
return s.readSlice(fc, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transcript files are served from the transcript store.
|
||||||
|
if strings.HasPrefix(path, "/tr/") {
|
||||||
|
plog.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
||||||
|
content, err := s.transcriptStore.Get(pathBase(path))
|
||||||
|
if err != nil {
|
||||||
|
return errFcall(fc, err.Error())
|
||||||
|
}
|
||||||
|
return s.readSlice(fc, content)
|
||||||
|
}
|
||||||
|
|
||||||
// Session files: /s/{id}/{file}
|
// Session files: /s/{id}/{file}
|
||||||
if strings.HasPrefix(path, "/s/") {
|
if strings.HasPrefix(path, "/s/") {
|
||||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||||
|
|
@ -1243,6 +1278,7 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir("s", "/s", true, plan9.DMDIR|0555))
|
||||||
dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555))
|
dirs = append(dirs, makeDir("sk", "/sk", true, plan9.DMDIR|0555))
|
||||||
dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777))
|
dirs = append(dirs, makeDir("t", "/t", true, plan9.DMDIR|0777))
|
||||||
|
dirs = append(dirs, makeDir("tr", "/tr", true, plan9.DMDIR|0555))
|
||||||
} else if path == "/a" {
|
} else if path == "/a" {
|
||||||
entries, _ := s.agentStore.List()
|
entries, _ := s.agentStore.List()
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
|
@ -1299,6 +1335,18 @@ func (s *Server) readDir(path string, offset uint64, count uint32) []byte {
|
||||||
}
|
}
|
||||||
dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), false, mode))
|
dirs = append(dirs, makeDir(e.Name(), "/t/"+e.Name(), false, mode))
|
||||||
}
|
}
|
||||||
|
} else if path == "/tr" {
|
||||||
|
entries, _ := s.transcriptStore.List()
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
d := makeDir(e.Name(), "/tr/"+e.Name(), false, 0444)
|
||||||
|
if info, err := e.Info(); err == nil {
|
||||||
|
d.Atime = uint32(info.ModTime().Unix())
|
||||||
|
d.Mtime = uint32(info.ModTime().Unix())
|
||||||
|
}
|
||||||
|
dirs = append(dirs, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if path == "/b" {
|
} else if path == "/b" {
|
||||||
entries, _ := s.batchStore.List()
|
entries, _ := s.batchStore.List()
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
|
|
@ -1521,6 +1569,12 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
||||||
if content, err := s.toolStore.Get(base); err == nil {
|
if content, err := s.toolStore.Get(base); err == nil {
|
||||||
dir.Length = uint64(len(content))
|
dir.Length = uint64(len(content))
|
||||||
}
|
}
|
||||||
|
case strings.HasPrefix(path, "/tr/"):
|
||||||
|
if info, err := s.transcriptStore.Stat(base); err == nil {
|
||||||
|
dir.Length = uint64(info.Size())
|
||||||
|
dir.Atime = uint32(info.ModTime().Unix())
|
||||||
|
dir.Mtime = uint32(info.ModTime().Unix())
|
||||||
|
}
|
||||||
case path == "/b/new" || path == "/b/idx":
|
case path == "/b/new" || path == "/b/idx":
|
||||||
if content, err := s.batchStore.Get(base); err == nil {
|
if content, err := s.batchStore.Get(base); err == nil {
|
||||||
dir.Length = uint64(len(content))
|
dir.Length = uint64(len(content))
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ var sessionFileList = []struct {
|
||||||
{"prompt", 0200},
|
{"prompt", 0200},
|
||||||
{"enqueue", 0200},
|
{"enqueue", 0200},
|
||||||
{"dequeue", 0444},
|
{"dequeue", 0444},
|
||||||
{"chat", 0444},
|
{"chat", 0666},
|
||||||
{"offset", 0444},
|
{"offset", 0444},
|
||||||
{"state", 0444},
|
{"state", 0444},
|
||||||
{"backend", 0666},
|
{"backend", 0666},
|
||||||
|
|
@ -39,10 +39,11 @@ type SessionFileStore struct {
|
||||||
sess *session
|
sess *session
|
||||||
kill func()
|
kill func()
|
||||||
rename func(newID string) error
|
rename func(newID string) error
|
||||||
|
saveTranscript func([]byte) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSessionFileStore(sess *session, kill func(), rename func(newID string) error) *SessionFileStore {
|
func NewSessionFileStore(sess *session, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore {
|
||||||
return &SessionFileStore{sess: sess, kill: kill, rename: rename}
|
return &SessionFileStore{sess: sess, kill: kill, rename: rename, saveTranscript: saveTranscript}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SessionFileStore) List() ([]os.DirEntry, error) {
|
func (s *SessionFileStore) List() ([]os.DirEntry, error) {
|
||||||
|
|
@ -103,6 +104,9 @@ func (s *SessionFileStore) Put(name string, data []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
switch name {
|
switch name {
|
||||||
|
case "chat":
|
||||||
|
return s.saveTranscript([]byte(input))
|
||||||
|
|
||||||
case "prompt":
|
case "prompt":
|
||||||
s.sess.core.Submit(s.sess.ctx, input, s.makePublish())
|
s.sess.core.Submit(s.sess.ctx, input, s.makePublish())
|
||||||
s.sess.trackMutable()
|
s.sess.trackMutable()
|
||||||
|
|
@ -221,6 +225,12 @@ func (s *SessionFileStore) handleCtl(input string) error {
|
||||||
plog.Error("rename: %v", err)
|
plog.Error("rename: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "save":
|
||||||
|
s.sess.mu.RLock()
|
||||||
|
data := make([]byte, len(s.sess.chatLog))
|
||||||
|
copy(data, s.sess.chatLog)
|
||||||
|
s.sess.mu.RUnlock()
|
||||||
|
return s.saveTranscript(data)
|
||||||
case "compact", "clear", "backend", "model", "models",
|
case "compact", "clear", "backend", "model", "models",
|
||||||
"agents", "agent", "sessions", "cwd", "skills",
|
"agents", "agent", "sessions", "cwd", "skills",
|
||||||
"tools", "mcp", "context", "usage", "history",
|
"tools", "mcp", "context", "usage", "history",
|
||||||
|
|
|
||||||
Reference in New Issue