agent: delegate bus, fifo, state, reply to session.Session

Move session-scoped concerns out of the agent struct:
- Bus() → sess.Bus()
- Queue()/PopQueue() → sess.Queue()/PopQueue()
- State()/setState() → sess.State()/SetState()
- Reply() → sess.Reply()/SetReply()

Remove corresponding fields from agent struct. Agent-level
WaitChange still uses its own changeMu/changeCond since it
observes multiple fields (state, usage, ctxsz, cwd, agent).
This commit is contained in:
Levi Neely 2026-07-29 18:28:14 +02:00
parent 457ec2312f
commit 64f3f18573
2 changed files with 36 additions and 42 deletions

View File

@ -22,6 +22,7 @@ import (
"ollie/backend" "ollie/backend"
olog "ollie/log" olog "ollie/log"
"ollie/paths" "ollie/paths"
"ollie/session"
"ollie/tools" "ollie/tools"
) )
@ -309,6 +310,7 @@ type AgentCoreConfig struct {
// agent is the Core implementation. It owns all agent and session state // agent is the Core implementation. It owns all agent and session state
// but has no knowledge of how output is rendered. // but has no knowledge of how output is rendered.
type agent struct { type agent struct {
sess *session.Session
history *History history *History
runtime *Runtime runtime *Runtime
cfg agentConfig // per-turn config built from runtime; set in executeTurn cfg agentConfig // per-turn config built from runtime; set in executeTurn
@ -330,12 +332,8 @@ type agent struct {
startupMessages []string startupMessages []string
currentAction atomic.Pointer[actionHandle] currentAction atomic.Pointer[actionHandle]
toolCallCount atomic.Int64 toolCallCount atomic.Int64
fifo PromptFIFO
bus *pubsub.Bus
pendingInject atomic.Pointer[string] pendingInject atomic.Pointer[string]
mu sync.RWMutex mu sync.RWMutex
state string // "idle", "thinking", "calling: <tool>"
reply string // assistant text from the most recently completed turn
envMu sync.RWMutex envMu sync.RWMutex
env map[string]string // session-scoped env vars env map[string]string // session-scoped env vars
changeMu sync.Mutex changeMu sync.Mutex
@ -478,6 +476,11 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
} }
a := &agent{ a := &agent{
sess: session.New(session.Config{
ID: cfg.SessionID,
Uname: cfg.Uname,
CWD: paths.ExpandHome(cfg.CWD),
}),
history: cfg.History, history: cfg.History,
runtime: rt, runtime: rt,
log: log, log: log,
@ -496,8 +499,6 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
newBackend: cfg.NewBackend, newBackend: cfg.NewBackend,
readPlanStep: readPlanStep, readPlanStep: readPlanStep,
listHandlers: cfg.ListHandlers, listHandlers: cfg.ListHandlers,
state: "idle",
bus: pubsub.NewBus(),
} }
a.changeCond = sync.NewCond(&a.changeMu) a.changeCond = sync.NewCond(&a.changeMu)
a.turnError = func(_ context.Context, errType, errMsg string) HookResult { a.turnError = func(_ context.Context, errType, errMsg string) HookResult {
@ -726,9 +727,7 @@ func (s *agent) ModelName() string {
} }
func (s *agent) State() string { func (s *agent) State() string {
s.mu.RLock() return s.sess.State()
defer s.mu.RUnlock()
return s.state
} }
func (s *agent) notifyChange() { func (s *agent) notifyChange() {
@ -737,6 +736,12 @@ func (s *agent) notifyChange() {
s.changeMu.Unlock() s.changeMu.Unlock()
} }
func (s *agent) setState(state string) {
s.sess.SetState(state)
s.log.Debug("state -> %q", state)
s.notifyChange()
}
// WaitChange blocks until the named field changes from current, then returns // WaitChange blocks until the named field changes from current, then returns
// the new value. Returns ("", false) if ctx is cancelled. // the new value. Returns ("", false) if ctx is cancelled.
func (s *agent) WaitChange(ctx context.Context, field, current string) (string, bool) { func (s *agent) WaitChange(ctx context.Context, field, current string) (string, bool) {
@ -776,19 +781,11 @@ func (s *agent) WaitChange(ctx context.Context, field, current string) (string,
return "", false return "", false
} }
func (s *agent) setState(state string) {
s.mu.Lock()
s.state = state
s.mu.Unlock()
s.log.Debug("state -> %q", state)
s.notifyChange()
}
func (s *agent) Reply() string { func (s *agent) Reply() string {
s.mu.RLock() r := s.sess.Reply()
defer s.mu.RUnlock() s.log.Debug("Reply() len=%d", len(r))
s.log.Debug("Reply() len=%d", len(s.reply)) return r
return s.reply
} }
// CWD returns the current working directory for tool execution. // CWD returns the current working directory for tool execution.
@ -1033,7 +1030,7 @@ func (s *agent) Inject(prompt string) {
// If an inject is already pending, fall back to the normal FIFO so nothing // If an inject is already pending, fall back to the normal FIFO so nothing
// is lost. Use CompareAndSwap to avoid a race between the nil check and store. // is lost. Use CompareAndSwap to avoid a race between the nil check and store.
if !s.pendingInject.CompareAndSwap(nil, &prompt) { if !s.pendingInject.CompareAndSwap(nil, &prompt) {
s.fifo.Push(prompt) s.sess.Queue(prompt)
return return
} }
s.emit(Event{Role: "info", Content: "\n"}) s.emit(Event{Role: "info", Content: "\n"})
@ -1047,32 +1044,26 @@ func (s *agent) injectRewrite(prompt string) {
} }
func (s *agent) Queue(prompt string) { func (s *agent) Queue(prompt string) {
s.log.Debug("Queue() len=%d", len(prompt)) s.sess.Queue(prompt)
s.fifo.Push(prompt) s.sess.Bus().Publish("queued", prompt)
s.bus.Publish("queued", prompt)
if !s.IsRunning() {
go s.drainQueue()
}
} }
func (s *agent) drainQueue() { func (s *agent) drainQueue() {
if prompt, ok := s.fifo.Pop(); ok { if prompt, ok := s.sess.PopQueue(); ok {
s.Submit(context.Background(), prompt) s.Submit(context.Background(), prompt)
} }
} }
func (s *agent) Bus() *pubsub.Bus { func (s *agent) Bus() *pubsub.Bus {
return s.bus return s.sess.Bus()
} }
func (s *agent) emit(ev Event) { func (s *agent) emit(ev Event) {
s.bus.Publish("event", ev) s.sess.Bus().Publish("event", ev)
} }
func (s *agent) PopQueue() (string, bool) { func (s *agent) PopQueue() (string, bool) {
v, ok := s.fifo.Pop() return s.sess.PopQueue()
s.log.Debug("PopQueue() ok=%v len=%d", ok, len(v))
return v, ok
} }
func (s *agent) IsRunning() bool { func (s *agent) IsRunning() bool {
@ -1221,7 +1212,7 @@ func (s *agent) Submit(ctx context.Context, input string) {
if s.handleCommand(ctx, input) { if s.handleCommand(ctx, input) {
return return
} }
s.fifo.Push(input) s.sess.Queue(input)
return return
} }
@ -1234,7 +1225,7 @@ func (s *agent) Submit(ctx context.Context, input string) {
return return
} }
if s.IsRunning() { if s.IsRunning() {
s.fifo.Push(input) s.sess.Queue(input)
return return
} }
@ -1474,7 +1465,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
} }
s.mu.Lock() s.mu.Lock()
s.reply = replyBuf.String() s.sess.SetReply(replyBuf.String())
s.mu.Unlock() s.mu.Unlock()
replyBuf.Reset() replyBuf.Reset()
s.setState("idle") s.setState("idle")
@ -1493,7 +1484,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
} }
s.emit(Event{Role: "error", Content: err.Error()}) s.emit(Event{Role: "error", Content: err.Error()})
// Drain one FIFO item — the turnError hook may have queued a recovery prompt. // Drain one FIFO item — the turnError hook may have queued a recovery prompt.
if next, ok := s.fifo.Pop(); ok { if next, ok := s.sess.PopQueue(); ok {
return next return next
} }
return "" return ""
@ -1520,7 +1511,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
s.emit(Event{Role: "info", Content: fmt.Sprintf("costLast=$%.4f\n", s.history.LastTurnCostUSD)}) 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", s.auditLog.Debug("turn: end reply=%s cost=$%.4f session_total=$%.4f session=%s",
auditTruncate(s.reply), s.history.LastTurnCostUSD, s.history.SessionCostUSD, s.sessionID) auditTruncate(s.sess.Reply()), s.history.LastTurnCostUSD, s.history.SessionCostUSD, s.sessionID)
s.notifyChange() s.notifyChange()
} }
s.saveSession() s.saveSession()
@ -1537,7 +1528,7 @@ func (s *agent) executeTurn(ctx context.Context, input string) string {
} }
// Drain one item from the FIFO; the outer loop handles the rest. // Drain one item from the FIFO; the outer loop handles the rest.
if next, ok := s.fifo.Pop(); ok { if next, ok := s.sess.PopQueue(); ok {
return next return next
} }

View File

@ -8,13 +8,16 @@ import (
"time" "time"
olog "ollie/log" olog "ollie/log"
"ollie/session"
) )
// newTestCore returns a minimal agent with changeCond wired up. // newTestCore returns a minimal agent with session wired up.
func newTestCore(initialState string) *agent { func newTestCore(initialState string) *agent {
sess := session.New(session.Config{ID: "test"})
sess.SetState(initialState)
a := &agent{ a := &agent{
state: initialState, sess: sess,
log: olog.NewWriter("test", olog.LevelError+1, io.Discard, io.Discard), log: olog.NewWriter("test", olog.LevelError+1, io.Discard, io.Discard),
} }
a.changeCond = sync.NewCond(&a.changeMu) a.changeCond = sync.NewCond(&a.changeMu)
return a return a