diff --git a/cmd/olliesrv/internal/agent/agent.go b/cmd/olliesrv/internal/agent/agent.go index 580de54..815191c 100644 --- a/cmd/olliesrv/internal/agent/agent.go +++ b/cmd/olliesrv/internal/agent/agent.go @@ -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()) diff --git a/cmd/olliesrv/internal/agent/agent_config.go b/cmd/olliesrv/internal/agent/agent_config.go index e362c1c..c598d6f 100644 --- a/cmd/olliesrv/internal/agent/agent_config.go +++ b/cmd/olliesrv/internal/agent/agent_config.go @@ -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"` diff --git a/cmd/olliesrv/internal/agent/commands.go b/cmd/olliesrv/internal/agent/commands.go index 0cd44ae..8fbe1b5 100644 --- a/cmd/olliesrv/internal/agent/commands.go +++ b/cmd/olliesrv/internal/agent/commands.go @@ -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 } diff --git a/cmd/olliesrv/internal/agent/config_paths.go b/cmd/olliesrv/internal/agent/config_paths.go index 93fadd0..5d7fde3 100644 --- a/cmd/olliesrv/internal/agent/config_paths.go +++ b/cmd/olliesrv/internal/agent/config_paths.go @@ -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" } diff --git a/cmd/olliesrv/internal/agent/history.go b/cmd/olliesrv/internal/agent/history.go index 0010c87..5173e51 100644 --- a/cmd/olliesrv/internal/agent/history.go +++ b/cmd/olliesrv/internal/agent/history.go @@ -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). diff --git a/cmd/olliesrv/internal/agent/loop.go b/cmd/olliesrv/internal/agent/loop.go index 7b635bc..9f8baa6 100644 --- a/cmd/olliesrv/internal/agent/loop.go +++ b/cmd/olliesrv/internal/agent/loop.go @@ -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\n"}) hadReasoning = false diff --git a/cmd/olliesrv/internal/agent/text_parse.go b/cmd/olliesrv/internal/agent/text_parse.go index e80ab47..ab39ad6 100644 --- a/cmd/olliesrv/internal/agent/text_parse.go +++ b/cmd/olliesrv/internal/agent/text_parse.go @@ -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 diff --git a/cmd/olliesrv/internal/agent/turn.go b/cmd/olliesrv/internal/agent/turn.go index 435f302..20d7322 100644 --- a/cmd/olliesrv/internal/agent/turn.go +++ b/cmd/olliesrv/internal/agent/turn.go @@ -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() } diff --git a/cmd/olliesrv/internal/backend/kiro.go b/cmd/olliesrv/internal/backend/kiro.go index f2380bf..8c20479 100644 --- a/cmd/olliesrv/internal/backend/kiro.go +++ b/cmd/olliesrv/internal/backend/kiro.go @@ -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 diff --git a/cmd/olliesrv/internal/backend/openai.go b/cmd/olliesrv/internal/backend/openai.go index a902dd6..92608db 100644 --- a/cmd/olliesrv/internal/backend/openai.go +++ b/cmd/olliesrv/internal/backend/openai.go @@ -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" } } diff --git a/cmd/olliesrv/internal/backend/openai_test.go b/cmd/olliesrv/internal/backend/openai_test.go index fc0cf42..d2701ac 100644 --- a/cmd/olliesrv/internal/backend/openai_test.go +++ b/cmd/olliesrv/internal/backend/openai_test.go @@ -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 { diff --git a/cmd/olliesrv/internal/fs/handlers_agent.go b/cmd/olliesrv/internal/fs/handlers_agent.go index a503fcf..8d947e6 100644 --- a/cmd/olliesrv/internal/fs/handlers_agent.go +++ b/cmd/olliesrv/internal/fs/handlers_agent.go @@ -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 {