all: delete dead interfaces, kill globalRootState

- agent: delete 'state' interface (one implementation: *History). run()
  now takes *History directly.
- agent: delete 'backendNamer' interface. resolveCompactionModel takes
  backend.Backend directly.
- fs: delete Shutdown/InterruptAll/Lookup wrappers that took *Tree they
  never used. Callers (olliesrv) now call session.* directly.
- fs: delete globalRootState. sessionBindings uses ctx.Root.Data.(*RootState)
  via the HandlerCtx that was always available.
This commit is contained in:
Ollie Agent 2026-08-08 14:58:47 +02:00
parent 63c4cd6173
commit 3b83b9d373
8 changed files with 29 additions and 74 deletions

View File

@ -1,16 +1,15 @@
package agent package agent
import "os" import (
"os"
// backendNamer is the subset of backend.Backend needed for model resolution. "ollie/backend"
type backendNamer interface { )
Name() string
}
// resolveCompactionModel returns the model to use for compaction. // 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. // 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). // 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 != "" { if cfgModel != "" {
return cfgModel return cfgModel
} }

View File

@ -132,7 +132,7 @@ type TurnCtx struct {
ResultCache *sync.Map 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 step int
var consecutiveErrors int // rounds where every tool call returned an error var consecutiveErrors int // rounds where every tool call returned an error
var roundsWithoutPlan int // consecutive tool rounds without a PLAN: block var roundsWithoutPlan int // consecutive tool rounds without a PLAN: block
@ -152,15 +152,15 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
for { for {
emit(ctx, Event{Role: "state", Content: "thinking"}) emit(ctx, Event{Role: "state", Content: "thinking"})
// Proactive context gate: strip cold material before calling the backend. // Proactive context gate: strip cold material before calling the backend.
if budget := contextBudget(ctx, rt.Backend); budget > 0 && state.estimateTokens() > budget { if budget := contextBudget(ctx, rt.Backend); budget > 0 && h.estimateTokens() > budget {
state.stripCold(ctx, rt.Backend) h.stripCold(ctx, rt.Backend)
} }
history := state.history() history := h.history()
if rt.Preamble != "" { if rt.Preamble != "" {
history = append([]backend.Message{{Role: "system", Content: rt.Preamble}}, history...) 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 { if msg := ts.render(); msg != "" && len(history) > 0 {
// Insert after system prompt, before conversation. // Insert after system prompt, before conversation.
history = append([]backend.Message{history[0], {Role: "user", Content: msg}}, history[1:]...) 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, IsError: true,
}) })
} }
state.update(msg, results) h.update(msg, results)
ctx.Save() ctx.Save()
return ctx.Err() return ctx.Err()
} }
@ -537,13 +537,13 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
i = j i = j
} }
state.update(msg, results) h.update(msg, results)
ctx.Save() ctx.Save()
// Auto-update TaskState from what actually happened this round. // 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) inferTaskStateUpdate(ts, msg, results, ctx.ReadPlanStep)
state.updateTaskState(*ts) h.updateTaskState(*ts)
} }
if interrupted { if interrupted {
@ -583,7 +583,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
} }
if consecutiveErrors == consecutiveErrorSoftLimit { if consecutiveErrors == consecutiveErrorSoftLimit {
// Nudge the model to try a different approach and keep the plan current. // Nudge the model to try a different approach and keep the plan current.
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: "<system-consecutive-errors>\nYour last several tool calls all failed. Try a different approach, or ask the user for help. Keep your plan current.\n</system-consecutive-errors>", Content: "<system-consecutive-errors>\nYour last several tool calls all failed. Try a different approach, or ask the user for help. Keep your plan current.\n</system-consecutive-errors>",
}, nil) }, nil)
@ -609,7 +609,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
lastErrorSig = sig lastErrorSig = sig
} }
if repeatErrorCount == 3 { if repeatErrorCount == 3 {
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: "<system-repeated-error>\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</system-repeated-error>", Content: "<system-repeated-error>\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</system-repeated-error>",
}, nil) }, nil)
@ -627,7 +627,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
roundsWithoutPlan++ roundsWithoutPlan++
} }
if roundsWithoutPlan >= replanGate { if roundsWithoutPlan >= replanGate {
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: fmt.Sprintf("<system-replan-required>\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</system-replan-required>", replanGate), Content: fmt.Sprintf("<system-replan-required>\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</system-replan-required>", replanGate),
}, nil) }, nil)
@ -636,7 +636,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
// Stall detection: if LastAction hasn't changed across rounds, the // Stall detection: if LastAction hasn't changed across rounds, the
// model is likely stuck in a loop. // model is likely stuck in a loop.
if ts := state.taskState(); ts != nil { if ts := h.taskState(); ts != nil {
if ts.LastAction == lastAction && lastAction != "" { if ts.LastAction == lastAction && lastAction != "" {
stallRounds++ stallRounds++
} else { } else {
@ -644,7 +644,7 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
} }
lastAction = ts.LastAction lastAction = ts.LastAction
if stallRounds >= stallThreshold { if stallRounds >= stallThreshold {
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: fmt.Sprintf("<system-stall-detected>\nYou have repeated the same action for %d rounds. Step back, reassess your approach, and try something different. Update your plan.\n</system-stall-detected>", stallRounds), Content: fmt.Sprintf("<system-stall-detected>\nYou have repeated the same action for %d rounds. Step back, reassess your approach, and try something different. Update your plan.\n</system-stall-detected>", stallRounds),
}, nil) }, nil)
@ -671,13 +671,13 @@ func run(rt *Runtime, ctx TurnCtx, state state) error {
if rt.MaxSteps > 0 { if rt.MaxSteps > 0 {
halfBudget := rt.MaxSteps / 2 halfBudget := rt.MaxSteps / 2
if step == halfBudget { if step == halfBudget {
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: fmt.Sprintf("<system-step-budget-warning>\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</system-step-budget-warning>", step, rt.MaxSteps), Content: fmt.Sprintf("<system-step-budget-warning>\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</system-step-budget-warning>", step, rt.MaxSteps),
}, nil) }, nil)
} else if step >= rt.MaxSteps-1 { } else if step >= rt.MaxSteps-1 {
emit(ctx, Event{Role: "maxsteps", Content: fmt.Sprintf("%d", step+1)}) emit(ctx, Event{Role: "maxsteps", Content: fmt.Sprintf("%d", step+1)})
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: fmt.Sprintf("<system-step-budget-stop>\nStep budget exhausted (%d/%d steps used). Stop calling tools. Summarize what you have done and what remains, then stop.\n</system-step-budget-stop>", step+1, rt.MaxSteps), Content: fmt.Sprintf("<system-step-budget-stop>\nStep budget exhausted (%d/%d steps used). Stop calling tools. Summarize what you have done and what remains, then stop.\n</system-step-budget-stop>", step+1, rt.MaxSteps),
}, nil) }, 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 // Periodic task state re-injection: every N tool rounds, surface the
// structured task state back into the conversation to keep the model on track. // structured task state back into the conversation to keep the model on track.
if step > 0 && step%planReinjectInterval == 0 { if step > 0 && step%planReinjectInterval == 0 {
if ts := state.taskState(); ts != nil { if ts := h.taskState(); ts != nil {
if msg := ts.render(); msg != "" { if msg := ts.render(); msg != "" {
state.update(backend.Message{ h.update(backend.Message{
Role: "user", Role: "user",
Content: "<system-task-state-review>\nReview your current task state and continue. Update it if your approach has changed.\n</system-task-state-review>\n\n" + msg, Content: "<system-task-state-review>\nReview your current task state and continue. Update it if your approach has changed.\n</system-task-state-review>\n\n" + msg,
}, nil) }, nil)

View File

@ -1,8 +1,6 @@
package agent package agent
import ( import (
"context"
"ollie/backend" "ollie/backend"
) )
@ -24,27 +22,3 @@ type toolResult struct {
IsError bool IsError bool
Tier ResultTier // default TierHot; tools may self-classify 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)
}

View File

@ -20,6 +20,7 @@ import (
"ollie/fs" "ollie/fs"
olog "ollie/log" olog "ollie/log"
"ollie/paths" "ollie/paths"
"ollie/session"
"ollie/skills" "ollie/skills"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
@ -127,7 +128,7 @@ func runServer(sockPath string) {
Notify: notifyFn, Notify: notifyFn,
Credential: credentialPrompt, Credential: credentialPrompt,
Logf: func(f string, a ...any) { fmt.Printf(f+"\n", a...) }, 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 { if err != nil {
fmt.Fprintf(os.Stderr, "warning: %v\n", err) fmt.Fprintf(os.Stderr, "warning: %v\n", err)

View File

@ -16,6 +16,7 @@ import (
"ollie/fs" "ollie/fs"
olog "ollie/log" olog "ollie/log"
"ollie/session"
"9fans.net/go/plan9" "9fans.net/go/plan9"
) )
@ -678,7 +679,7 @@ func (s *Server) Kill() {
conn.Close() conn.Close()
} }
s.connMu.Unlock() s.connMu.Unlock()
fs.Shutdown(s.rootTree) session.Shutdown()
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
s.serveWG.Wait() s.serveWG.Wait()

View File

@ -238,8 +238,8 @@ func requestSessionNew(_ HandlerCtx, data []byte) ([]byte, error) {
// sessionBindings returns bindings for all sessions. // sessionBindings returns bindings for all sessions.
// The Blueprint pattern clones the structure and binds ctx.Session. // The Blueprint pattern clones the structure and binds ctx.Session.
func sessionBindings(_ HandlerCtx) ([]Binding, error) { func sessionBindings(ctx HandlerCtx) ([]Binding, error) {
rs := globalRootState rs := ctx.Root.Data.(*RootState)
sessions := session.Sessions() sessions := session.Sessions()
out := make([]Binding, 0, len(sessions)) out := make([]Binding, 0, len(sessions))

View File

@ -6,10 +6,6 @@ import (
"ollie/session" "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. // RootState holds the root-level state for the 9P filesystem.
type RootState struct { type RootState struct {
Cfg Config Cfg Config
@ -56,18 +52,3 @@ func (rs *RootState) RenameNode(oldName, newName string) {
rs.SessionNodes[newName] = node 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)
}

View File

@ -75,7 +75,6 @@ func NewRoot(cfg Config) *Tree {
SessionNodes: make(map[string]*SessionNode), SessionNodes: make(map[string]*SessionNode),
} }
tree.Data = rs tree.Data = rs
globalRootState = rs
// Restore persisted sessions from disk. // Restore persisted sessions from disk.
for _, r := range session.RestoreAllSessions() { for _, r := range session.RestoreAllSessions() {