diff --git a/fs/handlers.go b/fs/handlers.go index 4c2bd78..db6f356 100644 --- a/fs/handlers.go +++ b/fs/handlers.go @@ -13,6 +13,7 @@ import ( "ollie/backend" "ollie/elevate" "ollie/fsedsl" + "ollie/paths" "ollie/session" "ollie/toolsrv" ) @@ -240,6 +241,10 @@ func readSessionIdx(_ HandlerCtx) ([]byte, error) { return session.BuildIndex(), nil } +func readAgentIdx(ctx HandlerCtx) ([]byte, error) { + return session.BuildAgentIndex(ctx.Session.Core), nil +} + func requestSessionNew(_ HandlerCtx, data []byte) ([]byte, error) { args := strings.Fields(string(data)) name := "" @@ -586,7 +591,7 @@ func readAgentCfg(ctx HandlerCtx) ([]byte, error) { fmt.Fprintf(&sb, "id=%s\n", ctx.Agent.ID()) fmt.Fprintf(&sb, "profile=%s\n", ctx.Agent.Profile()) fmt.Fprintf(&sb, "displayName=%s\n", ctx.Agent.Name()) - fmt.Fprintf(&sb, "cwd=%s\n", ctx.Session.Core.CWD()) + fmt.Fprintf(&sb, "cwd=%s\n", ctx.Agent.Cwd()) fmt.Fprintf(&sb, "remote=%s\n", ctx.AgentLog.remote) fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens) fmt.Fprintf(&sb, "temperature=%v\n", p.Temperature) @@ -658,7 +663,7 @@ func writeAgentCtl(ctx HandlerCtx, data []byte) error { } func readAgentCwd(ctx HandlerCtx) ([]byte, error) { - return []byte(ctx.Session.Core.CWD() + "\n"), nil + return []byte(ctx.Agent.Cwd() + "\n"), nil } func writeAgentCwd(ctx HandlerCtx, data []byte) error { @@ -666,7 +671,12 @@ func writeAgentCwd(ctx HandlerCtx, data []byte) error { if input == "" { return nil } - return ctx.Session.Core.SetCWD(input) + input = paths.ExpandHome(input) + if _, err := os.Stat(input); err != nil { + return fmt.Errorf("cwd: %w", err) + } + ctx.Agent.SetCWD(input) + return nil } func readAgentUsage(ctx HandlerCtx) ([]byte, error) { diff --git a/fs/spec.go b/fs/spec.go index b7aabf4..dff957c 100644 --- a/fs/spec.go +++ b/fs/spec.go @@ -233,6 +233,11 @@ var treeSpec = DirNode("/", Request(requestAgentNew), ), + FileNode("idx", 0444, + Doc("Agent index. Format: nameidcwdbackendmodel per line."), + Read(readAgentIdx), + ), + Each("{aname}", agentBindings, Doc("Agent directory. Addressable by name or immutable ID."), diff --git a/kde b/kde index 2c7b5e6..73e262f 160000 --- a/kde +++ b/kde @@ -1 +1 @@ -Subproject commit 2c7b5e6fc54b0eb78244650e6049a7b5e77a38b9 +Subproject commit 73e262ffafe3cc32012e35e9f142261bd7707794 diff --git a/session/persist.go b/session/persist.go index 1ba50de..703f761 100644 --- a/session/persist.go +++ b/session/persist.go @@ -33,6 +33,7 @@ type PersistedAgent struct { Profile string `json:"profile"` Backend string `json:"backend"` Model string `json:"model"` + CWD string `json:"cwd"` Messages []backend.Message `json:"messages"` TaskState *agent.TaskState `json:"taskState,omitempty"` // Usage tracking @@ -65,7 +66,6 @@ func PersistSession(name string) error { ps := PersistedSession{ ID: sess.ID(), Name: sess.Name(), - CWD: sess.CWD(), Remote: sess.Remote(), Paused: sess.IsPaused(), } @@ -77,6 +77,7 @@ func PersistSession(name string) error { Profile: ag.Profile(), Backend: ag.BackendName(), Model: ag.ModelName(), + CWD: ag.Cwd(), Messages: ag.Messages(), TaskState: ag.TaskState(), } @@ -192,51 +193,66 @@ func restoreSessionFromFile(path string) (*RestoredSession, error) { } func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) { - cwd := ps.CWD - if cwd == "" { - cwd, _ = os.Getwd() - } - - // For paused sessions, skip tool server setup entirely. - // It will be set up on Resume(). - var infra *SessionInfra - if !ps.Paused { - var err error - infra, err = SetupToolServer(ToolServerConfig{ - Ctx: serverCtx, - CWD: cwd, - RemoteTarget: ps.Remote, - Yolo: pkgYolo, - }) - if err != nil { - return nil, err - } - } - - // Create empty session + // Create empty session first ctx, cancel := context.WithCancel(serverCtx) sess := NewEmpty(ps.ID, ctx, cancel) sess.SetName(ps.Name) sess.SetRemote(ps.Remote) - if infra != nil { - sess.SetProc(infra.Proc) - sess.SetKeeper(infra.Keeper) - sess.SetToolsConn(infra.ToolsConn) - } if ps.Paused { sess.SetPaused(true) } var restoredAgents []RestoredAgent - // Build prompt env (works without live infra) - promptEnv := agent.PromptEnv(cwd) - if infra != nil { - promptEnv = infra.PromptEnv - } - - // Restore each agent + // Restore each agent with its own CWD for _, pa := range ps.Agents { + // Get agent's CWD (fall back to session CWD for backwards compat, then pwd) + cwd := pa.CWD + if cwd == "" { + cwd = ps.CWD + } + if cwd == "" { + cwd, _ = os.Getwd() + } + + // For paused sessions, skip tool server setup + var infra *SessionInfra + if !ps.Paused { + var err error + // Reuse existing infra if available (from earlier agents) + var reuseFrom *InfraConfig + if sess.Proc() != nil { + reuseFrom = &InfraConfig{ + Proc: sess.Proc(), + Keeper: sess.Keeper(), + ToolsConn: sess.ToolsConn(), + } + } + infra, err = SetupToolServer(ToolServerConfig{ + Ctx: serverCtx, + CWD: cwd, + RemoteTarget: ps.Remote, + Yolo: pkgYolo, + ReuseFrom: reuseFrom, + }) + if err != nil { + pkgLog.Error("restore agent %s: setup tool server: %v", pa.ID, err) + continue + } + // Store infra on session (first agent sets it up) + if sess.Proc() == nil { + sess.SetProc(infra.Proc) + sess.SetKeeper(infra.Keeper) + sess.SetToolsConn(infra.ToolsConn) + } + } + + // Build prompt env + promptEnv := agent.PromptEnv(cwd) + if infra != nil { + promptEnv = infra.PromptEnv + } + cfg := LoadAgentConfig(pkgAgentsDir, pa.Profile, nil) be, err := backend.NewWithName(pa.Backend) @@ -311,8 +327,8 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) { Register(sess.Name(), sess) toolCount := 0 - if infra != nil && infra.ToolsConn != nil { - if infos, err := infra.ToolsConn.ListTools(); err == nil { + if sess.ToolsConn() != nil { + if infos, err := sess.ToolsConn().ListTools(); err == nil { toolCount = len(infos) } } diff --git a/session/registry.go b/session/registry.go index 6d909d9..257414f 100644 --- a/session/registry.go +++ b/session/registry.go @@ -242,10 +242,12 @@ func BuildIndex() []byte { for _, e := range entries { sess := e.sess connected := sess.IsConnected() - cwd := sess.CWD() - var be, model string + var cwd, be, model string var agentList []string for _, ag := range sess.Agents() { + if cwd == "" { + cwd = ag.Cwd() + } if be == "" { be = ag.BackendName() model = ag.ModelName() @@ -259,6 +261,17 @@ func BuildIndex() []byte { return []byte(sb.String()) } +// BuildAgentIndex builds the agent index for a session. +// Format: nameidcwdbackendmodel per line. +func BuildAgentIndex(sess *Session) []byte { + var sb strings.Builder + for _, ag := range sess.Agents() { + fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\n", + ag.Name(), ag.ID(), ag.Cwd(), ag.BackendName(), ag.ModelName()) + } + return []byte(sb.String()) +} + // CreateEmpty creates an empty session (no agent). // If remote is non-empty, spawns a remote tool server via SSH. func CreateEmpty(name, remote string) (*Session, error) { @@ -324,7 +337,7 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) { // Parse args profile := "default" - var agentName, backendName, modelName, remoteTarget string + var agentName, backendName, modelName, remoteTarget, cwdOverride string for _, arg := range args { if k, v, ok := strings.Cut(arg, "="); ok { switch k { @@ -336,6 +349,8 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) { modelName = v case "remote": remoteTarget = v + case "cwd": + cwdOverride = v } } else if profile == "default" { profile = arg @@ -346,7 +361,10 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) { uname := NextUname() sessID := sess.ID() - cwd := sess.CWD() + if cwdOverride == "" { + return nil, fmt.Errorf("cwd= is required when creating an agent") + } + cwd := cwdOverride // Set up tool server - reuse existing session infrastructure infra, err := SetupToolServer(ToolServerConfig{ @@ -377,14 +395,22 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) { env = append(env, infra.PromptEnv...) rt := agent.BuildRuntime(cfg, infra.ToolsConn, cwd, env, layers.SystemPrompt, layers.EnvBlock) - // Apply backend/model overrides to runtime - if backendName != "" { - if be, err := backend.NewWithName(backendName); err == nil { - rt.Backend = be - } + // Create backend - use override if provided, otherwise use config or env default + beName := backendName + if beName == "" { + beName = rt.CfgBackend } + be, err := backend.NewWithName(beName) + if err != nil { + return nil, fmt.Errorf("create backend: %w", err) + } + rt.Backend = be + + // Apply model override if provided if modelName != "" { rt.Backend.SetModel(modelName) + } else if rt.CfgModel != "" { + rt.Backend.SetModel(rt.CfgModel) } // Create agent diff --git a/session/session.go b/session/session.go index e45b791..0f07e8e 100644 --- a/session/session.go +++ b/session/session.go @@ -272,18 +272,6 @@ func (a *Session) SetEnv(key, value string) { } } -// CWD returns the current working directory for tool execution -// (from the first agent). -func (a *Session) CWD() string { - if len(a.agents) > 0 { - if c := a.agents[0].Cwd(); c != "" { - return c - } - } - wd, _ := os.Getwd() - return wd -} - // Remote returns the remote target (e.g., SSH host) if any. func (a *Session) Remote() string { return a.remote @@ -294,20 +282,6 @@ func (a *Session) SetRemote(remote string) { a.remote = remote } -// SetCWD validates and sets the working directory on all agents. -func (a *Session) SetCWD(dir string) error { - dir = paths.ExpandHome(dir) - if dir != "" { - if _, err := os.Stat(dir); err != nil { - return fmt.Errorf("cwd: %w", err) - } - } - for _, ag := range a.agents { - ag.SetCWD(dir) - } - return nil -} - // SetSessionID renames the session. func (a *Session) SetSessionID(newID string) error { oldID := a.id @@ -555,23 +529,41 @@ func (a *Session) Resume() error { } a.toolsConn = conn } else { - // Session was restored paused without infra - set up now. - cwd := a.CWD() - infra, err := SetupToolServer(ToolServerConfig{ - Ctx: ctx, - CWD: cwd, - RemoteTarget: a.remote, - Yolo: pkgYolo, - }) - if err != nil { - return fmt.Errorf("resume setup failed: %w", err) - } - a.proc = infra.Proc - a.keeper = infra.Keeper - a.toolsConn = infra.ToolsConn - - // Update agents with the new tool server connection. - for _, ag := range a.agents { + // Session was restored paused without infra - set up per-agent. + for i, ag := range a.agents { + cwd := ag.Cwd() + if cwd == "" { + cwd, _ = os.Getwd() + } + + // First agent spawns the tool server, others reuse + var reuseFrom *InfraConfig + if a.proc != nil { + reuseFrom = &InfraConfig{ + Proc: a.proc, + Keeper: a.keeper, + ToolsConn: a.toolsConn, + } + } + + infra, err := SetupToolServer(ToolServerConfig{ + Ctx: ctx, + CWD: cwd, + RemoteTarget: a.remote, + Yolo: pkgYolo, + ReuseFrom: reuseFrom, + }) + if err != nil { + return fmt.Errorf("resume setup for agent %s failed: %w", ag.ID(), err) + } + + // First agent sets session infra + if i == 0 { + a.proc = infra.Proc + a.keeper = infra.Keeper + a.toolsConn = infra.ToolsConn + } + ag.SetToolServer(infra.NewToolServer, infra.ToolsConn) } }