diff --git a/agent/agent.go b/agent/agent.go index 49ea24a..676984e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -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", diff --git a/agent/agent_config.go b/agent/agent_config.go index a2bfa21..09d96a4 100644 --- a/agent/agent_config.go +++ b/agent/agent_config.go @@ -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, } } diff --git a/agent/history.go b/agent/history.go index 411b402..9e6f736 100644 --- a/agent/history.go +++ b/agent/history.go @@ -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. diff --git a/agent/loop.go b/agent/loop.go index 1b173a1..c0ee43c 100644 --- a/agent/loop.go +++ b/agent/loop.go @@ -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("\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) - 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("\nYou have repeated the same action for %d rounds. Step back, reassess your approach, and try something different. Update your plan.\n", 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: "\nReview your current task state and continue. Update it if your approach has changed.\n\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 diff --git a/agent/turn.go b/agent/turn.go index 3fb8595..2f490d6 100644 --- a/agent/turn.go +++ b/agent/turn.go @@ -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 diff --git a/backend/backend.go b/backend/backend.go index 48dd5f1..cc29cfb 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -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. diff --git a/session/persist.go b/session/persist.go index 1a51d61..543e19f 100644 --- a/session/persist.go +++ b/session/persist.go @@ -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 diff --git a/session/session.go b/session/session.go index 83e77bd..ba1d15d 100644 --- a/session/session.go +++ b/session/session.go @@ -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()