diff --git a/agent/compaction.go b/agent/compaction.go
index 7149e88..9ff78f6 100644
--- a/agent/compaction.go
+++ b/agent/compaction.go
@@ -1,16 +1,15 @@
package agent
-import "os"
+import (
+ "os"
-// backendNamer is the subset of backend.Backend needed for model resolution.
-type backendNamer interface {
- Name() string
-}
+ "ollie/backend"
+)
// resolveCompactionModel returns the model to use for compaction.
// Priority: agent config > OLLIE_COMPACTION_MODEL env > per-backend default from models.yaml > session's current model.
// Returns "" if no override is configured (use the session's current model).
-func resolveCompactionModel(cfgModel string, b backendNamer) string {
+func resolveCompactionModel(cfgModel string, b backend.Backend) string {
if cfgModel != "" {
return cfgModel
}
diff --git a/agent/loop.go b/agent/loop.go
index eaa1dde..ed1849d 100644
--- a/agent/loop.go
+++ b/agent/loop.go
@@ -132,7 +132,7 @@ type TurnCtx struct {
ResultCache *sync.Map
}
-func run(rt *Runtime, ctx TurnCtx, state state) error {
+func run(rt *Runtime, ctx TurnCtx, h *History) error {
var step int
var consecutiveErrors int // rounds where every tool call returned an error
var roundsWithoutPlan int // consecutive tool rounds without a PLAN: block
@@ -152,15 +152,15 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
for {
emit(ctx, Event{Role: "state", Content: "thinking"})
// Proactive context gate: strip cold material before calling the backend.
- if budget := contextBudget(ctx, rt.Backend); budget > 0 && state.estimateTokens() > budget {
- state.stripCold(ctx, rt.Backend)
+ if budget := contextBudget(ctx, rt.Backend); budget > 0 && h.estimateTokens() > budget {
+ h.stripCold(ctx, rt.Backend)
}
- history := state.history()
+ history := h.history()
if rt.Preamble != "" {
history = append([]backend.Message{{Role: "system", Content: rt.Preamble}}, history...)
}
- if ts := state.taskState(); ts != nil {
+ if ts := h.taskState(); ts != nil {
if msg := ts.render(); msg != "" && len(history) > 0 {
// Insert after system prompt, before conversation.
history = append([]backend.Message{history[0], {Role: "user", Content: msg}}, history[1:]...)
@@ -269,7 +269,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
IsError: true,
})
}
- state.update(msg, results)
+ h.update(msg, results)
ctx.Save()
return ctx.Err()
}
@@ -537,13 +537,13 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
i = j
}
- state.update(msg, results)
+ h.update(msg, results)
ctx.Save()
// Auto-update TaskState from what actually happened this round.
- if ts := state.taskState(); ts != nil {
+ if ts := h.taskState(); ts != nil {
inferTaskStateUpdate(ts, msg, results, ctx.ReadPlanStep)
- state.updateTaskState(*ts)
+ h.updateTaskState(*ts)
}
if interrupted {
@@ -583,7 +583,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
}
if consecutiveErrors == consecutiveErrorSoftLimit {
// Nudge the model to try a different approach and keep the plan current.
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: "\nYour last several tool calls all failed. Try a different approach, or ask the user for help. Keep your plan current.\n",
}, nil)
@@ -609,7 +609,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
lastErrorSig = sig
}
if repeatErrorCount == 3 {
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: "\nYou have made the same malformed tool call 3 times in a row. Read the error message carefully and fix the arguments. The error was:\n" + strings.TrimSpace(results[0].Content) + "\n",
}, nil)
@@ -627,7 +627,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
roundsWithoutPlan++
}
if roundsWithoutPlan >= replanGate {
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: fmt.Sprintf("\nYou have executed %d tool rounds without replanning. Stop and write a PLAN: block showing your current checklist before calling any more tools. Update session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan.\n", replanGate),
}, nil)
@@ -636,7 +636,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
// Stall detection: if LastAction hasn't changed across rounds, the
// model is likely stuck in a loop.
- if ts := state.taskState(); ts != nil {
+ if ts := h.taskState(); ts != nil {
if ts.LastAction == lastAction && lastAction != "" {
stallRounds++
} else {
@@ -644,7 +644,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
}
lastAction = ts.LastAction
if stallRounds >= stallThreshold {
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: fmt.Sprintf("\nYou have repeated the same action for %d rounds. Step back, reassess your approach, and try something different. Update your plan.\n", stallRounds),
}, nil)
@@ -671,13 +671,13 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
if rt.MaxSteps > 0 {
halfBudget := rt.MaxSteps / 2
if step == halfBudget {
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: fmt.Sprintf("\nYou have used %d/%d research steps without taking action. Consider making progress — write code, edit files, or run commands. Action tools reset this counter.\n", step, rt.MaxSteps),
}, nil)
} else if step >= rt.MaxSteps-1 {
emit(ctx, Event{Role: "maxsteps", Content: fmt.Sprintf("%d", step+1)})
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: fmt.Sprintf("\nStep budget exhausted (%d/%d steps used). Stop calling tools. Summarize what you have done and what remains, then stop.\n", step+1, rt.MaxSteps),
}, nil)
@@ -688,9 +688,9 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
// Periodic task state re-injection: every N tool rounds, surface the
// structured task state back into the conversation to keep the model on track.
if step > 0 && step%planReinjectInterval == 0 {
- if ts := state.taskState(); ts != nil {
+ if ts := h.taskState(); ts != nil {
if msg := ts.render(); msg != "" {
- state.update(backend.Message{
+ h.update(backend.Message{
Role: "user",
Content: "\nReview your current task state and continue. Update it if your approach has changed.\n\n\n" + msg,
}, nil)
diff --git a/agent/state.go b/agent/state.go
index 3e9ce93..b9aaaab 100644
--- a/agent/state.go
+++ b/agent/state.go
@@ -1,8 +1,6 @@
package agent
import (
- "context"
-
"ollie/backend"
)
@@ -24,27 +22,3 @@ type toolResult struct {
IsError bool
Tier ResultTier // default TierHot; tools may self-classify
}
-
-// state is the interface both ephemeral and bead-backed state must satisfy.
-// The loop reads from it on Observe, writes to it on Update.
-type state interface {
- // History returns the full conversation history for the current session.
- // The first entry is always the initial user message containing the goal.
- history() []backend.Message
-
- // taskState returns the current structured task state, or nil if not set.
- taskState() *TaskState
-
- // updateTaskState replaces the current task state.
- updateTaskState(ts TaskState)
-
- // Update appends the assistant's reply and any tool results to the history.
- // Called once per loop iteration after Act completes.
- update(assistant backend.Message, results []toolResult)
-
- // estimateTokens returns a rough token count of the current history.
- estimateTokens() int
-
- // stripCold summarizes large tool-result messages outside the hot tail to reduce context size.
- stripCold(ctx context.Context, b backend.Backend)
-}
diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go
index 799b0b1..222ad24 100644
--- a/cmd/olliesrv/main.go
+++ b/cmd/olliesrv/main.go
@@ -20,6 +20,7 @@ import (
"ollie/fs"
olog "ollie/log"
"ollie/paths"
+ "ollie/session"
"ollie/skills"
"github.com/godbus/dbus/v5"
@@ -127,7 +128,7 @@ func runServer(sockPath string) {
Notify: notifyFn,
Credential: credentialPrompt,
Logf: func(f string, a ...any) { fmt.Printf(f+"\n", a...) },
- SessionValid: func(id string) bool { return fs.Lookup(rootTree, id) != nil },
+ SessionValid: func(id string) bool { return session.Lookup(id) != nil },
})
if err != nil {
fmt.Fprintf(os.Stderr, "warning: %v\n", err)
diff --git a/cmd/olliesrv/server.go b/cmd/olliesrv/server.go
index 3bca8e1..32c9291 100644
--- a/cmd/olliesrv/server.go
+++ b/cmd/olliesrv/server.go
@@ -16,6 +16,7 @@ import (
"ollie/fs"
olog "ollie/log"
+ "ollie/session"
"9fans.net/go/plan9"
)
@@ -678,7 +679,7 @@ func (s *Server) Kill() {
conn.Close()
}
s.connMu.Unlock()
- fs.Shutdown(s.rootTree)
+ session.Shutdown()
done := make(chan struct{})
go func() {
s.serveWG.Wait()
diff --git a/fs/handlers.go b/fs/handlers.go
index c4a7c55..cf234d8 100644
--- a/fs/handlers.go
+++ b/fs/handlers.go
@@ -238,8 +238,8 @@ func requestSessionNew(_ HandlerCtx, data []byte) ([]byte, error) {
// sessionBindings returns bindings for all sessions.
// The Blueprint pattern clones the structure and binds ctx.Session.
-func sessionBindings(_ HandlerCtx) ([]Binding, error) {
- rs := globalRootState
+func sessionBindings(ctx HandlerCtx) ([]Binding, error) {
+ rs := ctx.Root.Data.(*RootState)
sessions := session.Sessions()
out := make([]Binding, 0, len(sessions))
diff --git a/fs/lifecycle.go b/fs/lifecycle.go
index f9cad42..a6f5cca 100644
--- a/fs/lifecycle.go
+++ b/fs/lifecycle.go
@@ -6,10 +6,6 @@ import (
"ollie/session"
)
-// globalRootState is set by NewRoot so handler functions can access
-// root state without going through ctx.Root.
-var globalRootState *RootState
-
// RootState holds the root-level state for the 9P filesystem.
type RootState struct {
Cfg Config
@@ -56,18 +52,3 @@ func (rs *RootState) RenameNode(oldName, newName string) {
rs.SessionNodes[newName] = node
}
}
-
-// Shutdown interrupts all sessions, waits for idle, persists, and closes.
-func Shutdown(root *Tree) {
- session.Shutdown()
-}
-
-// InterruptAll interrupts every active session.
-func InterruptAll(root *Tree) {
- session.InterruptAll()
-}
-
-// Lookup returns the session for the given ID, or nil.
-func Lookup(root *Tree, id string) *session.Session {
- return session.Lookup(id)
-}
diff --git a/fs/newroot.go b/fs/newroot.go
index 8ea37dc..2907964 100644
--- a/fs/newroot.go
+++ b/fs/newroot.go
@@ -75,7 +75,6 @@ func NewRoot(cfg Config) *Tree {
SessionNodes: make(map[string]*SessionNode),
}
tree.Data = rs
- globalRootState = rs
// Restore persisted sessions from disk.
for _, r := range session.RestoreAllSessions() {