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:
parent
9e4c7582a3
commit
573c9f4462
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue