fix naming problems

This commit is contained in:
Ollie Agent 2026-08-02 22:10:00 +02:00
parent 088aa38f1c
commit 119e673299
16 changed files with 250 additions and 141 deletions

View File

@ -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: <tool>"
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: <tool>"
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))
}

View File

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

View File

@ -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",

View File

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

60
agent/new.go.bak Normal file
View File

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

View File

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

View File

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

View File

@ -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 <name>` |
| `/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:

View File

@ -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:], " "))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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