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:
parent
b1cbb9ca7c
commit
9ef397045c
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue