165 lines
4.0 KiB
Go
165 lines
4.0 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"ollie/backend"
|
|
"ollie/tools"
|
|
)
|
|
|
|
// Agent holds the state of the current Agent entity (the "agent"
|
|
// in the traditional sense). It is swappable: when the user runs /agent,
|
|
// a new Agent is built from the new agent config while the session
|
|
// host remains stable.
|
|
type Agent struct {
|
|
history *History
|
|
runtime *Runtime
|
|
cfg agentConfig // per-turn config built from runtime
|
|
agentName string
|
|
agentsDir string
|
|
baseLayers []string // system prompt layers for /agent reloads
|
|
promptEnvExtra []string // PRIME_* vars for prompt resolution
|
|
newDispatcher func() tools.Dispatcher
|
|
newBackend func(string) (backend.Backend, error)
|
|
currentAction atomic.Pointer[actionHandle]
|
|
warnedContext bool
|
|
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
|
|
uname string // agent principal (numeric UID for 9P identity)
|
|
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
|
|
}
|
|
|
|
// Backend returns the active backend from the runtime.
|
|
func (ag *Agent) Backend() backend.Backend {
|
|
if ag.runtime == nil {
|
|
return nil
|
|
}
|
|
return ag.runtime.Backend
|
|
}
|
|
|
|
// Name returns the agent's name.
|
|
func (ag *Agent) Name() string { return ag.agentName }
|
|
|
|
// BackendName returns the name of the active backend.
|
|
func (ag *Agent) BackendName() string {
|
|
if ag.runtime == nil || ag.runtime.Backend == nil {
|
|
return ""
|
|
}
|
|
return ag.runtime.Backend.Name()
|
|
}
|
|
|
|
// ModelName returns the name of the active model.
|
|
func (ag *Agent) ModelName() string {
|
|
if ag.runtime == nil || ag.runtime.Backend == nil {
|
|
return ""
|
|
}
|
|
return ag.runtime.Backend.Model()
|
|
}
|
|
|
|
// State returns the agent's current execution state.
|
|
func (ag *Agent) State() string {
|
|
ag.stateMu.RLock()
|
|
s := ag.state
|
|
ag.stateMu.RUnlock()
|
|
return s
|
|
}
|
|
|
|
// SetState sets the agent's execution state and notifies waiters.
|
|
func (ag *Agent) SetState(state string) {
|
|
ag.stateMu.Lock()
|
|
ag.state = state
|
|
ag.stateMu.Unlock()
|
|
ag.notifyChange()
|
|
}
|
|
|
|
// Reply returns the agent's last assistant response.
|
|
func (ag *Agent) Reply() string {
|
|
ag.stateMu.RLock()
|
|
r := ag.reply
|
|
ag.stateMu.RUnlock()
|
|
return r
|
|
}
|
|
|
|
// SetReply sets the agent's last response.
|
|
func (ag *Agent) SetReply(reply string) {
|
|
ag.stateMu.Lock()
|
|
ag.reply = reply
|
|
ag.stateMu.Unlock()
|
|
}
|
|
|
|
// notifyChange wakes all goroutines waiting on state changes.
|
|
func (ag *Agent) notifyChange() {
|
|
ag.changeMu.Lock()
|
|
ag.changeCond.Broadcast()
|
|
ag.changeMu.Unlock()
|
|
}
|
|
|
|
// WaitChange blocks until the agent's state differs from current.
|
|
// Returns the new value and true, or ("", false) if ctx is cancelled.
|
|
func (ag *Agent) WaitChange(ctx context.Context, field, current string) (string, bool) {
|
|
done := make(chan struct{})
|
|
context.AfterFunc(ctx, func() {
|
|
ag.changeMu.Lock()
|
|
ag.changeCond.Broadcast()
|
|
ag.changeMu.Unlock()
|
|
close(done)
|
|
})
|
|
|
|
ag.changeMu.Lock()
|
|
for {
|
|
var val string
|
|
switch field {
|
|
case WatchState:
|
|
val = ag.State()
|
|
default:
|
|
ag.changeMu.Unlock()
|
|
return "", false
|
|
}
|
|
if val != current {
|
|
ag.changeMu.Unlock()
|
|
return val, true
|
|
}
|
|
if ctx.Err() != nil {
|
|
ag.changeMu.Unlock()
|
|
return "", false
|
|
}
|
|
ag.changeCond.Wait()
|
|
if ctx.Err() != nil {
|
|
ag.changeMu.Unlock()
|
|
return "", false
|
|
}
|
|
}
|
|
}
|
|
|
|
// InitCond initializes the changeCond. Must be called once after construction.
|
|
func (ag *Agent) InitCond() {
|
|
ag.changeCond = sync.NewCond(&ag.changeMu)
|
|
}
|
|
|
|
// CWD returns the agent's working directory.
|
|
func (ag *Agent) Cwd() string {
|
|
ag.stateMu.RLock()
|
|
c := ag.cwd
|
|
ag.stateMu.RUnlock()
|
|
return c
|
|
}
|
|
|
|
// SetCWD sets the agent's working directory (no validation — caller must validate).
|
|
func (ag *Agent) SetCwd(dir string) {
|
|
ag.stateMu.Lock()
|
|
ag.cwd = dir
|
|
ag.stateMu.Unlock()
|
|
}
|