package mgr import ( "ollie/session" "ollie/agent" "context" "fmt" "ollie/backend" "ollie/execute" "ollie/paths" "ollie/tools" "olliesrv/fs" "olliesrv/prompts" "os" "strings" ) func (s *Manager) CreateSession(args []string) (string, error) { name := "" backendOverride := "" modelOverride := "" agentName := "" cwd := "" remoteTarget := "" systemPromptOverride := "" for _, arg := range args { k, v, ok := strings.Cut(arg, "=") if !ok { return "", fmt.Errorf("invalid option %q (expected key=value)", arg) } if v == "" { continue } switch k { case "name": name = v case "backend": backendOverride = v case "model": modelOverride = v case "agent": agentName = v case "cwd": cwd = v case "remote": remoteTarget = v case "system_prompt": systemPromptOverride = v default: return "", fmt.Errorf("unknown option %q (valid: name, backend, model, agent, cwd, remote, system_prompt)", k) } } if agentName == "" { if v := os.Getenv("OLLIE_DEFAULT_AGENT"); v != "" { agentName = v } else { agentName = "default" } } cwd = paths.ExpandHome(os.ExpandEnv(cwd)) if cwd == "" { return "", fmt.Errorf("cwd is required (e.g. new cwd=/path/to/project)") } sessID := name if sessID == "" { sessID = session.NewSessionID() } s.mu.RLock() _, exists := s.sessions[sessID] s.mu.RUnlock() if exists { return "", fmt.Errorf("session already exists: %s", sessID) } var core *session.Session var sessPtr *Session uname := s.nextUname() if s.cfg.NewCore != nil { var err error core, err = s.cfg.NewCore(sessID, agentName, cwd) if err != nil { return "", err } } else { cfg := LoadAgentConfig(s.cfg.AgentsDir, agentName, nil) if cfg != nil { if backendOverride == "" && cfg.Backend != "" { backendOverride = cfg.Backend } if modelOverride == "" && cfg.Model != "" { modelOverride = cfg.Model } } be, err := backend.NewWithName(backendOverride) if err != nil { return "", fmt.Errorf("backend: %w", err) } if modelOverride == "" { modelOverride = os.Getenv("OLLIE_MODEL") } if modelOverride != "" { be.SetModel(modelOverride) } if err := s.cfg.MkdirAll(s.cfg.SessionsDir, fs.PermMkdirPrivate); err != nil { return "", fmt.Errorf("sessions dir: %w", err) } var execOpts []execute.Option if !s.cfg.NoMount { } if s.cfg.Strict { execOpts = append(execOpts, execute.WithStrict()) } if s.cfg.Yolo { execOpts = append(execOpts, execute.WithYolo()) } if cfg != nil && len(cfg.AllowTools) > 0 { execOpts = append(execOpts, execute.WithAllowTools(cfg.AllowTools)) } if s.cfg.ToolRegistry != nil { execOpts = append(execOpts, execute.WithToolRegistry(s.cfg.ToolRegistry, sessID)) } if s.cfg.SkillsRegistry != nil { execOpts = append(execOpts, execute.WithSkillsRegistry(s.cfg.SkillsRegistry)) } var newDisp func() tools.Dispatcher var remoteEnv []string // extra env vars from remote host info if remoteTarget != "" { // Dial eagerly so we get HostInfo for prompt resolution. // FIXME: Eager dial means session creation blocks on SSH connect. // Eventually make this async or move host info into the prompt // scripts themselves (running remotely via the RPC). rsrv, dialErr := execute.RemoteDial(context.Background(), execute.RemoteConfig{ SSHTarget: remoteTarget, CWD: cwd, }) if dialErr != nil { return "", fmt.Errorf("remote dial: %w", dialErr) } newDisp = tools.NewDispatcherFunc(map[string]func() tools.Server{ "execute": func() tools.Server { return rsrv }, }) remoteEnv = []string{ "PRIME_CWD=" + cwd, "PRIME_PLATFORM=" + rsrv.Info.Platform, "PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", rsrv.Info.IsGitRepo), } } else { newDisp = tools.NewDispatcherFunc(map[string]func() tools.Server{ "execute": execute.Decl(cwd, execOpts...), }) } var promptEnv []string if len(remoteEnv) > 0 { promptEnv = remoteEnv } else { promptEnv = agent.PromptEnv(cwd) } env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + uname} env = append(env, promptEnv...) // Compose base layers: system prompt + operational model + environment. 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(s.cfg.Enable9P, s.cfg.EnableDBus, envMap) // Determine environment values for the environment layer. 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, "") disp := newDisp() rt := agent.BuildRuntime(cfg, disp, cwd, env, sysPrompt, opModel, envBlock) // sessPtr is set after NewSession; the ReadPlanStep closure captures it. core = session.New(session.Config{ Backend: be, AgentName: agentName, AgentsDir: s.cfg.AgentsDir, SessionsDir: s.cfg.SessionsDir, SessionID: sessID, AgentID: uname, CWD: cwd, Remote: remoteTarget, Runtime: rt, NewDispatcher: newDisp, PromptEnvExtra: promptEnv, BaseLayers: []string{sysPrompt, opModel, envBlock}, Log: s.cfg.Sink.NewLogger("core"), ReadPlanStep: func() string { if sessPtr == nil { return "" } sessPtr.mu.RLock() data := make([]byte, len(sessPtr.plan)) copy(data, sessPtr.plan) sessPtr.mu.RUnlock() return session.NextUncheckedStep(data) }, }) } ctx, cancel := context.WithCancel(context.Background()) sess := NewSession(sessID, core, ctx, cancel) sessPtr = sess sess.remote = remoteTarget // For remote sessions, the mount isn't managed by a local execute.Server // (which doesn't exist). Start it directly so $OLLIE is available for s.mu.Lock() sess.uname = uname s.sessions[sessID] = sess s.mu.Unlock() // HACK: Load the agent now that the session mount is active. core.Submit(ctx, "/agent "+agentName) s.cfg.Log.Info("new session %s (backend=%s model=%s agent=%s)", sessID, core.BackendName(), core.ModelName(), core.AgentName()) if s.cfg.OnSessionCreated != nil { s.cfg.OnSessionCreated(sessID, sess) } return sessID, nil } // RenameSession renames a session (exported for D-Bus adapter). func (s *Manager) RenameSession(old, new string) error { return s.renameSession(old, new) } func (s *Manager) renameSession(old, new string) error { // Session rename: {oldID} -> {newID} oldID := old newID := new s.mu.Lock() sess, ok := s.sessions[oldID] if !ok { s.mu.Unlock() return fmt.Errorf("session not found: %s", oldID) } if _, exists := s.sessions[newID]; exists { s.mu.Unlock() return fmt.Errorf("session already exists: %s", newID) } if sess.Core.IsRunning() { s.mu.Unlock() return fmt.Errorf("cannot rename while agent is running") } if err := sess.Core.SetSessionID(newID); err != nil { s.mu.Unlock() return err } sess.id = newID s.sessions[newID] = sess delete(s.sessions, oldID) s.mu.Unlock() sess.AppendLog([]byte(fmt.Sprintf(":: session renamed: %s -> %s\n", oldID, newID))) s.cfg.Log.Info("renamed session %s -> %s", oldID, newID) if s.cfg.OnSessionRenamed != nil { s.cfg.OnSessionRenamed(oldID, newID) } return nil } // LoadAgentConfig resolves and loads the config for a named agent. // Returns nil if the config file does not exist; BuildRuntime handles nil configs.