Loop-break signals

MaxIters and token budget are hard stops. Clean stops matter too. The model can emit finish_reason "stop" or simply return content. The user can hit Ctrl-C. Both paths need to leave the process in a good state.

agent/agent.go
go
// Accept a context so the REPL can cancel on Ctrl-C.
func (a *Agent) RunCtx(ctx context.Context, userMsg string) (string, error) {
	for iter := 0; iter < a.MaxIters; iter++ {
		select {
		case <-ctx.Done():
			return "", ctx.Err()
		default:
		}
		// ... existing loop body
	}
	return "", fmt.Errorf("agent: exceeded max iterations (%d)", a.MaxIters)
}

A tiny context check at the top of each iteration. The REPL wires its signal handler to cancel the context, and a stuck tool call never blocks forever.

All the ways the loop ends

Clean stops, hard caps, and user cancellation.

Validation checklist: Loop safety checklist

Loading practice…

Checkpoint: Loop safety checkpoint

Loading practice…