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:
Levi Neely 2026-07-18 16:00:54 +02:00
parent 81ad172aa3
commit d8e0b9b5ba
3 changed files with 209 additions and 8 deletions

10
main.go
View File

@ -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 },

View File

@ -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)

View File

@ -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))
}