9p: call Agent() directly for all agent operations

Session pass-throughs are removed. All agent operations go through
sess.Core.Agent().Method() — proper encapsulation boundary.

Session-level: Bus(), Close(), SetEnv(), CWD(), SetCWD(), Interrupt(),
Queue(), InjectSystemEvent(), SetSessionID(), SaveSession().

Agent-level (via .Agent()): Submit(), State(), Reply(), BackendName(),
ModelName(), IsRunning(), CtxSz(), UsageStr(), Context(), etc.
This commit is contained in:
Levi Neely 2026-07-29 21:58:04 +02:00
parent f142601f5f
commit 50601ebf46
8 changed files with 76 additions and 74 deletions

38
dbus.go
View File

@ -137,7 +137,7 @@ func (a *DBusAdapter) startWatcher(id string, sess *mgr.Session) {
// State watcher
go func() {
current := sess.Core.State()
current := sess.Core.Agent().State()
for {
next, ok := sess.Core.WaitChange(ctx, agent.WatchState, current)
if !ok {
@ -153,7 +153,7 @@ func (a *DBusAdapter) startWatcher(id string, sess *mgr.Session) {
// Agent watcher — emits ConfigChanged when the agent name changes
go func() {
current := sess.Core.AgentName()
current := sess.Core.Agent().Name()
for {
next, ok := sess.Core.WaitChange(ctx, "agent", current)
if !ok {
@ -275,7 +275,7 @@ func (a *DBusAdapter) ListSessions() ([]string, *dbus.Error) {
continue
}
result = append(result, fmt.Sprintf("%s\t%s\t%s\t%s",
id, sess.Core.State(), sess.Core.ModelName(), sess.Core.AgentName()))
id, sess.Core.Agent().State(), sess.Core.Agent().ModelName(), sess.Core.Agent().Name()))
}
return result, nil
}
@ -301,7 +301,7 @@ func (a *DBusAdapter) Submit(sessionID, prompt string) (bool, *dbus.Error) {
if sess == nil {
return false, nil
}
go sess.Core.Submit(sess.Ctx, prompt)
go sess.Core.Agent().Submit(sess.Ctx, prompt)
return true, nil
}
@ -314,7 +314,7 @@ func (a *DBusAdapter) ReactTo(sessionID, responseID, emoji string) (bool, *dbus.
if sess == nil {
return false, nil
}
if err := sess.Core.ReactTo(responseID, emoji); err != nil {
if err := sess.Core.Agent().React(responseID, emoji); err != nil {
return false, dbus.MakeFailedError(err)
}
return true, nil
@ -334,7 +334,7 @@ func (a *DBusAdapter) GetState(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "", nil
}
return sess.Core.State(), nil
return sess.Core.Agent().State(), nil
}
func (a *DBusAdapter) GetChat(sessionID string, offset int64) (string, int64, *dbus.Error) {
@ -361,7 +361,7 @@ func (a *DBusAdapter) GetUsage(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "", nil
}
return sess.Core.Usage(), nil
return sess.Core.Agent().UsageStr(), nil
}
func (a *DBusAdapter) GetCost(sessionID string) (string, *dbus.Error) {
@ -369,7 +369,7 @@ func (a *DBusAdapter) GetCost(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "", nil
}
return sess.Core.Cost(), nil
return sess.Core.Agent().CostStr(), nil
}
func (a *DBusAdapter) GetConfig(sessionID string) (string, *dbus.Error) {
@ -378,11 +378,11 @@ func (a *DBusAdapter) GetConfig(sessionID string) (string, *dbus.Error) {
return "", nil
}
var sb strings.Builder
fmt.Fprintf(&sb, "model=%s\n", sess.Core.ModelName())
fmt.Fprintf(&sb, "backend=%s\n", sess.Core.BackendName())
fmt.Fprintf(&sb, "agent=%s\n", sess.Core.AgentName())
fmt.Fprintf(&sb, "model=%s\n", sess.Core.Agent().ModelName())
fmt.Fprintf(&sb, "backend=%s\n", sess.Core.Agent().BackendName())
fmt.Fprintf(&sb, "agent=%s\n", sess.Core.Agent().Name())
fmt.Fprintf(&sb, "cwd=%s\n", sess.Core.CWD())
fmt.Fprintf(&sb, "state=%s\n", sess.Core.State())
fmt.Fprintf(&sb, "state=%s\n", sess.Core.Agent().State())
return sb.String(), nil
}
@ -393,7 +393,7 @@ func (a *DBusAdapter) SetConfig(sessionID, key, value string) (bool, *dbus.Error
}
switch key {
case "model":
sess.Core.Submit(sess.Ctx, "/model "+value)
sess.Core.Agent().Submit(sess.Ctx, "/model "+value)
case "cwd":
if err := sess.Core.SetCWD(value); err != nil {
return false, nil
@ -409,7 +409,7 @@ func (a *DBusAdapter) GetReactions(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "{}", nil
}
data, err := json.Marshal(sess.Core.Reactions())
data, err := json.Marshal(sess.Core.Agent().Reactions())
if err != nil {
return "", dbus.MakeFailedError(err)
}
@ -421,7 +421,7 @@ func (a *DBusAdapter) GetContext(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "", nil
}
msgs := sess.Core.Context()
msgs := sess.Core.Agent().Context()
var sb strings.Builder
enc := json.NewEncoder(&sb)
enc.SetEscapeHTML(false)
@ -655,7 +655,7 @@ func (a *DBusAdapter) GetSystemPrompt(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "", nil
}
return sess.Core.SystemPrompt(), nil
return sess.Core.Agent().SystemPrompt(), nil
}
func (a *DBusAdapter) GetContextSize(sessionID string) (string, *dbus.Error) {
@ -663,7 +663,7 @@ func (a *DBusAdapter) GetContextSize(sessionID string) (string, *dbus.Error) {
if sess == nil {
return "", nil
}
return sess.Core.CtxSz(), nil
return sess.Core.Agent().CtxSz(), nil
}
func (a *DBusAdapter) GetPreviousPrompt(sessionID string) (string, *dbus.Error) {
@ -682,7 +682,7 @@ func (a *DBusAdapter) Compact(sessionID string) (bool, *dbus.Error) {
if sess == nil {
return false, nil
}
sess.Core.Submit(sess.Ctx, "/compact")
sess.Core.Agent().Submit(sess.Ctx, "/compact")
return true, nil
}
@ -691,7 +691,7 @@ func (a *DBusAdapter) ClearContext(sessionID string) (bool, *dbus.Error) {
if sess == nil {
return false, nil
}
sess.Core.Submit(sess.Ctx, "/clear")
sess.Core.Agent().Submit(sess.Ctx, "/clear")
return true, nil
}

View File

@ -326,7 +326,7 @@ func TestSessionManagerInterruptAll(t *testing.T) {
defer sess.Cancel()
// Submit in background to make it "running"
go sess.Core.Submit(sess.Ctx, "hello")
go sess.Core.Agent().Submit(sess.Ctx, "hello")
time.Sleep(50 * time.Millisecond) // let it enter thinking state
s.AddSession(sess)
@ -381,7 +381,7 @@ func TestSessionManagerRenameErrors(t *testing.T) {
}
// running
sess := testBlockingSession("r")
go sess.Core.Submit(sess.Ctx, "hello")
go sess.Core.Agent().Submit(sess.Ctx, "hello")
time.Sleep(50 * time.Millisecond)
s.AddSession(sess)
if err := s.Tree().Rename("r", "r2"); err == nil {
@ -566,8 +566,8 @@ func TestSessionFileStoreWritePrompt(t *testing.T) {
testStoreWrite(t, sf, "prompt", []byte("hello agent"))
// Verify state is back to idle (Submit completed)
time.Sleep(50 * time.Millisecond)
if sess.Core.State() != "idle" {
t.Errorf("state = %q; want idle after submit", sess.Core.State())
if sess.Core.Agent().State() != "idle" {
t.Errorf("state = %q; want idle after submit", sess.Core.Agent().State())
}
}
@ -577,7 +577,7 @@ func TestSessionFileStoreWriteFifoIn(t *testing.T) {
sf := newTestSessionFileStore(t, sess)
testStoreWrite(t, sf, "fifo.in", []byte("inject this"))
item, ok := sess.Core.PopQueue()
item, ok := sess.Core.Agent().PopQueue()
if !ok || item != "inject this" {
t.Errorf("PopQueue() = %q, %v; want inject this, true", item, ok)
}
@ -606,14 +606,14 @@ func TestSessionFileStoreWriteBackendModelAgent(t *testing.T) {
// With real session + NewBackend func, it switches backend
testStoreWrite(t, sf, "cfg", []byte("backend=openai"))
time.Sleep(50 * time.Millisecond)
if sess.Core.BackendName() != "openai" {
t.Errorf("BackendName() = %q; want openai", sess.Core.BackendName())
if sess.Core.Agent().BackendName() != "openai" {
t.Errorf("BackendName() = %q; want openai", sess.Core.Agent().BackendName())
}
testStoreWrite(t, sf, "cfg", []byte("model=gpt-4"))
time.Sleep(50 * time.Millisecond)
if sess.Core.ModelName() != "gpt-4" {
t.Errorf("ModelName() = %q; want gpt-4", sess.Core.ModelName())
if sess.Core.Agent().ModelName() != "gpt-4" {
t.Errorf("ModelName() = %q; want gpt-4", sess.Core.Agent().ModelName())
}
}
@ -622,7 +622,7 @@ func TestSessionFileStoreWriteBackendWhileRunning(t *testing.T) {
defer sess.Cancel()
// Make session running
go sess.Core.Submit(sess.Ctx, "hello")
go sess.Core.Agent().Submit(sess.Ctx, "hello")
time.Sleep(50 * time.Millisecond)
sf := newTestSessionFileStore(t, sess)
@ -647,7 +647,7 @@ func TestSessionFileStoreWriteParams(t *testing.T) {
sf := newTestSessionFileStore(t, sess)
testStoreWrite(t, sf, "cfg", []byte("maxTokens=2048"))
p := sess.Core.GenerationParams()
p := sess.Core.Agent().GenParams()
if p.MaxTokens != 2048 {
t.Errorf("MaxTokens = %d; want 2048", p.MaxTokens)
}
@ -657,7 +657,7 @@ func TestSessionFileStoreWriteParamsWhileRunning(t *testing.T) {
sess := testBlockingSession("s1")
defer sess.Cancel()
go sess.Core.Submit(sess.Ctx, "hello")
go sess.Core.Agent().Submit(sess.Ctx, "hello")
time.Sleep(50 * time.Millisecond)
sf := newTestSessionFileStore(t, sess)
@ -673,7 +673,7 @@ func TestSessionFileStoreHandleCtl(t *testing.T) {
defer sess.Cancel()
// Make session running for interrupt test
go sess.Core.Submit(sess.Ctx, "hello")
go sess.Core.Agent().Submit(sess.Ctx, "hello")
time.Sleep(50 * time.Millisecond)
sf := newTestSessionFileStore(t, sess)
@ -681,7 +681,7 @@ func TestSessionFileStoreHandleCtl(t *testing.T) {
// stop — interrupts the running session
testStoreWrite(t, sf, "ctl", []byte("stop"))
time.Sleep(50 * time.Millisecond)
if sess.Core.IsRunning() {
if sess.Core.Agent().IsRunning() {
t.Error("ctl stop should interrupt (session should not be running)")
}
@ -730,7 +730,7 @@ func TestSessionFileStoreBlockingRead(t *testing.T) {
time.Sleep(100 * time.Millisecond)
// Submit triggers state change: idle → thinking
go sess.Core.Submit(sess.Ctx, "trigger")
go sess.Core.Agent().Submit(sess.Ctx, "trigger")
select {
case v := <-done:
@ -1067,12 +1067,12 @@ func TestSessionInterrupt(t *testing.T) {
sess := testBlockingSession("s1")
defer sess.Cancel()
go sess.Core.Submit(sess.Ctx, "hello")
go sess.Core.Agent().Submit(sess.Ctx, "hello")
time.Sleep(50 * time.Millisecond)
sess.Interrupt()
time.Sleep(50 * time.Millisecond)
if sess.Core.IsRunning() {
if sess.Core.Agent().IsRunning() {
t.Error("session should not be running after interrupt")
}
}

View File

@ -241,10 +241,10 @@ func (s *Manager) CreateSession(args []string) (string, error) {
s.mu.Unlock()
// HACK: Load the agent now that the session mount is active.
core.Submit(ctx, "/agent "+agentName)
core.Agent().Submit(ctx, "/agent "+agentName)
s.cfg.Log.Info("new session %s (backend=%s model=%s agent=%s)",
sessID, core.BackendName(), core.ModelName(), core.AgentName())
sessID, core.Agent().BackendName(), core.Agent().ModelName(), core.Agent().Name())
if s.cfg.OnSessionCreated != nil {
s.cfg.OnSessionCreated(sessID, sess)
}
@ -271,7 +271,7 @@ func (s *Manager) renameSession(old, new string) error {
s.mu.Unlock()
return fmt.Errorf("session already exists: %s", newID)
}
if sess.Core.IsRunning() {
if sess.Core.Agent().IsRunning() {
s.mu.Unlock()
return fmt.Errorf("cannot rename while agent is running")
}

View File

@ -286,7 +286,7 @@ func (s *Manager) PeerSubmit(sessID, peerID, prompt string) error {
if !isPeer {
return fmt.Errorf("%s is not a peer of %s", peerID, sessID)
}
go peer.Core.Submit(peer.Ctx, prompt)
go peer.Core.Agent().Submit(peer.Ctx, prompt)
return nil
}

View File

@ -291,7 +291,7 @@ func (s *Manager) waitIdle(poll, timeout time.Duration) {
allIdle := true
s.mu.RLock()
for _, sess := range s.sessions {
if sess.Core.State() != "idle" {
if sess.Core.Agent().State() != "idle" {
allIdle = false
break
}
@ -332,11 +332,11 @@ func (s *Manager) index() []byte {
for _, id := range ids {
sess := s.sessions[id]
sess.mu.RLock()
state := sess.Core.State()
state := sess.Core.Agent().State()
cwd := sess.Core.CWD()
be := sess.Core.BackendName()
model := sess.Core.ModelName()
agent := sess.Core.AgentName()
be := sess.Core.Agent().BackendName()
model := sess.Core.Agent().ModelName()
agent := sess.Core.Agent().Name()
sess.mu.RUnlock()
fmt.Fprintf(&sb, "%s\t%s\t%s\t%s\t%s\t%s\n", id, state, cwd, be, model, agent)
}

View File

@ -3,6 +3,7 @@ package mgr
import (
"context"
"sync"
"strings"
"time"
"ollie/session"
@ -53,7 +54,8 @@ func (sess *Session) CachedListModels() string {
sess.modelsMu.Unlock()
// Cache miss — fetch and store.
result := sess.Core.ListModels()
models := sess.Core.Agent().ListModels()
result := strings.Join(models, "\n")
sess.modelsMu.Lock()
sess.modelsCache = result
sess.modelsCacheAt = time.Now()

View File

@ -129,7 +129,7 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
}
case "reactions":
fs.Read = func() ([]byte, error) {
data, err := json.Marshal(h.sess.Core.Reactions())
data, err := json.Marshal(h.sess.Core.Agent().Reactions())
if err != nil {
return nil, err
}
@ -151,7 +151,7 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
}
case "fifo.out":
fs.Read = func() ([]byte, error) {
item, ok := h.sess.Core.PopQueue()
item, ok := h.sess.Core.Agent().PopQueue()
if !ok {
return nil, nil
}
@ -203,7 +203,7 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
h.sess.prevPrompt = []byte(input)
h.sess.mu.Unlock()
go func() {
h.sess.Core.Submit(h.sess.Ctx, input)
h.sess.Core.Agent().Submit(h.sess.Ctx, input)
h.sess.EnsureTrailingNewline()
}()
return nil
@ -264,7 +264,7 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
if req.Emoji == "" {
return fmt.Errorf("react: emoji is required")
}
return h.sess.Core.ReactTo(req.ResponseID, req.Emoji)
return h.sess.Core.Agent().React(req.ResponseID, req.Emoji)
}
}
@ -275,11 +275,11 @@ func (h *sessionHelper) fileSpec(name string, mode os.FileMode) fs.FileSpec {
defer cancel()
context.AfterFunc(h.sess.Ctx, cancel)
if base == "" {
base = h.sess.Core.State()
base = h.sess.Core.Agent().State()
}
v, ok := h.sess.Core.WaitChange(ctx, agent.WatchState, base)
if !ok {
s := h.sess.Core.State()
s := h.sess.Core.Agent().State()
return []byte(s + "\n"), s, nil
}
return []byte(v + "\n"), v, nil
@ -294,18 +294,18 @@ func (h *sessionHelper) content(name string) string {
defer h.sess.mu.RUnlock()
switch name {
case "usage":
return h.sess.Core.Usage() + "\n"
return h.sess.Core.Agent().UsageStr() + "\n"
case "cost":
return h.sess.Core.Cost()
return h.sess.Core.Agent().CostStr()
case "ctxsz":
return h.sess.Core.CtxSz() + "\n"
return h.sess.Core.Agent().CtxSz() + "\n"
case "models":
h.log.Debug("content(models) called")
return h.sess.CachedListModels() + "\n"
case "systemprompt":
return h.sess.Core.SystemPrompt()
return h.sess.Core.Agent().SystemPrompt()
case "state":
return h.sess.Core.State() + "\n"
return h.sess.Core.Agent().State() + "\n"
case "offset":
return fmt.Sprintf("%d\n", h.sess.ChatOffset)
case "env":
@ -316,7 +316,7 @@ func (h *sessionHelper) content(name string) string {
var sb strings.Builder
enc := json.NewEncoder(&sb)
enc.SetEscapeHTML(false)
for _, m := range h.sess.Core.Context() {
for _, m := range h.sess.Core.Agent().Context() {
enc.Encode(m)
}
return sb.String()
@ -374,12 +374,12 @@ func (h *sessionHelper) envContent() string {
func (h *sessionHelper) cfgContent() string {
h.sess.mu.RLock()
defer h.sess.mu.RUnlock()
p := h.sess.Core.GenerationParams()
p := h.sess.Core.Agent().GenParams()
var sb strings.Builder
fmt.Fprintf(&sb, "name=%s\n", h.sess.id)
fmt.Fprintf(&sb, "backend=%s\n", h.sess.Core.BackendName())
fmt.Fprintf(&sb, "model=%s\n", h.sess.Core.ModelName())
fmt.Fprintf(&sb, "agent=%s\n", h.sess.Core.AgentName())
fmt.Fprintf(&sb, "backend=%s\n", h.sess.Core.Agent().BackendName())
fmt.Fprintf(&sb, "model=%s\n", h.sess.Core.Agent().ModelName())
fmt.Fprintf(&sb, "agent=%s\n", h.sess.Core.Agent().Name())
fmt.Fprintf(&sb, "cwd=%s\n", h.sess.Core.CWD())
fmt.Fprintf(&sb, "remote=%s\n", h.sess.remote)
fmt.Fprintf(&sb, "maxTokens=%d\n", p.MaxTokens)
@ -450,12 +450,12 @@ func (h *sessionHelper) cfgContent() string {
} else {
sb.WriteString("verbosity=\n")
}
fmt.Fprintf(&sb, "compactionModel=%s\n", h.sess.Core.CompactionModel())
fmt.Fprintf(&sb, "compactionModel=%s\n", h.sess.Core.Agent().CompactionModel())
return sb.String()
}
func (h *sessionHelper) handleCfg(input string) error {
p := h.sess.Core.GenerationParams()
p := h.sess.Core.Agent().GenParams()
hasParams := false
var deferredBackend, deferredModel string
for _, line := range strings.Split(input, "\n") {
@ -473,7 +473,7 @@ func (h *sessionHelper) handleCfg(input string) error {
if v == "" {
continue
}
if h.sess.Core.IsRunning() {
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot rename while agent is running")
}
if err := h.rename(v); err != nil {
@ -483,15 +483,15 @@ func (h *sessionHelper) handleCfg(input string) error {
if v == "" {
continue
}
if h.sess.Core.IsRunning() {
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot switch %s while agent is running", k)
}
h.sess.Core.Submit(h.sess.Ctx, "/agent "+v)
h.sess.Core.Agent().Submit(h.sess.Ctx, "/agent "+v)
case "backend", "model":
if v == "" {
continue
}
if h.sess.Core.IsRunning() {
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot switch %s while agent is running", k)
}
// Defer backend/model until after the loop so they override
@ -610,22 +610,22 @@ func (h *sessionHelper) handleCfg(input string) error {
hasParams = true
p.Verbosity = v
case "compactionModel":
h.sess.Core.SetCompactionModel(v)
h.sess.Core.Agent().SetCompactionModel(v)
// state → read-only, silently ignored
}
}
// Apply backend/model after agent= so manual overrides win.
if deferredBackend != "" {
h.sess.Core.Submit(h.sess.Ctx, "/backend "+deferredBackend)
h.sess.Core.Agent().Submit(h.sess.Ctx, "/backend "+deferredBackend)
}
if deferredModel != "" {
h.sess.Core.Submit(h.sess.Ctx, "/model "+deferredModel)
h.sess.Core.Agent().Submit(h.sess.Ctx, "/model "+deferredModel)
}
if hasParams {
if h.sess.Core.IsRunning() {
if h.sess.Core.Agent().IsRunning() {
return fmt.Errorf("cannot change params while agent is running")
}
return h.sess.Core.SetGenerationParams(p)
h.sess.Core.Agent().SetGenParams(p)
}
return nil
}
@ -680,7 +680,7 @@ func (h *sessionHelper) handleCtl(input string) error {
"agents", "agent", "sessions", "cwd", "skills",
"tools", "context", "usage", "cost", "history",
"irw", "help":
h.sess.Core.Submit(h.sess.Ctx, "/"+input)
h.sess.Core.Agent().Submit(h.sess.Ctx, "/"+input)
default:
return fmt.Errorf("unknown ctl command: %s", cmd[0])
}

BIN
olliesrv.test Executable file

Binary file not shown.