diff --git a/fs/adapter.go b/fs/adapter.go deleted file mode 100644 index a3a81ce..0000000 --- a/fs/adapter.go +++ /dev/null @@ -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 -) diff --git a/fs/handlers.go b/fs/handlers.go index a6f254d..feb0488 100644 --- a/fs/handlers.go +++ b/fs/handlers.go @@ -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 == "" { diff --git a/fs/lifecycle.go b/fs/lifecycle.go index 25da86b..fad1950 100644 --- a/fs/lifecycle.go +++ b/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 [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 [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 -} diff --git a/fs/persist.go b/fs/persist.go index ceed0a1..92b0a2c 100644 --- a/fs/persist.go +++ b/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) } } diff --git a/fs/session.go b/fs/session.go deleted file mode 100644 index 28df0d4..0000000 --- a/fs/session.go +++ /dev/null @@ -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 -} diff --git a/fs/setup.go b/fs/setup.go new file mode 100644 index 0000000..83eb2d4 --- /dev/null +++ b/fs/setup.go @@ -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) + } + } + } +} diff --git a/fs/spec.go b/fs/spec.go index 6ff0970..72a7634 100644 --- a/fs/spec.go +++ b/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 diff --git a/fs/types.go b/fs/types.go index b2f1c65..299c41f 100644 --- a/fs/types.go +++ b/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()