fs: add agent-level kill, remove activeAgentID from session
Add 'kill' command to agent ctl (removes agent from session). Add Session.RemoveAgent() method. Remove activeAgentID field, Agent(), SetActiveAgent, and dead WaitChange — 'active agent' is a GUI concept, not a server one. All callers updated to iterate Agents() or use FindAgent(id).
This commit is contained in:
parent
3f1e8bfd8b
commit
911733bd8a
|
|
@ -410,6 +410,11 @@ func handleAgentCtl(a *agent.Agent, s *Session, data []byte) error {
|
|||
return nil
|
||||
}
|
||||
switch cmd[0] {
|
||||
case "kill":
|
||||
if s.Core != nil {
|
||||
s.Core.RemoveAgent(a.ID())
|
||||
PostEvent("A kill session/" + s.Name() + "/agent/" + a.ID())
|
||||
}
|
||||
case "stop":
|
||||
a.Interrupt(agent.ErrInterrupted)
|
||||
case "compact":
|
||||
|
|
@ -426,12 +431,6 @@ func handleAgentCtl(a *agent.Agent, s *Session, data []byte) error {
|
|||
be.SetModel(strings.Join(cmd[1:], " "))
|
||||
}
|
||||
}
|
||||
case "agents":
|
||||
if len(cmd) > 1 {
|
||||
if s.Core != nil {
|
||||
s.Core.SetActiveAgent(strings.Join(cmd[1:], " "))
|
||||
}
|
||||
}
|
||||
case "name":
|
||||
if len(cmd) > 1 {
|
||||
a.SetName(strings.Join(cmd[1:], " "))
|
||||
|
|
|
|||
|
|
@ -179,9 +179,13 @@ func Rename(rs *rootState, old, new string) error {
|
|||
rs.mu.Unlock()
|
||||
return fmt.Errorf("session already exists: %s", new)
|
||||
}
|
||||
if sess.Core != nil && sess.Core.Agent().IsRunning() {
|
||||
rs.mu.Unlock()
|
||||
return fmt.Errorf("cannot rename while agent is running")
|
||||
if sess.Core != nil {
|
||||
for _, ag := range sess.Core.Agents() {
|
||||
if ag.IsRunning() {
|
||||
rs.mu.Unlock()
|
||||
return fmt.Errorf("cannot rename while agent is running")
|
||||
}
|
||||
}
|
||||
}
|
||||
sess.name = new
|
||||
rs.sessions[new] = sess
|
||||
|
|
@ -231,8 +235,15 @@ func waitIdle(rs *rootState, poll, timeout time.Duration) {
|
|||
allIdle := true
|
||||
rs.mu.RLock()
|
||||
for _, sess := range rs.sessions {
|
||||
if sess.Core != nil && sess.Core.Agent().State() != "idle" {
|
||||
allIdle = false
|
||||
if sess.Core != nil {
|
||||
for _, ag := range sess.Core.Agents() {
|
||||
if ag.State() != "idle" {
|
||||
allIdle = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !allIdle {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +263,9 @@ func InterruptAll(root *Tree) {
|
|||
defer rs.mu.RUnlock()
|
||||
for _, sess := range rs.sessions {
|
||||
if sess.Core != nil {
|
||||
sess.Core.Agent().Interrupt(agent.ErrInterrupted)
|
||||
for _, ag := range sess.Core.Agents() {
|
||||
ag.Interrupt(agent.ErrInterrupted)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -617,7 +630,7 @@ func CreateAgent(rs *rootState, sessName string, args []string) error {
|
|||
}
|
||||
rs.mu.Unlock()
|
||||
|
||||
ag := sess.Core.Agent()
|
||||
ag := sess.Core.FindAgent(uname)
|
||||
ag.Submit(sess.SessionCtx, "/agent "+profile)
|
||||
if name != "" {
|
||||
ag.SetName(name)
|
||||
|
|
@ -638,8 +651,9 @@ func CreateAgentFromRoot(root *Tree, sessName string, args []string) (string, er
|
|||
rs.mu.RLock()
|
||||
sess := rs.sessions[sessName]
|
||||
rs.mu.RUnlock()
|
||||
if sess == nil || sess.Core == nil {
|
||||
if sess == nil || sess.Core == nil || sess.Core.AgentCount() == 0 {
|
||||
return "", fmt.Errorf("agent not created")
|
||||
}
|
||||
return sess.Core.Agent().ID(), nil
|
||||
agents := sess.Core.Agents()
|
||||
return agents[len(agents)-1].ID(), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,14 +38,14 @@ func persistSession(rs *rootState, id string) {
|
|||
if !ok {
|
||||
return
|
||||
}
|
||||
if sess.Core == nil || sess.Core.Agent() == nil {
|
||||
if sess.Core == nil || sess.Core.AgentCount() == 0 {
|
||||
// Empty session — no agent to persist. Silently skip.
|
||||
return
|
||||
}
|
||||
dir := activeSessionsDir(rs)
|
||||
os.MkdirAll(dir, 0700)
|
||||
path := filepath.Join(dir, sess.id+".json")
|
||||
ag := sess.Core.Agent()
|
||||
ag := sess.Core.Agents()[0]
|
||||
if err := ag.SaveFull(path, sess.id, sess.Name(), sess.Core.CWD(), ""); err != nil {
|
||||
rs.cfg.Log.Error("persist session %s: %v", id, err)
|
||||
}
|
||||
|
|
@ -278,7 +278,7 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
|||
sess.toolsConn = toolSrv
|
||||
|
||||
// Restore agent display name from persisted session.
|
||||
ag := sess.Core.Agent()
|
||||
ag := sess.Core.FindAgent(uname)
|
||||
if ps.Agent != "" {
|
||||
ag.SetName(ps.Agent)
|
||||
}
|
||||
|
|
@ -287,7 +287,6 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error {
|
|||
|
||||
// Pre-load autoLoad tools into the remote.
|
||||
if cfg != nil {
|
||||
ag := sess.Core.Agent()
|
||||
for _, tl := range cfg.AutoLoad {
|
||||
if err := sess.loadTool(tl, ag); err != nil {
|
||||
rs.cfg.Log.Error("session %s: autoLoad tool %q: %v", sessID, tl, err)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ func listSessions(ctx HandlerCtx) ([]FsNodeDecl, error) {
|
|||
if err := CreateAgent(rs, s.Name(), args); err != nil {
|
||||
return nil, fmt.Errorf("create agent: %w", err)
|
||||
}
|
||||
return []byte(s.Core.Agent().ID() + "\n"), nil
|
||||
agents := s.Core.Agents()
|
||||
return []byte(agents[len(agents)-1].ID() + "\n"), nil
|
||||
})),
|
||||
|
||||
// {aid} — dynamic per-agent directory
|
||||
|
|
|
|||
10
fs/types.go
10
fs/types.go
|
|
@ -123,7 +123,11 @@ func (sess *Session) CachedListModels() string {
|
|||
sess.modelsMu.Unlock()
|
||||
|
||||
// Cache miss — fetch and store.
|
||||
models := sess.Core.Agent().ListModels()
|
||||
agents := sess.Core.Agents()
|
||||
if len(agents) == 0 {
|
||||
return ""
|
||||
}
|
||||
models := agents[0].ListModels()
|
||||
result := strings.Join(models, "\n")
|
||||
sess.modelsMu.Lock()
|
||||
sess.modelsCache = result
|
||||
|
|
@ -217,7 +221,9 @@ func (sess *Session) Cancel() {
|
|||
}
|
||||
|
||||
func (sess *Session) Interrupt() {
|
||||
sess.Core.Agent().Interrupt(agent.ErrInterrupted)
|
||||
for _, ag := range sess.Core.Agents() {
|
||||
ag.Interrupt(agent.ErrInterrupted)
|
||||
}
|
||||
}
|
||||
|
||||
// --- AgentLog methods ---
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
|
|
@ -43,8 +42,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// Session is the concrete session type. It owns session-level state and
|
||||
// delegates agent operations to its owned agents. Operations that act on
|
||||
// "the agent" (Agent(), CWD, etc.) use the agent identified by immutable activeAgentID.
|
||||
// delegates agent operations to its owned agents.
|
||||
type Session struct {
|
||||
id string
|
||||
envMu sync.RWMutex
|
||||
|
|
@ -52,9 +50,8 @@ type Session struct {
|
|||
plan []byte
|
||||
prevPrompt string
|
||||
|
||||
agents []*agent.Agent
|
||||
activeAgentID string // immutable ID of the agent returned by Agent()
|
||||
log *olog.Logger
|
||||
agents []*agent.Agent
|
||||
log *olog.Logger
|
||||
sessionsDir string
|
||||
listHandlers map[string]func() []string
|
||||
remote string
|
||||
|
|
@ -165,39 +162,10 @@ func New(cfg Config) *Session {
|
|||
})
|
||||
|
||||
a.agents = append(a.agents, ag)
|
||||
a.activeAgentID = ag.ID()
|
||||
ag.SetSessionEnv(a.id)
|
||||
return a
|
||||
}
|
||||
|
||||
// Agent returns the currently active agent by immutable ID.
|
||||
// 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.activeAgentID != "" {
|
||||
for _, ag := range a.agents {
|
||||
if ag.ID() == a.activeAgentID {
|
||||
return ag
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: first agent or match by ID
|
||||
return a.agents[0]
|
||||
}
|
||||
|
||||
// SetActiveAgent selects an agent by immutable ID.
|
||||
func (a *Session) SetActiveAgent(id string) error {
|
||||
for _, ag := range a.agents {
|
||||
if ag.ID() == id {
|
||||
a.activeAgentID = id
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("agent %q not found", id)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
|
@ -229,15 +197,30 @@ 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.
|
||||
// AddAgent appends an agent to the session.
|
||||
func (a *Session) AddAgent(ag *agent.Agent) {
|
||||
a.agents = append(a.agents, ag)
|
||||
if a.activeAgentID == "" {
|
||||
a.activeAgentID = ag.ID()
|
||||
}
|
||||
ag.SetSessionEnv(a.id)
|
||||
}
|
||||
|
||||
// RemoveAgent removes an agent by ID and closes it.
|
||||
// Returns false if the agent was not found.
|
||||
func (a *Session) RemoveAgent(id string) bool {
|
||||
idx := -1
|
||||
for i, ag := range a.agents {
|
||||
if ag.ID() == id {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return false
|
||||
}
|
||||
a.agents[idx].Close()
|
||||
a.agents = append(a.agents[:idx], a.agents[idx+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
// Close releases resources for this session.
|
||||
func (a *Session) Close() {
|
||||
a.log.Debug("Close() session=%q", a.id)
|
||||
|
|
@ -261,10 +244,10 @@ func (a *Session) SetEnv(key, value string) {
|
|||
}
|
||||
|
||||
// CWD returns the current working directory for tool execution
|
||||
// (from the primary agent).
|
||||
// (from the first agent).
|
||||
func (a *Session) CWD() string {
|
||||
if ag := a.Agent(); ag != nil {
|
||||
if c := ag.Cwd(); c != "" {
|
||||
if len(a.agents) > 0 {
|
||||
if c := a.agents[0].Cwd(); c != "" {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
|
@ -272,7 +255,7 @@ func (a *Session) CWD() string {
|
|||
return wd
|
||||
}
|
||||
|
||||
// SetCWD validates and sets the working directory on the primary agent.
|
||||
// SetCWD validates and sets the working directory on all agents.
|
||||
func (a *Session) SetCWD(dir string) error {
|
||||
dir = paths.ExpandHome(dir)
|
||||
if dir != "" {
|
||||
|
|
@ -280,7 +263,7 @@ func (a *Session) SetCWD(dir string) error {
|
|||
return fmt.Errorf("cwd: %w", err)
|
||||
}
|
||||
}
|
||||
if ag := a.Agent(); ag != nil {
|
||||
for _, ag := range a.agents {
|
||||
ag.SetCWD(dir)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -311,45 +294,13 @@ func (a *Session) SetSessionID(newID string) error {
|
|||
if _, err := os.Stat(oldTemp); err == nil {
|
||||
os.Rename(oldTemp, newTemp) //nolint:errcheck
|
||||
}
|
||||
if ag := a.Agent(); ag != nil {
|
||||
for _, ag := range a.agents {
|
||||
ag.SetSessionEnv(newID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitChange blocks until the named field changes from current.
|
||||
func (a *Session) WaitChange(ctx context.Context, field, current string) (string, bool) {
|
||||
ag := a.Agent()
|
||||
if ag == nil {
|
||||
return "", false
|
||||
}
|
||||
if field == agent.WatchState {
|
||||
return ag.WaitChange(ctx, field, current)
|
||||
}
|
||||
// Other fields — read via agent methods, use agent's change signal.
|
||||
read := func() string {
|
||||
switch field {
|
||||
case "usage":
|
||||
return ag.UsageStr()
|
||||
case "ctxsz":
|
||||
return ag.CtxSz()
|
||||
case "cwd":
|
||||
return a.CWD()
|
||||
case "agent":
|
||||
return ag.ID()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
stop := context.AfterFunc(ctx, func() { ag.BroadcastChange() })
|
||||
defer stop()
|
||||
for ctx.Err() == nil {
|
||||
if v := read(); v != current {
|
||||
return v, true
|
||||
}
|
||||
ag.WaitForChange(ctx)
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
|
||||
func (a *Session) activeSessionPath(id, suffix string) string {
|
||||
return filepath.Join(a.sessionsDir, "active", id+suffix)
|
||||
|
|
@ -381,7 +332,8 @@ func (a *Session) flushSave() {
|
|||
a.log.Error("session save: %v", err)
|
||||
return
|
||||
}
|
||||
if ag := a.Agent(); ag != nil {
|
||||
if len(a.agents) > 0 {
|
||||
ag := a.agents[0]
|
||||
if err := ag.SaveFull(path, a.id, "", a.CWD(), a.remote); err != nil {
|
||||
a.log.Error("session save: %v", err)
|
||||
}
|
||||
|
|
@ -391,8 +343,8 @@ func (a *Session) flushSave() {
|
|||
// SaveSession writes the current session state to the given path.
|
||||
// Returns nil if there is no agent (empty session).
|
||||
func (a *Session) SaveSession(path string) error {
|
||||
if ag := a.Agent(); ag != nil {
|
||||
return ag.SaveFull(path, a.id, "", a.CWD(), a.remote)
|
||||
if len(a.agents) > 0 {
|
||||
return a.agents[0].SaveFull(path, a.id, "", a.CWD(), a.remote)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue