remove maxSteps and ResetsCounter entirely

The step budget mechanism is gone. Agents run until they finish,
are interrupted by the user, or (for sub-agents) hit the timeout.

No replacement. The human is the kill switch.
This commit is contained in:
Levi Neely 2026-08-17 09:48:58 +02:00
parent da0ee4456f
commit 74ba5ae7ac
16 changed files with 27 additions and 79 deletions

View File

@ -30,8 +30,6 @@ type AgentConfig struct {
Tools *bool `json:"tools,omitempty"` Tools *bool `json:"tools,omitempty"`
AutoLoad []string `json:"autoLoad,omitempty"` AutoLoad []string `json:"autoLoad,omitempty"`
// MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited.
MaxSteps int `json:"maxSteps,omitempty"`
CompactionModel string `json:"compactionModel,omitempty"` CompactionModel string `json:"compactionModel,omitempty"`
// SystemPrompt overrides the embedded system prompt with a file path. // SystemPrompt overrides the embedded system prompt with a file path.
SystemPrompt string `json:"systemPrompt,omitempty"` SystemPrompt string `json:"systemPrompt,omitempty"`

View File

@ -162,59 +162,31 @@ func (ag *Agent) run(ctx context.Context) error {
} }
} }
msg := backend.Message{ID: responseID, Role: "assistant", Content: content, Reasoning: reasoning, ToolCalls: toolCalls} msg := backend.Message{ID: responseID, Role: "assistant", Content: content, Reasoning: reasoning, ToolCalls: toolCalls}
results, interrupted := ag.execToolCalls(ctx, toolCalls) results, interrupted := ag.execToolCalls(ctx, toolCalls)
h.update(msg, results) h.update(msg, results)
ag.save() ag.save()
if interrupted { if interrupted {
return ctx.Err() return ctx.Err()
}
if len(toolCalls) == 0 {
break
}
if abort := ag.trackErrors(ctx, results, &errs, step); abort != nil {
return abort
}
// Reset step counter when an action-oriented tool succeeds.
if rt.ToolMeta != nil {
for _, r := range results {
if !r.IsError && rt.ToolMeta[r.Name].ResetsCounter {
step = 0
break
}
} }
} if len(toolCalls) == 0 {
// Step budget warnings.
if rt.MaxSteps > 0 {
halfBudget := rt.MaxSteps / 2
if step == halfBudget {
h.update(backend.Message{
Role: "user",
Content: fmt.Sprintf("<system-step-budget-warning>\nYou have used %d/%d research steps without taking action. Consider making progress — write code, edit files, or run commands. Action tools reset this counter.\n</system-step-budget-warning>", step, rt.MaxSteps),
}, nil)
} else if step >= rt.MaxSteps-1 {
ag.emit(Event{Role: "maxsteps", Content: fmt.Sprintf("%d", step+1)})
h.update(backend.Message{
Role: "user",
Content: fmt.Sprintf("<system-step-budget-stop>\nStep budget exhausted (%d/%d steps used). Stop calling tools. Summarize what you have done and what remains, then stop.\n</system-step-budget-stop>", step+1, rt.MaxSteps),
}, nil)
break break
} }
}
_, compactErr := ag.autoCompact(ctx) if abort := ag.trackErrors(ctx, results, &errs, step); abort != nil {
if compactErr != nil { return abort
ag.emit(Event{Role: "error", Content: compactErr.Error()}) }
break
}
step++ _, compactErr := ag.autoCompact(ctx)
} if compactErr != nil {
ag.emit(Event{Role: "error", Content: compactErr.Error()})
break
}
step++
}
return nil return nil
} }

View File

@ -145,7 +145,6 @@ type Runtime struct {
ToolMeta map[string]protocol.ToolInfo ToolMeta map[string]protocol.ToolInfo
Exec toolExecutor Exec toolExecutor
GenParams backend.GenerationParams GenParams backend.GenerationParams
MaxSteps int
CompactionModel string CompactionModel string
UserPrompt string // resolved user prompts, prepended to each turn's input UserPrompt string // resolved user prompts, prepended to each turn's input
Messages []string Messages []string
@ -179,7 +178,6 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
var agentPrompt string var agentPrompt string
var userPrompt string var userPrompt string
var genParams backend.GenerationParams var genParams backend.GenerationParams
var maxSteps int
if cfg != nil { if cfg != nil {
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil { if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err) fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
@ -192,7 +190,6 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
userPrompt = resolved userPrompt = resolved
} }
genParams = cfg.GenerationParams genParams = cfg.GenerationParams
maxSteps = cfg.MaxSteps
} }
genParams.CWD = cwd genParams.CWD = cwd
@ -235,7 +232,6 @@ func BuildRuntime(cfg *AgentConfig, srv *toolclient.ToolsrvConn, cwd string, env
Exec: exec, Exec: exec,
Preamble: preamble, Preamble: preamble,
GenParams: genParams, GenParams: genParams,
MaxSteps: maxSteps,
CompactionModel: compactionModel, CompactionModel: compactionModel,
UserPrompt: userPrompt, UserPrompt: userPrompt,
Messages: messages, Messages: messages,

View File

@ -27,7 +27,6 @@
"lsp_rename", "lsp_rename",
"lsp_completion" "lsp_completion"
], ],
"maxSteps": 25,
"temperature": 0.5, "temperature": 0.5,
"maxTokens": 16384 "maxTokens": 16384
} }

View File

@ -41,7 +41,6 @@
], ],
"backend": "openrouter", "backend": "openrouter",
"model": "deepseek/deepseek-v4-flash", "model": "deepseek/deepseek-v4-flash",
"maxSteps": 25,
"temperature": 0.4, "temperature": 0.4,
"maxTokens": 16384 "maxTokens": 16384
} }

View File

@ -26,7 +26,6 @@
"code_rewrite", "code_rewrite",
"code_dependencies" "code_dependencies"
], ],
"maxSteps": 40,
"temperature": 0.3, "temperature": 0.3,
"maxTokens": 16384 "maxTokens": 16384
} }

View File

@ -15,7 +15,6 @@
"memory_recall", "memory_recall",
"memory_remember" "memory_remember"
], ],
"maxSteps": 20,
"temperature": 0.3, "temperature": 0.3,
"maxTokens": 16384 "maxTokens": 16384
} }

View File

@ -28,6 +28,5 @@
"code_dependencies" "code_dependencies"
], ],
"temperature": 0.3, "temperature": 0.3,
"maxTokens": 8192, "maxTokens": 8192
"maxSteps": 30
} }

View File

@ -23,7 +23,6 @@
"lsp_rename", "lsp_rename",
"lsp_completion" "lsp_completion"
], ],
"maxSteps": 5,
"temperature": 0.3, "temperature": 0.3,
"maxTokens": 4096 "maxTokens": 4096
} }

View File

@ -17,7 +17,6 @@
"memory_recall", "memory_recall",
"memory_remember" "memory_remember"
], ],
"maxSteps": 20,
"temperature": 0.3, "temperature": 0.3,
"maxTokens": 8192 "maxTokens": 8192
} }

View File

@ -17,7 +17,6 @@
"file_grep", "file_grep",
"file_write" "file_write"
], ],
"maxSteps": 30,
"temperature": 0.3, "temperature": 0.3,
"maxTokens": 8192 "maxTokens": 8192
} }

View File

@ -1,7 +1,6 @@
{ {
"description": "Replace text in a file. Has fuzzy matching (whitespace/indent flexible).", "description": "Replace text in a file. Has fuzzy matching (whitespace/indent flexible).",
"outputFormat": "diff", "outputFormat": "diff",
"resetsCounter": true,
"prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional)\n\n```\nfile_edit(path=\"<ABSOLUTE_PATH_FROM_PWD>\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"<ABSOLUTE_PATH_FROM_PWD>\", old_string=\"old text\", new_string=\"new text\", replace_all=\"true\")\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations \u2014 add surrounding context to disambiguate, or use `replace_all=true`.", "prompt": "## file_edit\n\nReplace text in a file. Has fuzzy matching (whitespace/indent flexible).\n\n**Args**: `path`, `old_string`, `new_string` (required); `replace_all` (optional)\n\n```\nfile_edit(path=\"<ABSOLUTE_PATH_FROM_PWD>\", old_string=\"old text\", new_string=\"new text\")\nfile_edit(path=\"<ABSOLUTE_PATH_FROM_PWD>\", old_string=\"old text\", new_string=\"new text\", replace_all=\"true\")\n```\n\n**Constraints**: Absolute paths only. Errors if `old_string` matches multiple locations \u2014 add surrounding context to disambiguate, or use `replace_all=true`.",
"args": { "args": {
"type": "object", "type": "object",

View File

@ -1,7 +1,6 @@
{ {
"description": "Create or overwrite a file. Produces a unified diff.", "description": "Create or overwrite a file. Produces a unified diff.",
"outputFormat": "diff", "outputFormat": "diff",
"resetsCounter": true,
"prompt": "## file_write\n\nCreate or overwrite a file. Produces a unified diff.\n\n**Args**: `[path, content]`\n\n```\nfile_write(path=\"<ABSOLUTE_PATH_FROM_PWD>\", content=\"file content here\")\n```\n\n**Constraints**: Absolute paths only. Parent directory must exist.", "prompt": "## file_write\n\nCreate or overwrite a file. Produces a unified diff.\n\n**Args**: `[path, content]`\n\n```\nfile_write(path=\"<ABSOLUTE_PATH_FROM_PWD>\", content=\"file content here\")\n```\n\n**Constraints**: Absolute paths only. Parent directory must exist.",
"args": { "args": {
"type": "object", "type": "object",

View File

@ -1,6 +1,5 @@
{ {
"description": "Execute a single bash command in a sandboxed environment.", "description": "Execute a single bash command in a sandboxed environment.",
"resetsCounter": true,
"prompt": "Execute a single bash command in a sandboxed environment.\n\nUsage: {\"cmd\": \"your command here\"}\n\nSandbox prevents dangerous operations. Use for computation, builds, scripting.\ntimeout applies to each call (default: 30s). A non-zero exit is an error.", "prompt": "Execute a single bash command in a sandboxed environment.\n\nUsage: {\"cmd\": \"your command here\"}\n\nSandbox prevents dangerous operations. Use for computation, builds, scripting.\ntimeout applies to each call (default: 30s). A non-zero exit is an error.",
"args": { "args": {
"type": "object", "type": "object",

View File

@ -19,7 +19,6 @@ type MetaFile struct {
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
OutputFormat string `json:"outputFormat,omitempty"` OutputFormat string `json:"outputFormat,omitempty"`
Cmd string `json:"cmd,omitempty"` Cmd string `json:"cmd,omitempty"`
ResetsCounter bool `json:"resetsCounter,omitempty"`
Variants []Variant `json:"variants,omitempty"` Variants []Variant `json:"variants,omitempty"`
} }
@ -35,7 +34,6 @@ type Variant struct {
Scope string `json:"scope,omitempty"` Scope string `json:"scope,omitempty"`
OutputFormat string `json:"outputFormat,omitempty"` OutputFormat string `json:"outputFormat,omitempty"`
Cmd string `json:"cmd,omitempty"` Cmd string `json:"cmd,omitempty"`
ResetsCounter bool `json:"resetsCounter,omitempty"`
} }
// LoadMetaFile reads and parses a .meta sidecar file for the given tool name. // LoadMetaFile reads and parses a .meta sidecar file for the given tool name.
@ -86,9 +84,6 @@ func (m *MetaFile) Resolve() *MetaFile {
if v.Scope != "" { if v.Scope != "" {
resolved.Scope = v.Scope resolved.Scope = v.Scope
} }
if v.ResetsCounter {
resolved.ResetsCounter = true
}
if v.Cmd != "" { if v.Cmd != "" {
resolved.Cmd = v.Cmd resolved.Cmd = v.Cmd
} }
@ -184,7 +179,6 @@ func ToolInfoFromMeta(name string, m *MetaFile) protocol.ToolInfo {
Tier: tier, Tier: tier,
Scope: resolved.Scope, Scope: resolved.Scope,
OutputFormat: resolved.OutputFormat, OutputFormat: resolved.OutputFormat,
ResetsCounter: resolved.ResetsCounter,
} }
} }

View File

@ -5,15 +5,14 @@ import "encoding/json"
// ToolInfo describes a tool provided by a server. // ToolInfo describes a tool provided by a server.
type ToolInfo struct { type ToolInfo struct {
Server string Server string
Name string Name string
Description string Description string
InputSchema json.RawMessage InputSchema json.RawMessage
Prompt string Prompt string
Tier string Tier string
Scope string Scope string
OutputFormat string OutputFormat string
ResetsCounter bool
} }
// ToolResult is the structured result from a tool execution. // ToolResult is the structured result from a tool execution.