diff --git a/agent/commands.go b/agent/commands.go index 8c4e8e1..106b004 100644 --- a/agent/commands.go +++ b/agent/commands.go @@ -175,9 +175,9 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool { return } d := s.newDispatcher() - env := []string{"OLLIE_SESSION_ID=" + s.sessionID, "OLLIE_UNAME=" + s.uname} + env := []string{"OLLIE_SESSION_ID=" + s.sess.ID(), "OLLIE_UNAME=" + s.sess.Uname()} env = append(env, s.promptEnvExtra...) - rt := BuildRuntime(cfg, d, s.cwd, env, s.baseLayers...) + rt := BuildRuntime(cfg, d, s.sess.CWD(), env, s.baseLayers...) if rt.CfgBackend != "" { newBe, err := s.newBackend(rt.CfgBackend) if err != nil { @@ -226,8 +226,8 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool { s.emit(infoEvent("nothing to compact")) return } - if s.sessionsDir != "" && s.sessionID != "" { - histPath := s.activeSessionPath(s.sessionID, ".compaction.jsonl") + if s.sessionsDir != "" && s.sess.ID() != "" { + histPath := s.activeSessionPath(s.sess.ID(), ".compaction.jsonl") if err := os.MkdirAll(filepath.Dir(histPath), 0700); err != nil { s.emit(infoEvent("compaction history save: " + err.Error())) } else if data, err := json.Marshal(snapshot); err == nil { @@ -351,7 +351,7 @@ func (s *agent) handleCommand(ctx context.Context, input string) bool { continue } marker := " " - if file.id == s.sessionID { + if file.id == s.sess.ID() { marker = "* " } goal := "" diff --git a/agent/core.go b/agent/core.go index bcc047c..b16069a 100644 --- a/agent/core.go +++ b/agent/core.go @@ -318,14 +318,11 @@ type agent struct { agentName string agentsDir string sessionsDir string - sessionID string - uname string newDispatcher func() tools.Dispatcher newBackend func(string) (backend.Backend, error) readPlanStep func() string listHandlers map[string]func() []string turnError func(ctx context.Context, errType, errMsg string) HookResult // overridable for tests - cwd string remote string // SSH target for remote execution promptEnvExtra []string // PRIME_* vars for prompt resolution baseLayers []string // system prompt, operational model, env block — for /agent reloads @@ -334,8 +331,6 @@ type agent struct { toolCallCount atomic.Int64 pendingInject atomic.Pointer[string] mu sync.RWMutex - envMu sync.RWMutex - env map[string]string // session-scoped env vars changeMu sync.Mutex changeCond *sync.Cond submitMu sync.Mutex // serializes Submit calls (commands + turns) @@ -360,12 +355,7 @@ func (s *agent) ToolCallCount() int64 { // SetEnv stores a session-scoped variable and propagates it to the execute server. func (s *agent) SetEnv(key, value string) { - s.envMu.Lock() - if s.env == nil { - s.env = make(map[string]string) - } - s.env[key] = value - s.envMu.Unlock() + s.sess.SetEnv(key, value) if s.runtime == nil || s.runtime.Dispatcher == nil { return } @@ -378,14 +368,14 @@ func (s *agent) SetEnv(key, value string) { // pushSessionEnv injects OLLIE_SESSION_ID into the execute server subprocess env. func (s *agent) pushSessionEnv() { - if s.runtime == nil || s.runtime.Dispatcher == nil || s.sessionID == "" { + if s.runtime == nil || s.runtime.Dispatcher == nil || s.sess.ID() == "" { return } if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok { if es, ok := srv.(tools.EnvSetter); ok { - es.SetEnv("OLLIE_SESSION_ID", s.sessionID) - if s.uname != "" { - es.SetEnv("OLLIE_UNAME", s.uname) + es.SetEnv("OLLIE_SESSION_ID", s.sess.ID()) + if s.sess.Uname() != "" { + es.SetEnv("OLLIE_UNAME", s.sess.Uname()) } } } @@ -393,7 +383,7 @@ func (s *agent) pushSessionEnv() { // pushLockDir sets the flock directory on the execute server to the session tmpdir. func (s *agent) pushLockDir() { - if s.runtime == nil || s.runtime.Dispatcher == nil || s.sessionID == "" { + if s.runtime == nil || s.runtime.Dispatcher == nil || s.sess.ID() == "" { return } @@ -488,9 +478,6 @@ func NewAgentCore(cfg AgentCoreConfig) Core { agentName: cfg.AgentName, agentsDir: cfg.AgentsDir, sessionsDir: cfg.SessionsDir, - sessionID: cfg.SessionID, - uname: cfg.Uname, - cwd: paths.ExpandHome(cfg.CWD), remote: cfg.Remote, promptEnvExtra: cfg.PromptEnvExtra, baseLayers: cfg.BaseLayers, @@ -505,7 +492,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core { hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(hookTimeout)*time.Second) defer cancel() return a.runtime.Hooks.Run(hookCtx, HookTurnError, map[string]string{ - "session_id": a.sessionID, + "session_id": a.sess.ID(), "cwd": a.CWD(), "model": a.runtime.Backend.Model(), "error_type": errType, @@ -519,7 +506,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core { // Close releases resources for this session, including its tmpdir. func (s *agent) Close() { - s.log.Debug("Close() session=%q", s.sessionID) + s.log.Debug("Close() session=%q", s.sess.ID()) s.flushSave() if s.runtime != nil && s.runtime.Dispatcher != nil { if srv, ok := s.runtime.Dispatcher.GetServer("execute"); ok { @@ -529,8 +516,8 @@ func (s *agent) Close() { } } } - if s.sessionID != "" { - os.RemoveAll(filepath.Join(ollieTmpDir(), s.sessionID)) //nolint:errcheck + if s.sess.ID() != "" { + os.RemoveAll(filepath.Join(ollieTmpDir(), s.sess.ID())) //nolint:errcheck } } @@ -790,9 +777,9 @@ func (s *agent) Reply() string { // CWD returns the current working directory for tool execution. func (s *agent) CWD() string { - if s.cwd != "" { - s.log.Debug("CWD() = %q", s.cwd) - return s.cwd + if s.sess.CWD() != "" { + s.log.Debug("CWD() = %q", s.sess.CWD()) + return s.sess.CWD() } wd, _ := os.Getwd() s.log.Debug("CWD() = %q (from getwd)", wd) @@ -809,8 +796,8 @@ func (s *agent) SetCWD(dir string) error { return fmt.Errorf("cwd: %w", err) } } - oldCwd := s.cwd - s.cwd = dir + oldCwd := s.sess.CWD() + s.sess.SetCWD(dir) // Update cwd references in the system prompt. if oldCwd != "" && dir != "" && oldCwd != dir { s.runtime.Preamble = strings.ReplaceAll(s.runtime.Preamble, oldCwd, dir) @@ -830,8 +817,8 @@ func (s *agent) SetCWD(dir string) error { // SetSessionID renames the session. It updates the in-memory ID, renames // persisted files on disk, and propagates to the execute server env. func (s *agent) SetSessionID(newID string) error { - s.log.Debug("SetSessionID(%q) old=%q", newID, s.sessionID) - oldID := s.sessionID + s.log.Debug("SetSessionID(%q) old=%q", newID, s.sess.ID()) + oldID := s.sess.ID() if oldID == newID { return nil } @@ -846,7 +833,7 @@ func (s *agent) SetSessionID(newID string) error { } } } - s.sessionID = newID + s.sess.SetID(newID) // Update session ID references in the system prompt. s.runtime.Preamble = strings.ReplaceAll(s.runtime.Preamble, oldID, newID) // Rename tmpdir so isread markers remain valid after rename. @@ -888,7 +875,7 @@ func (s *agent) autoWarnLimit(ctx context.Context) int { // agent-specific prompt with any agentSpawn hook output. func (s *agent) spawnContext(ctx context.Context) string { result := s.runtime.Hooks.Run(ctx, HookAgentSpawn, map[string]string{ - "session_id": s.sessionID, + "session_id": s.sess.ID(), "agent": s.agentName, "cwd": s.CWD(), "model": s.runtime.Backend.Model(), @@ -910,7 +897,7 @@ func (s *agent) spawnContext(ctx context.Context) string { // re-injection, post-hook. Returns (n compacted, error). Returns (0, nil) if // the pre-hook blocked or there was nothing to compact. Caller manages setState. func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) { - payload := map[string]string{"session_id": s.sessionID, "trigger": trigger, "cwd": s.CWD()} + payload := map[string]string{"session_id": s.sess.ID(), "trigger": trigger, "cwd": s.CWD()} pre := s.runtime.Hooks.Run(ctx, HookPreCompact, payload, s.log) if pre.Warning != "" { s.emit(infoEvent(pre.Warning)) @@ -937,7 +924,7 @@ func (s *agent) runCompact(ctx context.Context, trigger string) (int, error) { return 0, err } if n > 0 { - s.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, s.sessionID) + s.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, s.sess.ID()) s.warnedContext = false if sc := s.spawnContext(ctx); sc != "" { s.history.appendUserMessage(sc) @@ -982,15 +969,15 @@ func (s *agent) flushSave() { if !dirty { return } - if s.history == nil || s.sessionID == "" || s.sessionsDir == "" { + if s.history == nil || s.sess.ID() == "" || s.sessionsDir == "" { return } - path := s.activeSessionPath(s.sessionID, ".json") + path := s.activeSessionPath(s.sess.ID(), ".json") if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { s.log.Error("session save: %v", err) return } - if err := s.history.saveToFull(path, s.sessionID, s.agentName, + if err := s.history.saveToFull(path, s.sess.ID(), s.agentName, s.runtime.Backend.Name(), s.runtime.Backend.Model(), s.CWD(), s.remote); err != nil { s.log.Error("session save: %v", err) } @@ -1004,7 +991,7 @@ func (s *agent) SaveSession(path string) error { if s.history == nil { return fmt.Errorf("no active session") } - return s.history.saveToFull(path, s.sessionID, s.agentName, + return s.history.saveToFull(path, s.sess.ID(), s.agentName, s.runtime.Backend.Name(), s.runtime.Backend.Model(), s.CWD(), s.remote) } @@ -1240,7 +1227,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { s.emit(Event{Role: "user", Content: input}) hookResult := s.runtime.Hooks.Run(ctx, HookPreTurn, map[string]string{ - "session_id": s.sessionID, + "session_id": s.sess.ID(), "cwd": s.CWD(), "prompt": input, }, s.log) @@ -1286,7 +1273,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { s.currentAction.Store(handle) s.setState("thinking") - s.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), s.sessionID) + s.auditLog.Debug("turn: start input=%s session=%s", auditTruncate(input), s.sess.ID()) // Build per-turn agentConfig from the current runtime. s.cfg = agentConfig{ @@ -1348,7 +1335,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { } s.cfg.PreTool = func(ctx context.Context, name string, args json.RawMessage) HookResult { return s.runtime.Hooks.Run(ctx, HookPreTool, map[string]string{ - "session_id": s.sessionID, + "session_id": s.sess.ID(), "cwd": s.CWD(), "tool": name, "args": string(args), @@ -1356,7 +1343,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { } s.cfg.PostTool = func(ctx context.Context, name string, args json.RawMessage, result string) HookResult { return s.runtime.Hooks.Run(ctx, HookPostTool, map[string]string{ - "session_id": s.sessionID, + "session_id": s.sess.ID(), "cwd": s.CWD(), "tool": name, "args": string(args), @@ -1478,7 +1465,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { s.history.removeCancelledToolResults() } if errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted) { - s.auditLog.Debug("turn: interrupted session=%s", s.sessionID) + s.auditLog.Debug("turn: interrupted session=%s", s.sess.ID()) s.saveSession() return "" } @@ -1491,7 +1478,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { } stopResult := s.runtime.Hooks.Run(ctx, HookPostTurn, map[string]string{ - "session_id": s.sessionID, + "session_id": s.sess.ID(), "cwd": s.CWD(), }, s.log) if stopResult.Warning != "" { @@ -1506,12 +1493,12 @@ func (s *agent) executeTurn(ctx context.Context, input string) string { if s.history != nil { s.history.recordTurnCost(s.cfg.Backend.Model()) - appendUsageLog(s.sessionID, s.cfg.Backend.Name(), s.cfg.Backend.Model(), s.history) + appendUsageLog(s.sess.ID(), s.cfg.Backend.Name(), s.cfg.Backend.Model(), s.history) if s.history.LastTurnCostUSD > 0 { s.emit(Event{Role: "info", Content: fmt.Sprintf("costLast=$%.4f\n", s.history.LastTurnCostUSD)}) } s.auditLog.Debug("turn: end reply=%s cost=$%.4f session_total=$%.4f session=%s", - auditTruncate(s.sess.Reply()), s.history.LastTurnCostUSD, s.history.SessionCostUSD, s.sessionID) + auditTruncate(s.sess.Reply()), s.history.LastTurnCostUSD, s.history.SessionCostUSD, s.sess.ID()) s.notifyChange() } s.saveSession() diff --git a/agent/core_test.go b/agent/core_test.go index 998269f..c2aab8b 100644 --- a/agent/core_test.go +++ b/agent/core_test.go @@ -603,13 +603,13 @@ func TestCommand_Clear(t *testing.T) { if c.history == nil { t.Fatal("session nil after first turn") } - oldID := c.sessionID + oldID := c.sess.ID() collectEvents(context.Background(), c, "/clear") if c.history != nil { t.Error("session not nil after /clear") } - if c.sessionID != oldID { - t.Errorf("sessionID changed after /clear: %q → %q; want unchanged", oldID, c.sessionID) + if c.sess.ID() != oldID { + t.Errorf("sessionID changed after /clear: %q → %q; want unchanged", oldID, c.sess.ID()) } } @@ -1758,13 +1758,13 @@ func TestCore_Usage_WithSession(t *testing.T) { func TestSetSessionID_Rename(t *testing.T) { c := newCore(t, nil, nil) collectEvents(context.Background(), c, "hello") // saves session file - oldID := c.sessionID + oldID := c.sess.ID() newID := NewSessionID() if err := c.SetSessionID(newID); err != nil { t.Fatalf("SetSessionID: %v", err) } - if c.sessionID != newID { - t.Errorf("sessionID = %q; want %q", c.sessionID, newID) + if c.sess.ID() != newID { + t.Errorf("sessionID = %q; want %q", c.sess.ID(), newID) } if _, err := os.Stat(filepath.Join(c.sessionsDir, "active", oldID+".json")); !os.IsNotExist(err) { t.Errorf("old active session file still exists after rename; err=%v", err) @@ -1803,12 +1803,12 @@ func TestSetSessionID_UpdatesPreamble(t *testing.T) { func TestSetSessionID_SameID(t *testing.T) { c := newCore(t, nil, nil) - id := c.sessionID + id := c.sess.ID() if err := c.SetSessionID(id); err != nil { t.Fatalf("SetSessionID with same ID: %v", err) } - if c.sessionID != id { - t.Errorf("sessionID changed: got %q; want %q", c.sessionID, id) + if c.sess.ID() != id { + t.Errorf("sessionID changed: got %q; want %q", c.sess.ID(), id) } } @@ -2312,13 +2312,12 @@ func TestSetEnv_StoredInCore(t *testing.T) { c.SetEnv("FOO", "bar") c.SetEnv("BAZ", "qux") - c.envMu.RLock() - defer c.envMu.RUnlock() - if c.env["FOO"] != "bar" { - t.Errorf("env[FOO] = %q; want bar", c.env["FOO"]) + env := c.sess.Env() + if env["FOO"] != "bar" { + t.Errorf("env[FOO] = %q; want bar", env["FOO"]) } - if c.env["BAZ"] != "qux" { - t.Errorf("env[BAZ] = %q; want qux", c.env["BAZ"]) + if env["BAZ"] != "qux" { + t.Errorf("env[BAZ] = %q; want qux", env["BAZ"]) } } @@ -2327,8 +2326,9 @@ func TestSetEnv_NilDispatcher_NoPanic(t *testing.T) { c.runtime.Dispatcher = nil // Must not panic. c.SetEnv("K", "V") - if c.env["K"] != "V" { - t.Errorf("env[K] = %q; want V", c.env["K"]) + env := c.sess.Env() + if env["K"] != "V" { + t.Errorf("env[K] = %q; want V", env["K"]) } }