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.
This commit is contained in:
Ollie Agent 2026-08-01 20:44:47 +02:00
parent b1cbb9ca7c
commit 9ef397045c
4 changed files with 165 additions and 62 deletions

View File

@ -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
}

View File

@ -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])
}

View File

@ -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()

View File

@ -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)
}