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:
parent
474eb84e2d
commit
b7d46015d7
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
main.go
2
main.go
|
|
@ -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():
|
||||||
|
|
|
||||||
Reference in New Issue