diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index ed438059f..c1011bcd2 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -148,6 +148,8 @@ func NewAgentInstance( candidates := providers.ResolveCandidatesWithLookup(modelCfg, defaults.Provider, resolveFromModelList) + contextWindow := resolveContextWindow(model, maxTokens) + return &AgentInstance{ ID: agentID, Name: agentName, @@ -157,7 +159,7 @@ func NewAgentInstance( MaxIterations: maxIter, MaxTokens: maxTokens, Temperature: temperature, - ContextWindow: maxTokens, + ContextWindow: contextWindow, Provider: provider, Sessions: sessionsManager, ContextBuilder: contextBuilder, @@ -223,3 +225,29 @@ func expandHome(path string) string { } return path } + +// resolveContextWindow returns the actual context window size for a model. +// This is used for summarization thresholds and should reflect the model's +// real capacity rather than the output max_tokens setting. +func resolveContextWindow(model string, maxTokens int) int { + m := strings.ToLower(model) + switch { + case strings.Contains(m, "claude"): + return 200000 + case strings.Contains(m, "gpt-5"), strings.Contains(m, "gpt-4"): + return 128000 + case strings.Contains(m, "gemini"): + return 1000000 + case strings.Contains(m, "deepseek"): + return 64000 + case strings.Contains(m, "llama"): + return 128000 + case strings.Contains(m, "qwen"): + return 32000 + default: + if maxTokens > 32000 { + return maxTokens + } + return 32000 + } +} diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 0df637493..8f315ece9 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -123,7 +123,7 @@ func registerSharedTools( } else if searchTool != nil { agent.Tools.Register(searchTool) } - fetchTool, err := tools.NewWebFetchToolWithProxy(50000, cfg.Tools.Web.Proxy, cfg.Tools.Web.FetchLimitBytes) + fetchTool, err := tools.NewWebFetchToolWithProxy(20000, cfg.Tools.Web.Proxy, cfg.Tools.Web.FetchLimitBytes) if err != nil { logger.ErrorCF("agent", "Failed to create web fetch tool", map[string]any{"error": err.Error()}) } else { @@ -661,6 +661,7 @@ func (al *AgentLoop) runLLMIteration( ) (string, int, error) { iteration := 0 var finalContent string + var totalInputTokens, totalOutputTokens int for iteration < agent.MaxIterations { iteration++ @@ -805,6 +806,11 @@ func (al *AgentLoop) runLLMIteration( go al.handleReasoning(ctx, response.Reasoning, opts.Channel, al.targetReasoningChannelID(opts.Channel)) + if response.Usage != nil { + totalInputTokens += response.Usage.PromptTokens + totalOutputTokens += response.Usage.CompletionTokens + } + logger.DebugCF("agent", "LLM response", map[string]any{ "agent_id": agent.ID, @@ -968,6 +974,17 @@ func (al *AgentLoop) runLLMIteration( } } + if totalInputTokens > 0 || totalOutputTokens > 0 { + logger.InfoCF("agent", "Session token usage", + map[string]any{ + "agent_id": agent.ID, + "iterations": iteration, + "input_tokens": totalInputTokens, + "output_tokens": totalOutputTokens, + "total_tokens": totalInputTokens + totalOutputTokens, + }) + } + return finalContent, iteration, nil } diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 44f4de7e9..59140bb65 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -30,9 +30,9 @@ func DefaultConfig() *Config { RestrictToWorkspace: true, Provider: "", Model: "", - MaxTokens: 32768, - Temperature: nil, // nil means use provider default - MaxToolIterations: 50, + MaxTokens: 8192, + Temperature: nil, // nil means use provider default + MaxToolIterations: 15, }, }, Bindings: []AgentBinding{}, diff --git a/pkg/providers/anthropic/provider.go b/pkg/providers/anthropic/provider.go index c393c57e4..1f77f16b8 100644 --- a/pkg/providers/anthropic/provider.go +++ b/pkg/providers/anthropic/provider.go @@ -237,7 +237,7 @@ func buildParams( func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam { result := make([]anthropic.ToolUnionParam, 0, len(tools)) - for _, t := range tools { + for i, t := range tools { tool := anthropic.ToolParam{ Name: t.Function.Name, InputSchema: anthropic.ToolInputSchemaParam{ @@ -256,6 +256,9 @@ func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam { } tool.InputSchema.Required = required } + if i == len(tools)-1 { + tool.CacheControl = anthropic.NewCacheControlEphemeralParam() + } result = append(result, anthropic.ToolUnionParam{OfTool: &tool}) } return result diff --git a/pkg/tools/filesystem.go b/pkg/tools/filesystem.go index cd8da3195..62eb703c6 100644 --- a/pkg/tools/filesystem.go +++ b/pkg/tools/filesystem.go @@ -117,6 +117,8 @@ func (t *ReadFileTool) Parameters() map[string]any { } } +const maxReadFileChars = 10000 + func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult { path, ok := args["path"].(string) if !ok { @@ -127,7 +129,12 @@ func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolRe if err != nil { return ErrorResult(err.Error()) } - return NewToolResult(string(content)) + + text := string(content) + if len(text) > maxReadFileChars { + text = text[:maxReadFileChars] + fmt.Sprintf("\n\n... (truncated, showing %d of %d chars. Use exec with head/tail/sed for specific sections)", maxReadFileChars, len(content)) + } + return NewToolResult(text) } type WriteFileTool struct { diff --git a/pkg/tools/web.go b/pkg/tools/web.go index 10498126b..30ccfe603 100644 --- a/pkg/tools/web.go +++ b/pkg/tools/web.go @@ -22,7 +22,7 @@ const ( perplexityTimeout = 30 * time.Second // Perplexity (LLM-based, slower) fetchTimeout = 60 * time.Second // WebFetchTool - defaultMaxChars = 50000 + defaultMaxChars = 20000 maxRedirects = 5 )