298 lines
7.6 KiB
Go
298 lines
7.6 KiB
Go
package session
|
|
|
|
import (
|
|
agent "ollie/session"
|
|
"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 = agent.NewSessionID()
|
|
}
|
|
|
|
s.mu.RLock()
|
|
_, exists := s.sessions[sessID]
|
|
s.mu.RUnlock()
|
|
if exists {
|
|
return "", fmt.Errorf("session already exists: %s", sessID)
|
|
}
|
|
|
|
var core *agent.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 = agent.New(agent.Config{
|
|
Backend: be,
|
|
AgentName: agentName,
|
|
AgentsDir: s.cfg.AgentsDir,
|
|
SessionsDir: s.cfg.SessionsDir,
|
|
SessionID: sessID,
|
|
Uname: 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 agent.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.
|