remove dead code: TaskState subsystem, Verbosity, dead session fields

Remove entire TaskState machinery:
- TaskState struct, render(), taskState()/updateTaskState() on History
- inferTaskStateUpdate() — heuristic plan tracking
- Replan gate (replanGate const, roundsWithoutPlan counter)
- Stall detection (stallThreshold, stallRounds, lastAction)
- Plan reinject (planReinjectInterval, periodic re-injection)
- ReadPlanStep plumbing (AgentCfg → Agent → TurnCtx → loop)

None of this worked in practice — the model ignores the nudges,
ReadPlanStep was never wired to a non-nil value, and TaskState
fields were only populated by inferTaskStateUpdate (which depends
on ReadPlanStep being set).

Compaction now uses plain summary text in the cold zone instead
of serialized TaskState JSON.

Also remove:
- Verbosity field (declared, threaded, never read by any backend)
- DisallowTools (config field never wired to session)
- Session.listHandlers, plan, prevPrompt (never read)
- Config.Output, Config.ListHandlers, Config.ReadPlanStep (never set)

-246 net lines
This commit is contained in:
Ollie Agent 2026-08-09 01:03:19 +02:00
parent 625654fcef
commit 5c81eefe0a
8 changed files with 25 additions and 271 deletions

View File

@ -51,7 +51,6 @@ type Agent struct {
log *olog.Logger
sessionID string // the owning session's ID
startupMessages []string
readPlanStep func() string
save func() // trigger debounced persistence
flush func() // immediately flush persistence
onStateChange func(agentID, state string) // optional callback for state changes
@ -76,14 +75,6 @@ func (ag *Agent) Messages() []backend.Message {
return ag.history.history()
}
// TaskState returns the current task state, if any.
func (ag *Agent) TaskState() *TaskState {
if ag.history == nil {
return nil
}
return ag.history.TaskState
}
// UsageStats holds token usage and cost information.
type UsageStats struct {
TotalInputTokens int
@ -570,7 +561,6 @@ type AgentCfg struct {
Output EventHandler
Log *olog.Logger
StartupMsgs []string
ReadPlanStep func() string
Save func()
Flush func()
}
@ -594,7 +584,6 @@ func NewAgent(cfg AgentCfg) *Agent {
log: cfg.Log,
sessionID: cfg.SessionID,
startupMessages: cfg.StartupMsgs,
readPlanStep: cfg.ReadPlanStep,
save: cfg.Save,
flush: cfg.Flush,
state: "idle",

View File

@ -36,7 +36,6 @@ type AgentConfig struct {
Tools *bool `json:"tools,omitempty"`
AllowTools []string `json:"allowTools,omitempty"`
AutoLoad []string `json:"autoLoad,omitempty"`
DisallowTools []string `json:"disallowTools,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
MaxCompletionTokens int `json:"maxCompletionTokens,omitempty"`
// MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited.
@ -54,10 +53,8 @@ type AgentConfig struct {
IncludeReasoning *bool `json:"includeReasoning,omitempty"`
ResponseFormat string `json:"responseFormat,omitempty"`
Stop []string `json:"stop,omitempty"`
Verbosity string `json:"verbosity,omitempty"`
CompactionModel string `json:"compactionModel,omitempty"`
// SystemPrompt overrides the embedded system prompt with a file path.
// If set and the file exists, its content replaces the compiled-in default.
SystemPrompt string `json:"systemPrompt,omitempty"`
}
@ -94,6 +91,5 @@ func (c *AgentConfig) GenParams() backend.GenerationParams {
IncludeReasoning: c.IncludeReasoning,
ResponseFormat: c.ResponseFormat,
Stop: c.Stop,
Verbosity: c.Verbosity,
}
}

View File

@ -36,45 +36,6 @@ Be concise. Capture what another LLM needs to seamlessly continue.`
warmIndexSize = 10
)
// TaskState is a compact structured overlay that summarizes the agent's
// current position in a task. Injected at the top of every turn so the
// model doesn't rely on full history recall.
type TaskState struct {
Objective string `json:"objective"`
PlanStep string `json:"plan_step"`
Constraints []string `json:"constraints,omitempty"`
LastAction string `json:"last_action"`
OpenQuestions []string `json:"open_questions,omitempty"`
NextDecision string `json:"next_decision"`
}
// render produces a compact text representation for injection into the message history.
func (ts *TaskState) render() string {
if ts.Objective == "" {
return ""
}
var sb strings.Builder
sb.WriteString("[task state]\n")
sb.WriteString("objective: " + ts.Objective + "\n")
if ts.PlanStep != "" {
sb.WriteString("plan_step: " + ts.PlanStep + "\n")
}
if len(ts.Constraints) > 0 {
sb.WriteString("constraints: " + strings.Join(ts.Constraints, "; ") + "\n")
}
if ts.LastAction != "" {
sb.WriteString("last_action: " + ts.LastAction + "\n")
}
if len(ts.OpenQuestions) > 0 {
sb.WriteString("open_questions: " + strings.Join(ts.OpenQuestions, "; ") + "\n")
}
if ts.NextDecision != "" {
sb.WriteString("next_decision: " + ts.NextDecision + "\n")
}
sb.WriteString("[/task state]")
return sb.String()
}
// RestoreHistoryFromMessages reconstructs a History from just the message list.
// Used for multi-agent session restore where each agent has separate messages.
func RestoreHistoryFromMessages(messages []backend.Message) *History {
@ -97,9 +58,8 @@ func RestoreHistoryFromMessages(messages []backend.Message) *History {
// History is an ephemeral in-memory state backend.
type History struct {
goal string
messages []backend.Message
TaskState *TaskState
goal string
messages []backend.Message
// Cumulative usage tracking.
TotalInputTokens int
TotalCachedInputTokens int
@ -128,14 +88,6 @@ func (s *History) history() []backend.Message {
return s.messages
}
func (s *History) taskState() *TaskState {
return s.TaskState
}
func (s *History) updateTaskState(ts TaskState) {
s.TaskState = &ts
}
func (s *History) addUsage(u backend.Usage, estimated bool) {
s.TotalInputTokens += u.InputTokens
s.TotalCachedInputTokens += u.CachedInputTokens
@ -294,25 +246,16 @@ func (s *History) compact(ctx context.Context, b backend.Backend) (int, string,
return 0, "", fmt.Errorf("compact: empty summary")
}
// Parse structured task state from the model's JSON response.
var ts TaskState
raw := extractJSON(summaryText)
if err := json.Unmarshal([]byte(raw), &ts); err != nil {
// Fallback: treat as unstructured summary if JSON parse fails.
ts = TaskState{Objective: summaryText}
}
s.TaskState = &ts
beforeCount := len(s.messages)
s.messages = buildCompactedHistory(ts, s.messages)
s.messages = buildCompactedHistory(summaryText, s.messages)
return beforeCount - len(s.messages), summaryText, nil
}
// buildCompactedHistory constructs a three-zone post-compaction message list:
// - Cold: structured task state summary (everything older than warm+hot)
// - Cold: compaction summary (everything older than warm+hot)
// - Warm: brief index of recent decisions/actions (warmIndexSize messages)
// - Hot: last hotTailSize messages verbatim
func buildCompactedHistory(ts TaskState, allMessages []backend.Message) []backend.Message {
func buildCompactedHistory(summary string, allMessages []backend.Message) []backend.Message {
total := len(allMessages)
// Determine zone boundaries.
@ -335,11 +278,10 @@ func buildCompactedHistory(ts TaskState, allMessages []backend.Message) []backen
warmStart = 0
}
// Cold zone: structured task state.
stateJSON, _ := json.Marshal(ts)
// Cold zone: compaction summary.
cold := backend.Message{
Role: "user",
Content: "[compacted context — cold zone]\n```json\n" + string(stateJSON) + "\n```",
Content: "[compacted context]\n" + summary,
}
var out []backend.Message
@ -399,31 +341,6 @@ func summarizeMessage(m backend.Message) string {
}
// extractJSON finds the first JSON object in s (handling markdown fences).
func extractJSON(s string) string {
// Strip markdown code fence if present.
if i := strings.Index(s, "```json"); i >= 0 {
s = s[i+7:]
if j := strings.Index(s, "```"); j >= 0 {
s = s[:j]
}
} else if i := strings.Index(s, "```"); i >= 0 {
s = s[i+3:]
if j := strings.Index(s, "```"); j >= 0 {
s = s[:j]
}
}
// Find first { ... }
start := strings.Index(s, "{")
if start < 0 {
return s
}
end := strings.LastIndex(s, "}")
if end < start {
return s
}
return s[start : end+1]
}
// flattenToolMessages converts tool call/result sequences into plain text
// so the compaction request doesn't include tool-specific structures that
// the API may reject when no tools are defined.

View File

@ -30,18 +30,6 @@ const (
consecutiveErrorHardLimit = 10
)
// replanGate is the max consecutive tool rounds without a PLAN: block before
// a mandatory replan nudge is injected.
const replanGate = 8
// stallThreshold is the number of consecutive rounds where LastAction doesn't
// change before injecting a stall nudge.
const stallThreshold = 5
// planReinjectInterval is the number of tool-call rounds between periodic
// task state re-injection into the conversation.
const planReinjectInterval = 10
type toolExecutor func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error)
// cachedResult stores a tool result with file metadata for staleness detection.
@ -127,7 +115,6 @@ type TurnCtx struct {
PopInject func() string // returns and clears pending inject, or ""
AutoCompact func(ctx context.Context) // called after each tool round; may compact in-place
Save func() // called after each state.update(); persists mid-turn progress
ReadPlanStep func() string // next unchecked step from plan file, or ""
IncrToolCallCount func() int64
ResultCache *sync.Map
}
@ -135,9 +122,6 @@ type TurnCtx struct {
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
var lastAction string // previous round's LastAction for stall detection
var stallRounds int // consecutive rounds with unchanged LastAction
var lastErrorSig string // signature of previous round's error(s) for repeat detection
var repeatErrorCount int // consecutive rounds with identical error signature
// resultCache stores outputs of read-safe tool calls keyed by name+args.
@ -160,12 +144,6 @@ func run(rt *Runtime, ctx TurnCtx, h *History) error {
if preamble := rt.PreambleString(); preamble != "" {
history = append([]backend.Message{{Role: "system", Content: preamble}}, history...)
}
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:]...)
}
}
// Stream the assistant's response, retrying on rate limits, transient
// backend errors (5xx, network), and mid-stream drops. One stable ID is
@ -540,12 +518,6 @@ func run(rt *Runtime, ctx TurnCtx, h *History) error {
h.update(msg, results)
ctx.Save()
// Auto-update TaskState from what actually happened this round.
if ts := h.taskState(); ts != nil {
inferTaskStateUpdate(ts, msg, results, ctx.ReadPlanStep)
h.updateTaskState(*ts)
}
if interrupted {
return ctx.Err()
}
@ -619,43 +591,7 @@ func run(rt *Runtime, ctx TurnCtx, h *History) error {
lastErrorSig = ""
}
// Replan gate: if the model has been calling tools without emitting a
// PLAN: block, force it to replan before continuing.
if strings.Contains(msg.Content, "PLAN:") {
roundsWithoutPlan = 0
} else {
roundsWithoutPlan++
}
if roundsWithoutPlan >= replanGate {
h.update(backend.Message{
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),
}, nil)
roundsWithoutPlan = 0
}
// Stall detection: if LastAction hasn't changed across rounds, the
// model is likely stuck in a loop.
if ts := h.taskState(); ts != nil {
if ts.LastAction == lastAction && lastAction != "" {
stallRounds++
} else {
stallRounds = 0
}
lastAction = ts.LastAction
if stallRounds >= stallThreshold {
h.update(backend.Message{
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),
}, nil)
stallRounds = 0
}
}
// Reset step counter when an action-oriented tool is called successfully.
// This allows the agent to continue working without hitting the soft
// step-budget guardrail, as long as it's making active progress (writing
// files, renaming symbols, etc.) rather than looping on research.
if rt.ToolMeta != nil {
for _, r := range results {
if !r.IsError && rt.ToolMeta[r.Name].ResetsCounter {
@ -666,8 +602,6 @@ func run(rt *Runtime, ctx TurnCtx, h *History) error {
}
// Step budget warnings: nudge the agent at 50% and when exhausted.
// These only fire for research-heavy loops — action tools (file_write,
// file_edit, shell) reset the counter, so active progress never triggers.
if rt.MaxSteps > 0 {
halfBudget := rt.MaxSteps / 2
if step == halfBudget {
@ -685,19 +619,6 @@ func run(rt *Runtime, ctx TurnCtx, h *History) 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 := h.taskState(); ts != nil {
if msg := ts.render(); msg != "" {
h.update(backend.Message{
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,
}, nil)
}
}
}
if ctx.AutoCompact != nil {
ctx.AutoCompact(ctx)
}
@ -727,53 +648,6 @@ func contextBudget(ctx context.Context, b backend.Backend) int {
return ctxLen / 2
}
// inferTaskStateUpdate updates TaskState fields from the actual tool round
// without requiring model cooperation. Keeps the state current even if the
// model ignores re-injection nudges.
func inferTaskStateUpdate(ts *TaskState, msg backend.Message, results []toolResult, readPlanStep func() string) {
// LastAction: summarize what tools ran and whether they succeeded.
if len(msg.ToolCalls) > 0 {
names := make([]string, 0, len(msg.ToolCalls))
for _, tc := range msg.ToolCalls {
names = append(names, tc.Name)
}
errCount := 0
for _, r := range results {
if r.IsError {
errCount++
}
}
action := strings.Join(names, ", ")
if errCount > 0 {
action += fmt.Sprintf(" (%d/%d failed)", errCount, len(results))
}
ts.LastAction = action
}
// PlanStep: prefer the plan file (ground truth), fall back to text heuristic.
if readPlanStep != nil {
if step := readPlanStep(); step != "" {
ts.PlanStep = step
return
}
}
// Fallback: extract from PLAN: block if present in assistant text.
if idx := strings.Index(msg.Content, "PLAN:"); idx >= 0 {
lines := strings.Split(msg.Content[idx:], "\n")
for _, line := range lines[1:] {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "- [ ]") {
ts.PlanStep = strings.TrimSpace(trimmed[5:])
break
}
if trimmed == "" || (!strings.HasPrefix(trimmed, "- [") && trimmed != "") {
break
}
}
}
}
// defaultToolResultMaxBytes caps tool result content sent back to the model.
const defaultToolResultMaxBytes = 131072

View File

@ -120,10 +120,9 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
}
turnCtx := TurnCtx{
Output: ag.output,
ReadPlanStep: ag.readPlanStep,
Save: ag.save,
ResultCache: &ag.resultCache,
Output: ag.output,
Save: ag.save,
ResultCache: &ag.resultCache,
}
var replyBuf strings.Builder

View File

@ -138,7 +138,6 @@ type GenerationParams struct {
IncludeReasoning *bool // include reasoning in response (OpenRouter)
ResponseFormat string // "json_object", "text", etc.
Stop []string // stop sequences
Verbosity string // internal: controls output detail level
}
// Backend is the interface all LLM providers must implement.

View File

@ -34,7 +34,6 @@ type PersistedAgent struct {
Model string `json:"model"`
CWD string `json:"cwd"`
Messages []backend.Message `json:"messages"`
TaskState *agent.TaskState `json:"taskState,omitempty"`
// Usage tracking
TotalInputTokens int `json:"totalInputTokens,omitempty"`
TotalCachedInputTokens int `json:"totalCachedInputTokens,omitempty"`
@ -77,8 +76,7 @@ func PersistSession(name string) error {
Backend: ag.BackendName(),
Model: ag.ModelName(),
CWD: ag.Cwd(),
Messages: ag.Messages(),
TaskState: ag.TaskState(),
Messages: ag.Messages(),
}
if usage := ag.Usage(); usage != nil {
pa.TotalInputTokens = usage.TotalInputTokens

View File

@ -35,13 +35,10 @@ type Config struct {
NewBackend func(string) (backend.Backend, error)
Log *olog.Logger
MaxSteps int
ReadPlanStep func() string
ListHandlers map[string]func() []string
PromptEnvExtra []string
Remote string
SystemPrompt string
EnvBlock string
Output agent.EventHandler
}
// Session owns all session-level state: identity, agents, tool server lifecycle.
@ -60,26 +57,20 @@ type Session struct {
Remote string
// Agent state
agents []*agent.Agent
log *olog.Logger
sessionsDir string
listHandlers map[string]func() []string
agents []*agent.Agent
log *olog.Logger
sessionsDir string
// Environment
envMu sync.RWMutex
env map[string]string
plan []byte
prevPrompt string
envMu sync.RWMutex
env map[string]string
// Mutable state protected by mu
mu sync.RWMutex
name string // friendly name
toolsConn *toolsrv.Conn // multiplexed RPC connection
name string // friendly name
toolsConn *toolsrv.Conn // multiplexed RPC connection
paused bool
// Tool restrictions
disallowTools map[string]struct{}
// Autosave
saveMu sync.Mutex
saveDirty bool
@ -181,14 +172,13 @@ func New(cfg Config) *Session {
}
s := &Session{
ID: cfg.SessionID,
name: name,
Remote: cfg.Remote,
env: make(map[string]string),
agents: make([]*agent.Agent, 0, 1),
log: log,
sessionsDir: cfg.SessionsDir,
listHandlers: cfg.ListHandlers,
ID: cfg.SessionID,
name: name,
Remote: cfg.Remote,
env: make(map[string]string),
agents: make([]*agent.Agent, 0, 1),
log: log,
sessionsDir: cfg.SessionsDir,
}
ag := agent.NewAgent(agent.AgentCfg{
@ -203,11 +193,9 @@ func New(cfg Config) *Session {
PromptEnvExtra: cfg.PromptEnvExtra,
NewToolServer: cfg.NewToolServer,
NewBackend: cfg.NewBackend,
Output: cfg.Output,
Log: log,
SessionID: cfg.SessionID,
StartupMsgs: rt.Messages,
ReadPlanStep: cfg.ReadPlanStep,
Save: s.saveSession,
Flush: s.flushSave,
})
@ -405,12 +393,6 @@ func (s *Session) LoadTool(name string, ag *agent.Agent) error {
if name == "" {
return nil
}
s.mu.RLock()
_, blocked := s.disallowTools[name]
s.mu.RUnlock()
if blocked {
return fmt.Errorf("tool %q is disallowed for this session", name)
}
var conn *toolsrv.Conn
if s.ToolsConn() != nil {
conn = s.ToolsConn()