This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-9p/mgr/create.go

294 lines
7.5 KiB
Go

package mgr
import (
"ollie/session"
"ollie/agent"
"context"
"fmt"
"ollie/backend"
"ollie/tools"
"ollie/paths"
"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 []tools.Option
if !s.cfg.NoMount {
}
if s.cfg.Strict {
execOpts = append(execOpts, tools.WithStrict())
}
if s.cfg.Yolo {
execOpts = append(execOpts, tools.WithYolo())
}
if cfg != nil && len(cfg.AllowTools) > 0 {
execOpts = append(execOpts, tools.WithAllowTools(cfg.AllowTools))
}
if s.cfg.ToolRegistry != nil {
execOpts = append(execOpts, tools.WithToolRegistry(s.cfg.ToolRegistry, sessID))
}
if s.cfg.SkillsRegistry != nil {
execOpts = append(execOpts, tools.WithSkillsRegistry(s.cfg.SkillsRegistry))
}
var newToolServer func() tools.Runner
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 := tools.RemoteDial(s.cfg.Ctx, tools.RemoteConfig{
SSHTarget: remoteTarget,
CWD: cwd,
})
if dialErr != nil {
return "", fmt.Errorf("remote dial: %w", dialErr)
}
newToolServer = func() tools.Runner { return rsrv }
remoteEnv = []string{
"PRIME_CWD=" + cwd,
"PRIME_PLATFORM=" + rsrv.Info.Platform,
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", rsrv.Info.IsGitRepo),
}
} else {
newToolServer = tools.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, "")
toolSrv := newToolServer()
rt := agent.BuildRuntime(cfg, toolSrv, 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,
NewToolServer: newToolServer,
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)
},
})
}
sessionCtx, sessionCancel := context.WithCancel(s.cfg.Ctx)
sess := NewSession(sessID, core, sessionCtx, sessionCancel)
sessPtr = sess
sess.remote = remoteTarget
// For remote sessions, the mount isn't managed by a local tools.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.Agent().Submit(sessionCtx, "/agent "+agentName)
s.cfg.Log.Info("new session %s (backend=%s model=%s agent=%s)",
sessID, core.Agent().BackendName(), core.Agent().ModelName(), core.Agent().Name())
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.Agent().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.