agent: simplify loop — Run as function, fix stop condition

- Loop struct and New constructor removed; Run is now a package-level
  function taking (ctx, Config, State)
- Stop condition de-nested: natural stop (no tool calls → MarkComplete
  + break) and step-limit stop (step >= maxSteps-1 → break) are now
  separate, sequential checks instead of a redundant outer/inner pair

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-03 20:13:14 +02:00
parent 474eb84e2d
commit b7d46015d7
2 changed files with 21 additions and 29 deletions

View File

@ -29,28 +29,20 @@ type Config struct {
SystemPrompt string SystemPrompt string
} }
type Loop struct { func Run(ctx context.Context, cfg Config, state State) error {
cfg Config maxSteps := cfg.MaxSteps
}
func New(cfg Config) *Loop {
return &Loop{cfg: cfg}
}
func (l *Loop) Run(ctx context.Context, state State) error {
maxSteps := l.cfg.MaxSteps
if maxSteps <= 0 { if maxSteps <= 0 {
maxSteps = 1 maxSteps = 1
} }
for step := range maxSteps { for step := range maxSteps {
history := state.History() history := state.History()
if l.cfg.SystemPrompt != "" { if cfg.SystemPrompt != "" {
history = append([]backend.Message{{Role: "system", Content: l.cfg.SystemPrompt}}, history...) history = append([]backend.Message{{Role: "system", Content: cfg.SystemPrompt}}, history...)
} }
// Stream the assistant's response. // Stream the assistant's response.
ch, err := l.cfg.Backend.ChatStream(ctx, l.cfg.Model, history, l.cfg.Tools) ch, err := cfg.Backend.ChatStream(ctx, cfg.Model, history, cfg.Tools)
if err != nil { if err != nil {
return fmt.Errorf("step %d: %w", step, err) return fmt.Errorf("step %d: %w", step, err)
} }
@ -63,7 +55,7 @@ func (l *Loop) Run(ctx context.Context, state State) error {
for ev := range ch { for ev := range ch {
if ev.Content != "" { if ev.Content != "" {
content.WriteString(ev.Content) content.WriteString(ev.Content)
l.emit(OutputMsg{Role: "assistant", Content: ev.Content}) emit(cfg, OutputMsg{Role: "assistant", Content: ev.Content})
} }
toolCalls = append(toolCalls, ev.ToolCalls...) toolCalls = append(toolCalls, ev.ToolCalls...)
if ev.Done { if ev.Done {
@ -82,12 +74,12 @@ func (l *Loop) Run(ctx context.Context, state State) error {
var results []ToolResult var results []ToolResult
for _, tc := range toolCalls { for _, tc := range toolCalls {
l.emit(OutputMsg{Role: "call", Name: tc.Name, Content: string(tc.Arguments)}) emit(cfg, OutputMsg{Role: "call", Name: tc.Name, Content: string(tc.Arguments)})
var result string var result string
var isErr bool var isErr bool
if l.cfg.Exec != nil { if cfg.Exec != nil {
out, err := l.cfg.Exec(tc.Name, tc.Arguments) out, err := cfg.Exec(tc.Name, tc.Arguments)
if err != nil { if err != nil {
result = fmt.Sprintf("error: %v", err) result = fmt.Sprintf("error: %v", err)
isErr = true isErr = true
@ -105,34 +97,34 @@ func (l *Loop) Run(ctx context.Context, state State) error {
Content: result, Content: result,
IsError: isErr, IsError: isErr,
}) })
l.emit(OutputMsg{Role: "tool", Name: tc.Name, Content: result}) emit(cfg, OutputMsg{Role: "tool", Name: tc.Name, Content: result})
} }
// Emit usage when we have real token counts. // Emit usage when we have real token counts.
if usage.InputTokens > 0 || usage.OutputTokens > 0 { if usage.InputTokens > 0 || usage.OutputTokens > 0 {
l.emit(OutputMsg{Role: "usage", Usage: usage}) emit(cfg, OutputMsg{Role: "usage", Usage: usage})
} }
if err := state.Update(msg, results); err != nil { if err := state.Update(msg, results); err != nil {
return fmt.Errorf("step %d update: %w", step, err) return fmt.Errorf("step %d update: %w", step, err)
} }
// Stop when the model has nothing more to call. if len(toolCalls) == 0 {
if len(toolCalls) == 0 || step >= maxSteps-1 { if err := state.MarkComplete(); err != nil {
if len(toolCalls) == 0 { return fmt.Errorf("mark complete: %w", err)
if err := state.MarkComplete(); err != nil {
return fmt.Errorf("mark complete: %w", err)
}
} }
break break
} }
if step >= maxSteps-1 {
break
}
} }
return nil return nil
} }
func (l *Loop) emit(msg OutputMsg) { func emit(cfg Config, msg OutputMsg) {
if l.cfg.Output != nil { if cfg.Output != nil {
l.cfg.Output(msg) cfg.Output(msg)
} }
} }

View File

@ -570,7 +570,7 @@ func (m model) startAgent(session *agent.Session) (chan tea.Msg, context.CancelF
} }
} }
if err := agent.New(loopcfg).Run(ctx, session); err != nil { if err := agent.Run(ctx, loopcfg, session); err != nil {
select { select {
case ch <- agentMsg{role: "error", content: err.Error()}: case ch <- agentMsg{role: "error", content: err.Error()}:
case <-ctx.Done(): case <-ctx.Done():