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:
Levi Neely 2026-08-04 13:04:31 +02:00
parent 1f173f1b41
commit bf80a63e8a
8 changed files with 354 additions and 485 deletions

View File

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

View File

@ -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 == "" {

View File

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

View File

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

View File

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

205
fs/setup.go Normal file
View File

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

View File

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

View File

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