From 9ef397045c613f42453d49d8ed834da81f4b5190 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sat, 1 Aug 2026 20:44:47 +0200 Subject: [PATCH] session: add multi-agent support by name, not index - Session.Agent() now looks up activeAgentName in the agents slice instead of returning agents[0]. Falls back to first agent if unset. - Added SetActiveAgent(), FindAgent(), SaveFuncs() methods. - New() and AddAgent() set activeAgentName automatically. - CreateAgent: refactored to ADD to existing core session instead of overwriting it. If sess.Core exists, builds the new agent via agent.NewAgent() and adds it via Core.AddAgent(). - agent/new now lists all agents in the session directory. - 9P path handlers (rootReaddir/Stat/Open) use FindAgent() and AgentLogFor() to resolve agents by name/ID instead of using the first agent exclusively. - Added AgentLogFor(id) to fs Session for per-agent log lookup. --- fs/session/create.go | 92 ++++++++++++++++++++++++++++++-------------- fs/session/root.go | 62 ++++++++++++++++++----------- fs/session/types.go | 9 ++++- session/session.go | 64 +++++++++++++++++++++++++----- 4 files changed, 165 insertions(+), 62 deletions(-) diff --git a/fs/session/create.go b/fs/session/create.go index 31776c6..064cd29 100644 --- a/fs/session/create.go +++ b/fs/session/create.go @@ -246,32 +246,65 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { rt := agent.BuildRuntime(cfg, toolSrv, cwd, env, sysPrompt, opModel, envBlock) al := NewAgentLog(remoteTarget) - core := coresession.New(coresession.Config{ - Backend: be, - AgentName: agentName, - AgentsDir: rs.cfg.AgentsDir, - SessionsDir: rs.cfg.SessionsDir, - SessionID: sessID, - AgentID: uname, - CWD: cwd, - Remote: remoteTarget, - Runtime: rt, - NewToolServer: newToolServer, - PromptEnvExtra: promptEnv, - BaseLayers: []string{sysPrompt, opModel, envBlock}, - Log: rs.cfg.Sink.NewLogger("core"), - Output: NewEventHandler(al), - ReadPlanStep: func() string { - return "" - }, -}) - sess.mu.Lock() - sess.Core = core - sess.uname = uname - sess.setAgentLogLocked(uname, al) - sess.proc = proc - sess.mu.Unlock() + if sess.Core == nil { + // No core session yet — create one with the first agent. + core := coresession.New(coresession.Config{ + Backend: be, + AgentName: agentName, + AgentsDir: rs.cfg.AgentsDir, + SessionsDir: rs.cfg.SessionsDir, + SessionID: sessID, + AgentID: uname, + CWD: cwd, + Remote: remoteTarget, + Runtime: rt, + NewToolServer: newToolServer, + PromptEnvExtra: promptEnv, + BaseLayers: []string{sysPrompt, opModel, envBlock}, + Log: rs.cfg.Sink.NewLogger("core"), + Output: NewEventHandler(al), + ReadPlanStep: func() string { + return "" + }, + }) + + sess.mu.Lock() + sess.Core = core + sess.uname = uname + sess.setAgentLogLocked(uname, al) + sess.proc = proc + sess.mu.Unlock() + } else { + // Core session exists — add another agent to it. + saveFn, flushFn := sess.Core.SaveFuncs() + ag := agent.NewAgent(agent.AgentCfg{ + History: nil, + Runtime: rt, + AgentName: agentName, + AgentsDir: rs.cfg.AgentsDir, + AgentID: uname, + CWD: cwd, + BaseLayers: []string{sysPrompt, opModel, envBlock}, + PromptEnvExtra: promptEnv, + NewToolServer: newToolServer, + NewBackend: backend.NewWithName, + Output: NewEventHandler(al), + Log: rs.cfg.Sink.NewLogger("core"), + AuditLog: rs.cfg.Sink.NewLogger("core").Sub("audit"), + SessionID: sessID, + ReadPlanStep: func() string { + return "" + }, + SaveSession: saveFn, + FlushSave: flushFn, + }) + sess.Core.AddAgent(ag) + + sess.mu.Lock() + sess.setAgentLogLocked(uname, al) + sess.mu.Unlock() + } // Register session in the map if not already there rs.mu.Lock() @@ -280,15 +313,16 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { } rs.mu.Unlock() - core.Agent().Submit(sess.SessionCtx, "/agent "+agentName) + ag := sess.Core.Agent() + ag.Submit(sess.SessionCtx, "/agent "+agentName) if agentAlias != "" { - core.Agent().SetName(agentAlias) + ag.SetName(agentAlias) } else { - core.Agent().SetName(uname) + ag.SetName(uname) } rs.cfg.Log.Info("created agent in session %s (backend=%s model=%s agent=%s)", - sessName, core.Agent().BackendName(), core.Agent().ModelName(), core.Agent().Name()) + sessName, ag.BackendName(), ag.ModelName(), ag.Name()) broadcastEvents() return nil } diff --git a/fs/session/root.go b/fs/session/root.go index c14edbb..a44f85f 100644 --- a/fs/session/root.go +++ b/fs/session/root.go @@ -274,14 +274,14 @@ func rootReaddir(rs *rootState, name string) ([]os.DirEntry, error) { if len(parts) >= 2 && parts[1] == "agent" { if len(parts) == 2 { entries := []os.DirEntry{FileEntry("new", 0666)} - if sess.Core != nil && sess.Core.Agent() != nil { - agentName := sess.Core.Agent().Name() - aid := sess.Core.Agent().ID() - dirName := aid - if agentName != "" { - dirName = agentName + if sess.Core != nil { + for _, ag := range sess.Core.Agents() { + dirName := ag.ID() + if n := ag.Name(); n != "" { + dirName = n + } + entries = append(entries, DirEntry(dirName, 0755)) } - entries = append(entries, DirEntry(dirName, 0755)) } return entries, nil } @@ -294,14 +294,16 @@ func rootReaddir(rs *rootState, name string) ([]os.DirEntry, error) { if sess.Core == nil || sess.Core.Agent() == nil { return nil, fmt.Errorf("no agent in session %s", sessID) } - if len(parts) >= 3 { - agentName := sess.Core.Agent().Name() - if agentName == "" || parts[2] != agentName { - return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID) - } + ag := sess.Core.FindAgent(parts[2]) + if ag == nil { + return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID) } if len(parts) == 3 { - afs := openAgentTree(rs, sess) + al := sess.AgentLogFor(ag.ID()) + if al == nil { + return nil, fmt.Errorf("agent log not found for %s", parts[2]) + } + afs := NewAgentTree(sess, al, rs.cfg.Log, nil, rs.cfg.InvalidateModels, rs.cfg.ToolRegistry) entries, err := afs.List() if err != nil { return nil, err @@ -310,7 +312,7 @@ func rootReaddir(rs *rootState, name string) ([]os.DirEntry, error) { return entries, nil } if len(parts) == 4 && parts[3] == "proc" { - procs := sess.Core.Agent().ListDetached() + procs := ag.ListDetached() entries := make([]os.DirEntry, len(procs)) for i, p := range procs { entries[i] = FileEntry(fmt.Sprintf("%d", p.PID), 0666) @@ -318,6 +320,7 @@ func rootReaddir(rs *rootState, name string) ([]os.DirEntry, error) { return entries, nil } } + return nil, fmt.Errorf("%s: not a directory", name) } @@ -344,11 +347,11 @@ func rootStat(rs *rootState, name string) (os.FileInfo, error) { return &SyntheticFileInfo{Name_: "new", Mode_: Perms[PathSessionFile].Files["new"]}, nil } if len(parts) >= 3 { - if sess.Core == nil || sess.Core.Agent() == nil { + if sess.Core == nil { return nil, fmt.Errorf("no agent in session %s", sessID) } - agentName := sess.Core.Agent().Name() - if agentName == "" || parts[2] != agentName { + ag := sess.Core.FindAgent(parts[2]) + if ag == nil { return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID) } } @@ -358,7 +361,15 @@ func rootStat(rs *rootState, name string) (os.FileInfo, error) { if len(parts) >= 4 && parts[3] == "proc" { return &SyntheticFileInfo{Name_: "proc", Mode_: 0755, IsDir_: true}, nil } - afs := openAgentTree(rs, sess) + ag := sess.Core.FindAgent(parts[2]) + if ag == nil { + return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID) + } + al := sess.AgentLogFor(ag.ID()) + if al == nil { + return nil, fmt.Errorf("agent log not found for %s", parts[2]) + } + afs := NewAgentTree(sess, al, rs.cfg.Log, nil, rs.cfg.InvalidateModels, rs.cfg.ToolRegistry) return afs.Stat(parts[3]) } sfs := openSessionTree(rs, sess) @@ -497,14 +508,21 @@ func rootOpen(rs *rootState, name string) (File, error) { if len(parts) < 4 { return nil, fmt.Errorf("%s: is a directory", name) } - if sess.Core == nil || sess.Core.Agent() == nil { + if sess.Core == nil { return nil, fmt.Errorf("no agent in session %s", sessID) } - agentName := sess.Core.Agent().Name() - if agentName == "" || parts[2] != agentName { + ag := sess.Core.FindAgent(parts[2]) + if ag == nil { return nil, fmt.Errorf("agent %s not found in session %s", parts[2], sessID) } - afs := openAgentTree(rs, sess) + if len(parts) == 4 { + return nil, fmt.Errorf("%s: is a directory", parts[3]) + } + al := sess.AgentLogFor(ag.ID()) + if al == nil { + return nil, fmt.Errorf("agent log not found for %s", parts[2]) + } + afs := NewAgentTree(sess, al, rs.cfg.Log, nil, rs.cfg.InvalidateModels, rs.cfg.ToolRegistry) return afs.Open(parts[3]) } diff --git a/fs/session/types.go b/fs/session/types.go index ebdc040..186fc1c 100644 --- a/fs/session/types.go +++ b/fs/session/types.go @@ -65,7 +65,7 @@ func NewSession(id string, core *session.Session, ctx context.Context, cancel co // AgentLog returns the first agent's log for backward compatibility. // The iteration order over the map is non-deterministic; when multi-agent -// support is fully wired, callers should use AgentLogs directly by key. +// support is fully wired, callers should use AgentLogFor or AgentLogs directly. func (sess *Session) AgentLog() *AgentLog { sess.mu.RLock() defer sess.mu.RUnlock() @@ -75,6 +75,13 @@ func (sess *Session) AgentLog() *AgentLog { return nil } +// AgentLogFor returns the AgentLog for the given agent ID, or nil. +func (sess *Session) AgentLogFor(agentID string) *AgentLog { + sess.mu.RLock() + defer sess.mu.RUnlock() + return sess.AgentLogs[agentID] +} + // SetAgentLog stores the agent log for the given agent ID. func (sess *Session) SetAgentLog(agentID string, al *AgentLog) { sess.mu.Lock() diff --git a/session/session.go b/session/session.go index 0a6a300..712258e 100644 --- a/session/session.go +++ b/session/session.go @@ -43,7 +43,8 @@ type Config struct { } // Session is the concrete session type. It owns session-level state and -// delegates agent operations to its owned agents. +// delegates agent operations to its owned agents. Operations that act on +// "the agent" (Agent(), CWD, etc.) use the agent identified by activeAgentName. type Session struct { id string envMu sync.RWMutex @@ -51,13 +52,14 @@ type Session struct { plan []byte prevPrompt string - agents []*agent.Agent - log *olog.Logger - sessionsDir string - listHandlers map[string]func() []string - remote string - mu sync.RWMutex - auditLog *olog.Logger + agents []*agent.Agent + activeAgentName string // name of the agent returned by Agent() + log *olog.Logger + sessionsDir string + listHandlers map[string]func() []string + remote string + mu sync.RWMutex + auditLog *olog.Logger saveMu sync.Mutex saveDirty bool @@ -163,18 +165,50 @@ func New(cfg Config) *Session { }) a.agents = append(a.agents, ag) + a.activeAgentName = ag.Name() ag.SetSessionEnv(a.id) return a } -// Agent returns the first (primary) agent. Backward compatible. +// Agent returns the currently active agent by name. +// If no active agent is set, returns the first agent (backward compatible). func (a *Session) Agent() *agent.Agent { if len(a.agents) == 0 { return nil } + if a.activeAgentName != "" { + for _, ag := range a.agents { + if ag.Name() == a.activeAgentName { + return ag + } + } + } + // Fallback: first agent or match by ID return a.agents[0] } +// SetActiveAgent sets the name of the active agent. Returns nil if found, +// or an error if no agent with that name exists. +func (a *Session) SetActiveAgent(name string) error { + for _, ag := range a.agents { + if ag.Name() == name { + a.activeAgentName = name + return nil + } + } + return fmt.Errorf("agent %q not found", name) +} + +// FindAgent returns the agent matching the given name or ID, or nil. +func (a *Session) FindAgent(nameOrID string) *agent.Agent { + for _, ag := range a.agents { + if ag.Name() == nameOrID || ag.ID() == nameOrID { + return ag + } + } + return nil +} + // Agents returns the full agent slice. func (a *Session) Agents() []*agent.Agent { return a.agents } @@ -189,9 +223,19 @@ func (a *Session) AgentAt(idx int) *agent.Agent { // AgentCount returns the number of agents. func (a *Session) AgentCount() int { return len(a.agents) } -// AddAgent appends an agent to the session. +// SaveFuncs returns save and flush callbacks suitable for use as +// agent.AgentCfg.SaveSession and FlushSave when adding agents +// to this session. +func (a *Session) SaveFuncs() (save func(), flush func()) { + return a.saveSession, a.flushSave +} + +// AddAgent appends an agent to the session and sets it as the active agent. func (a *Session) AddAgent(ag *agent.Agent) { a.agents = append(a.agents, ag) + if a.activeAgentName == "" { + a.activeAgentName = ag.Name() + } ag.SetSessionEnv(a.id) }