fs: extract session setup, remove dead code and adapter
- Extract SetupToolServer, BuildPromptLayers, LoadAutoLoadTools into setup.go - Deduplicate session infrastructure setup between CreateAgent and restoreSession - Delete adapter.go, move HandlerCtx and type aliases into spec.go - Delete session.go (only contained unused Get/All functions) - Remove unused: GlobalRootState, PostEvent, LookupByUname, AddSession, CreateAgentFromRoot, KillFromRoot, RenameFromRoot, Session.Interrupt, Session.SetDisallowTools, Session.RunnableID - Replace RunnableID() calls with ID() - Remove comment banners Net: -131 lines, better DRY compliance
This commit is contained in:
parent
1f173f1b41
commit
bf80a63e8a
|
|
@ -1,91 +0,0 @@
|
|||
package fs
|
||||
|
||||
// This file provides type aliases that specialize fsedsl for ollie's HandlerCtx.
|
||||
// The fs package uses fsedsl directly (e.g., fsedsl.DirNode[HandlerCtx]).
|
||||
// These aliases allow other packages to use fs.FsNodeDecl, fs.Tree, etc.
|
||||
|
||||
import (
|
||||
"ollie/agent"
|
||||
"ollie/elevate"
|
||||
"ollie/fsedsl"
|
||||
olog "ollie/log"
|
||||
"ollie/skills"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// HandlerCtx carries runtime dependencies for handler functions.
|
||||
// The wire step populates the correct subset per node scope.
|
||||
// Handlers assert their scope by checking which pointer is non-nil.
|
||||
type HandlerCtx struct {
|
||||
// Root-level (always available).
|
||||
Root *Tree
|
||||
Log *olog.Logger
|
||||
ToolReg *toolsrv.Registry
|
||||
Skills *skills.Registry
|
||||
Models *ModelCache
|
||||
Elevate *elevate.Broker
|
||||
Invalidate func() // called by root ctl "invalidate"
|
||||
Shutdown func() // called by root ctl "kill"
|
||||
|
||||
// Session scope.
|
||||
Session *Session
|
||||
Remove func()
|
||||
Rename func(string) error
|
||||
SaveFn func([]byte) error
|
||||
|
||||
// Agent scope.
|
||||
Agent *agent.Agent
|
||||
AgentLog *AgentLog
|
||||
|
||||
// Generic binding data — type depends on context.
|
||||
Data any
|
||||
}
|
||||
|
||||
// ── Type aliases from fsedsl ─────────────────────────────────────
|
||||
|
||||
// FsNodeDecl describes one node in the 9P namespace.
|
||||
type FsNodeDecl = fsedsl.FsNodeDecl[HandlerCtx]
|
||||
|
||||
// NodeOption configures a FsNodeDecl at construction time.
|
||||
type NodeOption = fsedsl.NodeOption[HandlerCtx]
|
||||
|
||||
// Binding provides identity and data for one iteration of Each.
|
||||
type Binding = fsedsl.Binding[HandlerCtx]
|
||||
|
||||
// Tree is the filesystem tree implementation.
|
||||
type Tree = fsedsl.Tree
|
||||
|
||||
// File is an open handle to a single named blob.
|
||||
type File = fsedsl.File
|
||||
|
||||
// FileTree is a named collection of entries.
|
||||
type FileTree = fsedsl.FileTree
|
||||
|
||||
// FileConfig implements File via function pointers.
|
||||
type FileConfig = fsedsl.FileConfig
|
||||
|
||||
// SyntheticFileInfo implements os.FileInfo for entries with no backing file.
|
||||
type SyntheticFileInfo = fsedsl.SyntheticFileInfo
|
||||
|
||||
// SyntheticEntry implements os.DirEntry for entries with no backing file.
|
||||
type SyntheticEntry = fsedsl.SyntheticEntry
|
||||
|
||||
// TreeOption configures a Tree.
|
||||
type TreeOption = fsedsl.TreeOption
|
||||
|
||||
// ── Re-exported constants ────────────────────────────────────────
|
||||
|
||||
// Structural permissions — used by Tree for mounted children and synthetic files.
|
||||
const (
|
||||
PermChildDir = fsedsl.PermChildDir
|
||||
PermIdx = fsedsl.PermIdx
|
||||
PermMkdir = fsedsl.PermMkdir
|
||||
PermMkdirPrivate = fsedsl.PermMkdirPrivate
|
||||
)
|
||||
|
||||
// BindTarget declares what data an Each block binds to handlers.
|
||||
type BindTarget = fsedsl.BindTarget
|
||||
|
||||
const (
|
||||
BindData = fsedsl.BindData
|
||||
)
|
||||
|
|
@ -262,7 +262,7 @@ func sessionBindings(_ HandlerCtx) ([]Binding, error) {
|
|||
s := sess // capture
|
||||
out = append(out, Binding{
|
||||
Name: s.Name(),
|
||||
Aliases: []string{s.RunnableID()},
|
||||
Aliases: []string{s.ID()},
|
||||
UID: s.Uname(),
|
||||
GID: "agent",
|
||||
Applier: func(c HandlerCtx) HandlerCtx {
|
||||
|
|
@ -278,12 +278,10 @@ func sessionBindings(_ HandlerCtx) ([]Binding, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// ── Session file handlers (use ctx.Session) ──────────────────────
|
||||
|
||||
func readSessionEnv(ctx HandlerCtx) ([]byte, error) {
|
||||
s := ctx.Session
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", s.RunnableID())
|
||||
fmt.Fprintf(&sb, "OLLIE_SESSION_ID=%s\n", s.ID())
|
||||
for _, e := range os.Environ() {
|
||||
if strings.HasPrefix(e, "OLLIE_") && !strings.HasPrefix(e, "OLLIE_SESSION_ID=") {
|
||||
sb.WriteString(e)
|
||||
|
|
@ -349,7 +347,7 @@ func writeSessionName(ctx HandlerCtx, data []byte) error {
|
|||
}
|
||||
|
||||
func readSessionID(ctx HandlerCtx) ([]byte, error) {
|
||||
return []byte(ctx.Session.RunnableID()), nil
|
||||
return []byte(ctx.Session.ID()), nil
|
||||
}
|
||||
|
||||
func readSessionElevate(ctx HandlerCtx) ([]byte, error) {
|
||||
|
|
@ -416,8 +414,6 @@ func agentBindings(ctx HandlerCtx) ([]Binding, error) {
|
|||
return out, nil
|
||||
}
|
||||
|
||||
// ── Agent file handlers (use ctx.Agent, ctx.AgentLog, ctx.Session) ──
|
||||
|
||||
func writeAgentPrompt(ctx HandlerCtx, data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
if input == "" {
|
||||
|
|
|
|||
248
fs/lifecycle.go
248
fs/lifecycle.go
|
|
@ -15,24 +15,15 @@ import (
|
|||
"ollie/agent"
|
||||
"ollie/backend"
|
||||
"ollie/paths"
|
||||
"ollie/prompts"
|
||||
coresession "ollie/session"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// ── Event Bus (pubsub) ─────────────────────────────────────────
|
||||
|
||||
const eventRingCap = 100
|
||||
|
||||
// globalRootState is set by NewRoot so handler functions can access
|
||||
// the session map without going through ctx.Root (which is nil at
|
||||
// build time when closures are created).
|
||||
var globalRootState *RootState
|
||||
|
||||
// GlobalRootState returns the package-level root state for handler functions.
|
||||
// This is set by NewRoot and provides access to the session map.
|
||||
func GlobalRootState() *RootState { return globalRootState }
|
||||
|
||||
// RootState is the package-level state stored in root.Data.
|
||||
// Exported for use by fshandlers package.
|
||||
type RootState struct {
|
||||
|
|
@ -110,41 +101,6 @@ func WaitEvent(ctx context.Context, _ string) ([]byte, string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// PostEvent publishes an event. Converts old format to new topic format for compatibility.
|
||||
// Old: "A agentId state thinking" -> topic: "agent.agentId.state", payload: "thinking"
|
||||
// Old: "S sessionId pause" -> topic: "session.sessionId.pause", payload: ""
|
||||
func PostEvent(line string) {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
return
|
||||
}
|
||||
switch parts[0] {
|
||||
case "A":
|
||||
// Agent event: A <id> <action> [payload...]
|
||||
if len(parts) >= 3 {
|
||||
topic := "agent." + parts[1] + "." + parts[2]
|
||||
payload := ""
|
||||
if len(parts) > 3 {
|
||||
payload = strings.Join(parts[3:], " ")
|
||||
}
|
||||
PublishEvent(topic, payload)
|
||||
}
|
||||
case "S":
|
||||
// Session event: S <id> <action> [payload...]
|
||||
if len(parts) >= 3 {
|
||||
topic := "session." + parts[1] + "." + parts[2]
|
||||
payload := ""
|
||||
if len(parts) > 3 {
|
||||
payload = strings.Join(parts[3:], " ")
|
||||
}
|
||||
PublishEvent(topic, payload)
|
||||
}
|
||||
default:
|
||||
// Unknown format, publish as-is to catch-all
|
||||
PublishEvent("legacy", line)
|
||||
}
|
||||
}
|
||||
|
||||
// BuildIndex builds the session/idx content.
|
||||
//
|
||||
// Format: one line per session (not per agent)
|
||||
|
|
@ -201,8 +157,6 @@ func BuildIndex(rs *RootState) []byte {
|
|||
return []byte(sb.String())
|
||||
}
|
||||
|
||||
// ── Session lifecycle ───────────────────────────────────────────
|
||||
|
||||
// Kill terminates a session and removes it from the tree.
|
||||
func Kill(rs *RootState, id string) {
|
||||
rs.Mu.Lock()
|
||||
|
|
@ -359,29 +313,6 @@ func Lookup(root *Tree, id string) *Session {
|
|||
return nil
|
||||
}
|
||||
|
||||
// LookupByUname returns the session with the given uname, or nil.
|
||||
func LookupByUname(root *Tree, uname string) *Session {
|
||||
rs := root.Data.(*RootState)
|
||||
rs.Mu.RLock()
|
||||
defer rs.Mu.RUnlock()
|
||||
for _, sess := range rs.Sessions {
|
||||
if sess.uname == uname {
|
||||
return sess
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddSession inserts a pre-built session (used by tests).
|
||||
func AddSession(root *Tree, sess *Session) {
|
||||
rs := root.Data.(*RootState)
|
||||
rs.Mu.Lock()
|
||||
rs.Sessions[sess.RunnableID()] = sess
|
||||
rs.Mu.Unlock()
|
||||
}
|
||||
|
||||
// ── Create session/agent ────────────────────────────────────────
|
||||
|
||||
func nextUname(rs *RootState) string {
|
||||
b := make([]byte, 6)
|
||||
rand.Read(b)
|
||||
|
|
@ -559,116 +490,41 @@ func CreateAgent(rs *RootState, sessName string, args []string) error {
|
|||
return fmt.Errorf("sessions dir: %w", err)
|
||||
}
|
||||
|
||||
var dialOpts []toolsrv.Option
|
||||
if rs.Cfg.Yolo {
|
||||
dialOpts = append(dialOpts, toolsrv.WithYolo())
|
||||
}
|
||||
|
||||
var proc *toolsrv.Process
|
||||
var newToolServer func() toolsrv.Runner
|
||||
var promptEnv []string
|
||||
|
||||
if remoteTarget != "" {
|
||||
proc, err = toolsrv.SpawnRemote(rs.Cfg.Ctx, toolsrv.RemoteConfig{
|
||||
SSHTarget: remoteTarget,
|
||||
CWD: cwd,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("remote spawn: %w", err)
|
||||
}
|
||||
keeper := toolsrv.NewProcessKeeper(rs.Cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
|
||||
return toolsrv.SpawnRemote(ctx, toolsrv.RemoteConfig{SSHTarget: remoteTarget, CWD: cwd})
|
||||
})
|
||||
sess.keeper = keeper
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
promptEnv = []string{
|
||||
"PRIME_CWD=" + cwd,
|
||||
"PRIME_PLATFORM=" + proc.Info.Platform,
|
||||
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", proc.Info.IsGitRepo),
|
||||
}
|
||||
} else if sess.proc != nil {
|
||||
// Reuse the tool server spawned during Create.
|
||||
proc = sess.proc
|
||||
if sess.keeper != nil {
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := sess.keeper.Dial()
|
||||
if err != nil {
|
||||
rs.Cfg.Log.Error("toolsrv dial: %v", err)
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
} else {
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
return sess.toolsConn
|
||||
}
|
||||
}
|
||||
if cwc, ok := sess.toolsConn.(interface{ SetCWD(string) }); ok {
|
||||
cwc.SetCWD(cwd)
|
||||
}
|
||||
promptEnv = agent.PromptEnv(cwd)
|
||||
} else {
|
||||
proc, err = toolsrv.Spawn(rs.Cfg.Ctx, cwd, dialOpts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("local spawn: %w", err)
|
||||
}
|
||||
keeper := toolsrv.NewProcessKeeper(rs.Cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
|
||||
return toolsrv.Spawn(ctx, cwd, dialOpts...)
|
||||
})
|
||||
sess.keeper = keeper
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
rs.Cfg.Log.Error("toolsrv dial: %v", err)
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
promptEnv = agent.PromptEnv(cwd)
|
||||
}
|
||||
|
||||
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + uname}
|
||||
env = append(env, promptEnv...)
|
||||
|
||||
spOverride := systemPromptOverride
|
||||
if spOverride == "" && cfg != nil {
|
||||
spOverride = cfg.SystemPrompt
|
||||
}
|
||||
sysPrompt := prompts.ResolveSystemPrompt(spOverride)
|
||||
|
||||
envMap := make(map[string]string)
|
||||
for _, e := range env {
|
||||
if k, v, ok := strings.Cut(e, "="); ok {
|
||||
envMap[k] = v
|
||||
// Set up tool server infrastructure.
|
||||
// If session already has a proc from Create(), reuse it.
|
||||
var reuseFrom *InfraConfig
|
||||
if sess.proc != nil && remoteTarget == "" {
|
||||
reuseFrom = &InfraConfig{
|
||||
Proc: sess.proc,
|
||||
Keeper: sess.keeper,
|
||||
ToolsConn: sess.toolsConn,
|
||||
}
|
||||
}
|
||||
opModel := prompts.OperationalModel(true, false, envMap)
|
||||
platform := "linux"
|
||||
isGitRepo := false
|
||||
for _, e := range promptEnv {
|
||||
if k, v, ok := strings.Cut(e, "="); ok {
|
||||
switch k {
|
||||
case "PRIME_PLATFORM":
|
||||
platform = v
|
||||
case "PRIME_IS_GIT_REPO":
|
||||
isGitRepo = v == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||
|
||||
toolSrv := newToolServer()
|
||||
infra, err := SetupToolServer(ToolServerConfig{
|
||||
Ctx: rs.Cfg.Ctx,
|
||||
CWD: cwd,
|
||||
RemoteTarget: remoteTarget,
|
||||
Yolo: rs.Cfg.Yolo,
|
||||
ReuseFrom: reuseFrom,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update session's keeper if we spawned new infrastructure.
|
||||
if reuseFrom == nil {
|
||||
sess.keeper = infra.Keeper
|
||||
}
|
||||
|
||||
// Build prompt layers.
|
||||
layers := BuildPromptLayers(cfg, cwd, sessID, uname, infra.PromptEnv, systemPromptOverride)
|
||||
|
||||
toolSrv := infra.ToolsConn
|
||||
if toolSrv == nil {
|
||||
// Tool server is vital infrastructure — retry once with a fresh spawn.
|
||||
rs.Cfg.Log.Error("session %s: initial tool server dial failed, retrying...", sessName)
|
||||
if sess.keeper != nil {
|
||||
// Force respawn via keeper
|
||||
if conn, err := sess.keeper.Dial(); err == nil {
|
||||
toolSrv = conn
|
||||
}
|
||||
|
|
@ -677,17 +533,15 @@ func CreateAgent(rs *RootState, sessName string, args []string) error {
|
|||
return fmt.Errorf("tool server unavailable after retry (session infrastructure broken — try again)")
|
||||
}
|
||||
}
|
||||
// Load configured tools before building the runtime. BuildRuntime snapshots
|
||||
// the loaded-tool list to construct the agent's initial tool definitions.
|
||||
if cfg != nil {
|
||||
for _, tl := range cfg.AutoLoad {
|
||||
if err := loadToolOnRunner(toolSrv, tl); err != nil {
|
||||
rs.Cfg.Log.Error("session %s: autoLoad tool %q: %v", sessName, tl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock)
|
||||
// Load configured tools before building the runtime.
|
||||
LoadAutoLoadTools(cfg, toolSrv, func(f string, a ...any) {
|
||||
rs.Cfg.Log.Error("session %s: "+f, append([]any{sessName}, a...)...)
|
||||
})
|
||||
|
||||
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + uname}
|
||||
env = append(env, infra.PromptEnv...)
|
||||
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, layers.SystemPrompt, layers.OpModel, layers.EnvBlock)
|
||||
sess.toolsConn = toolSrv
|
||||
|
||||
al := NewAgentLog(remoteTarget)
|
||||
|
|
@ -703,9 +557,9 @@ func CreateAgent(rs *RootState, sessName string, args []string) error {
|
|||
CWD: cwd,
|
||||
Remote: remoteTarget,
|
||||
Runtime: rt,
|
||||
NewToolServer: newToolServer,
|
||||
PromptEnvExtra: promptEnv,
|
||||
BaseLayers: []string{sysPrompt, opModel, envBlock},
|
||||
NewToolServer: infra.NewToolServer,
|
||||
PromptEnvExtra: infra.PromptEnv,
|
||||
BaseLayers: layers.BaseLayers(),
|
||||
Log: rs.Cfg.Sink.NewLogger("core"),
|
||||
Output: NewEventHandler(al),
|
||||
ReadPlanStep: func() string {
|
||||
|
|
@ -716,7 +570,7 @@ func CreateAgent(rs *RootState, sessName string, args []string) error {
|
|||
sess.Core = core
|
||||
sess.uname = uname
|
||||
sess.setAgentLogLocked(uname, al)
|
||||
sess.proc = proc
|
||||
sess.proc = infra.Proc
|
||||
sess.mu.Unlock()
|
||||
} else {
|
||||
rt.Backend = be
|
||||
|
|
@ -728,9 +582,9 @@ func CreateAgent(rs *RootState, sessName string, args []string) error {
|
|||
AgentsDir: rs.Cfg.AgentsDir,
|
||||
AgentID: uname,
|
||||
CWD: cwd,
|
||||
BaseLayers: []string{sysPrompt, opModel, envBlock},
|
||||
PromptEnvExtra: promptEnv,
|
||||
NewToolServer: newToolServer,
|
||||
BaseLayers: layers.BaseLayers(),
|
||||
PromptEnvExtra: infra.PromptEnv,
|
||||
NewToolServer: infra.NewToolServer,
|
||||
NewBackend: backend.NewWithName,
|
||||
Output: NewEventHandler(al),
|
||||
Log: rs.Cfg.Sink.NewLogger("core"),
|
||||
|
|
@ -770,19 +624,3 @@ func CreateAgent(rs *RootState, sessName string, args []string) error {
|
|||
PublishEvent("session."+sessID+".agent."+ag.ID()+".new", ag.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAgentFromRoot is the public API for callers that have a *Tree.
|
||||
func CreateAgentFromRoot(root *Tree, sessName string, args []string) (string, error) {
|
||||
rs := root.Data.(*RootState)
|
||||
if err := CreateAgent(rs, sessName, args); err != nil {
|
||||
return "", err
|
||||
}
|
||||
rs.Mu.RLock()
|
||||
sess := rs.Sessions[sessName]
|
||||
rs.Mu.RUnlock()
|
||||
if sess == nil || sess.Core == nil || sess.Core.AgentCount() == 0 {
|
||||
return "", fmt.Errorf("agent not created")
|
||||
}
|
||||
agents := sess.Core.Agents()
|
||||
return agents[len(agents)-1].ID(), nil
|
||||
}
|
||||
|
|
|
|||
149
fs/persist.go
149
fs/persist.go
|
|
@ -10,21 +10,9 @@ import (
|
|||
|
||||
"ollie/agent"
|
||||
"ollie/backend"
|
||||
"ollie/prompts"
|
||||
coresession "ollie/session"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// KillFromRoot is the public API for callers with *Tree.
|
||||
func KillFromRoot(root *Tree, id string) {
|
||||
Kill(state(root), id)
|
||||
}
|
||||
|
||||
// RenameFromRoot is the public API for callers with *Tree.
|
||||
func RenameFromRoot(root *Tree, old, new string) error {
|
||||
return Rename(state(root), old, new)
|
||||
}
|
||||
|
||||
// --- Persistence ---
|
||||
|
||||
func activeSessionsDir(rs *RootState) string {
|
||||
|
|
@ -124,6 +112,7 @@ func restoreSession(rs *RootState, ps *agent.PersistedAgent) error {
|
|||
profile = "default"
|
||||
}
|
||||
sessID := ps.ID
|
||||
remoteTarget := ps.Remote
|
||||
|
||||
cfg := LoadAgentConfig(rs.Cfg.AgentsDir, profile, nil)
|
||||
|
||||
|
|
@ -150,101 +139,29 @@ func restoreSession(rs *RootState, ps *agent.PersistedAgent) error {
|
|||
if uname == "" {
|
||||
uname = nextUname(rs)
|
||||
}
|
||||
var newToolServer func() toolsrv.Runner
|
||||
var promptEnv []string
|
||||
var proc *toolsrv.Process
|
||||
remoteTarget := ps.Remote
|
||||
|
||||
var keeper *toolsrv.ProcessKeeper
|
||||
|
||||
if remoteTarget != "" {
|
||||
var spawnErr error
|
||||
proc, spawnErr = toolsrv.SpawnRemote(rs.Cfg.Ctx, toolsrv.RemoteConfig{
|
||||
SSHTarget: remoteTarget,
|
||||
CWD: cwd,
|
||||
})
|
||||
if spawnErr != nil {
|
||||
return fmt.Errorf("remote spawn: %w", spawnErr)
|
||||
}
|
||||
keeper = toolsrv.NewProcessKeeper(rs.Cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
|
||||
return toolsrv.SpawnRemote(ctx, toolsrv.RemoteConfig{SSHTarget: remoteTarget, CWD: cwd})
|
||||
})
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
promptEnv = []string{
|
||||
"PRIME_CWD=" + cwd,
|
||||
"PRIME_PLATFORM=" + proc.Info.Platform,
|
||||
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", proc.Info.IsGitRepo),
|
||||
}
|
||||
} else {
|
||||
var dialOpts []toolsrv.Option
|
||||
if rs.Cfg.Yolo {
|
||||
dialOpts = append(dialOpts, toolsrv.WithYolo())
|
||||
}
|
||||
var spawnErr error
|
||||
proc, spawnErr = toolsrv.Spawn(rs.Cfg.Ctx, cwd, dialOpts...)
|
||||
if spawnErr != nil {
|
||||
return fmt.Errorf("local spawn: %w", spawnErr)
|
||||
}
|
||||
keeper = toolsrv.NewProcessKeeper(rs.Cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
|
||||
return toolsrv.Spawn(ctx, cwd, dialOpts...)
|
||||
})
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
promptEnv = agent.PromptEnv(cwd)
|
||||
// Set up tool server infrastructure.
|
||||
infra, err := SetupToolServer(ToolServerConfig{
|
||||
Ctx: rs.Cfg.Ctx,
|
||||
CWD: cwd,
|
||||
RemoteTarget: remoteTarget,
|
||||
Yolo: rs.Cfg.Yolo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Build prompt layers.
|
||||
layers := BuildPromptLayers(cfg, cwd, sessID, uname, infra.PromptEnv, "")
|
||||
|
||||
// Load configured tools before building the runtime.
|
||||
LoadAutoLoadTools(cfg, infra.ToolsConn, func(f string, a ...any) {
|
||||
rs.Cfg.Log.Error("session %s: "+f, append([]any{sessID}, a...)...)
|
||||
})
|
||||
|
||||
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + uname}
|
||||
env = append(env, promptEnv...)
|
||||
|
||||
var spOverride string
|
||||
if cfg != nil {
|
||||
spOverride = cfg.SystemPrompt
|
||||
}
|
||||
sysPrompt := prompts.ResolveSystemPrompt(spOverride)
|
||||
envMap := make(map[string]string)
|
||||
for _, e := range env {
|
||||
if k, v, ok := strings.Cut(e, "="); ok {
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
opModel := prompts.OperationalModel(true, false, envMap)
|
||||
platform := "linux"
|
||||
isGitRepo := false
|
||||
for _, e := range promptEnv {
|
||||
if k, v, ok := strings.Cut(e, "="); ok {
|
||||
switch k {
|
||||
case "PRIME_PLATFORM":
|
||||
platform = v
|
||||
case "PRIME_IS_GIT_REPO":
|
||||
isGitRepo = v == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||
|
||||
toolSrv := newToolServer()
|
||||
// Load configured tools before building the runtime. BuildRuntime snapshots
|
||||
// the loaded-tool list to construct the restored agent's tool definitions.
|
||||
if cfg != nil {
|
||||
for _, tl := range cfg.AutoLoad {
|
||||
if err := loadToolOnRunner(toolSrv, tl); err != nil {
|
||||
rs.Cfg.Log.Error("session %s: autoLoad tool %q: %v", sessID, tl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock)
|
||||
env = append(env, infra.PromptEnv...)
|
||||
rt := agent.BuildRuntime(cfg, infra.ToolsConn, cwd, env, layers.SystemPrompt, layers.OpModel, layers.EnvBlock)
|
||||
|
||||
restoredSession := agent.RestoreHistory(ps)
|
||||
|
||||
|
|
@ -261,9 +178,9 @@ func restoreSession(rs *RootState, ps *agent.PersistedAgent) error {
|
|||
Remote: remoteTarget,
|
||||
History: restoredSession,
|
||||
Runtime: rt,
|
||||
NewToolServer: newToolServer,
|
||||
PromptEnvExtra: promptEnv,
|
||||
BaseLayers: []string{sysPrompt, opModel, envBlock},
|
||||
NewToolServer: infra.NewToolServer,
|
||||
PromptEnvExtra: infra.PromptEnv,
|
||||
BaseLayers: layers.BaseLayers(),
|
||||
Log: rs.Cfg.Sink.NewLogger("core"),
|
||||
Output: NewEventHandler(al),
|
||||
ReadPlanStep: func() string {
|
||||
|
|
@ -287,10 +204,9 @@ func restoreSession(rs *RootState, ps *agent.PersistedAgent) error {
|
|||
sessPtr = sess
|
||||
sess.uname = uname
|
||||
sess.SetAgentLog(uname, al)
|
||||
sess.proc = proc
|
||||
sess.keeper = keeper
|
||||
// Use the multiplexed connection for both agent execution and 9P management.
|
||||
sess.toolsConn = toolSrv
|
||||
sess.proc = infra.Proc
|
||||
sess.keeper = infra.Keeper
|
||||
sess.toolsConn = infra.ToolsConn
|
||||
|
||||
// Restore agent display name from persisted session.
|
||||
ag := sess.Core.FindAgent(uname)
|
||||
|
|
@ -300,23 +216,14 @@ func restoreSession(rs *RootState, ps *agent.PersistedAgent) error {
|
|||
|
||||
replayMessagesToLog(sess.AgentLog(), ps.Messages)
|
||||
|
||||
// Pre-load autoLoad tools into the remote.
|
||||
if cfg != nil {
|
||||
for _, tl := range cfg.AutoLoad {
|
||||
if err := sess.loadTool(tl, ag); err != nil {
|
||||
rs.Cfg.Log.Error("session %s: autoLoad tool %q: %v", sessID, tl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
name := sess.Name()
|
||||
rs.Mu.Lock()
|
||||
rs.Sessions[name] = sess
|
||||
rs.Mu.Unlock()
|
||||
|
||||
toolCount := 0
|
||||
if toolSrv != nil {
|
||||
if infos, err := toolSrv.ListTools(); err == nil {
|
||||
if infra.ToolsConn != nil {
|
||||
if infos, err := infra.ToolsConn.ListTools(); err == nil {
|
||||
toolCount = len(infos)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
// Package session builds the session/ file tree for the 9P namespace.
|
||||
// The tree IS the session collection — rootState holds the sessions map.
|
||||
// Package functions for lifecycle, lookup, and enumeration.
|
||||
package fs
|
||||
|
||||
import ()
|
||||
|
||||
// Get returns the Session for id, or nil.
|
||||
func Get(root *Tree, id string) *Session {
|
||||
return Lookup(root, id)
|
||||
}
|
||||
|
||||
// All returns all active sessions.
|
||||
func All(root *Tree) []*Session {
|
||||
rs := state(root)
|
||||
rs.Mu.RLock()
|
||||
defer rs.Mu.RUnlock()
|
||||
sessions := make([]*Session, 0, len(rs.Sessions))
|
||||
for _, sess := range rs.Sessions {
|
||||
sessions = append(sessions, sess)
|
||||
}
|
||||
return sessions
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"ollie/agent"
|
||||
"ollie/prompts"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// SessionInfra holds the tool server infrastructure for a session.
|
||||
// Created by SetupToolServer and used by both CreateAgent and restoreSession.
|
||||
type SessionInfra struct {
|
||||
Proc *toolsrv.Process
|
||||
Keeper *toolsrv.ProcessKeeper
|
||||
ToolsConn toolsrv.Runner
|
||||
NewToolServer func() toolsrv.Runner
|
||||
PromptEnv []string
|
||||
}
|
||||
|
||||
// ToolServerConfig configures tool server spawning.
|
||||
type ToolServerConfig struct {
|
||||
Ctx context.Context
|
||||
CWD string
|
||||
RemoteTarget string
|
||||
Yolo bool
|
||||
|
||||
// ReuseFrom, if non-nil, reuses existing infrastructure instead of spawning.
|
||||
// Used when a session already has a tool server from Create().
|
||||
ReuseFrom *InfraConfig
|
||||
}
|
||||
|
||||
// InfraConfig describes existing infrastructure to reuse.
|
||||
type InfraConfig struct {
|
||||
Proc *toolsrv.Process
|
||||
Keeper *toolsrv.ProcessKeeper
|
||||
ToolsConn toolsrv.Runner
|
||||
}
|
||||
|
||||
// SetupToolServer spawns the tool server process and creates the infrastructure.
|
||||
// For remote targets, it spawns via SSH. For local, it spawns a local process.
|
||||
// If ReuseInfra is set, reuses existing infrastructure instead of spawning.
|
||||
// Returns the infrastructure or an error.
|
||||
func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
|
||||
// Reuse existing infrastructure if provided.
|
||||
if cfg.ReuseFrom != nil {
|
||||
r := cfg.ReuseFrom
|
||||
var newToolServer func() toolsrv.Runner
|
||||
if r.Keeper != nil {
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := r.Keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
} else {
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
return r.ToolsConn
|
||||
}
|
||||
}
|
||||
// Update CWD if the connection supports it.
|
||||
if cwc, ok := r.ToolsConn.(interface{ SetCWD(string) }); ok {
|
||||
cwc.SetCWD(cfg.CWD)
|
||||
}
|
||||
return &SessionInfra{
|
||||
Proc: r.Proc,
|
||||
Keeper: r.Keeper,
|
||||
ToolsConn: newToolServer(),
|
||||
NewToolServer: newToolServer,
|
||||
PromptEnv: agent.PromptEnv(cfg.CWD),
|
||||
}, nil
|
||||
}
|
||||
var proc *toolsrv.Process
|
||||
var keeper *toolsrv.ProcessKeeper
|
||||
var newToolServer func() toolsrv.Runner
|
||||
var promptEnv []string
|
||||
var err error
|
||||
|
||||
if cfg.RemoteTarget != "" {
|
||||
proc, err = toolsrv.SpawnRemote(cfg.Ctx, toolsrv.RemoteConfig{
|
||||
SSHTarget: cfg.RemoteTarget,
|
||||
CWD: cfg.CWD,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("remote spawn: %w", err)
|
||||
}
|
||||
keeper = toolsrv.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
|
||||
return toolsrv.SpawnRemote(ctx, toolsrv.RemoteConfig{
|
||||
SSHTarget: cfg.RemoteTarget,
|
||||
CWD: cfg.CWD,
|
||||
})
|
||||
})
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
promptEnv = []string{
|
||||
"PRIME_CWD=" + cfg.CWD,
|
||||
"PRIME_PLATFORM=" + proc.Info.Platform,
|
||||
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", proc.Info.IsGitRepo),
|
||||
}
|
||||
} else {
|
||||
var dialOpts []toolsrv.Option
|
||||
if cfg.Yolo {
|
||||
dialOpts = append(dialOpts, toolsrv.WithYolo())
|
||||
}
|
||||
proc, err = toolsrv.Spawn(cfg.Ctx, cfg.CWD, dialOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("local spawn: %w", err)
|
||||
}
|
||||
keeper = toolsrv.NewProcessKeeper(cfg.Ctx, proc, func(ctx context.Context) (*toolsrv.Process, error) {
|
||||
return toolsrv.Spawn(ctx, cfg.CWD, dialOpts...)
|
||||
})
|
||||
newToolServer = func() toolsrv.Runner {
|
||||
conn, err := keeper.Dial()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return conn
|
||||
}
|
||||
promptEnv = agent.PromptEnv(cfg.CWD)
|
||||
}
|
||||
|
||||
toolsConn := newToolServer()
|
||||
|
||||
return &SessionInfra{
|
||||
Proc: proc,
|
||||
Keeper: keeper,
|
||||
ToolsConn: toolsConn,
|
||||
NewToolServer: newToolServer,
|
||||
PromptEnv: promptEnv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PromptLayers holds the resolved prompt components for an agent.
|
||||
type PromptLayers struct {
|
||||
SystemPrompt string
|
||||
OpModel string
|
||||
EnvBlock string
|
||||
}
|
||||
|
||||
// BuildPromptLayers resolves system prompt, operational model, and environment block.
|
||||
func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname string, promptEnv []string, systemPromptOverride string) PromptLayers {
|
||||
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + uname}
|
||||
env = append(env, promptEnv...)
|
||||
|
||||
spOverride := systemPromptOverride
|
||||
if spOverride == "" && cfg != nil {
|
||||
spOverride = cfg.SystemPrompt
|
||||
}
|
||||
sysPrompt := prompts.ResolveSystemPrompt(spOverride)
|
||||
|
||||
envMap := make(map[string]string)
|
||||
for _, e := range env {
|
||||
if k, v, ok := strings.Cut(e, "="); ok {
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
opModel := prompts.OperationalModel(true, false, envMap)
|
||||
|
||||
platform := "linux"
|
||||
isGitRepo := false
|
||||
for _, e := range promptEnv {
|
||||
if k, v, ok := strings.Cut(e, "="); ok {
|
||||
switch k {
|
||||
case "PRIME_PLATFORM":
|
||||
platform = v
|
||||
case "PRIME_IS_GIT_REPO":
|
||||
isGitRepo = v == "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
envBlock := prompts.Environment(cwd, platform, isGitRepo, "")
|
||||
|
||||
return PromptLayers{
|
||||
SystemPrompt: sysPrompt,
|
||||
OpModel: opModel,
|
||||
EnvBlock: envBlock,
|
||||
}
|
||||
}
|
||||
|
||||
// BaseLayers returns the three prompt layers as a slice for agent configuration.
|
||||
func (p PromptLayers) BaseLayers() []string {
|
||||
return []string{p.SystemPrompt, p.OpModel, p.EnvBlock}
|
||||
}
|
||||
|
||||
// LoadAutoLoadTools loads all tools from the agent config's autoLoad list.
|
||||
func LoadAutoLoadTools(cfg *agent.AgentConfig, runner toolsrv.Runner, logError func(string, ...any)) {
|
||||
if cfg == nil || runner == nil {
|
||||
return
|
||||
}
|
||||
for _, tl := range cfg.AutoLoad {
|
||||
if err := loadToolOnRunner(runner, tl); err != nil {
|
||||
if logError != nil {
|
||||
logError("autoLoad tool %q: %v", tl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
fs/spec.go
88
fs/spec.go
|
|
@ -4,24 +4,82 @@ package fs
|
|||
// Every file and directory is declared here with Doc() descriptions.
|
||||
// treeSpecUID/GID are set at NewRoot() time from the current system user.
|
||||
|
||||
import "ollie/fsedsl"
|
||||
import (
|
||||
"ollie/agent"
|
||||
"ollie/elevate"
|
||||
"ollie/fsedsl"
|
||||
olog "ollie/log"
|
||||
"ollie/skills"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
// HandlerCtx carries runtime dependencies for handler functions.
|
||||
// The wire step populates the correct subset per node scope.
|
||||
// Handlers assert their scope by checking which pointer is non-nil.
|
||||
type HandlerCtx struct {
|
||||
// Root-level (always available).
|
||||
Root *Tree
|
||||
Log *olog.Logger
|
||||
ToolReg *toolsrv.Registry
|
||||
Skills *skills.Registry
|
||||
Models *ModelCache
|
||||
Elevate *elevate.Broker
|
||||
Invalidate func() // called by root ctl "invalidate"
|
||||
Shutdown func() // called by root ctl "kill"
|
||||
|
||||
// Session scope.
|
||||
Session *Session
|
||||
Remove func()
|
||||
Rename func(string) error
|
||||
SaveFn func([]byte) error
|
||||
|
||||
// Agent scope.
|
||||
Agent *agent.Agent
|
||||
AgentLog *AgentLog
|
||||
|
||||
// Generic binding data — type depends on context.
|
||||
Data any
|
||||
}
|
||||
|
||||
// Type aliases from fsedsl, specialized to HandlerCtx.
|
||||
type (
|
||||
FsNodeDecl = fsedsl.FsNodeDecl[HandlerCtx]
|
||||
NodeOption = fsedsl.NodeOption[HandlerCtx]
|
||||
Binding = fsedsl.Binding[HandlerCtx]
|
||||
Tree = fsedsl.Tree
|
||||
File = fsedsl.File
|
||||
FileTree = fsedsl.FileTree
|
||||
FileConfig = fsedsl.FileConfig
|
||||
SyntheticFileInfo = fsedsl.SyntheticFileInfo
|
||||
SyntheticEntry = fsedsl.SyntheticEntry
|
||||
TreeOption = fsedsl.TreeOption
|
||||
BindTarget = fsedsl.BindTarget
|
||||
)
|
||||
|
||||
// Structural permissions — used by Tree for mounted children and synthetic files.
|
||||
const (
|
||||
PermChildDir = fsedsl.PermChildDir
|
||||
PermIdx = fsedsl.PermIdx
|
||||
PermMkdir = fsedsl.PermMkdir
|
||||
PermMkdirPrivate = fsedsl.PermMkdirPrivate
|
||||
BindData = fsedsl.BindData
|
||||
)
|
||||
|
||||
// Local aliases for fsedsl constructors, specialized to HandlerCtx.
|
||||
var (
|
||||
DirNode = fsedsl.DirNode[HandlerCtx]
|
||||
FileNode = fsedsl.FileNode[HandlerCtx]
|
||||
Each = fsedsl.Each[HandlerCtx]
|
||||
Doc = fsedsl.Doc[HandlerCtx]
|
||||
Read = fsedsl.Read[HandlerCtx]
|
||||
Write = fsedsl.Write[HandlerCtx]
|
||||
Stream = fsedsl.Stream[HandlerCtx]
|
||||
DirNode = fsedsl.DirNode[HandlerCtx]
|
||||
FileNode = fsedsl.FileNode[HandlerCtx]
|
||||
Each = fsedsl.Each[HandlerCtx]
|
||||
Doc = fsedsl.Doc[HandlerCtx]
|
||||
Read = fsedsl.Read[HandlerCtx]
|
||||
Write = fsedsl.Write[HandlerCtx]
|
||||
Stream = fsedsl.Stream[HandlerCtx]
|
||||
BlockOnce = fsedsl.BlockOnce[HandlerCtx]
|
||||
Request = fsedsl.Request[HandlerCtx]
|
||||
Remove = fsedsl.RemoveNode[HandlerCtx]
|
||||
Stat = fsedsl.StatOverride[HandlerCtx]
|
||||
UID = fsedsl.UID[HandlerCtx]
|
||||
GID = fsedsl.GID[HandlerCtx]
|
||||
Bind = fsedsl.Bind[HandlerCtx]
|
||||
Request = fsedsl.Request[HandlerCtx]
|
||||
Remove = fsedsl.RemoveNode[HandlerCtx]
|
||||
Stat = fsedsl.StatOverride[HandlerCtx]
|
||||
UID = fsedsl.UID[HandlerCtx]
|
||||
GID = fsedsl.GID[HandlerCtx]
|
||||
Bind = fsedsl.Bind[HandlerCtx]
|
||||
)
|
||||
|
||||
var treeSpecUID, treeSpecGID string
|
||||
|
|
|
|||
25
fs/types.go
25
fs/types.go
|
|
@ -191,23 +191,8 @@ func loadToolOnRunner(runner toolsrv.Runner, name string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetDisallowTools replaces the session's disallow tool list.
|
||||
func (sess *Session) SetDisallowTools(names []string) {
|
||||
sess.mu.Lock()
|
||||
defer sess.mu.Unlock()
|
||||
if len(names) == 0 {
|
||||
sess.disallowTools = nil
|
||||
return
|
||||
}
|
||||
sess.disallowTools = make(map[string]struct{}, len(names))
|
||||
for _, n := range names {
|
||||
sess.disallowTools[n] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (sess *Session) RunnableID() string { return sess.id }
|
||||
func (sess *Session) ID() string { return sess.id }
|
||||
func (sess *Session) Uname() string { return sess.uname }
|
||||
func (sess *Session) ID() string { return sess.id }
|
||||
func (sess *Session) Uname() string { return sess.uname }
|
||||
func (sess *Session) Name() string {
|
||||
sess.mu.RLock()
|
||||
defer sess.mu.RUnlock()
|
||||
|
|
@ -223,12 +208,6 @@ func (sess *Session) Cancel() {
|
|||
sess.cancel()
|
||||
}
|
||||
|
||||
func (sess *Session) Interrupt() {
|
||||
for _, ag := range sess.Core.Agents() {
|
||||
ag.Interrupt(agent.ErrInterrupted)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPaused returns true if the session is paused.
|
||||
func (sess *Session) IsPaused() bool {
|
||||
sess.mu.RLock()
|
||||
|
|
|
|||
Loading…
Reference in New Issue