cleanup: fix structural inconsistencies, remove dead code

- backend/openai: fix DefaultModel() dead 'anthropic' case, default to gpt-4o
- agent: SetCWD no longer sets stale envBlock first, uses runtime.GOOS
- backend/kiro: ContextLength cache now checks ctxModel consistently
- agent: AgentConfigPath uses agentsDir as the only search location
- fs: ctl 'tools' handler delegates to readAgentTools (dedup)
- agent: remove redundant TurnCtx field assignments in executeTurn
- agent: remove spending cap feature entirely (OLLIE_MAX_SESSION_COST)
- agent: inline stripColdResults into stripCold, remove wrapper
- agent: remove orphaned doc comment at end of text_parse.go
- agent: remove dead Prompt.MarshalJSON
- agent: cloneMessages now copies ID, Reasoning, ContentBlocks
- agent: fix newHistory comment (Session → History)
- agent: fix orphaned CompactionModel godoc on ListModels
- agent: toolCalls only captured from Done event (fix contract)
- agent: HandleCommand uses lib9p client instead of shelling out
This commit is contained in:
Ollie Agent 2026-08-11 10:46:38 +02:00
parent 9e4c7582a3
commit 573c9f4462
12 changed files with 42 additions and 96 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"runtime"
"slices"
"sync"
"sync/atomic"
@ -362,7 +363,7 @@ func (ag *Agent) SwitchProfile(name string) error {
// - "tool" → logs result
// - "state" → calls ag.SetState()
// - "limitretry"→ sets state to "limitretry"
// - "usage" → parses token counts, updates history, checks spending cap
// - "usage" → parses token counts, updates history
// - "error" → logs
type Event struct {
Role string
@ -423,11 +424,8 @@ func (ag *Agent) SetCWD(dir string) {
ag.stateMu.Unlock()
// Rebuild the environment section with the new cwd
if ag.runtime != nil && ag.runtime.Preamble != nil {
ag.runtime.Preamble.Set(SectionEnv, ag.envBlock)
// Update envBlock to reflect new cwd for future agent reloads
// (envBlock is set at construction; for cwd changes we just rebuild)
isGitRepo := paths.IsGitRepo(dir)
ag.envBlock = EnvironmentBlock(dir, "linux", isGitRepo, "")
ag.envBlock = EnvironmentBlock(dir, runtime.GOOS, isGitRepo, "")
ag.runtime.Preamble.Set(SectionEnv, ag.envBlock)
}
if ag.runtime.ToolServer != nil {
@ -548,7 +546,6 @@ func (ag *Agent) SetGenParams(params backend.GenerationParams) {
ag.runtime.GenParams = params
}
// CompactionModel returns the configured compaction model name.
// ListModels returns available models from the backend.
func (ag *Agent) ListModels() []string {
return ag.runtime.Backend.Models(context.Background())

View File

@ -22,13 +22,6 @@ func (p *Prompt) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &p.Value)
}
func (p Prompt) MarshalJSON() ([]byte, error) {
if len(p.Value) == 1 {
return json.Marshal(p.Value[0])
}
return json.Marshal(p.Value)
}
type AgentConfig struct {
Prompt Prompt `json:"prompt,omitempty"`
Backend string `json:"backend,omitempty"`

View File

@ -2,12 +2,13 @@ package agent
import (
"context"
"os/exec"
"strings"
lib9p "ollie/lib9p/client"
)
// HandleCommand intercepts /commands before they reach the LLM.
// / prefix routes to ctl via o: /compact → o session/agent ctl compact.
// Each slash command is a rdwr to the agent's ctl file: /compact → write "compact" to ctl.
// Returns true if the input was consumed as a command.
func (ag *Agent) HandleCommand(_ context.Context, input string) bool {
if !strings.HasPrefix(input, "/") {
@ -17,16 +18,20 @@ func (ag *Agent) HandleCommand(_ context.Context, input string) bool {
if rest == "" {
return false
}
// Use session/agent context syntax
ctx := ag.sessionID + "/" + ag.id
args := append([]string{ctx, "ctl"}, strings.Fields(rest)...)
cmd := exec.Command("o", args...)
out, err := cmd.CombinedOutput()
if len(out) > 0 {
ag.emit(infoEvent(strings.TrimRight(string(out), "\n")))
}
fsys, err := lib9p.DialDefault()
if err != nil {
ag.emit(infoEvent("error: " + err.Error()))
return true
}
defer fsys.Close()
ctlPath := "session/" + ag.sessionID + "/agent/" + ag.id + "/ctl"
out, err := fsys.Rdwr(ctlPath, []byte(rest))
if err != nil {
ag.emit(infoEvent("error: " + err.Error()))
return true
}
if len(out) > 0 {
ag.emit(infoEvent(strings.TrimRight(string(out), "\n")))
}
return true
}

View File

@ -13,12 +13,6 @@ func AgentsDirs() []string {
// AgentConfigPath resolves the config file path for a named agent.
func AgentConfigPath(agentsDir, name string) string {
for _, dir := range AgentsDirs() {
p := dir + "/" + name + ".json"
if _, err := os.Stat(p); err == nil {
return p
}
}
return agentsDir + "/" + name + ".json"
}

View File

@ -76,7 +76,7 @@ type History struct {
turnCostUSD float64 // >0 when backend reported cost directly (e.g. OpenRouter)
}
// newHistory creates a new empty Session. The caller is responsible for
// newHistory creates a new empty History. The caller is responsible for
// appending the initial user message via appendUserMessage.
func newHistory(goal string) *History {
return &History{goal: goal}
@ -380,10 +380,16 @@ func cloneMessages(msgs []backend.Message) []backend.Message {
out := make([]backend.Message, len(msgs))
for i, m := range msgs {
out[i] = backend.Message{
ID: m.ID,
Role: m.Role,
Content: m.Content,
Reasoning: m.Reasoning,
ToolCallID: m.ToolCallID,
}
if len(m.ContentBlocks) > 0 {
out[i].ContentBlocks = make([]backend.ContentBlock, len(m.ContentBlocks))
copy(out[i].ContentBlocks, m.ContentBlocks)
}
if len(m.ToolCalls) > 0 {
out[i].ToolCalls = make([]backend.ToolCall, len(m.ToolCalls))
for j, tc := range m.ToolCalls {
@ -410,9 +416,9 @@ func (s *History) estimateTokens() int {
return chars / 4
}
// stripColdResults summarizes large tool-result messages outside the hot tail
// stripCold summarizes large tool-result messages outside the hot tail
// using an LLM call. Messages in the last hotTailSize slots are left verbatim.
func (s *History) stripColdResults(ctx context.Context, b backend.Backend) {
func (s *History) stripCold(ctx context.Context, b backend.Backend) {
hot := len(s.messages) - hotTailSize
for i := range s.messages {
if i >= hot {
@ -425,11 +431,6 @@ func (s *History) stripColdResults(ctx context.Context, b backend.Backend) {
}
}
// stripCold implements the state interface.
func (s *History) stripCold(ctx context.Context, b backend.Backend) {
s.stripColdResults(ctx, b)
}
// 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).

View File

@ -294,8 +294,8 @@ func streamResponse(rt *Runtime, ctx TurnCtx, history []backend.Message, respons
contentBuf.WriteString(ev.Content)
emit(ctx, Event{Role: "assistant", Content: ev.Content, ResponseID: responseID})
}
toolCalls = append(toolCalls, ev.ToolCalls...)
if ev.Done {
toolCalls = append(toolCalls, ev.ToolCalls...)
if hadReasoning {
emit(ctx, Event{Role: "reasoning", Content: "\n</think>\n"})
hadReasoning = false

View File

@ -284,5 +284,3 @@ func relaxJSON(s string) string {
}
return out.String()
}
// transientWait returns the retry wait for a retryable error and whether it is

View File

@ -6,7 +6,6 @@ import (
"fmt"
"os"
"runtime/debug"
"strconv"
"strings"
"ollie/cmd/olliesrv/internal/backend"
@ -71,9 +70,8 @@ func (ag *Agent) Submit(ctx context.Context, input string) {
func (ag *Agent) executeTurn(ctx context.Context, input string) string {
ag.emit(Event{Role: "user", Content: input})
// Snapshot session state before this turn modifies it. Restored on failure
// so the session is clean for the next attempt.
snapSession := ag.history
// Snapshot messages before this turn modifies them. Restored on context
// overflow so the retry starts from a clean state.
var snapMessages []backend.Message
if ag.history != nil {
snapMessages = cloneMessages(ag.history.messages)
@ -120,11 +118,8 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
}
turnCtx := TurnCtx{
Output: ag.output,
Save: ag.save,
ResultCache: &ag.resultCache,
BgTracker: &ag.bgProcs,
ToolServer: ag.runtime.ToolServer,
BgTracker: &ag.bgProcs,
ToolServer: ag.runtime.ToolServer,
}
var replyBuf strings.Builder
@ -156,12 +151,6 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
CostUSD: costUSD,
}, est != 0)
ag.notifyChange()
if maxCostStr := os.Getenv("OLLIE_MAX_SESSION_COST"); maxCostStr != "" {
if limit, ferr := strconv.ParseFloat(maxCostStr, 64); ferr == nil && limit > 0 && ag.history.SessionCostUSD >= limit {
ag.emit(infoEvent(fmt.Sprintf("spending cap $%.2f reached — stopping", limit)))
ag.Interrupt(ErrInterrupted)
}
}
}
ag.emit(ev)
}
@ -213,24 +202,6 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
}
}
// Spending cap: reject before spending more tokens.
if maxCostStr := os.Getenv("OLLIE_MAX_SESSION_COST"); maxCostStr != "" && ag.history != nil {
if limit, err := strconv.ParseFloat(maxCostStr, 64); err == nil && limit > 0 {
if ag.history.SessionCostUSD >= limit {
ag.emit(Event{Role: "error", Content: fmt.Sprintf("spending cap $%.2f reached (session total $%.4f)", limit, ag.history.SessionCostUSD)})
ag.SetState("idle")
actCancel(nil)
ag.currentAction.CompareAndSwap(handle, nil)
if snapSession == nil {
ag.history = nil
} else {
ag.history.messages = snapMessages
}
return ""
}
}
}
if ag.history != nil {
ag.history.resetTurnAccumulators()
}

View File

@ -114,7 +114,7 @@ func (b *KiroBackend) fetchModels(ctx context.Context) *kiroListModelsResponse {
}
func (b *KiroBackend) ContextLength(ctx context.Context) int {
if b.ctxLen > 0 {
if b.ctxLen > 0 && b.ctxModel == b.model {
return b.ctxLen
}
resp := b.fetchModels(ctx)
@ -124,11 +124,13 @@ func (b *KiroBackend) ContextLength(ctx context.Context) int {
for _, m := range resp.Models {
if m.TokenLimits != nil && (b.model == "auto" || m.ModelID == b.model) {
b.ctxLen = m.TokenLimits.MaxInputTokens
b.ctxModel = b.model
return b.ctxLen
}
}
if resp.DefaultModel != nil && resp.DefaultModel.TokenLimits != nil {
b.ctxLen = resp.DefaultModel.TokenLimits.MaxInputTokens
b.ctxModel = b.model
return b.ctxLen
}
return 0

View File

@ -105,10 +105,10 @@ func (b *OpenAIBackend) DefaultModel() string {
switch b.name {
case "openrouter":
return "deepseek/deepseek-v3.2"
case "anthropic":
return "claude-sonnet-4-5"
case "gemini":
return geminiDefaultModel
default:
return "qwen3.5:9b"
return "gpt-4o"
}
}

View File

@ -423,9 +423,9 @@ func TestDefaultModel(t *testing.T) {
want string
}{
{"openrouter", "deepseek/deepseek-v3.2"},
{"anthropic", "claude-sonnet-4-5"},
{"openai", "qwen3.5:9b"},
{"unknown", "qwen3.5:9b"},
{"gemini", geminiDefaultModel},
{"openai", "gpt-4o"},
{"unknown", "gpt-4o"},
}
for _, tt := range tests {
if got := (&OpenAIBackend{baseBackend: baseBackend{name: tt.name}}).DefaultModel(); got != tt.want {

View File

@ -329,22 +329,7 @@ var agentCtlHandlers = map[string]rdwrHandler{
return []byte(ctx.Session.CachedListModels() + "\n"), nil
},
"tools": func(ctx HandlerCtx, _ []string) ([]byte, error) {
if ctx.Session.ToolsConn() == nil {
return []byte("(no tool server)\n"), nil
}
loaded, err := ctx.Session.ToolsConn().ListTools()
if err != nil {
return nil, err
}
var sb strings.Builder
for _, ti := range loaded {
if ti.Description != "" {
fmt.Fprintf(&sb, "%-20s %s\n", ti.Name, ti.Description)
} else {
sb.WriteString(ti.Name + "\n")
}
}
return []byte(sb.String()), nil
return readAgentTools(ctx)
},
"tool_load": func(ctx HandlerCtx, args []string) ([]byte, error) {
if len(args) == 0 {