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() {