From 31d3466b3b0b28493ff9b29f367e6c16ed3e0727 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 11 Aug 2026 21:41:12 +0200 Subject: [PATCH] agent: add chat log fields and methods Moved log buffer, plan, mutex, and condvar onto agent.Agent directly. These were previously on the fs package's AgentLog type. The fs package will be migrated to use these in the next commit. New methods: AppendChat, EnsureTrailingNewline, ChatMu, ChatCond, ChatLog, Plan, SetPlan. --- cmd/olliesrv/internal/agent/agent.go | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index 88e47e6..f29403d 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -56,6 +56,13 @@ type Agent struct { save func() // trigger debounced persistence flush func() // immediately flush persistence onStateChange func(agentID, state string) // optional callback for state changes + + // Chat log — the rendered output stream, exposed via 9P. + chatMu sync.RWMutex + chatLog []byte + chatVers uint32 + chatCond *sync.Cond + plan []byte } // Backend returns the active backend from the runtime. @@ -116,6 +123,56 @@ func (ag *Agent) SetName(name string) { // ID returns the agent's unique identity. func (ag *Agent) ID() string { return ag.id } +// --- Chat log methods --- + +// AppendChat appends data to the chat log and notifies stream readers. +func (ag *Agent) AppendChat(data []byte) { + if len(data) == 0 { + return + } + ag.chatMu.Lock() + ag.chatLog = append(ag.chatLog, data...) + ag.chatVers++ + ag.chatMu.Unlock() + ag.chatCond.Broadcast() +} + +// EnsureTrailingNewline appends a newline if the log doesn't end with one. +func (ag *Agent) EnsureTrailingNewline() { + ag.chatMu.Lock() + if len(ag.chatLog) > 0 && ag.chatLog[len(ag.chatLog)-1] != '\n' { + ag.chatLog = append(ag.chatLog, '\n') + } + ag.chatVers++ + ag.chatMu.Unlock() +} + +// ChatMu returns the chat log mutex for external locking (streaming). +func (ag *Agent) ChatMu() *sync.RWMutex { return &ag.chatMu } + +// ChatCond returns the condvar for blocking chat readers. +func (ag *Agent) ChatCond() *sync.Cond { return ag.chatCond } + +// ChatLog returns the raw chat log bytes (caller must hold ChatMu.RLock). +func (ag *Agent) ChatLog() []byte { return ag.chatLog } + +// Plan returns a copy of the plan. +func (ag *Agent) Plan() []byte { + ag.chatMu.RLock() + p := make([]byte, len(ag.plan)) + copy(p, ag.plan) + ag.chatMu.RUnlock() + return p +} + +// SetPlan replaces the plan. +func (ag *Agent) SetPlan(data []byte) { + ag.chatMu.Lock() + ag.plan = make([]byte, len(data)) + copy(ag.plan, data) + ag.chatMu.Unlock() +} + // BackendName returns the name of the active backend. func (ag *Agent) BackendName() string { if ag.runtime.Backend == nil { @@ -651,6 +708,7 @@ func NewAgent(cfg AgentParams) *Agent { state: "idle", } ag.signalCh = make(chan struct{}) + ag.chatCond = sync.NewCond(ag.chatMu.RLocker()) ag.SetSessionEnv(cfg.SessionID) return ag }