9p: add /complete file for stateless code completion
Follows the Plan 9 /net/dns pattern: per-fid request-response. Write JSON request (or plain prefix), write blocks during LLM call, read returns the completion result. Per-fid state means concurrent clients get independent request/response pairs. Also includes the Complete method, findOrCreateCopilot, and noise stripping helpers in the session package.
This commit is contained in:
parent
81ad172aa3
commit
d8e0b9b5ba
10
main.go
10
main.go
|
|
@ -354,7 +354,7 @@ func NewRootStore() *fs.Tree {
|
|||
if _, ok := readEntries[name]; ok {
|
||||
return name, nil
|
||||
}
|
||||
if name == "ctl" {
|
||||
if name == "ctl" || name == "complete" {
|
||||
return name, nil
|
||||
}
|
||||
return "", fmt.Errorf("%s: not found", name)
|
||||
|
|
@ -368,6 +368,14 @@ func NewRootStore() *fs.Tree {
|
|||
}, nil
|
||||
}),
|
||||
fs.WithOpener(func(_ []string, name string) (fs.File, error) {
|
||||
if name == "complete" {
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "complete", Mode_: 0666}, nil },
|
||||
ReadFn: func() ([]byte, error) { return nil, nil },
|
||||
WriteFn: func([]byte) error { return nil },
|
||||
BlockingReadFn: notBlocking,
|
||||
}, nil
|
||||
}
|
||||
if name == "ctl" {
|
||||
return &fs.FileConfig{
|
||||
StatFn: func() (os.FileInfo, error) { return &fs.SyntheticFileInfo{Name_: "ctl", Mode_: 0666}, nil },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ package server
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
|
|
@ -38,12 +39,13 @@ const (
|
|||
|
||||
// fid tracks per-descriptor state for a single 9P connection.
|
||||
type fid struct {
|
||||
path string
|
||||
qid plan9.Qid
|
||||
mode uint8
|
||||
writeBuf []byte
|
||||
waitBase string // for *wait files: value snapshotted at open time
|
||||
dirCache []byte // cached serialized dir entries for readdir
|
||||
path string
|
||||
qid plan9.Qid
|
||||
mode uint8
|
||||
writeBuf []byte
|
||||
waitBase string // for *wait files: value snapshotted at open time
|
||||
dirCache []byte // cached serialized dir entries for readdir
|
||||
completeResult []byte // for /complete: result of last write (per-fid state)
|
||||
}
|
||||
|
||||
// connState tracks all open fids for a single 9P connection.
|
||||
|
|
@ -753,6 +755,12 @@ func (s *Server) read(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan
|
|||
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data}
|
||||
}
|
||||
|
||||
// /complete: return per-fid result from previous write
|
||||
if path == "/complete" {
|
||||
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
||||
return s.readSlice(fc, f.completeResult)
|
||||
}
|
||||
|
||||
s.log.Debug("Tread path=%q offset=%d count=%d", path, fc.Offset, fc.Count)
|
||||
entry, err := s.openEntry(path)
|
||||
if err != nil {
|
||||
|
|
@ -840,6 +848,29 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
return errFcall(fc, "bad fid")
|
||||
}
|
||||
s.log.Debug("Twrite fid=%d path=%q offset=%d len=%d", fc.Fid, f.path, fc.Offset, len(fc.Data))
|
||||
|
||||
// /complete: synchronous request-response (per-fid state, like /net/dns)
|
||||
if f.path == "/complete" {
|
||||
cs.mu.Unlock()
|
||||
var req struct {
|
||||
CWD string `json:"cwd"`
|
||||
FilePath string `json:"file"`
|
||||
Prefix string `json:"prefix"`
|
||||
Suffix string `json:"suffix"`
|
||||
ExtraContext string `json:"context"`
|
||||
}
|
||||
if err := json.Unmarshal(fc.Data, &req); err != nil {
|
||||
// If not JSON, treat entire write as prefix (simple mode)
|
||||
req.Prefix = strings.TrimSpace(string(fc.Data))
|
||||
}
|
||||
result, err := s.sessionMgr.Complete(req.CWD, req.FilePath, req.Prefix, req.Suffix, req.ExtraContext)
|
||||
if err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
f.completeResult = []byte(result)
|
||||
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) {
|
||||
|
|
@ -1013,7 +1044,7 @@ func (s *Server) buildDirData(path string) []byte {
|
|||
var dirs []plan9.Dir
|
||||
|
||||
if path == "/" {
|
||||
rootEntries := []string{"agents", "backends", "ctl", "help", "models", "s"}
|
||||
rootEntries := []string{"agents", "backends", "complete", "ctl", "help", "models", "s"}
|
||||
for _, name := range rootEntries {
|
||||
fpath := "/" + name
|
||||
st := s.makeStat(fpath)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package session
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
|
@ -1081,3 +1082,164 @@ func FormatEvent(ev agent.Event) []byte {
|
|||
func squashWhitespace(s string) string {
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
|
||||
// --- Code Completion ---
|
||||
|
||||
// Complete performs a single-shot code completion using a dedicated copilot session.
|
||||
func (s *Manager) Complete(cwd, filePath, prefix, suffix, extraContext string) (string, error) {
|
||||
if cwd == "" {
|
||||
cwd, _ = os.Getwd()
|
||||
}
|
||||
cwd = paths.ExpandHome(os.ExpandEnv(cwd))
|
||||
|
||||
model := os.Getenv("OLLIE_COMPLETE_MODEL")
|
||||
backendName := os.Getenv("OLLIE_COMPLETE_BACKEND")
|
||||
if model == "" || backendName == "" {
|
||||
return "", fmt.Errorf("OLLIE_COMPLETE_MODEL and OLLIE_COMPLETE_BACKEND must be set")
|
||||
}
|
||||
|
||||
const prefixMax = 12000
|
||||
const suffixMax = 1000
|
||||
if len(prefix) > prefixMax {
|
||||
prefix = prefix[len(prefix)-prefixMax:]
|
||||
}
|
||||
if len(suffix) > suffixMax {
|
||||
suffix = suffix[:suffixMax]
|
||||
}
|
||||
|
||||
sessID := s.findOrCreateCopilot(cwd, backendName, model)
|
||||
if sessID == "" {
|
||||
return "", fmt.Errorf("failed to create copilot session")
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
sess, ok := s.sessions[sessID]
|
||||
s.mu.RUnlock()
|
||||
if !ok {
|
||||
return "", fmt.Errorf("copilot session disappeared")
|
||||
}
|
||||
|
||||
fileHint := ""
|
||||
if filePath != "" {
|
||||
fileHint = " in " + filePath
|
||||
}
|
||||
contextBlock := ""
|
||||
if extraContext != "" {
|
||||
contextBlock = "\n" + extraContext
|
||||
}
|
||||
prompt := fmt.Sprintf(`Implement the code at the cursor%s. The prefix ends at the point where new code is needed. Write the implementation — do not echo stubs, TODOs, or placeholder returns from the prefix. Output ONLY raw code. No reasoning, no shell commands, no explanations, no markdown fences, no backticks, no preamble. Your entire response must be valid code that can be inserted directly into the file.
|
||||
%s
|
||||
<prefix>
|
||||
%s
|
||||
</prefix>
|
||||
<suffix>
|
||||
%s
|
||||
</suffix>`, fileHint, contextBlock, prefix, suffix)
|
||||
|
||||
sess.Core.Submit(sess.Ctx, "/clear")
|
||||
sess.Core.Submit(sess.Ctx, prompt)
|
||||
|
||||
for {
|
||||
state := sess.Core.State()
|
||||
if state == "idle" {
|
||||
break
|
||||
}
|
||||
next, ok := sess.Core.WaitChange(sess.Ctx, agent.WatchState, state)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("copilot session cancelled")
|
||||
}
|
||||
if next == "idle" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := sess.Core.Reply()
|
||||
result = stripCompletionNoise(result)
|
||||
result = stripPrefixEcho(prefix, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Manager) findOrCreateCopilot(cwd, backendName, modelName string) string {
|
||||
sessID := fmt.Sprintf("%d-copilot", crc32Str(cwd))
|
||||
|
||||
s.mu.RLock()
|
||||
_, exists := s.sessions[sessID]
|
||||
s.mu.RUnlock()
|
||||
if exists {
|
||||
return sessID
|
||||
}
|
||||
|
||||
cfg := LoadAgentConfig(s.cfg.AgentsDir, "copilot", nil)
|
||||
be, err := backend.NewWithName(backendName)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if modelName != "" {
|
||||
be.SetModel(modelName)
|
||||
}
|
||||
|
||||
uname := s.nextUname()
|
||||
newDisp := tools.NewDispatcherFunc(map[string]func() tools.Server{
|
||||
"execute": execute.Decl(cwd),
|
||||
})
|
||||
rt := agent.BuildRuntime(cfg, newDisp(), cwd, []string{"OLLIE_SESSION_ID=" + sessID})
|
||||
|
||||
core := agent.NewAgentCore(agent.AgentCoreConfig{
|
||||
Backend: be,
|
||||
AgentName: "copilot",
|
||||
AgentsDir: s.cfg.AgentsDir,
|
||||
SessionsDir: s.cfg.SessionsDir,
|
||||
SessionID: sessID,
|
||||
Uname: uname,
|
||||
CWD: cwd,
|
||||
Runtime: rt,
|
||||
NewDispatcher: newDisp,
|
||||
Log: s.cfg.Sink.NewLogger("copilot"),
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sess := NewSession(sessID, core, ctx, cancel)
|
||||
sess.uname = uname
|
||||
|
||||
s.mu.Lock()
|
||||
s.sessions[sessID] = sess
|
||||
s.mu.Unlock()
|
||||
return sessID
|
||||
}
|
||||
|
||||
func stripCompletionNoise(s string) string {
|
||||
var lines []string
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "```") {
|
||||
continue
|
||||
}
|
||||
if trimmed == "<prefix>" || trimmed == "</prefix>" ||
|
||||
trimmed == "<suffix>" || trimmed == "</suffix>" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, ":: ") {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func stripPrefixEcho(prefix, result string) string {
|
||||
tailMax := 200
|
||||
if len(prefix) < tailMax {
|
||||
tailMax = len(prefix)
|
||||
}
|
||||
for i := tailMax; i > 0; i-- {
|
||||
tail := prefix[len(prefix)-i:]
|
||||
if strings.HasPrefix(result, tail) {
|
||||
return result[len(tail):]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func crc32Str(s string) uint32 {
|
||||
return crc32.ChecksumIEEE([]byte(s))
|
||||
}
|
||||
|
|
|
|||
Reference in New Issue