From 119e673299695b868b6773d079c1f4c3018cf692 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sun, 2 Aug 2026 22:10:00 +0200 Subject: [PATCH] fix naming problems --- agent/agent.go | 74 ++++++++++++++++++++++++++++------------------ agent/commands.go | 6 ++-- agent/history.go | 14 +++++---- agent/new.go | 7 +++-- agent/new.go.bak | 60 +++++++++++++++++++++++++++++++++++++ agent/turn.go | 4 ++- doc/CORE.md | 8 +++-- doc/MULTI_AGENT.md | 4 +-- fs/agentfiles.go | 74 +++++++++++++++++++++++++--------------------- fs/builder.go | 7 +++++ fs/fsnode.go | 3 +- fs/lifecycle.go | 40 ++++++++++--------------- fs/newroot.go | 2 +- fs/persist.go | 33 ++++++++++++++++----- fs/sessionfiles.go | 4 +-- session/session.go | 51 ++++++++++++++++---------------- 16 files changed, 250 insertions(+), 141 deletions(-) create mode 100644 agent/new.go.bak diff --git a/agent/agent.go b/agent/agent.go index ee769de..aa70d2b 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -23,7 +23,8 @@ type Agent struct { history *History runtime *Runtime cfg agentConfig // per-turn config built from runtime - agentName string + name string // display name (defaults to id) + profile string // config profile name (e.g. "default") agentsDir string baseLayers []string // system prompt layers for /agent reloads promptEnvExtra []string // PRIME_* vars for prompt resolution @@ -34,28 +35,28 @@ type Agent struct { resultCache sync.Map // Execution state — owned by the agent, protected by stateMu. - state string // "idle", "thinking", "calling: " - reply string // last assistant response - cwd string // working directory for tool execution - id string // agent identity (unique principal) - fifo Fifo // prompt queue + state string // "idle", "thinking", "calling: " + reply string // last assistant response + cwd string // working directory for tool execution + id string // agent identity (unique principal) + fifo Fifo // prompt queue toolCallCount atomic.Int64 pendingInject atomic.Pointer[string] - submitMu sync.Mutex // serializes Submit calls (commands + turns) - stateMu sync.RWMutex - changeMu sync.Mutex - changeCond *sync.Cond + submitMu sync.Mutex // serializes Submit calls (commands + turns) + stateMu sync.RWMutex + changeMu sync.Mutex + changeCond *sync.Cond // Event handler — receives events from this agent. - output EventHandler - log *olog.Logger - auditLog *olog.Logger - sessionID string // the owning session's ID + output EventHandler + log *olog.Logger + auditLog *olog.Logger + sessionID string // the owning session's ID startupMessages []string - readPlanStep func() string - saveSession func() // trigger debounced persistence - flushSave func() // immediately flush persistence - turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests + readPlanStep func() string + saveSession func() // trigger debounced persistence + flushSave func() // immediately flush persistence + turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests } // Backend returns the active backend from the runtime. @@ -63,13 +64,16 @@ func (ag *Agent) Backend() backend.Backend { return ag.runtime.Backend } -// Name returns the agent's name. -func (ag *Agent) Name() string { return ag.agentName } +// Name returns the agent's display name. +func (ag *Agent) Name() string { return ag.name } + +// Profile returns the config profile name (e.g. "default"). +func (ag *Agent) Profile() string { return ag.profile } // SetName changes the agent's display name without reloading config. func (ag *Agent) SetName(name string) { ag.stateMu.Lock() - ag.agentName = name + ag.name = name ag.stateMu.Unlock() } @@ -250,7 +254,7 @@ func (ag *Agent) SaveTo(path, name, cwd string) error { if ag.history == nil { return fmt.Errorf("no active session") } - return ag.history.saveTo(path, name, ag.agentName, cwd) + return ag.history.saveTo(path, name, ag.Name(), cwd) } // Restore restores agent history from a persisted session. @@ -363,7 +367,7 @@ func (ag *Agent) SaveFull(path, sessionID, cwd, remote string) error { if ag.history == nil { return fmt.Errorf("no active session") } - return ag.history.saveToFull(path, sessionID, ag.agentName, + return ag.history.saveToFull(path, sessionID, ag.Name(), ag.profile, ag.runtime.Backend.Name(), ag.runtime.Backend.Model(), cwd, remote) } @@ -521,11 +525,21 @@ func (ag *Agent) ListDetached() []DetachedInfo { for _, r := range raw { if m, ok := r.(map[string]any); ok { di := DetachedInfo{} - if v, ok := m["pid"].(int); ok { di.PID = v } - if v, ok := m["command"].(string); ok { di.Command = v } - if v, ok := m["started"].(int64); ok { di.Started = v } - if v, ok := m["exited"].(bool); ok { di.Exited = v } - if v, ok := m["exit_code"].(int); ok { di.ExitCode = v } + if v, ok := m["pid"].(int); ok { + di.PID = v + } + if v, ok := m["command"].(string); ok { + di.Command = v + } + if v, ok := m["started"].(int64); ok { + di.Started = v + } + if v, ok := m["exited"].(bool); ok { + di.Exited = v + } + if v, ok := m["exit_code"].(int); ok { + di.ExitCode = v + } out = append(out, di) } } @@ -535,7 +549,9 @@ func (ag *Agent) ListDetached() []DetachedInfo { // SignalDetached sends a signal to a detached process. func (ag *Agent) SignalDetached(pid int, signal int) error { if srv := ag.execServer(); srv != nil { - type signaler interface{ SignalDetached(int, syscall.Signal) error } + type signaler interface { + SignalDetached(int, syscall.Signal) error + } if sg, ok := srv.(signaler); ok { return sg.SignalDetached(pid, syscall.Signal(signal)) } diff --git a/agent/commands.go b/agent/commands.go index cf820bd..c76ac2b 100644 --- a/agent/commands.go +++ b/agent/commands.go @@ -146,7 +146,7 @@ func (ag *Agent) cmdAgents(_ context.Context, _ []string) { } seen[name] = true marker := " " - if name == ag.agentName { + if name == ag.profile { marker = "* " } ag.emit(infoEvent(marker + name)) @@ -160,7 +160,7 @@ func (ag *Agent) cmdAgents(_ context.Context, _ []string) { func (ag *Agent) cmdAgent(_ context.Context, args []string) { if len(args) == 0 { - ag.emit(infoEvent("active agent: " + ag.agentName)) + ag.emit(infoEvent("active profile: " + ag.profile + " (agent: " + ag.Name() + ")")) return } if ag.IsRunning() { @@ -201,7 +201,7 @@ func (ag *Agent) cmdAgent(_ context.Context, args []string) { } } ag.runtime = rt - ag.agentName = name + ag.profile = name ag.history = nil ag.wireToolsChanged() ag.notifyChange() diff --git a/agent/history.go b/agent/history.go index 5925193..aed4ada 100644 --- a/agent/history.go +++ b/agent/history.go @@ -2,8 +2,8 @@ package agent import ( "context" - "encoding/json" "crypto/rand" + "encoding/json" "fmt" "os" "slices" @@ -38,6 +38,7 @@ Be concise. Capture what another LLM needs to seamlessly continue.` type PersistedAgent struct { ID string `json:"id"` Agent string `json:"agent,omitempty"` + Profile string `json:"profile,omitempty"` Backend string `json:"backend,omitempty"` Model string `json:"model,omitempty"` CWD string `json:"cwd,omitempty"` @@ -95,8 +96,8 @@ func (ts *TaskState) render() string { } // saveTo writes the full message history to path as JSON. -func (s *History) saveTo(path, id, agentName, cwd string) error { - return s.saveToFull(path, id, agentName, "", "", cwd, "") +func (s *History) saveTo(path, id, displayName, cwd string) error { + return s.saveToFull(path, id, displayName, "", "", "", cwd, "") } // saveToFull writes session state including backend/model info. @@ -117,10 +118,11 @@ func sanitizeMessages(msgs []backend.Message) []backend.Message { return out } -func (s *History) saveToFull(path, id, agentName, backendName, modelName, cwd, remote string) error { +func (s *History) saveToFull(path, id, displayName, profile, backendName, modelName, cwd, remote string) error { ps := PersistedAgent{ ID: id, - Agent: agentName, + Agent: displayName, + Profile: profile, Backend: backendName, Model: modelName, CWD: cwd, @@ -654,7 +656,7 @@ func (s *History) contextDebug() string { // NewResponseID generates a UUIDv4 for a single assistant response. func NewResponseID() string { b := make([]byte, 16) - rand.Read(b) //nolint:errcheck + rand.Read(b) //nolint:errcheck b[6] = (b[6] & 0x0f) | 0x40 // version 4 b[8] = (b[8] & 0x3f) | 0x80 // variant 10xx return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", diff --git a/agent/new.go b/agent/new.go index db1e9a6..3c40741 100644 --- a/agent/new.go +++ b/agent/new.go @@ -12,9 +12,9 @@ import ( type AgentCfg struct { History *History Runtime *Runtime - AgentName string + Profile string // config profile name (e.g. "default" → agents/default.json) AgentsDir string - AgentID string // unique agent identity + AgentID string // unique agent identity (uname) CWD string // working directory for tool execution BaseLayers []string PromptEnvExtra []string @@ -35,7 +35,8 @@ func NewAgent(cfg AgentCfg) *Agent { ag := &Agent{ history: cfg.History, runtime: cfg.Runtime, - agentName: cfg.AgentName, + profile: cfg.Profile, + name: cfg.AgentID, // display name defaults to uname agentsDir: cfg.AgentsDir, id: cfg.AgentID, cwd: cfg.CWD, diff --git a/agent/new.go.bak b/agent/new.go.bak new file mode 100644 index 0000000..db1e9a6 --- /dev/null +++ b/agent/new.go.bak @@ -0,0 +1,60 @@ +package agent + +import ( + "sync" + + "ollie/backend" + olog "ollie/log" + "ollie/toolsrv" +) + +// AgentCfg is the configuration for constructing a new Agent. +type AgentCfg struct { + History *History + Runtime *Runtime + AgentName string + AgentsDir string + AgentID string // unique agent identity + CWD string // working directory for tool execution + BaseLayers []string + PromptEnvExtra []string + NewToolServer func() toolsrv.Runner + NewBackend func(string) (backend.Backend, error) + Output EventHandler + Log *olog.Logger + AuditLog *olog.Logger + SessionID string + StartupMsgs []string + ReadPlanStep func() string + SaveSession func() + FlushSave func() +} + +// NewAgent constructs an Agent from the given configuration. +func NewAgent(cfg AgentCfg) *Agent { + ag := &Agent{ + history: cfg.History, + runtime: cfg.Runtime, + agentName: cfg.AgentName, + agentsDir: cfg.AgentsDir, + id: cfg.AgentID, + cwd: cfg.CWD, + baseLayers: cfg.BaseLayers, + promptEnvExtra: cfg.PromptEnvExtra, + newToolServer: cfg.NewToolServer, + newBackend: cfg.NewBackend, + output: cfg.Output, + log: cfg.Log, + auditLog: cfg.AuditLog, + sessionID: cfg.SessionID, + startupMessages: cfg.StartupMsgs, + readPlanStep: cfg.ReadPlanStep, + saveSession: cfg.SaveSession, + flushSave: cfg.FlushSave, + state: "idle", + } + ag.changeCond = sync.NewCond(&ag.changeMu) + ag.turnError = ag.defaultTurnError + ag.wireToolsChanged() + return ag +} diff --git a/agent/turn.go b/agent/turn.go index e383d45..85fe5bc 100644 --- a/agent/turn.go +++ b/agent/turn.go @@ -415,7 +415,9 @@ func (ag *Agent) autoWarnLimit(ctx context.Context) int { func (ag *Agent) spawnContext(ctx context.Context) string { result := ag.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{ "session_id": ag.sessionID, - "agent": ag.agentName, + "agent_id": ag.ID(), + "profile": ag.Profile(), + "agent": ag.Name(), "cwd": ag.effectiveCwd(), "model": ag.runtime.Backend.Model(), }, ag.log) diff --git a/doc/CORE.md b/doc/CORE.md index c918bb8..9de3001 100644 --- a/doc/CORE.md +++ b/doc/CORE.md @@ -102,7 +102,9 @@ type Agent struct { history *History runtime *Runtime cfg agentConfig - agentName string + profileName string + agentID string + displayName string agentsDir string baseLayers []string promptEnvExtra []string @@ -127,7 +129,9 @@ The full construction config: |---|---| | `History` | Pre-existing session history to restore (nil = new) | | `Runtime` | Pre-built *Runtime (tools, hooks, prompt, params) | -| `AgentName` | Name of the active agent config | +| `Profile` | Name of the active agent configuration profile | +| `ID` | Immutable agent ID (`uname`) | +| `Name` | Mutable display name | | `AgentsDir` | Path to agent JSON directory | | `SessionsDir` | Path to persisted session files | | `SessionID` | Unique session identifier | diff --git a/doc/MULTI_AGENT.md b/doc/MULTI_AGENT.md index f4b2066..3c6bac8 100644 --- a/doc/MULTI_AGENT.md +++ b/doc/MULTI_AGENT.md @@ -24,8 +24,8 @@ to its `name` file moves the entire directory subtree. | `/session/{name}/agent/{aid}/cfg` | Agent config KV: backend, model, agent, cwd, params | | `/session/{name}/agent/{aid}/statewait` | **Blocking read** — returns when state changes | | `/session/{name}/agent/{aid}/ctl` | Control commands: `stop`, `kill`, `compact`, `rn ` | -| `/session/{name}/agent/{aid}/id` | Immutable agent UUID | -| `/session/{name}/agent/{aid}/name` | Mutable agent name (write to rename) | +| `/session/{name}/agent/{aid}/id` | Immutable agent ID (uname) | +| `/session/{name}/agent/{aid}/name` | Mutable display name (write to rename) | Global request-response files provide stateless operations without sessions: diff --git a/fs/agentfiles.go b/fs/agentfiles.go index 2465f21..99551f2 100644 --- a/fs/agentfiles.go +++ b/fs/agentfiles.go @@ -28,9 +28,6 @@ func listAgents(ctx HandlerCtx) ([]FsNodeDecl, error) { } dirName := a.ID() - if n := a.Name(); n != "" { - dirName = n - } agentDir := Dir(dirName, // prompt — submit a prompt to the agent @@ -227,37 +224,33 @@ func listAgents(ctx HandlerCtx) ([]FsNodeDecl, error) { return []byte("#!/bin/sh\nexec tail -f \"$(dirname \"$0\")/chat\"\n"), nil })), - // tools — list all available tools (read) / load a tool (write) - Leaf("tools", 0666, - Read(func(ctx HandlerCtx) ([]byte, error) { - // Use the session's dedicated tools management connection, - // NOT a.ToolServer() which would deadlock if the agent - // loop is mid-execution (it holds the Conn mutex). - if s.toolsConn == nil { - return []byte("(no tool server)\n"), nil + // tools — list all available tools (read) / load a tool (write) + Leaf("tools", 0666, + Read(func(ctx HandlerCtx) ([]byte, error) { + // Use the session's dedicated tools management connection, + // NOT a.ToolServer() which would deadlock if the agent + // loop is mid-execution (it holds the Conn mutex). + if s.toolsConn == nil { + return []byte("(no tool server)\n"), nil + } + loaded, err := s.toolsConn.ListTools() + if err != nil { + return []byte(fmt.Sprintf("(error: %v)\n", err)), nil + } + var sb strings.Builder + for _, ti := range loaded { + if ti.Description != "" { + fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description) + } else { + sb.WriteString(ti.Name + "\n") } - loaded, err := s.toolsConn.ListTools() - if err != nil { - return []byte(fmt.Sprintf("(error: %v)\n", err)), nil - } - var sb strings.Builder - for _, ti := range loaded { - if ti.Description != "" { - fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description) - } else { - sb.WriteString(ti.Name + "\n") - } - } - return []byte(sb.String()), nil - }), - Write(func(ctx HandlerCtx, data []byte) error { - return s.loadTool(string(data), a) - }), - ), - - - - + } + return []byte(sb.String()), nil + }), + Write(func(ctx HandlerCtx, data []byte) error { + return s.loadTool(string(data), a) + }), + ), // name — agent name Leaf("name", 0666, @@ -284,6 +277,10 @@ func listAgents(ctx HandlerCtx) ([]FsNodeDecl, error) { return listProcesses(ctx, s, a) }), ) + agentDir.Rename = func(newName string) error { + a.SetName(newName) + return nil + } out = append(out, agentDir) } return out, nil @@ -332,7 +329,9 @@ func agentCfgContent(a *agent.Agent, s *Session, al *AgentLog) string { fmt.Fprintf(&sb, "name=%s\n", s.id) fmt.Fprintf(&sb, "backend=%s\n", a.BackendName()) fmt.Fprintf(&sb, "model=%s\n", a.ModelName()) - fmt.Fprintf(&sb, "agent=%s\n", a.Name()) + fmt.Fprintf(&sb, "id=%s\n", a.ID()) + fmt.Fprintf(&sb, "profile=%s\n", a.Profile()) + fmt.Fprintf(&sb, "displayName=%s\n", a.Name()) fmt.Fprintf(&sb, "cwd=%s\n", s.Core.CWD()) fmt.Fprintf(&sb, "remote=%s\n", al.remote) fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens) @@ -354,6 +353,9 @@ func handleAgentCfg(a *agent.Agent, s *Session, al *AgentLog, data []byte) error } switch parts[0] { case "name": + if strings.TrimSpace(parts[1]) != "" { + a.SetName(strings.TrimSpace(parts[1])) + } case "backend": case "model": default: @@ -396,6 +398,10 @@ func handleAgentCtl(a *agent.Agent, s *Session, data []byte) error { s.Core.SetActiveAgent(strings.Join(cmd[1:], " ")) } } + case "name": + if len(cmd) > 1 { + a.SetName(strings.Join(cmd[1:], " ")) + } case "cwd": if len(cmd) > 1 { a.SetCWD(strings.Join(cmd[1:], " ")) diff --git a/fs/builder.go b/fs/builder.go index 07edf7b..f7a0cb9 100644 --- a/fs/builder.go +++ b/fs/builder.go @@ -261,6 +261,13 @@ func deleteDir(d *FsNodeDecl, name string, ctx HandlerCtx) error { } func renameDir(d *FsNodeDecl, old, new string, ctx HandlerCtx) error { + child := findChild(d, old, ctx) + if child == nil { + return fmt.Errorf("%s: not found", old) + } + if child.Rename != nil { + return child.Rename(new) + } return fmt.Errorf("rename not supported") } diff --git a/fs/fsnode.go b/fs/fsnode.go index 224a547..55b46e8 100644 --- a/fs/fsnode.go +++ b/fs/fsnode.go @@ -129,9 +129,10 @@ type FsNodeDecl struct { // Request — rdwr: write triggers result, read returns it once. Request func(ctx HandlerCtx, data []byte) ([]byte, error) - // Tcreate + Tremove (directory ops). + // Tcreate + Tremove + Trename (directory ops). Create func(ctx HandlerCtx, name string) error Remove func(ctx HandlerCtx) error + Rename func(newName string) error } // NodeOption configures a FsNodeDecl at construction time. diff --git a/fs/lifecycle.go b/fs/lifecycle.go index b1bbe59..2fc0eb8 100644 --- a/fs/lifecycle.go +++ b/fs/lifecycle.go @@ -149,7 +149,7 @@ func Kill(rs *rootState, id string) { var agentNames []string if sess.Core != nil { for _, ag := range sess.Core.Agents() { - agentNames = append(agentNames, ag.Name()) + agentNames = append(agentNames, ag.ID()) } sess.Core.Close() } @@ -366,12 +366,11 @@ func CreateFromRoot(root *Tree, args []string) (string, error) { func CreateAgent(rs *rootState, sessName string, args []string) error { backendOverride := "" modelOverride := "" - agentName := "" - agentAlias := "" + profile := "" + name := "" cwd := "" remoteTarget := "" systemPromptOverride := "" - name := "" for _, arg := range args { k, v, ok := strings.Cut(arg, "=") if !ok { @@ -388,9 +387,7 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { case "model": modelOverride = v case "agent": - agentName = v - case "agent_alias": - agentAlias = v + profile = v case "cwd": cwd = v case "remote": @@ -398,12 +395,9 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { case "system_prompt": systemPromptOverride = v default: - return fmt.Errorf("unknown option %q (valid: name, backend, model, agent, agent_alias, cwd, remote, system_prompt)", k) + return fmt.Errorf("unknown option %q (valid: name, backend, model, agent, cwd, remote, system_prompt)", k) } } - if name != "" && agentAlias == "" { - agentAlias = name - } rs.mu.RLock() sess, ok := rs.sessions[sessName] @@ -415,11 +409,11 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { sess.SetName(sessName) } - if agentName == "" { + if profile == "" { if v := os.Getenv("OLLIE_DEFAULT_AGENT"); v != "" { - agentName = v + profile = v } else { - agentName = "default" + profile = "default" } } @@ -438,7 +432,7 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { uname := nextUname(rs) sessID := sess.id - cfg := LoadAgentConfig(rs.cfg.AgentsDir, agentName, nil) + cfg := LoadAgentConfig(rs.cfg.AgentsDir, profile, nil) if cfg != nil { if backendOverride == "" && cfg.Backend != "" { backendOverride = cfg.Backend @@ -561,7 +555,7 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { if sess.Core == nil { core := coresession.New(coresession.Config{ Backend: be, - AgentName: agentName, + Profile: profile, AgentsDir: rs.cfg.AgentsDir, SessionsDir: rs.cfg.SessionsDir, SessionID: sessID, @@ -589,7 +583,7 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { ag := agent.NewAgent(agent.AgentCfg{ History: nil, Runtime: rt, - AgentName: agentName, + Profile: profile, AgentsDir: rs.cfg.AgentsDir, AgentID: uname, CWD: cwd, @@ -620,16 +614,14 @@ func CreateAgent(rs *rootState, sessName string, args []string) error { rs.mu.Unlock() ag := sess.Core.Agent() - ag.Submit(sess.SessionCtx, "/agent "+agentName) - if agentAlias != "" { - ag.SetName(agentAlias) - } else { - ag.SetName(uname) + ag.Submit(sess.SessionCtx, "/agent "+profile) + if name != "" { + ag.SetName(name) } rs.cfg.Log.Info("created agent in session %s (backend=%s model=%s agent=%s)", sessName, ag.BackendName(), ag.ModelName(), ag.Name()) - PostEvent("A new session/" + sessName + "/agent/" + ag.Name()) + PostEvent("A new session/" + sessName + "/agent/" + ag.ID()) return nil } @@ -645,5 +637,5 @@ func CreateAgentFromRoot(root *Tree, sessName string, args []string) (string, er if sess == nil || sess.Core == nil { return "", fmt.Errorf("agent not created") } - return sess.Core.Agent().Name(), nil + return sess.Core.Agent().ID(), nil } diff --git a/fs/newroot.go b/fs/newroot.go index 13e779a..f0aded9 100644 --- a/fs/newroot.go +++ b/fs/newroot.go @@ -20,7 +20,7 @@ type Config struct { AgentsDir string SessionsDir string MkdirAll func(string, os.FileMode) error - NewCore func(sessionID, agentName, cwd string) (*coresession.Session, error) + NewCore func(sessionID, profile, cwd string) (*coresession.Session, error) Yolo bool SkillsRegistry *skills.Registry ModelCache *ModelCache diff --git a/fs/persist.go b/fs/persist.go index 5bac395..26184e3 100644 --- a/fs/persist.go +++ b/fs/persist.go @@ -114,13 +114,16 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error { if cwd == "" { cwd, _ = os.Getwd() } - agentName := ps.Agent - if agentName == "" { - agentName = "default" + profile := ps.Profile + if profile == "" { + profile = ps.Agent + if profile == "" { + profile = "default" + } } sessID := ps.ID - cfg := LoadAgentConfig(rs.cfg.AgentsDir, agentName, nil) + cfg := LoadAgentConfig(rs.cfg.AgentsDir, profile, nil) backendName := ps.Backend if backendName == "" && cfg != nil && cfg.Backend != "" { @@ -141,7 +144,10 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error { be.SetModel(modelName) } - uname := nextUname(rs) + uname := ps.ID + if uname == "" { + uname = nextUname(rs) + } var newToolServer func() toolsrv.Runner var promptEnv []string var proc *toolsrv.Process @@ -230,7 +236,7 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error { al := NewAgentLog(remoteTarget) core := coresession.New(coresession.Config{ Backend: be, - AgentName: agentName, + Profile: profile, AgentsDir: rs.cfg.AgentsDir, SessionsDir: rs.cfg.SessionsDir, SessionID: sessID, @@ -266,6 +272,12 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error { // Use the multiplexed connection for both agent execution and 9P management. sess.toolsConn = toolSrv + // Restore agent display name from persisted session. + ag := sess.Core.Agent() + if ps.Agent != "" { + ag.SetName(ps.Agent) + } + replayMessagesToLog(sess.AgentLog(), ps.Messages) // Pre-load autoLoad tools into the remote. @@ -283,6 +295,13 @@ func restoreSession(rs *rootState, ps *agent.PersistedAgent) error { rs.sessions[name] = sess rs.mu.Unlock() - rs.cfg.Log.Info("restored session %s (backend=%s model=%s agent=%s)", sessID, backendName, modelName, agentName) + toolCount := 0 + if toolSrv != nil { + if infos, err := toolSrv.ListTools(); err == nil { + toolCount = len(infos) + } + } + rs.cfg.Log.Info("restored session %s: cfg=%v autoload=%d tools=%d (backend=%s model=%s agent=%s)", + sessID, cfg != nil, func() int { if cfg != nil { return len(cfg.AutoLoad) }; return -1 }(), toolCount, backendName, modelName, profile) return nil } diff --git a/fs/sessionfiles.go b/fs/sessionfiles.go index d28c353..5a58a8e 100644 --- a/fs/sessionfiles.go +++ b/fs/sessionfiles.go @@ -123,7 +123,7 @@ 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().Name() + "\n"), nil + return []byte(s.Core.Agent().ID() + "\n"), nil })), // {aid} — dynamic per-agent directory @@ -196,4 +196,4 @@ func requestAgentNew(ctx HandlerCtx, data []byte) ([]byte, error) { // used as a standalone — agent creation is handled inline in the // session's agent/new handler above. return nil, fmt.Errorf("not available") -} \ No newline at end of file +} diff --git a/session/session.go b/session/session.go index 712258e..ad2ddfd 100644 --- a/session/session.go +++ b/session/session.go @@ -22,7 +22,7 @@ import ( type Config struct { Backend backend.Backend ModelName string - AgentName string + Profile string // config profile name (e.g. "default") AgentsDir string SessionsDir string SessionID string @@ -44,7 +44,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 activeAgentName. +// "the agent" (Agent(), CWD, etc.) use the agent identified by immutable activeAgentID. type Session struct { id string envMu sync.RWMutex @@ -52,14 +52,14 @@ type Session struct { plan []byte prevPrompt string - 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 + agents []*agent.Agent + activeAgentID string // immutable ID 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 @@ -69,7 +69,7 @@ type Session struct { // NewSessionID generates a UUIDv4 session identifier. func NewSessionID() string { b := make([]byte, 16) - rand.Read(b) //nolint:errcheck + rand.Read(b) //nolint:errcheck b[6] = (b[6] & 0x0f) | 0x40 // version 4 b[8] = (b[8] & 0x3f) | 0x80 // variant 10xx return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", @@ -146,7 +146,7 @@ func New(cfg Config) *Session { ag := agent.NewAgent(agent.AgentCfg{ History: cfg.History, Runtime: rt, - AgentName: cfg.AgentName, + Profile: cfg.Profile, AgentsDir: cfg.AgentsDir, AgentID: cfg.AgentID, CWD: paths.ExpandHome(cfg.CWD), @@ -162,23 +162,23 @@ func New(cfg Config) *Session { ReadPlanStep: cfg.ReadPlanStep, SaveSession: a.saveSession, FlushSave: a.flushSave, -}) + }) a.agents = append(a.agents, ag) - a.activeAgentName = ag.Name() + a.activeAgentID = ag.ID() ag.SetSessionEnv(a.id) return a } -// Agent returns the currently active agent by name. +// 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.activeAgentName != "" { + if a.activeAgentID != "" { for _, ag := range a.agents { - if ag.Name() == a.activeAgentName { + if ag.ID() == a.activeAgentID { return ag } } @@ -187,16 +187,15 @@ func (a *Session) Agent() *agent.Agent { 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 { +// SetActiveAgent selects an agent by immutable ID. +func (a *Session) SetActiveAgent(id string) error { for _, ag := range a.agents { - if ag.Name() == name { - a.activeAgentName = name + if ag.ID() == id { + a.activeAgentID = id return nil } } - return fmt.Errorf("agent %q not found", name) + return fmt.Errorf("agent %q not found", id) } // FindAgent returns the agent matching the given name or ID, or nil. @@ -233,8 +232,8 @@ func (a *Session) SaveFuncs() (save func(), flush func()) { // 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() + if a.activeAgentID == "" { + a.activeAgentID = ag.ID() } ag.SetSessionEnv(a.id) } @@ -337,7 +336,7 @@ func (a *Session) WaitChange(ctx context.Context, field, current string) (string case "cwd": return a.CWD() case "agent": - return ag.Name() + return ag.ID() } return "" }